From 30712c4886a6da7e0628fadbaa855d8c95a1536f Mon Sep 17 00:00:00 2001
From: Ember <me@ember-is.gay>
Date: Thu, 18 Jan 2024 00:41:52 +1100
Subject: [PATCH] add blobfox theme

---
 .../flavours/blobfox/actions/account_notes.ts |   18 +
 .../flavours/blobfox/actions/accounts.js      |  774 +++++++
 .../blobfox/actions/accounts_typed.ts         |   97 +
 .../flavours/blobfox/actions/alerts.js        |   59 +
 .../flavours/blobfox/actions/announcements.js |  181 ++
 .../flavours/blobfox/actions/app.ts           |    9 +
 .../flavours/blobfox/actions/blocks.js        |  100 +
 .../flavours/blobfox/actions/bookmarks.js     |   91 +
 .../flavours/blobfox/actions/boosts.js        |   32 +
 .../flavours/blobfox/actions/bundles.js       |   25 +
 .../flavours/blobfox/actions/columns.js       |   54 +
 .../flavours/blobfox/actions/compose.js       |  840 ++++++++
 .../flavours/blobfox/actions/conversations.js |  113 +
 .../flavours/blobfox/actions/custom_emojis.js |   40 +
 .../flavours/blobfox/actions/directory.js     |   62 +
 .../flavours/blobfox/actions/domain_blocks.js |  152 ++
 .../blobfox/actions/domain_blocks_typed.ts    |   13 +
 .../flavours/blobfox/actions/dropdown_menu.ts |   11 +
 .../flavours/blobfox/actions/emojis.js        |   14 +
 .../flavours/blobfox/actions/favourites.js    |   94 +
 .../flavours/blobfox/actions/featured_tags.js |   34 +
 .../flavours/blobfox/actions/filters.js       |   97 +
 .../flavours/blobfox/actions/height_cache.js  |   17 +
 .../flavours/blobfox/actions/history.js       |   38 +
 .../blobfox/actions/importer/index.js         |   93 +
 .../blobfox/actions/importer/normalizer.js    |  135 ++
 .../flavours/blobfox/actions/interactions.js  |  600 ++++++
 .../flavours/blobfox/actions/languages.js     |   12 +
 .../flavours/blobfox/actions/lists.js         |  373 ++++
 .../blobfox/actions/local_settings.js         |   81 +
 .../flavours/blobfox/actions/markers.js       |  152 ++
 .../flavours/blobfox/actions/modal.ts         |   19 +
 .../flavours/blobfox/actions/mutes.js         |  117 +
 .../flavours/blobfox/actions/notifications.js |  404 ++++
 .../blobfox/actions/notifications_typed.ts    |   23 +
 .../flavours/blobfox/actions/onboarding.js    |    8 +
 .../blobfox/actions/picture_in_picture.js     |   46 +
 .../flavours/blobfox/actions/pin_statuses.js  |   42 +
 .../flavours/blobfox/actions/polls.js         |   61 +
 .../actions/push_notifications/index.js       |   17 +
 .../actions/push_notifications/registerer.js  |  134 ++
 .../actions/push_notifications/setter.js      |   34 +
 .../flavours/blobfox/actions/reports.js       |   42 +
 .../flavours/blobfox/actions/search.js        |  201 ++
 .../flavours/blobfox/actions/server.js        |  131 ++
 .../flavours/blobfox/actions/settings.js      |   36 +
 .../flavours/blobfox/actions/statuses.js      |  351 +++
 .../flavours/blobfox/actions/store.js         |   43 +
 .../flavours/blobfox/actions/streaming.js     |  184 ++
 .../flavours/blobfox/actions/suggestions.js   |   65 +
 .../flavours/blobfox/actions/tags.js          |  172 ++
 .../flavours/blobfox/actions/timelines.js     |  235 ++
 .../flavours/blobfox/actions/trends.js        |  140 ++
 app/javascript/flavours/blobfox/api.ts        |   63 +
 .../flavours/blobfox/api_types/accounts.ts    |   45 +
 .../blobfox/api_types/custom_emoji.ts         |    8 +
 .../blobfox/api_types/relationships.ts        |   18 +
 app/javascript/flavours/blobfox/blurhash.ts   |  111 +
 app/javascript/flavours/blobfox/compare_id.ts |   11 +
 .../components/__tests__/hashtag_bar.tsx      |  214 ++
 .../flavours/blobfox/components/account.jsx   |  182 ++
 .../blobfox/components/admin/Counter.jsx      |  121 ++
 .../blobfox/components/admin/Dimension.jsx    |   95 +
 .../blobfox/components/admin/ImpactReport.jsx |   91 +
 .../components/admin/ReportReasonSelector.jsx |  165 ++
 .../blobfox/components/admin/Retention.jsx    |  155 ++
 .../blobfox/components/admin/Trends.jsx       |   76 +
 .../blobfox/components/animated_number.tsx    |   81 +
 .../blobfox/components/attachment_list.jsx    |   51 +
 .../blobfox/components/autosuggest_emoji.jsx  |   43 +
 .../components/autosuggest_hashtag.tsx        |   42 +
 .../blobfox/components/autosuggest_input.jsx  |  230 ++
 .../components/autosuggest_textarea.jsx       |  240 +++
 .../flavours/blobfox/components/avatar.tsx    |   49 +
 .../blobfox/components/avatar_composite.jsx   |  106 +
 .../blobfox/components/avatar_overlay.tsx     |   57 +
 .../flavours/blobfox/components/blurhash.tsx  |   48 +
 .../flavours/blobfox/components/button.tsx    |   58 +
 .../flavours/blobfox/components/check.tsx     |   13 +
 .../blobfox/components/circular_progress.tsx  |   27 +
 .../flavours/blobfox/components/column.jsx    |   73 +
 .../blobfox/components/column_back_button.jsx |   63 +
 .../components/column_back_button_slim.jsx    |   40 +
 .../blobfox/components/column_header.jsx      |  219 ++
 .../flavours/blobfox/components/counters.tsx  |   45 +
 .../blobfox/components/dismissable_banner.tsx |   66 +
 .../blobfox/components/display_name.tsx       |  127 ++
 .../flavours/blobfox/components/domain.tsx    |   44 +
 .../blobfox/components/dropdown_menu.jsx      |  338 +++
 .../containers/dropdown_menu_container.js     |   32 +
 .../components/edited_timestamp/index.jsx     |   77 +
 .../blobfox/components/empty_account.tsx      |   33 +
 .../blobfox/components/error_boundary.jsx     |  111 +
 .../flavours/blobfox/components/gifv.tsx      |   70 +
 .../flavours/blobfox/components/hashtag.jsx   |  123 ++
 .../blobfox/components/hashtag_bar.tsx        |  234 ++
 .../flavours/blobfox/components/icon.tsx      |   20 +
 .../blobfox/components/icon_button.tsx        |  180 ++
 .../blobfox/components/icon_with_badge.tsx    |   24 +
 .../blobfox/components/inline_account.jsx     |   37 +
 .../intersection_observer_article.jsx         |  131 ++
 .../flavours/blobfox/components/link.jsx      |   97 +
 .../flavours/blobfox/components/load_gap.tsx  |   34 +
 .../flavours/blobfox/components/load_more.tsx |   24 +
 .../blobfox/components/load_pending.tsx       |   18 +
 .../blobfox/components/loading_indicator.tsx  |    7 +
 .../flavours/blobfox/components/logo.jsx      |   14 +
 .../blobfox/components/media_attachments.jsx  |  128 ++
 .../blobfox/components/media_gallery.jsx      |  395 ++++
 .../blobfox/components/modal_root.jsx         |  165 ++
 .../blobfox/components/navigation_portal.tsx  |   25 +
 .../components/not_signed_in_indicator.tsx    |   12 +
 .../components/notification_purge_buttons.jsx |   64 +
 .../flavours/blobfox/components/permalink.jsx |   50 +
 .../picture_in_picture_placeholder.jsx        |   33 +
 .../flavours/blobfox/components/poll.jsx      |  249 +++
 .../blobfox/components/radio_button.tsx       |   33 +
 .../components/regeneration_indicator.jsx     |   18 +
 .../blobfox/components/relative_timestamp.tsx |  282 +++
 .../flavours/blobfox/components/router.tsx    |   80 +
 .../blobfox/components/scrollable_list.jsx    |  405 ++++
 .../blobfox/components/server_banner.jsx      |   97 +
 .../blobfox/components/server_hero_image.tsx  |   35 +
 .../blobfox/components/setting_text.jsx       |   35 +
 .../blobfox/components/short_number.tsx       |   90 +
 .../flavours/blobfox/components/skeleton.tsx  |   10 +
 .../flavours/blobfox/components/status.jsx    |  879 ++++++++
 .../blobfox/components/status_action_bar.jsx  |  371 ++++
 .../blobfox/components/status_content.jsx     |  491 +++++
 .../blobfox/components/status_header.jsx      |   71 +
 .../blobfox/components/status_icons.jsx       |  147 ++
 .../blobfox/components/status_list.jsx        |  137 ++
 .../blobfox/components/status_prepend.jsx     |  158 ++
 .../blobfox/components/status_reactions.jsx   |  175 ++
 .../components/status_visibility_icon.jsx     |   54 +
 .../blobfox/components/timeline_hint.tsx      |   25 +
 .../blobfox/components/verified_badge.tsx     |   27 +
 .../blobfox/containers/account_container.jsx  |   76 +
 .../blobfox/containers/admin_component.jsx    |   22 +
 .../blobfox/containers/compose_container.jsx  |   31 +
 .../blobfox/containers/domain_container.jsx   |   36 +
 .../containers/dropdown_menu_container.js     |   37 +
 ...intersection_observer_article_container.js |   18 +
 .../flavours/blobfox/containers/mastodon.jsx  |   97 +
 .../blobfox/containers/media_container.jsx    |  127 ++
 .../notification_purge_buttons_container.js   |   53 +
 .../blobfox/containers/poll_container.js      |   26 +
 .../blobfox/containers/scroll_container.js    |   18 +
 .../blobfox/containers/status_container.js    |  313 +++
 .../flavours/blobfox/features/about/index.jsx |  225 ++
 .../account/components/account_note.jsx       |  174 ++
 .../account/components/action_bar.jsx         |   85 +
 .../account/components/featured_tags.jsx      |   52 +
 .../components/follow_request_note.jsx        |   38 +
 .../features/account/components/header.jsx    |  404 ++++
 .../components/profile_column_header.jsx      |   36 +
 .../containers/account_note_container.js      |   19 +
 .../containers/featured_tags_container.js     |   17 +
 .../follow_request_note_container.js          |   17 +
 .../blobfox/features/account/navigation.jsx   |   55 +
 .../account_gallery/components/media_item.jsx |  155 ++
 .../features/account_gallery/index.jsx        |  239 +++
 .../account_timeline/components/header.jsx    |  165 ++
 .../components/limited_account_hint.tsx       |   35 +
 .../components/memorial_note.jsx              |   11 +
 .../components/moved_note.jsx                 |   52 +
 .../containers/header_container.jsx           |  186 ++
 .../features/account_timeline/index.jsx       |  210 ++
 .../flavours/blobfox/features/audio/index.jsx |  600 ++++++
 .../blobfox/features/audio/visualizer.js      |  136 ++
 .../blobfox/features/blocks/index.jsx         |   82 +
 .../features/bookmarked_statuses/index.jsx    |  113 +
 .../closed_registrations_modal/index.jsx      |   77 +
 .../components/column_settings.jsx            |   45 +
 .../containers/column_settings_container.js   |   29 +
 .../features/community_timeline/index.jsx     |  166 ++
 .../compose/components/action_bar.jsx         |   73 +
 .../components/autosuggest_account.jsx        |   24 +
 .../compose/components/character_counter.jsx  |   26 +
 .../compose/components/compose_form.jsx       |  356 ++++
 .../features/compose/components/dropdown.jsx  |  243 +++
 .../compose/components/dropdown_menu.jsx      |  200 ++
 .../components/emoji_picker_dropdown.jsx      |  422 ++++
 .../features/compose/components/header.jsx    |  134 ++
 .../compose/components/language_dropdown.jsx  |  334 +++
 .../compose/components/navigation_bar.jsx     |   54 +
 .../features/compose/components/options.jsx   |  311 +++
 .../features/compose/components/poll_form.jsx |  181 ++
 .../compose/components/privacy_dropdown.jsx   |   90 +
 .../features/compose/components/publisher.jsx |   92 +
 .../compose/components/reply_indicator.jsx    |   70 +
 .../features/compose/components/search.jsx    |  400 ++++
 .../compose/components/search_results.jsx     |   89 +
 .../compose/components/text_icon_button.jsx   |   38 +
 .../compose/components/textarea_icons.jsx     |   61 +
 .../features/compose/components/upload.jsx    |   66 +
 .../compose/components/upload_form.jsx        |   32 +
 .../compose/components/upload_progress.jsx    |   56 +
 .../features/compose/components/warning.jsx   |   28 +
 .../autosuggest_account_container.js          |   16 +
 .../containers/compose_form_container.js      |  150 ++
 .../compose/containers/dropdown_container.js  |   14 +
 .../emoji_picker_dropdown_container.js        |   85 +
 .../compose/containers/header_container.js    |   42 +
 .../containers/language_dropdown_container.js |   37 +
 .../containers/navigation_container.js        |   36 +
 .../compose/containers/options_container.js   |   56 +
 .../compose/containers/poll_form_container.js |   53 +
 .../containers/privacy_dropdown_container.js  |   30 +
 .../containers/reply_indicator_container.js   |   33 +
 .../compose/containers/search_container.js    |   53 +
 .../containers/search_results_container.js    |   20 +
 .../containers/sensitive_button_container.jsx |   77 +
 .../compose/containers/upload_container.js    |   26 +
 .../containers/upload_form_container.js       |    9 +
 .../containers/upload_progress_container.js   |   11 +
 .../compose/containers/warning_container.jsx  |   47 +
 .../blobfox/features/compose/index.jsx        |  122 ++
 .../blobfox/features/compose/util/counter.js  |    9 +
 .../features/compose/util/url_regex.js        |   30 +
 .../components/column_settings.jsx            |   47 +
 .../components/conversation.jsx               |  233 ++
 .../components/conversations_list.jsx         |   77 +
 .../containers/column_settings_container.js   |   19 +
 .../containers/conversation_container.js      |   81 +
 .../conversations_list_container.js           |   16 +
 .../features/direct_timeline/index.jsx        |  165 ++
 .../directory/components/account_card.jsx     |  255 +++
 .../blobfox/features/directory/index.jsx      |  179 ++
 .../blobfox/features/domain_blocks/index.jsx  |   87 +
 .../flavours/blobfox/features/emoji/emoji.js  |  166 ++
 .../features/emoji/emoji_compressed.d.ts      |   57 +
 .../features/emoji/emoji_compressed.js        |  135 ++
 .../blobfox/features/emoji/emoji_map.json     |    1 +
 .../features/emoji/emoji_mart_data_light.ts   |   43 +
 .../features/emoji/emoji_mart_search_light.js |  185 ++
 .../blobfox/features/emoji/emoji_picker.js    |    7 +
 .../emoji/emoji_unicode_mapping_light.ts      |   60 +
 .../blobfox/features/emoji/emoji_utils.js     |  258 +++
 .../features/emoji/unicode_to_filename.js     |   29 +
 .../features/emoji/unicode_to_unified_name.js |   24 +
 .../explore/components/search_section.jsx     |   20 +
 .../features/explore/components/story.jsx     |   61 +
 .../blobfox/features/explore/index.jsx        |  114 +
 .../blobfox/features/explore/links.jsx        |   90 +
 .../blobfox/features/explore/results.jsx      |  229 ++
 .../blobfox/features/explore/statuses.jsx     |   78 +
 .../blobfox/features/explore/suggestions.jsx  |   70 +
 .../blobfox/features/explore/tags.jsx         |   76 +
 .../features/favourited_statuses/index.jsx    |  113 +
 .../blobfox/features/favourites/index.jsx     |  114 +
 .../features/filters/added_to_filter.jsx      |  106 +
 .../features/filters/select_filter.jsx        |  196 ++
 .../blobfox/features/firehose/index.jsx       |  229 ++
 .../components/account_authorize.jsx          |   52 +
 .../containers/account_authorize_container.js |   27 +
 .../features/follow_requests/index.jsx        |   96 +
 .../blobfox/features/followed_tags/index.jsx  |   93 +
 .../blobfox/features/followers/index.jsx      |  177 ++
 .../blobfox/features/following/index.jsx      |  177 ++
 .../components/announcements.jsx              |  455 ++++
 .../getting_started/components/trends.jsx     |   54 +
 .../containers/announcements_container.js     |   22 +
 .../containers/trends_container.js            |   15 +
 .../features/getting_started/index.jsx        |  208 ++
 .../features/getting_started_misc/index.jsx   |   68 +
 .../components/column_settings.jsx            |  138 ++
 .../components/hashtag_header.jsx             |   79 +
 .../containers/column_settings_container.js   |   33 +
 .../features/hashtag_timeline/index.jsx       |  226 ++
 .../components/column_settings.tsx            |  109 +
 .../components/critical_update_banner.tsx     |   26 +
 .../components/explore_prompt.tsx             |   46 +
 .../blobfox/features/home_timeline/index.jsx  |  239 +++
 .../features/interaction_modal/index.jsx      |  417 ++++
 .../features/keyboard_shortcuts/index.jsx     |  152 ++
 .../list_adder/components/account.jsx         |   43 +
 .../features/list_adder/components/list.jsx   |   72 +
 .../blobfox/features/list_adder/index.jsx     |   76 +
 .../list_editor/components/account.jsx        |   79 +
 .../list_editor/components/edit_list_form.jsx |   73 +
 .../list_editor/components/search.jsx         |   81 +
 .../blobfox/features/list_editor/index.jsx    |   83 +
 .../blobfox/features/list_timeline/index.jsx  |  244 +++
 .../lists/components/new_list_form.jsx        |   81 +
 .../flavours/blobfox/features/lists/index.jsx |   93 +
 .../blobfox/features/local_settings/index.jsx |   70 +
 .../local_settings/navigation/index.jsx       |   95 +
 .../local_settings/navigation/item/index.jsx  |   73 +
 .../page/deprecated_item/index.jsx            |   83 +
 .../features/local_settings/page/index.jsx    |  505 +++++
 .../local_settings/page/item/index.jsx        |  118 ++
 .../flavours/blobfox/features/mutes/index.jsx |   88 +
 .../notifications/components/admin_report.jsx |  115 +
 .../notifications/components/admin_signup.jsx |  108 +
 .../components/clear_column_button.jsx        |   20 +
 .../components/column_settings.jsx            |  218 ++
 .../notifications/components/filter_bar.jsx   |  121 ++
 .../notifications/components/follow.jsx       |  108 +
 .../components/follow_request.jsx             |  141 ++
 .../components/grant_permission_button.jsx    |   20 +
 .../notifications/components/notification.jsx |  258 +++
 .../notifications_permission_banner.jsx       |   51 +
 .../notifications/components/overlay.jsx      |   61 +
 .../components/pill_bar_button.jsx            |   43 +
 .../notifications/components/report.jsx       |   67 +
 .../components/setting_toggle.jsx             |   38 +
 .../containers/admin_report_container.js      |   15 +
 .../containers/column_settings_container.js   |   78 +
 .../containers/filter_bar_container.js        |   17 +
 .../containers/follow_request_container.js    |   17 +
 .../containers/notification_container.js      |   25 +
 .../containers/overlay_container.js           |   19 +
 .../blobfox/features/notifications/index.jsx  |  368 ++++
 .../components/arrow_small_right.jsx          |    7 +
 .../components/progress_indicator.jsx         |   28 +
 .../features/onboarding/components/step.jsx   |   50 +
 .../blobfox/features/onboarding/follows.jsx   |   80 +
 .../blobfox/features/onboarding/index.jsx     |  149 ++
 .../blobfox/features/onboarding/share.jsx     |  200 ++
 .../picture_in_picture/components/footer.jsx  |  231 ++
 .../picture_in_picture/components/header.jsx  |   50 +
 .../features/picture_in_picture/index.jsx     |   93 +
 .../containers/account_container.js           |   25 +
 .../containers/search_container.js            |   24 +
 .../features/pinned_accounts_editor/index.jsx |   82 +
 .../features/pinned_statuses/index.jsx        |   70 +
 .../blobfox/features/privacy_policy/index.jsx |   65 +
 .../components/column_settings.jsx            |   46 +
 .../containers/column_settings_container.js   |   29 +
 .../features/public_timeline/index.jsx        |  171 ++
 .../blobfox/features/reblogs/index.jsx        |  115 +
 .../blobfox/features/report/category.jsx      |  108 +
 .../blobfox/features/report/comment.jsx       |  121 ++
 .../features/report/components/option.jsx     |   62 +
 .../report/components/status_check_box.jsx    |   67 +
 .../containers/status_check_box_container.js  |   17 +
 .../blobfox/features/report/rules.jsx         |   69 +
 .../blobfox/features/report/statuses.jsx      |   65 +
 .../blobfox/features/report/thanks.jsx        |   88 +
 .../features/standalone/compose/index.jsx     |   21 +
 .../features/status/components/action_bar.jsx |  267 +++
 .../features/status/components/card.jsx       |  256 +++
 .../status/components/detailed_status.jsx     |  362 ++++
 .../containers/detailed_status_container.js   |  176 ++
 .../blobfox/features/status/index.jsx         |  807 +++++++
 .../subscribed_languages_modal/index.jsx      |  127 ++
 .../features/ui/components/actions_modal.jsx  |   94 +
 .../features/ui/components/audio_modal.jsx    |   61 +
 .../features/ui/components/block_modal.jsx    |  100 +
 .../features/ui/components/boost_modal.jsx    |  131 ++
 .../blobfox/features/ui/components/bundle.jsx |  106 +
 .../ui/components/bundle_column_error.jsx     |  166 ++
 .../ui/components/bundle_modal_error.jsx      |   54 +
 .../blobfox/features/ui/components/column.jsx |   78 +
 .../features/ui/components/column_header.jsx  |   40 +
 .../features/ui/components/column_link.jsx    |   57 +
 .../features/ui/components/column_loading.jsx |   32 +
 .../ui/components/column_subheading.jsx       |   15 +
 .../features/ui/components/columns_area.jsx   |  180 ++
 .../ui/components/compare_history_modal.jsx   |  110 +
 .../features/ui/components/compose_panel.jsx  |   62 +
 .../ui/components/confirmation_modal.jsx      |   83 +
 .../components/deprecated_settings_modal.jsx  |   82 +
 .../ui/components/disabled_account_banner.jsx |   99 +
 .../features/ui/components/doodle_modal.jsx   |  619 ++++++
 .../features/ui/components/drawer_loading.jsx |    9 +
 .../features/ui/components/embed_modal.jsx    |  100 +
 .../ui/components/favourite_modal.jsx         |   94 +
 .../features/ui/components/filter_modal.jsx   |  136 ++
 .../ui/components/focal_point_modal.jsx       |  426 ++++
 .../follow_requests_column_link.jsx           |   54 +
 .../blobfox/features/ui/components/header.jsx |  124 ++
 .../features/ui/components/image_loader.jsx   |  174 ++
 .../features/ui/components/image_modal.jsx    |   64 +
 .../features/ui/components/link_footer.jsx    |  111 +
 .../features/ui/components/list_panel.jsx     |   56 +
 .../features/ui/components/media_modal.jsx    |  261 +++
 .../features/ui/components/modal_loading.jsx  |   18 +
 .../features/ui/components/modal_root.jsx     |  142 ++
 .../features/ui/components/mute_modal.jsx     |  139 ++
 .../ui/components/navigation_panel.jsx        |  127 ++
 .../components/notifications_counter_icon.js  |   10 +
 .../features/ui/components/report_modal.jsx   |  227 ++
 .../features/ui/components/sign_in_banner.jsx |   54 +
 .../features/ui/components/upload_area.jsx    |   55 +
 .../features/ui/components/video_modal.jsx    |   74 +
 .../features/ui/components/zoomable_image.jsx |  456 ++++
 .../ui/containers/bundle_container.js         |   18 +
 .../ui/containers/columns_area_container.js   |   22 +
 .../ui/containers/loading_bar_container.js    |    9 +
 .../features/ui/containers/modal_container.js |   36 +
 .../ui/containers/notifications_container.js  |   33 +
 .../ui/containers/status_list_container.js    |   89 +
 .../flavours/blobfox/features/ui/index.jsx    |  675 ++++++
 .../features/ui/util/async-components.js      |  203 ++
 .../blobfox/features/ui/util/fullscreen.js    |   46 +
 .../features/ui/util/get_rect_from_entry.js   |   21 +
 .../ui/util/intersection_observer_wrapper.js  |   57 +
 .../features/ui/util/optional_motion.js       |    7 +
 .../features/ui/util/react_router_helpers.jsx |  105 +
 .../features/ui/util/reduced_motion.jsx       |   45 +
 .../features/ui/util/schedule_idle_task.js    |   29 +
 .../flavours/blobfox/features/video/index.jsx |  665 ++++++
 .../flavours/blobfox/hooks/useHovering.ts     |   17 +
 .../images/elephant_ui_disappointed.svg       |    1 +
 .../blobfox/images/elephant_ui_working.svg    |    1 +
 .../blobfox/images/glitch-preview.jpg         |  Bin 0 -> 197277 bytes
 .../blobfox/images/logo_warn_glitch.svg       |   49 +
 .../flavours/blobfox/images/mbstobon-ui-0.png |  Bin 0 -> 39646 bytes
 .../flavours/blobfox/images/mbstobon-ui-1.png |  Bin 0 -> 43609 bytes
 .../flavours/blobfox/images/mbstobon-ui-2.png |  Bin 0 -> 40376 bytes
 .../flavours/blobfox/images/mbstobon-ui-3.png |  Bin 0 -> 32449 bytes
 .../blobfox/images/wave-drawer-glitched.png   |  Bin 0 -> 3544 bytes
 .../flavours/blobfox/images/wave-drawer.png   |  Bin 0 -> 3269 bytes
 .../flavours/blobfox/initial_state.js         |  144 ++
 app/javascript/flavours/blobfox/is_mobile.ts  |   34 +
 .../blobfox/load_keyboard_extensions.js       |   16 +
 app/javascript/flavours/blobfox/main.jsx      |   47 +
 .../flavours/blobfox/models/account.ts        |  149 ++
 .../flavours/blobfox/models/custom_emoji.ts   |   15 +
 .../flavours/blobfox/models/relationship.ts   |   29 +
 app/javascript/flavours/blobfox/names.yml     |   40 +
 .../flavours/blobfox/packs/admin.jsx          |   25 +
 .../flavours/blobfox/packs/common.js          |    8 +
 .../flavours/blobfox/packs/error.js           |   14 +
 app/javascript/flavours/blobfox/packs/home.js |   11 +
 .../flavours/blobfox/packs/public.jsx         |  242 +++
 .../flavours/blobfox/packs/settings.js        |   42 +
 .../flavours/blobfox/packs/share.jsx          |   27 +
 .../flavours/blobfox/packs/sign_up.js         |   42 +
 .../flavours/blobfox/performance.js           |   30 +
 .../flavours/blobfox/permissions.ts           |    4 +
 .../blobfox/polyfills/extra_polyfills.ts      |    1 +
 .../flavours/blobfox/polyfills/index.ts       |   21 +
 .../flavours/blobfox/polyfills/intl.ts        |  106 +
 app/javascript/flavours/blobfox/ready.js      |   32 +
 .../flavours/blobfox/reducers/accounts.ts     |   84 +
 .../flavours/blobfox/reducers/accounts_map.js |   24 +
 .../flavours/blobfox/reducers/alerts.js       |   30 +
 .../blobfox/reducers/announcements.js         |  103 +
 .../flavours/blobfox/reducers/blocks.js       |   22 +
 .../flavours/blobfox/reducers/boosts.js       |   25 +
 .../flavours/blobfox/reducers/compose.js      |  660 ++++++
 .../flavours/blobfox/reducers/contexts.js     |  107 +
 .../blobfox/reducers/conversations.js         |  118 ++
 .../blobfox/reducers/custom_emojis.js         |   16 +
 .../flavours/blobfox/reducers/domain_lists.js |   26 +
 .../blobfox/reducers/dropdown_menu.ts         |   33 +
 .../flavours/blobfox/reducers/filters.js      |   45 +
 .../blobfox/reducers/followed_tags.js         |   43 +
 .../flavours/blobfox/reducers/height_cache.js |   24 +
 .../flavours/blobfox/reducers/history.js      |   29 +
 .../flavours/blobfox/reducers/index.ts        |  111 +
 .../flavours/blobfox/reducers/list_adder.js   |   48 +
 .../flavours/blobfox/reducers/list_editor.js  |   99 +
 .../flavours/blobfox/reducers/lists.js        |   38 +
 .../blobfox/reducers/local_settings.js        |   79 +
 .../flavours/blobfox/reducers/markers.js      |   26 +
 .../blobfox/reducers/media_attachments.js     |   16 +
 .../flavours/blobfox/reducers/meta.js         |   25 +
 .../flavours/blobfox/reducers/modal.ts        |   83 +
 .../flavours/blobfox/reducers/mutes.js        |   31 +
 .../blobfox/reducers/notifications.js         |  376 ++++
 .../blobfox/reducers/picture_in_picture.js    |   26 +
 .../reducers/pinned_accounts_editor.js        |   58 +
 .../flavours/blobfox/reducers/polls.js        |   45 +
 .../blobfox/reducers/push_notifications.js    |   54 +
 .../blobfox/reducers/relationships.ts         |  123 ++
 .../flavours/blobfox/reducers/search.js       |   82 +
 .../flavours/blobfox/reducers/server.js       |   63 +
 .../flavours/blobfox/reducers/settings.js     |  203 ++
 .../flavours/blobfox/reducers/status_lists.js |  151 ++
 .../flavours/blobfox/reducers/statuses.js     |  191 ++
 .../flavours/blobfox/reducers/suggestions.js  |   40 +
 .../flavours/blobfox/reducers/tags.js         |   26 +
 .../flavours/blobfox/reducers/timelines.js    |  233 ++
 .../flavours/blobfox/reducers/trends.js       |   47 +
 .../flavours/blobfox/reducers/user_lists.js   |  216 ++
 app/javascript/flavours/blobfox/scroll.ts     |   50 +
 .../flavours/blobfox/selectors/accounts.ts    |   47 +
 .../flavours/blobfox/selectors/index.js       |  118 ++
 app/javascript/flavours/blobfox/settings.js   |   50 +
 .../flavours/blobfox/store/index.ts           |    8 +
 .../blobfox/store/middlewares/errors.ts       |   21 +
 .../blobfox/store/middlewares/loading_bar.ts  |   69 +
 .../blobfox/store/middlewares/sounds.ts       |   64 +
 .../flavours/blobfox/store/store.ts           |   39 +
 .../flavours/blobfox/store/typed_functions.ts |   15 +
 app/javascript/flavours/blobfox/stream.js     |  275 +++
 .../flavours/blobfox/styles/_mixins.scss      |   97 +
 .../flavours/blobfox/styles/about.scss        |   56 +
 .../blobfox/styles/accessibility.scss         |   55 +
 .../flavours/blobfox/styles/accounts.scss     |  381 ++++
 .../flavours/blobfox/styles/admin.scss        | 1884 +++++++++++++++++
 .../flavours/blobfox/styles/basics.scss       |  294 +++
 .../flavours/blobfox/styles/branding.scss     |    3 +
 .../blobfox/styles/components/about.scss      |  295 +++
 .../blobfox/styles/components/accounts.scss   |  807 +++++++
 .../styles/components/announcements.scss      |  233 ++
 .../blobfox/styles/components/boost.scss      |   44 +
 .../blobfox/styles/components/columns.scss    | 1364 ++++++++++++
 .../styles/components/compose_form.scss       |  685 ++++++
 .../blobfox/styles/components/directory.scss  |   68 +
 .../blobfox/styles/components/domains.scss    |   23 +
 .../blobfox/styles/components/doodle.scss     |   86 +
 .../blobfox/styles/components/drawer.scss     |  298 +++
 .../blobfox/styles/components/emoji.scss      |  104 +
 .../styles/components/emoji_picker.scss       |  261 +++
 .../blobfox/styles/components/explore.scss    |  147 ++
 .../blobfox/styles/components/index.scss      |   25 +
 .../blobfox/styles/components/lists.scss      |   94 +
 .../styles/components/local_settings.scss     |  173 ++
 .../blobfox/styles/components/media.scss      |  795 +++++++
 .../blobfox/styles/components/misc.scss       | 1740 +++++++++++++++
 .../blobfox/styles/components/modal.scss      | 1502 +++++++++++++
 .../styles/components/privacy_policy.scss     |  209 ++
 .../components/regeneration_indicator.scss    |   43 +
 .../blobfox/styles/components/search.scss     |  337 +++
 .../blobfox/styles/components/sensitive.scss  |   26 +
 .../blobfox/styles/components/signed_out.scss |  110 +
 .../styles/components/single_column.scss      |  335 +++
 .../blobfox/styles/components/status.scss     | 1212 +++++++++++
 .../flavours/blobfox/styles/containers.scss   |  109 +
 .../flavours/blobfox/styles/contrast.scss     |    3 +
 .../blobfox/styles/contrast/diff.scss         |   79 +
 .../blobfox/styles/contrast/variables.scss    |   22 +
 .../flavours/blobfox/styles/dashboard.scss    |  123 ++
 .../flavours/blobfox/styles/forms.scss        | 1224 +++++++++++
 .../flavours/blobfox/styles/index.scss        |   24 +
 .../flavours/blobfox/styles/lists.scss        |   19 +
 .../blobfox/styles/mastodon-light.scss        |    3 +
 .../blobfox/styles/mastodon-light/diff.scss   |  743 +++++++
 .../styles/mastodon-light/variables.scss      |   57 +
 .../flavours/blobfox/styles/modal.scss        |   37 +
 .../flavours/blobfox/styles/polls.scss        |  314 +++
 .../flavours/blobfox/styles/reset.scss        |   95 +
 .../flavours/blobfox/styles/rich_text.scss    |   99 +
 .../flavours/blobfox/styles/rtl.scss          |  123 ++
 .../flavours/blobfox/styles/statuses.scss     |  232 ++
 .../flavours/blobfox/styles/tables.scss       |  376 ++++
 .../flavours/blobfox/styles/variables.scss    |  103 +
 .../flavours/blobfox/styles/widgets.scss      |  402 ++++
 app/javascript/flavours/blobfox/theme.yml     |   48 +
 app/javascript/flavours/blobfox/types/util.ts |    1 +
 .../flavours/blobfox/utils/backend_links.js   |   18 +
 .../flavours/blobfox/utils/base64.ts          |   10 +
 .../flavours/blobfox/utils/config.js          |   10 +
 .../flavours/blobfox/utils/content_warning.js |   31 +
 .../flavours/blobfox/utils/filters.ts         |   16 +
 .../flavours/blobfox/utils/hashtag.js         |    8 +
 .../flavours/blobfox/utils/hashtags.ts        |   29 +
 app/javascript/flavours/blobfox/utils/html.js |    6 +
 .../flavours/blobfox/utils/icons.jsx          |   13 +
 app/javascript/flavours/blobfox/utils/idna.js |   10 +
 .../flavours/blobfox/utils/js_helpers.js      |    5 +
 .../flavours/blobfox/utils/log_out.js         |   35 +
 .../flavours/blobfox/utils/notifications.js   |   30 +
 .../flavours/blobfox/utils/numbers.ts         |   71 +
 .../blobfox/utils/privacy_preference.js       |    5 +
 .../flavours/blobfox/utils/react_helpers.js   |   21 +
 .../flavours/blobfox/utils/react_router.jsx   |   61 +
 .../flavours/blobfox/utils/resize_image.js    |  191 ++
 .../flavours/blobfox/utils/scrollbar.js       |   34 +
 app/javascript/flavours/blobfox/uuid.ts       |    8 +
 .../skins/blobfox/contrast/common.scss        |    1 +
 .../skins/blobfox/contrast/names.yml          |   12 +
 .../skins/blobfox/mastodon-light/common.scss  |    1 +
 .../skins/blobfox/mastodon-light/names.yml    |   12 +
 .../blobfox/queens-pink-contrast/common.scss  |    3 +
 .../blobfox/queens-pink-contrast/diff.scss    |  381 ++++
 .../blobfox/queens-pink-contrast/names.yml    |    4 +
 .../queens-pink-contrast/screenshot.jpg       |  Bin 0 -> 173565 bytes
 .../queens-pink-contrast/variables.scss       |   81 +
 .../skins/blobfox/solarpunk/common.scss       |    3 +
 .../skins/blobfox/solarpunk/diff.scss         |  143 ++
 .../skins/blobfox/solarpunk/names.yml         |    4 +
 .../skins/blobfox/solarpunk/variables.scss    |   75 +
 578 files changed, 71749 insertions(+)
 create mode 100644 app/javascript/flavours/blobfox/actions/account_notes.ts
 create mode 100644 app/javascript/flavours/blobfox/actions/accounts.js
 create mode 100644 app/javascript/flavours/blobfox/actions/accounts_typed.ts
 create mode 100644 app/javascript/flavours/blobfox/actions/alerts.js
 create mode 100644 app/javascript/flavours/blobfox/actions/announcements.js
 create mode 100644 app/javascript/flavours/blobfox/actions/app.ts
 create mode 100644 app/javascript/flavours/blobfox/actions/blocks.js
 create mode 100644 app/javascript/flavours/blobfox/actions/bookmarks.js
 create mode 100644 app/javascript/flavours/blobfox/actions/boosts.js
 create mode 100644 app/javascript/flavours/blobfox/actions/bundles.js
 create mode 100644 app/javascript/flavours/blobfox/actions/columns.js
 create mode 100644 app/javascript/flavours/blobfox/actions/compose.js
 create mode 100644 app/javascript/flavours/blobfox/actions/conversations.js
 create mode 100644 app/javascript/flavours/blobfox/actions/custom_emojis.js
 create mode 100644 app/javascript/flavours/blobfox/actions/directory.js
 create mode 100644 app/javascript/flavours/blobfox/actions/domain_blocks.js
 create mode 100644 app/javascript/flavours/blobfox/actions/domain_blocks_typed.ts
 create mode 100644 app/javascript/flavours/blobfox/actions/dropdown_menu.ts
 create mode 100644 app/javascript/flavours/blobfox/actions/emojis.js
 create mode 100644 app/javascript/flavours/blobfox/actions/favourites.js
 create mode 100644 app/javascript/flavours/blobfox/actions/featured_tags.js
 create mode 100644 app/javascript/flavours/blobfox/actions/filters.js
 create mode 100644 app/javascript/flavours/blobfox/actions/height_cache.js
 create mode 100644 app/javascript/flavours/blobfox/actions/history.js
 create mode 100644 app/javascript/flavours/blobfox/actions/importer/index.js
 create mode 100644 app/javascript/flavours/blobfox/actions/importer/normalizer.js
 create mode 100644 app/javascript/flavours/blobfox/actions/interactions.js
 create mode 100644 app/javascript/flavours/blobfox/actions/languages.js
 create mode 100644 app/javascript/flavours/blobfox/actions/lists.js
 create mode 100644 app/javascript/flavours/blobfox/actions/local_settings.js
 create mode 100644 app/javascript/flavours/blobfox/actions/markers.js
 create mode 100644 app/javascript/flavours/blobfox/actions/modal.ts
 create mode 100644 app/javascript/flavours/blobfox/actions/mutes.js
 create mode 100644 app/javascript/flavours/blobfox/actions/notifications.js
 create mode 100644 app/javascript/flavours/blobfox/actions/notifications_typed.ts
 create mode 100644 app/javascript/flavours/blobfox/actions/onboarding.js
 create mode 100644 app/javascript/flavours/blobfox/actions/picture_in_picture.js
 create mode 100644 app/javascript/flavours/blobfox/actions/pin_statuses.js
 create mode 100644 app/javascript/flavours/blobfox/actions/polls.js
 create mode 100644 app/javascript/flavours/blobfox/actions/push_notifications/index.js
 create mode 100644 app/javascript/flavours/blobfox/actions/push_notifications/registerer.js
 create mode 100644 app/javascript/flavours/blobfox/actions/push_notifications/setter.js
 create mode 100644 app/javascript/flavours/blobfox/actions/reports.js
 create mode 100644 app/javascript/flavours/blobfox/actions/search.js
 create mode 100644 app/javascript/flavours/blobfox/actions/server.js
 create mode 100644 app/javascript/flavours/blobfox/actions/settings.js
 create mode 100644 app/javascript/flavours/blobfox/actions/statuses.js
 create mode 100644 app/javascript/flavours/blobfox/actions/store.js
 create mode 100644 app/javascript/flavours/blobfox/actions/streaming.js
 create mode 100644 app/javascript/flavours/blobfox/actions/suggestions.js
 create mode 100644 app/javascript/flavours/blobfox/actions/tags.js
 create mode 100644 app/javascript/flavours/blobfox/actions/timelines.js
 create mode 100644 app/javascript/flavours/blobfox/actions/trends.js
 create mode 100644 app/javascript/flavours/blobfox/api.ts
 create mode 100644 app/javascript/flavours/blobfox/api_types/accounts.ts
 create mode 100644 app/javascript/flavours/blobfox/api_types/custom_emoji.ts
 create mode 100644 app/javascript/flavours/blobfox/api_types/relationships.ts
 create mode 100644 app/javascript/flavours/blobfox/blurhash.ts
 create mode 100644 app/javascript/flavours/blobfox/compare_id.ts
 create mode 100644 app/javascript/flavours/blobfox/components/__tests__/hashtag_bar.tsx
 create mode 100644 app/javascript/flavours/blobfox/components/account.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/admin/Counter.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/admin/Dimension.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/admin/ImpactReport.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/admin/ReportReasonSelector.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/admin/Retention.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/admin/Trends.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/animated_number.tsx
 create mode 100644 app/javascript/flavours/blobfox/components/attachment_list.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/autosuggest_emoji.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/autosuggest_hashtag.tsx
 create mode 100644 app/javascript/flavours/blobfox/components/autosuggest_input.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/autosuggest_textarea.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/avatar.tsx
 create mode 100644 app/javascript/flavours/blobfox/components/avatar_composite.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/avatar_overlay.tsx
 create mode 100644 app/javascript/flavours/blobfox/components/blurhash.tsx
 create mode 100644 app/javascript/flavours/blobfox/components/button.tsx
 create mode 100644 app/javascript/flavours/blobfox/components/check.tsx
 create mode 100644 app/javascript/flavours/blobfox/components/circular_progress.tsx
 create mode 100644 app/javascript/flavours/blobfox/components/column.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/column_back_button.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/column_back_button_slim.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/column_header.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/counters.tsx
 create mode 100644 app/javascript/flavours/blobfox/components/dismissable_banner.tsx
 create mode 100644 app/javascript/flavours/blobfox/components/display_name.tsx
 create mode 100644 app/javascript/flavours/blobfox/components/domain.tsx
 create mode 100644 app/javascript/flavours/blobfox/components/dropdown_menu.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/edited_timestamp/containers/dropdown_menu_container.js
 create mode 100644 app/javascript/flavours/blobfox/components/edited_timestamp/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/empty_account.tsx
 create mode 100644 app/javascript/flavours/blobfox/components/error_boundary.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/gifv.tsx
 create mode 100644 app/javascript/flavours/blobfox/components/hashtag.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/hashtag_bar.tsx
 create mode 100644 app/javascript/flavours/blobfox/components/icon.tsx
 create mode 100644 app/javascript/flavours/blobfox/components/icon_button.tsx
 create mode 100644 app/javascript/flavours/blobfox/components/icon_with_badge.tsx
 create mode 100644 app/javascript/flavours/blobfox/components/inline_account.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/intersection_observer_article.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/link.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/load_gap.tsx
 create mode 100644 app/javascript/flavours/blobfox/components/load_more.tsx
 create mode 100644 app/javascript/flavours/blobfox/components/load_pending.tsx
 create mode 100644 app/javascript/flavours/blobfox/components/loading_indicator.tsx
 create mode 100644 app/javascript/flavours/blobfox/components/logo.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/media_attachments.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/media_gallery.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/modal_root.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/navigation_portal.tsx
 create mode 100644 app/javascript/flavours/blobfox/components/not_signed_in_indicator.tsx
 create mode 100644 app/javascript/flavours/blobfox/components/notification_purge_buttons.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/permalink.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/picture_in_picture_placeholder.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/poll.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/radio_button.tsx
 create mode 100644 app/javascript/flavours/blobfox/components/regeneration_indicator.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/relative_timestamp.tsx
 create mode 100644 app/javascript/flavours/blobfox/components/router.tsx
 create mode 100644 app/javascript/flavours/blobfox/components/scrollable_list.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/server_banner.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/server_hero_image.tsx
 create mode 100644 app/javascript/flavours/blobfox/components/setting_text.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/short_number.tsx
 create mode 100644 app/javascript/flavours/blobfox/components/skeleton.tsx
 create mode 100644 app/javascript/flavours/blobfox/components/status.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/status_action_bar.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/status_content.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/status_header.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/status_icons.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/status_list.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/status_prepend.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/status_reactions.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/status_visibility_icon.jsx
 create mode 100644 app/javascript/flavours/blobfox/components/timeline_hint.tsx
 create mode 100644 app/javascript/flavours/blobfox/components/verified_badge.tsx
 create mode 100644 app/javascript/flavours/blobfox/containers/account_container.jsx
 create mode 100644 app/javascript/flavours/blobfox/containers/admin_component.jsx
 create mode 100644 app/javascript/flavours/blobfox/containers/compose_container.jsx
 create mode 100644 app/javascript/flavours/blobfox/containers/domain_container.jsx
 create mode 100644 app/javascript/flavours/blobfox/containers/dropdown_menu_container.js
 create mode 100644 app/javascript/flavours/blobfox/containers/intersection_observer_article_container.js
 create mode 100644 app/javascript/flavours/blobfox/containers/mastodon.jsx
 create mode 100644 app/javascript/flavours/blobfox/containers/media_container.jsx
 create mode 100644 app/javascript/flavours/blobfox/containers/notification_purge_buttons_container.js
 create mode 100644 app/javascript/flavours/blobfox/containers/poll_container.js
 create mode 100644 app/javascript/flavours/blobfox/containers/scroll_container.js
 create mode 100644 app/javascript/flavours/blobfox/containers/status_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/about/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/account/components/account_note.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/account/components/action_bar.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/account/components/featured_tags.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/account/components/follow_request_note.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/account/components/header.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/account/components/profile_column_header.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/account/containers/account_note_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/account/containers/featured_tags_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/account/containers/follow_request_note_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/account/navigation.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/account_gallery/components/media_item.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/account_gallery/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/account_timeline/components/header.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/account_timeline/components/limited_account_hint.tsx
 create mode 100644 app/javascript/flavours/blobfox/features/account_timeline/components/memorial_note.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/account_timeline/components/moved_note.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/account_timeline/containers/header_container.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/account_timeline/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/audio/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/audio/visualizer.js
 create mode 100644 app/javascript/flavours/blobfox/features/blocks/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/bookmarked_statuses/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/closed_registrations_modal/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/community_timeline/components/column_settings.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/community_timeline/containers/column_settings_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/community_timeline/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/compose/components/action_bar.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/compose/components/autosuggest_account.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/compose/components/character_counter.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/compose/components/compose_form.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/compose/components/dropdown.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/compose/components/dropdown_menu.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/compose/components/emoji_picker_dropdown.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/compose/components/header.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/compose/components/language_dropdown.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/compose/components/navigation_bar.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/compose/components/options.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/compose/components/poll_form.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/compose/components/privacy_dropdown.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/compose/components/publisher.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/compose/components/reply_indicator.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/compose/components/search.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/compose/components/search_results.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/compose/components/text_icon_button.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/compose/components/textarea_icons.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/compose/components/upload.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/compose/components/upload_form.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/compose/components/upload_progress.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/compose/components/warning.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/compose/containers/autosuggest_account_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/compose/containers/compose_form_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/compose/containers/dropdown_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/compose/containers/emoji_picker_dropdown_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/compose/containers/header_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/compose/containers/language_dropdown_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/compose/containers/navigation_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/compose/containers/options_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/compose/containers/poll_form_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/compose/containers/privacy_dropdown_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/compose/containers/reply_indicator_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/compose/containers/search_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/compose/containers/search_results_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/compose/containers/sensitive_button_container.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/compose/containers/upload_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/compose/containers/upload_form_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/compose/containers/upload_progress_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/compose/containers/warning_container.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/compose/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/compose/util/counter.js
 create mode 100644 app/javascript/flavours/blobfox/features/compose/util/url_regex.js
 create mode 100644 app/javascript/flavours/blobfox/features/direct_timeline/components/column_settings.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/direct_timeline/components/conversation.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/direct_timeline/components/conversations_list.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/direct_timeline/containers/column_settings_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/direct_timeline/containers/conversation_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/direct_timeline/containers/conversations_list_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/direct_timeline/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/directory/components/account_card.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/directory/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/domain_blocks/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/emoji/emoji.js
 create mode 100644 app/javascript/flavours/blobfox/features/emoji/emoji_compressed.d.ts
 create mode 100644 app/javascript/flavours/blobfox/features/emoji/emoji_compressed.js
 create mode 100644 app/javascript/flavours/blobfox/features/emoji/emoji_map.json
 create mode 100644 app/javascript/flavours/blobfox/features/emoji/emoji_mart_data_light.ts
 create mode 100644 app/javascript/flavours/blobfox/features/emoji/emoji_mart_search_light.js
 create mode 100644 app/javascript/flavours/blobfox/features/emoji/emoji_picker.js
 create mode 100644 app/javascript/flavours/blobfox/features/emoji/emoji_unicode_mapping_light.ts
 create mode 100644 app/javascript/flavours/blobfox/features/emoji/emoji_utils.js
 create mode 100644 app/javascript/flavours/blobfox/features/emoji/unicode_to_filename.js
 create mode 100644 app/javascript/flavours/blobfox/features/emoji/unicode_to_unified_name.js
 create mode 100644 app/javascript/flavours/blobfox/features/explore/components/search_section.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/explore/components/story.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/explore/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/explore/links.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/explore/results.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/explore/statuses.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/explore/suggestions.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/explore/tags.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/favourited_statuses/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/favourites/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/filters/added_to_filter.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/filters/select_filter.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/firehose/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/follow_requests/components/account_authorize.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/follow_requests/containers/account_authorize_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/follow_requests/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/followed_tags/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/followers/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/following/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/getting_started/components/announcements.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/getting_started/components/trends.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/getting_started/containers/announcements_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/getting_started/containers/trends_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/getting_started/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/getting_started_misc/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/hashtag_timeline/components/column_settings.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/hashtag_timeline/components/hashtag_header.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/hashtag_timeline/containers/column_settings_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/hashtag_timeline/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/home_timeline/components/column_settings.tsx
 create mode 100644 app/javascript/flavours/blobfox/features/home_timeline/components/critical_update_banner.tsx
 create mode 100644 app/javascript/flavours/blobfox/features/home_timeline/components/explore_prompt.tsx
 create mode 100644 app/javascript/flavours/blobfox/features/home_timeline/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/interaction_modal/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/keyboard_shortcuts/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/list_adder/components/account.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/list_adder/components/list.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/list_adder/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/list_editor/components/account.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/list_editor/components/edit_list_form.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/list_editor/components/search.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/list_editor/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/list_timeline/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/lists/components/new_list_form.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/lists/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/local_settings/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/local_settings/navigation/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/local_settings/navigation/item/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/local_settings/page/deprecated_item/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/local_settings/page/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/local_settings/page/item/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/mutes/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/notifications/components/admin_report.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/notifications/components/admin_signup.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/notifications/components/clear_column_button.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/notifications/components/column_settings.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/notifications/components/filter_bar.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/notifications/components/follow.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/notifications/components/follow_request.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/notifications/components/grant_permission_button.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/notifications/components/notification.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/notifications/components/notifications_permission_banner.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/notifications/components/overlay.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/notifications/components/pill_bar_button.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/notifications/components/report.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/notifications/components/setting_toggle.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/notifications/containers/admin_report_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/notifications/containers/column_settings_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/notifications/containers/filter_bar_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/notifications/containers/follow_request_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/notifications/containers/notification_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/notifications/containers/overlay_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/notifications/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/onboarding/components/arrow_small_right.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/onboarding/components/progress_indicator.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/onboarding/components/step.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/onboarding/follows.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/onboarding/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/onboarding/share.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/picture_in_picture/components/footer.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/picture_in_picture/components/header.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/picture_in_picture/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/pinned_accounts_editor/containers/account_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/pinned_accounts_editor/containers/search_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/pinned_accounts_editor/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/pinned_statuses/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/privacy_policy/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/public_timeline/components/column_settings.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/public_timeline/containers/column_settings_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/public_timeline/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/reblogs/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/report/category.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/report/comment.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/report/components/option.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/report/components/status_check_box.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/report/containers/status_check_box_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/report/rules.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/report/statuses.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/report/thanks.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/standalone/compose/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/status/components/action_bar.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/status/components/card.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/status/components/detailed_status.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/status/containers/detailed_status_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/status/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/subscribed_languages_modal/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/actions_modal.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/audio_modal.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/block_modal.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/boost_modal.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/bundle.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/bundle_column_error.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/bundle_modal_error.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/column.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/column_header.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/column_link.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/column_loading.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/column_subheading.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/columns_area.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/compare_history_modal.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/compose_panel.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/confirmation_modal.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/deprecated_settings_modal.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/disabled_account_banner.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/doodle_modal.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/drawer_loading.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/embed_modal.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/favourite_modal.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/filter_modal.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/focal_point_modal.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/follow_requests_column_link.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/header.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/image_loader.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/image_modal.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/link_footer.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/list_panel.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/media_modal.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/modal_loading.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/modal_root.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/mute_modal.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/navigation_panel.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/notifications_counter_icon.js
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/report_modal.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/sign_in_banner.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/upload_area.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/video_modal.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/components/zoomable_image.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/containers/bundle_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/ui/containers/columns_area_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/ui/containers/loading_bar_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/ui/containers/modal_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/ui/containers/notifications_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/ui/containers/status_list_container.js
 create mode 100644 app/javascript/flavours/blobfox/features/ui/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/util/async-components.js
 create mode 100644 app/javascript/flavours/blobfox/features/ui/util/fullscreen.js
 create mode 100644 app/javascript/flavours/blobfox/features/ui/util/get_rect_from_entry.js
 create mode 100644 app/javascript/flavours/blobfox/features/ui/util/intersection_observer_wrapper.js
 create mode 100644 app/javascript/flavours/blobfox/features/ui/util/optional_motion.js
 create mode 100644 app/javascript/flavours/blobfox/features/ui/util/react_router_helpers.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/util/reduced_motion.jsx
 create mode 100644 app/javascript/flavours/blobfox/features/ui/util/schedule_idle_task.js
 create mode 100644 app/javascript/flavours/blobfox/features/video/index.jsx
 create mode 100644 app/javascript/flavours/blobfox/hooks/useHovering.ts
 create mode 100644 app/javascript/flavours/blobfox/images/elephant_ui_disappointed.svg
 create mode 100644 app/javascript/flavours/blobfox/images/elephant_ui_working.svg
 create mode 100644 app/javascript/flavours/blobfox/images/glitch-preview.jpg
 create mode 100644 app/javascript/flavours/blobfox/images/logo_warn_glitch.svg
 create mode 100644 app/javascript/flavours/blobfox/images/mbstobon-ui-0.png
 create mode 100644 app/javascript/flavours/blobfox/images/mbstobon-ui-1.png
 create mode 100644 app/javascript/flavours/blobfox/images/mbstobon-ui-2.png
 create mode 100644 app/javascript/flavours/blobfox/images/mbstobon-ui-3.png
 create mode 100644 app/javascript/flavours/blobfox/images/wave-drawer-glitched.png
 create mode 100644 app/javascript/flavours/blobfox/images/wave-drawer.png
 create mode 100644 app/javascript/flavours/blobfox/initial_state.js
 create mode 100644 app/javascript/flavours/blobfox/is_mobile.ts
 create mode 100644 app/javascript/flavours/blobfox/load_keyboard_extensions.js
 create mode 100644 app/javascript/flavours/blobfox/main.jsx
 create mode 100644 app/javascript/flavours/blobfox/models/account.ts
 create mode 100644 app/javascript/flavours/blobfox/models/custom_emoji.ts
 create mode 100644 app/javascript/flavours/blobfox/models/relationship.ts
 create mode 100644 app/javascript/flavours/blobfox/names.yml
 create mode 100644 app/javascript/flavours/blobfox/packs/admin.jsx
 create mode 100644 app/javascript/flavours/blobfox/packs/common.js
 create mode 100644 app/javascript/flavours/blobfox/packs/error.js
 create mode 100644 app/javascript/flavours/blobfox/packs/home.js
 create mode 100644 app/javascript/flavours/blobfox/packs/public.jsx
 create mode 100644 app/javascript/flavours/blobfox/packs/settings.js
 create mode 100644 app/javascript/flavours/blobfox/packs/share.jsx
 create mode 100644 app/javascript/flavours/blobfox/packs/sign_up.js
 create mode 100644 app/javascript/flavours/blobfox/performance.js
 create mode 100644 app/javascript/flavours/blobfox/permissions.ts
 create mode 100644 app/javascript/flavours/blobfox/polyfills/extra_polyfills.ts
 create mode 100644 app/javascript/flavours/blobfox/polyfills/index.ts
 create mode 100644 app/javascript/flavours/blobfox/polyfills/intl.ts
 create mode 100644 app/javascript/flavours/blobfox/ready.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/accounts.ts
 create mode 100644 app/javascript/flavours/blobfox/reducers/accounts_map.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/alerts.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/announcements.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/blocks.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/boosts.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/compose.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/contexts.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/conversations.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/custom_emojis.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/domain_lists.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/dropdown_menu.ts
 create mode 100644 app/javascript/flavours/blobfox/reducers/filters.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/followed_tags.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/height_cache.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/history.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/index.ts
 create mode 100644 app/javascript/flavours/blobfox/reducers/list_adder.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/list_editor.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/lists.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/local_settings.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/markers.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/media_attachments.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/meta.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/modal.ts
 create mode 100644 app/javascript/flavours/blobfox/reducers/mutes.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/notifications.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/picture_in_picture.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/pinned_accounts_editor.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/polls.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/push_notifications.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/relationships.ts
 create mode 100644 app/javascript/flavours/blobfox/reducers/search.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/server.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/settings.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/status_lists.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/statuses.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/suggestions.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/tags.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/timelines.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/trends.js
 create mode 100644 app/javascript/flavours/blobfox/reducers/user_lists.js
 create mode 100644 app/javascript/flavours/blobfox/scroll.ts
 create mode 100644 app/javascript/flavours/blobfox/selectors/accounts.ts
 create mode 100644 app/javascript/flavours/blobfox/selectors/index.js
 create mode 100644 app/javascript/flavours/blobfox/settings.js
 create mode 100644 app/javascript/flavours/blobfox/store/index.ts
 create mode 100644 app/javascript/flavours/blobfox/store/middlewares/errors.ts
 create mode 100644 app/javascript/flavours/blobfox/store/middlewares/loading_bar.ts
 create mode 100644 app/javascript/flavours/blobfox/store/middlewares/sounds.ts
 create mode 100644 app/javascript/flavours/blobfox/store/store.ts
 create mode 100644 app/javascript/flavours/blobfox/store/typed_functions.ts
 create mode 100644 app/javascript/flavours/blobfox/stream.js
 create mode 100644 app/javascript/flavours/blobfox/styles/_mixins.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/about.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/accessibility.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/accounts.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/admin.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/basics.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/branding.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/components/about.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/components/accounts.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/components/announcements.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/components/boost.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/components/columns.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/components/compose_form.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/components/directory.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/components/domains.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/components/doodle.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/components/drawer.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/components/emoji.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/components/emoji_picker.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/components/explore.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/components/index.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/components/lists.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/components/local_settings.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/components/media.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/components/misc.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/components/modal.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/components/privacy_policy.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/components/regeneration_indicator.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/components/search.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/components/sensitive.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/components/signed_out.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/components/single_column.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/components/status.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/containers.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/contrast.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/contrast/diff.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/contrast/variables.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/dashboard.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/forms.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/index.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/lists.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/mastodon-light.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/mastodon-light/diff.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/mastodon-light/variables.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/modal.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/polls.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/reset.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/rich_text.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/rtl.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/statuses.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/tables.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/variables.scss
 create mode 100644 app/javascript/flavours/blobfox/styles/widgets.scss
 create mode 100644 app/javascript/flavours/blobfox/theme.yml
 create mode 100644 app/javascript/flavours/blobfox/types/util.ts
 create mode 100644 app/javascript/flavours/blobfox/utils/backend_links.js
 create mode 100644 app/javascript/flavours/blobfox/utils/base64.ts
 create mode 100644 app/javascript/flavours/blobfox/utils/config.js
 create mode 100644 app/javascript/flavours/blobfox/utils/content_warning.js
 create mode 100644 app/javascript/flavours/blobfox/utils/filters.ts
 create mode 100644 app/javascript/flavours/blobfox/utils/hashtag.js
 create mode 100644 app/javascript/flavours/blobfox/utils/hashtags.ts
 create mode 100644 app/javascript/flavours/blobfox/utils/html.js
 create mode 100644 app/javascript/flavours/blobfox/utils/icons.jsx
 create mode 100644 app/javascript/flavours/blobfox/utils/idna.js
 create mode 100644 app/javascript/flavours/blobfox/utils/js_helpers.js
 create mode 100644 app/javascript/flavours/blobfox/utils/log_out.js
 create mode 100644 app/javascript/flavours/blobfox/utils/notifications.js
 create mode 100644 app/javascript/flavours/blobfox/utils/numbers.ts
 create mode 100644 app/javascript/flavours/blobfox/utils/privacy_preference.js
 create mode 100644 app/javascript/flavours/blobfox/utils/react_helpers.js
 create mode 100644 app/javascript/flavours/blobfox/utils/react_router.jsx
 create mode 100644 app/javascript/flavours/blobfox/utils/resize_image.js
 create mode 100644 app/javascript/flavours/blobfox/utils/scrollbar.js
 create mode 100644 app/javascript/flavours/blobfox/uuid.ts
 create mode 100644 app/javascript/skins/blobfox/contrast/common.scss
 create mode 100644 app/javascript/skins/blobfox/contrast/names.yml
 create mode 100644 app/javascript/skins/blobfox/mastodon-light/common.scss
 create mode 100644 app/javascript/skins/blobfox/mastodon-light/names.yml
 create mode 100644 app/javascript/skins/blobfox/queens-pink-contrast/common.scss
 create mode 100644 app/javascript/skins/blobfox/queens-pink-contrast/diff.scss
 create mode 100644 app/javascript/skins/blobfox/queens-pink-contrast/names.yml
 create mode 100644 app/javascript/skins/blobfox/queens-pink-contrast/screenshot.jpg
 create mode 100644 app/javascript/skins/blobfox/queens-pink-contrast/variables.scss
 create mode 100644 app/javascript/skins/blobfox/solarpunk/common.scss
 create mode 100644 app/javascript/skins/blobfox/solarpunk/diff.scss
 create mode 100644 app/javascript/skins/blobfox/solarpunk/names.yml
 create mode 100644 app/javascript/skins/blobfox/solarpunk/variables.scss

diff --git a/app/javascript/flavours/blobfox/actions/account_notes.ts b/app/javascript/flavours/blobfox/actions/account_notes.ts
new file mode 100644
index 00000000000000..b49327584764c9
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/account_notes.ts
@@ -0,0 +1,18 @@
+import type { ApiRelationshipJSON } from 'flavours/blobfox/api_types/relationships';
+import { createAppAsyncThunk } from 'flavours/blobfox/store/typed_functions';
+
+import api from '../api';
+
+export const submitAccountNote = createAppAsyncThunk(
+  'account_note/submit',
+  async (args: { id: string; value: string }, { getState }) => {
+    const response = await api(getState).post<ApiRelationshipJSON>(
+      `/api/v1/accounts/${args.id}/note`,
+      {
+        comment: args.value,
+      },
+    );
+
+    return { relationship: response.data };
+  },
+);
diff --git a/app/javascript/flavours/blobfox/actions/accounts.js b/app/javascript/flavours/blobfox/actions/accounts.js
new file mode 100644
index 00000000000000..a93c027def6332
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/accounts.js
@@ -0,0 +1,774 @@
+import api, { getLinks } from '../api';
+
+import {
+  followAccountSuccess, unfollowAccountSuccess,
+  authorizeFollowRequestSuccess, rejectFollowRequestSuccess,
+  followAccountRequest, followAccountFail,
+  unfollowAccountRequest, unfollowAccountFail,
+  muteAccountSuccess, unmuteAccountSuccess,
+  blockAccountSuccess, unblockAccountSuccess,
+  pinAccountSuccess, unpinAccountSuccess,
+  fetchRelationshipsSuccess,
+} from './accounts_typed';
+import { importFetchedAccount, importFetchedAccounts } from './importer';
+
+export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
+export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS';
+export const ACCOUNT_FETCH_FAIL    = 'ACCOUNT_FETCH_FAIL';
+
+export const ACCOUNT_LOOKUP_REQUEST = 'ACCOUNT_LOOKUP_REQUEST';
+export const ACCOUNT_LOOKUP_SUCCESS = 'ACCOUNT_LOOKUP_SUCCESS';
+export const ACCOUNT_LOOKUP_FAIL    = 'ACCOUNT_LOOKUP_FAIL';
+
+export const ACCOUNT_BLOCK_REQUEST = 'ACCOUNT_BLOCK_REQUEST';
+export const ACCOUNT_BLOCK_FAIL    = 'ACCOUNT_BLOCK_FAIL';
+
+export const ACCOUNT_UNBLOCK_REQUEST = 'ACCOUNT_UNBLOCK_REQUEST';
+export const ACCOUNT_UNBLOCK_FAIL    = 'ACCOUNT_UNBLOCK_FAIL';
+
+export const ACCOUNT_MUTE_REQUEST = 'ACCOUNT_MUTE_REQUEST';
+export const ACCOUNT_MUTE_FAIL    = 'ACCOUNT_MUTE_FAIL';
+
+export const ACCOUNT_UNMUTE_REQUEST = 'ACCOUNT_UNMUTE_REQUEST';
+export const ACCOUNT_UNMUTE_FAIL    = 'ACCOUNT_UNMUTE_FAIL';
+
+export const ACCOUNT_PIN_REQUEST = 'ACCOUNT_PIN_REQUEST';
+export const ACCOUNT_PIN_FAIL    = 'ACCOUNT_PIN_FAIL';
+
+export const ACCOUNT_UNPIN_REQUEST = 'ACCOUNT_UNPIN_REQUEST';
+export const ACCOUNT_UNPIN_FAIL    = 'ACCOUNT_UNPIN_FAIL';
+
+export const FOLLOWERS_FETCH_REQUEST = 'FOLLOWERS_FETCH_REQUEST';
+export const FOLLOWERS_FETCH_SUCCESS = 'FOLLOWERS_FETCH_SUCCESS';
+export const FOLLOWERS_FETCH_FAIL    = 'FOLLOWERS_FETCH_FAIL';
+
+export const FOLLOWERS_EXPAND_REQUEST = 'FOLLOWERS_EXPAND_REQUEST';
+export const FOLLOWERS_EXPAND_SUCCESS = 'FOLLOWERS_EXPAND_SUCCESS';
+export const FOLLOWERS_EXPAND_FAIL    = 'FOLLOWERS_EXPAND_FAIL';
+
+export const FOLLOWING_FETCH_REQUEST = 'FOLLOWING_FETCH_REQUEST';
+export const FOLLOWING_FETCH_SUCCESS = 'FOLLOWING_FETCH_SUCCESS';
+export const FOLLOWING_FETCH_FAIL    = 'FOLLOWING_FETCH_FAIL';
+
+export const FOLLOWING_EXPAND_REQUEST = 'FOLLOWING_EXPAND_REQUEST';
+export const FOLLOWING_EXPAND_SUCCESS = 'FOLLOWING_EXPAND_SUCCESS';
+export const FOLLOWING_EXPAND_FAIL    = 'FOLLOWING_EXPAND_FAIL';
+
+export const RELATIONSHIPS_FETCH_REQUEST = 'RELATIONSHIPS_FETCH_REQUEST';
+export const RELATIONSHIPS_FETCH_FAIL    = 'RELATIONSHIPS_FETCH_FAIL';
+
+export const FOLLOW_REQUESTS_FETCH_REQUEST = 'FOLLOW_REQUESTS_FETCH_REQUEST';
+export const FOLLOW_REQUESTS_FETCH_SUCCESS = 'FOLLOW_REQUESTS_FETCH_SUCCESS';
+export const FOLLOW_REQUESTS_FETCH_FAIL    = 'FOLLOW_REQUESTS_FETCH_FAIL';
+
+export const FOLLOW_REQUESTS_EXPAND_REQUEST = 'FOLLOW_REQUESTS_EXPAND_REQUEST';
+export const FOLLOW_REQUESTS_EXPAND_SUCCESS = 'FOLLOW_REQUESTS_EXPAND_SUCCESS';
+export const FOLLOW_REQUESTS_EXPAND_FAIL    = 'FOLLOW_REQUESTS_EXPAND_FAIL';
+
+export const FOLLOW_REQUEST_AUTHORIZE_REQUEST = 'FOLLOW_REQUEST_AUTHORIZE_REQUEST';
+export const FOLLOW_REQUEST_AUTHORIZE_SUCCESS = 'FOLLOW_REQUEST_AUTHORIZE_SUCCESS';
+export const FOLLOW_REQUEST_AUTHORIZE_FAIL    = 'FOLLOW_REQUEST_AUTHORIZE_FAIL';
+
+export const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST';
+export const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS';
+export const FOLLOW_REQUEST_REJECT_FAIL    = 'FOLLOW_REQUEST_REJECT_FAIL';
+
+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_CLEAR  = 'PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CLEAR';
+export const PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE = 'PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE';
+
+export const PINNED_ACCOUNTS_EDITOR_RESET = 'PINNED_ACCOUNTS_EDITOR_RESET';
+
+export const ACCOUNT_REVEAL = 'ACCOUNT_REVEAL';
+
+export * from './accounts_typed';
+
+export function fetchAccount(id) {
+  return (dispatch, getState) => {
+    dispatch(fetchRelationships([id]));
+
+    if (getState().getIn(['accounts', id], null) !== null) {
+      return;
+    }
+
+    dispatch(fetchAccountRequest(id));
+
+    api(getState).get(`/api/v1/accounts/${id}`).then(response => {
+      dispatch(importFetchedAccount(response.data));
+      dispatch(fetchAccountSuccess());
+    }).catch(error => {
+      dispatch(fetchAccountFail(id, error));
+    });
+  };
+}
+
+export const lookupAccount = acct => (dispatch, getState) => {
+  dispatch(lookupAccountRequest(acct));
+
+  api(getState).get('/api/v1/accounts/lookup', { params: { acct } }).then(response => {
+    dispatch(fetchRelationships([response.data.id]));
+    dispatch(importFetchedAccount(response.data));
+    dispatch(lookupAccountSuccess());
+  }).catch(error => {
+    dispatch(lookupAccountFail(acct, error));
+  });
+};
+
+export const lookupAccountRequest = (acct) => ({
+  type: ACCOUNT_LOOKUP_REQUEST,
+  acct,
+});
+
+export const lookupAccountSuccess = () => ({
+  type: ACCOUNT_LOOKUP_SUCCESS,
+});
+
+export const lookupAccountFail = (acct, error) => ({
+  type: ACCOUNT_LOOKUP_FAIL,
+  acct,
+  error,
+  skipAlert: true,
+});
+
+export function fetchAccountRequest(id) {
+  return {
+    type: ACCOUNT_FETCH_REQUEST,
+    id,
+  };
+}
+
+export function fetchAccountSuccess() {
+  return {
+    type: ACCOUNT_FETCH_SUCCESS,
+  };
+}
+
+export function fetchAccountFail(id, error) {
+  return {
+    type: ACCOUNT_FETCH_FAIL,
+    id,
+    error,
+    skipAlert: true,
+  };
+}
+
+export function followAccount(id, options = { reblogs: true }) {
+  return (dispatch, getState) => {
+    const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
+    const locked = getState().getIn(['accounts', id, 'locked'], false);
+
+    dispatch(followAccountRequest({ id, locked }));
+
+    api(getState).post(`/api/v1/accounts/${id}/follow`, options).then(response => {
+      dispatch(followAccountSuccess({relationship: response.data, alreadyFollowing}));
+    }).catch(error => {
+      dispatch(followAccountFail({ id, error, locked }));
+    });
+  };
+}
+
+export function unfollowAccount(id) {
+  return (dispatch, getState) => {
+    dispatch(unfollowAccountRequest(id));
+
+    api(getState).post(`/api/v1/accounts/${id}/unfollow`).then(response => {
+      dispatch(unfollowAccountSuccess({relationship: response.data, statuses: getState().get('statuses')}));
+    }).catch(error => {
+      dispatch(unfollowAccountFail({ id, error }));
+    });
+  };
+}
+
+export function blockAccount(id) {
+  return (dispatch, getState) => {
+    dispatch(blockAccountRequest(id));
+
+    api(getState).post(`/api/v1/accounts/${id}/block`).then(response => {
+      // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
+      dispatch(blockAccountSuccess({ relationship: response.data, statuses: getState().get('statuses') }));
+    }).catch(error => {
+      dispatch(blockAccountFail({ id, error }));
+    });
+  };
+}
+
+export function unblockAccount(id) {
+  return (dispatch, getState) => {
+    dispatch(unblockAccountRequest(id));
+
+    api(getState).post(`/api/v1/accounts/${id}/unblock`).then(response => {
+      dispatch(unblockAccountSuccess({ relationship: response.data }));
+    }).catch(error => {
+      dispatch(unblockAccountFail({ id, error }));
+    });
+  };
+}
+
+export function blockAccountRequest(id) {
+  return {
+    type: ACCOUNT_BLOCK_REQUEST,
+    id,
+  };
+}
+export function blockAccountFail(error) {
+  return {
+    type: ACCOUNT_BLOCK_FAIL,
+    error,
+  };
+}
+
+export function unblockAccountRequest(id) {
+  return {
+    type: ACCOUNT_UNBLOCK_REQUEST,
+    id,
+  };
+}
+
+export function unblockAccountFail(error) {
+  return {
+    type: ACCOUNT_UNBLOCK_FAIL,
+    error,
+  };
+}
+
+
+export function muteAccount(id, notifications, duration=0) {
+  return (dispatch, getState) => {
+    dispatch(muteAccountRequest(id));
+
+    api(getState).post(`/api/v1/accounts/${id}/mute`, { notifications, duration }).then(response => {
+      // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
+      dispatch(muteAccountSuccess({ relationship: response.data, statuses: getState().get('statuses') }));
+    }).catch(error => {
+      dispatch(muteAccountFail({ id, error }));
+    });
+  };
+}
+
+export function unmuteAccount(id) {
+  return (dispatch, getState) => {
+    dispatch(unmuteAccountRequest(id));
+
+    api(getState).post(`/api/v1/accounts/${id}/unmute`).then(response => {
+      dispatch(unmuteAccountSuccess({ relationship: response.data }));
+    }).catch(error => {
+      dispatch(unmuteAccountFail({ id, error }));
+    });
+  };
+}
+
+export function muteAccountRequest(id) {
+  return {
+    type: ACCOUNT_MUTE_REQUEST,
+    id,
+  };
+}
+
+export function muteAccountFail(error) {
+  return {
+    type: ACCOUNT_MUTE_FAIL,
+    error,
+  };
+}
+
+export function unmuteAccountRequest(id) {
+  return {
+    type: ACCOUNT_UNMUTE_REQUEST,
+    id,
+  };
+}
+
+export function unmuteAccountFail(error) {
+  return {
+    type: ACCOUNT_UNMUTE_FAIL,
+    error,
+  };
+}
+
+
+export function fetchFollowers(id) {
+  return (dispatch, getState) => {
+    dispatch(fetchFollowersRequest(id));
+
+    api(getState).get(`/api/v1/accounts/${id}/followers`).then(response => {
+      const next = getLinks(response).refs.find(link => link.rel === 'next');
+
+      dispatch(importFetchedAccounts(response.data));
+      dispatch(fetchFollowersSuccess(id, response.data, next ? next.uri : null));
+      dispatch(fetchRelationships(response.data.map(item => item.id)));
+    }).catch(error => {
+      dispatch(fetchFollowersFail(id, error));
+    });
+  };
+}
+
+export function fetchFollowersRequest(id) {
+  return {
+    type: FOLLOWERS_FETCH_REQUEST,
+    id,
+  };
+}
+
+export function fetchFollowersSuccess(id, accounts, next) {
+  return {
+    type: FOLLOWERS_FETCH_SUCCESS,
+    id,
+    accounts,
+    next,
+  };
+}
+
+export function fetchFollowersFail(id, error) {
+  return {
+    type: FOLLOWERS_FETCH_FAIL,
+    id,
+    error,
+    skipNotFound: true,
+  };
+}
+
+export function expandFollowers(id) {
+  return (dispatch, getState) => {
+    const url = getState().getIn(['user_lists', 'followers', id, 'next']);
+
+    if (url === null) {
+      return;
+    }
+
+    dispatch(expandFollowersRequest(id));
+
+    api(getState).get(url).then(response => {
+      const next = getLinks(response).refs.find(link => link.rel === 'next');
+
+      dispatch(importFetchedAccounts(response.data));
+      dispatch(expandFollowersSuccess(id, response.data, next ? next.uri : null));
+      dispatch(fetchRelationships(response.data.map(item => item.id)));
+    }).catch(error => {
+      dispatch(expandFollowersFail(id, error));
+    });
+  };
+}
+
+export function expandFollowersRequest(id) {
+  return {
+    type: FOLLOWERS_EXPAND_REQUEST,
+    id,
+  };
+}
+
+export function expandFollowersSuccess(id, accounts, next) {
+  return {
+    type: FOLLOWERS_EXPAND_SUCCESS,
+    id,
+    accounts,
+    next,
+  };
+}
+
+export function expandFollowersFail(id, error) {
+  return {
+    type: FOLLOWERS_EXPAND_FAIL,
+    id,
+    error,
+  };
+}
+
+export function fetchFollowing(id) {
+  return (dispatch, getState) => {
+    dispatch(fetchFollowingRequest(id));
+
+    api(getState).get(`/api/v1/accounts/${id}/following`).then(response => {
+      const next = getLinks(response).refs.find(link => link.rel === 'next');
+
+      dispatch(importFetchedAccounts(response.data));
+      dispatch(fetchFollowingSuccess(id, response.data, next ? next.uri : null));
+      dispatch(fetchRelationships(response.data.map(item => item.id)));
+    }).catch(error => {
+      dispatch(fetchFollowingFail(id, error));
+    });
+  };
+}
+
+export function fetchFollowingRequest(id) {
+  return {
+    type: FOLLOWING_FETCH_REQUEST,
+    id,
+  };
+}
+
+export function fetchFollowingSuccess(id, accounts, next) {
+  return {
+    type: FOLLOWING_FETCH_SUCCESS,
+    id,
+    accounts,
+    next,
+  };
+}
+
+export function fetchFollowingFail(id, error) {
+  return {
+    type: FOLLOWING_FETCH_FAIL,
+    id,
+    error,
+    skipNotFound: true,
+  };
+}
+
+export function expandFollowing(id) {
+  return (dispatch, getState) => {
+    const url = getState().getIn(['user_lists', 'following', id, 'next']);
+
+    if (url === null) {
+      return;
+    }
+
+    dispatch(expandFollowingRequest(id));
+
+    api(getState).get(url).then(response => {
+      const next = getLinks(response).refs.find(link => link.rel === 'next');
+
+      dispatch(importFetchedAccounts(response.data));
+      dispatch(expandFollowingSuccess(id, response.data, next ? next.uri : null));
+      dispatch(fetchRelationships(response.data.map(item => item.id)));
+    }).catch(error => {
+      dispatch(expandFollowingFail(id, error));
+    });
+  };
+}
+
+export function expandFollowingRequest(id) {
+  return {
+    type: FOLLOWING_EXPAND_REQUEST,
+    id,
+  };
+}
+
+export function expandFollowingSuccess(id, accounts, next) {
+  return {
+    type: FOLLOWING_EXPAND_SUCCESS,
+    id,
+    accounts,
+    next,
+  };
+}
+
+export function expandFollowingFail(id, error) {
+  return {
+    type: FOLLOWING_EXPAND_FAIL,
+    id,
+    error,
+  };
+}
+
+export function fetchRelationships(accountIds) {
+  return (dispatch, getState) => {
+    const state = getState();
+    const loadedRelationships = state.get('relationships');
+    const newAccountIds = accountIds.filter(id => loadedRelationships.get(id, null) === null);
+    const signedIn = !!state.getIn(['meta', 'me']);
+
+    if (!signedIn || newAccountIds.length === 0) {
+      return;
+    }
+
+    dispatch(fetchRelationshipsRequest(newAccountIds));
+
+    api(getState).get(`/api/v1/accounts/relationships?with_suspended=true&${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => {
+      dispatch(fetchRelationshipsSuccess({ relationships: response.data }));
+    }).catch(error => {
+      dispatch(fetchRelationshipsFail(error));
+    });
+  };
+}
+
+export function fetchRelationshipsRequest(ids) {
+  return {
+    type: RELATIONSHIPS_FETCH_REQUEST,
+    ids,
+    skipLoading: true,
+  };
+}
+
+export function fetchRelationshipsFail(error) {
+  return {
+    type: RELATIONSHIPS_FETCH_FAIL,
+    error,
+    skipLoading: true,
+    skipNotFound: true,
+  };
+}
+
+export function fetchFollowRequests() {
+  return (dispatch, getState) => {
+    dispatch(fetchFollowRequestsRequest());
+
+    api(getState).get('/api/v1/follow_requests').then(response => {
+      const next = getLinks(response).refs.find(link => link.rel === 'next');
+      dispatch(importFetchedAccounts(response.data));
+      dispatch(fetchFollowRequestsSuccess(response.data, next ? next.uri : null));
+    }).catch(error => dispatch(fetchFollowRequestsFail(error)));
+  };
+}
+
+export function fetchFollowRequestsRequest() {
+  return {
+    type: FOLLOW_REQUESTS_FETCH_REQUEST,
+  };
+}
+
+export function fetchFollowRequestsSuccess(accounts, next) {
+  return {
+    type: FOLLOW_REQUESTS_FETCH_SUCCESS,
+    accounts,
+    next,
+  };
+}
+
+export function fetchFollowRequestsFail(error) {
+  return {
+    type: FOLLOW_REQUESTS_FETCH_FAIL,
+    error,
+  };
+}
+
+export function expandFollowRequests() {
+  return (dispatch, getState) => {
+    const url = getState().getIn(['user_lists', 'follow_requests', 'next']);
+
+    if (url === null) {
+      return;
+    }
+
+    dispatch(expandFollowRequestsRequest());
+
+    api(getState).get(url).then(response => {
+      const next = getLinks(response).refs.find(link => link.rel === 'next');
+      dispatch(importFetchedAccounts(response.data));
+      dispatch(expandFollowRequestsSuccess(response.data, next ? next.uri : null));
+    }).catch(error => dispatch(expandFollowRequestsFail(error)));
+  };
+}
+
+export function expandFollowRequestsRequest() {
+  return {
+    type: FOLLOW_REQUESTS_EXPAND_REQUEST,
+  };
+}
+
+export function expandFollowRequestsSuccess(accounts, next) {
+  return {
+    type: FOLLOW_REQUESTS_EXPAND_SUCCESS,
+    accounts,
+    next,
+  };
+}
+
+export function expandFollowRequestsFail(error) {
+  return {
+    type: FOLLOW_REQUESTS_EXPAND_FAIL,
+    error,
+  };
+}
+
+export function authorizeFollowRequest(id) {
+  return (dispatch, getState) => {
+    dispatch(authorizeFollowRequestRequest(id));
+
+    api(getState)
+      .post(`/api/v1/follow_requests/${id}/authorize`)
+      .then(() => dispatch(authorizeFollowRequestSuccess({ id })))
+      .catch(error => dispatch(authorizeFollowRequestFail(id, error)));
+  };
+}
+
+export function authorizeFollowRequestRequest(id) {
+  return {
+    type: FOLLOW_REQUEST_AUTHORIZE_REQUEST,
+    id,
+  };
+}
+
+export function authorizeFollowRequestFail(id, error) {
+  return {
+    type: FOLLOW_REQUEST_AUTHORIZE_FAIL,
+    id,
+    error,
+  };
+}
+
+
+export function rejectFollowRequest(id) {
+  return (dispatch, getState) => {
+    dispatch(rejectFollowRequestRequest(id));
+
+    api(getState)
+      .post(`/api/v1/follow_requests/${id}/reject`)
+      .then(() => dispatch(rejectFollowRequestSuccess({ id })))
+      .catch(error => dispatch(rejectFollowRequestFail(id, error)));
+  };
+}
+
+export function rejectFollowRequestRequest(id) {
+  return {
+    type: FOLLOW_REQUEST_REJECT_REQUEST,
+    id,
+  };
+}
+
+export function rejectFollowRequestFail(id, error) {
+  return {
+    type: FOLLOW_REQUEST_REJECT_FAIL,
+    id,
+    error,
+  };
+}
+
+export function pinAccount(id) {
+  return (dispatch, getState) => {
+    dispatch(pinAccountRequest(id));
+
+    api(getState).post(`/api/v1/accounts/${id}/pin`).then(response => {
+      dispatch(pinAccountSuccess({ relationship: response.data }));
+    }).catch(error => {
+      dispatch(pinAccountFail(error));
+    });
+  };
+}
+
+export function unpinAccount(id) {
+  return (dispatch, getState) => {
+    dispatch(unpinAccountRequest(id));
+
+    api(getState).post(`/api/v1/accounts/${id}/unpin`).then(response => {
+      dispatch(unpinAccountSuccess({ relationship: response.data }));
+    }).catch(error => {
+      dispatch(unpinAccountFail(error));
+    });
+  };
+}
+
+export function pinAccountRequest(id) {
+  return {
+    type: ACCOUNT_PIN_REQUEST,
+    id,
+  };
+}
+
+export function pinAccountFail(error) {
+  return {
+    type: ACCOUNT_PIN_FAIL,
+    error,
+  };
+}
+
+export function unpinAccountRequest(id) {
+  return {
+    type: ACCOUNT_UNPIN_REQUEST,
+    id,
+  };
+}
+
+export function unpinAccountFail(error) {
+  return {
+    type: ACCOUNT_UNPIN_FAIL,
+    error,
+  };
+}
+
+export function fetchPinnedAccounts() {
+  return (dispatch, getState) => {
+    dispatch(fetchPinnedAccountsRequest());
+
+    api(getState).get('/api/v1/endorsements', { params: { limit: 0 } }).then(response => {
+      dispatch(importFetchedAccounts(response.data));
+      dispatch(fetchPinnedAccountsSuccess(response.data));
+    }).catch(err => dispatch(fetchPinnedAccountsFail(err)));
+  };
+}
+
+export function fetchPinnedAccountsRequest() {
+  return {
+    type: PINNED_ACCOUNTS_FETCH_REQUEST,
+  };
+}
+
+export function fetchPinnedAccountsSuccess(accounts, next) {
+  return {
+    type: PINNED_ACCOUNTS_FETCH_SUCCESS,
+    accounts,
+    next,
+  };
+}
+
+export function fetchPinnedAccountsFail(error) {
+  return {
+    type: PINNED_ACCOUNTS_FETCH_FAIL,
+    error,
+  };
+}
+
+export function fetchPinnedAccountsSuggestions(q) {
+  return (dispatch, getState) => {
+    dispatch(fetchPinnedAccountsSuggestionsRequest());
+
+    const params = {
+      q,
+      resolve: false,
+      limit: 4,
+      following: true,
+    };
+
+    api(getState).get('/api/v1/accounts/search', { params }).then(response => {
+      dispatch(importFetchedAccounts(response.data));
+      dispatch(fetchPinnedAccountsSuggestionsSuccess(q, response.data));
+    }).catch(err => dispatch(fetchPinnedAccountsSuggestionsFail(err)));
+  };
+}
+
+export function fetchPinnedAccountsSuggestionsRequest() {
+  return {
+    type: PINNED_ACCOUNTS_SUGGESTIONS_FETCH_REQUEST,
+  };
+}
+
+export function fetchPinnedAccountsSuggestionsSuccess(query, accounts) {
+  return {
+    type: PINNED_ACCOUNTS_SUGGESTIONS_FETCH_SUCCESS,
+    query,
+    accounts,
+  };
+}
+
+export function fetchPinnedAccountsSuggestionsFail(error) {
+  return {
+    type: PINNED_ACCOUNTS_SUGGESTIONS_FETCH_FAIL,
+    error,
+  };
+}
+
+export function clearPinnedAccountsSuggestions() {
+  return {
+    type: PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CLEAR,
+  };
+}
+
+export function changePinnedAccountsSuggestions(value) {
+  return {
+    type: PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE,
+    value,
+  };
+}
+
+export function resetPinnedAccountsEditor() {
+  return {
+    type: PINNED_ACCOUNTS_EDITOR_RESET,
+  };
+}
+
diff --git a/app/javascript/flavours/blobfox/actions/accounts_typed.ts b/app/javascript/flavours/blobfox/actions/accounts_typed.ts
new file mode 100644
index 00000000000000..f57bf854f8ee58
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/accounts_typed.ts
@@ -0,0 +1,97 @@
+import { createAction } from '@reduxjs/toolkit';
+
+import type { ApiAccountJSON } from 'flavours/blobfox/api_types/accounts';
+import type { ApiRelationshipJSON } from 'flavours/blobfox/api_types/relationships';
+
+export const revealAccount = createAction<{
+  id: string;
+}>('accounts/revealAccount');
+
+export const importAccounts = createAction<{ accounts: ApiAccountJSON[] }>(
+  'accounts/importAccounts',
+);
+
+function actionWithSkipLoadingTrue<Args extends object>(args: Args) {
+  return {
+    payload: {
+      ...args,
+      skipLoading: true,
+    },
+  };
+}
+
+export const followAccountSuccess = createAction(
+  'accounts/followAccount/SUCCESS',
+  actionWithSkipLoadingTrue<{
+    relationship: ApiRelationshipJSON;
+    alreadyFollowing: boolean;
+  }>,
+);
+
+export const unfollowAccountSuccess = createAction(
+  'accounts/unfollowAccount/SUCCESS',
+  actionWithSkipLoadingTrue<{
+    relationship: ApiRelationshipJSON;
+    statuses: unknown;
+    alreadyFollowing?: boolean;
+  }>,
+);
+
+export const authorizeFollowRequestSuccess = createAction<{ id: string }>(
+  'accounts/followRequestAuthorize/SUCCESS',
+);
+
+export const rejectFollowRequestSuccess = createAction<{ id: string }>(
+  'accounts/followRequestReject/SUCCESS',
+);
+
+export const followAccountRequest = createAction(
+  'accounts/follow/REQUEST',
+  actionWithSkipLoadingTrue<{ id: string; locked: boolean }>,
+);
+
+export const followAccountFail = createAction(
+  'accounts/follow/FAIL',
+  actionWithSkipLoadingTrue<{ id: string; error: string; locked: boolean }>,
+);
+
+export const unfollowAccountRequest = createAction(
+  'accounts/unfollow/REQUEST',
+  actionWithSkipLoadingTrue<{ id: string }>,
+);
+
+export const unfollowAccountFail = createAction(
+  'accounts/unfollow/FAIL',
+  actionWithSkipLoadingTrue<{ id: string; error: string }>,
+);
+
+export const blockAccountSuccess = createAction<{
+  relationship: ApiRelationshipJSON;
+  statuses: unknown;
+}>('accounts/block/SUCCESS');
+
+export const unblockAccountSuccess = createAction<{
+  relationship: ApiRelationshipJSON;
+}>('accounts/unblock/SUCCESS');
+
+export const muteAccountSuccess = createAction<{
+  relationship: ApiRelationshipJSON;
+  statuses: unknown;
+}>('accounts/mute/SUCCESS');
+
+export const unmuteAccountSuccess = createAction<{
+  relationship: ApiRelationshipJSON;
+}>('accounts/unmute/SUCCESS');
+
+export const pinAccountSuccess = createAction<{
+  relationship: ApiRelationshipJSON;
+}>('accounts/pin/SUCCESS');
+
+export const unpinAccountSuccess = createAction<{
+  relationship: ApiRelationshipJSON;
+}>('accounts/unpin/SUCCESS');
+
+export const fetchRelationshipsSuccess = createAction(
+  'relationships/fetch/SUCCESS',
+  actionWithSkipLoadingTrue<{ relationships: ApiRelationshipJSON[] }>,
+);
diff --git a/app/javascript/flavours/blobfox/actions/alerts.js b/app/javascript/flavours/blobfox/actions/alerts.js
new file mode 100644
index 00000000000000..42834146bf5ba6
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/alerts.js
@@ -0,0 +1,59 @@
+import { defineMessages } from 'react-intl';
+
+const messages = defineMessages({
+  unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' },
+  unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' },
+  rateLimitedTitle: { id: 'alert.rate_limited.title', defaultMessage: 'Rate limited' },
+  rateLimitedMessage: { id: 'alert.rate_limited.message', defaultMessage: 'Please retry after {retry_time, time, medium}.' },
+});
+
+export const ALERT_SHOW    = 'ALERT_SHOW';
+export const ALERT_DISMISS = 'ALERT_DISMISS';
+export const ALERT_CLEAR   = 'ALERT_CLEAR';
+export const ALERT_NOOP    = 'ALERT_NOOP';
+
+export const dismissAlert = alert => ({
+  type: ALERT_DISMISS,
+  alert,
+});
+
+export const clearAlert = () => ({
+  type: ALERT_CLEAR,
+});
+
+export const showAlert = alert => ({
+  type: ALERT_SHOW,
+  alert,
+});
+
+export const showAlertForError = (error, skipNotFound = false) => {
+  if (error.response) {
+    const { data, status, statusText, headers } = error.response;
+
+    // Skip these errors as they are reflected in the UI
+    if (skipNotFound && (status === 404 || status === 410)) {
+      return { type: ALERT_NOOP };
+    }
+
+    // Rate limit errors
+    if (status === 429 && headers['x-ratelimit-reset']) {
+      return showAlert({
+        title: messages.rateLimitedTitle,
+        message: messages.rateLimitedMessage,
+        values: { 'retry_time': new Date(headers['x-ratelimit-reset']) },
+      });
+    }
+
+    return showAlert({
+      title: `${status}`,
+      message: data.error || statusText,
+    });
+  }
+
+  console.error(error);
+
+  return showAlert({
+    title: messages.unexpectedTitle,
+    message: messages.unexpectedMessage,
+  });
+};
diff --git a/app/javascript/flavours/blobfox/actions/announcements.js b/app/javascript/flavours/blobfox/actions/announcements.js
new file mode 100644
index 00000000000000..339c5f3adc5a8e
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/announcements.js
@@ -0,0 +1,181 @@
+import api from '../api';
+
+import { normalizeAnnouncement } from './importer/normalizer';
+
+export const ANNOUNCEMENTS_FETCH_REQUEST = 'ANNOUNCEMENTS_FETCH_REQUEST';
+export const ANNOUNCEMENTS_FETCH_SUCCESS = 'ANNOUNCEMENTS_FETCH_SUCCESS';
+export const ANNOUNCEMENTS_FETCH_FAIL    = 'ANNOUNCEMENTS_FETCH_FAIL';
+export const ANNOUNCEMENTS_UPDATE        = 'ANNOUNCEMENTS_UPDATE';
+export const ANNOUNCEMENTS_DELETE        = 'ANNOUNCEMENTS_DELETE';
+
+export const ANNOUNCEMENTS_DISMISS_REQUEST = 'ANNOUNCEMENTS_DISMISS_REQUEST';
+export const ANNOUNCEMENTS_DISMISS_SUCCESS = 'ANNOUNCEMENTS_DISMISS_SUCCESS';
+export const ANNOUNCEMENTS_DISMISS_FAIL    = 'ANNOUNCEMENTS_DISMISS_FAIL';
+
+export const ANNOUNCEMENTS_REACTION_ADD_REQUEST = 'ANNOUNCEMENTS_REACTION_ADD_REQUEST';
+export const ANNOUNCEMENTS_REACTION_ADD_SUCCESS = 'ANNOUNCEMENTS_REACTION_ADD_SUCCESS';
+export const ANNOUNCEMENTS_REACTION_ADD_FAIL    = 'ANNOUNCEMENTS_REACTION_ADD_FAIL';
+
+export const ANNOUNCEMENTS_REACTION_REMOVE_REQUEST = 'ANNOUNCEMENTS_REACTION_REMOVE_REQUEST';
+export const ANNOUNCEMENTS_REACTION_REMOVE_SUCCESS = 'ANNOUNCEMENTS_REACTION_REMOVE_SUCCESS';
+export const ANNOUNCEMENTS_REACTION_REMOVE_FAIL    = 'ANNOUNCEMENTS_REACTION_REMOVE_FAIL';
+
+export const ANNOUNCEMENTS_REACTION_UPDATE = 'ANNOUNCEMENTS_REACTION_UPDATE';
+
+export const ANNOUNCEMENTS_TOGGLE_SHOW = 'ANNOUNCEMENTS_TOGGLE_SHOW';
+
+const noOp = () => {};
+
+export const fetchAnnouncements = (done = noOp) => (dispatch, getState) => {
+  dispatch(fetchAnnouncementsRequest());
+
+  api(getState).get('/api/v1/announcements').then(response => {
+    dispatch(fetchAnnouncementsSuccess(response.data.map(x => normalizeAnnouncement(x))));
+  }).catch(error => {
+    dispatch(fetchAnnouncementsFail(error));
+  }).finally(() => {
+    done();
+  });
+};
+
+export const fetchAnnouncementsRequest = () => ({
+  type: ANNOUNCEMENTS_FETCH_REQUEST,
+  skipLoading: true,
+});
+
+export const fetchAnnouncementsSuccess = announcements => ({
+  type: ANNOUNCEMENTS_FETCH_SUCCESS,
+  announcements,
+  skipLoading: true,
+});
+
+export const fetchAnnouncementsFail= error => ({
+  type: ANNOUNCEMENTS_FETCH_FAIL,
+  error,
+  skipLoading: true,
+  skipAlert: true,
+});
+
+export const updateAnnouncements = announcement => ({
+  type: ANNOUNCEMENTS_UPDATE,
+  announcement: normalizeAnnouncement(announcement),
+});
+
+export const dismissAnnouncement = announcementId => (dispatch, getState) => {
+  dispatch(dismissAnnouncementRequest(announcementId));
+
+  api(getState).post(`/api/v1/announcements/${announcementId}/dismiss`).then(() => {
+    dispatch(dismissAnnouncementSuccess(announcementId));
+  }).catch(error => {
+    dispatch(dismissAnnouncementFail(announcementId, error));
+  });
+};
+
+export const dismissAnnouncementRequest = announcementId => ({
+  type: ANNOUNCEMENTS_DISMISS_REQUEST,
+  id: announcementId,
+});
+
+export const dismissAnnouncementSuccess = announcementId => ({
+  type: ANNOUNCEMENTS_DISMISS_SUCCESS,
+  id: announcementId,
+});
+
+export const dismissAnnouncementFail = (announcementId, error) => ({
+  type: ANNOUNCEMENTS_DISMISS_FAIL,
+  id: announcementId,
+  error,
+});
+
+export const addReaction = (announcementId, name) => (dispatch, getState) => {
+  const announcement = getState().getIn(['announcements', 'items']).find(x => x.get('id') === announcementId);
+
+  let alreadyAdded = false;
+
+  if (announcement) {
+    const reaction = announcement.get('reactions').find(x => x.get('name') === name);
+    if (reaction && reaction.get('me')) {
+      alreadyAdded = true;
+    }
+  }
+
+  if (!alreadyAdded) {
+    dispatch(addReactionRequest(announcementId, name, alreadyAdded));
+  }
+
+  api(getState).put(`/api/v1/announcements/${announcementId}/reactions/${encodeURIComponent(name)}`).then(() => {
+    dispatch(addReactionSuccess(announcementId, name, alreadyAdded));
+  }).catch(err => {
+    if (!alreadyAdded) {
+      dispatch(addReactionFail(announcementId, name, err));
+    }
+  });
+};
+
+export const addReactionRequest = (announcementId, name) => ({
+  type: ANNOUNCEMENTS_REACTION_ADD_REQUEST,
+  id: announcementId,
+  name,
+  skipLoading: true,
+});
+
+export const addReactionSuccess = (announcementId, name) => ({
+  type: ANNOUNCEMENTS_REACTION_ADD_SUCCESS,
+  id: announcementId,
+  name,
+  skipLoading: true,
+});
+
+export const addReactionFail = (announcementId, name, error) => ({
+  type: ANNOUNCEMENTS_REACTION_ADD_FAIL,
+  id: announcementId,
+  name,
+  error,
+  skipLoading: true,
+});
+
+export const removeReaction = (announcementId, name) => (dispatch, getState) => {
+  dispatch(removeReactionRequest(announcementId, name));
+
+  api(getState).delete(`/api/v1/announcements/${announcementId}/reactions/${encodeURIComponent(name)}`).then(() => {
+    dispatch(removeReactionSuccess(announcementId, name));
+  }).catch(err => {
+    dispatch(removeReactionFail(announcementId, name, err));
+  });
+};
+
+export const removeReactionRequest = (announcementId, name) => ({
+  type: ANNOUNCEMENTS_REACTION_REMOVE_REQUEST,
+  id: announcementId,
+  name,
+  skipLoading: true,
+});
+
+export const removeReactionSuccess = (announcementId, name) => ({
+  type: ANNOUNCEMENTS_REACTION_REMOVE_SUCCESS,
+  id: announcementId,
+  name,
+  skipLoading: true,
+});
+
+export const removeReactionFail = (announcementId, name, error) => ({
+  type: ANNOUNCEMENTS_REACTION_REMOVE_FAIL,
+  id: announcementId,
+  name,
+  error,
+  skipLoading: true,
+});
+
+export const updateReaction = reaction => ({
+  type: ANNOUNCEMENTS_REACTION_UPDATE,
+  reaction,
+});
+
+export const toggleShowAnnouncements = () => ({
+  type: ANNOUNCEMENTS_TOGGLE_SHOW,
+});
+
+export const deleteAnnouncement = id => ({
+  type: ANNOUNCEMENTS_DELETE,
+  id,
+});
diff --git a/app/javascript/flavours/blobfox/actions/app.ts b/app/javascript/flavours/blobfox/actions/app.ts
new file mode 100644
index 00000000000000..6fbfc07f68c931
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/app.ts
@@ -0,0 +1,9 @@
+import { createAction } from '@reduxjs/toolkit';
+
+import type { LayoutType } from '../is_mobile';
+
+interface ChangeLayoutPayload {
+  layout: LayoutType;
+}
+export const changeLayout =
+  createAction<ChangeLayoutPayload>('APP_LAYOUT_CHANGE');
diff --git a/app/javascript/flavours/blobfox/actions/blocks.js b/app/javascript/flavours/blobfox/actions/blocks.js
new file mode 100644
index 00000000000000..e293657ad36ef9
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/blocks.js
@@ -0,0 +1,100 @@
+import api, { getLinks } from '../api';
+
+import { fetchRelationships } from './accounts';
+import { importFetchedAccounts } from './importer';
+import { openModal } from './modal';
+
+export const BLOCKS_FETCH_REQUEST = 'BLOCKS_FETCH_REQUEST';
+export const BLOCKS_FETCH_SUCCESS = 'BLOCKS_FETCH_SUCCESS';
+export const BLOCKS_FETCH_FAIL    = 'BLOCKS_FETCH_FAIL';
+
+export const BLOCKS_EXPAND_REQUEST = 'BLOCKS_EXPAND_REQUEST';
+export const BLOCKS_EXPAND_SUCCESS = 'BLOCKS_EXPAND_SUCCESS';
+export const BLOCKS_EXPAND_FAIL    = 'BLOCKS_EXPAND_FAIL';
+
+export const BLOCKS_INIT_MODAL = 'BLOCKS_INIT_MODAL';
+
+export function fetchBlocks() {
+  return (dispatch, getState) => {
+    dispatch(fetchBlocksRequest());
+
+    api(getState).get('/api/v1/blocks').then(response => {
+      const next = getLinks(response).refs.find(link => link.rel === 'next');
+      dispatch(importFetchedAccounts(response.data));
+      dispatch(fetchBlocksSuccess(response.data, next ? next.uri : null));
+      dispatch(fetchRelationships(response.data.map(item => item.id)));
+    }).catch(error => dispatch(fetchBlocksFail(error)));
+  };
+}
+
+export function fetchBlocksRequest() {
+  return {
+    type: BLOCKS_FETCH_REQUEST,
+  };
+}
+
+export function fetchBlocksSuccess(accounts, next) {
+  return {
+    type: BLOCKS_FETCH_SUCCESS,
+    accounts,
+    next,
+  };
+}
+
+export function fetchBlocksFail(error) {
+  return {
+    type: BLOCKS_FETCH_FAIL,
+    error,
+  };
+}
+
+export function expandBlocks() {
+  return (dispatch, getState) => {
+    const url = getState().getIn(['user_lists', 'blocks', 'next']);
+
+    if (url === null) {
+      return;
+    }
+
+    dispatch(expandBlocksRequest());
+
+    api(getState).get(url).then(response => {
+      const next = getLinks(response).refs.find(link => link.rel === 'next');
+      dispatch(importFetchedAccounts(response.data));
+      dispatch(expandBlocksSuccess(response.data, next ? next.uri : null));
+      dispatch(fetchRelationships(response.data.map(item => item.id)));
+    }).catch(error => dispatch(expandBlocksFail(error)));
+  };
+}
+
+export function expandBlocksRequest() {
+  return {
+    type: BLOCKS_EXPAND_REQUEST,
+  };
+}
+
+export function expandBlocksSuccess(accounts, next) {
+  return {
+    type: BLOCKS_EXPAND_SUCCESS,
+    accounts,
+    next,
+  };
+}
+
+export function expandBlocksFail(error) {
+  return {
+    type: BLOCKS_EXPAND_FAIL,
+    error,
+  };
+}
+
+export function initBlockModal(account) {
+  return dispatch => {
+    dispatch({
+      type: BLOCKS_INIT_MODAL,
+      account,
+    });
+
+    dispatch(openModal({ modalType: 'BLOCK' }));
+  };
+}
diff --git a/app/javascript/flavours/blobfox/actions/bookmarks.js b/app/javascript/flavours/blobfox/actions/bookmarks.js
new file mode 100644
index 00000000000000..0b16f61e63635a
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/bookmarks.js
@@ -0,0 +1,91 @@
+import api, { getLinks } from '../api';
+
+import { importFetchedStatuses } from './importer';
+
+export const BOOKMARKED_STATUSES_FETCH_REQUEST = 'BOOKMARKED_STATUSES_FETCH_REQUEST';
+export const BOOKMARKED_STATUSES_FETCH_SUCCESS = 'BOOKMARKED_STATUSES_FETCH_SUCCESS';
+export const BOOKMARKED_STATUSES_FETCH_FAIL    = 'BOOKMARKED_STATUSES_FETCH_FAIL';
+
+export const BOOKMARKED_STATUSES_EXPAND_REQUEST = 'BOOKMARKED_STATUSES_EXPAND_REQUEST';
+export const BOOKMARKED_STATUSES_EXPAND_SUCCESS = 'BOOKMARKED_STATUSES_EXPAND_SUCCESS';
+export const BOOKMARKED_STATUSES_EXPAND_FAIL    = 'BOOKMARKED_STATUSES_EXPAND_FAIL';
+
+export function fetchBookmarkedStatuses() {
+  return (dispatch, getState) => {
+    if (getState().getIn(['status_lists', 'bookmarks', 'isLoading'])) {
+      return;
+    }
+
+    dispatch(fetchBookmarkedStatusesRequest());
+
+    api(getState).get('/api/v1/bookmarks').then(response => {
+      const next = getLinks(response).refs.find(link => link.rel === 'next');
+      dispatch(importFetchedStatuses(response.data));
+      dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
+    }).catch(error => {
+      dispatch(fetchBookmarkedStatusesFail(error));
+    });
+  };
+}
+
+export function fetchBookmarkedStatusesRequest() {
+  return {
+    type: BOOKMARKED_STATUSES_FETCH_REQUEST,
+  };
+}
+
+export function fetchBookmarkedStatusesSuccess(statuses, next) {
+  return {
+    type: BOOKMARKED_STATUSES_FETCH_SUCCESS,
+    statuses,
+    next,
+  };
+}
+
+export function fetchBookmarkedStatusesFail(error) {
+  return {
+    type: BOOKMARKED_STATUSES_FETCH_FAIL,
+    error,
+  };
+}
+
+export function expandBookmarkedStatuses() {
+  return (dispatch, getState) => {
+    const url = getState().getIn(['status_lists', 'bookmarks', 'next'], null);
+
+    if (url === null || getState().getIn(['status_lists', 'bookmarks', 'isLoading'])) {
+      return;
+    }
+
+    dispatch(expandBookmarkedStatusesRequest());
+
+    api(getState).get(url).then(response => {
+      const next = getLinks(response).refs.find(link => link.rel === 'next');
+      dispatch(importFetchedStatuses(response.data));
+      dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
+    }).catch(error => {
+      dispatch(expandBookmarkedStatusesFail(error));
+    });
+  };
+}
+
+export function expandBookmarkedStatusesRequest() {
+  return {
+    type: BOOKMARKED_STATUSES_EXPAND_REQUEST,
+  };
+}
+
+export function expandBookmarkedStatusesSuccess(statuses, next) {
+  return {
+    type: BOOKMARKED_STATUSES_EXPAND_SUCCESS,
+    statuses,
+    next,
+  };
+}
+
+export function expandBookmarkedStatusesFail(error) {
+  return {
+    type: BOOKMARKED_STATUSES_EXPAND_FAIL,
+    error,
+  };
+}
diff --git a/app/javascript/flavours/blobfox/actions/boosts.js b/app/javascript/flavours/blobfox/actions/boosts.js
new file mode 100644
index 00000000000000..1fc2e391e26827
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/boosts.js
@@ -0,0 +1,32 @@
+import { openModal } from './modal';
+
+export const BOOSTS_INIT_MODAL = 'BOOSTS_INIT_MODAL';
+export const BOOSTS_CHANGE_PRIVACY = 'BOOSTS_CHANGE_PRIVACY';
+
+export function initBoostModal(props) {
+  return (dispatch, getState) => {
+    const default_privacy = getState().getIn(['compose', 'default_privacy']);
+
+    const privacy = props.status.get('visibility') === 'private' ? 'private' : default_privacy;
+
+    dispatch({
+      type: BOOSTS_INIT_MODAL,
+      privacy,
+    });
+
+    dispatch(openModal({
+      modalType: 'BOOST',
+      modalProps: props,
+    }));
+  };
+}
+
+
+export function changeBoostPrivacy(privacy) {
+  return dispatch => {
+    dispatch({
+      type: BOOSTS_CHANGE_PRIVACY,
+      privacy,
+    });
+  };
+}
diff --git a/app/javascript/flavours/blobfox/actions/bundles.js b/app/javascript/flavours/blobfox/actions/bundles.js
new file mode 100644
index 00000000000000..ecc9c8f7d3ec22
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/bundles.js
@@ -0,0 +1,25 @@
+export const BUNDLE_FETCH_REQUEST = 'BUNDLE_FETCH_REQUEST';
+export const BUNDLE_FETCH_SUCCESS = 'BUNDLE_FETCH_SUCCESS';
+export const BUNDLE_FETCH_FAIL = 'BUNDLE_FETCH_FAIL';
+
+export function fetchBundleRequest(skipLoading) {
+  return {
+    type: BUNDLE_FETCH_REQUEST,
+    skipLoading,
+  };
+}
+
+export function fetchBundleSuccess(skipLoading) {
+  return {
+    type: BUNDLE_FETCH_SUCCESS,
+    skipLoading,
+  };
+}
+
+export function fetchBundleFail(error, skipLoading) {
+  return {
+    type: BUNDLE_FETCH_FAIL,
+    error,
+    skipLoading,
+  };
+}
diff --git a/app/javascript/flavours/blobfox/actions/columns.js b/app/javascript/flavours/blobfox/actions/columns.js
new file mode 100644
index 00000000000000..302c3f0f9b34cb
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/columns.js
@@ -0,0 +1,54 @@
+import { saveSettings } from './settings';
+
+export const COLUMN_ADD           = 'COLUMN_ADD';
+export const COLUMN_REMOVE        = 'COLUMN_REMOVE';
+export const COLUMN_MOVE          = 'COLUMN_MOVE';
+export const COLUMN_PARAMS_CHANGE = 'COLUMN_PARAMS_CHANGE';
+
+export function addColumn(id, params) {
+  return dispatch => {
+    dispatch({
+      type: COLUMN_ADD,
+      id,
+      params,
+    });
+
+    dispatch(saveSettings());
+  };
+}
+
+export function removeColumn(uuid) {
+  return dispatch => {
+    dispatch({
+      type: COLUMN_REMOVE,
+      uuid,
+    });
+
+    dispatch(saveSettings());
+  };
+}
+
+export function moveColumn(uuid, direction) {
+  return dispatch => {
+    dispatch({
+      type: COLUMN_MOVE,
+      uuid,
+      direction,
+    });
+
+    dispatch(saveSettings());
+  };
+}
+
+export function changeColumnParams(uuid, path, value) {
+  return dispatch => {
+    dispatch({
+      type: COLUMN_PARAMS_CHANGE,
+      uuid,
+      path,
+      value,
+    });
+
+    dispatch(saveSettings());
+  };
+}
diff --git a/app/javascript/flavours/blobfox/actions/compose.js b/app/javascript/flavours/blobfox/actions/compose.js
new file mode 100644
index 00000000000000..0c1cbdd052c800
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/compose.js
@@ -0,0 +1,840 @@
+import { defineMessages } from 'react-intl';
+
+import axios from 'axios';
+import { throttle } from 'lodash';
+
+import api from 'flavours/blobfox/api';
+import { search as emojiSearch } from 'flavours/blobfox/features/emoji/emoji_mart_search_light';
+import { tagHistory } from 'flavours/blobfox/settings';
+import { recoverHashtags } from 'flavours/blobfox/utils/hashtag';
+
+import { showAlert, showAlertForError } from './alerts';
+import { useEmoji } from './emojis';
+import { importFetchedAccounts, importFetchedStatus } from './importer';
+import { openModal } from './modal';
+import { updateTimeline } from './timelines';
+
+/** @type {AbortController | undefined} */
+let fetchComposeSuggestionsAccountsController;
+/** @type {AbortController | undefined} */
+let fetchComposeSuggestionsTagsController;
+
+export const COMPOSE_CHANGE          = 'COMPOSE_CHANGE';
+export const COMPOSE_CYCLE_ELEFRIEND = 'COMPOSE_CYCLE_ELEFRIEND';
+export const COMPOSE_SUBMIT_REQUEST  = 'COMPOSE_SUBMIT_REQUEST';
+export const COMPOSE_SUBMIT_SUCCESS  = 'COMPOSE_SUBMIT_SUCCESS';
+export const COMPOSE_SUBMIT_FAIL     = 'COMPOSE_SUBMIT_FAIL';
+export const COMPOSE_REPLY           = 'COMPOSE_REPLY';
+export const COMPOSE_REPLY_CANCEL    = 'COMPOSE_REPLY_CANCEL';
+export const COMPOSE_DIRECT          = 'COMPOSE_DIRECT';
+export const COMPOSE_MENTION         = 'COMPOSE_MENTION';
+export const COMPOSE_RESET           = 'COMPOSE_RESET';
+
+export const COMPOSE_UPLOAD_REQUEST    = 'COMPOSE_UPLOAD_REQUEST';
+export const COMPOSE_UPLOAD_SUCCESS    = 'COMPOSE_UPLOAD_SUCCESS';
+export const COMPOSE_UPLOAD_FAIL       = 'COMPOSE_UPLOAD_FAIL';
+export const COMPOSE_UPLOAD_PROGRESS   = 'COMPOSE_UPLOAD_PROGRESS';
+export const COMPOSE_UPLOAD_PROCESSING = 'COMPOSE_UPLOAD_PROCESSING';
+export const COMPOSE_UPLOAD_UNDO       = 'COMPOSE_UPLOAD_UNDO';
+
+export const THUMBNAIL_UPLOAD_REQUEST  = 'THUMBNAIL_UPLOAD_REQUEST';
+export const THUMBNAIL_UPLOAD_SUCCESS  = 'THUMBNAIL_UPLOAD_SUCCESS';
+export const THUMBNAIL_UPLOAD_FAIL     = 'THUMBNAIL_UPLOAD_FAIL';
+export const THUMBNAIL_UPLOAD_PROGRESS = 'THUMBNAIL_UPLOAD_PROGRESS';
+
+export const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR';
+export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY';
+export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT';
+export const COMPOSE_SUGGESTION_IGNORE = 'COMPOSE_SUGGESTION_IGNORE';
+export const COMPOSE_SUGGESTION_TAGS_UPDATE = 'COMPOSE_SUGGESTION_TAGS_UPDATE';
+
+export const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE';
+
+export const COMPOSE_MOUNT   = 'COMPOSE_MOUNT';
+export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT';
+
+export const COMPOSE_ADVANCED_OPTIONS_CHANGE = 'COMPOSE_ADVANCED_OPTIONS_CHANGE';
+export const COMPOSE_SENSITIVITY_CHANGE  = 'COMPOSE_SENSITIVITY_CHANGE';
+export const COMPOSE_SPOILERNESS_CHANGE  = 'COMPOSE_SPOILERNESS_CHANGE';
+export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
+export const COMPOSE_VISIBILITY_CHANGE   = 'COMPOSE_VISIBILITY_CHANGE';
+export const COMPOSE_LISTABILITY_CHANGE  = 'COMPOSE_LISTABILITY_CHANGE';
+export const COMPOSE_CONTENT_TYPE_CHANGE = 'COMPOSE_CONTENT_TYPE_CHANGE';
+export const COMPOSE_LANGUAGE_CHANGE     = 'COMPOSE_LANGUAGE_CHANGE';
+
+export const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT';
+
+export const COMPOSE_UPLOAD_CHANGE_REQUEST     = 'COMPOSE_UPLOAD_UPDATE_REQUEST';
+export const COMPOSE_UPLOAD_CHANGE_SUCCESS     = 'COMPOSE_UPLOAD_UPDATE_SUCCESS';
+export const COMPOSE_UPLOAD_CHANGE_FAIL        = 'COMPOSE_UPLOAD_UPDATE_FAIL';
+
+export const COMPOSE_DOODLE_SET        = 'COMPOSE_DOODLE_SET';
+
+export const COMPOSE_POLL_ADD             = 'COMPOSE_POLL_ADD';
+export const COMPOSE_POLL_REMOVE          = 'COMPOSE_POLL_REMOVE';
+export const COMPOSE_POLL_OPTION_ADD      = 'COMPOSE_POLL_OPTION_ADD';
+export const COMPOSE_POLL_OPTION_CHANGE   = 'COMPOSE_POLL_OPTION_CHANGE';
+export const COMPOSE_POLL_OPTION_REMOVE   = 'COMPOSE_POLL_OPTION_REMOVE';
+export const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE';
+
+export const INIT_MEDIA_EDIT_MODAL = 'INIT_MEDIA_EDIT_MODAL';
+
+export const COMPOSE_CHANGE_MEDIA_DESCRIPTION = 'COMPOSE_CHANGE_MEDIA_DESCRIPTION';
+export const COMPOSE_CHANGE_MEDIA_FOCUS       = 'COMPOSE_CHANGE_MEDIA_FOCUS';
+
+export const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS';
+export const COMPOSE_FOCUS = 'COMPOSE_FOCUS';
+
+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.' },
+  open: { id: 'compose.published.open', defaultMessage: 'Open' },
+  published: { id: 'compose.published.body', defaultMessage: 'Post published.' },
+  saved: { id: 'compose.saved.body', defaultMessage: 'Post saved.' },
+});
+
+export const ensureComposeIsVisible = (getState, routerHistory) => {
+  if (!getState().getIn(['compose', 'mounted'])) {
+    routerHistory.push('/publish');
+  }
+};
+
+export function setComposeToStatus(status, text, spoiler_text, content_type) {
+  return{
+    type: COMPOSE_SET_STATUS,
+    status,
+    text,
+    spoiler_text,
+    content_type,
+  };
+}
+
+export function changeCompose(text) {
+  return {
+    type: COMPOSE_CHANGE,
+    text: text,
+  };
+}
+
+export function cycleElefriendCompose() {
+  return {
+    type: COMPOSE_CYCLE_ELEFRIEND,
+  };
+}
+
+export function replyCompose(status, routerHistory) {
+  return (dispatch, getState) => {
+    const prependCWRe = getState().getIn(['local_settings', 'prepend_cw_re']);
+    dispatch({
+      type: COMPOSE_REPLY,
+      status: status,
+      prependCWRe: prependCWRe,
+    });
+
+    ensureComposeIsVisible(getState, routerHistory);
+  };
+}
+
+export function cancelReplyCompose() {
+  return {
+    type: COMPOSE_REPLY_CANCEL,
+  };
+}
+
+export function resetCompose() {
+  return {
+    type: COMPOSE_RESET,
+  };
+}
+
+export const focusCompose = (routerHistory, defaultText) => dispatch => {
+  dispatch({
+    type: COMPOSE_FOCUS,
+    defaultText,
+  });
+
+  ensureComposeIsVisible(routerHistory);
+};
+
+export function mentionCompose(account, routerHistory) {
+  return (dispatch, getState) => {
+    dispatch({
+      type: COMPOSE_MENTION,
+      account: account,
+    });
+
+    ensureComposeIsVisible(getState, routerHistory);
+  };
+}
+
+export function directCompose(account, routerHistory) {
+  return (dispatch, getState) => {
+    dispatch({
+      type: COMPOSE_DIRECT,
+      account: account,
+    });
+
+    ensureComposeIsVisible(getState, routerHistory);
+  };
+}
+
+export function submitCompose(routerHistory) {
+  return function (dispatch, getState) {
+    let status     = getState().getIn(['compose', 'text'], '');
+    const media    = getState().getIn(['compose', 'media_attachments']);
+    const statusId = getState().getIn(['compose', 'id'], null);
+    const spoilers = getState().getIn(['compose', 'spoiler']) || getState().getIn(['local_settings', 'always_show_spoilers_field']);
+    let spoilerText = spoilers ? getState().getIn(['compose', 'spoiler_text'], '') : '';
+
+    if ((!status || !status.length) && media.size === 0) {
+      return;
+    }
+
+    if (getState().getIn(['compose', 'advanced_options', 'do_not_federate'])) {
+      status = status + ' 👁️';
+    }
+
+    dispatch(submitComposeRequest());
+
+    // If we're editing a post with media attachments, those have not
+    // necessarily been changed on the server. Do it now in the same
+    // API call.
+    let media_attributes;
+    if (statusId !== null) {
+      media_attributes = media.map(item => {
+        let focus;
+
+        if (item.getIn(['meta', 'focus'])) {
+          focus = `${item.getIn(['meta', 'focus', 'x']).toFixed(2)},${item.getIn(['meta', 'focus', 'y']).toFixed(2)}`;
+        }
+
+        return {
+          id: item.get('id'),
+          description: item.get('description'),
+          focus,
+        };
+      });
+    }
+
+    api(getState).request({
+      url: statusId === null ? '/api/v1/statuses' : `/api/v1/statuses/${statusId}`,
+      method: statusId === null ? 'post' : 'put',
+      data: {
+        status,
+        content_type: getState().getIn(['compose', 'content_type']),
+        in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
+        media_ids: media.map(item => item.get('id')),
+        media_attributes,
+        sensitive: getState().getIn(['compose', 'sensitive']) || (spoilerText.length > 0 && media.size !== 0),
+        spoiler_text: spoilerText,
+        visibility: getState().getIn(['compose', 'privacy']),
+        poll: getState().getIn(['compose', 'poll'], null),
+        language: getState().getIn(['compose', 'language']),
+      },
+      headers: {
+        'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
+      },
+    }).then(function (response) {
+      if (routerHistory
+          && (routerHistory.location.pathname === '/publish' || routerHistory.location.pathname === '/statuses/new')
+          && window.history.state
+          && !getState().getIn(['compose', 'advanced_options', 'threaded_mode'])) {
+        routerHistory.goBack();
+      }
+
+      dispatch(insertIntoTagHistory(response.data.tags, status));
+      dispatch(submitComposeSuccess({ ...response.data }));
+
+      //  If the response has no data then we can't do anything else.
+      if (!response.data) {
+        return;
+      }
+
+      // To make the app more responsive, immediately push the status
+      // into the columns
+      const insertIfOnline = timelineId => {
+        const timeline = getState().getIn(['timelines', timelineId]);
+
+        if (timeline && timeline.get('items').size > 0 && timeline.getIn(['items', 0]) !== null && timeline.get('online')) {
+          dispatch(updateTimeline(timelineId, { ...response.data }));
+        }
+      };
+
+      if (statusId) {
+        dispatch(importFetchedStatus({ ...response.data }));
+      }
+
+      if (statusId === null) {
+        insertIfOnline('home');
+      }
+
+      if (statusId === null && response.data.in_reply_to_id === null && response.data.visibility === 'public') {
+        insertIfOnline('community');
+        if (!response.data.local_only) {
+          insertIfOnline('public');
+        }
+      } else if (statusId === null && response.data.visibility === 'direct') {
+        insertIfOnline('direct');
+      }
+
+      dispatch(showAlert({
+        message: statusId === null ? messages.published : messages.saved,
+        action: messages.open,
+        dismissAfter: 10000,
+        onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`),
+      }));
+    }).catch(function (error) {
+      dispatch(submitComposeFail(error));
+    });
+  };
+}
+
+export function submitComposeRequest() {
+  return {
+    type: COMPOSE_SUBMIT_REQUEST,
+  };
+}
+
+export function submitComposeSuccess(status) {
+  return {
+    type: COMPOSE_SUBMIT_SUCCESS,
+    status: status,
+  };
+}
+
+export function submitComposeFail(error) {
+  return {
+    type: COMPOSE_SUBMIT_FAIL,
+    error: error,
+  };
+}
+
+export function doodleSet(options) {
+  return {
+    type: COMPOSE_DOODLE_SET,
+    options: options,
+  };
+}
+
+export function uploadCompose(files) {
+  return function (dispatch, getState) {
+    const uploadLimit = 4;
+    const media = getState().getIn(['compose', 'media_attachments']);
+    const pending = getState().getIn(['compose', 'pending_media_attachments']);
+    const progress = new Array(files.length).fill(0);
+
+    let total = Array.from(files).reduce((a, v) => a + v.size, 0);
+
+    if (files.length + media.size + pending > uploadLimit) {
+      dispatch(showAlert({ message: messages.uploadErrorLimit }));
+      return;
+    }
+
+    if (getState().getIn(['compose', 'poll'])) {
+      dispatch(showAlert({ message: messages.uploadErrorPoll }));
+      return;
+    }
+
+    dispatch(uploadComposeRequest());
+
+    for (const [i, file] of Array.from(files).entries()) {
+      if (media.size + i > 3) break;
+
+      const data = new FormData();
+      data.append('file', file);
+
+      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());
+
+          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)));
+          };
+
+          poll();
+        }
+        
+      }).catch(error => dispatch(uploadComposeFail(error)));
+    }
+  };
+}
+
+export const uploadComposeProcessing = () => ({
+  type: COMPOSE_UPLOAD_PROCESSING,
+});
+
+export const uploadThumbnail = (id, file) => (dispatch, getState) => {
+  dispatch(uploadThumbnailRequest());
+
+  const total = file.size;
+  const data = new FormData();
+
+  data.append('thumbnail', file);
+
+  api(getState).put(`/api/v1/media/${id}`, data, {
+    onUploadProgress: ({ loaded }) => {
+      dispatch(uploadThumbnailProgress(loaded, total));
+    },
+  }).then(({ data }) => {
+    dispatch(uploadThumbnailSuccess(data));
+  }).catch(error => {
+    dispatch(uploadThumbnailFail(id, error));
+  });
+};
+
+export const uploadThumbnailRequest = () => ({
+  type: THUMBNAIL_UPLOAD_REQUEST,
+  skipLoading: true,
+});
+
+export const uploadThumbnailProgress = (loaded, total) => ({
+  type: THUMBNAIL_UPLOAD_PROGRESS,
+  loaded,
+  total,
+  skipLoading: true,
+});
+
+export const uploadThumbnailSuccess = media => ({
+  type: THUMBNAIL_UPLOAD_SUCCESS,
+  media,
+  skipLoading: true,
+});
+
+export const uploadThumbnailFail = error => ({
+  type: THUMBNAIL_UPLOAD_FAIL,
+  error,
+  skipLoading: true,
+});
+
+export function initMediaEditModal(id) {
+  return dispatch => {
+    dispatch({
+      type: INIT_MEDIA_EDIT_MODAL,
+      id,
+    });
+
+    dispatch(openModal({
+      modalType: 'FOCAL_POINT',
+      modalProps: { id },
+    }));
+  };
+}
+
+export function onChangeMediaDescription(description) {
+  return {
+    type: COMPOSE_CHANGE_MEDIA_DESCRIPTION,
+    description,
+  };
+}
+
+export function onChangeMediaFocus(focusX, focusY) {
+  return {
+    type: COMPOSE_CHANGE_MEDIA_FOCUS,
+    focusX,
+    focusY,
+  };
+}
+
+export function changeUploadCompose(id, params) {
+  return (dispatch, getState) => {
+    dispatch(changeUploadComposeRequest());
+
+    let media = getState().getIn(['compose', 'media_attachments']).find((item) => item.get('id') === id);
+
+    // 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 };
+
+      if (focus) {
+        const [x, y] = focus.split(',');
+        data.meta = { focus: { x: parseFloat(x), y: parseFloat(y) } };
+      }
+
+      dispatch(changeUploadComposeSuccess(data, true));
+    } else {
+      api(getState).put(`/api/v1/media/${id}`, params).then(response => {
+        dispatch(changeUploadComposeSuccess(response.data, false));
+      }).catch(error => {
+        dispatch(changeUploadComposeFail(id, error));
+      });
+    }
+  };
+}
+
+export function changeUploadComposeRequest() {
+  return {
+    type: COMPOSE_UPLOAD_CHANGE_REQUEST,
+    skipLoading: true,
+  };
+}
+
+export function changeUploadComposeSuccess(media, attached) {
+  return {
+    type: COMPOSE_UPLOAD_CHANGE_SUCCESS,
+    media: media,
+    attached: attached,
+    skipLoading: true,
+  };
+}
+
+export function changeUploadComposeFail(error) {
+  return {
+    type: COMPOSE_UPLOAD_CHANGE_FAIL,
+    error: error,
+    skipLoading: true,
+  };
+}
+
+export function uploadComposeRequest() {
+  return {
+    type: COMPOSE_UPLOAD_REQUEST,
+    skipLoading: true,
+  };
+}
+
+export function uploadComposeProgress(loaded, total) {
+  return {
+    type: COMPOSE_UPLOAD_PROGRESS,
+    loaded: loaded,
+    total: total,
+  };
+}
+
+export function uploadComposeSuccess(media, file) {
+  return {
+    type: COMPOSE_UPLOAD_SUCCESS,
+    media: media,
+    file: file,
+    skipLoading: true,
+  };
+}
+
+export function uploadComposeFail(error) {
+  return {
+    type: COMPOSE_UPLOAD_FAIL,
+    error: error,
+    skipLoading: true,
+  };
+}
+
+export function undoUploadCompose(media_id) {
+  return {
+    type: COMPOSE_UPLOAD_UNDO,
+    media_id: media_id,
+  };
+}
+
+export function clearComposeSuggestions() {
+  if (fetchComposeSuggestionsAccountsController) {
+    fetchComposeSuggestionsAccountsController.abort();
+  }
+  return {
+    type: COMPOSE_SUGGESTIONS_CLEAR,
+  };
+}
+
+const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) => {
+  if (fetchComposeSuggestionsAccountsController) {
+    fetchComposeSuggestionsAccountsController.abort();
+  }
+
+  fetchComposeSuggestionsAccountsController = new AbortController();
+
+  api(getState).get('/api/v1/accounts/search', {
+    signal: fetchComposeSuggestionsAccountsController.signal,
+
+    params: {
+      q: token.slice(1),
+      resolve: false,
+      limit: 4,
+    },
+  }).then(response => {
+    dispatch(importFetchedAccounts(response.data));
+    dispatch(readyComposeSuggestionsAccounts(token, response.data));
+  }).catch(error => {
+    if (!axios.isCancel(error)) {
+      dispatch(showAlertForError(error));
+    }
+  }).finally(() => {
+    fetchComposeSuggestionsAccountsController = undefined;
+  });
+}, 200, { leading: true, trailing: true });
+
+const fetchComposeSuggestionsEmojis = (dispatch, getState, token) => {
+  const results = emojiSearch(token.replace(':', ''), { maxResults: 5 });
+  dispatch(readyComposeSuggestionsEmojis(token, results));
+};
+
+const fetchComposeSuggestionsTags = throttle((dispatch, getState, token) => {
+  if (fetchComposeSuggestionsTagsController) {
+    fetchComposeSuggestionsTagsController.abort();
+  }
+
+  dispatch(updateSuggestionTags(token));
+
+  fetchComposeSuggestionsTagsController = new AbortController();
+
+  api(getState).get('/api/v2/search', {
+    signal: fetchComposeSuggestionsTagsController.signal,
+
+    params: {
+      type: 'hashtags',
+      q: token.slice(1),
+      resolve: false,
+      limit: 4,
+    },
+  }).then(({ data }) => {
+    dispatch(readyComposeSuggestionsTags(token, data.hashtags));
+  }).catch(error => {
+    if (!axios.isCancel(error)) {
+      dispatch(showAlertForError(error));
+    }
+  }).finally(() => {
+    fetchComposeSuggestionsTagsController = undefined;
+  });
+}, 200, { leading: true, trailing: true });
+
+export function fetchComposeSuggestions(token) {
+  return (dispatch, getState) => {
+    switch (token[0]) {
+    case ':':
+      fetchComposeSuggestionsEmojis(dispatch, getState, token);
+      break;
+    case '#':
+      fetchComposeSuggestionsTags(dispatch, getState, token);
+      break;
+    default:
+      fetchComposeSuggestionsAccounts(dispatch, getState, token);
+      break;
+    }
+  };
+}
+
+export function readyComposeSuggestionsEmojis(token, emojis) {
+  return {
+    type: COMPOSE_SUGGESTIONS_READY,
+    token,
+    emojis,
+  };
+}
+
+export function readyComposeSuggestionsAccounts(token, accounts) {
+  return {
+    type: COMPOSE_SUGGESTIONS_READY,
+    token,
+    accounts,
+  };
+}
+
+export const readyComposeSuggestionsTags = (token, tags) => ({
+  type: COMPOSE_SUGGESTIONS_READY,
+  token,
+  tags,
+});
+
+export function selectComposeSuggestion(position, token, suggestion, path) {
+  return (dispatch, getState) => {
+    let completion;
+    if (suggestion.type === 'emoji') {
+      completion = suggestion.native || suggestion.colons;
+
+      dispatch(useEmoji(suggestion));
+    } else if (suggestion.type === 'hashtag') {
+      completion = `#${suggestion.name}`;
+    } else if (suggestion.type === 'account') {
+      completion = '@' + getState().getIn(['accounts', suggestion.id, 'acct']);
+    }
+
+    // We don't want to replace hashtags that vary only in case due to accessibility, but we need to fire off an event so that
+    // the suggestions are dismissed and the cursor moves forward.
+    if (suggestion.type !== 'hashtag' || token.slice(1).localeCompare(suggestion.name, undefined, { sensitivity: 'accent' }) !== 0) {
+      dispatch({
+        type: COMPOSE_SUGGESTION_SELECT,
+        position,
+        token,
+        completion,
+        path,
+      });
+    } else {
+      dispatch({
+        type: COMPOSE_SUGGESTION_IGNORE,
+        position,
+        token,
+        completion,
+        path,
+      });
+    }
+  };
+}
+
+export function updateSuggestionTags(token) {
+  return {
+    type: COMPOSE_SUGGESTION_TAGS_UPDATE,
+    token,
+  };
+}
+
+export function updateTagHistory(tags) {
+  return {
+    type: COMPOSE_TAG_HISTORY_UPDATE,
+    tags,
+  };
+}
+
+export function hydrateCompose() {
+  return (dispatch, getState) => {
+    const me = getState().getIn(['meta', 'me']);
+    const history = tagHistory.get(me);
+
+    if (history !== null) {
+      dispatch(updateTagHistory(history));
+    }
+  };
+}
+
+function insertIntoTagHistory(recognizedTags, text) {
+  return (dispatch, getState) => {
+    const state = getState();
+    const oldHistory = state.getIn(['compose', 'tagHistory']);
+    const me = state.getIn(['meta', 'me']);
+    const names = recoverHashtags(recognizedTags, text);
+    const intersectedOldHistory = oldHistory.filter(name => names.findIndex(newName => newName.toLowerCase() === name.toLowerCase()) === -1);
+
+    names.push(...intersectedOldHistory.toJS());
+
+    const newHistory = names.slice(0, 1000);
+
+    tagHistory.set(me, newHistory);
+    dispatch(updateTagHistory(newHistory));
+  };
+}
+
+export function mountCompose() {
+  return {
+    type: COMPOSE_MOUNT,
+  };
+}
+
+export function unmountCompose() {
+  return {
+    type: COMPOSE_UNMOUNT,
+  };
+}
+
+export function changeComposeAdvancedOption(option, value) {
+  return {
+    option,
+    type: COMPOSE_ADVANCED_OPTIONS_CHANGE,
+    value,
+  };
+}
+
+export function changeComposeSensitivity() {
+  return {
+    type: COMPOSE_SENSITIVITY_CHANGE,
+  };
+}
+
+export const changeComposeLanguage = language => ({
+  type: COMPOSE_LANGUAGE_CHANGE,
+  language,
+});
+
+export function changeComposeSpoilerness() {
+  return {
+    type: COMPOSE_SPOILERNESS_CHANGE,
+  };
+}
+
+export function changeComposeSpoilerText(text) {
+  return {
+    type: COMPOSE_SPOILER_TEXT_CHANGE,
+    text,
+  };
+}
+
+export function changeComposeVisibility(value) {
+  return {
+    type: COMPOSE_VISIBILITY_CHANGE,
+    value,
+  };
+}
+
+export function changeComposeContentType(value) {
+  return {
+    type: COMPOSE_CONTENT_TYPE_CHANGE,
+    value,
+  };
+}
+
+export function insertEmojiCompose(position, emoji) {
+  return {
+    type: COMPOSE_EMOJI_INSERT,
+    position,
+    emoji,
+  };
+}
+
+export function addPoll() {
+  return {
+    type: COMPOSE_POLL_ADD,
+  };
+}
+
+export function removePoll() {
+  return {
+    type: COMPOSE_POLL_REMOVE,
+  };
+}
+
+export function addPollOption(title) {
+  return {
+    type: COMPOSE_POLL_OPTION_ADD,
+    title,
+  };
+}
+
+export function changePollOption(index, title) {
+  return {
+    type: COMPOSE_POLL_OPTION_CHANGE,
+    index,
+    title,
+  };
+}
+
+export function removePollOption(index) {
+  return {
+    type: COMPOSE_POLL_OPTION_REMOVE,
+    index,
+  };
+}
+
+export function changePollSettings(expiresIn, isMultiple) {
+  return {
+    type: COMPOSE_POLL_SETTINGS_CHANGE,
+    expiresIn,
+    isMultiple,
+  };
+}
diff --git a/app/javascript/flavours/blobfox/actions/conversations.js b/app/javascript/flavours/blobfox/actions/conversations.js
new file mode 100644
index 00000000000000..8c4c4529fb7e8b
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/conversations.js
@@ -0,0 +1,113 @@
+import api, { getLinks } from '../api';
+
+import {
+  importFetchedAccounts,
+  importFetchedStatuses,
+  importFetchedStatus,
+} from './importer';
+
+export const CONVERSATIONS_MOUNT   = 'CONVERSATIONS_MOUNT';
+export const CONVERSATIONS_UNMOUNT = 'CONVERSATIONS_UNMOUNT';
+
+export const CONVERSATIONS_FETCH_REQUEST = 'CONVERSATIONS_FETCH_REQUEST';
+export const CONVERSATIONS_FETCH_SUCCESS = 'CONVERSATIONS_FETCH_SUCCESS';
+export const CONVERSATIONS_FETCH_FAIL    = 'CONVERSATIONS_FETCH_FAIL';
+export const CONVERSATIONS_UPDATE        = 'CONVERSATIONS_UPDATE';
+
+export const CONVERSATIONS_READ = 'CONVERSATIONS_READ';
+
+export const CONVERSATIONS_DELETE_REQUEST = 'CONVERSATIONS_DELETE_REQUEST';
+export const CONVERSATIONS_DELETE_SUCCESS = 'CONVERSATIONS_DELETE_SUCCESS';
+export const CONVERSATIONS_DELETE_FAIL    = 'CONVERSATIONS_DELETE_FAIL';
+
+export const mountConversations = () => ({
+  type: CONVERSATIONS_MOUNT,
+});
+
+export const unmountConversations = () => ({
+  type: CONVERSATIONS_UNMOUNT,
+});
+
+export const markConversationRead = conversationId => (dispatch, getState) => {
+  dispatch({
+    type: CONVERSATIONS_READ,
+    id: conversationId,
+  });
+
+  api(getState).post(`/api/v1/conversations/${conversationId}/read`);
+};
+
+export const expandConversations = ({ maxId } = {}) => (dispatch, getState) => {
+  dispatch(expandConversationsRequest());
+
+  const params = { max_id: maxId };
+
+  if (!maxId) {
+    params.since_id = getState().getIn(['conversations', 'items', 0, 'last_status']);
+  }
+
+  const isLoadingRecent = !!params.since_id;
+
+  api(getState).get('/api/v1/conversations', { params })
+    .then(response => {
+      const next = getLinks(response).refs.find(link => link.rel === 'next');
+
+      dispatch(importFetchedAccounts(response.data.reduce((aggr, item) => aggr.concat(item.accounts), [])));
+      dispatch(importFetchedStatuses(response.data.map(item => item.last_status).filter(x => !!x)));
+      dispatch(expandConversationsSuccess(response.data, next ? next.uri : null, isLoadingRecent));
+    })
+    .catch(err => dispatch(expandConversationsFail(err)));
+};
+
+export const expandConversationsRequest = () => ({
+  type: CONVERSATIONS_FETCH_REQUEST,
+});
+
+export const expandConversationsSuccess = (conversations, next, isLoadingRecent) => ({
+  type: CONVERSATIONS_FETCH_SUCCESS,
+  conversations,
+  next,
+  isLoadingRecent,
+});
+
+export const expandConversationsFail = error => ({
+  type: CONVERSATIONS_FETCH_FAIL,
+  error,
+});
+
+export const updateConversations = conversation => dispatch => {
+  dispatch(importFetchedAccounts(conversation.accounts));
+
+  if (conversation.last_status) {
+    dispatch(importFetchedStatus(conversation.last_status));
+  }
+
+  dispatch({
+    type: CONVERSATIONS_UPDATE,
+    conversation,
+  });
+};
+
+export const deleteConversation = conversationId => (dispatch, getState) => {
+  dispatch(deleteConversationRequest(conversationId));
+
+  api(getState).delete(`/api/v1/conversations/${conversationId}`)
+    .then(() => dispatch(deleteConversationSuccess(conversationId)))
+    .catch(error => dispatch(deleteConversationFail(conversationId, error)));
+};
+
+export const deleteConversationRequest = id => ({
+  type: CONVERSATIONS_DELETE_REQUEST,
+  id,
+});
+
+export const deleteConversationSuccess = id => ({
+  type: CONVERSATIONS_DELETE_SUCCESS,
+  id,
+});
+
+export const deleteConversationFail = (id, error) => ({
+  type: CONVERSATIONS_DELETE_FAIL,
+  id,
+  error,
+});
diff --git a/app/javascript/flavours/blobfox/actions/custom_emojis.js b/app/javascript/flavours/blobfox/actions/custom_emojis.js
new file mode 100644
index 00000000000000..9ec8156b170a21
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/custom_emojis.js
@@ -0,0 +1,40 @@
+import api from '../api';
+
+export const CUSTOM_EMOJIS_FETCH_REQUEST = 'CUSTOM_EMOJIS_FETCH_REQUEST';
+export const CUSTOM_EMOJIS_FETCH_SUCCESS = 'CUSTOM_EMOJIS_FETCH_SUCCESS';
+export const CUSTOM_EMOJIS_FETCH_FAIL = 'CUSTOM_EMOJIS_FETCH_FAIL';
+
+export function fetchCustomEmojis() {
+  return (dispatch, getState) => {
+    dispatch(fetchCustomEmojisRequest());
+
+    api(getState).get('/api/v1/custom_emojis').then(response => {
+      dispatch(fetchCustomEmojisSuccess(response.data));
+    }).catch(error => {
+      dispatch(fetchCustomEmojisFail(error));
+    });
+  };
+}
+
+export function fetchCustomEmojisRequest() {
+  return {
+    type: CUSTOM_EMOJIS_FETCH_REQUEST,
+    skipLoading: true,
+  };
+}
+
+export function fetchCustomEmojisSuccess(custom_emojis) {
+  return {
+    type: CUSTOM_EMOJIS_FETCH_SUCCESS,
+    custom_emojis,
+    skipLoading: true,
+  };
+}
+
+export function fetchCustomEmojisFail(error) {
+  return {
+    type: CUSTOM_EMOJIS_FETCH_FAIL,
+    error,
+    skipLoading: true,
+  };
+}
diff --git a/app/javascript/flavours/blobfox/actions/directory.js b/app/javascript/flavours/blobfox/actions/directory.js
new file mode 100644
index 00000000000000..cda63f2b5a43eb
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/directory.js
@@ -0,0 +1,62 @@
+import api from '../api';
+
+import { fetchRelationships } from './accounts';
+import { importFetchedAccounts } from './importer';
+
+export const DIRECTORY_FETCH_REQUEST = 'DIRECTORY_FETCH_REQUEST';
+export const DIRECTORY_FETCH_SUCCESS = 'DIRECTORY_FETCH_SUCCESS';
+export const DIRECTORY_FETCH_FAIL    = 'DIRECTORY_FETCH_FAIL';
+
+export const DIRECTORY_EXPAND_REQUEST = 'DIRECTORY_EXPAND_REQUEST';
+export const DIRECTORY_EXPAND_SUCCESS = 'DIRECTORY_EXPAND_SUCCESS';
+export const DIRECTORY_EXPAND_FAIL    = 'DIRECTORY_EXPAND_FAIL';
+
+export const fetchDirectory = params => (dispatch, getState) => {
+  dispatch(fetchDirectoryRequest());
+
+  api(getState).get('/api/v1/directory', { params: { ...params, limit: 20 } }).then(({ data }) => {
+    dispatch(importFetchedAccounts(data));
+    dispatch(fetchDirectorySuccess(data));
+    dispatch(fetchRelationships(data.map(x => x.id)));
+  }).catch(error => dispatch(fetchDirectoryFail(error)));
+};
+
+export const fetchDirectoryRequest = () => ({
+  type: DIRECTORY_FETCH_REQUEST,
+});
+
+export const fetchDirectorySuccess = accounts => ({
+  type: DIRECTORY_FETCH_SUCCESS,
+  accounts,
+});
+
+export const fetchDirectoryFail = error => ({
+  type: DIRECTORY_FETCH_FAIL,
+  error,
+});
+
+export const expandDirectory = params => (dispatch, getState) => {
+  dispatch(expandDirectoryRequest());
+
+  const loadedItems = getState().getIn(['user_lists', 'directory', 'items']).size;
+
+  api(getState).get('/api/v1/directory', { params: { ...params, offset: loadedItems, limit: 20 } }).then(({ data }) => {
+    dispatch(importFetchedAccounts(data));
+    dispatch(expandDirectorySuccess(data));
+    dispatch(fetchRelationships(data.map(x => x.id)));
+  }).catch(error => dispatch(expandDirectoryFail(error)));
+};
+
+export const expandDirectoryRequest = () => ({
+  type: DIRECTORY_EXPAND_REQUEST,
+});
+
+export const expandDirectorySuccess = accounts => ({
+  type: DIRECTORY_EXPAND_SUCCESS,
+  accounts,
+});
+
+export const expandDirectoryFail = error => ({
+  type: DIRECTORY_EXPAND_FAIL,
+  error,
+});
diff --git a/app/javascript/flavours/blobfox/actions/domain_blocks.js b/app/javascript/flavours/blobfox/actions/domain_blocks.js
new file mode 100644
index 00000000000000..718002613f4053
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/domain_blocks.js
@@ -0,0 +1,152 @@
+import api, { getLinks } from '../api';
+
+import { blockDomainSuccess, unblockDomainSuccess } from "./domain_blocks_typed";
+
+export * from "./domain_blocks_typed";
+
+export const DOMAIN_BLOCK_REQUEST = 'DOMAIN_BLOCK_REQUEST';
+export const DOMAIN_BLOCK_FAIL    = 'DOMAIN_BLOCK_FAIL';
+
+export const DOMAIN_UNBLOCK_REQUEST = 'DOMAIN_UNBLOCK_REQUEST';
+export const DOMAIN_UNBLOCK_FAIL    = 'DOMAIN_UNBLOCK_FAIL';
+
+export const DOMAIN_BLOCKS_FETCH_REQUEST = 'DOMAIN_BLOCKS_FETCH_REQUEST';
+export const DOMAIN_BLOCKS_FETCH_SUCCESS = 'DOMAIN_BLOCKS_FETCH_SUCCESS';
+export const DOMAIN_BLOCKS_FETCH_FAIL    = 'DOMAIN_BLOCKS_FETCH_FAIL';
+
+export const DOMAIN_BLOCKS_EXPAND_REQUEST = 'DOMAIN_BLOCKS_EXPAND_REQUEST';
+export const DOMAIN_BLOCKS_EXPAND_SUCCESS = 'DOMAIN_BLOCKS_EXPAND_SUCCESS';
+export const DOMAIN_BLOCKS_EXPAND_FAIL    = 'DOMAIN_BLOCKS_EXPAND_FAIL';
+
+export function blockDomain(domain) {
+  return (dispatch, getState) => {
+    dispatch(blockDomainRequest(domain));
+
+    api(getState).post('/api/v1/domain_blocks', { domain }).then(() => {
+      const at_domain = '@' + domain;
+      const accounts = getState().get('accounts').filter(item => item.get('acct').endsWith(at_domain)).valueSeq().map(item => item.get('id'));
+
+      dispatch(blockDomainSuccess({ domain, accounts }));
+    }).catch(err => {
+      dispatch(blockDomainFail(domain, err));
+    });
+  };
+}
+
+export function blockDomainRequest(domain) {
+  return {
+    type: DOMAIN_BLOCK_REQUEST,
+    domain,
+  };
+}
+
+export function blockDomainFail(domain, error) {
+  return {
+    type: DOMAIN_BLOCK_FAIL,
+    domain,
+    error,
+  };
+}
+
+export function unblockDomain(domain) {
+  return (dispatch, getState) => {
+    dispatch(unblockDomainRequest(domain));
+
+    api(getState).delete('/api/v1/domain_blocks', { params: { domain } }).then(() => {
+      const at_domain = '@' + domain;
+      const accounts = getState().get('accounts').filter(item => item.get('acct').endsWith(at_domain)).valueSeq().map(item => item.get('id'));
+      dispatch(unblockDomainSuccess({ domain, accounts }));
+    }).catch(err => {
+      dispatch(unblockDomainFail(domain, err));
+    });
+  };
+}
+
+export function unblockDomainRequest(domain) {
+  return {
+    type: DOMAIN_UNBLOCK_REQUEST,
+    domain,
+  };
+}
+
+export function unblockDomainFail(domain, error) {
+  return {
+    type: DOMAIN_UNBLOCK_FAIL,
+    domain,
+    error,
+  };
+}
+
+export function fetchDomainBlocks() {
+  return (dispatch, getState) => {
+    dispatch(fetchDomainBlocksRequest());
+
+    api(getState).get('/api/v1/domain_blocks').then(response => {
+      const next = getLinks(response).refs.find(link => link.rel === 'next');
+      dispatch(fetchDomainBlocksSuccess(response.data, next ? next.uri : null));
+    }).catch(err => {
+      dispatch(fetchDomainBlocksFail(err));
+    });
+  };
+}
+
+export function fetchDomainBlocksRequest() {
+  return {
+    type: DOMAIN_BLOCKS_FETCH_REQUEST,
+  };
+}
+
+export function fetchDomainBlocksSuccess(domains, next) {
+  return {
+    type: DOMAIN_BLOCKS_FETCH_SUCCESS,
+    domains,
+    next,
+  };
+}
+
+export function fetchDomainBlocksFail(error) {
+  return {
+    type: DOMAIN_BLOCKS_FETCH_FAIL,
+    error,
+  };
+}
+
+export function expandDomainBlocks() {
+  return (dispatch, getState) => {
+    const url = getState().getIn(['domain_lists', 'blocks', 'next']);
+
+    if (!url) {
+      return;
+    }
+
+    dispatch(expandDomainBlocksRequest());
+
+    api(getState).get(url).then(response => {
+      const next = getLinks(response).refs.find(link => link.rel === 'next');
+      dispatch(expandDomainBlocksSuccess(response.data, next ? next.uri : null));
+    }).catch(err => {
+      dispatch(expandDomainBlocksFail(err));
+    });
+  };
+}
+
+export function expandDomainBlocksRequest() {
+  return {
+    type: DOMAIN_BLOCKS_EXPAND_REQUEST,
+  };
+}
+
+export function expandDomainBlocksSuccess(domains, next) {
+  return {
+    type: DOMAIN_BLOCKS_EXPAND_SUCCESS,
+    domains,
+    next,
+  };
+}
+
+export function expandDomainBlocksFail(error) {
+  return {
+    type: DOMAIN_BLOCKS_EXPAND_FAIL,
+    error,
+  };
+}
diff --git a/app/javascript/flavours/blobfox/actions/domain_blocks_typed.ts b/app/javascript/flavours/blobfox/actions/domain_blocks_typed.ts
new file mode 100644
index 00000000000000..4be6f7ed63495e
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/domain_blocks_typed.ts
@@ -0,0 +1,13 @@
+import { createAction } from '@reduxjs/toolkit';
+
+import type { Account } from 'flavours/blobfox/models/account';
+
+export const blockDomainSuccess = createAction<{
+  domain: string;
+  accounts: Account[];
+}>('domain_blocks/block/SUCCESS');
+
+export const unblockDomainSuccess = createAction<{
+  domain: string;
+  accounts: Account[];
+}>('domain_blocks/unblock/SUCCESS');
diff --git a/app/javascript/flavours/blobfox/actions/dropdown_menu.ts b/app/javascript/flavours/blobfox/actions/dropdown_menu.ts
new file mode 100644
index 00000000000000..3694df1ae03e29
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/dropdown_menu.ts
@@ -0,0 +1,11 @@
+import { createAction } from '@reduxjs/toolkit';
+
+export const openDropdownMenu = createAction<{
+  id: string;
+  keyboard: boolean;
+  scrollKey: string;
+}>('dropdownMenu/open');
+
+export const closeDropdownMenu = createAction<{ id: string }>(
+  'dropdownMenu/close',
+);
diff --git a/app/javascript/flavours/blobfox/actions/emojis.js b/app/javascript/flavours/blobfox/actions/emojis.js
new file mode 100644
index 00000000000000..3b5d53996c8204
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/emojis.js
@@ -0,0 +1,14 @@
+import { saveSettings } from './settings';
+
+export const EMOJI_USE = 'EMOJI_USE';
+
+export function useEmoji(emoji) {
+  return dispatch => {
+    dispatch({
+      type: EMOJI_USE,
+      emoji,
+    });
+
+    dispatch(saveSettings());
+  };
+}
diff --git a/app/javascript/flavours/blobfox/actions/favourites.js b/app/javascript/flavours/blobfox/actions/favourites.js
new file mode 100644
index 00000000000000..2d4d4e6206e845
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/favourites.js
@@ -0,0 +1,94 @@
+import api, { getLinks } from '../api';
+
+import { importFetchedStatuses } from './importer';
+
+export const FAVOURITED_STATUSES_FETCH_REQUEST = 'FAVOURITED_STATUSES_FETCH_REQUEST';
+export const FAVOURITED_STATUSES_FETCH_SUCCESS = 'FAVOURITED_STATUSES_FETCH_SUCCESS';
+export const FAVOURITED_STATUSES_FETCH_FAIL    = 'FAVOURITED_STATUSES_FETCH_FAIL';
+
+export const FAVOURITED_STATUSES_EXPAND_REQUEST = 'FAVOURITED_STATUSES_EXPAND_REQUEST';
+export const FAVOURITED_STATUSES_EXPAND_SUCCESS = 'FAVOURITED_STATUSES_EXPAND_SUCCESS';
+export const FAVOURITED_STATUSES_EXPAND_FAIL    = 'FAVOURITED_STATUSES_EXPAND_FAIL';
+
+export function fetchFavouritedStatuses() {
+  return (dispatch, getState) => {
+    if (getState().getIn(['status_lists', 'favourites', 'isLoading'])) {
+      return;
+    }
+
+    dispatch(fetchFavouritedStatusesRequest());
+
+    api(getState).get('/api/v1/favourites').then(response => {
+      const next = getLinks(response).refs.find(link => link.rel === 'next');
+      dispatch(importFetchedStatuses(response.data));
+      dispatch(fetchFavouritedStatusesSuccess(response.data, next ? next.uri : null));
+    }).catch(error => {
+      dispatch(fetchFavouritedStatusesFail(error));
+    });
+  };
+}
+
+export function fetchFavouritedStatusesRequest() {
+  return {
+    type: FAVOURITED_STATUSES_FETCH_REQUEST,
+    skipLoading: true,
+  };
+}
+
+export function fetchFavouritedStatusesSuccess(statuses, next) {
+  return {
+    type: FAVOURITED_STATUSES_FETCH_SUCCESS,
+    statuses,
+    next,
+    skipLoading: true,
+  };
+}
+
+export function fetchFavouritedStatusesFail(error) {
+  return {
+    type: FAVOURITED_STATUSES_FETCH_FAIL,
+    error,
+    skipLoading: true,
+  };
+}
+
+export function expandFavouritedStatuses() {
+  return (dispatch, getState) => {
+    const url = getState().getIn(['status_lists', 'favourites', 'next'], null);
+
+    if (url === null || getState().getIn(['status_lists', 'favourites', 'isLoading'])) {
+      return;
+    }
+
+    dispatch(expandFavouritedStatusesRequest());
+
+    api(getState).get(url).then(response => {
+      const next = getLinks(response).refs.find(link => link.rel === 'next');
+      dispatch(importFetchedStatuses(response.data));
+      dispatch(expandFavouritedStatusesSuccess(response.data, next ? next.uri : null));
+    }).catch(error => {
+      dispatch(expandFavouritedStatusesFail(error));
+    });
+  };
+}
+
+export function expandFavouritedStatusesRequest() {
+  return {
+    type: FAVOURITED_STATUSES_EXPAND_REQUEST,
+  };
+}
+
+export function expandFavouritedStatusesSuccess(statuses, next) {
+  return {
+    type: FAVOURITED_STATUSES_EXPAND_SUCCESS,
+    statuses,
+    next,
+  };
+}
+
+export function expandFavouritedStatusesFail(error) {
+  return {
+    type: FAVOURITED_STATUSES_EXPAND_FAIL,
+    error,
+  };
+}
diff --git a/app/javascript/flavours/blobfox/actions/featured_tags.js b/app/javascript/flavours/blobfox/actions/featured_tags.js
new file mode 100644
index 00000000000000..18bb615394571c
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/featured_tags.js
@@ -0,0 +1,34 @@
+import api from '../api';
+
+export const FEATURED_TAGS_FETCH_REQUEST = 'FEATURED_TAGS_FETCH_REQUEST';
+export const FEATURED_TAGS_FETCH_SUCCESS = 'FEATURED_TAGS_FETCH_SUCCESS';
+export const FEATURED_TAGS_FETCH_FAIL    = 'FEATURED_TAGS_FETCH_FAIL';
+
+export const fetchFeaturedTags = (id) => (dispatch, getState) => {
+  if (getState().getIn(['user_lists', 'featured_tags', id, 'items'])) {
+    return;
+  }
+
+  dispatch(fetchFeaturedTagsRequest(id));
+
+  api(getState).get(`/api/v1/accounts/${id}/featured_tags`)
+    .then(({ data }) => dispatch(fetchFeaturedTagsSuccess(id, data)))
+    .catch(err => dispatch(fetchFeaturedTagsFail(id, err)));
+};
+
+export const fetchFeaturedTagsRequest = (id) => ({
+  type: FEATURED_TAGS_FETCH_REQUEST,
+  id,
+});
+
+export const fetchFeaturedTagsSuccess = (id, tags) => ({
+  type: FEATURED_TAGS_FETCH_SUCCESS,
+  id,
+  tags,
+});
+
+export const fetchFeaturedTagsFail = (id, error) => ({
+  type: FEATURED_TAGS_FETCH_FAIL,
+  id,
+  error,
+});
diff --git a/app/javascript/flavours/blobfox/actions/filters.js b/app/javascript/flavours/blobfox/actions/filters.js
new file mode 100644
index 00000000000000..a11956ac564c6a
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/filters.js
@@ -0,0 +1,97 @@
+import api from '../api';
+
+import { openModal } from './modal';
+
+export const FILTERS_FETCH_REQUEST = 'FILTERS_FETCH_REQUEST';
+export const FILTERS_FETCH_SUCCESS = 'FILTERS_FETCH_SUCCESS';
+export const FILTERS_FETCH_FAIL    = 'FILTERS_FETCH_FAIL';
+
+export const FILTERS_STATUS_CREATE_REQUEST = 'FILTERS_STATUS_CREATE_REQUEST';
+export const FILTERS_STATUS_CREATE_SUCCESS = 'FILTERS_STATUS_CREATE_SUCCESS';
+export const FILTERS_STATUS_CREATE_FAIL    = 'FILTERS_STATUS_CREATE_FAIL';
+
+export const FILTERS_CREATE_REQUEST = 'FILTERS_CREATE_REQUEST';
+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,
+    },
+  }));
+
+export const fetchFilters = () => (dispatch, getState) => {
+  dispatch({
+    type: FILTERS_FETCH_REQUEST,
+    skipLoading: true,
+  });
+
+  api(getState)
+    .get('/api/v2/filters')
+    .then(({ data }) => dispatch({
+      type: FILTERS_FETCH_SUCCESS,
+      filters: data,
+      skipLoading: true,
+    }))
+    .catch(err => dispatch({
+      type: FILTERS_FETCH_FAIL,
+      err,
+      skipLoading: true,
+      skipAlert: true,
+    }));
+};
+
+export const createFilterStatus = (params, onSuccess, onFail) => (dispatch, getState) => {
+  dispatch(createFilterStatusRequest());
+
+  api(getState).post(`/api/v2/filters/${params.filter_id}/statuses`, params).then(response => {
+    dispatch(createFilterStatusSuccess(response.data));
+    if (onSuccess) onSuccess();
+  }).catch(error => {
+    dispatch(createFilterStatusFail(error));
+    if (onFail) onFail();
+  });
+};
+
+export const createFilterStatusRequest = () => ({
+  type: FILTERS_STATUS_CREATE_REQUEST,
+});
+
+export const createFilterStatusSuccess = filter_status => ({
+  type: FILTERS_STATUS_CREATE_SUCCESS,
+  filter_status,
+});
+
+export const createFilterStatusFail = error => ({
+  type: FILTERS_STATUS_CREATE_FAIL,
+  error,
+});
+
+export const createFilter = (params, onSuccess, onFail) => (dispatch, getState) => {
+  dispatch(createFilterRequest());
+
+  api(getState).post('/api/v2/filters', params).then(response => {
+    dispatch(createFilterSuccess(response.data));
+    if (onSuccess) onSuccess(response.data);
+  }).catch(error => {
+    dispatch(createFilterFail(error));
+    if (onFail) onFail();
+  });
+};
+
+export const createFilterRequest = () => ({
+  type: FILTERS_CREATE_REQUEST,
+});
+
+export const createFilterSuccess = filter => ({
+  type: FILTERS_CREATE_SUCCESS,
+  filter,
+});
+
+export const createFilterFail = error => ({
+  type: FILTERS_CREATE_FAIL,
+  error,
+});
diff --git a/app/javascript/flavours/blobfox/actions/height_cache.js b/app/javascript/flavours/blobfox/actions/height_cache.js
new file mode 100644
index 00000000000000..a8645410c85b1b
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/height_cache.js
@@ -0,0 +1,17 @@
+export const HEIGHT_CACHE_SET = 'HEIGHT_CACHE_SET';
+export const HEIGHT_CACHE_CLEAR = 'HEIGHT_CACHE_CLEAR';
+
+export function setHeight (key, id, height) {
+  return {
+    type: HEIGHT_CACHE_SET,
+    key,
+    id,
+    height,
+  };
+}
+
+export function clearHeight () {
+  return {
+    type: HEIGHT_CACHE_CLEAR,
+  };
+}
diff --git a/app/javascript/flavours/blobfox/actions/history.js b/app/javascript/flavours/blobfox/actions/history.js
new file mode 100644
index 00000000000000..52401b7dce3f95
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/history.js
@@ -0,0 +1,38 @@
+import api from '../api';
+
+import { importFetchedAccounts } from './importer';
+
+export const HISTORY_FETCH_REQUEST = 'HISTORY_FETCH_REQUEST';
+export const HISTORY_FETCH_SUCCESS = 'HISTORY_FETCH_SUCCESS';
+export const HISTORY_FETCH_FAIL    = 'HISTORY_FETCH_FAIL';
+
+export const fetchHistory = statusId => (dispatch, getState) => {
+  const loading = getState().getIn(['history', statusId, 'loading']);
+
+  if (loading) {
+    return;
+  }
+
+  dispatch(fetchHistoryRequest(statusId));
+
+  api(getState).get(`/api/v1/statuses/${statusId}/history`).then(({ data }) => {
+    dispatch(importFetchedAccounts(data.map(x => x.account)));
+    dispatch(fetchHistorySuccess(statusId, data));
+  }).catch(error => dispatch(fetchHistoryFail(error)));
+};
+
+export const fetchHistoryRequest = statusId => ({
+  type: HISTORY_FETCH_REQUEST,
+  statusId,
+});
+
+export const fetchHistorySuccess = (statusId, history) => ({
+  type: HISTORY_FETCH_SUCCESS,
+  statusId,
+  history,
+});
+
+export const fetchHistoryFail = error => ({
+  type: HISTORY_FETCH_FAIL,
+  error,
+});
diff --git a/app/javascript/flavours/blobfox/actions/importer/index.js b/app/javascript/flavours/blobfox/actions/importer/index.js
new file mode 100644
index 00000000000000..5fbc9bb5bbb64a
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/importer/index.js
@@ -0,0 +1,93 @@
+import { importAccounts } from '../accounts_typed';
+
+import { normalizeStatus, normalizePoll } from './normalizer';
+
+export const STATUS_IMPORT   = 'STATUS_IMPORT';
+export const STATUSES_IMPORT = 'STATUSES_IMPORT';
+export const POLLS_IMPORT    = 'POLLS_IMPORT';
+export const FILTERS_IMPORT  = 'FILTERS_IMPORT';
+
+function pushUnique(array, object) {
+  if (array.every(element => element.id !== object.id)) {
+    array.push(object);
+  }
+}
+
+export function importStatus(status) {
+  return { type: STATUS_IMPORT, status };
+}
+
+export function importStatuses(statuses) {
+  return { type: STATUSES_IMPORT, statuses };
+}
+
+export function importFilters(filters) {
+  return { type: FILTERS_IMPORT, filters };
+}
+
+export function importPolls(polls) {
+  return { type: POLLS_IMPORT, polls };
+}
+
+export function importFetchedAccount(account) {
+  return importFetchedAccounts([account]);
+}
+
+export function importFetchedAccounts(accounts) {
+  const normalAccounts = [];
+
+  function processAccount(account) {
+    pushUnique(normalAccounts, account);
+
+    if (account.moved) {
+      processAccount(account.moved);
+    }
+  }
+
+  accounts.forEach(processAccount);
+
+  return importAccounts({ accounts: normalAccounts });
+}
+
+export function importFetchedStatus(status) {
+  return importFetchedStatuses([status]);
+}
+
+export function importFetchedStatuses(statuses) {
+  return (dispatch, getState) => {
+    const accounts = [];
+    const normalStatuses = [];
+    const polls = [];
+    const filters = [];
+
+    function processStatus(status) {
+      pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id]), getState().get('local_settings')));
+      pushUnique(accounts, status.account);
+
+      if (status.filtered) {
+        status.filtered.forEach(result => pushUnique(filters, result.filter));
+      }
+
+      if (status.reblog && status.reblog.id) {
+        processStatus(status.reblog);
+      }
+
+      if (status.poll && status.poll.id) {
+        pushUnique(polls, normalizePoll(status.poll, getState().getIn(['polls', status.poll.id])));
+      }
+    }
+
+    statuses.forEach(processStatus);
+
+    dispatch(importPolls(polls));
+    dispatch(importFetchedAccounts(accounts));
+    dispatch(importStatuses(normalStatuses));
+    dispatch(importFilters(filters));
+  };
+}
+
+export function importFetchedPoll(poll) {
+  return (dispatch, getState) => {
+    dispatch(importPolls([normalizePoll(poll, getState().getIn(['polls', poll.id]))]));
+  };
+}
diff --git a/app/javascript/flavours/blobfox/actions/importer/normalizer.js b/app/javascript/flavours/blobfox/actions/importer/normalizer.js
new file mode 100644
index 00000000000000..c2ad0f9908a126
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/importer/normalizer.js
@@ -0,0 +1,135 @@
+import escapeTextContentForBrowser from 'escape-html';
+
+import emojify from '../../features/emoji/emoji';
+import { autoHideCW } from '../../utils/content_warning';
+
+const domParser = new DOMParser();
+
+const makeEmojiMap = emojis => emojis.reduce((obj, emoji) => {
+  obj[`:${emoji.shortcode}:`] = emoji;
+  return obj;
+}, {});
+
+export function searchTextFromRawStatus (status) {
+  const spoilerText   = status.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(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
+  return domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
+}
+
+export function normalizeFilterResult(result) {
+  const normalResult = { ...result };
+
+  normalResult.filter = normalResult.filter.id;
+
+  return normalResult;
+}
+
+export function normalizeStatus(status, normalOldStatus, settings) {
+  const normalStatus   = { ...status };
+  normalStatus.account = status.account.id;
+
+  if (status.reblog && status.reblog.id) {
+    normalStatus.reblog = status.reblog.id;
+  }
+
+  if (status.poll && status.poll.id) {
+    normalStatus.poll = status.poll.id;
+  }
+
+  if (status.filtered) {
+    normalStatus.filtered = status.filtered.map(normalizeFilterResult);
+  }
+
+  // Only calculate these values when status first encountered and
+  // when the underlying values change. Otherwise keep the ones
+  // already in the reducer
+  if (normalOldStatus && normalOldStatus.get('content') === normalStatus.content && normalOldStatus.get('spoiler_text') === normalStatus.spoiler_text) {
+    normalStatus.search_index = normalOldStatus.get('search_index');
+    normalStatus.contentHtml = normalOldStatus.get('contentHtml');
+    normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml');
+    normalStatus.hidden = normalOldStatus.get('hidden');
+
+    if (normalOldStatus.get('translation')) {
+      normalStatus.translation = normalOldStatus.get('translation');
+    }
+  } 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(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
+    const emojiMap      = makeEmojiMap(normalStatus.emojis);
+
+    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 (normalOldStatus) {
+    const list = normalOldStatus.get('media_attachments');
+    if (normalStatus.media_attachments && list) {
+      normalStatus.media_attachments.forEach(item => {
+        const oldItem = list.find(i => i.get('id') === item.id);
+        if (oldItem && oldItem.get('description') === item.description) {
+          item.translation = oldItem.get('translation');
+        }
+      });
+    }
+  }
+
+  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, normalOldPoll) {
+  const normalPoll = { ...poll };
+  const emojiMap = makeEmojiMap(poll.emojis);
+
+  normalPoll.options = poll.options.map((option, index) => {
+    const normalOption = {
+      ...option,
+      voted: poll.own_votes && poll.own_votes.includes(index),
+      titleHtml: emojify(escapeTextContentForBrowser(option.title), emojiMap),
+    };
+
+    if (normalOldPoll && normalOldPoll.getIn(['options', index, 'title']) === option.title) {
+      normalOption.translation = normalOldPoll.getIn(['options', index, 'translation']);
+    }
+
+    return normalOption;
+  });
+
+  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);
+
+  normalAnnouncement.contentHtml = emojify(normalAnnouncement.content, emojiMap);
+
+  return normalAnnouncement;
+}
diff --git a/app/javascript/flavours/blobfox/actions/interactions.js b/app/javascript/flavours/blobfox/actions/interactions.js
new file mode 100644
index 00000000000000..7cc7863af6319d
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/interactions.js
@@ -0,0 +1,600 @@
+import api, { getLinks } from '../api';
+
+import { fetchRelationships } from './accounts';
+import { importFetchedAccounts, importFetchedStatus } from './importer';
+
+export const REBLOG_REQUEST = 'REBLOG_REQUEST';
+export const REBLOG_SUCCESS = 'REBLOG_SUCCESS';
+export const REBLOG_FAIL    = 'REBLOG_FAIL';
+
+export const REBLOGS_EXPAND_REQUEST = 'REBLOGS_EXPAND_REQUEST';
+export const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS';
+export const REBLOGS_EXPAND_FAIL = 'REBLOGS_EXPAND_FAIL';
+
+export const FAVOURITE_REQUEST = 'FAVOURITE_REQUEST';
+export const FAVOURITE_SUCCESS = 'FAVOURITE_SUCCESS';
+export const FAVOURITE_FAIL    = 'FAVOURITE_FAIL';
+
+export const UNREBLOG_REQUEST = 'UNREBLOG_REQUEST';
+export const UNREBLOG_SUCCESS = 'UNREBLOG_SUCCESS';
+export const UNREBLOG_FAIL    = 'UNREBLOG_FAIL';
+
+export const UNFAVOURITE_REQUEST = 'UNFAVOURITE_REQUEST';
+export const UNFAVOURITE_SUCCESS = 'UNFAVOURITE_SUCCESS';
+export const UNFAVOURITE_FAIL    = 'UNFAVOURITE_FAIL';
+
+export const REBLOGS_FETCH_REQUEST = 'REBLOGS_FETCH_REQUEST';
+export const REBLOGS_FETCH_SUCCESS = 'REBLOGS_FETCH_SUCCESS';
+export const REBLOGS_FETCH_FAIL    = 'REBLOGS_FETCH_FAIL';
+
+export const FAVOURITES_FETCH_REQUEST = 'FAVOURITES_FETCH_REQUEST';
+export const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS';
+export const FAVOURITES_FETCH_FAIL    = 'FAVOURITES_FETCH_FAIL';
+
+export const FAVOURITES_EXPAND_REQUEST = 'FAVOURITES_EXPAND_REQUEST';
+export const FAVOURITES_EXPAND_SUCCESS = 'FAVOURITES_EXPAND_SUCCESS';
+export const FAVOURITES_EXPAND_FAIL = 'FAVOURITES_EXPAND_FAIL';
+
+export const PIN_REQUEST = 'PIN_REQUEST';
+export const PIN_SUCCESS = 'PIN_SUCCESS';
+export const PIN_FAIL    = 'PIN_FAIL';
+
+export const UNPIN_REQUEST = 'UNPIN_REQUEST';
+export const UNPIN_SUCCESS = 'UNPIN_SUCCESS';
+export const UNPIN_FAIL    = 'UNPIN_FAIL';
+
+export const BOOKMARK_REQUEST = 'BOOKMARK_REQUEST';
+export const BOOKMARK_SUCCESS = 'BOOKMARKED_SUCCESS';
+export const BOOKMARK_FAIL    = 'BOOKMARKED_FAIL';
+
+export const UNBOOKMARK_REQUEST = 'UNBOOKMARKED_REQUEST';
+export const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS';
+export const UNBOOKMARK_FAIL    = 'UNBOOKMARKED_FAIL';
+
+export const REACTION_UPDATE = 'REACTION_UPDATE';
+
+export const REACTION_ADD_REQUEST = 'REACTION_ADD_REQUEST';
+export const REACTION_ADD_SUCCESS = 'REACTION_ADD_SUCCESS';
+export const REACTION_ADD_FAIL    = 'REACTION_ADD_FAIL';
+
+export const REACTION_REMOVE_REQUEST = 'REACTION_REMOVE_REQUEST';
+export const REACTION_REMOVE_SUCCESS = 'REACTION_REMOVE_SUCCESS';
+export const REACTION_REMOVE_FAIL    = 'REACTION_REMOVE_FAIL';
+
+export function reblog(status, visibility) {
+  return function (dispatch, getState) {
+    dispatch(reblogRequest(status));
+
+    api(getState).post(`/api/v1/statuses/${status.get('id')}/reblog`, { visibility }).then(function (response) {
+      // The reblog API method returns a new status wrapped around the original. In this case we are only
+      // interested in how the original is modified, hence passing it skipping the wrapper
+      dispatch(importFetchedStatus(response.data.reblog));
+      dispatch(reblogSuccess(status));
+    }).catch(function (error) {
+      dispatch(reblogFail(status, error));
+    });
+  };
+}
+
+export function unreblog(status) {
+  return (dispatch, getState) => {
+    dispatch(unreblogRequest(status));
+
+    api(getState).post(`/api/v1/statuses/${status.get('id')}/unreblog`).then(response => {
+      dispatch(importFetchedStatus(response.data));
+      dispatch(unreblogSuccess(status));
+    }).catch(error => {
+      dispatch(unreblogFail(status, error));
+    });
+  };
+}
+
+export function reblogRequest(status) {
+  return {
+    type: REBLOG_REQUEST,
+    status: status,
+    skipLoading: true,
+  };
+}
+
+export function reblogSuccess(status) {
+  return {
+    type: REBLOG_SUCCESS,
+    status: status,
+    skipLoading: true,
+  };
+}
+
+export function reblogFail(status, error) {
+  return {
+    type: REBLOG_FAIL,
+    status: status,
+    error: error,
+    skipLoading: true,
+  };
+}
+
+export function unreblogRequest(status) {
+  return {
+    type: UNREBLOG_REQUEST,
+    status: status,
+    skipLoading: true,
+  };
+}
+
+export function unreblogSuccess(status) {
+  return {
+    type: UNREBLOG_SUCCESS,
+    status: status,
+    skipLoading: true,
+  };
+}
+
+export function unreblogFail(status, error) {
+  return {
+    type: UNREBLOG_FAIL,
+    status: status,
+    error: error,
+    skipLoading: true,
+  };
+}
+
+export function favourite(status) {
+  return function (dispatch, getState) {
+    dispatch(favouriteRequest(status));
+
+    api(getState).post(`/api/v1/statuses/${status.get('id')}/favourite`).then(function (response) {
+      dispatch(importFetchedStatus(response.data));
+      dispatch(favouriteSuccess(status));
+    }).catch(function (error) {
+      dispatch(favouriteFail(status, error));
+    });
+  };
+}
+
+export function unfavourite(status) {
+  return (dispatch, getState) => {
+    dispatch(unfavouriteRequest(status));
+
+    api(getState).post(`/api/v1/statuses/${status.get('id')}/unfavourite`).then(response => {
+      dispatch(importFetchedStatus(response.data));
+      dispatch(unfavouriteSuccess(status));
+    }).catch(error => {
+      dispatch(unfavouriteFail(status, error));
+    });
+  };
+}
+
+export function favouriteRequest(status) {
+  return {
+    type: FAVOURITE_REQUEST,
+    status: status,
+    skipLoading: true,
+  };
+}
+
+export function favouriteSuccess(status) {
+  return {
+    type: FAVOURITE_SUCCESS,
+    status: status,
+    skipLoading: true,
+  };
+}
+
+export function favouriteFail(status, error) {
+  return {
+    type: FAVOURITE_FAIL,
+    status: status,
+    error: error,
+    skipLoading: true,
+  };
+}
+
+export function unfavouriteRequest(status) {
+  return {
+    type: UNFAVOURITE_REQUEST,
+    status: status,
+    skipLoading: true,
+  };
+}
+
+export function unfavouriteSuccess(status) {
+  return {
+    type: UNFAVOURITE_SUCCESS,
+    status: status,
+    skipLoading: true,
+  };
+}
+
+export function unfavouriteFail(status, error) {
+  return {
+    type: UNFAVOURITE_FAIL,
+    status: status,
+    error: error,
+    skipLoading: true,
+  };
+}
+
+export function bookmark(status) {
+  return function (dispatch, getState) {
+    dispatch(bookmarkRequest(status));
+
+    api(getState).post(`/api/v1/statuses/${status.get('id')}/bookmark`).then(function (response) {
+      dispatch(importFetchedStatus(response.data));
+      dispatch(bookmarkSuccess(status, response.data));
+    }).catch(function (error) {
+      dispatch(bookmarkFail(status, error));
+    });
+  };
+}
+
+export function unbookmark(status) {
+  return (dispatch, getState) => {
+    dispatch(unbookmarkRequest(status));
+
+    api(getState).post(`/api/v1/statuses/${status.get('id')}/unbookmark`).then(response => {
+      dispatch(importFetchedStatus(response.data));
+      dispatch(unbookmarkSuccess(status, response.data));
+    }).catch(error => {
+      dispatch(unbookmarkFail(status, error));
+    });
+  };
+}
+
+export function bookmarkRequest(status) {
+  return {
+    type: BOOKMARK_REQUEST,
+    status: status,
+  };
+}
+
+export function bookmarkSuccess(status, response) {
+  return {
+    type: BOOKMARK_SUCCESS,
+    status: status,
+    response: response,
+  };
+}
+
+export function bookmarkFail(status, error) {
+  return {
+    type: BOOKMARK_FAIL,
+    status: status,
+    error: error,
+  };
+}
+
+export function unbookmarkRequest(status) {
+  return {
+    type: UNBOOKMARK_REQUEST,
+    status: status,
+  };
+}
+
+export function unbookmarkSuccess(status, response) {
+  return {
+    type: UNBOOKMARK_SUCCESS,
+    status: status,
+    response: response,
+  };
+}
+
+export function unbookmarkFail(status, error) {
+  return {
+    type: UNBOOKMARK_FAIL,
+    status: status,
+    error: error,
+  };
+}
+
+export function fetchReblogs(id) {
+  return (dispatch, getState) => {
+    dispatch(fetchReblogsRequest(id));
+
+    api(getState).get(`/api/v1/statuses/${id}/reblogged_by`).then(response => {
+      const next = getLinks(response).refs.find(link => link.rel === 'next');
+      dispatch(importFetchedAccounts(response.data));
+      dispatch(fetchReblogsSuccess(id, response.data, next ? next.uri : null));
+      dispatch(fetchRelationships(response.data.map(item => item.id)));
+    }).catch(error => {
+      dispatch(fetchReblogsFail(id, error));
+    });
+  };
+}
+
+export function fetchReblogsRequest(id) {
+  return {
+    type: REBLOGS_FETCH_REQUEST,
+    id,
+  };
+}
+
+export function fetchReblogsSuccess(id, accounts, next) {
+  return {
+    type: REBLOGS_FETCH_SUCCESS,
+    id,
+    accounts,
+    next,
+  };
+}
+
+export function fetchReblogsFail(id, error) {
+  return {
+    type: REBLOGS_FETCH_FAIL,
+    id,
+    error,
+  };
+}
+
+export function expandReblogs(id) {
+  return (dispatch, getState) => {
+    const url = getState().getIn(['user_lists', 'reblogged_by', id, 'next']);
+    if (url === null) {
+      return;
+    }
+
+    dispatch(expandReblogsRequest(id));
+
+    api(getState).get(url).then(response => {
+      const next = getLinks(response).refs.find(link => link.rel === 'next');
+
+      dispatch(importFetchedAccounts(response.data));
+      dispatch(expandReblogsSuccess(id, response.data, next ? next.uri : null));
+      dispatch(fetchRelationships(response.data.map(item => item.id)));
+    }).catch(error => dispatch(expandReblogsFail(id, error)));
+  };
+}
+
+export function expandReblogsRequest(id) {
+  return {
+    type: REBLOGS_EXPAND_REQUEST,
+    id,
+  };
+}
+
+export function expandReblogsSuccess(id, accounts, next) {
+  return {
+    type: REBLOGS_EXPAND_SUCCESS,
+    id,
+    accounts,
+    next,
+  };
+}
+
+export function expandReblogsFail(id, error) {
+  return {
+    type: REBLOGS_EXPAND_FAIL,
+    id,
+    error,
+  };
+}
+
+export function fetchFavourites(id) {
+  return (dispatch, getState) => {
+    dispatch(fetchFavouritesRequest(id));
+
+    api(getState).get(`/api/v1/statuses/${id}/favourited_by`).then(response => {
+      const next = getLinks(response).refs.find(link => link.rel === 'next');
+      dispatch(importFetchedAccounts(response.data));
+      dispatch(fetchFavouritesSuccess(id, response.data, next ? next.uri : null));
+      dispatch(fetchRelationships(response.data.map(item => item.id)));
+    }).catch(error => {
+      dispatch(fetchFavouritesFail(id, error));
+    });
+  };
+}
+
+export function fetchFavouritesRequest(id) {
+  return {
+    type: FAVOURITES_FETCH_REQUEST,
+    id,
+  };
+}
+
+export function fetchFavouritesSuccess(id, accounts, next) {
+  return {
+    type: FAVOURITES_FETCH_SUCCESS,
+    id,
+    accounts,
+    next,
+  };
+}
+
+export function fetchFavouritesFail(id, error) {
+  return {
+    type: FAVOURITES_FETCH_FAIL,
+    id,
+    error,
+  };
+}
+
+export function expandFavourites(id) {
+  return (dispatch, getState) => {
+    const url = getState().getIn(['user_lists', 'favourited_by', id, 'next']);
+    if (url === null) {
+      return;
+    }
+
+    dispatch(expandFavouritesRequest(id));
+
+    api(getState).get(url).then(response => {
+      const next = getLinks(response).refs.find(link => link.rel === 'next');
+
+      dispatch(importFetchedAccounts(response.data));
+      dispatch(expandFavouritesSuccess(id, response.data, next ? next.uri : null));
+      dispatch(fetchRelationships(response.data.map(item => item.id)));
+    }).catch(error => dispatch(expandFavouritesFail(id, error)));
+  };
+}
+
+export function expandFavouritesRequest(id) {
+  return {
+    type: FAVOURITES_EXPAND_REQUEST,
+    id,
+  };
+}
+
+export function expandFavouritesSuccess(id, accounts, next) {
+  return {
+    type: FAVOURITES_EXPAND_SUCCESS,
+    id,
+    accounts,
+    next,
+  };
+}
+
+export function expandFavouritesFail(id, error) {
+  return {
+    type: FAVOURITES_EXPAND_FAIL,
+    id,
+    error,
+  };
+}
+
+export function pin(status) {
+  return (dispatch, getState) => {
+    dispatch(pinRequest(status));
+
+    api(getState).post(`/api/v1/statuses/${status.get('id')}/pin`).then(response => {
+      dispatch(importFetchedStatus(response.data));
+      dispatch(pinSuccess(status));
+    }).catch(error => {
+      dispatch(pinFail(status, error));
+    });
+  };
+}
+
+export function pinRequest(status) {
+  return {
+    type: PIN_REQUEST,
+    status,
+    skipLoading: true,
+  };
+}
+
+export function pinSuccess(status) {
+  return {
+    type: PIN_SUCCESS,
+    status,
+    skipLoading: true,
+  };
+}
+
+export function pinFail(status, error) {
+  return {
+    type: PIN_FAIL,
+    status,
+    error,
+    skipLoading: true,
+  };
+}
+
+export function unpin (status) {
+  return (dispatch, getState) => {
+    dispatch(unpinRequest(status));
+
+    api(getState).post(`/api/v1/statuses/${status.get('id')}/unpin`).then(response => {
+      dispatch(importFetchedStatus(response.data));
+      dispatch(unpinSuccess(status));
+    }).catch(error => {
+      dispatch(unpinFail(status, error));
+    });
+  };
+}
+
+export function unpinRequest(status) {
+  return {
+    type: UNPIN_REQUEST,
+    status,
+    skipLoading: true,
+  };
+}
+
+export function unpinSuccess(status) {
+  return {
+    type: UNPIN_SUCCESS,
+    status,
+    skipLoading: true,
+  };
+}
+
+export function unpinFail(status, error) {
+  return {
+    type: UNPIN_FAIL,
+    status,
+    error,
+    skipLoading: true,
+  };
+}
+
+export const addReaction = (statusId, name, url) => (dispatch, getState) => {
+  const status = getState().get('statuses').get(statusId);
+  let alreadyAdded = false;
+  if (status) {
+    const reaction = status.get('reactions').find(x => x.get('name') === name);
+    if (reaction && reaction.get('me')) {
+      alreadyAdded = true;
+    }
+  }
+  if (!alreadyAdded) {
+    dispatch(addReactionRequest(statusId, name, url));
+  }
+
+  // encodeURIComponent is required for the Keycap Number Sign emoji, see:
+  // <https://github.com/blobfox-soc/mastodon/pull/1980#issuecomment-1345538932>
+  api(getState).post(`/api/v1/statuses/${statusId}/react/${encodeURIComponent(name)}`).then(() => {
+    dispatch(addReactionSuccess(statusId, name));
+  }).catch(err => {
+    if (!alreadyAdded) {
+      dispatch(addReactionFail(statusId, name, err));
+    }
+  });
+};
+
+export const addReactionRequest = (statusId, name, url) => ({
+  type: REACTION_ADD_REQUEST,
+  id: statusId,
+  name,
+  url,
+});
+
+export const addReactionSuccess = (statusId, name) => ({
+  type: REACTION_ADD_SUCCESS,
+  id: statusId,
+  name,
+});
+
+export const addReactionFail = (statusId, name, error) => ({
+  type: REACTION_ADD_FAIL,
+  id: statusId,
+  name,
+  error,
+});
+
+export const removeReaction = (statusId, name) => (dispatch, getState) => {
+  dispatch(removeReactionRequest(statusId, name));
+
+  api(getState).post(`/api/v1/statuses/${statusId}/unreact/${encodeURIComponent(name)}`).then(() => {
+    dispatch(removeReactionSuccess(statusId, name));
+  }).catch(err => {
+    dispatch(removeReactionFail(statusId, name, err));
+  });
+};
+
+export const removeReactionRequest = (statusId, name) => ({
+  type: REACTION_REMOVE_REQUEST,
+  id: statusId,
+  name,
+});
+
+export const removeReactionSuccess = (statusId, name) => ({
+  type: REACTION_REMOVE_SUCCESS,
+  id: statusId,
+  name,
+});
+
+export const removeReactionFail = (statusId, name) => ({
+  type: REACTION_REMOVE_FAIL,
+  id: statusId,
+  name,
+});
diff --git a/app/javascript/flavours/blobfox/actions/languages.js b/app/javascript/flavours/blobfox/actions/languages.js
new file mode 100644
index 00000000000000..ad186ba0ccd3e5
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/languages.js
@@ -0,0 +1,12 @@
+import { saveSettings } from './settings';
+
+export const LANGUAGE_USE = 'LANGUAGE_USE';
+
+export const useLanguage = language => dispatch => {
+  dispatch({
+    type: LANGUAGE_USE,
+    language,
+  });
+
+  dispatch(saveSettings());
+};
diff --git a/app/javascript/flavours/blobfox/actions/lists.js b/app/javascript/flavours/blobfox/actions/lists.js
new file mode 100644
index 00000000000000..b0789cd426450a
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/lists.js
@@ -0,0 +1,373 @@
+import api from '../api';
+
+import { showAlertForError } from './alerts';
+import { importFetchedAccounts } from './importer';
+
+export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST';
+export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS';
+export const LIST_FETCH_FAIL    = 'LIST_FETCH_FAIL';
+
+export const LISTS_FETCH_REQUEST = 'LISTS_FETCH_REQUEST';
+export const LISTS_FETCH_SUCCESS = 'LISTS_FETCH_SUCCESS';
+export const LISTS_FETCH_FAIL    = 'LISTS_FETCH_FAIL';
+
+export const LIST_EDITOR_TITLE_CHANGE = 'LIST_EDITOR_TITLE_CHANGE';
+export const LIST_EDITOR_RESET        = 'LIST_EDITOR_RESET';
+export const LIST_EDITOR_SETUP        = 'LIST_EDITOR_SETUP';
+
+export const LIST_CREATE_REQUEST = 'LIST_CREATE_REQUEST';
+export const LIST_CREATE_SUCCESS = 'LIST_CREATE_SUCCESS';
+export const LIST_CREATE_FAIL    = 'LIST_CREATE_FAIL';
+
+export const LIST_UPDATE_REQUEST = 'LIST_UPDATE_REQUEST';
+export const LIST_UPDATE_SUCCESS = 'LIST_UPDATE_SUCCESS';
+export const LIST_UPDATE_FAIL    = 'LIST_UPDATE_FAIL';
+
+export const LIST_DELETE_REQUEST = 'LIST_DELETE_REQUEST';
+export const LIST_DELETE_SUCCESS = 'LIST_DELETE_SUCCESS';
+export const LIST_DELETE_FAIL    = 'LIST_DELETE_FAIL';
+
+export const LIST_ACCOUNTS_FETCH_REQUEST = 'LIST_ACCOUNTS_FETCH_REQUEST';
+export const LIST_ACCOUNTS_FETCH_SUCCESS = 'LIST_ACCOUNTS_FETCH_SUCCESS';
+export const LIST_ACCOUNTS_FETCH_FAIL    = 'LIST_ACCOUNTS_FETCH_FAIL';
+
+export const LIST_EDITOR_SUGGESTIONS_CHANGE = 'LIST_EDITOR_SUGGESTIONS_CHANGE';
+export const LIST_EDITOR_SUGGESTIONS_READY  = 'LIST_EDITOR_SUGGESTIONS_READY';
+export const LIST_EDITOR_SUGGESTIONS_CLEAR  = 'LIST_EDITOR_SUGGESTIONS_CLEAR';
+
+export const LIST_EDITOR_ADD_REQUEST = 'LIST_EDITOR_ADD_REQUEST';
+export const LIST_EDITOR_ADD_SUCCESS = 'LIST_EDITOR_ADD_SUCCESS';
+export const LIST_EDITOR_ADD_FAIL    = 'LIST_EDITOR_ADD_FAIL';
+
+export const LIST_EDITOR_REMOVE_REQUEST = 'LIST_EDITOR_REMOVE_REQUEST';
+export const LIST_EDITOR_REMOVE_SUCCESS = 'LIST_EDITOR_REMOVE_SUCCESS';
+export const LIST_EDITOR_REMOVE_FAIL    = 'LIST_EDITOR_REMOVE_FAIL';
+
+export const LIST_ADDER_RESET = 'LIST_ADDER_RESET';
+export const LIST_ADDER_SETUP = 'LIST_ADDER_SETUP';
+
+export const LIST_ADDER_LISTS_FETCH_REQUEST = 'LIST_ADDER_LISTS_FETCH_REQUEST';
+export const LIST_ADDER_LISTS_FETCH_SUCCESS = 'LIST_ADDER_LISTS_FETCH_SUCCESS';
+export const LIST_ADDER_LISTS_FETCH_FAIL    = 'LIST_ADDER_LISTS_FETCH_FAIL';
+
+export const fetchList = id => (dispatch, getState) => {
+  if (getState().getIn(['lists', id])) {
+    return;
+  }
+
+  dispatch(fetchListRequest(id));
+
+  api(getState).get(`/api/v1/lists/${id}`)
+    .then(({ data }) => dispatch(fetchListSuccess(data)))
+    .catch(err => dispatch(fetchListFail(id, err)));
+};
+
+export const fetchListRequest = id => ({
+  type: LIST_FETCH_REQUEST,
+  id,
+});
+
+export const fetchListSuccess = list => ({
+  type: LIST_FETCH_SUCCESS,
+  list,
+});
+
+export const fetchListFail = (id, error) => ({
+  type: LIST_FETCH_FAIL,
+  id,
+  error,
+});
+
+export const fetchLists = () => (dispatch, getState) => {
+  dispatch(fetchListsRequest());
+
+  api(getState).get('/api/v1/lists')
+    .then(({ data }) => dispatch(fetchListsSuccess(data)))
+    .catch(err => dispatch(fetchListsFail(err)));
+};
+
+export const fetchListsRequest = () => ({
+  type: LISTS_FETCH_REQUEST,
+});
+
+export const fetchListsSuccess = lists => ({
+  type: LISTS_FETCH_SUCCESS,
+  lists,
+});
+
+export const fetchListsFail = error => ({
+  type: LISTS_FETCH_FAIL,
+  error,
+});
+
+export const submitListEditor = shouldReset => (dispatch, getState) => {
+  const listId = getState().getIn(['listEditor', 'listId']);
+  const title  = getState().getIn(['listEditor', 'title']);
+
+  if (listId === null) {
+    dispatch(createList(title, shouldReset));
+  } else {
+    dispatch(updateList(listId, title, shouldReset));
+  }
+};
+
+export const setupListEditor = listId => (dispatch, getState) => {
+  dispatch({
+    type: LIST_EDITOR_SETUP,
+    list: getState().getIn(['lists', listId]),
+  });
+
+  dispatch(fetchListAccounts(listId));
+};
+
+export const changeListEditorTitle = value => ({
+  type: LIST_EDITOR_TITLE_CHANGE,
+  value,
+});
+
+export const createList = (title, shouldReset) => (dispatch, getState) => {
+  dispatch(createListRequest());
+
+  api(getState).post('/api/v1/lists', { title }).then(({ data }) => {
+    dispatch(createListSuccess(data));
+
+    if (shouldReset) {
+      dispatch(resetListEditor());
+    }
+  }).catch(err => dispatch(createListFail(err)));
+};
+
+export const createListRequest = () => ({
+  type: LIST_CREATE_REQUEST,
+});
+
+export const createListSuccess = list => ({
+  type: LIST_CREATE_SUCCESS,
+  list,
+});
+
+export const createListFail = error => ({
+  type: LIST_CREATE_FAIL,
+  error,
+});
+
+export const updateList = (id, title, shouldReset, isExclusive, 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 }) => {
+    dispatch(updateListSuccess(data));
+
+    if (shouldReset) {
+      dispatch(resetListEditor());
+    }
+  }).catch(err => dispatch(updateListFail(id, err)));
+};
+
+export const updateListRequest = id => ({
+  type: LIST_UPDATE_REQUEST,
+  id,
+});
+
+export const updateListSuccess = list => ({
+  type: LIST_UPDATE_SUCCESS,
+  list,
+});
+
+export const updateListFail = (id, error) => ({
+  type: LIST_UPDATE_FAIL,
+  id,
+  error,
+});
+
+export const resetListEditor = () => ({
+  type: LIST_EDITOR_RESET,
+});
+
+export const deleteList = id => (dispatch, getState) => {
+  dispatch(deleteListRequest(id));
+
+  api(getState).delete(`/api/v1/lists/${id}`)
+    .then(() => dispatch(deleteListSuccess(id)))
+    .catch(err => dispatch(deleteListFail(id, err)));
+};
+
+export const deleteListRequest = id => ({
+  type: LIST_DELETE_REQUEST,
+  id,
+});
+
+export const deleteListSuccess = id => ({
+  type: LIST_DELETE_SUCCESS,
+  id,
+});
+
+export const deleteListFail = (id, error) => ({
+  type: LIST_DELETE_FAIL,
+  id,
+  error,
+});
+
+export const fetchListAccounts = listId => (dispatch, getState) => {
+  dispatch(fetchListAccountsRequest(listId));
+
+  api(getState).get(`/api/v1/lists/${listId}/accounts`, { params: { limit: 0 } }).then(({ data }) => {
+    dispatch(importFetchedAccounts(data));
+    dispatch(fetchListAccountsSuccess(listId, data));
+  }).catch(err => dispatch(fetchListAccountsFail(listId, err)));
+};
+
+export const fetchListAccountsRequest = id => ({
+  type: LIST_ACCOUNTS_FETCH_REQUEST,
+  id,
+});
+
+export const fetchListAccountsSuccess = (id, accounts, next) => ({
+  type: LIST_ACCOUNTS_FETCH_SUCCESS,
+  id,
+  accounts,
+  next,
+});
+
+export const fetchListAccountsFail = (id, error) => ({
+  type: LIST_ACCOUNTS_FETCH_FAIL,
+  id,
+  error,
+});
+
+export const fetchListSuggestions = q => (dispatch, getState) => {
+  const params = {
+    q,
+    resolve: false,
+    limit: 4,
+    following: true,
+  };
+
+  api(getState).get('/api/v1/accounts/search', { params }).then(({ data }) => {
+    dispatch(importFetchedAccounts(data));
+    dispatch(fetchListSuggestionsReady(q, data));
+  }).catch(error => dispatch(showAlertForError(error)));
+};
+
+export const fetchListSuggestionsReady = (query, accounts) => ({
+  type: LIST_EDITOR_SUGGESTIONS_READY,
+  query,
+  accounts,
+});
+
+export const clearListSuggestions = () => ({
+  type: LIST_EDITOR_SUGGESTIONS_CLEAR,
+});
+
+export const changeListSuggestions = value => ({
+  type: LIST_EDITOR_SUGGESTIONS_CHANGE,
+  value,
+});
+
+export const addToListEditor = accountId => (dispatch, getState) => {
+  dispatch(addToList(getState().getIn(['listEditor', 'listId']), accountId));
+};
+
+export const addToList = (listId, accountId) => (dispatch, getState) => {
+  dispatch(addToListRequest(listId, accountId));
+
+  api(getState).post(`/api/v1/lists/${listId}/accounts`, { account_ids: [accountId] })
+    .then(() => dispatch(addToListSuccess(listId, accountId)))
+    .catch(err => dispatch(addToListFail(listId, accountId, err)));
+};
+
+export const addToListRequest = (listId, accountId) => ({
+  type: LIST_EDITOR_ADD_REQUEST,
+  listId,
+  accountId,
+});
+
+export const addToListSuccess = (listId, accountId) => ({
+  type: LIST_EDITOR_ADD_SUCCESS,
+  listId,
+  accountId,
+});
+
+export const addToListFail = (listId, accountId, error) => ({
+  type: LIST_EDITOR_ADD_FAIL,
+  listId,
+  accountId,
+  error,
+});
+
+export const removeFromListEditor = accountId => (dispatch, getState) => {
+  dispatch(removeFromList(getState().getIn(['listEditor', 'listId']), accountId));
+};
+
+export const removeFromList = (listId, accountId) => (dispatch, getState) => {
+  dispatch(removeFromListRequest(listId, accountId));
+
+  api(getState).delete(`/api/v1/lists/${listId}/accounts`, { params: { account_ids: [accountId] } })
+    .then(() => dispatch(removeFromListSuccess(listId, accountId)))
+    .catch(err => dispatch(removeFromListFail(listId, accountId, err)));
+};
+
+export const removeFromListRequest = (listId, accountId) => ({
+  type: LIST_EDITOR_REMOVE_REQUEST,
+  listId,
+  accountId,
+});
+
+export const removeFromListSuccess = (listId, accountId) => ({
+  type: LIST_EDITOR_REMOVE_SUCCESS,
+  listId,
+  accountId,
+});
+
+export const removeFromListFail = (listId, accountId, error) => ({
+  type: LIST_EDITOR_REMOVE_FAIL,
+  listId,
+  accountId,
+  error,
+});
+
+export const resetListAdder = () => ({
+  type: LIST_ADDER_RESET,
+});
+
+export const setupListAdder = accountId => (dispatch, getState) => {
+  dispatch({
+    type: LIST_ADDER_SETUP,
+    account: getState().getIn(['accounts', accountId]),
+  });
+  dispatch(fetchLists());
+  dispatch(fetchAccountLists(accountId));
+};
+
+export const fetchAccountLists = accountId => (dispatch, getState) => {
+  dispatch(fetchAccountListsRequest(accountId));
+
+  api(getState).get(`/api/v1/accounts/${accountId}/lists`)
+    .then(({ data }) => dispatch(fetchAccountListsSuccess(accountId, data)))
+    .catch(err => dispatch(fetchAccountListsFail(accountId, err)));
+};
+
+export const fetchAccountListsRequest = id => ({
+  type:LIST_ADDER_LISTS_FETCH_REQUEST,
+  id,
+});
+
+export const fetchAccountListsSuccess = (id, lists) => ({
+  type: LIST_ADDER_LISTS_FETCH_SUCCESS,
+  id,
+  lists,
+});
+
+export const fetchAccountListsFail = (id, err) => ({
+  type: LIST_ADDER_LISTS_FETCH_FAIL,
+  id,
+  err,
+});
+
+export const addToListAdder = listId => (dispatch, getState) => {
+  dispatch(addToList(listId, getState().getIn(['listAdder', 'accountId'])));
+};
+
+export const removeFromListAdder = listId => (dispatch, getState) => {
+  dispatch(removeFromList(listId, getState().getIn(['listAdder', 'accountId'])));
+};
+
diff --git a/app/javascript/flavours/blobfox/actions/local_settings.js b/app/javascript/flavours/blobfox/actions/local_settings.js
new file mode 100644
index 00000000000000..a02e9d763eb9cd
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/local_settings.js
@@ -0,0 +1,81 @@
+import { expandSpoilers, disableSwiping } from 'flavours/blobfox/initial_state';
+
+import { openModal } from './modal';
+
+export const LOCAL_SETTING_CHANGE = 'LOCAL_SETTING_CHANGE';
+export const LOCAL_SETTING_DELETE = 'LOCAL_SETTING_DELETE';
+
+export function checkDeprecatedLocalSettings() {
+  return (dispatch, getState) => {
+    const local_auto_unfold = getState().getIn(['local_settings', 'content_warnings', 'auto_unfold']);
+    const local_swipe_to_change_columns = getState().getIn(['local_settings', 'swipe_to_change_columns']);
+    let changed_settings = [];
+
+    if (local_auto_unfold !== null && local_auto_unfold !== undefined) {
+      if (local_auto_unfold === expandSpoilers) {
+        dispatch(deleteLocalSetting(['content_warnings', 'auto_unfold']));
+      } else {
+        changed_settings.push('user_setting_expand_spoilers');
+      }
+    }
+
+    if (local_swipe_to_change_columns !== null && local_swipe_to_change_columns !== undefined) {
+      if (local_swipe_to_change_columns === !disableSwiping) {
+        dispatch(deleteLocalSetting(['swipe_to_change_columns']));
+      } else {
+        changed_settings.push('user_setting_disable_swiping');
+      }
+    }
+
+    if (changed_settings.length > 0) {
+      dispatch(openModal({
+        modalType: 'DEPRECATED_SETTINGS',
+        modalProps: {
+          settings: changed_settings,
+          onConfirm: () => dispatch(clearDeprecatedLocalSettings()),
+        },
+      }));
+    }
+  };
+}
+
+export function clearDeprecatedLocalSettings() {
+  return (dispatch) => {
+    dispatch(deleteLocalSetting(['content_warnings', 'auto_unfold']));
+    dispatch(deleteLocalSetting(['swipe_to_change_columns']));
+  };
+}
+
+export function changeLocalSetting(key, value) {
+  return dispatch => {
+    dispatch({
+      type: LOCAL_SETTING_CHANGE,
+      key,
+      value,
+    });
+
+    dispatch(saveLocalSettings());
+  };
+}
+
+export function deleteLocalSetting(key) {
+  return dispatch => {
+    dispatch({
+      type: LOCAL_SETTING_DELETE,
+      key,
+    });
+
+    dispatch(saveLocalSettings());
+  };
+}
+
+//  __TODO :__
+//  Right now `saveLocalSettings()` doesn't keep track of which user
+//  is currently signed in, but it might be better to give each user
+//  their *own* local settings.
+export function saveLocalSettings() {
+  return (_, getState) => {
+    const localSettings = getState().get('local_settings').toJS();
+    localStorage.setItem('mastodon-settings', JSON.stringify(localSettings));
+  };
+}
diff --git a/app/javascript/flavours/blobfox/actions/markers.js b/app/javascript/flavours/blobfox/actions/markers.js
new file mode 100644
index 00000000000000..ccb1b23d6f9f54
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/markers.js
@@ -0,0 +1,152 @@
+import { List as ImmutableList } from 'immutable';
+
+import { debounce } from 'lodash';
+
+import api from '../api';
+import { compareId } from '../compare_id';
+
+export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST';
+export const MARKERS_FETCH_SUCCESS = 'MARKERS_FETCH_SUCCESS';
+export const MARKERS_FETCH_FAIL    = 'MARKERS_FETCH_FAIL';
+export const MARKERS_SUBMIT_SUCCESS = 'MARKERS_SUBMIT_SUCCESS';
+
+export const synchronouslySubmitMarkers = () => (dispatch, getState) => {
+  const accessToken = getState().getIn(['meta', 'access_token'], '');
+  const params      = _buildParams(getState());
+
+  if (Object.keys(params).length === 0 || accessToken === '') {
+    return;
+  }
+
+  // The Fetch API allows us to perform requests that will be carried out
+  // after the page closes. But that only works if the `keepalive` attribute
+  // is supported.
+  if (window.fetch && 'keepalive' in new Request('')) {
+    fetch('/api/v1/markers', {
+      keepalive: true,
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+        'Authorization': `Bearer ${accessToken}`,
+      },
+      body: JSON.stringify(params),
+    });
+
+    return;
+  } else if (navigator && navigator.sendBeacon) {
+    // Failing that, we can use sendBeacon, but we have to encode the data as
+    // FormData for DoorKeeper to recognize the token.
+    const formData = new FormData();
+
+    formData.append('bearer_token', accessToken);
+
+    for (const [id, value] of Object.entries(params)) {
+      formData.append(`${id}[last_read_id]`, value.last_read_id);
+    }
+
+    if (navigator.sendBeacon('/api/v1/markers', formData)) {
+      return;
+    }
+  }
+
+  // If neither Fetch nor sendBeacon worked, try to perform a synchronous
+  // request.
+  try {
+    const client = new XMLHttpRequest();
+
+    client.open('POST', '/api/v1/markers', false);
+    client.setRequestHeader('Content-Type', 'application/json');
+    client.setRequestHeader('Authorization', `Bearer ${accessToken}`);
+    client.send(JSON.stringify(params));
+  } catch (e) {
+    // Do not make the BeforeUnload handler error out
+  }
+};
+
+const _buildParams = (state) => {
+  const params = {};
+
+  const lastHomeId         = state.getIn(['timelines', 'home', 'items'], ImmutableList()).find(item => item !== null);
+  const lastNotificationId = state.getIn(['notifications', 'lastReadId']);
+
+  if (lastHomeId && compareId(lastHomeId, state.getIn(['markers', 'home'])) > 0) {
+    params.home = {
+      last_read_id: lastHomeId,
+    };
+  }
+
+  if (lastNotificationId && lastNotificationId !== '0' && compareId(lastNotificationId, state.getIn(['markers', 'notifications'])) > 0) {
+    params.notifications = {
+      last_read_id: lastNotificationId,
+    };
+  }
+
+  return params;
+};
+
+const debouncedSubmitMarkers = debounce((dispatch, getState) => {
+  const accessToken = getState().getIn(['meta', 'access_token'], '');
+  const params      = _buildParams(getState());
+
+  if (Object.keys(params).length === 0 || accessToken === '') {
+    return;
+  }
+
+  api(getState).post('/api/v1/markers', params).then(() => {
+    dispatch(submitMarkersSuccess(params));
+  }).catch(() => {});
+}, 300000, { leading: true, trailing: true });
+
+export function submitMarkersSuccess({ home, notifications }) {
+  return {
+    type: MARKERS_SUBMIT_SUCCESS,
+    home: (home || {}).last_read_id,
+    notifications: (notifications || {}).last_read_id,
+  };
+}
+
+export function submitMarkers(params = {}) {
+  const result = (dispatch, getState) => debouncedSubmitMarkers(dispatch, getState);
+
+  if (params.immediate === true) {
+    debouncedSubmitMarkers.flush();
+  }
+
+  return result;
+}
+
+export const fetchMarkers = () => (dispatch, getState) => {
+  const params = { timeline: ['notifications'] };
+
+  dispatch(fetchMarkersRequest());
+
+  api(getState).get('/api/v1/markers', { params }).then(response => {
+    dispatch(fetchMarkersSuccess(response.data));
+  }).catch(error => {
+    dispatch(fetchMarkersFail(error));
+  });
+};
+
+export function fetchMarkersRequest() {
+  return {
+    type: MARKERS_FETCH_REQUEST,
+    skipLoading: true,
+  };
+}
+
+export function fetchMarkersSuccess(markers) {
+  return {
+    type: MARKERS_FETCH_SUCCESS,
+    markers,
+    skipLoading: true,
+  };
+}
+
+export function fetchMarkersFail(error) {
+  return {
+    type: MARKERS_FETCH_FAIL,
+    error,
+    skipLoading: true,
+    skipAlert: true,
+  };
+}
diff --git a/app/javascript/flavours/blobfox/actions/modal.ts b/app/javascript/flavours/blobfox/actions/modal.ts
new file mode 100644
index 00000000000000..441d185a9c808c
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/modal.ts
@@ -0,0 +1,19 @@
+import { createAction } from '@reduxjs/toolkit';
+
+import type { ModalProps } from 'flavours/blobfox/reducers/modal';
+
+import type { MODAL_COMPONENTS } from '../features/ui/components/modal_root';
+
+export type ModalType = keyof typeof MODAL_COMPONENTS;
+
+interface OpenModalPayload {
+  modalType: ModalType;
+  modalProps: ModalProps;
+}
+export const openModal = createAction<OpenModalPayload>('MODAL_OPEN');
+
+interface CloseModalPayload {
+  modalType: ModalType | undefined;
+  ignoreFocus: boolean;
+}
+export const closeModal = createAction<CloseModalPayload>('MODAL_CLOSE');
diff --git a/app/javascript/flavours/blobfox/actions/mutes.js b/app/javascript/flavours/blobfox/actions/mutes.js
new file mode 100644
index 00000000000000..fb041078b84488
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/mutes.js
@@ -0,0 +1,117 @@
+import api, { getLinks } from '../api';
+
+import { fetchRelationships } from './accounts';
+import { importFetchedAccounts } from './importer';
+import { openModal } from './modal';
+
+export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST';
+export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS';
+export const MUTES_FETCH_FAIL    = 'MUTES_FETCH_FAIL';
+
+export const MUTES_EXPAND_REQUEST = 'MUTES_EXPAND_REQUEST';
+export const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS';
+export const MUTES_EXPAND_FAIL    = 'MUTES_EXPAND_FAIL';
+
+export const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL';
+export const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS';
+export const MUTES_CHANGE_DURATION = 'MUTES_CHANGE_DURATION';
+
+export function fetchMutes() {
+  return (dispatch, getState) => {
+    dispatch(fetchMutesRequest());
+
+    api(getState).get('/api/v1/mutes').then(response => {
+      const next = getLinks(response).refs.find(link => link.rel === 'next');
+      dispatch(importFetchedAccounts(response.data));
+      dispatch(fetchMutesSuccess(response.data, next ? next.uri : null));
+      dispatch(fetchRelationships(response.data.map(item => item.id)));
+    }).catch(error => dispatch(fetchMutesFail(error)));
+  };
+}
+
+export function fetchMutesRequest() {
+  return {
+    type: MUTES_FETCH_REQUEST,
+  };
+}
+
+export function fetchMutesSuccess(accounts, next) {
+  return {
+    type: MUTES_FETCH_SUCCESS,
+    accounts,
+    next,
+  };
+}
+
+export function fetchMutesFail(error) {
+  return {
+    type: MUTES_FETCH_FAIL,
+    error,
+  };
+}
+
+export function expandMutes() {
+  return (dispatch, getState) => {
+    const url = getState().getIn(['user_lists', 'mutes', 'next']);
+
+    if (url === null) {
+      return;
+    }
+
+    dispatch(expandMutesRequest());
+
+    api(getState).get(url).then(response => {
+      const next = getLinks(response).refs.find(link => link.rel === 'next');
+      dispatch(importFetchedAccounts(response.data));
+      dispatch(expandMutesSuccess(response.data, next ? next.uri : null));
+      dispatch(fetchRelationships(response.data.map(item => item.id)));
+    }).catch(error => dispatch(expandMutesFail(error)));
+  };
+}
+
+export function expandMutesRequest() {
+  return {
+    type: MUTES_EXPAND_REQUEST,
+  };
+}
+
+export function expandMutesSuccess(accounts, next) {
+  return {
+    type: MUTES_EXPAND_SUCCESS,
+    accounts,
+    next,
+  };
+}
+
+export function expandMutesFail(error) {
+  return {
+    type: MUTES_EXPAND_FAIL,
+    error,
+  };
+}
+
+export function initMuteModal(account) {
+  return dispatch => {
+    dispatch({
+      type: MUTES_INIT_MODAL,
+      account,
+    });
+
+    dispatch(openModal({ modalType: 'MUTE' }));
+  };
+}
+
+export function toggleHideNotifications() {
+  return dispatch => {
+    dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS });
+  };
+}
+
+export function changeMuteDuration(duration) {
+  return dispatch => {
+    dispatch({
+      type: MUTES_CHANGE_DURATION,
+      duration,
+    });
+  };
+}
diff --git a/app/javascript/flavours/blobfox/actions/notifications.js b/app/javascript/flavours/blobfox/actions/notifications.js
new file mode 100644
index 00000000000000..ced89453d236a5
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/notifications.js
@@ -0,0 +1,404 @@
+import { IntlMessageFormat } from 'intl-messageformat';
+import { defineMessages } from 'react-intl';
+
+import { List as ImmutableList } from 'immutable';
+
+import { compareId } from 'flavours/blobfox/compare_id';
+import { usePendingItems as preferPendingItems } from 'flavours/blobfox/initial_state';
+
+import api, { getLinks } from '../api';
+import { unescapeHTML } from '../utils/html';
+import { requestNotificationPermission } from '../utils/notifications';
+
+import { fetchFollowRequests, fetchRelationships } from './accounts';
+import {
+  importFetchedAccount,
+  importFetchedAccounts,
+  importFetchedStatus,
+  importFetchedStatuses,
+} from './importer';
+import { submitMarkers } from './markers';
+import { notificationsUpdate } from "./notifications_typed";
+import { register as registerPushNotifications } from './push_notifications';
+import { saveSettings } from './settings';
+
+export * from "./notifications_typed";
+
+export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
+
+// tracking the notif cleaning request
+export const NOTIFICATIONS_DELETE_MARKED_REQUEST = 'NOTIFICATIONS_DELETE_MARKED_REQUEST';
+export const NOTIFICATIONS_DELETE_MARKED_SUCCESS = 'NOTIFICATIONS_DELETE_MARKED_SUCCESS';
+export const NOTIFICATIONS_DELETE_MARKED_FAIL = 'NOTIFICATIONS_DELETE_MARKED_FAIL';
+export const NOTIFICATIONS_MARK_ALL_FOR_DELETE = 'NOTIFICATIONS_MARK_ALL_FOR_DELETE';
+export const NOTIFICATIONS_ENTER_CLEARING_MODE = 'NOTIFICATIONS_ENTER_CLEARING_MODE'; // arg: yes
+// Unmark notifications (when the cleaning mode is left)
+export const NOTIFICATIONS_UNMARK_ALL_FOR_DELETE = 'NOTIFICATIONS_UNMARK_ALL_FOR_DELETE';
+// Mark one for delete
+export const NOTIFICATION_MARK_FOR_DELETE = 'NOTIFICATION_MARK_FOR_DELETE';
+
+export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST';
+export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS';
+export const NOTIFICATIONS_EXPAND_FAIL    = 'NOTIFICATIONS_EXPAND_FAIL';
+
+export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET';
+
+export const NOTIFICATIONS_CLEAR        = 'NOTIFICATIONS_CLEAR';
+export const NOTIFICATIONS_SCROLL_TOP   = 'NOTIFICATIONS_SCROLL_TOP';
+export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING';
+
+export const NOTIFICATIONS_MOUNT   = 'NOTIFICATIONS_MOUNT';
+export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT';
+
+export const NOTIFICATIONS_SET_VISIBILITY = 'NOTIFICATIONS_SET_VISIBILITY';
+
+export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ';
+
+export const NOTIFICATIONS_SET_BROWSER_SUPPORT    = 'NOTIFICATIONS_SET_BROWSER_SUPPORT';
+export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION';
+
+defineMessages({
+  mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
+});
+
+const fetchRelatedRelationships = (dispatch, notifications) => {
+  const accountIds = notifications.filter(item => ['follow', 'follow_request', 'admin.sign_up'].indexOf(item.type) !== -1).map(item => item.account.id);
+
+  if (accountIds.length > 0) {
+    dispatch(fetchRelationships(accountIds));
+  }
+};
+
+export const loadPending = () => ({
+  type: NOTIFICATIONS_LOAD_PENDING,
+});
+
+export function updateNotifications(notification, intlMessages, intlLocale) {
+  return (dispatch, getState) => {
+    const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']);
+    const showInColumn = activeFilter === 'all' ? getState().getIn(['settings', 'notifications', 'shows', notification.type], true) : activeFilter === notification.type;
+    const showAlert    = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true);
+    const playSound    = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true);
+
+    let filtered = false;
+
+    if (['mention', 'status'].includes(notification.type) && notification.status.filtered) {
+      const filters = notification.status.filtered.filter(result => result.filter.context.includes('notifications'));
+
+      if (filters.some(result => result.filter.filter_action === 'hide')) {
+        return;
+      }
+
+      filtered = filters.length > 0;
+    }
+
+    if (['follow_request'].includes(notification.type)) {
+      dispatch(fetchFollowRequests());
+    }
+
+    dispatch(submitMarkers());
+
+    if (showInColumn) {
+      dispatch(importFetchedAccount(notification.account));
+
+      if (notification.status) {
+        dispatch(importFetchedStatus(notification.status));
+      }
+
+      if (notification.report) {
+        dispatch(importFetchedAccount(notification.report.target_account));
+      }
+
+
+      dispatch(notificationsUpdate({ notification, preferPendingItems, playSound: playSound && !filtered}));
+
+      fetchRelatedRelationships(dispatch, [notification]);
+    } else if (playSound && !filtered) {
+      dispatch({
+        type: NOTIFICATIONS_UPDATE_NOOP,
+        meta: { sound: 'boop' },
+      });
+    }
+
+    // Desktop notifications
+    if (typeof window.Notification !== 'undefined' && showAlert && !filtered) {
+      const title = new IntlMessageFormat(intlMessages[`notification.${notification.type}`], intlLocale).format({ name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username });
+      const body  = (notification.status && notification.status.spoiler_text.length > 0) ? notification.status.spoiler_text : unescapeHTML(notification.status ? notification.status.content : '');
+
+      const notify = new Notification(title, { body, icon: notification.account.avatar, tag: notification.id });
+
+      notify.addEventListener('click', () => {
+        window.focus();
+        notify.close();
+      });
+    }
+  };
+}
+
+const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
+
+const excludeTypesFromFilter = filter => {
+  const allTypes = ImmutableList([
+    'follow',
+    'follow_request',
+    'favourite',
+    'reaction',
+    'reblog',
+    'mention',
+    'poll',
+    'status',
+    'update',
+    'admin.sign_up',
+    'admin.report',
+  ]);
+
+  return allTypes.filterNot(item => item === filter).toJS();
+};
+
+const noOp = () => {};
+
+let expandNotificationsController = new AbortController();
+
+export function expandNotifications({ maxId, forceLoad } = {}, done = noOp) {
+  return (dispatch, getState) => {
+    const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']);
+    const notifications = getState().get('notifications');
+    const isLoadingMore = !!maxId;
+
+    if (notifications.get('isLoading')) {
+      if (forceLoad) {
+        expandNotificationsController.abort();
+        expandNotificationsController = new AbortController();
+      } else {
+        done();
+        return;
+      }
+    }
+
+    const params = {
+      max_id: maxId,
+      exclude_types: activeFilter === 'all'
+        ? excludeTypesFromSettings(getState())
+        : excludeTypesFromFilter(activeFilter),
+    };
+
+    if (!params.max_id && (notifications.get('items', ImmutableList()).size + notifications.get('pendingItems', ImmutableList()).size) > 0) {
+      const a = notifications.getIn(['pendingItems', 0, 'id']);
+      const b = notifications.getIn(['items', 0, 'id']);
+
+      if (a && b && compareId(a, b) > 0) {
+        params.since_id = a;
+      } else {
+        params.since_id = b || a;
+      }
+    }
+
+    const isLoadingRecent = !!params.since_id;
+
+    dispatch(expandNotificationsRequest(isLoadingMore));
+
+    api(getState).get('/api/v1/notifications', { params, signal: expandNotificationsController.signal }).then(response => {
+      const next = getLinks(response).refs.find(link => link.rel === 'next');
+
+      dispatch(importFetchedAccounts(response.data.map(item => item.account)));
+      dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
+      dispatch(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account)));
+
+      dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems));
+      fetchRelatedRelationships(dispatch, response.data);
+      dispatch(submitMarkers());
+    }).catch(error => {
+      dispatch(expandNotificationsFail(error, isLoadingMore));
+    }).finally(() => {
+      done();
+    });
+  };
+}
+
+export function expandNotificationsRequest(isLoadingMore) {
+  return {
+    type: NOTIFICATIONS_EXPAND_REQUEST,
+    skipLoading: !isLoadingMore,
+  };
+}
+
+export function expandNotificationsSuccess(notifications, next, isLoadingMore, isLoadingRecent, usePendingItems) {
+  return {
+    type: NOTIFICATIONS_EXPAND_SUCCESS,
+    notifications,
+    next,
+    isLoadingRecent: isLoadingRecent,
+    usePendingItems,
+    skipLoading: !isLoadingMore,
+  };
+}
+
+export function expandNotificationsFail(error, isLoadingMore) {
+  return {
+    type: NOTIFICATIONS_EXPAND_FAIL,
+    error,
+    skipLoading: !isLoadingMore,
+    skipAlert: !isLoadingMore || error.name === 'AbortError',
+  };
+}
+
+export function clearNotifications() {
+  return (dispatch, getState) => {
+    dispatch({
+      type: NOTIFICATIONS_CLEAR,
+    });
+
+    api(getState).post('/api/v1/notifications/clear');
+  };
+}
+
+export function scrollTopNotifications(top) {
+  return {
+    type: NOTIFICATIONS_SCROLL_TOP,
+    top,
+  };
+}
+
+export function deleteMarkedNotifications() {
+  return (dispatch, getState) => {
+    dispatch(deleteMarkedNotificationsRequest());
+
+    let ids = [];
+    getState().getIn(['notifications', 'items']).forEach((n) => {
+      if (n.get('markedForDelete')) {
+        ids.push(n.get('id'));
+      }
+    });
+
+    if (ids.length === 0) {
+      return;
+    }
+
+    api(getState).delete(`/api/v1/notifications/destroy_multiple?ids[]=${ids.join('&ids[]=')}`).then(() => {
+      dispatch(deleteMarkedNotificationsSuccess());
+    }).catch(error => {
+      console.error(error);
+      dispatch(deleteMarkedNotificationsFail(error));
+    });
+  };
+}
+
+export function enterNotificationClearingMode(yes) {
+  return {
+    type: NOTIFICATIONS_ENTER_CLEARING_MODE,
+    yes: yes,
+  };
+}
+
+export function markAllNotifications(yes) {
+  return {
+    type: NOTIFICATIONS_MARK_ALL_FOR_DELETE,
+    yes: yes, // true, false or null. null = invert
+  };
+}
+
+export function deleteMarkedNotificationsRequest() {
+  return {
+    type: NOTIFICATIONS_DELETE_MARKED_REQUEST,
+  };
+}
+
+export function deleteMarkedNotificationsFail() {
+  return {
+    type: NOTIFICATIONS_DELETE_MARKED_FAIL,
+  };
+}
+
+export function markNotificationForDelete(id, yes) {
+  return {
+    type: NOTIFICATION_MARK_FOR_DELETE,
+    id: id,
+    yes: yes,
+  };
+}
+
+export function deleteMarkedNotificationsSuccess() {
+  return {
+    type: NOTIFICATIONS_DELETE_MARKED_SUCCESS,
+  };
+}
+
+export function mountNotifications() {
+  return {
+    type: NOTIFICATIONS_MOUNT,
+  };
+}
+
+export function unmountNotifications() {
+  return {
+    type: NOTIFICATIONS_UNMOUNT,
+  };
+}
+
+export function notificationsSetVisibility(visibility) {
+  return {
+    type: NOTIFICATIONS_SET_VISIBILITY,
+    visibility: visibility,
+  };
+}
+
+export function setFilter (filterType) {
+  return dispatch => {
+    dispatch({
+      type: NOTIFICATIONS_FILTER_SET,
+      path: ['notifications', 'quickFilter', 'active'],
+      value: filterType,
+    });
+    dispatch(expandNotifications({ forceLoad: true }));
+    dispatch(saveSettings());
+  };
+}
+
+export function markNotificationsAsRead() {
+  return {
+    type: NOTIFICATIONS_MARK_AS_READ,
+  };
+}
+
+// Browser support
+export function setupBrowserNotifications() {
+  return dispatch => {
+    dispatch(setBrowserSupport('Notification' in window));
+    if ('Notification' in window) {
+      dispatch(setBrowserPermission(Notification.permission));
+    }
+
+    if ('Notification' in window && 'permissions' in navigator) {
+      navigator.permissions.query({ name: 'notifications' }).then((status) => {
+        status.onchange = () => dispatch(setBrowserPermission(Notification.permission));
+      }).catch(console.warn);
+    }
+  };
+}
+
+export function requestBrowserPermission(callback = noOp) {
+  return dispatch => {
+    requestNotificationPermission((permission) => {
+      dispatch(setBrowserPermission(permission));
+      callback(permission);
+
+      if (permission === 'granted') {
+        dispatch(registerPushNotifications());
+      }
+    });
+  };
+}
+
+export function setBrowserSupport (value) {
+  return {
+    type: NOTIFICATIONS_SET_BROWSER_SUPPORT,
+    value,
+  };
+}
+
+export function setBrowserPermission (value) {
+  return {
+    type: NOTIFICATIONS_SET_BROWSER_PERMISSION,
+    value,
+  };
+}
diff --git a/app/javascript/flavours/blobfox/actions/notifications_typed.ts b/app/javascript/flavours/blobfox/actions/notifications_typed.ts
new file mode 100644
index 00000000000000..176362f4b1edd4
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/notifications_typed.ts
@@ -0,0 +1,23 @@
+import { createAction } from '@reduxjs/toolkit';
+
+import type { ApiAccountJSON } from '../api_types/accounts';
+// To be replaced once ApiNotificationJSON type exists
+interface FakeApiNotificationJSON {
+  type: string;
+  account: ApiAccountJSON;
+}
+
+export const notificationsUpdate = createAction(
+  'notifications/update',
+  ({
+    playSound,
+    ...args
+  }: {
+    notification: FakeApiNotificationJSON;
+    usePendingItems: boolean;
+    playSound: boolean;
+  }) => ({
+    payload: args,
+    meta: { sound: playSound ? 'boop' : undefined },
+  }),
+);
diff --git a/app/javascript/flavours/blobfox/actions/onboarding.js b/app/javascript/flavours/blobfox/actions/onboarding.js
new file mode 100644
index 00000000000000..a1dd3a731eddc1
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/onboarding.js
@@ -0,0 +1,8 @@
+import { changeSetting, saveSettings } from './settings';
+
+export const INTRODUCTION_VERSION = 20181216044202;
+
+export const closeOnboarding = () => dispatch => {
+  dispatch(changeSetting(['introductionVersion'], INTRODUCTION_VERSION));
+  dispatch(saveSettings());
+};
diff --git a/app/javascript/flavours/blobfox/actions/picture_in_picture.js b/app/javascript/flavours/blobfox/actions/picture_in_picture.js
new file mode 100644
index 00000000000000..898375abeb9337
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/picture_in_picture.js
@@ -0,0 +1,46 @@
+// @ts-check
+
+export const PICTURE_IN_PICTURE_DEPLOY = 'PICTURE_IN_PICTURE_DEPLOY';
+export const PICTURE_IN_PICTURE_REMOVE = 'PICTURE_IN_PICTURE_REMOVE';
+
+/**
+ * @typedef MediaProps
+ * @property {string} src
+ * @property {boolean} muted
+ * @property {number} volume
+ * @property {number} currentTime
+ * @property {string} poster
+ * @property {string} backgroundColor
+ * @property {string} foregroundColor
+ * @property {string} accentColor
+ */
+
+/**
+ * @param {string} statusId
+ * @param {string} accountId
+ * @param {string} playerType
+ * @param {MediaProps} props
+ * @returns {object}
+ */
+export const deployPictureInPicture = (statusId, accountId, playerType, props) => {
+  // @ts-expect-error
+  return (dispatch, getState) => {
+    // Do not open a player for a toot that does not exist
+    if (getState().hasIn(['statuses', statusId])) {
+      dispatch({
+        type: PICTURE_IN_PICTURE_DEPLOY,
+        statusId,
+        accountId,
+        playerType,
+        props,
+      });
+    }
+  };
+};
+
+/*
+ * @return {object}
+ */
+export const removePictureInPicture = () => ({
+  type: PICTURE_IN_PICTURE_REMOVE,
+});
diff --git a/app/javascript/flavours/blobfox/actions/pin_statuses.js b/app/javascript/flavours/blobfox/actions/pin_statuses.js
new file mode 100644
index 00000000000000..baa10d15627d66
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/pin_statuses.js
@@ -0,0 +1,42 @@
+import api from '../api';
+import { me } from '../initial_state';
+
+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';
+
+export function fetchPinnedStatuses() {
+  return (dispatch, getState) => {
+    dispatch(fetchPinnedStatusesRequest());
+
+    api(getState).get(`/api/v1/accounts/${me}/statuses`, { params: { pinned: true } }).then(response => {
+      dispatch(importFetchedStatuses(response.data));
+      dispatch(fetchPinnedStatusesSuccess(response.data, null));
+    }).catch(error => {
+      dispatch(fetchPinnedStatusesFail(error));
+    });
+  };
+}
+
+export function fetchPinnedStatusesRequest() {
+  return {
+    type: PINNED_STATUSES_FETCH_REQUEST,
+  };
+}
+
+export function fetchPinnedStatusesSuccess(statuses, next) {
+  return {
+    type: PINNED_STATUSES_FETCH_SUCCESS,
+    statuses,
+    next,
+  };
+}
+
+export function fetchPinnedStatusesFail(error) {
+  return {
+    type: PINNED_STATUSES_FETCH_FAIL,
+    error,
+  };
+}
diff --git a/app/javascript/flavours/blobfox/actions/polls.js b/app/javascript/flavours/blobfox/actions/polls.js
new file mode 100644
index 00000000000000..a37410dc90fa4d
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/polls.js
@@ -0,0 +1,61 @@
+import api from '../api';
+
+import { importFetchedPoll } from './importer';
+
+export const POLL_VOTE_REQUEST = 'POLL_VOTE_REQUEST';
+export const POLL_VOTE_SUCCESS = 'POLL_VOTE_SUCCESS';
+export const POLL_VOTE_FAIL    = 'POLL_VOTE_FAIL';
+
+export const POLL_FETCH_REQUEST = 'POLL_FETCH_REQUEST';
+export const POLL_FETCH_SUCCESS = 'POLL_FETCH_SUCCESS';
+export const POLL_FETCH_FAIL    = 'POLL_FETCH_FAIL';
+
+export const vote = (pollId, choices) => (dispatch, getState) => {
+  dispatch(voteRequest());
+
+  api(getState).post(`/api/v1/polls/${pollId}/votes`, { choices })
+    .then(({ data }) => {
+      dispatch(importFetchedPoll(data));
+      dispatch(voteSuccess(data));
+    })
+    .catch(err => dispatch(voteFail(err)));
+};
+
+export const fetchPoll = pollId => (dispatch, getState) => {
+  dispatch(fetchPollRequest());
+
+  api(getState).get(`/api/v1/polls/${pollId}`)
+    .then(({ data }) => {
+      dispatch(importFetchedPoll(data));
+      dispatch(fetchPollSuccess(data));
+    })
+    .catch(err => dispatch(fetchPollFail(err)));
+};
+
+export const voteRequest = () => ({
+  type: POLL_VOTE_REQUEST,
+});
+
+export const voteSuccess = poll => ({
+  type: POLL_VOTE_SUCCESS,
+  poll,
+});
+
+export const voteFail = error => ({
+  type: POLL_VOTE_FAIL,
+  error,
+});
+
+export const fetchPollRequest = () => ({
+  type: POLL_FETCH_REQUEST,
+});
+
+export const fetchPollSuccess = poll => ({
+  type: POLL_FETCH_SUCCESS,
+  poll,
+});
+
+export const fetchPollFail = error => ({
+  type: POLL_FETCH_FAIL,
+  error,
+});
diff --git a/app/javascript/flavours/blobfox/actions/push_notifications/index.js b/app/javascript/flavours/blobfox/actions/push_notifications/index.js
new file mode 100644
index 00000000000000..46b63867f1c224
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/push_notifications/index.js
@@ -0,0 +1,17 @@
+import { saveSettings } from './registerer';
+import { setAlerts } from './setter';
+
+export function changeAlerts(path, value) {
+  return dispatch => {
+    dispatch(setAlerts(path, value));
+    dispatch(saveSettings());
+  };
+}
+
+export {
+  CLEAR_SUBSCRIPTION,
+  SET_BROWSER_SUPPORT,
+  SET_SUBSCRIPTION,
+  SET_ALERTS,
+} from './setter';
+export { register } from './registerer';
diff --git a/app/javascript/flavours/blobfox/actions/push_notifications/registerer.js b/app/javascript/flavours/blobfox/actions/push_notifications/registerer.js
new file mode 100644
index 00000000000000..b3d3850e31d115
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/push_notifications/registerer.js
@@ -0,0 +1,134 @@
+import api from '../../api';
+import { me } from '../../initial_state';
+import { pushNotificationsSetting } from '../../settings';
+import { decode as decodeBase64 } from '../../utils/base64';
+
+import { setBrowserSupport, setSubscription, clearSubscription } from './setter';
+
+// Taken from https://www.npmjs.com/package/web-push
+const urlBase64ToUint8Array = (base64String) => {
+  const padding = '='.repeat((4 - base64String.length % 4) % 4);
+  const base64 = (base64String + padding)
+    .replace(/-/g, '+')
+    .replace(/_/g, '/');
+
+  return decodeBase64(base64);
+};
+
+const getApplicationServerKey = () => document.querySelector('[name="applicationServerKey"]').getAttribute('content');
+
+const getRegistration = () => navigator.serviceWorker.ready;
+
+const getPushSubscription = (registration) =>
+  registration.pushManager.getSubscription()
+    .then(subscription => ({ registration, subscription }));
+
+const subscribe = (registration) =>
+  registration.pushManager.subscribe({
+    userVisibleOnly: true,
+    applicationServerKey: urlBase64ToUint8Array(getApplicationServerKey()),
+  });
+
+const unsubscribe = ({ registration, subscription }) =>
+  subscription ? subscription.unsubscribe().then(() => registration) : registration;
+
+const sendSubscriptionToBackend = (subscription) => {
+  const params = { subscription };
+
+  if (me) {
+    const data = pushNotificationsSetting.get(me);
+    if (data) {
+      params.data = data;
+    }
+  }
+
+  return api().post('/api/web/push_subscriptions', params).then(response => response.data);
+};
+
+// Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload
+const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager' in window && 'getKey' in PushSubscription.prototype);
+
+export function register () {
+  return (dispatch, getState) => {
+    dispatch(setBrowserSupport(supportsPushNotifications));
+
+    if (supportsPushNotifications) {
+      if (!getApplicationServerKey()) {
+        console.error('The VAPID public key is not set. You will not be able to receive Web Push Notifications.');
+        return;
+      }
+
+      getRegistration()
+        .then(getPushSubscription)
+        .then(({ registration, subscription }) => {
+          if (subscription !== null) {
+            // We have a subscription, check if it is still valid
+            const currentServerKey = (new Uint8Array(subscription.options.applicationServerKey)).toString();
+            const subscriptionServerKey = urlBase64ToUint8Array(getApplicationServerKey()).toString();
+            const serverEndpoint = getState().getIn(['push_notifications', 'subscription', 'endpoint']);
+
+            // If the VAPID public key did not change and the endpoint corresponds
+            // to the endpoint saved in the backend, the subscription is valid
+            if (subscriptionServerKey === currentServerKey && subscription.endpoint === serverEndpoint) {
+              return subscription;
+            } else {
+              // Something went wrong, try to subscribe again
+              return unsubscribe({ registration, subscription }).then(subscribe).then(
+                subscription => sendSubscriptionToBackend(subscription));
+            }
+          }
+
+          // No subscription, try to subscribe
+          return subscribe(registration).then(
+            subscription => sendSubscriptionToBackend(subscription));
+        })
+        .then(subscription => {
+          // If we got a PushSubscription (and not a subscription object from the backend)
+          // it means that the backend subscription is valid (and was set during hydration)
+          if (!(subscription instanceof PushSubscription)) {
+            dispatch(setSubscription(subscription));
+            if (me) {
+              pushNotificationsSetting.set(me, { alerts: subscription.alerts });
+            }
+          }
+        })
+        .catch(error => {
+          if (error.code === 20 && error.name === 'AbortError') {
+            console.warn('Your browser supports Web Push Notifications, but does not seem to implement the VAPID protocol.');
+          } else if (error.code === 5 && error.name === 'InvalidCharacterError') {
+            console.error('The VAPID public key seems to be invalid:', getApplicationServerKey());
+          }
+
+          // Clear alerts and hide UI settings
+          dispatch(clearSubscription());
+          if (me) {
+            pushNotificationsSetting.remove(me);
+          }
+
+          return getRegistration()
+            .then(getPushSubscription)
+            .then(unsubscribe);
+        })
+        .catch(console.warn);
+    } else {
+      console.warn('Your browser does not support Web Push Notifications.');
+    }
+  };
+}
+
+export function saveSettings() {
+  return (_, getState) => {
+    const state = getState().get('push_notifications');
+    const subscription = state.get('subscription');
+    const alerts = state.get('alerts');
+    const data = { alerts };
+
+    api().put(`/api/web/push_subscriptions/${subscription.get('id')}`, {
+      data,
+    }).then(() => {
+      if (me) {
+        pushNotificationsSetting.set(me, data);
+      }
+    }).catch(console.warn);
+  };
+}
diff --git a/app/javascript/flavours/blobfox/actions/push_notifications/setter.js b/app/javascript/flavours/blobfox/actions/push_notifications/setter.js
new file mode 100644
index 00000000000000..5561766bff42b3
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/push_notifications/setter.js
@@ -0,0 +1,34 @@
+export const SET_BROWSER_SUPPORT = 'PUSH_NOTIFICATIONS_SET_BROWSER_SUPPORT';
+export const SET_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_SET_SUBSCRIPTION';
+export const CLEAR_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_CLEAR_SUBSCRIPTION';
+export const SET_ALERTS = 'PUSH_NOTIFICATIONS_SET_ALERTS';
+
+export function setBrowserSupport (value) {
+  return {
+    type: SET_BROWSER_SUPPORT,
+    value,
+  };
+}
+
+export function setSubscription (subscription) {
+  return {
+    type: SET_SUBSCRIPTION,
+    subscription,
+  };
+}
+
+export function clearSubscription () {
+  return {
+    type: CLEAR_SUBSCRIPTION,
+  };
+}
+
+export function setAlerts (path, value) {
+  return dispatch => {
+    dispatch({
+      type: SET_ALERTS,
+      path,
+      value,
+    });
+  };
+}
diff --git a/app/javascript/flavours/blobfox/actions/reports.js b/app/javascript/flavours/blobfox/actions/reports.js
new file mode 100644
index 00000000000000..756b8cd05e1648
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/reports.js
@@ -0,0 +1,42 @@
+import api from '../api';
+
+import { openModal } from './modal';
+
+export const REPORT_SUBMIT_REQUEST = 'REPORT_SUBMIT_REQUEST';
+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'),
+    },
+  }));
+
+export const submitReport = (params, onSuccess, onFail) => (dispatch, getState) => {
+  dispatch(submitReportRequest());
+
+  api(getState).post('/api/v1/reports', params).then(response => {
+    dispatch(submitReportSuccess(response.data));
+    if (onSuccess) onSuccess();
+  }).catch(error => {
+    dispatch(submitReportFail(error));
+    if (onFail) onFail();
+  });
+};
+
+export const submitReportRequest = () => ({
+  type: REPORT_SUBMIT_REQUEST,
+});
+
+export const submitReportSuccess = report => ({
+  type: REPORT_SUBMIT_SUCCESS,
+  report,
+});
+
+export const submitReportFail = error => ({
+  type: REPORT_SUBMIT_FAIL,
+  error,
+});
diff --git a/app/javascript/flavours/blobfox/actions/search.js b/app/javascript/flavours/blobfox/actions/search.js
new file mode 100644
index 00000000000000..7fcc1edcd8db07
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/search.js
@@ -0,0 +1,201 @@
+import { fromJS } from 'immutable';
+
+import { searchHistory } from 'flavours/blobfox/settings';
+
+import api from '../api';
+
+import { fetchRelationships } from './accounts';
+import { importFetchedAccounts, importFetchedStatuses } from './importer';
+
+export const SEARCH_CHANGE = 'SEARCH_CHANGE';
+export const SEARCH_CLEAR  = 'SEARCH_CLEAR';
+export const SEARCH_SHOW   = 'SEARCH_SHOW';
+
+export const SEARCH_FETCH_REQUEST = 'SEARCH_FETCH_REQUEST';
+export const SEARCH_FETCH_SUCCESS = 'SEARCH_FETCH_SUCCESS';
+export const SEARCH_FETCH_FAIL    = 'SEARCH_FETCH_FAIL';
+
+export const SEARCH_EXPAND_REQUEST = 'SEARCH_EXPAND_REQUEST';
+export const SEARCH_EXPAND_SUCCESS = 'SEARCH_EXPAND_SUCCESS';
+export const SEARCH_EXPAND_FAIL    = 'SEARCH_EXPAND_FAIL';
+
+export const SEARCH_HISTORY_UPDATE  = 'SEARCH_HISTORY_UPDATE';
+
+export function changeSearch(value) {
+  return {
+    type: SEARCH_CHANGE,
+    value,
+  };
+}
+
+export function clearSearch() {
+  return {
+    type: SEARCH_CLEAR,
+  };
+}
+
+export function submitSearch(type) {
+  return (dispatch, getState) => {
+    const value    = getState().getIn(['search', 'value']);
+    const signedIn = !!getState().getIn(['meta', 'me']);
+
+    if (value.length === 0) {
+      dispatch(fetchSearchSuccess({ accounts: [], statuses: [], hashtags: [] }, '', type));
+      return;
+    }
+
+    dispatch(fetchSearchRequest(type));
+
+    api(getState).get('/api/v2/search', {
+      params: {
+        q: value,
+        resolve: signedIn,
+        limit: 11,
+        type,
+      },
+    }).then(response => {
+      if (response.data.accounts) {
+        dispatch(importFetchedAccounts(response.data.accounts));
+      }
+
+      if (response.data.statuses) {
+        dispatch(importFetchedStatuses(response.data.statuses));
+      }
+
+      dispatch(fetchSearchSuccess(response.data, value, type));
+      dispatch(fetchRelationships(response.data.accounts.map(item => item.id)));
+    }).catch(error => {
+      dispatch(fetchSearchFail(error));
+    });
+  };
+}
+
+export function fetchSearchRequest(searchType) {
+  return {
+    type: SEARCH_FETCH_REQUEST,
+    searchType,
+  };
+}
+
+export function fetchSearchSuccess(results, searchTerm, searchType) {
+  return {
+    type: SEARCH_FETCH_SUCCESS,
+    results,
+    searchType,
+    searchTerm,
+  };
+}
+
+export function fetchSearchFail(error) {
+  return {
+    type: SEARCH_FETCH_FAIL,
+    error,
+  };
+}
+
+export const expandSearch = type => (dispatch, getState) => {
+  const value  = getState().getIn(['search', 'value']);
+  const offset = getState().getIn(['search', 'results', type]).size - 1;
+
+  dispatch(expandSearchRequest(type));
+
+  api(getState).get('/api/v2/search', {
+    params: {
+      q: value,
+      type,
+      offset,
+      limit: 11,
+    },
+  }).then(({ data }) => {
+    if (data.accounts) {
+      dispatch(importFetchedAccounts(data.accounts));
+    }
+
+    if (data.statuses) {
+      dispatch(importFetchedStatuses(data.statuses));
+    }
+
+    dispatch(expandSearchSuccess(data, value, type));
+    dispatch(fetchRelationships(data.accounts.map(item => item.id)));
+  }).catch(error => {
+    dispatch(expandSearchFail(error));
+  });
+};
+
+export const expandSearchRequest = (searchType) => ({
+  type: SEARCH_EXPAND_REQUEST,
+  searchType,
+});
+
+export const expandSearchSuccess = (results, searchTerm, searchType) => ({
+  type: SEARCH_EXPAND_SUCCESS,
+  results,
+  searchTerm,
+  searchType,
+});
+
+export const expandSearchFail = error => ({
+  type: SEARCH_EXPAND_FAIL,
+  error,
+});
+
+export const showSearch = () => ({
+  type: SEARCH_SHOW,
+});
+
+export const openURL = routerHistory => (dispatch, getState) => {
+  const value = getState().getIn(['search', 'value']);
+  const signedIn = !!getState().getIn(['meta', 'me']);
+
+  if (!signedIn) {
+    return;
+  }
+
+  dispatch(fetchSearchRequest());
+
+  api(getState).get('/api/v2/search', { params: { q: value, resolve: true } }).then(response => {
+    if (response.data.accounts?.length > 0) {
+      dispatch(importFetchedAccounts(response.data.accounts));
+      routerHistory.push(`/@${response.data.accounts[0].acct}`);
+    } else if (response.data.statuses?.length > 0) {
+      dispatch(importFetchedStatuses(response.data.statuses));
+      routerHistory.push(`/@${response.data.statuses[0].account.acct}/${response.data.statuses[0].id}`);
+    }
+
+    dispatch(fetchSearchSuccess(response.data, value));
+  }).catch(err => {
+    dispatch(fetchSearchFail(err));
+  });
+};
+
+export const clickSearchResult = (q, type) => (dispatch, getState) => {
+  const previous = getState().getIn(['search', 'recent']);
+  const me = getState().getIn(['meta', 'me']);
+  const current = previous.add(fromJS({ type, q })).takeLast(4);
+
+  searchHistory.set(me, current.toJS());
+  dispatch(updateSearchHistory(current));
+};
+
+export const forgetSearchResult = q => (dispatch, getState) => {
+  const previous = getState().getIn(['search', 'recent']);
+  const me = getState().getIn(['meta', 'me']);
+  const current = previous.filterNot(result => result.get('q') === q);
+
+  searchHistory.set(me, current.toJS());
+  dispatch(updateSearchHistory(current));
+};
+
+export const updateSearchHistory = recent => ({
+  type: SEARCH_HISTORY_UPDATE,
+  recent,
+});
+
+export const hydrateSearch = () => (dispatch, getState) => {
+  const me = getState().getIn(['meta', 'me']);
+  const history = searchHistory.get(me);
+
+  if (history !== null) {
+    dispatch(updateSearchHistory(history));
+  }
+};
\ No newline at end of file
diff --git a/app/javascript/flavours/blobfox/actions/server.js b/app/javascript/flavours/blobfox/actions/server.js
new file mode 100644
index 00000000000000..65f3efc3a72a3d
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/server.js
@@ -0,0 +1,131 @@
+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';
+
+export const SERVER_DOMAIN_BLOCKS_FETCH_REQUEST = 'SERVER_DOMAIN_BLOCKS_FETCH_REQUEST';
+export const SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS = 'SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS';
+export const SERVER_DOMAIN_BLOCKS_FETCH_FAIL    = 'SERVER_DOMAIN_BLOCKS_FETCH_FAIL';
+
+export const fetchServer = () => (dispatch, getState) => {
+  if (getState().getIn(['server', 'server', 'isLoading'])) {
+    return;
+  }
+
+  dispatch(fetchServerRequest());
+
+  api(getState)
+    .get('/api/v2/instance').then(({ data }) => {
+      if (data.contact.account) dispatch(importFetchedAccount(data.contact.account));
+      dispatch(fetchServerSuccess(data));
+    }).catch(err => dispatch(fetchServerFail(err)));
+};
+
+const fetchServerRequest = () => ({
+  type: SERVER_FETCH_REQUEST,
+});
+
+const fetchServerSuccess = server => ({
+  type: SERVER_FETCH_SUCCESS,
+  server,
+});
+
+const fetchServerFail = error => ({
+  type: SERVER_FETCH_FAIL,
+  error,
+});
+
+export const fetchServerTranslationLanguages = () => (dispatch, getState) => {
+  dispatch(fetchServerTranslationLanguagesRequest());
+
+  api(getState)
+    .get('/api/v1/instance/translation_languages').then(({ data }) => {
+      dispatch(fetchServerTranslationLanguagesSuccess(data));
+    }).catch(err => dispatch(fetchServerTranslationLanguagesFail(err)));
+};
+
+const fetchServerTranslationLanguagesRequest = () => ({
+  type: SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST,
+});
+
+const fetchServerTranslationLanguagesSuccess = translationLanguages => ({
+  type: SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS,
+  translationLanguages,
+});
+
+const fetchServerTranslationLanguagesFail = error => ({
+  type: SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL,
+  error,
+});
+
+export const fetchExtendedDescription = () => (dispatch, getState) => {
+  if (getState().getIn(['server', 'extendedDescription', 'isLoading'])) {
+    return;
+  }
+
+  dispatch(fetchExtendedDescriptionRequest());
+
+  api(getState)
+    .get('/api/v1/instance/extended_description')
+    .then(({ data }) => dispatch(fetchExtendedDescriptionSuccess(data)))
+    .catch(err => dispatch(fetchExtendedDescriptionFail(err)));
+};
+
+const fetchExtendedDescriptionRequest = () => ({
+  type: EXTENDED_DESCRIPTION_REQUEST,
+});
+
+const fetchExtendedDescriptionSuccess = description => ({
+  type: EXTENDED_DESCRIPTION_SUCCESS,
+  description,
+});
+
+const fetchExtendedDescriptionFail = error => ({
+  type: EXTENDED_DESCRIPTION_FAIL,
+  error,
+});
+
+export const fetchDomainBlocks = () => (dispatch, getState) => {
+  if (getState().getIn(['server', 'domainBlocks', 'isLoading'])) {
+    return;
+  }
+
+  dispatch(fetchDomainBlocksRequest());
+
+  api(getState)
+    .get('/api/v1/instance/domain_blocks')
+    .then(({ data }) => dispatch(fetchDomainBlocksSuccess(true, data)))
+    .catch(err => {
+      if (err.response.status === 404) {
+        dispatch(fetchDomainBlocksSuccess(false, []));
+      } else {
+        dispatch(fetchDomainBlocksFail(err));
+      }
+    });
+};
+
+const fetchDomainBlocksRequest = () => ({
+  type: SERVER_DOMAIN_BLOCKS_FETCH_REQUEST,
+});
+
+const fetchDomainBlocksSuccess = (isAvailable, blocks) => ({
+  type: SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS,
+  isAvailable,
+  blocks,
+});
+
+const fetchDomainBlocksFail = error => ({
+  type: SERVER_DOMAIN_BLOCKS_FETCH_FAIL,
+  error,
+});
diff --git a/app/javascript/flavours/blobfox/actions/settings.js b/app/javascript/flavours/blobfox/actions/settings.js
new file mode 100644
index 00000000000000..3685b0684e0b83
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/settings.js
@@ -0,0 +1,36 @@
+import { debounce } from 'lodash';
+
+import api from '../api';
+
+import { showAlertForError } from './alerts';
+
+export const SETTING_CHANGE = 'SETTING_CHANGE';
+export const SETTING_SAVE   = 'SETTING_SAVE';
+
+export function changeSetting(path, value) {
+  return dispatch => {
+    dispatch({
+      type: SETTING_CHANGE,
+      path,
+      value,
+    });
+
+    dispatch(saveSettings());
+  };
+}
+
+const debouncedSave = debounce((dispatch, getState) => {
+  if (getState().getIn(['settings', 'saved'])) {
+    return;
+  }
+
+  const data = getState().get('settings').filter((_, path) => path !== 'saved').toJS();
+
+  api().put('/api/web/settings', { data })
+    .then(() => dispatch({ type: SETTING_SAVE }))
+    .catch(error => dispatch(showAlertForError(error)));
+}, 5000, { trailing: true });
+
+export function saveSettings() {
+  return (dispatch, getState) => debouncedSave(dispatch, getState);
+}
diff --git a/app/javascript/flavours/blobfox/actions/statuses.js b/app/javascript/flavours/blobfox/actions/statuses.js
new file mode 100644
index 00000000000000..5bdd31c3438cbe
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/statuses.js
@@ -0,0 +1,351 @@
+import api from '../api';
+
+import { ensureComposeIsVisible, setComposeToStatus } from './compose';
+import { importFetchedStatus, importFetchedStatuses } from './importer';
+import { deleteFromTimelines } from './timelines';
+
+export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
+export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
+export const STATUS_FETCH_FAIL    = 'STATUS_FETCH_FAIL';
+
+export const STATUS_DELETE_REQUEST = 'STATUS_DELETE_REQUEST';
+export const STATUS_DELETE_SUCCESS = 'STATUS_DELETE_SUCCESS';
+export const STATUS_DELETE_FAIL    = 'STATUS_DELETE_FAIL';
+
+export const CONTEXT_FETCH_REQUEST = 'CONTEXT_FETCH_REQUEST';
+export const CONTEXT_FETCH_SUCCESS = 'CONTEXT_FETCH_SUCCESS';
+export const CONTEXT_FETCH_FAIL    = 'CONTEXT_FETCH_FAIL';
+
+export const STATUS_MUTE_REQUEST = 'STATUS_MUTE_REQUEST';
+export const STATUS_MUTE_SUCCESS = 'STATUS_MUTE_SUCCESS';
+export const STATUS_MUTE_FAIL    = 'STATUS_MUTE_FAIL';
+
+export const STATUS_UNMUTE_REQUEST = 'STATUS_UNMUTE_REQUEST';
+export const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS';
+export const STATUS_UNMUTE_FAIL    = 'STATUS_UNMUTE_FAIL';
+
+export const STATUS_REVEAL   = 'STATUS_REVEAL';
+export const STATUS_HIDE     = 'STATUS_HIDE';
+export const STATUS_COLLAPSE = 'STATUS_COLLAPSE';
+
+export const REDRAFT = 'REDRAFT';
+
+export const STATUS_FETCH_SOURCE_REQUEST = 'STATUS_FETCH_SOURCE_REQUEST';
+export const STATUS_FETCH_SOURCE_SUCCESS = 'STATUS_FETCH_SOURCE_SUCCESS';
+export const STATUS_FETCH_SOURCE_FAIL    = 'STATUS_FETCH_SOURCE_FAIL';
+
+export const STATUS_TRANSLATE_REQUEST = 'STATUS_TRANSLATE_REQUEST';
+export const STATUS_TRANSLATE_SUCCESS = 'STATUS_TRANSLATE_SUCCESS';
+export const STATUS_TRANSLATE_FAIL    = 'STATUS_TRANSLATE_FAIL';
+export const STATUS_TRANSLATE_UNDO    = 'STATUS_TRANSLATE_UNDO';
+
+export function fetchStatusRequest(id, skipLoading) {
+  return {
+    type: STATUS_FETCH_REQUEST,
+    id,
+    skipLoading,
+  };
+}
+
+export function fetchStatus(id, forceFetch = false) {
+  return (dispatch, getState) => {
+    const skipLoading = !forceFetch && getState().getIn(['statuses', id], null) !== null;
+
+    dispatch(fetchContext(id));
+
+    if (skipLoading) {
+      return;
+    }
+
+    dispatch(fetchStatusRequest(id, skipLoading));
+
+    api(getState).get(`/api/v1/statuses/${id}`).then(response => {
+      dispatch(importFetchedStatus(response.data));
+      dispatch(fetchStatusSuccess(skipLoading));
+    }).catch(error => {
+      dispatch(fetchStatusFail(id, error, skipLoading));
+    });
+  };
+}
+
+export function fetchStatusSuccess(skipLoading) {
+  return {
+    type: STATUS_FETCH_SUCCESS,
+    skipLoading,
+  };
+}
+
+export function fetchStatusFail(id, error, skipLoading) {
+  return {
+    type: STATUS_FETCH_FAIL,
+    id,
+    error,
+    skipLoading,
+    skipAlert: true,
+  };
+}
+
+export function redraft(status, raw_text, content_type) {
+  return {
+    type: REDRAFT,
+    status,
+    raw_text,
+    content_type,
+  };
+}
+
+export const editStatus = (id, routerHistory) => (dispatch, getState) => {
+  let status = getState().getIn(['statuses', id]);
+
+  if (status.get('poll')) {
+    status = status.set('poll', getState().getIn(['polls', status.get('poll')]));
+  }
+
+  dispatch(fetchStatusSourceRequest());
+
+  api(getState).get(`/api/v1/statuses/${id}/source`).then(response => {
+    dispatch(fetchStatusSourceSuccess());
+    ensureComposeIsVisible(getState, routerHistory);
+    dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text, response.data.content_type));
+  }).catch(error => {
+    dispatch(fetchStatusSourceFail(error));
+  });
+};
+
+export const fetchStatusSourceRequest = () => ({
+  type: STATUS_FETCH_SOURCE_REQUEST,
+});
+
+export const fetchStatusSourceSuccess = () => ({
+  type: STATUS_FETCH_SOURCE_SUCCESS,
+});
+
+export const fetchStatusSourceFail = error => ({
+  type: STATUS_FETCH_SOURCE_FAIL,
+  error,
+});
+
+export function deleteStatus(id, routerHistory, withRedraft = false) {
+  return (dispatch, getState) => {
+    let status = getState().getIn(['statuses', id]);
+
+    if (status.get('poll')) {
+      status = status.set('poll', getState().getIn(['polls', status.get('poll')]));
+    }
+
+    dispatch(deleteStatusRequest(id));
+
+    api(getState).delete(`/api/v1/statuses/${id}`).then(response => {
+      dispatch(deleteStatusSuccess(id));
+      dispatch(deleteFromTimelines(id));
+
+      if (withRedraft) {
+        dispatch(redraft(status, response.data.text, response.data.content_type));
+
+        ensureComposeIsVisible(getState, routerHistory);
+      }
+    }).catch(error => {
+      dispatch(deleteStatusFail(id, error));
+    });
+  };
+}
+
+export function deleteStatusRequest(id) {
+  return {
+    type: STATUS_DELETE_REQUEST,
+    id: id,
+  };
+}
+
+export function deleteStatusSuccess(id) {
+  return {
+    type: STATUS_DELETE_SUCCESS,
+    id: id,
+  };
+}
+
+export function deleteStatusFail(id, error) {
+  return {
+    type: STATUS_DELETE_FAIL,
+    id: id,
+    error: error,
+  };
+}
+
+export const updateStatus = status => dispatch =>
+  dispatch(importFetchedStatus(status));
+
+export function fetchContext(id) {
+  return (dispatch, getState) => {
+    dispatch(fetchContextRequest(id));
+
+    api(getState).get(`/api/v1/statuses/${id}/context`).then(response => {
+      dispatch(importFetchedStatuses(response.data.ancestors.concat(response.data.descendants)));
+      dispatch(fetchContextSuccess(id, response.data.ancestors, response.data.descendants));
+
+    }).catch(error => {
+      if (error.response && error.response.status === 404) {
+        dispatch(deleteFromTimelines(id));
+      }
+
+      dispatch(fetchContextFail(id, error));
+    });
+  };
+}
+
+export function fetchContextRequest(id) {
+  return {
+    type: CONTEXT_FETCH_REQUEST,
+    id,
+  };
+}
+
+export function fetchContextSuccess(id, ancestors, descendants) {
+  return {
+    type: CONTEXT_FETCH_SUCCESS,
+    id,
+    ancestors,
+    descendants,
+    statuses: ancestors.concat(descendants),
+  };
+}
+
+export function fetchContextFail(id, error) {
+  return {
+    type: CONTEXT_FETCH_FAIL,
+    id,
+    error,
+    skipAlert: true,
+  };
+}
+
+export function muteStatus(id) {
+  return (dispatch, getState) => {
+    dispatch(muteStatusRequest(id));
+
+    api(getState).post(`/api/v1/statuses/${id}/mute`).then(() => {
+      dispatch(muteStatusSuccess(id));
+    }).catch(error => {
+      dispatch(muteStatusFail(id, error));
+    });
+  };
+}
+
+export function muteStatusRequest(id) {
+  return {
+    type: STATUS_MUTE_REQUEST,
+    id,
+  };
+}
+
+export function muteStatusSuccess(id) {
+  return {
+    type: STATUS_MUTE_SUCCESS,
+    id,
+  };
+}
+
+export function muteStatusFail(id, error) {
+  return {
+    type: STATUS_MUTE_FAIL,
+    id,
+    error,
+  };
+}
+
+export function unmuteStatus(id) {
+  return (dispatch, getState) => {
+    dispatch(unmuteStatusRequest(id));
+
+    api(getState).post(`/api/v1/statuses/${id}/unmute`).then(() => {
+      dispatch(unmuteStatusSuccess(id));
+    }).catch(error => {
+      dispatch(unmuteStatusFail(id, error));
+    });
+  };
+}
+
+export function unmuteStatusRequest(id) {
+  return {
+    type: STATUS_UNMUTE_REQUEST,
+    id,
+  };
+}
+
+export function unmuteStatusSuccess(id) {
+  return {
+    type: STATUS_UNMUTE_SUCCESS,
+    id,
+  };
+}
+
+export function unmuteStatusFail(id, error) {
+  return {
+    type: STATUS_UNMUTE_FAIL,
+    id,
+    error,
+  };
+}
+
+export function hideStatus(ids) {
+  if (!Array.isArray(ids)) {
+    ids = [ids];
+  }
+
+  return {
+    type: STATUS_HIDE,
+    ids,
+  };
+}
+
+export function revealStatus(ids) {
+  if (!Array.isArray(ids)) {
+    ids = [ids];
+  }
+
+  return {
+    type: STATUS_REVEAL,
+    ids,
+  };
+}
+
+export function toggleStatusCollapse(id, isCollapsed) {
+  return {
+    type: STATUS_COLLAPSE,
+    id,
+    isCollapsed,
+  };
+}
+
+export const translateStatus = id => (dispatch, getState) => {
+  dispatch(translateStatusRequest(id));
+
+  api(getState).post(`/api/v1/statuses/${id}/translate`).then(response => {
+    dispatch(translateStatusSuccess(id, response.data));
+  }).catch(error => {
+    dispatch(translateStatusFail(id, error));
+  });
+};
+
+export const translateStatusRequest = id => ({
+  type: STATUS_TRANSLATE_REQUEST,
+  id,
+});
+
+export const translateStatusSuccess = (id, translation) => ({
+  type: STATUS_TRANSLATE_SUCCESS,
+  id,
+  translation,
+});
+
+export const translateStatusFail = (id, error) => ({
+  type: STATUS_TRANSLATE_FAIL,
+  id,
+  error,
+});
+
+export const undoStatusTranslation = (id, pollId) => ({
+  type: STATUS_TRANSLATE_UNDO,
+  id,
+  pollId,
+});
diff --git a/app/javascript/flavours/blobfox/actions/store.js b/app/javascript/flavours/blobfox/actions/store.js
new file mode 100644
index 00000000000000..86a42433d25092
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/store.js
@@ -0,0 +1,43 @@
+import { Iterable, fromJS } from 'immutable';
+
+import { hydrateCompose } from './compose';
+import { importFetchedAccounts } from './importer';
+import { hydrateSearch } from './search';
+import { saveSettings } from './settings';
+
+export const STORE_HYDRATE = 'STORE_HYDRATE';
+export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY';
+
+const convertState = rawState =>
+  fromJS(rawState, (k, v) =>
+    Iterable.isIndexed(v) ? v.toList() : v.toMap());
+
+const applyMigrations = (state) => {
+  return state.withMutations(state => {
+    // Migrate blobfox-soc local-only “Show unread marker” setting to Mastodon's setting
+    if (state.getIn(['local_settings', 'notifications', 'show_unread']) !== undefined) {
+      // Only change if the Mastodon setting does not deviate from default
+      if (state.getIn(['settings', 'notifications', 'showUnread']) !== false) {
+        state.setIn(['settings', 'notifications', 'showUnread'], state.getIn(['local_settings', 'notifications', 'show_unread']));
+      }
+      state.removeIn(['local_settings', 'notifications', 'show_unread']);
+    }
+  });
+};
+
+
+export function hydrateStore(rawState) {
+  return dispatch => {
+    const state = applyMigrations(convertState(rawState));
+
+    dispatch({
+      type: STORE_HYDRATE,
+      state,
+    });
+
+    dispatch(hydrateCompose());
+    dispatch(hydrateSearch());
+    dispatch(importFetchedAccounts(Object.values(rawState.accounts)));
+    dispatch(saveSettings());
+  };
+}
diff --git a/app/javascript/flavours/blobfox/actions/streaming.js b/app/javascript/flavours/blobfox/actions/streaming.js
new file mode 100644
index 00000000000000..d8341a5c1682ce
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/streaming.js
@@ -0,0 +1,184 @@
+// @ts-check
+
+import { getLocale } from '../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,
+  expandHomeTimeline,
+  connectTimeline,
+  disconnectTimeline,
+  fillHomeTimelineGaps,
+  fillPublicTimelineGaps,
+  fillCommunityTimelineGaps,
+  fillListTimelineGaps,
+} from './timelines';
+
+/**
+ * @param {number} max
+ * @returns {number}
+ */
+const randomUpTo = max =>
+  Math.floor(Math.random() * Math.floor(max));
+
+/**
+ * @param {string} timelineId
+ * @param {string} channelName
+ * @param {Object.<string, string>} params
+ * @param {Object} options
+ * @param {function(Function, Function): void} [options.fallback]
+ * @param {function(): void} [options.fillGaps]
+ * @param {function(object): boolean} [options.accept]
+ * @returns {function(): void}
+ */
+export const connectTimelineStream = (timelineId, channelName, params = {}, options = {}) => {
+  const { messages } = getLocale();
+
+  return connectStream(channelName, params, (dispatch, getState) => {
+    const locale = getState().getIn(['meta', 'locale']);
+
+    // @ts-expect-error
+    let pollingId;
+
+    /**
+     * @param {function(Function, Function): void} fallback
+     */
+
+    const useFallback = fallback => {
+      fallback(dispatch, () => {
+        // eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a react hook
+        pollingId = setTimeout(() => useFallback(fallback), 20000 + randomUpTo(20000));
+      });
+    };
+
+    return {
+      onConnect() {
+        dispatch(connectTimeline(timelineId));
+
+        // @ts-expect-error
+        if (pollingId) {
+          // @ts-ignore
+          clearTimeout(pollingId); pollingId = null;
+        }
+
+        if (options.fillGaps) {
+          dispatch(options.fillGaps());
+        }
+      },
+
+      onDisconnect() {
+        dispatch(disconnectTimeline(timelineId));
+
+        if (options.fallback) {
+          // @ts-expect-error
+          pollingId = setTimeout(() => useFallback(options.fallback), randomUpTo(40000));
+        }
+      },
+
+      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':
+          dispatch(deleteAnnouncement(data.payload));
+          break;
+        }
+      },
+    };
+  });
+};
+
+/**
+ * @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}
+ */
+export const connectUserStream = () =>
+  // @ts-expect-error
+  connectTimelineStream('home', 'user', {}, { fallback: refreshHomeTimelineAndNotification, fillGaps: fillHomeTimelineGaps });
+
+/**
+ * @param {Object} options
+ * @param {boolean} [options.onlyMedia]
+ * @returns {function(): void}
+ */
+export const connectCommunityStream = ({ onlyMedia } = {}) =>
+  connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`, {}, { fillGaps: () => (fillCommunityTimelineGaps({ onlyMedia })) });
+
+/**
+ * @param {Object} options
+ * @param {boolean} [options.onlyMedia]
+ * @param {boolean} [options.onlyRemote]
+ * @param {boolean} [options.allowLocalOnly]
+ * @returns {function(): void}
+ */
+export const connectPublicStream = ({ onlyMedia, onlyRemote, allowLocalOnly } = {}) =>
+  connectTimelineStream(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, {}, { fillGaps: () => fillPublicTimelineGaps({ onlyMedia, onlyRemote, allowLocalOnly }) });
+
+/**
+ * @param {string} columnId
+ * @param {string} tagName
+ * @param {boolean} onlyLocal
+ * @param {function(object): boolean} accept
+ * @returns {function(): void}
+ */
+export const connectHashtagStream = (columnId, tagName, onlyLocal, accept) =>
+  connectTimelineStream(`hashtag:${columnId}${onlyLocal ? ':local' : ''}`, `hashtag${onlyLocal ? ':local' : ''}`, { tag: tagName }, { accept });
+
+/**
+ * @returns {function(): void}
+ */
+export const connectDirectStream = () =>
+  connectTimelineStream('direct', 'direct');
+
+/**
+ * @param {string} listId
+ * @returns {function(): void}
+ */
+export const connectListStream = listId =>
+  connectTimelineStream(`list:${listId}`, 'list', { list: listId }, { fillGaps: () => fillListTimelineGaps(listId) });
diff --git a/app/javascript/flavours/blobfox/actions/suggestions.js b/app/javascript/flavours/blobfox/actions/suggestions.js
new file mode 100644
index 00000000000000..870a311024d12f
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/suggestions.js
@@ -0,0 +1,65 @@
+import api from '../api';
+
+import { fetchRelationships } from './accounts';
+import { importFetchedAccounts } from './importer';
+
+export const SUGGESTIONS_FETCH_REQUEST = 'SUGGESTIONS_FETCH_REQUEST';
+export const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS';
+export const SUGGESTIONS_FETCH_FAIL    = 'SUGGESTIONS_FETCH_FAIL';
+
+export const SUGGESTIONS_DISMISS = 'SUGGESTIONS_DISMISS';
+
+export function fetchSuggestions(withRelationships = false) {
+  return (dispatch, getState) => {
+    dispatch(fetchSuggestionsRequest());
+
+    api(getState).get('/api/v2/suggestions', { params: { limit: 20 } }).then(response => {
+      dispatch(importFetchedAccounts(response.data.map(x => x.account)));
+      dispatch(fetchSuggestionsSuccess(response.data));
+
+      if (withRelationships) {
+        dispatch(fetchRelationships(response.data.map(item => item.account.id)));
+      }
+    }).catch(error => dispatch(fetchSuggestionsFail(error)));
+  };
+}
+
+export function fetchSuggestionsRequest() {
+  return {
+    type: SUGGESTIONS_FETCH_REQUEST,
+    skipLoading: true,
+  };
+}
+
+export function fetchSuggestionsSuccess(suggestions) {
+  return {
+    type: SUGGESTIONS_FETCH_SUCCESS,
+    suggestions,
+    skipLoading: true,
+  };
+}
+
+export function fetchSuggestionsFail(error) {
+  return {
+    type: SUGGESTIONS_FETCH_FAIL,
+    error,
+    skipLoading: true,
+    skipAlert: true,
+  };
+}
+
+export const dismissSuggestion = accountId => (dispatch, getState) => {
+  dispatch({
+    type: SUGGESTIONS_DISMISS,
+    id: accountId,
+  });
+
+  api(getState).delete(`/api/v1/suggestions/${accountId}`).then(() => {
+    dispatch(fetchSuggestionsRequest());
+
+    api(getState).get('/api/v2/suggestions').then(response => {
+      dispatch(importFetchedAccounts(response.data.map(x => x.account)));
+      dispatch(fetchSuggestionsSuccess(response.data));
+    }).catch(error => dispatch(fetchSuggestionsFail(error)));
+  }).catch(() => {});
+};
diff --git a/app/javascript/flavours/blobfox/actions/tags.js b/app/javascript/flavours/blobfox/actions/tags.js
new file mode 100644
index 00000000000000..dda8c924bb59a4
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/tags.js
@@ -0,0 +1,172 @@
+import api, { getLinks } from '../api';
+
+export const HASHTAG_FETCH_REQUEST = 'HASHTAG_FETCH_REQUEST';
+export const HASHTAG_FETCH_SUCCESS = 'HASHTAG_FETCH_SUCCESS';
+export const HASHTAG_FETCH_FAIL    = 'HASHTAG_FETCH_FAIL';
+
+export const FOLLOWED_HASHTAGS_FETCH_REQUEST = 'FOLLOWED_HASHTAGS_FETCH_REQUEST';
+export const FOLLOWED_HASHTAGS_FETCH_SUCCESS = 'FOLLOWED_HASHTAGS_FETCH_SUCCESS';
+export const FOLLOWED_HASHTAGS_FETCH_FAIL    = 'FOLLOWED_HASHTAGS_FETCH_FAIL';
+
+export const FOLLOWED_HASHTAGS_EXPAND_REQUEST = 'FOLLOWED_HASHTAGS_EXPAND_REQUEST';
+export const FOLLOWED_HASHTAGS_EXPAND_SUCCESS = 'FOLLOWED_HASHTAGS_EXPAND_SUCCESS';
+export const FOLLOWED_HASHTAGS_EXPAND_FAIL    = 'FOLLOWED_HASHTAGS_EXPAND_FAIL';
+
+export const HASHTAG_FOLLOW_REQUEST = 'HASHTAG_FOLLOW_REQUEST';
+export const HASHTAG_FOLLOW_SUCCESS = 'HASHTAG_FOLLOW_SUCCESS';
+export const HASHTAG_FOLLOW_FAIL    = 'HASHTAG_FOLLOW_FAIL';
+
+export const HASHTAG_UNFOLLOW_REQUEST = 'HASHTAG_UNFOLLOW_REQUEST';
+export const HASHTAG_UNFOLLOW_SUCCESS = 'HASHTAG_UNFOLLOW_SUCCESS';
+export const HASHTAG_UNFOLLOW_FAIL    = 'HASHTAG_UNFOLLOW_FAIL';
+
+export const fetchHashtag = name => (dispatch, getState) => {
+  dispatch(fetchHashtagRequest());
+
+  api(getState).get(`/api/v1/tags/${name}`).then(({ data }) => {
+    dispatch(fetchHashtagSuccess(name, data));
+  }).catch(err => {
+    dispatch(fetchHashtagFail(err));
+  });
+};
+
+export const fetchHashtagRequest = () => ({
+  type: HASHTAG_FETCH_REQUEST,
+});
+
+export const fetchHashtagSuccess = (name, tag) => ({
+  type: HASHTAG_FETCH_SUCCESS,
+  name,
+  tag,
+});
+
+export const fetchHashtagFail = error => ({
+  type: HASHTAG_FETCH_FAIL,
+  error,
+});
+
+export const fetchFollowedHashtags = () => (dispatch, getState) => {
+  dispatch(fetchFollowedHashtagsRequest());
+
+  api(getState).get('/api/v1/followed_tags').then(response => {
+    const next = getLinks(response).refs.find(link => link.rel === 'next');
+    dispatch(fetchFollowedHashtagsSuccess(response.data, next ? next.uri : null));
+  }).catch(err => {
+    dispatch(fetchFollowedHashtagsFail(err));
+  });
+};
+
+export function fetchFollowedHashtagsRequest() {
+  return {
+    type: FOLLOWED_HASHTAGS_FETCH_REQUEST,
+  };
+}
+
+export function fetchFollowedHashtagsSuccess(followed_tags, next) {
+  return {
+    type: FOLLOWED_HASHTAGS_FETCH_SUCCESS,
+    followed_tags,
+    next,
+  };
+}
+
+export function fetchFollowedHashtagsFail(error) {
+  return {
+    type: FOLLOWED_HASHTAGS_FETCH_FAIL,
+    error,
+  };
+}
+
+export function expandFollowedHashtags() {
+  return (dispatch, getState) => {
+    const url = getState().getIn(['followed_tags', 'next']);
+
+    if (url === null) {
+      return;
+    }
+
+    dispatch(expandFollowedHashtagsRequest());
+
+    api(getState).get(url).then(response => {
+      const next = getLinks(response).refs.find(link => link.rel === 'next');
+      dispatch(expandFollowedHashtagsSuccess(response.data, next ? next.uri : null));
+    }).catch(error => {
+      dispatch(expandFollowedHashtagsFail(error));
+    });
+  };
+}
+
+export function expandFollowedHashtagsRequest() {
+  return {
+    type: FOLLOWED_HASHTAGS_EXPAND_REQUEST,
+  };
+}
+
+export function expandFollowedHashtagsSuccess(followed_tags, next) {
+  return {
+    type: FOLLOWED_HASHTAGS_EXPAND_SUCCESS,
+    followed_tags,
+    next,
+  };
+}
+
+export function expandFollowedHashtagsFail(error) {
+  return {
+    type: FOLLOWED_HASHTAGS_EXPAND_FAIL,
+    error,
+  };
+}
+
+export const followHashtag = name => (dispatch, getState) => {
+  dispatch(followHashtagRequest(name));
+
+  api(getState).post(`/api/v1/tags/${name}/follow`).then(({ data }) => {
+    dispatch(followHashtagSuccess(name, data));
+  }).catch(err => {
+    dispatch(followHashtagFail(name, err));
+  });
+};
+
+export const followHashtagRequest = name => ({
+  type: HASHTAG_FOLLOW_REQUEST,
+  name,
+});
+
+export const followHashtagSuccess = (name, tag) => ({
+  type: HASHTAG_FOLLOW_SUCCESS,
+  name,
+  tag,
+});
+
+export const followHashtagFail = (name, error) => ({
+  type: HASHTAG_FOLLOW_FAIL,
+  name,
+  error,
+});
+
+export const unfollowHashtag = name => (dispatch, getState) => {
+  dispatch(unfollowHashtagRequest(name));
+
+  api(getState).post(`/api/v1/tags/${name}/unfollow`).then(({ data }) => {
+    dispatch(unfollowHashtagSuccess(name, data));
+  }).catch(err => {
+    dispatch(unfollowHashtagFail(name, err));
+  });
+};
+
+export const unfollowHashtagRequest = name => ({
+  type: HASHTAG_UNFOLLOW_REQUEST,
+  name,
+});
+
+export const unfollowHashtagSuccess = (name, tag) => ({
+  type: HASHTAG_UNFOLLOW_SUCCESS,
+  name,
+  tag,
+});
+
+export const unfollowHashtagFail = (name, error) => ({
+  type: HASHTAG_UNFOLLOW_FAIL,
+  name,
+  error,
+});
diff --git a/app/javascript/flavours/blobfox/actions/timelines.js b/app/javascript/flavours/blobfox/actions/timelines.js
new file mode 100644
index 00000000000000..429efc5a0ef10b
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/timelines.js
@@ -0,0 +1,235 @@
+import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
+
+import api, { getLinks } from 'flavours/blobfox/api';
+import { compareId } from 'flavours/blobfox/compare_id';
+import { usePendingItems as preferPendingItems } from 'flavours/blobfox/initial_state';
+import { toServerSideType } from 'flavours/blobfox/utils/filters';
+
+import { importFetchedStatus, importFetchedStatuses } from './importer';
+import { submitMarkers } from './markers';
+
+export const TIMELINE_UPDATE  = 'TIMELINE_UPDATE';
+export const TIMELINE_DELETE  = 'TIMELINE_DELETE';
+export const TIMELINE_CLEAR   = 'TIMELINE_CLEAR';
+
+export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST';
+export const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS';
+export const TIMELINE_EXPAND_FAIL    = 'TIMELINE_EXPAND_FAIL';
+
+export const TIMELINE_SCROLL_TOP   = 'TIMELINE_SCROLL_TOP';
+export const TIMELINE_LOAD_PENDING = 'TIMELINE_LOAD_PENDING';
+export const TIMELINE_DISCONNECT   = 'TIMELINE_DISCONNECT';
+export const TIMELINE_CONNECT      = 'TIMELINE_CONNECT';
+
+export const TIMELINE_MARK_AS_PARTIAL = 'TIMELINE_MARK_AS_PARTIAL';
+
+export const loadPending = timeline => ({
+  type: TIMELINE_LOAD_PENDING,
+  timeline,
+});
+
+export function updateTimeline(timeline, status, accept) {
+  return (dispatch, getState) => {
+    if (typeof accept === 'function' && !accept(status)) {
+      return;
+    }
+
+    if (getState().getIn(['timelines', timeline, 'isPartial'])) {
+      // Prevent new items from being added to a partial timeline,
+      // since it will be reloaded anyway
+
+      return;
+    }
+
+    let filtered = false;
+
+    if (status.filtered) {
+      const contextType = toServerSideType(timeline);
+      const filters = status.filtered.filter(result => result.filter.context.includes(contextType));
+
+      filtered = filters.length > 0;
+    }
+
+    dispatch(importFetchedStatus(status));
+
+    dispatch({
+      type: TIMELINE_UPDATE,
+      timeline,
+      status,
+      usePendingItems: preferPendingItems,
+      filtered,
+    });
+
+    if (timeline === 'home') {
+      dispatch(submitMarkers());
+    }
+  };
+}
+
+export function deleteFromTimelines(id) {
+  return (dispatch, getState) => {
+    const accountId  = getState().getIn(['statuses', id, 'account']);
+    const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => status.get('id'));
+    const reblogOf   = getState().getIn(['statuses', id, 'reblog'], null);
+
+    dispatch({
+      type: TIMELINE_DELETE,
+      id,
+      accountId,
+      references,
+      reblogOf,
+    });
+  };
+}
+
+export function clearTimeline(timeline) {
+  return (dispatch) => {
+    dispatch({ type: TIMELINE_CLEAR, timeline });
+  };
+}
+
+const noOp = () => {};
+
+const parseTags = (tags = {}, mode) => {
+  return (tags[mode] || []).map((tag) => {
+    return tag.value;
+  });
+};
+
+export function expandTimeline(timelineId, path, params = {}, done = noOp) {
+  return (dispatch, getState) => {
+    const timeline = getState().getIn(['timelines', timelineId], ImmutableMap());
+    const isLoadingMore = !!params.max_id;
+
+    if (timeline.get('isLoading')) {
+      done();
+      return;
+    }
+
+    if (!params.max_id && !params.pinned && (timeline.get('items', ImmutableList()).size + timeline.get('pendingItems', ImmutableList()).size) > 0) {
+      const a = timeline.getIn(['pendingItems', 0]);
+      const b = timeline.getIn(['items', 0]);
+
+      if (a && b && compareId(a, b) > 0) {
+        params.since_id = a;
+      } else {
+        params.since_id = b || a;
+      }
+    }
+
+    const isLoadingRecent = !!params.since_id;
+
+    dispatch(expandTimelineRequest(timelineId, isLoadingMore));
+
+    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));
+
+      if (timelineId === 'home') {
+        dispatch(submitMarkers());
+      }
+    }).catch(error => {
+      dispatch(expandTimelineFail(timelineId, error, isLoadingMore));
+    }).finally(() => {
+      done();
+    });
+  };
+}
+
+export function fillTimelineGaps(timelineId, path, params = {}, done = noOp) {
+  return (dispatch, getState) => {
+    const timeline = getState().getIn(['timelines', timelineId], ImmutableMap());
+    const items = timeline.get('items');
+    const nullIndexes = items.map((statusId, index) => statusId === null ? index : null);
+    const gaps = nullIndexes.map(index => index > 0 ? items.get(index - 1) : null);
+
+    // Only expand at most two gaps to avoid doing too many requests
+    done = gaps.take(2).reduce((done, maxId) => {
+      return (() => dispatch(expandTimeline(timelineId, path, { ...params, maxId }, done)));
+    }, done);
+
+    done();
+  };
+}
+
+export const expandHomeTimeline            = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done);
+export const expandPublicTimeline          = ({ maxId, onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, allow_local_only: !!allowLocalOnly, max_id: maxId, only_media: !!onlyMedia }, done);
+export const expandCommunityTimeline       = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
+export const expandDirectTimeline          = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done);
+export const expandAccountTimeline         = (accountId, { maxId, withReplies, tagged } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, exclude_reblogs: withReplies, tagged, max_id: maxId });
+export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged });
+export const expandAccountMediaTimeline    = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
+export const expandListTimeline            = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
+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,
+  }, done);
+};
+
+export const fillHomeTimelineGaps      = (done = noOp) => fillTimelineGaps('home', '/api/v1/timelines/home', {}, done);
+export const fillPublicTimelineGaps    = ({ onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => fillTimelineGaps(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, only_media: !!onlyMedia, allow_local_only: !!allowLocalOnly }, done);
+export const fillCommunityTimelineGaps = ({ onlyMedia } = {}, done = noOp) => fillTimelineGaps(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, only_media: !!onlyMedia }, done);
+export const fillListTimelineGaps      = (id, done = noOp) => fillTimelineGaps(`list:${id}`, `/api/v1/timelines/list/${id}`, {}, done);
+
+export function expandTimelineRequest(timeline, isLoadingMore) {
+  return {
+    type: TIMELINE_EXPAND_REQUEST,
+    timeline,
+    skipLoading: !isLoadingMore,
+  };
+}
+
+export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadingRecent, isLoadingMore, usePendingItems) {
+  return {
+    type: TIMELINE_EXPAND_SUCCESS,
+    timeline,
+    statuses,
+    next,
+    partial,
+    isLoadingRecent,
+    usePendingItems,
+    skipLoading: !isLoadingMore,
+  };
+}
+
+export function expandTimelineFail(timeline, error, isLoadingMore) {
+  return {
+    type: TIMELINE_EXPAND_FAIL,
+    timeline,
+    error,
+    skipLoading: !isLoadingMore,
+    skipNotFound: timeline.startsWith('account:'),
+  };
+}
+
+export function scrollTopTimeline(timeline, top) {
+  return {
+    type: TIMELINE_SCROLL_TOP,
+    timeline,
+    top,
+  };
+}
+
+export function connectTimeline(timeline) {
+  return {
+    type: TIMELINE_CONNECT,
+    timeline,
+    usePendingItems: preferPendingItems,
+  };
+}
+
+export const disconnectTimeline = timeline => ({
+  type: TIMELINE_DISCONNECT,
+  timeline,
+  usePendingItems: preferPendingItems,
+});
+
+export const markAsPartial = timeline => ({
+  type: TIMELINE_MARK_AS_PARTIAL,
+  timeline,
+});
diff --git a/app/javascript/flavours/blobfox/actions/trends.js b/app/javascript/flavours/blobfox/actions/trends.js
new file mode 100644
index 00000000000000..d314423884efe1
--- /dev/null
+++ b/app/javascript/flavours/blobfox/actions/trends.js
@@ -0,0 +1,140 @@
+import api, { getLinks } from '../api';
+
+import { importFetchedStatuses } from './importer';
+
+export const TRENDS_TAGS_FETCH_REQUEST = 'TRENDS_TAGS_FETCH_REQUEST';
+export const TRENDS_TAGS_FETCH_SUCCESS = 'TRENDS_TAGS_FETCH_SUCCESS';
+export const TRENDS_TAGS_FETCH_FAIL    = 'TRENDS_TAGS_FETCH_FAIL';
+
+export const TRENDS_LINKS_FETCH_REQUEST = 'TRENDS_LINKS_FETCH_REQUEST';
+export const TRENDS_LINKS_FETCH_SUCCESS = 'TRENDS_LINKS_FETCH_SUCCESS';
+export const TRENDS_LINKS_FETCH_FAIL    = 'TRENDS_LINKS_FETCH_FAIL';
+
+export const TRENDS_STATUSES_FETCH_REQUEST = 'TRENDS_STATUSES_FETCH_REQUEST';
+export const TRENDS_STATUSES_FETCH_SUCCESS = 'TRENDS_STATUSES_FETCH_SUCCESS';
+export const TRENDS_STATUSES_FETCH_FAIL    = 'TRENDS_STATUSES_FETCH_FAIL';
+
+export const TRENDS_STATUSES_EXPAND_REQUEST = 'TRENDS_STATUSES_EXPAND_REQUEST';
+export const TRENDS_STATUSES_EXPAND_SUCCESS = 'TRENDS_STATUSES_EXPAND_SUCCESS';
+export const TRENDS_STATUSES_EXPAND_FAIL    = 'TRENDS_STATUSES_EXPAND_FAIL';
+
+export const fetchTrendingHashtags = () => (dispatch, getState) => {
+  dispatch(fetchTrendingHashtagsRequest());
+
+  api(getState)
+    .get('/api/v1/trends/tags')
+    .then(({ data }) => dispatch(fetchTrendingHashtagsSuccess(data)))
+    .catch(err => dispatch(fetchTrendingHashtagsFail(err)));
+};
+
+export const fetchTrendingHashtagsRequest = () => ({
+  type: TRENDS_TAGS_FETCH_REQUEST,
+  skipLoading: true,
+});
+
+export const fetchTrendingHashtagsSuccess = trends => ({
+  type: TRENDS_TAGS_FETCH_SUCCESS,
+  trends,
+  skipLoading: true,
+});
+
+export const fetchTrendingHashtagsFail = error => ({
+  type: TRENDS_TAGS_FETCH_FAIL,
+  error,
+  skipLoading: true,
+  skipAlert: true,
+});
+
+export const fetchTrendingLinks = () => (dispatch, getState) => {
+  dispatch(fetchTrendingLinksRequest());
+
+  api(getState)
+    .get('/api/v1/trends/links')
+    .then(({ data }) => dispatch(fetchTrendingLinksSuccess(data)))
+    .catch(err => dispatch(fetchTrendingLinksFail(err)));
+};
+
+export const fetchTrendingLinksRequest = () => ({
+  type: TRENDS_LINKS_FETCH_REQUEST,
+  skipLoading: true,
+});
+
+export const fetchTrendingLinksSuccess = trends => ({
+  type: TRENDS_LINKS_FETCH_SUCCESS,
+  trends,
+  skipLoading: true,
+});
+
+export const fetchTrendingLinksFail = error => ({
+  type: TRENDS_LINKS_FETCH_FAIL,
+  error,
+  skipLoading: true,
+  skipAlert: true,
+});
+
+export const fetchTrendingStatuses = () => (dispatch, getState) => {
+  if (getState().getIn(['status_lists', 'trending', 'isLoading'])) {
+    return;
+  }
+
+  dispatch(fetchTrendingStatusesRequest());
+
+  api(getState).get('/api/v1/trends/statuses').then(response => {
+    const next = getLinks(response).refs.find(link => link.rel === 'next');
+    dispatch(importFetchedStatuses(response.data));
+    dispatch(fetchTrendingStatusesSuccess(response.data, next ? next.uri : null));
+  }).catch(err => dispatch(fetchTrendingStatusesFail(err)));
+};
+
+export const fetchTrendingStatusesRequest = () => ({
+  type: TRENDS_STATUSES_FETCH_REQUEST,
+  skipLoading: true,
+});
+
+export const fetchTrendingStatusesSuccess = (statuses, next) => ({
+  type: TRENDS_STATUSES_FETCH_SUCCESS,
+  statuses,
+  next,
+  skipLoading: true,
+});
+
+export const fetchTrendingStatusesFail = error => ({
+  type: TRENDS_STATUSES_FETCH_FAIL,
+  error,
+  skipLoading: true,
+  skipAlert: true,
+});
+
+
+export const expandTrendingStatuses = () => (dispatch, getState) => {
+  const url = getState().getIn(['status_lists', 'trending', 'next'], null);
+
+  if (url === null || getState().getIn(['status_lists', 'trending', 'isLoading'])) {
+    return;
+  }
+
+  dispatch(expandTrendingStatusesRequest());
+
+  api(getState).get(url).then(response => {
+    const next = getLinks(response).refs.find(link => link.rel === 'next');
+    dispatch(importFetchedStatuses(response.data));
+    dispatch(expandTrendingStatusesSuccess(response.data, next ? next.uri : null));
+  }).catch(error => {
+    dispatch(expandTrendingStatusesFail(error));
+  });
+};
+
+export const expandTrendingStatusesRequest = () => ({
+  type: TRENDS_STATUSES_EXPAND_REQUEST,
+});
+
+export const expandTrendingStatusesSuccess = (statuses, next) => ({
+  type: TRENDS_STATUSES_EXPAND_SUCCESS,
+  statuses,
+  next,
+});
+
+export const expandTrendingStatusesFail = error => ({
+  type: TRENDS_STATUSES_EXPAND_FAIL,
+  error,
+});
diff --git a/app/javascript/flavours/blobfox/api.ts b/app/javascript/flavours/blobfox/api.ts
new file mode 100644
index 00000000000000..f262fd85707941
--- /dev/null
+++ b/app/javascript/flavours/blobfox/api.ts
@@ -0,0 +1,63 @@
+import type { AxiosResponse, RawAxiosRequestHeaders } from 'axios';
+import axios from 'axios';
+import LinkHeader from 'http-link-header';
+
+import ready from './ready';
+import type { GetState } from './store';
+
+export const getLinks = (response: AxiosResponse) => {
+  const value = response.headers.link as string | undefined;
+
+  if (!value) {
+    return new LinkHeader();
+  }
+
+  return LinkHeader.parse(value);
+};
+
+const csrfHeader: RawAxiosRequestHeaders = {};
+
+const setCSRFHeader = () => {
+  const csrfToken = document.querySelector<HTMLMetaElement>(
+    'meta[name=csrf-token]',
+  );
+
+  if (csrfToken) {
+    csrfHeader['X-CSRF-Token'] = csrfToken.content;
+  }
+};
+
+void ready(setCSRFHeader);
+
+const authorizationHeaderFromState = (getState?: GetState) => {
+  const accessToken =
+    getState && (getState().meta.get('access_token', '') as string);
+
+  if (!accessToken) {
+    return {};
+  }
+
+  return {
+    Authorization: `Bearer ${accessToken}`,
+  } as RawAxiosRequestHeaders;
+};
+
+// eslint-disable-next-line import/no-default-export
+export default function api(getState: GetState) {
+  return axios.create({
+    headers: {
+      ...csrfHeader,
+      ...authorizationHeaderFromState(getState),
+    },
+
+    transformResponse: [
+      function (data: unknown) {
+        try {
+          return JSON.parse(data as string) as unknown;
+        } catch {
+          return data;
+        }
+      },
+    ],
+  });
+}
diff --git a/app/javascript/flavours/blobfox/api_types/accounts.ts b/app/javascript/flavours/blobfox/api_types/accounts.ts
new file mode 100644
index 00000000000000..ce55dc604ad001
--- /dev/null
+++ b/app/javascript/flavours/blobfox/api_types/accounts.ts
@@ -0,0 +1,45 @@
+import type { ApiCustomEmojiJSON } from './custom_emoji';
+
+export interface ApiAccountFieldJSON {
+  name: string;
+  value: string;
+  verified_at: string | null;
+}
+
+export interface ApiAccountRoleJSON {
+  color: string;
+  id: string;
+  name: string;
+}
+
+// See app/serializers/rest/account_serializer.rb
+export interface ApiAccountJSON {
+  acct: string;
+  avatar: string;
+  avatar_static: string;
+  bot: boolean;
+  created_at: string;
+  discoverable: boolean;
+  display_name: string;
+  emojis: ApiCustomEmojiJSON[];
+  fields: ApiAccountFieldJSON[];
+  followers_count: number;
+  following_count: number;
+  group: boolean;
+  header: string;
+  header_static: string;
+  id: string;
+  last_status_at: string;
+  locked: boolean;
+  noindex?: boolean;
+  note: string;
+  roles?: ApiAccountJSON[];
+  statuses_count: number;
+  uri: string;
+  url: string;
+  username: string;
+  moved?: ApiAccountJSON;
+  suspended?: boolean;
+  limited?: boolean;
+  memorial?: boolean;
+}
diff --git a/app/javascript/flavours/blobfox/api_types/custom_emoji.ts b/app/javascript/flavours/blobfox/api_types/custom_emoji.ts
new file mode 100644
index 00000000000000..05144d6f68d0e8
--- /dev/null
+++ b/app/javascript/flavours/blobfox/api_types/custom_emoji.ts
@@ -0,0 +1,8 @@
+// See app/serializers/rest/account_serializer.rb
+export interface ApiCustomEmojiJSON {
+  shortcode: string;
+  static_url: string;
+  url: string;
+  category?: string;
+  visible_in_picker: boolean;
+}
diff --git a/app/javascript/flavours/blobfox/api_types/relationships.ts b/app/javascript/flavours/blobfox/api_types/relationships.ts
new file mode 100644
index 00000000000000..9f26a0ce9b333d
--- /dev/null
+++ b/app/javascript/flavours/blobfox/api_types/relationships.ts
@@ -0,0 +1,18 @@
+// See app/serializers/rest/relationship_serializer.rb
+export interface ApiRelationshipJSON {
+  blocked_by: boolean;
+  blocking: boolean;
+  domain_blocking: boolean;
+  endorsed: boolean;
+  followed_by: boolean;
+  following: boolean;
+  id: string;
+  languages: string[] | null;
+  muting_notifications: boolean;
+  muting: boolean;
+  note: string;
+  notifying: boolean;
+  requested_by: boolean;
+  requested: boolean;
+  showing_reblogs: boolean;
+}
diff --git a/app/javascript/flavours/blobfox/blurhash.ts b/app/javascript/flavours/blobfox/blurhash.ts
new file mode 100644
index 00000000000000..cafe7b12dcff8b
--- /dev/null
+++ b/app/javascript/flavours/blobfox/blurhash.ts
@@ -0,0 +1,111 @@
+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: string) => {
+  let value = 0;
+  let digit;
+
+  for (const c of str) {
+    digit = DIGIT_CHARACTERS.indexOf(c);
+    value = value * 83 + digit;
+  }
+
+  return value;
+};
+
+export const intToRGB = (int: number) => ({
+  r: Math.max(0, int >> 16),
+  g: Math.max(0, (int >> 8) & 255),
+  b: Math.max(0, int & 255),
+});
+
+export const getAverageFromBlurhash = (blurhash: string) => {
+  if (!blurhash) {
+    return null;
+  }
+
+  return intToRGB(decode83(blurhash.slice(2, 6)));
+};
diff --git a/app/javascript/flavours/blobfox/compare_id.ts b/app/javascript/flavours/blobfox/compare_id.ts
new file mode 100644
index 00000000000000..30b05724817892
--- /dev/null
+++ b/app/javascript/flavours/blobfox/compare_id.ts
@@ -0,0 +1,11 @@
+export function compareId(id1: string, id2: string) {
+  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/blobfox/components/__tests__/hashtag_bar.tsx b/app/javascript/flavours/blobfox/components/__tests__/hashtag_bar.tsx
new file mode 100644
index 00000000000000..b7225fc92e01e4
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/__tests__/hashtag_bar.tsx
@@ -0,0 +1,214 @@
+import { fromJS } from 'immutable';
+
+import type { StatusLike } from '../hashtag_bar';
+import { computeHashtagBarForStatus } from '../hashtag_bar';
+
+function createStatus(
+  content: string,
+  hashtags: string[],
+  hasMedia = false,
+  spoilerText?: string,
+) {
+  return fromJS({
+    tags: hashtags.map((name) => ({ name })),
+    contentHtml: content,
+    media_attachments: hasMedia ? ['fakeMedia'] : [],
+    spoiler_text: spoilerText,
+  }) as unknown as StatusLike; // need to force the type here, as it is not properly defined
+}
+
+describe('computeHashtagBarForStatus', () => {
+  it('does nothing when there are no tags', () => {
+    const status = createStatus('<p>Simple text</p>', []);
+
+    const { hashtagsInBar, statusContentProps } =
+      computeHashtagBarForStatus(status);
+
+    expect(hashtagsInBar).toEqual([]);
+    expect(statusContentProps.statusContent).toMatchInlineSnapshot(
+      `"<p>Simple text</p>"`,
+    );
+  });
+
+  it('displays out of band hashtags in the bar', () => {
+    const status = createStatus(
+      '<p>Simple text <a href="test">#hashtag</a></p>',
+      ['hashtag', 'test'],
+    );
+
+    const { hashtagsInBar, statusContentProps } =
+      computeHashtagBarForStatus(status);
+
+    expect(hashtagsInBar).toEqual(['test']);
+    expect(statusContentProps.statusContent).toMatchInlineSnapshot(
+      `"<p>Simple text <a href="test">#hashtag</a></p>"`,
+    );
+  });
+
+  it('does not truncate the contents when the last child is a text node', () => {
+    const status = createStatus(
+      'this is a #<a class="zrl" href="https://example.com/search?tag=test">test</a>. Some more text',
+      ['test'],
+    );
+
+    const { hashtagsInBar, statusContentProps } =
+      computeHashtagBarForStatus(status);
+
+    expect(hashtagsInBar).toEqual([]);
+    expect(statusContentProps.statusContent).toMatchInlineSnapshot(
+      `"this is a #<a class="zrl" href="https://example.com/search?tag=test">test</a>. Some more text"`,
+    );
+  });
+
+  it('extract tags from the last line', () => {
+    const status = createStatus(
+      '<p>Simple text</p><p><a href="test">#hashtag</a></p>',
+      ['hashtag'],
+    );
+
+    const { hashtagsInBar, statusContentProps } =
+      computeHashtagBarForStatus(status);
+
+    expect(hashtagsInBar).toEqual(['hashtag']);
+    expect(statusContentProps.statusContent).toMatchInlineSnapshot(
+      `"<p>Simple text</p>"`,
+    );
+  });
+
+  it('does not include tags from content', () => {
+    const status = createStatus(
+      '<p>Simple text with a <a href="test">#hashtag</a></p><p><a href="test">#hashtag</a></p>',
+      ['hashtag'],
+    );
+
+    const { hashtagsInBar, statusContentProps } =
+      computeHashtagBarForStatus(status);
+
+    expect(hashtagsInBar).toEqual([]);
+    expect(statusContentProps.statusContent).toMatchInlineSnapshot(
+      `"<p>Simple text with a <a href="test">#hashtag</a></p>"`,
+    );
+  });
+
+  it('works with one line status and hashtags', () => {
+    const status = createStatus(
+      '<p><a href="test">#test</a>. And another <a href="test">#hashtag</a></p>',
+      ['hashtag', 'test'],
+    );
+
+    const { hashtagsInBar, statusContentProps } =
+      computeHashtagBarForStatus(status);
+
+    expect(hashtagsInBar).toEqual([]);
+    expect(statusContentProps.statusContent).toMatchInlineSnapshot(
+      `"<p><a href="test">#test</a>. And another <a href="test">#hashtag</a></p>"`,
+    );
+  });
+
+  it('de-duplicate accentuated characters with case differences', () => {
+    const status = createStatus(
+      '<p>Text</p><p><a href="test">#éaa</a> <a href="test">#Éaa</a></p>',
+      ['éaa'],
+    );
+
+    const { hashtagsInBar, statusContentProps } =
+      computeHashtagBarForStatus(status);
+
+    expect(hashtagsInBar).toEqual(['Éaa']);
+    expect(statusContentProps.statusContent).toMatchInlineSnapshot(
+      `"<p>Text</p>"`,
+    );
+  });
+
+  it('handles server-side normalized tags with accentuated characters', () => {
+    const status = createStatus(
+      '<p>Text</p><p><a href="test">#éaa</a> <a href="test">#Éaa</a></p>',
+      ['eaa'], // The server may normalize the hashtags in the `tags` attribute
+    );
+
+    const { hashtagsInBar, statusContentProps } =
+      computeHashtagBarForStatus(status);
+
+    expect(hashtagsInBar).toEqual(['Éaa']);
+    expect(statusContentProps.statusContent).toMatchInlineSnapshot(
+      `"<p>Text</p>"`,
+    );
+  });
+
+  it('does not display in bar a hashtag in content with a case difference', () => {
+    const status = createStatus(
+      '<p>Text <a href="test">#Éaa</a></p><p><a href="test">#éaa</a></p>',
+      ['éaa'],
+    );
+
+    const { hashtagsInBar, statusContentProps } =
+      computeHashtagBarForStatus(status);
+
+    expect(hashtagsInBar).toEqual([]);
+    expect(statusContentProps.statusContent).toMatchInlineSnapshot(
+      `"<p>Text <a href="test">#Éaa</a></p>"`,
+    );
+  });
+
+  it('does not modify a status with a line of hashtags only', () => {
+    const status = createStatus(
+      '<p><a href="test">#test</a>  <a href="test">#hashtag</a></p>',
+      ['test', 'hashtag'],
+    );
+
+    const { hashtagsInBar, statusContentProps } =
+      computeHashtagBarForStatus(status);
+
+    expect(hashtagsInBar).toEqual([]);
+    expect(statusContentProps.statusContent).toMatchInlineSnapshot(
+      `"<p><a href="test">#test</a>  <a href="test">#hashtag</a></p>"`,
+    );
+  });
+
+  it('puts the hashtags in the bar if a status content has hashtags in the only line and has a media', () => {
+    const status = createStatus(
+      '<p>This is my content! <a href="test">#hashtag</a></p>',
+      ['hashtag'],
+      true,
+    );
+
+    const { hashtagsInBar, statusContentProps } =
+      computeHashtagBarForStatus(status);
+
+    expect(hashtagsInBar).toEqual([]);
+    expect(statusContentProps.statusContent).toMatchInlineSnapshot(
+      `"<p>This is my content! <a href="test">#hashtag</a></p>"`,
+    );
+  });
+
+  it('puts the hashtags in the bar if a status content is only hashtags and has a media', () => {
+    const status = createStatus(
+      '<p><a href="test">#test</a>  <a href="test">#hashtag</a></p>',
+      ['test', 'hashtag'],
+      true,
+    );
+
+    const { hashtagsInBar, statusContentProps } =
+      computeHashtagBarForStatus(status);
+
+    expect(hashtagsInBar).toEqual(['test', 'hashtag']);
+    expect(statusContentProps.statusContent).toMatchInlineSnapshot(`""`);
+  });
+
+  it('does not use the hashtag bar if the status content is only hashtags, has a CW and a media', () => {
+    const status = createStatus(
+      '<p><a href="test">#test</a>  <a href="test">#hashtag</a></p>',
+      ['test', 'hashtag'],
+      true,
+      'My CW text',
+    );
+
+    const { hashtagsInBar, statusContentProps } =
+      computeHashtagBarForStatus(status);
+
+    expect(hashtagsInBar).toEqual([]);
+    expect(statusContentProps.statusContent).toMatchInlineSnapshot(
+      `"<p><a href="test">#test</a>  <a href="test">#hashtag</a></p>"`,
+    );
+  });
+});
diff --git a/app/javascript/flavours/blobfox/components/account.jsx b/app/javascript/flavours/blobfox/components/account.jsx
new file mode 100644
index 00000000000000..a83c9c066d87ad
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/account.jsx
@@ -0,0 +1,182 @@
+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 { EmptyAccount } from 'flavours/blobfox/components/empty_account';
+import { ShortNumber } from 'flavours/blobfox/components/short_number';
+import { VerifiedBadge } from 'flavours/blobfox/components/verified_badge';
+
+import { me } from '../initial_state';
+
+import { Avatar } from './avatar';
+import { Button } from './button';
+import { FollowersCounter } from './counters';
+import { DisplayName } from './display_name';
+import Permalink from './permalink';
+import { RelativeTimestamp } from './relative_timestamp';
+
+const messages = defineMessages({
+  follow: { id: 'account.follow', defaultMessage: 'Follow' },
+  unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
+  cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Withdraw follow request' },
+  unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
+  unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
+  mute_notifications: { id: 'account.mute_notifications_short', defaultMessage: 'Mute notifications' },
+  unmute_notifications: { id: 'account.unmute_notifications_short', defaultMessage: 'Unmute notifications' },
+  mute: { id: 'account.mute_short', defaultMessage: 'Mute' },
+  block: { id: 'account.block_short', defaultMessage: 'Block' },
+});
+
+class Account extends ImmutablePureComponent {
+
+  static propTypes = {
+    size: PropTypes.number,
+    account: ImmutablePropTypes.record,
+    onFollow: PropTypes.func.isRequired,
+    onBlock: PropTypes.func.isRequired,
+    onMute: PropTypes.func.isRequired,
+    onMuteNotifications: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+    hidden: PropTypes.bool,
+    minimal: PropTypes.bool,
+    defaultAction: PropTypes.string,
+    withBio: PropTypes.bool,
+  };
+
+  static defaultProps = {
+    size: 46,
+  };
+
+  handleFollow = () => {
+    this.props.onFollow(this.props.account);
+  };
+
+  handleBlock = () => {
+    this.props.onBlock(this.props.account);
+  };
+
+  handleMute = () => {
+    this.props.onMute(this.props.account);
+  };
+
+  handleMuteNotifications = () => {
+    this.props.onMuteNotifications(this.props.account, true);
+  };
+
+  handleUnmuteNotifications = () => {
+    this.props.onMuteNotifications(this.props.account, false);
+  };
+
+  render () {
+    const { account, intl, hidden, withBio, defaultAction, size, minimal } = this.props;
+
+    if (!account) {
+      return <EmptyAccount size={size} minimal={minimal} />;
+    }
+
+    if (hidden) {
+      return (
+        <>
+          {account.get('display_name')}
+          {account.get('username')}
+        </>
+      );
+    }
+
+    let buttons;
+
+    if (account.get('id') !== me && account.get('relationship', null) !== null) {
+      const following = account.getIn(['relationship', 'following']);
+      const requested = account.getIn(['relationship', 'requested']);
+      const blocking  = account.getIn(['relationship', 'blocking']);
+      const muting  = account.getIn(['relationship', 'muting']);
+
+      if (requested) {
+        buttons = <Button text={intl.formatMessage(messages.cancel_follow_request)} onClick={this.handleFollow} />;
+      } else if (blocking) {
+        buttons = <Button text={intl.formatMessage(messages.unblock)} onClick={this.handleBlock} />;
+      } else if (muting) {
+        let hidingNotificationsButton;
+
+        if (account.getIn(['relationship', 'muting_notifications'])) {
+          hidingNotificationsButton = <Button text={intl.formatMessage(messages.unmute_notifications)} onClick={this.handleUnmuteNotifications} />;
+        } else {
+          hidingNotificationsButton = <Button text={intl.formatMessage(messages.mute_notifications)} onClick={this.handleMuteNotifications} />;
+        }
+
+        buttons = (
+          <>
+            <Button text={intl.formatMessage(messages.unmute)} onClick={this.handleMute} />
+            {hidingNotificationsButton}
+          </>
+        );
+      } else if (defaultAction === 'mute') {
+        buttons = <Button title={intl.formatMessage(messages.mute)} onClick={this.handleMute} />;
+      } else if (defaultAction === 'block') {
+        buttons = <Button text={intl.formatMessage(messages.block)} onClick={this.handleBlock} />;
+      } else if (!account.get('suspended') && !account.get('moved') || following) {
+        buttons = <Button text={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />;
+      }
+    }
+
+    let muteTimeRemaining;
+
+    if (account.get('mute_expires_at')) {
+      muteTimeRemaining = <>· <RelativeTimestamp timestamp={account.get('mute_expires_at')} futureDate /></>;
+    }
+
+    let verification;
+
+    const firstVerifiedField = account.get('fields').find(item => !!item.get('verified_at'));
+
+    if (firstVerifiedField) {
+      verification = <VerifiedBadge link={firstVerifiedField.get('value')} />;
+    }
+
+    return (
+      <div className={classNames('account', { 'account--minimal': minimal })}>
+        <div className='account__wrapper'>
+          <Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`}>
+            <div className='account__avatar-wrapper'>
+              <Avatar account={account} size={size} />
+            </div>
+
+            <div className='account__contents'>
+              <DisplayName account={account} inline />
+              {!minimal && (
+                <div className='account__details'>
+                  {account.get('followers_count') !== -1 && (
+                    <ShortNumber value={account.get('followers_count')} renderer={FollowersCounter} />
+                  )} {verification} {muteTimeRemaining}
+                </div>
+              )}
+            </div>
+          </Permalink>
+
+          {!minimal && (
+            <div className='account__relationship'>
+              {buttons}
+            </div>
+          )}
+        </div>
+
+        {withBio && (account.get('note').length > 0 ? (
+          <div
+            className='account__note translate'
+            dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
+          />
+        ) : (
+          <div className='account__note account__note--missing'><FormattedMessage id='account.no_bio' defaultMessage='No description provided.' /></div>
+        ))}
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(Account);
diff --git a/app/javascript/flavours/blobfox/components/admin/Counter.jsx b/app/javascript/flavours/blobfox/components/admin/Counter.jsx
new file mode 100644
index 00000000000000..f9a5fc53962a6c
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/admin/Counter.jsx
@@ -0,0 +1,121 @@
+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/blobfox/api';
+import { Skeleton } from 'flavours/blobfox/components/skeleton';
+
+const percIncrease = (a, b) => {
+  let percent;
+
+  if (b !== 0) {
+    if (a !== 0) {
+      percent = (b - a) / a;
+    } else {
+      percent = 1;
+    }
+  } else if (b === 0 && a === 0) {
+    percent = 0;
+  } else {
+    percent = - 1;
+  }
+
+  return percent;
+};
+
+export default class Counter extends PureComponent {
+
+  static propTypes = {
+    measure: PropTypes.string.isRequired,
+    start_at: PropTypes.string.isRequired,
+    end_at: PropTypes.string.isRequired,
+    label: PropTypes.string.isRequired,
+    href: PropTypes.string,
+    params: PropTypes.object,
+    target: PropTypes.string,
+  };
+
+  state = {
+    loading: true,
+    data: null,
+  };
+
+  componentDidMount () {
+    const { measure, start_at, end_at, params } = this.props;
+
+    api().post('/api/v1/admin/measures', { keys: [measure], start_at, end_at, [measure]: params }).then(res => {
+      this.setState({
+        loading: false,
+        data: res.data,
+      });
+    }).catch(err => {
+      console.error(err);
+    });
+  }
+
+  render () {
+    const { label, href, target } = this.props;
+    const { loading, data } = this.state;
+
+    let content;
+
+    if (loading) {
+      content = (
+        <>
+          <span className='sparkline__value__total'><Skeleton width={43} /></span>
+          <span className='sparkline__value__change'><Skeleton width={43} /></span>
+        </>
+      );
+    } else {
+      const measure = data[0];
+      const percentChange = measure.previous_total && percIncrease(measure.previous_total * 1, measure.total * 1);
+
+      content = (
+        <>
+          <span className='sparkline__value__total'>{measure.human_value || <FormattedNumber value={measure.total} />}</span>
+          {measure.previous_total && (<span className={classNames('sparkline__value__change', { positive: percentChange > 0, negative: percentChange < 0 })}>{percentChange > 0 && '+'}<FormattedNumber value={percentChange} style='percent' /></span>)}
+        </>
+      );
+    }
+
+    const inner = (
+      <>
+        <div className='sparkline__value'>
+          {content}
+        </div>
+
+        <div className='sparkline__label'>
+          {label}
+        </div>
+
+        <div className='sparkline__graph'>
+          {!loading && (
+            <Sparklines width={259} height={55} data={data[0].data.map(x => x.value * 1)}>
+              <SparklinesCurve />
+            </Sparklines>
+          )}
+        </div>
+      </>
+    );
+
+    if (href) {
+      return (
+        <a href={href} className='sparkline' target={target}>
+          {inner}
+        </a>
+      );
+    } else {
+      return (
+        <div className='sparkline'>
+          {inner}
+        </div>
+      );
+    }
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/components/admin/Dimension.jsx b/app/javascript/flavours/blobfox/components/admin/Dimension.jsx
new file mode 100644
index 00000000000000..bcd4afa3ac16eb
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/admin/Dimension.jsx
@@ -0,0 +1,95 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { FormattedNumber } from 'react-intl';
+
+import api from 'flavours/blobfox/api';
+import { Skeleton } from 'flavours/blobfox/components/skeleton';
+import { roundTo10 } from 'flavours/blobfox/utils/numbers';
+
+export default class Dimension extends PureComponent {
+
+  static propTypes = {
+    dimension: PropTypes.string.isRequired,
+    start_at: PropTypes.string.isRequired,
+    end_at: PropTypes.string.isRequired,
+    limit: PropTypes.number.isRequired,
+    label: PropTypes.string.isRequired,
+    params: PropTypes.object,
+  };
+
+  state = {
+    loading: true,
+    data: null,
+  };
+
+  componentDidMount () {
+    const { start_at, end_at, dimension, limit, params } = this.props;
+
+    api().post('/api/v1/admin/dimensions', { keys: [dimension], start_at, end_at, limit, [dimension]: params }).then(res => {
+      this.setState({
+        loading: false,
+        data: res.data,
+      });
+    }).catch(err => {
+      console.error(err);
+    });
+  }
+
+  render () {
+    const { label, limit } = this.props;
+    const { loading, data } = this.state;
+
+    let content;
+
+    if (loading) {
+      content = (
+        <table>
+          <tbody>
+            {Array.from(Array(limit)).map((_, i) => (
+              <tr className='dimension__item' key={i}>
+                <td className='dimension__item__key'>
+                  <Skeleton width={100} />
+                </td>
+
+                <td className='dimension__item__value'>
+                  <Skeleton width={60} />
+                </td>
+              </tr>
+            ))}
+          </tbody>
+        </table>
+      );
+    } else {
+      const sum = data[0].data.reduce((sum, cur) => sum + (cur.value * 1), 0);
+
+      content = (
+        <table>
+          <tbody>
+            {data[0].data.map(item => (
+              <tr className='dimension__item' key={item.key}>
+                <td className='dimension__item__key'>
+                  <span className={`dimension__item__indicator dimension__item__indicator--${roundTo10(((item.value * 1) / sum) * 100)}`} />
+                  <span title={item.key}>{item.human_key}</span>
+                </td>
+
+                <td className='dimension__item__value'>
+                  {typeof item.human_value !== 'undefined' ? item.human_value : <FormattedNumber value={item.value} />}
+                </td>
+              </tr>
+            ))}
+          </tbody>
+        </table>
+      );
+    }
+
+    return (
+      <div className='dimension'>
+        <h4>{label}</h4>
+
+        {content}
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/components/admin/ImpactReport.jsx b/app/javascript/flavours/blobfox/components/admin/ImpactReport.jsx
new file mode 100644
index 00000000000000..fe3a2ea29ff5f5
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/admin/ImpactReport.jsx
@@ -0,0 +1,91 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { FormattedNumber, FormattedMessage } from 'react-intl';
+
+import classNames from 'classnames';
+
+import api from 'flavours/blobfox/api';
+import { Skeleton } from 'flavours/blobfox/components/skeleton';
+
+export default class ImpactReport extends PureComponent {
+
+  static propTypes = {
+    domain: PropTypes.string.isRequired,
+  };
+
+  state = {
+    loading: true,
+    data: null,
+  };
+
+  componentDidMount () {
+    const { domain } = this.props;
+
+    const params = {
+      domain: domain,
+      include_subdomains: true,
+    };
+
+    api().post('/api/v1/admin/measures', {
+      keys: ['instance_accounts', 'instance_follows', 'instance_followers'],
+      start_at: null,
+      end_at: null,
+      instance_accounts: params,
+      instance_follows: params,
+      instance_followers: params,
+    }).then(res => {
+      this.setState({
+        loading: false,
+        data: res.data,
+      });
+    }).catch(err => {
+      console.error(err);
+    });
+  }
+
+  render () {
+    const { loading, data } = this.state;
+
+    return (
+      <div className='dimension'>
+        <h4><FormattedMessage id='admin.impact_report.title' defaultMessage='Impact summary' /></h4>
+
+        <table>
+          <tbody>
+            <tr className='dimension__item'>
+              <td className='dimension__item__key'>
+                <FormattedMessage id='admin.impact_report.instance_accounts' defaultMessage='Accounts profiles this would delete' />
+              </td>
+
+              <td className='dimension__item__value'>
+                {loading ? <Skeleton width={60} /> : <FormattedNumber value={data[0].total} />}
+              </td>
+            </tr>
+
+            <tr className={classNames('dimension__item', { negative: !loading && data[1].total > 0 })}>
+              <td className='dimension__item__key'>
+                <FormattedMessage id='admin.impact_report.instance_follows' defaultMessage='Followers their users would lose' />
+              </td>
+
+              <td className='dimension__item__value'>
+                {loading ? <Skeleton width={60} /> : <FormattedNumber value={data[1].total} />}
+              </td>
+            </tr>
+
+            <tr className={classNames('dimension__item', { negative: !loading && data[2].total > 0 })}>
+              <td className='dimension__item__key'>
+                <FormattedMessage id='admin.impact_report.instance_followers' defaultMessage='Followers our users would lose' />
+              </td>
+
+              <td className='dimension__item__value'>
+                {loading ? <Skeleton width={60} /> : <FormattedNumber value={data[2].total} />}
+              </td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/components/admin/ReportReasonSelector.jsx b/app/javascript/flavours/blobfox/components/admin/ReportReasonSelector.jsx
new file mode 100644
index 00000000000000..6ef9c912330469
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/admin/ReportReasonSelector.jsx
@@ -0,0 +1,165 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { injectIntl, defineMessages } from 'react-intl';
+
+import classNames from 'classnames';
+
+import api from 'flavours/blobfox/api';
+
+const messages = defineMessages({
+  legal: { id: 'report.categories.legal', defaultMessage: 'Legal' },
+  other: { id: 'report.categories.other', defaultMessage: 'Other' },
+  spam: { id: 'report.categories.spam', defaultMessage: 'Spam' },
+  violation: { id: 'report.categories.violation', defaultMessage: 'Content violates one or more server rules' },
+});
+
+class Category extends PureComponent {
+
+  static propTypes = {
+    id: PropTypes.string.isRequired,
+    text: PropTypes.string.isRequired,
+    selected: PropTypes.bool,
+    disabled: PropTypes.bool,
+    onSelect: PropTypes.func,
+    children: PropTypes.node,
+  };
+
+  handleClick = () => {
+    const { id, disabled, onSelect } = this.props;
+
+    if (!disabled) {
+      onSelect(id);
+    }
+  };
+
+  render () {
+    const { id, text, disabled, selected, children } = this.props;
+
+    return (
+      <div tabIndex={0} role='button' className={classNames('report-reason-selector__category', { selected, disabled })} onClick={this.handleClick}>
+        {selected && <input type='hidden' name='report[category]' value={id} />}
+
+        <div className='report-reason-selector__category__label'>
+          <span className={classNames('poll__input', { active: selected, disabled })} />
+          {text}
+        </div>
+
+        {(selected && children) && (
+          <div className='report-reason-selector__category__rules'>
+            {children}
+          </div>
+        )}
+      </div>
+    );
+  }
+
+}
+
+class Rule extends PureComponent {
+
+  static propTypes = {
+    id: PropTypes.string.isRequired,
+    text: PropTypes.string.isRequired,
+    selected: PropTypes.bool,
+    disabled: PropTypes.bool,
+    onToggle: PropTypes.func,
+  };
+
+  handleClick = () => {
+    const { id, disabled, onToggle } = this.props;
+
+    if (!disabled) {
+      onToggle(id);
+    }
+  };
+
+  render () {
+    const { id, text, disabled, selected } = this.props;
+
+    return (
+      <div tabIndex={0} role='button' className={classNames('report-reason-selector__rule', { selected, disabled })} onClick={this.handleClick}>
+        <span className={classNames('poll__input', { checkbox: true, active: selected, disabled })} />
+        {selected && <input type='hidden' name='report[rule_ids][]' value={id} />}
+        {text}
+      </div>
+    );
+  }
+
+}
+
+class ReportReasonSelector extends PureComponent {
+
+  static propTypes = {
+    id: PropTypes.string.isRequired,
+    category: PropTypes.string.isRequired,
+    rule_ids: PropTypes.arrayOf(PropTypes.string),
+    disabled: PropTypes.bool,
+    intl: PropTypes.object.isRequired,
+  };
+
+  state = {
+    category: this.props.category,
+    rule_ids: this.props.rule_ids || [],
+    rules: [],
+  };
+
+  componentDidMount() {
+    api().get('/api/v1/instance').then(res => {
+      this.setState({
+        rules: res.data.rules,
+      });
+    }).catch(err => {
+      console.error(err);
+    });
+  }
+
+  _save = () => {
+    const { id, disabled } = this.props;
+    const { category, rule_ids } = this.state;
+
+    if (disabled) {
+      return;
+    }
+
+    api().put(`/api/v1/admin/reports/${id}`, {
+      category,
+      rule_ids,
+    }).catch(err => {
+      console.error(err);
+    });
+  };
+
+  handleSelect = id => {
+    this.setState({ category: id }, () => this._save());
+  };
+
+  handleToggle = id => {
+    const { rule_ids } = this.state;
+
+    if (rule_ids.includes(id)) {
+      this.setState({ rule_ids: rule_ids.filter(x => x !== id ) }, () => this._save());
+    } else {
+      this.setState({ rule_ids: [...rule_ids, id] }, () => this._save());
+    }
+  };
+
+  render () {
+    const { disabled, intl } = this.props;
+    const { rules, category, rule_ids } = this.state;
+
+    return (
+      <div className='report-reason-selector'>
+        <Category id='other' text={intl.formatMessage(messages.other)} selected={category === 'other'} onSelect={this.handleSelect} disabled={disabled} />
+        <Category id='legal' text={intl.formatMessage(messages.legal)} selected={category === 'legal'} onSelect={this.handleSelect} disabled={disabled} />
+        <Category id='spam' text={intl.formatMessage(messages.spam)} selected={category === 'spam'} onSelect={this.handleSelect} disabled={disabled} />
+        <Category id='violation' text={intl.formatMessage(messages.violation)} selected={category === 'violation'} onSelect={this.handleSelect} disabled={disabled}>
+          {rules.map(rule => <Rule key={rule.id} id={rule.id} text={rule.text} selected={rule_ids.includes(rule.id)} onToggle={this.handleToggle} disabled={disabled} />)}
+        </Category>
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(ReportReasonSelector);
diff --git a/app/javascript/flavours/blobfox/components/admin/Retention.jsx b/app/javascript/flavours/blobfox/components/admin/Retention.jsx
new file mode 100644
index 00000000000000..3c1bec0fdd2c29
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/admin/Retention.jsx
@@ -0,0 +1,155 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { FormattedMessage, FormattedNumber, FormattedDate } from 'react-intl';
+
+import classNames from 'classnames';
+
+import api from 'flavours/blobfox/api';
+import { roundTo10 } from 'flavours/blobfox/utils/numbers';
+
+const dateForCohort = cohort => {
+  const timeZone = 'UTC';
+  switch(cohort.frequency) {
+  case 'day':
+    return <FormattedDate value={cohort.period} month='long' day='2-digit' timeZone={timeZone} />;
+  default:
+    return <FormattedDate value={cohort.period} month='long' year='numeric' timeZone={timeZone} />;
+  }
+};
+
+export default class Retention extends PureComponent {
+
+  static propTypes = {
+    start_at: PropTypes.string,
+    end_at: PropTypes.string,
+    frequency: PropTypes.string,
+  };
+
+  state = {
+    loading: true,
+    data: null,
+  };
+
+  componentDidMount () {
+    const { start_at, end_at, frequency } = this.props;
+
+    api().post('/api/v1/admin/retention', { start_at, end_at, frequency }).then(res => {
+      this.setState({
+        loading: false,
+        data: res.data,
+      });
+    }).catch(err => {
+      console.error(err);
+    });
+  }
+
+  render () {
+    const { loading, data } = this.state;
+    const { frequency } = this.props;
+
+    let content;
+
+    if (loading) {
+      content = <FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' />;
+    } else {
+      content = (
+        <table className='retention__table'>
+          <thead>
+            <tr>
+              <th>
+                <div className='retention__table__date retention__table__label'>
+                  <FormattedMessage id='admin.dashboard.retention.cohort' defaultMessage='Sign-up month' />
+                </div>
+              </th>
+
+              <th>
+                <div className='retention__table__number retention__table__label'>
+                  <FormattedMessage id='admin.dashboard.retention.cohort_size' defaultMessage='New users' />
+                </div>
+              </th>
+
+              {data[0].data.slice(1).map((retention, i) => (
+                <th key={retention.date}>
+                  <div className='retention__table__number retention__table__label'>
+                    {i + 1}
+                  </div>
+                </th>
+              ))}
+            </tr>
+
+            <tr>
+              <td>
+                <div className='retention__table__date retention__table__average'>
+                  <FormattedMessage id='admin.dashboard.retention.average' defaultMessage='Average' />
+                </div>
+              </td>
+
+              <td>
+                <div className='retention__table__size'>
+                  <FormattedNumber value={data.reduce((sum, cohort, i) => sum + ((cohort.data[0].value * 1) - sum) / (i + 1), 0)} maximumFractionDigits={0} />
+                </div>
+              </td>
+
+              {data[0].data.slice(1).map((retention, i) => {
+                const average = data.reduce((sum, cohort, k) => cohort.data[i + 1] ? sum + (cohort.data[i + 1].rate - sum)/(k + 1) : sum, 0);
+
+                return (
+                  <td key={retention.date}>
+                    <div className={classNames('retention__table__box', 'retention__table__average', `retention__table__box--${roundTo10(average * 100)}`)}>
+                      <FormattedNumber value={average} style='percent' />
+                    </div>
+                  </td>
+                );
+              })}
+            </tr>
+          </thead>
+
+          <tbody>
+            {data.slice(0, -1).map(cohort => (
+              <tr key={cohort.period}>
+                <td>
+                  <div className='retention__table__date'>
+                    {dateForCohort(cohort)}
+                  </div>
+                </td>
+
+                <td>
+                  <div className='retention__table__size'>
+                    <FormattedNumber value={cohort.data[0].value} />
+                  </div>
+                </td>
+
+                {cohort.data.slice(1).map(retention => (
+                  <td key={retention.date}>
+                    <div className={classNames('retention__table__box', `retention__table__box--${roundTo10(retention.rate * 100)}`)}>
+                      <FormattedNumber value={retention.rate} style='percent' />
+                    </div>
+                  </td>
+                ))}
+              </tr>
+            ))}
+          </tbody>
+        </table>
+      );
+    }
+
+    let title = null;
+    switch(frequency) {
+    case 'day':
+      title = <FormattedMessage id='admin.dashboard.daily_retention' defaultMessage='User retention rate by day after sign-up' />;
+      break;
+    default:
+      title = <FormattedMessage id='admin.dashboard.monthly_retention' defaultMessage='User retention rate by month after sign-up' />;
+    }
+
+    return (
+      <div className='retention'>
+        <h4>{title}</h4>
+
+        {content}
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/components/admin/Trends.jsx b/app/javascript/flavours/blobfox/components/admin/Trends.jsx
new file mode 100644
index 00000000000000..6dad32390d8017
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/admin/Trends.jsx
@@ -0,0 +1,76 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import classNames from 'classnames';
+
+import api from 'flavours/blobfox/api';
+import Hashtag from 'flavours/blobfox/components/hashtag';
+
+export default class Trends extends PureComponent {
+
+  static propTypes = {
+    limit: PropTypes.number.isRequired,
+  };
+
+  state = {
+    loading: true,
+    data: null,
+  };
+
+  componentDidMount () {
+    const { limit } = this.props;
+
+    api().get('/api/v1/admin/trends/tags', { params: { limit } }).then(res => {
+      this.setState({
+        loading: false,
+        data: res.data,
+      });
+    }).catch(err => {
+      console.error(err);
+    });
+  }
+
+  render () {
+    const { limit } = this.props;
+    const { loading, data } = this.state;
+
+    let content;
+
+    if (loading) {
+      content = (
+        <div>
+          {Array.from(Array(limit)).map((_, i) => (
+            <Hashtag key={i} />
+          ))}
+        </div>
+      );
+    } else {
+      content = (
+        <div>
+          {data.map(hashtag => (
+            <Hashtag
+              key={hashtag.name}
+              name={hashtag.name}
+              href={hashtag.id === undefined ? undefined : `/admin/tags/${hashtag.id}`}
+              people={hashtag.history[0].accounts * 1 + hashtag.history[1].accounts * 1}
+              uses={hashtag.history[0].uses * 1 + hashtag.history[1].uses * 1}
+              history={hashtag.history.reverse().map(day => day.uses)}
+              className={classNames(hashtag.requires_review && 'trends__item--requires-review', !hashtag.trendable && !hashtag.requires_review && 'trends__item--disabled')}
+            />
+          ))}
+        </div>
+      );
+    }
+
+    return (
+      <div className='trends trends--compact'>
+        <h4><FormattedMessage id='trends.trending_now' defaultMessage='Trending now' /></h4>
+
+        {content}
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/components/animated_number.tsx b/app/javascript/flavours/blobfox/components/animated_number.tsx
new file mode 100644
index 00000000000000..05a7e01898411e
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/animated_number.tsx
@@ -0,0 +1,81 @@
+import { useCallback, useState } from 'react';
+
+import { TransitionMotion, spring } from 'react-motion';
+
+import { reduceMotion } from '../initial_state';
+
+import { ShortNumber } from './short_number';
+
+const obfuscatedCount = (count: number) => {
+  if (count < 0) {
+    return 0;
+  } else if (count <= 1) {
+    return count;
+  } else {
+    return '1+';
+  }
+};
+
+interface Props {
+  value: number;
+  obfuscate?: boolean;
+}
+export const AnimatedNumber: React.FC<Props> = ({ value, obfuscate }) => {
+  const [previousValue, setPreviousValue] = useState(value);
+  const [direction, setDirection] = useState<1 | -1>(1);
+
+  if (previousValue !== value) {
+    setPreviousValue(value);
+    setDirection(value > previousValue ? 1 : -1);
+  }
+
+  const willEnter = useCallback(() => ({ y: -1 * direction }), [direction]);
+  const willLeave = useCallback(
+    () => ({ y: spring(1 * direction, { damping: 35, stiffness: 400 }) }),
+    [direction],
+  );
+
+  if (reduceMotion) {
+    return obfuscate ? (
+      <>{obfuscatedCount(value)}</>
+    ) : (
+      <ShortNumber value={value} />
+    );
+  }
+
+  const styles = [
+    {
+      key: `${value}`,
+      data: value,
+      style: { y: spring(0, { damping: 35, stiffness: 400 }) },
+    },
+  ];
+
+  return (
+    <TransitionMotion
+      styles={styles}
+      willEnter={willEnter}
+      willLeave={willLeave}
+    >
+      {(items) => (
+        <span className='animated-number'>
+          {items.map(({ key, data, style }) => (
+            <span
+              key={key}
+              style={{
+                position: direction * style.y > 0 ? 'absolute' : 'static',
+                transform: `translateY(${style.y * 100}%)`,
+              }}
+            >
+              {obfuscate ? (
+                obfuscatedCount(data as number)
+              ) : (
+                <ShortNumber value={data as number} />
+              )}
+            </span>
+          ))}
+        </span>
+      )}
+    </TransitionMotion>
+  );
+};
diff --git a/app/javascript/flavours/blobfox/components/attachment_list.jsx b/app/javascript/flavours/blobfox/components/attachment_list.jsx
new file mode 100644
index 00000000000000..e54584f766e190
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/attachment_list.jsx
@@ -0,0 +1,51 @@
+import PropTypes from 'prop-types';
+
+import { FormattedMessage } from 'react-intl';
+
+import classNames from 'classnames';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import { Icon } from 'flavours/blobfox/components/icon';
+
+const filename = url => url.split('/').pop().split('#')[0].split('?')[0];
+
+export default class AttachmentList extends ImmutablePureComponent {
+
+  static propTypes = {
+    media: ImmutablePropTypes.list.isRequired,
+    compact: PropTypes.bool,
+  };
+
+  render () {
+    const { media, compact } = this.props;
+
+    return (
+      <div className={classNames('attachment-list', { compact })}>
+        {!compact && (
+          <div className='attachment-list__icon'>
+            <Icon id='link' />
+          </div>
+        )}
+
+        <ul className='attachment-list__list'>
+          {media.map(attachment => {
+            const displayUrl = attachment.get('remote_url') || attachment.get('url');
+
+            return (
+              <li key={attachment.get('id')}>
+                <a href={displayUrl} target='_blank' rel='noopener noreferrer'>
+                  {compact && <Icon id='link' />}
+                  {compact && ' ' }
+                  {displayUrl ? filename(displayUrl) : <FormattedMessage id='attachments_list.unprocessed' defaultMessage='(unprocessed)' />}
+                </a>
+              </li>
+            );
+          })}
+        </ul>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/components/autosuggest_emoji.jsx b/app/javascript/flavours/blobfox/components/autosuggest_emoji.jsx
new file mode 100644
index 00000000000000..1d56f9139c1387
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/autosuggest_emoji.jsx
@@ -0,0 +1,43 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { assetHost } from 'flavours/blobfox/utils/config';
+
+import { unicodeMapping } from '../features/emoji/emoji_unicode_mapping_light';
+
+export default class AutosuggestEmoji extends PureComponent {
+
+  static propTypes = {
+    emoji: PropTypes.object.isRequired,
+  };
+
+  render () {
+    const { emoji } = this.props;
+    let url;
+
+    if (emoji.custom) {
+      url = emoji.imageUrl;
+    } else {
+      const mapping = unicodeMapping[emoji.native] || unicodeMapping[emoji.native.replace(/\uFE0F$/, '')];
+
+      if (!mapping) {
+        return null;
+      }
+
+      url = `${assetHost}/emoji/${mapping.filename}.svg`;
+    }
+
+    return (
+      <div className='autosuggest-emoji'>
+        <img
+          className='emojione'
+          src={url}
+          alt={emoji.native || emoji.colons}
+        />
+
+        {emoji.colons}
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/components/autosuggest_hashtag.tsx b/app/javascript/flavours/blobfox/components/autosuggest_hashtag.tsx
new file mode 100644
index 00000000000000..f45aaf1714cd45
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/autosuggest_hashtag.tsx
@@ -0,0 +1,42 @@
+import { FormattedMessage } from 'react-intl';
+
+import { ShortNumber } from 'flavours/blobfox/components/short_number';
+
+interface Props {
+  tag: {
+    name: string;
+    url?: string;
+    history?: {
+      uses: number;
+      accounts: string;
+      day: string;
+    }[];
+    following?: boolean;
+    type: 'hashtag';
+  };
+}
+
+export const AutosuggestHashtag: React.FC<Props> = ({ tag }) => {
+  const weeklyUses = tag.history && (
+    <ShortNumber
+      value={tag.history.reduce((total, day) => total + day.uses * 1, 0)}
+    />
+  );
+
+  return (
+    <div className='autosuggest-hashtag'>
+      <div className='autosuggest-hashtag__name'>
+        #<strong>{tag.name}</strong>
+      </div>
+      {tag.history !== undefined && (
+        <div className='autosuggest-hashtag__uses'>
+          <FormattedMessage
+            id='autosuggest_hashtag.per_week'
+            defaultMessage='{count} per week'
+            values={{ count: weeklyUses }}
+          />
+        </div>
+      )}
+    </div>
+  );
+};
diff --git a/app/javascript/flavours/blobfox/components/autosuggest_input.jsx b/app/javascript/flavours/blobfox/components/autosuggest_input.jsx
new file mode 100644
index 00000000000000..6d2474b4426bb0
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/autosuggest_input.jsx
@@ -0,0 +1,230 @@
+import PropTypes from 'prop-types';
+
+import classNames from 'classnames';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
+
+import AutosuggestEmoji from './autosuggest_emoji';
+import { AutosuggestHashtag } from './autosuggest_hashtag';
+
+const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => {
+  let word;
+
+  let left  = str.slice(0, caretPosition).search(/[^\s\u200B]+$/);
+  let right = str.slice(caretPosition).search(/[\s\u200B]/);
+
+  if (right < 0) {
+    word = str.slice(left);
+  } else {
+    word = str.slice(left, right + caretPosition);
+  }
+
+  if (!word || word.trim().length < 3 || searchTokens.indexOf(word[0]) === -1) {
+    return [null, null];
+  }
+
+  word = word.trim().toLowerCase();
+
+  if (word.length > 0) {
+    return [left, word];
+  } else {
+    return [null, null];
+  }
+};
+
+export default class AutosuggestInput extends ImmutablePureComponent {
+
+  static propTypes = {
+    value: PropTypes.string,
+    suggestions: ImmutablePropTypes.list,
+    disabled: PropTypes.bool,
+    placeholder: PropTypes.string,
+    onSuggestionSelected: PropTypes.func.isRequired,
+    onSuggestionsClearRequested: PropTypes.func.isRequired,
+    onSuggestionsFetchRequested: PropTypes.func.isRequired,
+    onChange: PropTypes.func.isRequired,
+    onKeyUp: PropTypes.func,
+    onKeyDown: PropTypes.func,
+    autoFocus: PropTypes.bool,
+    className: PropTypes.string,
+    id: PropTypes.string,
+    searchTokens: PropTypes.arrayOf(PropTypes.string),
+    maxLength: PropTypes.number,
+    lang: PropTypes.string,
+    spellCheck: PropTypes.bool,
+  };
+
+  static defaultProps = {
+    autoFocus: true,
+    searchTokens: ['@', ':', '#'],
+  };
+
+  state = {
+    suggestionsHidden: true,
+    focused: false,
+    selectedSuggestion: 0,
+    lastToken: null,
+    tokenStart: 0,
+  };
+
+  onChange = (e) => {
+    const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart, this.props.searchTokens);
+
+    if (token !== null && this.state.lastToken !== token) {
+      this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart });
+      this.props.onSuggestionsFetchRequested(token);
+    } else if (token === null) {
+      this.setState({ lastToken: null });
+      this.props.onSuggestionsClearRequested();
+    }
+
+    this.props.onChange(e);
+  };
+
+  onKeyDown = (e) => {
+    const { suggestions, disabled } = this.props;
+    const { selectedSuggestion, suggestionsHidden } = this.state;
+
+    if (disabled) {
+      e.preventDefault();
+      return;
+    }
+
+    if (e.which === 229 || e.isComposing) {
+      // Ignore key events during text composition
+      // e.key may be a name of the physical key even in this case (e.x. Safari / Chrome on Mac)
+      return;
+    }
+
+    switch(e.key) {
+    case 'Escape':
+      if (suggestions.size === 0 || suggestionsHidden) {
+        document.querySelector('.ui').parentElement.focus();
+      } else {
+        e.preventDefault();
+        this.setState({ suggestionsHidden: true });
+      }
+
+      break;
+    case 'ArrowDown':
+      if (suggestions.size > 0 && !suggestionsHidden) {
+        e.preventDefault();
+        this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) });
+      }
+
+      break;
+    case 'ArrowUp':
+      if (suggestions.size > 0 && !suggestionsHidden) {
+        e.preventDefault();
+        this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) });
+      }
+
+      break;
+    case 'Enter':
+    case 'Tab':
+      // Select suggestion
+      if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) {
+        e.preventDefault();
+        e.stopPropagation();
+        this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
+      }
+
+      break;
+    }
+
+    if (e.defaultPrevented || !this.props.onKeyDown) {
+      return;
+    }
+
+    this.props.onKeyDown(e);
+  };
+
+  onBlur = () => {
+    this.setState({ suggestionsHidden: true, focused: false });
+  };
+
+  onFocus = () => {
+    this.setState({ focused: true });
+  };
+
+  onSuggestionClick = (e) => {
+    const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index'));
+    e.preventDefault();
+    this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
+    this.input.focus();
+  };
+
+  UNSAFE_componentWillReceiveProps (nextProps) {
+    if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) {
+      this.setState({ suggestionsHidden: false });
+    }
+  }
+
+  setInput = (c) => {
+    this.input = c;
+  };
+
+  renderSuggestion = (suggestion, i) => {
+    const { selectedSuggestion } = this.state;
+    let inner, key;
+
+    if (suggestion.type === 'emoji') {
+      inner = <AutosuggestEmoji emoji={suggestion} />;
+      key   = suggestion.id;
+    } else if (suggestion.type ==='hashtag') {
+      inner = <AutosuggestHashtag tag={suggestion} />;
+      key   = suggestion.name;
+    } else if (suggestion.type === 'account') {
+      inner = <AutosuggestAccountContainer id={suggestion.id} />;
+      key   = suggestion.id;
+    }
+
+    return (
+      <div role='button' tabIndex={0} key={key} data-index={i} className={classNames('autosuggest-textarea__suggestions__item', { selected: i === selectedSuggestion })} onMouseDown={this.onSuggestionClick}>
+        {inner}
+      </div>
+    );
+  };
+
+  render () {
+    const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength, lang, spellCheck } = this.props;
+    const { suggestionsHidden } = this.state;
+
+    return (
+      <div className='autosuggest-input'>
+        <label>
+          <span style={{ display: 'none' }}>{placeholder}</span>
+
+          <input
+            type='text'
+            ref={this.setInput}
+            disabled={disabled}
+            placeholder={placeholder}
+            autoFocus={autoFocus}
+            value={value}
+            onChange={this.onChange}
+            onKeyDown={this.onKeyDown}
+            onKeyUp={onKeyUp}
+            onFocus={this.onFocus}
+            onBlur={this.onBlur}
+            dir='auto'
+            aria-autocomplete='list'
+            id={id}
+            className={className}
+            maxLength={maxLength}
+            lang={lang}
+            spellCheck={spellCheck}
+          />
+        </label>
+
+        <div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
+          {suggestions.map(this.renderSuggestion)}
+        </div>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/components/autosuggest_textarea.jsx b/app/javascript/flavours/blobfox/components/autosuggest_textarea.jsx
new file mode 100644
index 00000000000000..28384075c3c16e
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/autosuggest_textarea.jsx
@@ -0,0 +1,240 @@
+import PropTypes from 'prop-types';
+import { useCallback, useRef, useState, useEffect, forwardRef } from 'react';
+
+import classNames from 'classnames';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
+import Textarea from 'react-textarea-autosize';
+
+import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
+
+import AutosuggestEmoji from './autosuggest_emoji';
+import { AutosuggestHashtag } from './autosuggest_hashtag';
+
+const textAtCursorMatchesToken = (str, caretPosition) => {
+  let word;
+
+  let left  = str.slice(0, caretPosition).search(/[^\s\u200B]+$/);
+  let right = str.slice(caretPosition).search(/[\s\u200B]/);
+
+  if (right < 0) {
+    word = str.slice(left);
+  } else {
+    word = str.slice(left, right + caretPosition);
+  }
+
+  if (!word || word.trim().length < 3 || ['@', ':', '#'].indexOf(word[0]) === -1) {
+    return [null, null];
+  }
+
+  word = word.trim().toLowerCase();
+
+  if (word.length > 0) {
+    return [left, word];
+  } else {
+    return [null, null];
+  }
+};
+
+const AutosuggestTextarea = forwardRef(({
+  value,
+  suggestions,
+  disabled,
+  placeholder,
+  onSuggestionSelected,
+  onSuggestionsClearRequested,
+  onSuggestionsFetchRequested,
+  onChange,
+  onKeyUp,
+  onKeyDown,
+  onPaste,
+  onFocus,
+  autoFocus = true,
+  lang,
+  children,
+}, textareaRef) => {
+
+  const [suggestionsHidden, setSuggestionsHidden] = useState(true);
+  const [selectedSuggestion, setSelectedSuggestion] = useState(0);
+  const lastTokenRef = useRef(null);
+  const tokenStartRef = useRef(0);
+
+  const handleChange = useCallback((e) => {
+    const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart);
+
+    if (token !== null && lastTokenRef.current !== token) {
+      tokenStartRef.current = tokenStart;
+      lastTokenRef.current = token;
+      setSelectedSuggestion(0);
+      onSuggestionsFetchRequested(token);
+    } else if (token === null) {
+      lastTokenRef.current = null;
+      onSuggestionsClearRequested();
+    }
+
+    onChange(e);
+  }, [onSuggestionsFetchRequested, onSuggestionsClearRequested, onChange, setSelectedSuggestion]);
+
+  const handleKeyDown = useCallback((e) => {
+    if (disabled) {
+      e.preventDefault();
+      return;
+    }
+
+    if (e.which === 229 || e.isComposing) {
+      // Ignore key events during text composition
+      // e.key may be a name of the physical key even in this case (e.x. Safari / Chrome on Mac)
+      return;
+    }
+
+    switch(e.key) {
+    case 'Escape':
+      if (suggestions.size === 0 || suggestionsHidden) {
+        document.querySelector('.ui').parentElement.focus();
+      } else {
+        e.preventDefault();
+        setSuggestionsHidden(true);
+      }
+
+      break;
+    case 'ArrowDown':
+      if (suggestions.size > 0 && !suggestionsHidden) {
+        e.preventDefault();
+        setSelectedSuggestion(Math.min(selectedSuggestion + 1, suggestions.size - 1));
+      }
+
+      break;
+    case 'ArrowUp':
+      if (suggestions.size > 0 && !suggestionsHidden) {
+        e.preventDefault();
+        setSelectedSuggestion(Math.max(selectedSuggestion - 1, 0));
+      }
+
+      break;
+    case 'Enter':
+    case 'Tab':
+      // Select suggestion
+      if (lastTokenRef.current !== null && suggestions.size > 0 && !suggestionsHidden) {
+        e.preventDefault();
+        e.stopPropagation();
+        onSuggestionSelected(tokenStartRef.current, lastTokenRef.current, suggestions.get(selectedSuggestion));
+      }
+
+      break;
+    }
+
+    if (e.defaultPrevented || !onKeyDown) {
+      return;
+    }
+
+    onKeyDown(e);
+  }, [disabled, suggestions, suggestionsHidden, selectedSuggestion, setSelectedSuggestion, setSuggestionsHidden, onSuggestionSelected, onKeyDown]);
+
+  const handleBlur = useCallback(() => {
+    setSuggestionsHidden(true);
+  }, [setSuggestionsHidden]);
+
+  const handleFocus = useCallback((e) => {
+    if (onFocus) {
+      onFocus(e);
+    }
+  }, [onFocus]);
+
+  const handleSuggestionClick = useCallback((e) => {
+    const suggestion = suggestions.get(e.currentTarget.getAttribute('data-index'));
+    e.preventDefault();
+    onSuggestionSelected(tokenStartRef.current, lastTokenRef.current, suggestion);
+    textareaRef.current?.focus();
+  }, [suggestions, onSuggestionSelected, textareaRef]);
+
+  const handlePaste = useCallback((e) => {
+    if (e.clipboardData && e.clipboardData.files.length === 1) {
+      onPaste(e.clipboardData.files);
+      e.preventDefault();
+    }
+  }, [onPaste]);
+
+  // Show the suggestions again whenever they change and the textarea is focused
+  useEffect(() => {
+    if (suggestions.size > 0 && textareaRef.current === document.activeElement) {
+      setSuggestionsHidden(false);
+    }
+  }, [suggestions, textareaRef, setSuggestionsHidden]);
+
+  const renderSuggestion = (suggestion, i) => {
+    let inner, key;
+
+    if (suggestion.type === 'emoji') {
+      inner = <AutosuggestEmoji emoji={suggestion} />;
+      key   = suggestion.id;
+    } else if (suggestion.type === 'hashtag') {
+      inner = <AutosuggestHashtag tag={suggestion} />;
+      key   = suggestion.name;
+    } else if (suggestion.type === 'account') {
+      inner = <AutosuggestAccountContainer id={suggestion.id} />;
+      key   = suggestion.id;
+    }
+
+    return (
+      <div role='button' tabIndex={0} key={key} data-index={i} className={classNames('autosuggest-textarea__suggestions__item', { selected: i === selectedSuggestion })} onMouseDown={handleSuggestionClick}>
+        {inner}
+      </div>
+    );
+  };
+
+  return [
+    <div className='compose-form__autosuggest-wrapper' key='autosuggest-wrapper'>
+      <div className='autosuggest-textarea'>
+        <label>
+          <span style={{ display: 'none' }}>{placeholder}</span>
+
+          <Textarea
+            ref={textareaRef}
+            className='autosuggest-textarea__textarea'
+            disabled={disabled}
+            placeholder={placeholder}
+            autoFocus={autoFocus}
+            value={value}
+            onChange={handleChange}
+            onKeyDown={handleKeyDown}
+            onKeyUp={onKeyUp}
+            onFocus={handleFocus}
+            onBlur={handleBlur}
+            onPaste={handlePaste}
+            dir='auto'
+            aria-autocomplete='list'
+            lang={lang}
+          />
+        </label>
+      </div>
+      {children}
+    </div>,
+
+    <div className='autosuggest-textarea__suggestions-wrapper' key='suggestions-wrapper'>
+      <div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
+        {suggestions.map(renderSuggestion)}
+      </div>
+    </div>,
+  ];
+});
+
+AutosuggestTextarea.propTypes = {
+  value: PropTypes.string,
+  suggestions: ImmutablePropTypes.list,
+  disabled: PropTypes.bool,
+  placeholder: PropTypes.string,
+  onSuggestionSelected: PropTypes.func.isRequired,
+  onSuggestionsClearRequested: PropTypes.func.isRequired,
+  onSuggestionsFetchRequested: PropTypes.func.isRequired,
+  onChange: PropTypes.func.isRequired,
+  onKeyUp: PropTypes.func,
+  onKeyDown: PropTypes.func,
+  onPaste: PropTypes.func.isRequired,
+  onFocus:PropTypes.func,
+  children: PropTypes.node,
+  autoFocus: PropTypes.bool,
+  lang: PropTypes.string,
+};
+
+export default AutosuggestTextarea;
diff --git a/app/javascript/flavours/blobfox/components/avatar.tsx b/app/javascript/flavours/blobfox/components/avatar.tsx
new file mode 100644
index 00000000000000..5a4b595fcfa6d2
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/avatar.tsx
@@ -0,0 +1,49 @@
+import classNames from 'classnames';
+
+import type { Account } from 'flavours/blobfox/models/account';
+
+import { useHovering } from '../hooks/useHovering';
+import { autoPlayGif } from '../initial_state';
+
+interface Props {
+  account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
+  size: number;
+  style?: React.CSSProperties;
+  inline?: boolean;
+  animate?: boolean;
+}
+
+export const Avatar: React.FC<Props> = ({
+  account,
+  animate = autoPlayGif,
+  size = 20,
+  inline = false,
+  style: styleFromParent,
+}) => {
+  const { hovering, handleMouseEnter, handleMouseLeave } = useHovering(animate);
+
+  const style = {
+    ...styleFromParent,
+    width: `${size}px`,
+    height: `${size}px`,
+  };
+
+  const src =
+    hovering || animate
+      ? account?.get('avatar')
+      : account?.get('avatar_static');
+
+  return (
+    <div
+      className={classNames('account__avatar', {
+        'account__avatar-inline': inline,
+      })}
+      onMouseEnter={handleMouseEnter}
+      onMouseLeave={handleMouseLeave}
+      style={style}
+      data-avatar-of={account && `@${account.get('acct')}`}
+    >
+      {src && <img src={src} alt={account?.get('acct')} />}
+    </div>
+  );
+};
diff --git a/app/javascript/flavours/blobfox/components/avatar_composite.jsx b/app/javascript/flavours/blobfox/components/avatar_composite.jsx
new file mode 100644
index 00000000000000..c736f1dd53200d
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/avatar_composite.jsx
@@ -0,0 +1,106 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
+import { autoPlayGif } from '../initial_state';
+
+import { Avatar } from './avatar';
+
+export default class AvatarComposite extends PureComponent {
+
+  static propTypes = {
+    accounts: ImmutablePropTypes.list.isRequired,
+    animate: PropTypes.bool,
+    size: PropTypes.number.isRequired,
+  };
+
+  static defaultProps = {
+    animate: autoPlayGif,
+  };
+
+  renderItem (account, size, index) {
+    const { animate } = this.props;
+
+    let width  = 50;
+    let height = 100;
+    let top    = 'auto';
+    let left   = 'auto';
+    let bottom = 'auto';
+    let right  = 'auto';
+
+    if (size === 1) {
+      width = 100;
+    }
+
+    if (size === 4 || (size === 3 && index > 0)) {
+      height = 50;
+    }
+
+    if (size === 2) {
+      if (index === 0) {
+        right = '1px';
+      } else {
+        left = '1px';
+      }
+    } else if (size === 3) {
+      if (index === 0) {
+        right = '1px';
+      } else if (index > 0) {
+        left = '1px';
+      }
+
+      if (index === 1) {
+        bottom = '1px';
+      } else if (index > 1) {
+        top = '1px';
+      }
+    } else if (size === 4) {
+      if (index === 0 || index === 2) {
+        right = '1px';
+      }
+
+      if (index === 1 || index === 3) {
+        left = '1px';
+      }
+
+      if (index < 2) {
+        bottom = '1px';
+      } else {
+        top = '1px';
+      }
+    }
+
+    const style = {
+      left: left,
+      top: top,
+      right: right,
+      bottom: bottom,
+      width: `${width}%`,
+      height: `${height}%`,
+    };
+
+    return (
+      <div key={account.get('id')} style={style}>
+        <Avatar account={account} animate={animate} />
+      </div>
+    );
+  }
+
+  render() {
+    const { accounts, size } = this.props;
+
+    return (
+      <div className='account__avatar-composite' style={{ width: `${size}px`, height: `${size}px` }}>
+        {accounts.take(4).map((account, i) => this.renderItem(account, Math.min(accounts.size, 4), i))}
+
+        {accounts.size > 4 && (
+          <span className='account__avatar-composite__label'>
+            +{accounts.size - 4}
+          </span>
+        )}
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/components/avatar_overlay.tsx b/app/javascript/flavours/blobfox/components/avatar_overlay.tsx
new file mode 100644
index 00000000000000..d2d26aaab40e22
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/avatar_overlay.tsx
@@ -0,0 +1,57 @@
+import type { Account } from 'flavours/blobfox/models/account';
+
+import { useHovering } from '../hooks/useHovering';
+import { autoPlayGif } from '../initial_state';
+
+interface Props {
+  account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
+  friend: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
+  size?: number;
+  baseSize?: number;
+  overlaySize?: number;
+}
+
+export const AvatarOverlay: React.FC<Props> = ({
+  account,
+  friend,
+  size = 46,
+  baseSize = 36,
+  overlaySize = 24,
+}) => {
+  const { hovering, handleMouseEnter, handleMouseLeave } =
+    useHovering(autoPlayGif);
+  const accountSrc = hovering
+    ? account?.get('avatar')
+    : account?.get('avatar_static');
+  const friendSrc = hovering
+    ? friend?.get('avatar')
+    : friend?.get('avatar_static');
+
+  return (
+    <div
+      className='account__avatar-overlay'
+      style={{ width: size, height: size }}
+      onMouseEnter={handleMouseEnter}
+      onMouseLeave={handleMouseLeave}
+    >
+      <div className='account__avatar-overlay-base'>
+        <div
+          className='account__avatar'
+          style={{ width: `${baseSize}px`, height: `${baseSize}px` }}
+          data-avatar-of={`@${account?.get('acct')}`}
+        >
+          {accountSrc && <img src={accountSrc} alt={account?.get('acct')} />}
+        </div>
+      </div>
+      <div className='account__avatar-overlay-overlay'>
+        <div
+          className='account__avatar'
+          style={{ width: `${overlaySize}px`, height: `${overlaySize}px` }}
+          data-avatar-of={`@${friend?.get('acct')}`}
+        >
+          {friendSrc && <img src={friendSrc} alt={friend?.get('acct')} />}
+        </div>
+      </div>
+    </div>
+  );
+};
diff --git a/app/javascript/flavours/blobfox/components/blurhash.tsx b/app/javascript/flavours/blobfox/components/blurhash.tsx
new file mode 100644
index 00000000000000..8e2a8af23e5937
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/blurhash.tsx
@@ -0,0 +1,48 @@
+import { memo, useRef, useEffect } from 'react';
+
+import { decode } from 'blurhash';
+
+interface Props extends React.HTMLAttributes<HTMLCanvasElement> {
+  hash: string;
+  width?: number;
+  height?: number;
+  dummy?: boolean; // Whether dummy mode is enabled. If enabled, nothing is rendered and canvas left untouched
+  children?: never;
+}
+const Blurhash: React.FC<Props> = ({
+  hash,
+  width = 32,
+  height = width,
+  dummy = false,
+  ...canvasProps
+}) => {
+  const canvasRef = useRef<HTMLCanvasElement>(null);
+
+  useEffect(() => {
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    const canvas = canvasRef.current!;
+
+    // eslint-disable-next-line no-self-assign
+    canvas.width = canvas.width; // resets canvas
+
+    if (dummy || !hash) return;
+
+    try {
+      const pixels = decode(hash, width, height);
+      const ctx = canvas.getContext('2d');
+      const imageData = new ImageData(pixels, width, height);
+
+      ctx?.putImageData(imageData, 0, 0);
+    } catch (err) {
+      console.error('Blurhash decoding failure', { err, hash });
+    }
+  }, [dummy, hash, width, height]);
+
+  return (
+    <canvas {...canvasProps} ref={canvasRef} width={width} height={height} />
+  );
+};
+
+const MemoizedBlurhash = memo(Blurhash);
+
+export { MemoizedBlurhash as Blurhash };
diff --git a/app/javascript/flavours/blobfox/components/button.tsx b/app/javascript/flavours/blobfox/components/button.tsx
new file mode 100644
index 00000000000000..0b6a0f267ebd6e
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/button.tsx
@@ -0,0 +1,58 @@
+import { useCallback } from 'react';
+
+import classNames from 'classnames';
+
+interface BaseProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
+  block?: boolean;
+  secondary?: boolean;
+  text?: JSX.Element;
+}
+
+interface PropsWithChildren extends BaseProps {
+  text?: never;
+}
+
+interface PropsWithText extends BaseProps {
+  text: JSX.Element;
+  children: never;
+}
+
+type Props = PropsWithText | PropsWithChildren;
+
+export const Button: React.FC<Props> = ({
+  text,
+  type = 'button',
+  onClick,
+  disabled,
+  block,
+  secondary,
+  className,
+  title,
+  children,
+  ...props
+}) => {
+  const handleClick = useCallback<React.MouseEventHandler<HTMLButtonElement>>(
+    (e) => {
+      if (!disabled && onClick) {
+        onClick(e);
+      }
+    },
+    [disabled, onClick],
+  );
+
+  return (
+    <button
+      className={classNames('button', className, {
+        'button-secondary': secondary,
+        'button--block': block,
+      })}
+      disabled={disabled}
+      onClick={handleClick}
+      title={title}
+      type={type}
+      {...props}
+    >
+      {text ?? children}
+    </button>
+  );
+};
diff --git a/app/javascript/flavours/blobfox/components/check.tsx b/app/javascript/flavours/blobfox/components/check.tsx
new file mode 100644
index 00000000000000..901f89fc5b2bfa
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/check.tsx
@@ -0,0 +1,13 @@
+export const Check: React.FC = () => (
+  <svg
+    xmlns='http://www.w3.org/2000/svg'
+    viewBox='0 0 20 20'
+    fill='currentColor'
+  >
+    <path
+      fillRule='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'
+      clipRule='evenodd'
+    />
+  </svg>
+);
diff --git a/app/javascript/flavours/blobfox/components/circular_progress.tsx b/app/javascript/flavours/blobfox/components/circular_progress.tsx
new file mode 100644
index 00000000000000..850eb93e482cf1
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/circular_progress.tsx
@@ -0,0 +1,27 @@
+interface Props {
+  size: number;
+  strokeWidth: number;
+}
+
+export const CircularProgress: React.FC<Props> = ({ size, strokeWidth }) => {
+  const viewBox = `0 0 ${size} ${size}`;
+  const radius = (size - strokeWidth) / 2;
+
+  return (
+    <svg
+      width={size}
+      height={size}
+      viewBox={viewBox}
+      className='circular-progress'
+      role='progressbar'
+    >
+      <circle
+        fill='none'
+        cx={size / 2}
+        cy={size / 2}
+        r={radius}
+        strokeWidth={`${strokeWidth}px`}
+      />
+    </svg>
+  );
+};
diff --git a/app/javascript/flavours/blobfox/components/column.jsx b/app/javascript/flavours/blobfox/components/column.jsx
new file mode 100644
index 00000000000000..22d6eabed7bd3c
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/column.jsx
@@ -0,0 +1,73 @@
+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 {
+
+  static propTypes = {
+    children: PropTypes.node,
+    extraClasses: PropTypes.string,
+    label: PropTypes.string,
+    bindToDocument: PropTypes.bool,
+  };
+
+  scrollTop () {
+    let scrollable = null;
+
+    if (this.props.bindToDocument) {
+      scrollable = document.scrollingElement;
+    } else {
+      scrollable = this.node.querySelector('.scrollable');
+    }
+
+    if (!scrollable) {
+      return;
+    }
+
+    this._interruptScrollAnimation = scrollTop(scrollable);
+  }
+
+  handleWheel = () => {
+    if (typeof this._interruptScrollAnimation !== 'function') {
+      return;
+    }
+
+    this._interruptScrollAnimation();
+  };
+
+  setRef = c => {
+    this.node = c;
+  };
+
+  componentDidMount () {
+    if (this.props.bindToDocument) {
+      document.addEventListener('wheel', this.handleWheel, listenerOptions);
+    } else {
+      this.node.addEventListener('wheel', this.handleWheel, listenerOptions);
+    }
+  }
+
+  componentWillUnmount () {
+    if (this.props.bindToDocument) {
+      document.removeEventListener('wheel', this.handleWheel, listenerOptions);
+    } else {
+      this.node.removeEventListener('wheel', this.handleWheel, listenerOptions);
+    }
+  }
+
+  render () {
+    const { label, children, extraClasses } = this.props;
+
+    return (
+      <div role='region' aria-label={label} className={`column ${extraClasses || ''}`} ref={this.setRef}>
+        {children}
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/components/column_back_button.jsx b/app/javascript/flavours/blobfox/components/column_back_button.jsx
new file mode 100644
index 00000000000000..d51d6be09dba94
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/column_back_button.jsx
@@ -0,0 +1,63 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+import { createPortal } from 'react-dom';
+
+import { FormattedMessage } from 'react-intl';
+
+import { withRouter } from 'react-router-dom';
+
+import { Icon }  from 'flavours/blobfox/components/icon';
+import { WithRouterPropTypes } from 'flavours/blobfox/utils/react_router';
+
+export class ColumnBackButton extends PureComponent {
+
+  static propTypes = {
+    multiColumn: PropTypes.bool,
+    onClick: PropTypes.func,
+    ...WithRouterPropTypes,
+  };
+
+  handleClick = () => {
+    const { onClick, history } = this.props;
+
+    if (onClick) {
+      onClick();
+    } else if (history.location?.state?.fromMastodon) {
+      history.goBack();
+    } else {
+      history.push('/');
+    }
+  };
+
+  render () {
+    const { multiColumn } = this.props;
+
+    const component = (
+      <button onClick={this.handleClick} className='column-back-button'>
+        <Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
+        <FormattedMessage id='column_back_button.label' defaultMessage='Back' />
+      </button>
+    );
+
+    if (multiColumn) {
+      return component;
+    } else {
+      // The portal container and the component may be rendered to the DOM in
+      // the same React render pass, so the container might not be available at
+      // the time `render()` is called.
+      const container = document.getElementById('tabs-bar__portal');
+      if (container === null) {
+        // The container wasn't available, force a re-render so that the
+        // component can eventually be inserted in the container and not scroll
+        // with the rest of the area.
+        this.forceUpdate();
+        return component;
+      } else {
+        return createPortal(component, container);
+      }
+    }
+  }
+
+}
+
+export default withRouter(ColumnBackButton);
diff --git a/app/javascript/flavours/blobfox/components/column_back_button_slim.jsx b/app/javascript/flavours/blobfox/components/column_back_button_slim.jsx
new file mode 100644
index 00000000000000..82961df5561568
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/column_back_button_slim.jsx
@@ -0,0 +1,40 @@
+import { PureComponent } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import { withRouter } from 'react-router-dom';
+
+import { Icon } from 'flavours/blobfox/components/icon';
+import { WithRouterPropTypes } from 'flavours/blobfox/utils/react_router';
+
+class ColumnBackButtonSlim extends PureComponent {
+
+  static propTypes = {
+    ...WithRouterPropTypes,
+  };
+
+  handleClick = () => {
+    const { location, history } = this.props;
+
+    // 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 (location.key) {
+      history.goBack();
+    } else {
+      history.push('/');
+    }
+  };
+
+  render () {
+    return (
+      <div className='column-back-button--slim'>
+        <div role='button' tabIndex={0} onClick={this.handleClick} className='column-back-button column-back-button--slim-button'>
+          <Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
+          <FormattedMessage id='column_back_button.label' defaultMessage='Back' />
+        </div>
+      </div>
+    );
+  }
+}
+
+export default withRouter(ColumnBackButtonSlim);
diff --git a/app/javascript/flavours/blobfox/components/column_header.jsx b/app/javascript/flavours/blobfox/components/column_header.jsx
new file mode 100644
index 00000000000000..dce67ab9950d82
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/column_header.jsx
@@ -0,0 +1,219 @@
+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 { withRouter } from 'react-router-dom';
+
+import { Icon }  from 'flavours/blobfox/components/icon';
+import { WithRouterPropTypes } from 'flavours/blobfox/utils/react_router';
+
+const messages = defineMessages({
+  show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
+  hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
+  moveLeft: { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' },
+  moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' },
+});
+
+class ColumnHeader extends PureComponent {
+
+  static contextTypes = {
+    identity: PropTypes.object,
+  };
+
+  static propTypes = {
+    intl: PropTypes.object.isRequired,
+    title: PropTypes.node,
+    icon: PropTypes.string,
+    active: PropTypes.bool,
+    multiColumn: PropTypes.bool,
+    extraButton: PropTypes.node,
+    showBackButton: PropTypes.bool,
+    children: PropTypes.node,
+    pinned: PropTypes.bool,
+    placeholder: PropTypes.bool,
+    onPin: PropTypes.func,
+    onMove: PropTypes.func,
+    onClick: PropTypes.func,
+    appendContent: PropTypes.node,
+    collapseIssues: PropTypes.bool,
+    ...WithRouterPropTypes,
+  };
+
+  state = {
+    collapsed: true,
+    animating: false,
+  };
+
+  handleToggleClick = (e) => {
+    e.stopPropagation();
+    this.setState({ collapsed: !this.state.collapsed, animating: true });
+  };
+
+  handleTitleClick = () => {
+    this.props.onClick?.();
+  };
+
+  handleMoveLeft = () => {
+    this.props.onMove(-1);
+  };
+
+  handleMoveRight = () => {
+    this.props.onMove(1);
+  };
+
+  handleBackClick = () => {
+    const { history } = this.props;
+
+    if (history.location?.state?.fromMastodon) {
+      history.goBack();
+    } else {
+      history.push('/');
+    }
+  };
+
+  handleTransitionEnd = () => {
+    this.setState({ animating: false });
+  };
+
+  handlePin = () => {
+    if (!this.props.pinned) {
+      this.props.history.replace('/');
+    }
+
+    this.props.onPin();
+  };
+
+  render () {
+    const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder, appendContent, collapseIssues, history } = this.props;
+    const { collapsed, animating } = this.state;
+
+    const wrapperClassName = classNames('column-header__wrapper', {
+      'active': active,
+    });
+
+    const buttonClassName = classNames('column-header', {
+      'active': active,
+    });
+
+    const collapsibleClassName = classNames('column-header__collapsible', {
+      'collapsed': collapsed,
+      'animating': animating,
+    });
+
+    const collapsibleButtonClassName = classNames('column-header__button', {
+      'active': !collapsed,
+    });
+
+    let extraContent, pinButton, moveButtons, backButton, collapseButton;
+
+    if (children) {
+      extraContent = (
+        <div key='extra-content' className='column-header__collapsible__extra'>
+          {children}
+        </div>
+      );
+    }
+
+    if (multiColumn && pinned) {
+      pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='times' /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>;
+
+      moveButtons = (
+        <div key='move-buttons' className='column-header__setting-arrows'>
+          <button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='icon-button column-header__setting-btn' onClick={this.handleMoveLeft}><Icon id='chevron-left' /></button>
+          <button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='icon-button column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' /></button>
+        </div>
+      );
+    } else if (multiColumn && this.props.onPin) {
+      pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
+    }
+
+    if (!pinned && ((multiColumn && history.location?.state?.fromMastodon) || showBackButton)) {
+      backButton = (
+        <button onClick={this.handleBackClick} className='column-header__back-button'>
+          <Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
+          <FormattedMessage id='column_back_button.label' defaultMessage='Back' />
+        </button>
+      );
+    }
+
+    const collapsedContent = [
+      extraContent,
+    ];
+
+    if (multiColumn) {
+      collapsedContent.push(pinButton);
+      collapsedContent.push(moveButtons);
+    }
+
+    if (this.context.identity.signedIn && (children || (multiColumn && this.props.onPin))) {
+      collapseButton = (
+        <button
+          className={collapsibleButtonClassName}
+          title={formatMessage(collapsed ? messages.show : messages.hide)}
+          aria-label={formatMessage(collapsed ? messages.show : messages.hide)}
+          onClick={this.handleToggleClick}
+        >
+          <i className='icon-with-badge'>
+            <Icon id='sliders' />
+            {collapseIssues && <i className='icon-with-badge__issue-badge' />}
+          </i>
+        </button>
+      );
+    }
+
+    const hasTitle = icon && title;
+
+    const component = (
+      <div className={wrapperClassName}>
+        <h1 className={buttonClassName}>
+          {hasTitle && (
+            <button onClick={this.handleTitleClick}>
+              <Icon id={icon} fixedWidth className='column-header__icon' />
+              {title}
+            </button>
+          )}
+
+          {!hasTitle && backButton}
+
+          <div className='column-header__buttons'>
+            {hasTitle && backButton}
+            {extraButton}
+            {collapseButton}
+          </div>
+        </h1>
+
+        <div className={collapsibleClassName} tabIndex={collapsed ? -1 : null} onTransitionEnd={this.handleTransitionEnd}>
+          <div className='column-header__collapsible-inner'>
+            {(!collapsed || animating) && collapsedContent}
+          </div>
+        </div>
+
+        {appendContent}
+      </div>
+    );
+
+    if (multiColumn || placeholder) {
+      return component;
+    } else {
+      // The portal container and the component may be rendered to the DOM in
+      // the same React render pass, so the container might not be available at
+      // the time `render()` is called.
+      const container = document.getElementById('tabs-bar__portal');
+      if (container === null) {
+        // The container wasn't available, force a re-render so that the
+        // component can eventually be inserted in the container and not scroll
+        // with the rest of the area.
+        this.forceUpdate();
+        return component;
+      } else {
+        return createPortal(component, container);
+      }
+    }
+  }
+
+}
+
+export default injectIntl(withRouter(ColumnHeader));
diff --git a/app/javascript/flavours/blobfox/components/counters.tsx b/app/javascript/flavours/blobfox/components/counters.tsx
new file mode 100644
index 00000000000000..35b0ad8d607421
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/counters.tsx
@@ -0,0 +1,45 @@
+import React from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+export const StatusesCounter = (
+  displayNumber: React.ReactNode,
+  pluralReady: number,
+) => (
+  <FormattedMessage
+    id='account.statuses_counter'
+    defaultMessage='{count, plural, one {{counter} Post} other {{counter} Posts}}'
+    values={{
+      count: pluralReady,
+      counter: <strong>{displayNumber}</strong>,
+    }}
+  />
+);
+
+export const FollowingCounter = (
+  displayNumber: React.ReactNode,
+  pluralReady: number,
+) => (
+  <FormattedMessage
+    id='account.following_counter'
+    defaultMessage='{count, plural, one {{counter} Following} other {{counter} Following}}'
+    values={{
+      count: pluralReady,
+      counter: <strong>{displayNumber}</strong>,
+    }}
+  />
+);
+
+export const FollowersCounter = (
+  displayNumber: React.ReactNode,
+  pluralReady: number,
+) => (
+  <FormattedMessage
+    id='account.followers_counter'
+    defaultMessage='{count, plural, one {{counter} Follower} other {{counter} Followers}}'
+    values={{
+      count: pluralReady,
+      counter: <strong>{displayNumber}</strong>,
+    }}
+  />
+);
diff --git a/app/javascript/flavours/blobfox/components/dismissable_banner.tsx b/app/javascript/flavours/blobfox/components/dismissable_banner.tsx
new file mode 100644
index 00000000000000..40ee47fb120834
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/dismissable_banner.tsx
@@ -0,0 +1,66 @@
+/* eslint-disable @typescript-eslint/no-unsafe-call,
+                  @typescript-eslint/no-unsafe-return,
+                  @typescript-eslint/no-unsafe-assignment,
+                  @typescript-eslint/no-unsafe-member-access
+                  -- the settings store is not yet typed */
+import type { PropsWithChildren } from 'react';
+import { useCallback, useState, useEffect } from 'react';
+
+import { defineMessages, useIntl } from 'react-intl';
+
+import { changeSetting } from 'flavours/blobfox/actions/settings';
+import { bannerSettings } from 'flavours/blobfox/settings';
+import { useAppSelector, useAppDispatch } from 'flavours/blobfox/store';
+
+import { IconButton } from './icon_button';
+
+const messages = defineMessages({
+  dismiss: { id: 'dismissable_banner.dismiss', defaultMessage: 'Dismiss' },
+});
+
+interface Props {
+  id: string;
+}
+
+export const DismissableBanner: React.FC<PropsWithChildren<Props>> = ({
+  id,
+  children,
+}) => {
+  const dismissed = useAppSelector((state) =>
+    state.settings.getIn(['dismissed_banners', id], false),
+  );
+  const dispatch = useAppDispatch();
+
+  const [visible, setVisible] = useState(!bannerSettings.get(id) && !dismissed);
+  const intl = useIntl();
+
+  const handleDismiss = useCallback(() => {
+    setVisible(false);
+    bannerSettings.set(id, true);
+    dispatch(changeSetting(['dismissed_banners', id], true));
+  }, [id, dispatch]);
+
+  useEffect(() => {
+    if (!visible && !dismissed) {
+      dispatch(changeSetting(['dismissed_banners', id], true));
+    }
+  }, [id, dispatch, visible, dismissed]);
+
+  if (!visible) {
+    return null;
+  }
+
+  return (
+    <div className='dismissable-banner'>
+      <div className='dismissable-banner__action'>
+        <IconButton
+          icon='times'
+          title={intl.formatMessage(messages.dismiss)}
+          onClick={handleDismiss}
+        />
+      </div>
+
+      <div className='dismissable-banner__message'>{children}</div>
+    </div>
+  );
+};
diff --git a/app/javascript/flavours/blobfox/components/display_name.tsx b/app/javascript/flavours/blobfox/components/display_name.tsx
new file mode 100644
index 00000000000000..3eae2e14504640
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/display_name.tsx
@@ -0,0 +1,127 @@
+import React from 'react';
+
+import classNames from 'classnames';
+
+import type { List } from 'immutable';
+
+import type { Account } from 'flavours/blobfox/models/account';
+
+import { autoPlayGif } from '../initial_state';
+
+import { Skeleton } from './skeleton';
+
+interface Props {
+  account?: Account;
+  others?: List<Account>;
+  localDomain?: string;
+  inline?: boolean;
+}
+
+export class DisplayName extends React.PureComponent<Props> {
+  handleMouseEnter: React.ReactEventHandler<HTMLSpanElement> = ({
+    currentTarget,
+  }) => {
+    if (autoPlayGif) {
+      return;
+    }
+
+    const emojis =
+      currentTarget.querySelectorAll<HTMLImageElement>('img.custom-emoji');
+
+    emojis.forEach((emoji) => {
+      const originalSrc = emoji.getAttribute('data-original');
+      if (originalSrc != null) emoji.src = originalSrc;
+    });
+  };
+
+  handleMouseLeave: React.ReactEventHandler<HTMLSpanElement> = ({
+    currentTarget,
+  }) => {
+    if (autoPlayGif) {
+      return;
+    }
+
+    const emojis =
+      currentTarget.querySelectorAll<HTMLImageElement>('img.custom-emoji');
+
+    emojis.forEach((emoji) => {
+      const staticSrc = emoji.getAttribute('data-static');
+      if (staticSrc != null) emoji.src = staticSrc;
+    });
+  };
+
+  render() {
+    const { others, localDomain, inline } = this.props;
+
+    let displayName: React.ReactNode,
+      suffix: React.ReactNode,
+      account: Account | undefined;
+
+    if (others && others.size > 0) {
+      account = others.first();
+    } else if (this.props.account) {
+      account = this.props.account;
+    }
+
+    if (others && others.size > 1) {
+      displayName = others
+        .take(2)
+        .map((a) => (
+          <bdi key={a.get('id')}>
+            <strong
+              className='display-name__html'
+              dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }}
+            />
+          </bdi>
+        ))
+        .reduce((prev, cur) => [prev, ', ', cur]);
+
+      if (others.size - 2 > 0) {
+        suffix = `+${others.size - 2}`;
+      }
+    } else if (account) {
+      let acct = account.get('acct');
+
+      if (!acct.includes('@') && localDomain) {
+        acct = `${acct}@${localDomain}`;
+      }
+
+      displayName = (
+        <bdi>
+          <strong
+            className='display-name__html'
+            dangerouslySetInnerHTML={{
+              __html: account.get('display_name_html'),
+            }}
+          />
+        </bdi>
+      );
+      suffix = <span className='display-name__account'>@{acct}</span>;
+    } else {
+      displayName = (
+        <bdi>
+          <strong className='display-name__html'>
+            <Skeleton width='10ch' />
+          </strong>
+        </bdi>
+      );
+      suffix = (
+        <span className='display-name__account'>
+          <Skeleton width='7ch' />
+        </span>
+      );
+    }
+
+    return (
+      <span
+        className={classNames('display-name', { inline })}
+        onMouseEnter={this.handleMouseEnter}
+        onMouseLeave={this.handleMouseLeave}
+      >
+        {displayName}
+        {inline ? ' ' : null}
+        {suffix}
+      </span>
+    );
+  }
+}
diff --git a/app/javascript/flavours/blobfox/components/domain.tsx b/app/javascript/flavours/blobfox/components/domain.tsx
new file mode 100644
index 00000000000000..f4a3b9d4b69d2a
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/domain.tsx
@@ -0,0 +1,44 @@
+import { useCallback } from 'react';
+
+import { defineMessages, useIntl } from 'react-intl';
+
+import { IconButton } from './icon_button';
+
+const messages = defineMessages({
+  unblockDomain: {
+    id: 'account.unblock_domain',
+    defaultMessage: 'Unblock domain {domain}',
+  },
+});
+
+interface Props {
+  domain: string;
+  onUnblockDomain: (domain: string) => void;
+}
+
+export const Domain: React.FC<Props> = ({ domain, onUnblockDomain }) => {
+  const intl = useIntl();
+
+  const handleDomainUnblock = useCallback(() => {
+    onUnblockDomain(domain);
+  }, [domain, onUnblockDomain]);
+
+  return (
+    <div className='domain'>
+      <div className='domain__wrapper'>
+        <span className='domain__domain-name'>
+          <strong>{domain}</strong>
+        </span>
+
+        <div className='domain__buttons'>
+          <IconButton
+            active
+            icon='unlock'
+            title={intl.formatMessage(messages.unblockDomain, { domain })}
+            onClick={handleDomainUnblock}
+          />
+        </div>
+      </div>
+    </div>
+  );
+};
diff --git a/app/javascript/flavours/blobfox/components/dropdown_menu.jsx b/app/javascript/flavours/blobfox/components/dropdown_menu.jsx
new file mode 100644
index 00000000000000..9640b4fdfeca45
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/dropdown_menu.jsx
@@ -0,0 +1,338 @@
+import PropTypes from 'prop-types';
+import { PureComponent, cloneElement, Children } from 'react';
+
+import classNames from 'classnames';
+import { withRouter } from 'react-router-dom';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
+import { supportsPassiveEvents } from 'detect-passive-events';
+import Overlay from 'react-overlays/Overlay';
+
+import { CircularProgress } from 'flavours/blobfox/components/circular_progress';
+import { WithRouterPropTypes } from 'flavours/blobfox/utils/react_router';
+
+import { IconButton } from './icon_button';
+
+const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
+let id = 0;
+
+class DropdownMenu extends PureComponent {
+
+  static propTypes = {
+    items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]).isRequired,
+    loading: PropTypes.bool,
+    scrollable: PropTypes.bool,
+    onClose: PropTypes.func.isRequired,
+    style: PropTypes.object,
+    openedViaKeyboard: PropTypes.bool,
+    renderItem: PropTypes.func,
+    renderHeader: PropTypes.func,
+    onItemClick: PropTypes.func.isRequired,
+  };
+
+  static defaultProps = {
+    style: {},
+  };
+
+  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('touchend', this.handleDocumentClick, listenerOptions);
+
+    if (this.focusedItem && this.props.openedViaKeyboard) {
+      this.focusedItem.focus({ preventScroll: true });
+    }
+  }
+
+  componentWillUnmount () {
+    document.removeEventListener('click', this.handleDocumentClick, { capture: true });
+    document.removeEventListener('keydown', this.handleKeyDown, { capture: true });
+    document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
+  }
+
+  setRef = c => {
+    this.node = c;
+  };
+
+  setFocusRef = c => {
+    this.focusedItem = c;
+  };
+
+  handleKeyDown = e => {
+    const items = Array.from(this.node.querySelectorAll('a, button'));
+    const index = items.indexOf(document.activeElement);
+    let element = null;
+
+    switch(e.key) {
+    case 'ArrowDown':
+      element = items[index+1] || items[0];
+      break;
+    case 'ArrowUp':
+      element = items[index-1] || items[items.length-1];
+      break;
+    case 'Tab':
+      if (e.shiftKey) {
+        element = items[index-1] || items[items.length-1];
+      } else {
+        element = items[index+1] || items[0];
+      }
+      break;
+    case 'Home':
+      element = items[0];
+      break;
+    case 'End':
+      element = items[items.length-1];
+      break;
+    case 'Escape':
+      this.props.onClose();
+      break;
+    }
+
+    if (element) {
+      element.focus();
+      e.preventDefault();
+      e.stopPropagation();
+    }
+  };
+
+  handleItemKeyPress = e => {
+    if (e.key === 'Enter' || e.key === ' ') {
+      this.handleClick(e);
+    }
+  };
+
+  handleClick = e => {
+    const { onItemClick } = this.props;
+    onItemClick(e);
+  };
+
+  renderItem = (option, i) => {
+    if (option === null) {
+      return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
+    }
+
+    const { text, href = '#', target = '_blank', method, dangerous } = option;
+
+    return (
+      <li className={classNames('dropdown-menu__item', { 'dropdown-menu__item--dangerous': dangerous })} key={`${text}-${i}`}>
+        <a href={href} target={target} data-method={method} rel='noopener noreferrer' role='button' tabIndex={0} ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}>
+          {text}
+        </a>
+      </li>
+    );
+  };
+
+  render () {
+    const { items, scrollable, renderHeader, loading } = this.props;
+
+    let renderItem = this.props.renderItem || this.renderItem;
+
+    return (
+      <div className={classNames('dropdown-menu__container', { 'dropdown-menu__container--loading': loading })} ref={this.setRef}>
+        {loading && (
+          <CircularProgress size={30} strokeWidth={3.5} />
+        )}
+
+        {!loading && renderHeader && (
+          <div className='dropdown-menu__container__header'>
+            {renderHeader(items)}
+          </div>
+        )}
+
+        {!loading && (
+          <ul className={classNames('dropdown-menu__container__list', { 'dropdown-menu__container__list--scrollable': scrollable })}>
+            {items.map((option, i) => renderItem(option, i, { onClick: this.handleClick, onKeyPress: this.handleItemKeyPress }))}
+          </ul>
+        )}
+      </div>
+    );
+  }
+
+}
+
+class Dropdown extends PureComponent {
+
+  static propTypes = {
+    children: PropTypes.node,
+    icon: PropTypes.string,
+    items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]).isRequired,
+    loading: PropTypes.bool,
+    size: PropTypes.number,
+    title: PropTypes.string,
+    disabled: PropTypes.bool,
+    scrollable: PropTypes.bool,
+    status: ImmutablePropTypes.map,
+    isUserTouching: PropTypes.func,
+    onOpen: PropTypes.func.isRequired,
+    onClose: PropTypes.func.isRequired,
+    openDropdownId: PropTypes.number,
+    openedViaKeyboard: PropTypes.bool,
+    renderItem: PropTypes.func,
+    renderHeader: PropTypes.func,
+    onItemClick: PropTypes.func,
+    ...WithRouterPropTypes
+  };
+
+  static defaultProps = {
+    title: 'Menu',
+  };
+
+  state = {
+    id: id++,
+  };
+
+  handleClick = ({ type }) => {
+    if (this.state.id === this.props.openDropdownId) {
+      this.handleClose();
+    } else {
+      this.props.onOpen(this.state.id, this.handleItemClick, type !== 'click');
+    }
+  };
+
+  handleClose = () => {
+    if (this.activeElement) {
+      this.activeElement.focus({ preventScroll: true });
+      this.activeElement = null;
+    }
+    this.props.onClose(this.state.id);
+  };
+
+  handleMouseDown = () => {
+    if (!this.state.open) {
+      this.activeElement = document.activeElement;
+    }
+  };
+
+  handleButtonKeyDown = (e) => {
+    switch(e.key) {
+    case ' ':
+    case 'Enter':
+      this.handleMouseDown();
+      break;
+    }
+  };
+
+  handleKeyPress = (e) => {
+    switch(e.key) {
+    case ' ':
+    case 'Enter':
+      this.handleClick(e);
+      e.stopPropagation();
+      e.preventDefault();
+      break;
+    }
+  };
+
+  handleItemClick = e => {
+    const { onItemClick } = this.props;
+    const i = Number(e.currentTarget.getAttribute('data-index'));
+    const item = this.props.items[i];
+
+    this.handleClose();
+
+    if (typeof onItemClick === 'function') {
+      e.preventDefault();
+      onItemClick(item, i);
+    } else if (item && typeof item.action === 'function') {
+      e.preventDefault();
+      item.action();
+    } else if (item && item.to) {
+      e.preventDefault();
+      this.props.history.push(item.to);
+    }
+  };
+
+  setTargetRef = c => {
+    this.target = c;
+  };
+
+  findTarget = () => {
+    return this.target;
+  };
+
+  componentWillUnmount = () => {
+    if (this.state.id === this.props.openDropdownId) {
+      this.handleClose();
+    }
+  };
+
+  close = () => {
+    this.handleClose();
+  };
+
+  render () {
+    const {
+      icon,
+      items,
+      size,
+      title,
+      disabled,
+      loading,
+      scrollable,
+      openDropdownId,
+      openedViaKeyboard,
+      children,
+      renderItem,
+      renderHeader,
+    } = this.props;
+
+    const open = this.state.id === openDropdownId;
+
+    const button = children ? cloneElement(Children.only(children), {
+      onClick: this.handleClick,
+      onMouseDown: this.handleMouseDown,
+      onKeyDown: this.handleButtonKeyDown,
+      onKeyPress: this.handleKeyPress,
+    }) : (
+      <IconButton
+        icon={icon}
+        title={title}
+        active={open}
+        disabled={disabled}
+        size={size}
+        onClick={this.handleClick}
+        onMouseDown={this.handleMouseDown}
+        onKeyDown={this.handleButtonKeyDown}
+        onKeyPress={this.handleKeyPress}
+      />
+    );
+
+    return (
+      <>
+        <span ref={this.setTargetRef}>
+          {button}
+        </span>
+        <Overlay show={open} offset={[5, 5]} placement={'bottom'} flip target={this.findTarget} popperConfig={{ strategy: 'fixed' }}>
+          {({ props, arrowProps, placement }) => (
+            <div {...props}>
+              <div className={`dropdown-animation dropdown-menu ${placement}`}>
+                <div className={`dropdown-menu__arrow ${placement}`} {...arrowProps} />
+                <DropdownMenu
+                  items={items}
+                  loading={loading}
+                  scrollable={scrollable}
+                  onClose={this.handleClose}
+                  openedViaKeyboard={openedViaKeyboard}
+                  renderItem={renderItem}
+                  renderHeader={renderHeader}
+                  onItemClick={this.handleItemClick}
+                />
+              </div>
+            </div>
+          )}
+        </Overlay>
+      </>
+    );
+  }
+
+}
+
+export default withRouter(Dropdown);
diff --git a/app/javascript/flavours/blobfox/components/edited_timestamp/containers/dropdown_menu_container.js b/app/javascript/flavours/blobfox/components/edited_timestamp/containers/dropdown_menu_container.js
new file mode 100644
index 00000000000000..b52f19e46705c4
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/edited_timestamp/containers/dropdown_menu_container.js
@@ -0,0 +1,32 @@
+import { connect } from 'react-redux';
+
+import { openDropdownMenu, closeDropdownMenu } from 'flavours/blobfox/actions/dropdown_menu';
+import { fetchHistory } from 'flavours/blobfox/actions/history';
+import DropdownMenu from 'flavours/blobfox/components/dropdown_menu';
+
+/**
+ *
+ * @param {import('flavours/blobfox/store').RootState} state
+ * @param {*} props
+ */
+const mapStateToProps = (state, { statusId }) => ({
+  openDropdownId: state.dropdownMenu.openId,
+  openedViaKeyboard: state.dropdownMenu.keyboard,
+  items: state.getIn(['history', statusId, 'items']),
+  loading: state.getIn(['history', statusId, 'loading']),
+});
+
+const mapDispatchToProps = (dispatch, { statusId }) => ({
+
+  onOpen (id, onItemClick, keyboard) {
+    dispatch(fetchHistory(statusId));
+    dispatch(openDropdownMenu({ id, keyboard }));
+  },
+
+  onClose (id) {
+    dispatch(closeDropdownMenu({ id }));
+  },
+
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(DropdownMenu);
diff --git a/app/javascript/flavours/blobfox/components/edited_timestamp/index.jsx b/app/javascript/flavours/blobfox/components/edited_timestamp/index.jsx
new file mode 100644
index 00000000000000..d38b2865b4a2fa
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/edited_timestamp/index.jsx
@@ -0,0 +1,77 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { FormattedMessage, injectIntl } from 'react-intl';
+
+import { connect } from 'react-redux';
+
+import { openModal } from 'flavours/blobfox/actions/modal';
+import { Icon }  from 'flavours/blobfox/components/icon';
+import InlineAccount from 'flavours/blobfox/components/inline_account';
+import { RelativeTimestamp } from 'flavours/blobfox/components/relative_timestamp';
+
+import DropdownMenu from './containers/dropdown_menu_container';
+
+const mapDispatchToProps = (dispatch, { statusId }) => ({
+
+  onItemClick (index) {
+    dispatch(openModal({
+      modalType: 'COMPARE_HISTORY',
+      modalProps: { index, statusId },
+    }));
+  },
+
+});
+
+class EditedTimestamp extends PureComponent {
+
+  static propTypes = {
+    statusId: PropTypes.string.isRequired,
+    timestamp: PropTypes.string.isRequired,
+    intl: PropTypes.object.isRequired,
+    onItemClick: PropTypes.func.isRequired,
+  };
+
+  handleItemClick = (item, i) => {
+    const { onItemClick } = this.props;
+    onItemClick(i);
+  };
+
+  renderHeader = items => {
+    return (
+      <FormattedMessage id='status.edited_x_times' defaultMessage='Edited {count, plural, one {# time} other {# times}}' values={{ count: items.size - 1 }} />
+    );
+  };
+
+  renderItem = (item, index, { onClick, onKeyPress }) => {
+    const formattedDate = <RelativeTimestamp timestamp={item.get('created_at')} short={false} />;
+    const formattedName = <InlineAccount accountId={item.get('account')} />;
+
+    const label = item.get('original') ? (
+      <FormattedMessage id='status.history.created' defaultMessage='{name} created {date}' values={{ name: formattedName, date: formattedDate }} />
+    ) : (
+      <FormattedMessage id='status.history.edited' defaultMessage='{name} edited {date}' values={{ name: formattedName, date: formattedDate }} />
+    );
+
+    return (
+      <li className='dropdown-menu__item edited-timestamp__history__item' key={item.get('created_at')}>
+        <button data-index={index} onClick={onClick} onKeyPress={onKeyPress}>{label}</button>
+      </li>
+    );
+  };
+
+  render () {
+    const { timestamp, intl, statusId } = this.props;
+
+    return (
+      <DropdownMenu statusId={statusId} renderItem={this.renderItem} scrollable renderHeader={this.renderHeader} onItemClick={this.handleItemClick}>
+        <button className='dropdown-menu__text-button'>
+          <FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: intl.formatDate(timestamp, { hour12: false, month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) }} /> <Icon id='caret-down' />
+        </button>
+      </DropdownMenu>
+    );
+  }
+
+}
+
+export default connect(null, mapDispatchToProps)(injectIntl(EditedTimestamp));
diff --git a/app/javascript/flavours/blobfox/components/empty_account.tsx b/app/javascript/flavours/blobfox/components/empty_account.tsx
new file mode 100644
index 00000000000000..1c2ddc1aed2035
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/empty_account.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+
+import classNames from 'classnames';
+
+import { DisplayName } from 'flavours/blobfox/components/display_name';
+import { Skeleton } from 'flavours/blobfox/components/skeleton';
+
+interface Props {
+  size?: number;
+  minimal?: boolean;
+}
+
+export const EmptyAccount: React.FC<Props> = ({
+  size = 46,
+  minimal = false,
+}) => {
+  return (
+    <div className={classNames('account', { 'account--minimal': minimal })}>
+      <div className='account__wrapper'>
+        <div className='account__display-name'>
+          <div className='account__avatar-wrapper'>
+            <Skeleton width={size} height={size} />
+          </div>
+
+          <div>
+            <DisplayName />
+            <Skeleton width='7ch' />
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+};
diff --git a/app/javascript/flavours/blobfox/components/error_boundary.jsx b/app/javascript/flavours/blobfox/components/error_boundary.jsx
new file mode 100644
index 00000000000000..3cea43c06ecf39
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/error_boundary.jsx
@@ -0,0 +1,111 @@
+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 { version, source_url } from 'flavours/blobfox/initial_state';
+
+export default class ErrorBoundary extends PureComponent {
+
+  static propTypes = {
+    children: PropTypes.node,
+  };
+
+  state = {
+    hasError: false,
+    errorMessage: undefined,
+    stackTrace: undefined,
+    mappedStackTrace: undefined,
+    componentStack: undefined,
+  };
+
+  componentDidCatch (error, info) {
+    this.setState({
+      hasError: true,
+      errorMessage: error.toString(),
+      stackTrace: error.stack,
+      componentStack: info && info.componentStack,
+      mappedStackTrace: undefined,
+    });
+
+    StackTrace.fromError(error).then((stackframes) => {
+      this.setState({
+        mappedStackTrace: stackframes.map((sf) => sf.toString()).join('\n'),
+      });
+    }).catch(() => {
+      this.setState({
+        mappedStackTrace: undefined,
+      });
+    });
+  }
+
+  handleCopyStackTrace = () => {
+    const { errorMessage, stackTrace, mappedStackTrace } = this.state;
+    const textarea = document.createElement('textarea');
+
+    let contents = [errorMessage, stackTrace];
+    if (mappedStackTrace) {
+      contents.push(mappedStackTrace);
+    }
+
+    textarea.textContent    = contents.join('\n\n\n');
+    textarea.style.position = 'fixed';
+
+    document.body.appendChild(textarea);
+
+    try {
+      textarea.select();
+      document.execCommand('copy');
+    } catch (e) {
+
+    } finally {
+      document.body.removeChild(textarea);
+    }
+
+    this.setState({ copied: true });
+    setTimeout(() => this.setState({ copied: false }), 700);
+  };
+
+  render() {
+    const { hasError, copied, errorMessage } = this.state;
+
+    if (!hasError) {
+      return this.props.children;
+    }
+
+    const likelyBrowserAddonIssue = errorMessage && errorMessage.includes('NotFoundError');
+
+    return (
+      <div className='error-boundary'>
+        <div>
+          <p className='error-boundary__error'>
+            { likelyBrowserAddonIssue ? (
+              <FormattedMessage id='error.unexpected_crash.explanation_addons' defaultMessage='This page could not be displayed correctly. This error is likely caused by a browser add-on or automatic translation tools.' />
+            ) : (
+              <FormattedMessage id='error.unexpected_crash.explanation' defaultMessage='Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.' />
+            )}
+          </p>
+
+          <p>
+            { likelyBrowserAddonIssue ? (
+              <FormattedMessage id='error.unexpected_crash.next_steps_addons' defaultMessage='Try disabling them and refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.' />
+            ) : (
+              <FormattedMessage id='error.unexpected_crash.next_steps' defaultMessage='Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.' />
+            )}
+          </p>
+
+          <p className='error-boundary__footer'>Mastodon v{version} · <a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='errors.unexpected_crash.report_issue' defaultMessage='Report issue' /></a> · <button onClick={this.handleCopyStackTrace} className={copied ? 'copied' : ''}><FormattedMessage id='errors.unexpected_crash.copy_stacktrace' defaultMessage='Copy stacktrace to clipboard' /></button></p>
+        </div>
+
+        <Helmet>
+          <meta name='robots' content='noindex' />
+        </Helmet>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/components/gifv.tsx b/app/javascript/flavours/blobfox/components/gifv.tsx
new file mode 100644
index 00000000000000..c2be591128f458
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/gifv.tsx
@@ -0,0 +1,70 @@
+import { useCallback, useState } from 'react';
+
+interface Props {
+  src: string;
+  key: string;
+  alt?: string;
+  lang?: string;
+  width: number;
+  height: number;
+  onClick?: () => void;
+}
+
+export const GIFV: React.FC<Props> = ({
+  src,
+  alt,
+  lang,
+  width,
+  height,
+  onClick,
+}) => {
+  const [loading, setLoading] = useState(true);
+
+  const handleLoadedData: React.ReactEventHandler<HTMLVideoElement> =
+    useCallback(() => {
+      setLoading(false);
+    }, [setLoading]);
+
+  const handleClick: React.MouseEventHandler = useCallback(
+    (e) => {
+      if (onClick) {
+        e.stopPropagation();
+        onClick();
+      }
+    },
+    [onClick],
+  );
+
+  return (
+    <div className='gifv' style={{ position: 'relative' }}>
+      {loading && (
+        <canvas
+          width={width}
+          height={height}
+          role='button'
+          tabIndex={0}
+          aria-label={alt}
+          title={alt}
+          lang={lang}
+          onClick={handleClick}
+        />
+      )}
+
+      <video
+        src={src}
+        role='button'
+        tabIndex={0}
+        aria-label={alt}
+        title={alt}
+        lang={lang}
+        muted
+        loop
+        autoPlay
+        playsInline
+        onClick={handleClick}
+        onLoadedData={handleLoadedData}
+        style={{ position: loading ? 'absolute' : 'static', top: 0, left: 0 }}
+      />
+    </div>
+  );
+};
diff --git a/app/javascript/flavours/blobfox/components/hashtag.jsx b/app/javascript/flavours/blobfox/components/hashtag.jsx
new file mode 100644
index 00000000000000..101c8c518a4efd
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/hashtag.jsx
@@ -0,0 +1,123 @@
+// @ts-check
+import PropTypes from 'prop-types';
+import { Component } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import classNames from 'classnames';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
+import { Sparklines, SparklinesCurve } from 'react-sparklines';
+
+import { ShortNumber } from 'flavours/blobfox/components/short_number';
+import { Skeleton } from 'flavours/blobfox/components/skeleton';
+
+import Permalink from './permalink';
+
+class SilentErrorBoundary extends Component {
+
+  static propTypes = {
+    children: PropTypes.node,
+  };
+
+  state = {
+    error: false,
+  };
+
+  componentDidCatch() {
+    this.setState({ error: true });
+  }
+
+  render() {
+    if (this.state.error) {
+      return null;
+    }
+
+    return this.props.children;
+  }
+
+}
+
+/**
+ * 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) => (
+  <FormattedMessage
+    id='trends.counter_by_accounts'
+    defaultMessage='{count, plural, one {{counter} person} other {{counter} people}} in the past {days, plural, one {day} other {# days}}'
+    values={{
+      count: pluralReady,
+      counter: <strong>{displayNumber}</strong>,
+      days: 2,
+    }}
+  />
+);
+
+// @ts-expect-error
+export const ImmutableHashtag = ({ hashtag }) => (
+  <Hashtag
+    name={hashtag.get('name')}
+    href={hashtag.get('url')}
+    to={`/tags/${hashtag.get('name')}`}
+    people={hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1}
+    // @ts-expect-error
+    history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()}
+  />
+);
+
+ImmutableHashtag.propTypes = {
+  hashtag: ImmutablePropTypes.map.isRequired,
+};
+
+// @ts-expect-error
+const Hashtag = ({ name, href, to, people, uses, history, className, description, withGraph }) => (
+  <div className={classNames('trends__item', className)}>
+    <div className='trends__item__name'>
+      <Permalink href={href} to={to}>
+        {name ? <>#<span>{name}</span></> : <Skeleton width={50} />}
+      </Permalink>
+
+      {description ? (
+        <span>{description}</span>
+      ) : (
+        typeof people !== 'undefined' ? <ShortNumber value={people} renderer={accountsCountRenderer} /> : <Skeleton width={100} />
+      )}
+    </div>
+
+    {typeof uses !== 'undefined' && (
+      <div className='trends__item__current'>
+        <ShortNumber value={uses} />
+      </div>
+    )}
+
+    {withGraph && (
+      <div className='trends__item__sparkline'>
+        <SilentErrorBoundary>
+          <Sparklines width={50} height={28} data={history ? history : Array.from(Array(7)).map(() => 0)}>
+            <SparklinesCurve style={{ fill: 'none' }} />
+          </Sparklines>
+        </SilentErrorBoundary>
+      </div>
+    )}
+  </div>
+);
+
+Hashtag.propTypes = {
+  name: PropTypes.string,
+  href: PropTypes.string,
+  to: PropTypes.string,
+  people: PropTypes.number,
+  description: PropTypes.node,
+  uses: PropTypes.number,
+  history: PropTypes.arrayOf(PropTypes.number),
+  className: PropTypes.string,
+  withGraph: PropTypes.bool,
+};
+
+Hashtag.defaultProps = {
+  withGraph: true,
+};
+
+export default Hashtag;
diff --git a/app/javascript/flavours/blobfox/components/hashtag_bar.tsx b/app/javascript/flavours/blobfox/components/hashtag_bar.tsx
new file mode 100644
index 00000000000000..91fa9221983a70
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/hashtag_bar.tsx
@@ -0,0 +1,234 @@
+import { useState, useCallback } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import { Link } from 'react-router-dom';
+
+import type { List, Record } from 'immutable';
+
+import { groupBy, minBy } from 'lodash';
+
+import { getStatusContent } from './status_content';
+
+// Fit on a single line on desktop
+const VISIBLE_HASHTAGS = 3;
+
+// Those types are not correct, they need to be replaced once this part of the state is typed
+export type TagLike = Record<{ name: string }>;
+export type StatusLike = Record<{
+  tags: List<TagLike>;
+  contentHTML: string;
+  media_attachments: List<unknown>;
+  spoiler_text?: string;
+}>;
+
+function normalizeHashtag(hashtag: string) {
+  return (
+    hashtag && hashtag.startsWith('#') ? hashtag.slice(1) : hashtag
+  ).normalize('NFKC');
+}
+
+function isNodeLinkHashtag(element: Node): element is HTMLLinkElement {
+  return (
+    element instanceof HTMLAnchorElement &&
+    // it may be a <a> starting with a hashtag
+    (element.textContent?.[0] === '#' ||
+      // or a #<a>
+      element.previousSibling?.textContent?.[
+        element.previousSibling.textContent.length - 1
+      ] === '#')
+  );
+}
+
+/**
+ * Removes duplicates from an hashtag list, case-insensitive, keeping only the best one
+ * "Best" here is defined by the one with the more casing difference (ie, the most camel-cased one)
+ * @param hashtags The list of hashtags
+ * @returns The input hashtags, but with only 1 occurence of each (case-insensitive)
+ */
+function uniqueHashtagsWithCaseHandling(hashtags: string[]) {
+  const groups = groupBy(hashtags, (tag) =>
+    tag.normalize('NFKD').toLowerCase(),
+  );
+
+  return Object.values(groups).map((tags) => {
+    if (tags.length === 1) return tags[0];
+
+    // The best match is the one where we have the less difference between upper and lower case letter count
+    const best = minBy(tags, (tag) => {
+      const upperCase = Array.from(tag).reduce(
+        (acc, char) => (acc += char.toUpperCase() === char ? 1 : 0),
+        0,
+      );
+
+      const lowerCase = tag.length - upperCase;
+
+      return Math.abs(lowerCase - upperCase);
+    });
+
+    return best ?? tags[0];
+  });
+}
+
+// Create the collator once, this is much more efficient
+const collator = new Intl.Collator(undefined, {
+  sensitivity: 'base', // we use this to emulate the ASCII folding done on the server-side, hopefuly more efficiently
+});
+
+function localeAwareInclude(collection: string[], value: string) {
+  const normalizedValue = value.normalize('NFKC');
+
+  return !!collection.find(
+    (item) => collator.compare(item.normalize('NFKC'), normalizedValue) === 0,
+  );
+}
+
+// We use an intermediate function here to make it easier to test
+export function computeHashtagBarForStatus(status: StatusLike): {
+  statusContentProps: { statusContent: string };
+  hashtagsInBar: string[];
+} {
+  let statusContent = getStatusContent(status);
+
+  const tagNames = status
+    .get('tags')
+    .map((tag) => tag.get('name'))
+    .toJS();
+
+  // this is returned if we stop the processing early, it does not change what is displayed
+  const defaultResult = {
+    statusContentProps: { statusContent },
+    hashtagsInBar: [],
+  };
+
+  // return early if this status does not have any tags
+  if (tagNames.length === 0) return defaultResult;
+
+  const template = document.createElement('template');
+  template.innerHTML = statusContent.trim();
+
+  const lastChild = template.content.lastChild;
+
+  if (!lastChild || lastChild.nodeType === Node.TEXT_NODE) return defaultResult;
+
+  template.content.removeChild(lastChild);
+  const contentWithoutLastLine = template;
+
+  // First, try to parse
+  const contentHashtags = Array.from(
+    contentWithoutLastLine.content.querySelectorAll<HTMLLinkElement>('a[href]'),
+  ).reduce<string[]>((result, link) => {
+    if (isNodeLinkHashtag(link)) {
+      if (link.textContent) result.push(normalizeHashtag(link.textContent));
+    }
+    return result;
+  }, []);
+
+  // Now we parse the last line, and try to see if it only contains hashtags
+  const lastLineHashtags: string[] = [];
+  // try to see if the last line is only hashtags
+  let onlyHashtags = true;
+
+  const normalizedTagNames = tagNames.map((tag) => tag.normalize('NFKC'));
+
+  Array.from(lastChild.childNodes).forEach((node) => {
+    if (isNodeLinkHashtag(node) && node.textContent) {
+      const normalized = normalizeHashtag(node.textContent);
+
+      if (!localeAwareInclude(normalizedTagNames, normalized)) {
+        // stop here, this is not a real hashtag, so consider it as text
+        onlyHashtags = false;
+        return;
+      }
+
+      if (!localeAwareInclude(contentHashtags, normalized))
+        // only add it if it does not appear in the rest of the content
+        lastLineHashtags.push(normalized);
+    } else if (node.nodeType !== Node.TEXT_NODE || node.nodeValue?.trim()) {
+      // not a space
+      onlyHashtags = false;
+    }
+  });
+
+  const hashtagsInBar = tagNames.filter((tag) => {
+    const normalizedTag = tag.normalize('NFKC');
+    // the tag does not appear at all in the status content, it is an out-of-band tag
+    return (
+      !localeAwareInclude(contentHashtags, normalizedTag) &&
+      !localeAwareInclude(lastLineHashtags, normalizedTag)
+    );
+  });
+
+  const isOnlyOneLine = contentWithoutLastLine.content.childElementCount === 0;
+  const hasMedia = status.get('media_attachments').size > 0;
+  const hasSpoiler = !!status.get('spoiler_text');
+
+  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- due to https://github.com/microsoft/TypeScript/issues/9998
+  if (onlyHashtags && ((hasMedia && !hasSpoiler) || !isOnlyOneLine)) {
+    // if the last line only contains hashtags, and we either:
+    // - have other content in the status
+    // - dont have other content, but a media and no CW. If it has a CW, then we do not remove the content to avoid having an empty content behind the CW button
+    statusContent = contentWithoutLastLine.innerHTML;
+    // and add the tags to the bar
+    hashtagsInBar.push(...lastLineHashtags);
+  }
+
+  return {
+    statusContentProps: { statusContent },
+    hashtagsInBar: uniqueHashtagsWithCaseHandling(hashtagsInBar),
+  };
+}
+
+/**
+ *  This function will process a status to, at the same time (avoiding parsing it twice):
+ * - build the HashtagBar for this status
+ * - remove the last-line hashtags from the status content
+ * @param status The status to process
+ * @returns Props to be passed to the <StatusContent> component, and the hashtagBar to render
+ */
+export function getHashtagBarForStatus(status: StatusLike) {
+  const { statusContentProps, hashtagsInBar } =
+    computeHashtagBarForStatus(status);
+
+  return {
+    statusContentProps,
+    hashtagBar: <HashtagBar hashtags={hashtagsInBar} />,
+  };
+}
+
+const HashtagBar: React.FC<{
+  hashtags: string[];
+}> = ({ hashtags }) => {
+  const [expanded, setExpanded] = useState(false);
+  const handleClick = useCallback(() => {
+    setExpanded(true);
+  }, []);
+
+  if (hashtags.length === 0) {
+    return null;
+  }
+
+  const revealedHashtags = expanded
+    ? hashtags
+    : hashtags.slice(0, VISIBLE_HASHTAGS);
+
+  return (
+    <div className='hashtag-bar'>
+      {revealedHashtags.map((hashtag) => (
+        <Link key={hashtag} to={`/tags/${hashtag}`}>
+          #<span>{hashtag}</span>
+        </Link>
+      ))}
+
+      {!expanded && hashtags.length > VISIBLE_HASHTAGS && (
+        <button className='link-button' onClick={handleClick}>
+          <FormattedMessage
+            id='hashtags.and_other'
+            defaultMessage='…and {count, plural, other {# more}}'
+            values={{ count: hashtags.length - VISIBLE_HASHTAGS }}
+          />
+        </button>
+      )}
+    </div>
+  );
+};
diff --git a/app/javascript/flavours/blobfox/components/icon.tsx b/app/javascript/flavours/blobfox/components/icon.tsx
new file mode 100644
index 00000000000000..3d091c7059e18c
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/icon.tsx
@@ -0,0 +1,20 @@
+import classNames from 'classnames';
+
+interface Props extends React.HTMLAttributes<HTMLImageElement> {
+  id: string;
+  className?: string;
+  fixedWidth?: boolean;
+  children?: never;
+}
+
+export const Icon: React.FC<Props> = ({
+  id,
+  className,
+  fixedWidth,
+  ...other
+}) => (
+  <i
+    className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })}
+    {...other}
+  />
+);
diff --git a/app/javascript/flavours/blobfox/components/icon_button.tsx b/app/javascript/flavours/blobfox/components/icon_button.tsx
new file mode 100644
index 00000000000000..8bca60fa971f78
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/icon_button.tsx
@@ -0,0 +1,180 @@
+import { PureComponent } from 'react';
+
+import classNames from 'classnames';
+
+import { AnimatedNumber } from './animated_number';
+import { Icon } from './icon';
+
+interface Props {
+  className?: string;
+  title: string;
+  icon: string;
+  onClick?: React.MouseEventHandler<HTMLButtonElement>;
+  onMouseDown?: React.MouseEventHandler<HTMLButtonElement>;
+  onKeyDown?: React.KeyboardEventHandler<HTMLButtonElement>;
+  onKeyPress?: React.KeyboardEventHandler<HTMLButtonElement>;
+  size: number;
+  active: boolean;
+  expanded?: boolean;
+  style?: React.CSSProperties;
+  activeStyle?: React.CSSProperties;
+  disabled: boolean;
+  inverted?: boolean;
+  animate: boolean;
+  overlay: boolean;
+  tabIndex: number;
+  label?: string;
+  counter?: number;
+  obfuscateCount?: boolean;
+  href?: string;
+  ariaHidden: boolean;
+}
+interface States {
+  activate: boolean;
+  deactivate: boolean;
+}
+export class IconButton extends PureComponent<Props, States> {
+  static defaultProps = {
+    size: 18,
+    active: false,
+    disabled: false,
+    animate: false,
+    overlay: false,
+    tabIndex: 0,
+    ariaHidden: false,
+  };
+
+  state = {
+    activate: false,
+    deactivate: false,
+  };
+
+  UNSAFE_componentWillReceiveProps(nextProps: Props) {
+    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: React.MouseEventHandler<HTMLButtonElement> = (e) => {
+    e.preventDefault();
+
+    if (!this.props.disabled && this.props.onClick != null) {
+      this.props.onClick(e);
+    }
+  };
+
+  handleKeyPress: React.KeyboardEventHandler<HTMLButtonElement> = (e) => {
+    if (this.props.onKeyPress && !this.props.disabled) {
+      this.props.onKeyPress(e);
+    }
+  };
+
+  handleMouseDown: React.MouseEventHandler<HTMLButtonElement> = (e) => {
+    if (!this.props.disabled && this.props.onMouseDown) {
+      this.props.onMouseDown(e);
+    }
+  };
+
+  handleKeyDown: React.KeyboardEventHandler<HTMLButtonElement> = (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`;
+    }
+
+    const 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 = (
+      <>
+        <Icon id={icon} fixedWidth aria-hidden='true' />{' '}
+        {typeof counter !== 'undefined' && (
+          <span className='icon-button__counter'>
+            <AnimatedNumber value={counter} obfuscate={obfuscateCount} />
+          </span>
+        )}
+        {this.props.label}
+      </>
+    );
+
+    if (href != null) {
+      contents = (
+        <a href={href} target='_blank' rel='noopener noreferrer'>
+          {contents}
+        </a>
+      );
+    }
+
+    return (
+      <button
+        type='button'
+        aria-label={title}
+        aria-expanded={expanded}
+        aria-hidden={ariaHidden}
+        title={title}
+        className={classes}
+        onClick={this.handleClick}
+        onMouseDown={this.handleMouseDown}
+        onKeyDown={this.handleKeyDown}
+        onKeyPress={this.handleKeyPress}
+        style={style}
+        tabIndex={tabIndex}
+        disabled={disabled}
+      >
+        {contents}
+      </button>
+    );
+  }
+}
diff --git a/app/javascript/flavours/blobfox/components/icon_with_badge.tsx b/app/javascript/flavours/blobfox/components/icon_with_badge.tsx
new file mode 100644
index 00000000000000..8898f413299480
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/icon_with_badge.tsx
@@ -0,0 +1,24 @@
+import { Icon } from './icon';
+
+const formatNumber = (num: number): number | string => (num > 40 ? '40+' : num);
+
+interface Props {
+  id: string;
+  count: number;
+  issueBadge: boolean;
+  className: string;
+}
+export const IconWithBadge: React.FC<Props> = ({
+  id,
+  count,
+  issueBadge,
+  className,
+}) => (
+  <i className='icon-with-badge'>
+    <Icon id={id} fixedWidth className={className} />
+    {count > 0 && (
+      <i className='icon-with-badge__badge'>{formatNumber(count)}</i>
+    )}
+    {issueBadge && <i className='icon-with-badge__issue-badge' />}
+  </i>
+);
diff --git a/app/javascript/flavours/blobfox/components/inline_account.jsx b/app/javascript/flavours/blobfox/components/inline_account.jsx
new file mode 100644
index 00000000000000..dd913c5e81ccef
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/inline_account.jsx
@@ -0,0 +1,37 @@
+import { PureComponent } from 'react';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
+
+import { Avatar } from 'flavours/blobfox/components/avatar';
+import { makeGetAccount } from 'flavours/blobfox/selectors';
+
+const makeMapStateToProps = () => {
+  const getAccount = makeGetAccount();
+
+  const mapStateToProps = (state, { accountId }) => ({
+    account: getAccount(state, accountId),
+  });
+
+  return mapStateToProps;
+};
+
+class InlineAccount extends PureComponent {
+
+  static propTypes = {
+    account: ImmutablePropTypes.record.isRequired,
+  };
+
+  render () {
+    const { account } = this.props;
+
+    return (
+      <span className='inline-account'>
+        <Avatar size={13} account={account} /> <strong>{account.get('username')}</strong>
+      </span>
+    );
+  }
+
+}
+
+export default connect(makeMapStateToProps)(InlineAccount);
diff --git a/app/javascript/flavours/blobfox/components/intersection_observer_article.jsx b/app/javascript/flavours/blobfox/components/intersection_observer_article.jsx
new file mode 100644
index 00000000000000..8efa969f9bff56
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/intersection_observer_article.jsx
@@ -0,0 +1,131 @@
+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';
+
+// Diff these props in the "unrendered" state
+const updateOnPropsForUnrendered = ['id', 'index', 'listLength', 'cachedHeight'];
+
+export default class IntersectionObserverArticle extends Component {
+
+  static propTypes = {
+    intersectionObserverWrapper: PropTypes.object.isRequired,
+    id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+    index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+    listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+    saveHeightKey: PropTypes.string,
+    cachedHeight: PropTypes.number,
+    onHeightChange: PropTypes.func,
+    children: PropTypes.node,
+  };
+
+  state = {
+    isHidden: false, // set to true in requestIdleCallback to trigger un-render
+  };
+
+  shouldComponentUpdate (nextProps, nextState) {
+    const isUnrendered = !this.state.isIntersecting && (this.state.isHidden || this.props.cachedHeight);
+    const willBeUnrendered = !nextState.isIntersecting && (nextState.isHidden || nextProps.cachedHeight);
+    if (!!isUnrendered !== !!willBeUnrendered) {
+      // If we're going from rendered to unrendered (or vice versa) then update
+      return true;
+    }
+    // If we are and remain hidden, diff based on props
+    if (isUnrendered) {
+      return !updateOnPropsForUnrendered.every(prop => nextProps[prop] === this.props[prop]);
+    }
+    // Else, assume the children have changed
+    return true;
+  }
+
+  componentDidMount () {
+    const { intersectionObserverWrapper, id } = this.props;
+
+    intersectionObserverWrapper.observe(
+      id,
+      this.node,
+      this.handleIntersection,
+    );
+
+    this.componentMounted = true;
+  }
+
+  componentWillUnmount () {
+    const { intersectionObserverWrapper, id } = this.props;
+    intersectionObserverWrapper.unobserve(id, this.node);
+
+    this.componentMounted = false;
+  }
+
+  handleIntersection = (entry) => {
+    this.entry = entry;
+
+    scheduleIdleTask(this.calculateHeight);
+    this.setState(this.updateStateAfterIntersection);
+  };
+
+  updateStateAfterIntersection = (prevState) => {
+    if (prevState.isIntersecting !== false && !this.entry.isIntersecting) {
+      scheduleIdleTask(this.hideIfNotIntersecting);
+    }
+    return {
+      isIntersecting: this.entry.isIntersecting,
+      isHidden: false,
+    };
+  };
+
+  calculateHeight = () => {
+    const { onHeightChange, saveHeightKey, id } = this.props;
+    // save the height of the fully-rendered element (this is expensive
+    // on Chrome, where we need to fall back to getBoundingClientRect)
+    this.height = getRectFromEntry(this.entry).height;
+
+    if (onHeightChange && saveHeightKey) {
+      onHeightChange(saveHeightKey, id, this.height);
+    }
+  };
+
+  hideIfNotIntersecting = () => {
+    if (!this.componentMounted) {
+      return;
+    }
+
+    // When the browser gets a chance, test if we're still not intersecting,
+    // and if so, set our isHidden to true to trigger an unrender. The point of
+    // this is to save DOM nodes and avoid using up too much memory.
+    // See: https://github.com/mastodon/mastodon/issues/2900
+    this.setState((prevState) => ({ isHidden: !prevState.isIntersecting }));
+  };
+
+  handleRef = (node) => {
+    this.node = node;
+  };
+
+  render () {
+    const { children, id, index, listLength, cachedHeight } = this.props;
+    const { isIntersecting, isHidden } = this.state;
+
+    if (!isIntersecting && (isHidden || cachedHeight)) {
+      return (
+        <article
+          ref={this.handleRef}
+          aria-posinset={index + 1}
+          aria-setsize={listLength}
+          style={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: 'hidden' }}
+          data-id={id}
+          tabIndex={-1}
+        >
+          {children && cloneElement(children, { hidden: true })}
+        </article>
+      );
+    }
+
+    return (
+      <article ref={this.handleRef} aria-posinset={index + 1} aria-setsize={listLength} data-id={id} tabIndex={-1}>
+        {children && cloneElement(children, { hidden: false })}
+      </article>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/components/link.jsx b/app/javascript/flavours/blobfox/components/link.jsx
new file mode 100644
index 00000000000000..5059922f6ab1e6
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/link.jsx
@@ -0,0 +1,97 @@
+//  Inspired by <CommonLink> from Mastodon GO!
+//  ~ 😘 kibi!
+
+//  Package imports.
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import classNames from 'classnames';
+
+//  Utils.
+import { assignHandlers } from 'flavours/blobfox/utils/react_helpers';
+//  Handlers.
+const handlers = {
+
+  //  We don't handle clicks that are made with modifiers, since these
+  //  often have special browser meanings (eg, "open in new tab").
+  click (e) {
+    const { onClick } = this.props;
+    if (!onClick || e.button || e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) {
+      return;
+    }
+    onClick(e);
+    e.preventDefault();  //  Prevents following of the link
+  },
+};
+
+//  The component.
+export default class Link extends PureComponent {
+
+  //  Constructor.
+  constructor (props) {
+    super(props);
+    assignHandlers(this, handlers);
+  }
+
+  //  Rendering.
+  render () {
+    const { click } = this.handlers;
+    const {
+      children,
+      className,
+      href,
+      onClick,
+      role,
+      title,
+      ...rest
+    } = this.props;
+    const computedClass = classNames('link', className, `role-${role}`);
+
+    //  We assume that our `onClick` is a routing function and give it
+    //  the qualities of a link even if no `href` is provided. However,
+    //  if we have neither an `onClick` or an `href`, our link is
+    //  purely presentational.
+    const conditionalProps = {};
+    if (href) {
+      conditionalProps.href = href;
+      conditionalProps.onClick = click;
+    } else if (onClick) {
+      conditionalProps.onClick = click;
+      conditionalProps.role = 'link';
+      conditionalProps.tabIndex = 0;
+    } else {
+      conditionalProps.role = 'presentation';
+    }
+
+    //  If we were provided a `role` it overwrites any that we may have
+    //  set above.  This can be used for "links" which are actually
+    //  buttons.
+    if (role) {
+      conditionalProps.role = role;
+    }
+
+    //  Rendering.  We set `rel='noopener'` for user privacy, and our
+    //  `target` as `'_blank'`.
+    return (
+      <a
+        className={computedClass}
+        {...conditionalProps}
+        rel='noopener'
+        target='_blank'
+        title={title}
+        {...rest}
+      >{children}</a>
+    );
+  }
+
+}
+
+//  Props.
+Link.propTypes = {
+  children: PropTypes.node,
+  className: PropTypes.string,
+  href: PropTypes.string,  //  The link destination
+  onClick: PropTypes.func,  //  A function to call instead of opening the link
+  role: PropTypes.string,  //  An ARIA role for the link
+  title: PropTypes.string,  //  A title for the link
+};
diff --git a/app/javascript/flavours/blobfox/components/load_gap.tsx b/app/javascript/flavours/blobfox/components/load_gap.tsx
new file mode 100644
index 00000000000000..565930bb6ba668
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/load_gap.tsx
@@ -0,0 +1,34 @@
+import { useCallback } from 'react';
+
+import { useIntl, defineMessages } from 'react-intl';
+
+import { Icon } from 'flavours/blobfox/components/icon';
+
+const messages = defineMessages({
+  load_more: { id: 'status.load_more', defaultMessage: 'Load more' },
+});
+
+interface Props {
+  disabled: boolean;
+  maxId: string;
+  onClick: (maxId: string) => void;
+}
+
+export const LoadGap: React.FC<Props> = ({ disabled, maxId, onClick }) => {
+  const intl = useIntl();
+
+  const handleClick = useCallback(() => {
+    onClick(maxId);
+  }, [maxId, onClick]);
+
+  return (
+    <button
+      className='load-more load-gap'
+      disabled={disabled}
+      onClick={handleClick}
+      aria-label={intl.formatMessage(messages.load_more)}
+    >
+      <Icon id='ellipsis-h' />
+    </button>
+  );
+};
diff --git a/app/javascript/flavours/blobfox/components/load_more.tsx b/app/javascript/flavours/blobfox/components/load_more.tsx
new file mode 100644
index 00000000000000..8b5746ad30b34a
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/load_more.tsx
@@ -0,0 +1,24 @@
+import { FormattedMessage } from 'react-intl';
+
+interface Props {
+  onClick: (event: React.MouseEvent) => void;
+  disabled?: boolean;
+  visible?: boolean;
+}
+export const LoadMore: React.FC<Props> = ({
+  onClick,
+  disabled,
+  visible = true,
+}) => {
+  return (
+    <button
+      type='button'
+      className='load-more'
+      disabled={disabled || !visible}
+      style={{ visibility: visible ? 'visible' : 'hidden' }}
+      onClick={onClick}
+    >
+      <FormattedMessage id='status.load_more' defaultMessage='Load more' />
+    </button>
+  );
+};
diff --git a/app/javascript/flavours/blobfox/components/load_pending.tsx b/app/javascript/flavours/blobfox/components/load_pending.tsx
new file mode 100644
index 00000000000000..f7589622edb2d5
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/load_pending.tsx
@@ -0,0 +1,18 @@
+import { FormattedMessage } from 'react-intl';
+
+interface Props {
+  onClick: (event: React.MouseEvent) => void;
+  count: number;
+}
+
+export const LoadPending: React.FC<Props> = ({ onClick, count }) => {
+  return (
+    <button className='load-more load-gap' onClick={onClick}>
+      <FormattedMessage
+        id='load_pending'
+        defaultMessage='{count, plural, one {# new item} other {# new items}}'
+        values={{ count }}
+      />
+    </button>
+  );
+};
diff --git a/app/javascript/flavours/blobfox/components/loading_indicator.tsx b/app/javascript/flavours/blobfox/components/loading_indicator.tsx
new file mode 100644
index 00000000000000..6bc24a0d617ecf
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/loading_indicator.tsx
@@ -0,0 +1,7 @@
+import { CircularProgress } from './circular_progress';
+
+export const LoadingIndicator: React.FC = () => (
+  <div className='loading-indicator'>
+    <CircularProgress size={50} strokeWidth={6} />
+  </div>
+);
diff --git a/app/javascript/flavours/blobfox/components/logo.jsx b/app/javascript/flavours/blobfox/components/logo.jsx
new file mode 100644
index 00000000000000..16ca9f80fd0b04
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/logo.jsx
@@ -0,0 +1,14 @@
+import logo from 'mastodon/../images/logo.svg';
+
+export const WordmarkLogo = () => (
+  <svg viewBox='0 0 261 66' className='logo logo--wordmark' role='img'>
+    <title>Mastodon</title>
+    <use xlinkHref='#logo-symbol-wordmark' />
+  </svg>
+);
+
+export const SymbolLogo = () => (
+  <img src={logo} alt='Mastodon' className='logo logo--icon' />
+);
+
+export default WordmarkLogo;
diff --git a/app/javascript/flavours/blobfox/components/media_attachments.jsx b/app/javascript/flavours/blobfox/components/media_attachments.jsx
new file mode 100644
index 00000000000000..a10a1b26115f26
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/media_attachments.jsx
@@ -0,0 +1,128 @@
+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/blobfox/features/ui/components/bundle';
+import { MediaGallery, Video, Audio } from 'flavours/blobfox/features/ui/util/async-components';
+
+export default class MediaAttachments extends ImmutablePureComponent {
+
+  static propTypes = {
+    status: ImmutablePropTypes.map.isRequired,
+    lang: PropTypes.string,
+    height: PropTypes.number,
+    width: PropTypes.number,
+    visible: PropTypes.bool,
+  };
+
+  static defaultProps = {
+    height: 110,
+    width: 239,
+  };
+
+  updateOnProps = [
+    'status',
+  ];
+
+  renderLoadingMediaGallery = () => {
+    const { height, width } = this.props;
+
+    return (
+      <div className='media-gallery' style={{ height, width }} />
+    );
+  };
+
+  renderLoadingVideoPlayer = () => {
+    const { height, width } = this.props;
+
+    return (
+      <div className='video-player' style={{ height, width }} />
+    );
+  };
+
+  renderLoadingAudioPlayer = () => {
+    const { height, width } = this.props;
+
+    return (
+      <div className='audio-player' style={{ height, width }} />
+    );
+  };
+
+  render () {
+    const { status, width, height, visible } = 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;
+    }
+
+    if (mediaAttachments.getIn([0, 'type']) === 'audio') {
+      const audio = mediaAttachments.get(0);
+      const description = audio.getIn(['translation', 'description']) || audio.get('description');
+
+      return (
+        <Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
+          {Component => (
+            <Component
+              src={audio.get('url')}
+              alt={description}
+              lang={language}
+              width={width}
+              height={height}
+              poster={audio.get('preview_url') || status.getIn(['account', 'avatar_static'])}
+              backgroundColor={audio.getIn(['meta', 'colors', 'background'])}
+              foregroundColor={audio.getIn(['meta', 'colors', 'foreground'])}
+              accentColor={audio.getIn(['meta', 'colors', 'accent'])}
+              duration={audio.getIn(['meta', 'original', 'duration'], 0)}
+            />
+          )}
+        </Bundle>
+      );
+    } else if (mediaAttachments.getIn([0, 'type']) === 'video') {
+      const video = mediaAttachments.get(0);
+      const description = video.getIn(['translation', 'description']) || video.get('description');
+
+      return (
+        <Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
+          {Component => (
+            <Component
+              preview={video.get('preview_url')}
+              frameRate={video.getIn(['meta', 'original', 'frame_rate'])}
+              blurhash={video.get('blurhash')}
+              src={video.get('url')}
+              alt={description}
+              lang={language}
+              width={width}
+              height={height}
+              inline
+              sensitive={status.get('sensitive')}
+              visible={visible}
+              onOpenVideo={noop}
+            />
+          )}
+        </Bundle>
+      );
+    } else {
+      return (
+        <Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} >
+          {Component => (
+            <Component
+              media={mediaAttachments}
+              lang={language}
+              sensitive={status.get('sensitive')}
+              defaultWidth={width}
+              visible={visible}
+              height={height}
+              onOpenMedia={noop}
+            />
+          )}
+        </Bundle>
+      );
+    }
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/components/media_gallery.jsx b/app/javascript/flavours/blobfox/components/media_gallery.jsx
new file mode 100644
index 00000000000000..a2778c39873194
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/media_gallery.jsx
@@ -0,0 +1,395 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import classNames from 'classnames';
+
+import { is } from 'immutable';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
+import { debounce } from 'lodash';
+
+import { Blurhash } from 'flavours/blobfox/components/blurhash';
+
+import { autoPlayGif, displayMedia, useBlurhash } from '../initial_state';
+
+import { IconButton } from './icon_button';
+
+const messages = defineMessages({
+  hidden: {
+    defaultMessage: 'Media hidden',
+    id: 'status.media_hidden',
+  },
+  sensitive: {
+    defaultMessage: 'Sensitive',
+    id: 'media_gallery.sensitive',
+  },
+  toggle: {
+    defaultMessage: 'Click to view',
+    id: 'status.sensitive_toggle',
+  },
+  toggle_visible: {
+    defaultMessage: '{number, plural, one {Hide image} other {Hide images}}',
+    id: 'media_gallery.toggle_visible',
+  },
+  warning: {
+    defaultMessage: 'Sensitive content',
+    id: 'status.sensitive_warning',
+  },
+});
+
+class Item extends PureComponent {
+
+  static propTypes = {
+    attachment: ImmutablePropTypes.map.isRequired,
+    lang: PropTypes.string,
+    standalone: PropTypes.bool,
+    index: PropTypes.number.isRequired,
+    size: PropTypes.number.isRequired,
+    letterbox: PropTypes.bool,
+    onClick: PropTypes.func.isRequired,
+    displayWidth: PropTypes.number,
+    visible: PropTypes.bool.isRequired,
+    autoplay: PropTypes.bool,
+  };
+
+  static defaultProps = {
+    standalone: false,
+    index: 0,
+    size: 1,
+  };
+
+  state = {
+    loaded: false,
+  };
+
+  handleMouseEnter = (e) => {
+    if (this.hoverToPlay()) {
+      e.target.play();
+    }
+  };
+
+  handleMouseLeave = (e) => {
+    if (this.hoverToPlay()) {
+      e.target.pause();
+      e.target.currentTime = 0;
+    }
+  };
+
+  getAutoPlay() {
+    return this.props.autoplay || autoPlayGif;
+  }
+
+  hoverToPlay () {
+    const { attachment } = this.props;
+    return !this.getAutoPlay() && attachment.get('type') === 'gifv';
+  }
+
+  handleClick = (e) => {
+    const { index, onClick } = this.props;
+
+    if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
+      if (this.hoverToPlay()) {
+        e.target.pause();
+        e.target.currentTime = 0;
+      }
+      e.preventDefault();
+      onClick(index);
+    }
+
+    e.stopPropagation();
+  };
+
+  handleImageLoad = () => {
+    this.setState({ loaded: true });
+  };
+
+  render () {
+    const { attachment, lang, index, size, standalone, letterbox, displayWidth, visible } = this.props;
+
+    let badges = [], thumbnail;
+
+    let width  = 50;
+    let height = 100;
+
+    if (size === 1) {
+      width = 100;
+    }
+
+    if (size === 4 || (size === 3 && index > 0)) {
+      height = 50;
+    }
+
+    if (attachment.get('description')?.length > 0) {
+      badges.push(<span key='alt' className='media-gallery__gifv__label'>ALT</span>);
+    }
+
+    const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
+
+    if (attachment.get('type') === 'unknown') {
+      return (
+        <div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}>
+          <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={description} lang={lang} target='_blank' rel='noopener noreferrer'>
+            <Blurhash
+              hash={attachment.get('blurhash')}
+              className='media-gallery__preview'
+              dummy={!useBlurhash}
+            />
+          </a>
+        </div>
+      );
+    } else if (attachment.get('type') === 'image') {
+      const previewUrl   = attachment.get('preview_url');
+      const previewWidth = attachment.getIn(['meta', 'small', 'width']);
+
+      const originalUrl   = attachment.get('url');
+      const originalWidth = attachment.getIn(['meta', 'original', 'width']);
+
+      const hasSize = typeof originalWidth === 'number' && typeof previewWidth === 'number';
+
+      const srcSet = hasSize ? `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w` : null;
+      const sizes  = hasSize && (displayWidth > 0) ? `${displayWidth * (width / 100)}px` : null;
+
+      const focusX = attachment.getIn(['meta', 'focus', 'x']) || 0;
+      const focusY = attachment.getIn(['meta', 'focus', 'y']) || 0;
+      const x      = ((focusX /  2) + .5) * 100;
+      const y      = ((focusY / -2) + .5) * 100;
+
+      thumbnail = (
+        <a
+          className='media-gallery__item-thumbnail'
+          href={attachment.get('remote_url') || originalUrl}
+          onClick={this.handleClick}
+          target='_blank'
+          rel='noopener noreferrer'
+        >
+          <img
+            className={letterbox ? 'letterbox' : null}
+            src={previewUrl}
+            srcSet={srcSet}
+            sizes={sizes}
+            alt={description}
+            title={description}
+            lang={lang}
+            style={{ objectPosition: letterbox ? null : `${x}% ${y}%` }}
+            onLoad={this.handleImageLoad}
+          />
+        </a>
+      );
+    } else if (attachment.get('type') === 'gifv') {
+      const autoPlay = this.getAutoPlay();
+
+      badges.push(<span key='gif' className='media-gallery__gifv__label'>GIF</span>);
+
+      thumbnail = (
+        <div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
+          <video
+            className={`media-gallery__item-gifv-thumbnail${letterbox ? ' letterbox' : ''}`}
+            aria-label={description}
+            title={description}
+            lang={lang}
+            role='application'
+            src={attachment.get('url')}
+            onClick={this.handleClick}
+            onMouseEnter={this.handleMouseEnter}
+            onMouseLeave={this.handleMouseLeave}
+            autoPlay={autoPlay}
+            playsInline
+            loop
+            muted
+          />
+        </div>
+      );
+    }
+
+    return (
+      <div className={classNames('media-gallery__item', { standalone, letterbox, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}>
+        <Blurhash
+          hash={attachment.get('blurhash')}
+          dummy={!useBlurhash}
+          className={classNames('media-gallery__preview', {
+            'media-gallery__preview--hidden': visible && this.state.loaded,
+          })}
+        />
+
+        {visible && thumbnail}
+
+        {badges && (
+          <div className='media-gallery__item__badges'>
+            {badges}
+          </div>
+        )}
+      </div>
+    );
+  }
+
+}
+
+class MediaGallery extends PureComponent {
+
+  static propTypes = {
+    sensitive: PropTypes.bool,
+    standalone: PropTypes.bool,
+    letterbox: PropTypes.bool,
+    fullwidth: PropTypes.bool,
+    hidden: PropTypes.bool,
+    media: ImmutablePropTypes.list.isRequired,
+    lang: PropTypes.string,
+    size: PropTypes.object,
+    onOpenMedia: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+    defaultWidth: PropTypes.number,
+    cacheWidth: PropTypes.func,
+    visible: PropTypes.bool,
+    autoplay: PropTypes.bool,
+    onToggleVisibility: PropTypes.func,
+  };
+
+  static defaultProps = {
+    standalone: false,
+  };
+
+  state = {
+    visible: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'),
+    width: this.props.defaultWidth,
+  };
+
+  componentDidMount () {
+    window.addEventListener('resize', this.handleResize, { passive: true });
+  }
+
+  componentWillUnmount () {
+    window.removeEventListener('resize', this.handleResize);
+  }
+
+  UNSAFE_componentWillReceiveProps (nextProps) {
+    if (!is(nextProps.media, this.props.media) && nextProps.visible === undefined) {
+      this.setState({ visible: displayMedia !== 'hide_all' && !nextProps.sensitive || displayMedia === 'show_all' });
+    } else if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) {
+      this.setState({ visible: nextProps.visible });
+    }
+  }
+
+  componentDidUpdate () {
+    if (this.node) {
+      this.handleResize();
+    }
+  }
+
+  handleResize = debounce(() => {
+    if (this.node) {
+      this._setDimensions();
+    }
+  }, 250, {
+    leading: true,
+    trailing: true,
+  });
+
+  handleOpen = () => {
+    if (this.props.onToggleVisibility) {
+      this.props.onToggleVisibility();
+    } else {
+      this.setState({ visible: !this.state.visible });
+    }
+  };
+
+  handleClick = (index) => {
+    this.props.onOpenMedia(this.props.media, index, this.props.lang);
+  };
+
+  handleRef = (node) => {
+    this.node = node;
+
+    if (this.node) {
+      this._setDimensions();
+    }
+  };
+
+  _setDimensions () {
+    const width = this.node.offsetWidth;
+
+    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);
+      }
+
+      this.setState({
+        width: width,
+      });
+    }
+  }
+
+  isStandaloneEligible() {
+    const { media, standalone } = this.props;
+    return standalone && media.size === 1 && media.getIn([0, 'meta', 'small', 'aspect']);
+  }
+
+  render () {
+    const { media, lang, intl, sensitive, letterbox, fullwidth, defaultWidth, autoplay } = this.props;
+    const { visible } = this.state;
+    const size     = media.take(4).size;
+    const uncached = media.every(attachment => attachment.get('type') === 'unknown');
+
+    const width = this.state.width || defaultWidth;
+
+    let children, spoilerButton;
+
+    const style = {};
+
+    const computedClass = classNames('media-gallery', { 'full-width': fullwidth });
+
+    if (this.isStandaloneEligible()) { // TODO: cropImages setting
+      style.aspectRatio = `${this.props.media.getIn([0, 'meta', 'small', 'aspect'])}`;
+    } else {
+      style.aspectRatio = '16 / 9';
+    }
+
+    if (this.isStandaloneEligible()) {
+      children = <Item standalone autoplay={autoplay} onClick={this.handleClick} attachment={media.get(0)} lang={lang} displayWidth={width} visible={visible} />;
+    } else {
+      children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} autoplay={autoplay} onClick={this.handleClick} attachment={attachment} index={i} lang={lang} size={size} letterbox={letterbox} displayWidth={width} visible={visible || uncached} />);
+    }
+
+    if (uncached) {
+      spoilerButton = (
+        <button type='button' disabled className='spoiler-button__overlay'>
+          <span className='spoiler-button__overlay__label'>
+            <FormattedMessage id='status.uncached_media_warning' defaultMessage='Preview not available' />
+            <span className='spoiler-button__overlay__action'><FormattedMessage id='status.media.open' defaultMessage='Click to open' /></span>
+          </span>
+        </button>
+      );
+    } else if (visible) {
+      spoilerButton = <IconButton title={intl.formatMessage(messages.toggle_visible, { number: size })} icon='eye-slash' overlay onClick={this.handleOpen} ariaHidden />;
+    } else {
+      spoilerButton = (
+        <button type='button' onClick={this.handleOpen} className='spoiler-button__overlay'>
+          <span className='spoiler-button__overlay__label'>
+            {sensitive ? <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /> : <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />}
+            <span className='spoiler-button__overlay__action'><FormattedMessage id='status.media.show' defaultMessage='Click to show' /></span>
+          </span>
+        </button>
+      );
+    }
+
+    return (
+      <div className={computedClass} style={style} ref={this.handleRef}>
+        <div className={classNames('spoiler-button', { 'spoiler-button--minified': visible && !uncached, 'spoiler-button--click-thru': uncached })}>
+          {spoilerButton}
+          {visible && sensitive && (
+            <span className='sensitive-marker'>
+              <FormattedMessage {...messages.sensitive} />
+            </span>
+          )}
+        </div>
+
+        {children}
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(MediaGallery);
diff --git a/app/javascript/flavours/blobfox/components/modal_root.jsx b/app/javascript/flavours/blobfox/components/modal_root.jsx
new file mode 100644
index 00000000000000..d5723c58cde47a
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/modal_root.jsx
@@ -0,0 +1,165 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import 'wicg-inert';
+
+import { multiply } from 'color-blend';
+import { createBrowserHistory } from 'history';
+
+import { WithOptionalRouterPropTypes, withOptionalRouter } from 'flavours/blobfox/utils/react_router';
+
+class ModalRoot extends PureComponent {
+
+  static propTypes = {
+    children: PropTypes.node,
+    onClose: PropTypes.func.isRequired,
+    backgroundColor: PropTypes.shape({
+      r: PropTypes.number,
+      g: PropTypes.number,
+      b: PropTypes.number,
+    }),
+    noEsc: PropTypes.bool,
+    ignoreFocus: PropTypes.bool,
+    ...WithOptionalRouterPropTypes,
+  };
+
+  activeElement = this.props.children ? document.activeElement : null;
+
+  handleKeyUp = (e) => {
+    if ((e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27)
+         && !!this.props.children && !this.props.noEsc) {
+      this.props.onClose();
+    }
+  };
+
+  handleKeyDown = (e) => {
+    if (e.key === 'Tab') {
+      const focusable = Array.from(this.node.querySelectorAll('button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])')).filter((x) => window.getComputedStyle(x).display !== 'none');
+      const index = focusable.indexOf(e.target);
+
+      let element;
+
+      if (e.shiftKey) {
+        element = focusable[index - 1] || focusable[focusable.length - 1];
+      } else {
+        element = focusable[index + 1] || focusable[0];
+      }
+
+      if (element) {
+        element.focus();
+        e.stopPropagation();
+        e.preventDefault();
+      }
+    }
+  };
+
+  componentDidMount () {
+    window.addEventListener('keyup', this.handleKeyUp, false);
+    window.addEventListener('keydown', this.handleKeyDown, false);
+    this.history = this.props.history || createBrowserHistory();
+
+    if (this.props.children) {
+      this._handleModalOpen();
+    }
+  }
+
+  UNSAFE_componentWillReceiveProps (nextProps) {
+    if (!!nextProps.children && !this.props.children) {
+      this.activeElement = document.activeElement;
+
+      this.getSiblings().forEach(sibling => sibling.setAttribute('inert', true));
+    }
+  }
+
+  componentDidUpdate (prevProps) {
+    if (!this.props.children && !!prevProps.children) {
+      this.getSiblings().forEach(sibling => sibling.removeAttribute('inert'));
+
+      // Because of the wicg-inert polyfill, the activeElement may not be
+      // immediately selectable, we have to wait for observers to run, as
+      // described in https://github.com/WICG/inert#performance-and-gotchas
+      Promise.resolve().then(() => {
+        if (!this.props.ignoreFocus) {
+          this.activeElement.focus({ preventScroll: true });
+        }
+        this.activeElement = null;
+      }).catch(console.error);
+
+      this._handleModalClose();
+    }
+    if (this.props.children && !prevProps.children) {
+      this._handleModalOpen();
+    }
+    if (this.props.children) {
+      this._ensureHistoryBuffer();
+    }
+  }
+
+  componentWillUnmount () {
+    window.removeEventListener('keyup', this.handleKeyUp);
+    window.removeEventListener('keydown', this.handleKeyDown);
+  }
+
+  _handleModalOpen () {
+    this._modalHistoryKey = Date.now();
+    this.unlistenHistory = this.history.listen((_, action) => {
+      if (action === 'POP') {
+        this.props.onClose();
+      }
+    });
+  }
+
+  _handleModalClose () {
+    if (this.unlistenHistory) {
+      this.unlistenHistory();
+    }
+    const { state } = this.history.location;
+    if (state && state.mastodonModalKey === this._modalHistoryKey) {
+      this.history.goBack();
+    }
+  }
+
+  _ensureHistoryBuffer () {
+    const { pathname, state } = this.history.location;
+    if (!state || state.mastodonModalKey !== this._modalHistoryKey) {
+      this.history.push(pathname, { ...state, mastodonModalKey: this._modalHistoryKey });
+    }
+  }
+
+  getSiblings = () => {
+    return Array(...this.node.parentElement.childNodes).filter(node => node !== this.node);
+  };
+
+  setRef = ref => {
+    this.node = ref;
+  };
+
+  render () {
+    const { children, onClose } = this.props;
+    const visible = !!children;
+
+    if (!visible) {
+      return (
+        <div className='modal-root' ref={this.setRef} style={{ opacity: 0 }} />
+      );
+    }
+
+    let backgroundColor = null;
+
+    if (this.props.backgroundColor) {
+      backgroundColor = multiply({ ...this.props.backgroundColor, a: 1 }, { r: 0, g: 0, b: 0, a: 0.7 });
+    }
+
+    return (
+      <div className='modal-root' ref={this.setRef}>
+        <div style={{ pointerEvents: visible ? 'auto' : 'none' }}>
+          <div role='presentation' className='modal-root__overlay' onClick={onClose} style={{ backgroundColor: backgroundColor ? `rgba(${backgroundColor.r}, ${backgroundColor.g}, ${backgroundColor.b}, 0.7)` : null }} />
+          <div role='dialog' className='modal-root__container'>{children}</div>
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default withOptionalRouter(ModalRoot);
diff --git a/app/javascript/flavours/blobfox/components/navigation_portal.tsx b/app/javascript/flavours/blobfox/components/navigation_portal.tsx
new file mode 100644
index 00000000000000..b1856f4509d6a7
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/navigation_portal.tsx
@@ -0,0 +1,25 @@
+import { Switch, Route } from 'react-router-dom';
+
+import AccountNavigation from 'flavours/blobfox/features/account/navigation';
+import Trends from 'flavours/blobfox/features/getting_started/containers/trends_container';
+import { showTrends } from 'flavours/blobfox/initial_state';
+
+const DefaultNavigation: React.FC = () =>
+  showTrends ? (
+    <>
+      <div className='flex-spacer' />
+      <Trends />
+    </>
+  ) : null;
+
+export const NavigationPortal: React.FC = () => (
+  <Switch>
+    <Route path='/@:acct' exact component={AccountNavigation} />
+    <Route path='/@:acct/tagged/:tagged?' exact component={AccountNavigation} />
+    <Route path='/@:acct/with_replies' exact component={AccountNavigation} />
+    <Route path='/@:acct/followers' exact component={AccountNavigation} />
+    <Route path='/@:acct/following' exact component={AccountNavigation} />
+    <Route path='/@:acct/media' exact component={AccountNavigation} />
+    <Route component={DefaultNavigation} />
+  </Switch>
+);
diff --git a/app/javascript/flavours/blobfox/components/not_signed_in_indicator.tsx b/app/javascript/flavours/blobfox/components/not_signed_in_indicator.tsx
new file mode 100644
index 00000000000000..015f74dcaeabed
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/not_signed_in_indicator.tsx
@@ -0,0 +1,12 @@
+import { FormattedMessage } from 'react-intl';
+
+export const NotSignedInIndicator: React.FC = () => (
+  <div className='scrollable scrollable--flex'>
+    <div className='empty-column-indicator'>
+      <FormattedMessage
+        id='not_signed_in_indicator.not_signed_in'
+        defaultMessage='You need to login to access this resource.'
+      />
+    </div>
+  </div>
+);
diff --git a/app/javascript/flavours/blobfox/components/notification_purge_buttons.jsx b/app/javascript/flavours/blobfox/components/notification_purge_buttons.jsx
new file mode 100644
index 00000000000000..2500d082a5c503
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/notification_purge_buttons.jsx
@@ -0,0 +1,64 @@
+/**
+ * Buttons widget for controlling the notification clearing mode.
+ * In idle state, the cleaning mode button is shown. When the mode is active,
+ * a Confirm and Abort buttons are shown in its place.
+ */
+
+
+//  Package imports  //
+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/blobfox/components/icon';
+
+const messages = defineMessages({
+  btnAll : { id: 'notification_purge.btn_all', defaultMessage: 'Select\nall' },
+  btnNone : { id: 'notification_purge.btn_none', defaultMessage: 'Select\nnone' },
+  btnInvert : { id: 'notification_purge.btn_invert', defaultMessage: 'Invert\nselection' },
+  btnApply : { id: 'notification_purge.btn_apply', defaultMessage: 'Clear\nselected' },
+});
+
+class NotificationPurgeButtons extends ImmutablePureComponent {
+
+  static propTypes = {
+    onDeleteMarked : PropTypes.func.isRequired,
+    onMarkAll : PropTypes.func.isRequired,
+    onMarkNone : PropTypes.func.isRequired,
+    onInvert : PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+    markNewForDelete: PropTypes.bool,
+  };
+
+  render () {
+    const { intl, markNewForDelete } = this.props;
+
+    //className='active'
+    return (
+      <div className='column-header__notif-cleaning-buttons'>
+        <button onClick={this.props.onMarkAll} className={classNames('column-header__button', { active: markNewForDelete })}>
+          <b>∀</b><br />{intl.formatMessage(messages.btnAll)}
+        </button>
+
+        <button onClick={this.props.onMarkNone} className={classNames('column-header__button', { active: !markNewForDelete })}>
+          <b>∅</b><br />{intl.formatMessage(messages.btnNone)}
+        </button>
+
+        <button onClick={this.props.onInvert} className='column-header__button'>
+          <b>¬</b><br />{intl.formatMessage(messages.btnInvert)}
+        </button>
+
+        <button onClick={this.props.onDeleteMarked} className='column-header__button'>
+          <Icon id='trash' /><br />{intl.formatMessage(messages.btnApply)}
+        </button>
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(NotificationPurgeButtons);
diff --git a/app/javascript/flavours/blobfox/components/permalink.jsx b/app/javascript/flavours/blobfox/components/permalink.jsx
new file mode 100644
index 00000000000000..d3e77efd3890ea
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/permalink.jsx
@@ -0,0 +1,50 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { withOptionalRouter, WithOptionalRouterPropTypes } from 'flavours/blobfox/utils/react_router';
+
+class Permalink extends PureComponent {
+
+  static propTypes = {
+    className: PropTypes.string,
+    href: PropTypes.string.isRequired,
+    to: PropTypes.string.isRequired,
+    children: PropTypes.node,
+    onInterceptClick: PropTypes.func,
+    ...WithOptionalRouterPropTypes,
+  };
+
+  handleClick = (e) => {
+    if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
+      if (this.props.onInterceptClick && this.props.onInterceptClick()) {
+        e.preventDefault();
+        return;
+      }
+
+      if (this.props.history) {
+        e.preventDefault();
+        this.props.history.push(this.props.to);
+      }
+    }
+  };
+
+  render () {
+    const {
+      children,
+      className,
+      href,
+      to,
+      onInterceptClick,
+      ...other
+    } = this.props;
+
+    return (
+      <a target='_blank' href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}>
+        {children}
+      </a>
+    );
+  }
+
+}
+
+export default withOptionalRouter(Permalink);
diff --git a/app/javascript/flavours/blobfox/components/picture_in_picture_placeholder.jsx b/app/javascript/flavours/blobfox/components/picture_in_picture_placeholder.jsx
new file mode 100644
index 00000000000000..159719b4a12e68
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/picture_in_picture_placeholder.jsx
@@ -0,0 +1,33 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import { connect } from 'react-redux';
+
+import { removePictureInPicture } from 'flavours/blobfox/actions/picture_in_picture';
+import { Icon }  from 'flavours/blobfox/components/icon';
+
+class PictureInPicturePlaceholder extends PureComponent {
+
+  static propTypes = {
+    dispatch: PropTypes.func.isRequired,
+  };
+
+  handleClick = () => {
+    const { dispatch } = this.props;
+    dispatch(removePictureInPicture());
+  };
+
+  render () {
+    return (
+      <div className='picture-in-picture-placeholder' role='button' tabIndex={0} onClick={this.handleClick}>
+        <Icon id='window-restore' />
+        <FormattedMessage id='picture_in_picture.restore' defaultMessage='Put it back' />
+      </div>
+    );
+  }
+
+}
+
+export default connect()(PictureInPicturePlaceholder);
diff --git a/app/javascript/flavours/blobfox/components/poll.jsx b/app/javascript/flavours/blobfox/components/poll.jsx
new file mode 100644
index 00000000000000..db3e49073b8415
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/poll.jsx
@@ -0,0 +1,249 @@
+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/blobfox/components/icon';
+import emojify from 'flavours/blobfox/features/emoji/emoji';
+import Motion from 'flavours/blobfox/features/ui/util/optional_motion';
+
+import { RelativeTimestamp } from './relative_timestamp';
+
+const messages = defineMessages({
+  closed: {
+    id: 'poll.closed',
+    defaultMessage: 'Closed',
+  },
+  voted: {
+    id: 'poll.voted',
+    defaultMessage: 'You voted for this answer',
+  },
+  votes: {
+    id: 'poll.votes',
+    defaultMessage: '{votes, plural, one {# vote} other {# votes}}',
+  },
+});
+
+const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
+  obj[`:${emoji.get('shortcode')}:`] = emoji.toJS();
+  return obj;
+}, {});
+
+class Poll extends ImmutablePureComponent {
+
+  static contextTypes = {
+    identity: PropTypes.object,
+  };
+
+  static propTypes = {
+    poll: ImmutablePropTypes.map,
+    lang: PropTypes.string,
+    intl: PropTypes.object.isRequired,
+    disabled: PropTypes.bool,
+    refresh: PropTypes.func,
+    onVote: PropTypes.func,
+  };
+
+  state = {
+    selected: {},
+    expired: null,
+  };
+
+  static getDerivedStateFromProps (props, state) {
+    const { poll } = props;
+    const expires_at = poll.get('expires_at');
+    const expired = poll.get('expired') || expires_at !== null && (new Date(expires_at)).getTime() < Date.now();
+    return (expired === state.expired) ? null : { expired };
+  }
+
+  componentDidMount () {
+    this._setupTimer();
+  }
+
+  componentDidUpdate () {
+    this._setupTimer();
+  }
+
+  componentWillUnmount () {
+    clearTimeout(this._timer);
+  }
+
+  _setupTimer () {
+    const { poll } = this.props;
+    clearTimeout(this._timer);
+    if (!this.state.expired) {
+      const delay = (new Date(poll.get('expires_at'))).getTime() - Date.now();
+      this._timer = setTimeout(() => {
+        this.setState({ expired: true });
+      }, delay);
+    }
+  }
+
+  _toggleOption = value => {
+    if (this.props.poll.get('multiple')) {
+      const tmp = { ...this.state.selected };
+      if (tmp[value]) {
+        delete tmp[value];
+      } else {
+        tmp[value] = true;
+      }
+      this.setState({ selected: tmp });
+    } else {
+      const tmp = {};
+      tmp[value] = true;
+      this.setState({ selected: tmp });
+    }
+  };
+
+  handleOptionChange = ({ target: { value } }) => {
+    this._toggleOption(value);
+  };
+
+  handleOptionKeyPress = (e) => {
+    if (e.key === 'Enter' || e.key === ' ') {
+      this._toggleOption(e.target.getAttribute('data-index'));
+      e.stopPropagation();
+      e.preventDefault();
+    }
+  };
+
+  handleVote = () => {
+    if (this.props.disabled) {
+      return;
+    }
+
+    this.props.onVote(Object.keys(this.state.selected));
+  };
+
+  handleRefresh = () => {
+    if (this.props.disabled) {
+      return;
+    }
+
+    this.props.refresh();
+  };
+
+  handleReveal = () => {
+    this.setState({ revealed: true });
+  };
+
+  renderOption (option, optionIndex, showResults) {
+    const { poll, lang, disabled, intl } = this.props;
+    const pollVotesCount  = poll.get('voters_count') || poll.get('votes_count');
+    const percent         = pollVotesCount === 0 ? 0 : (option.get('votes_count') / pollVotesCount) * 100;
+    const leading         = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') >= other.get('votes_count'));
+    const active          = !!this.state.selected[`${optionIndex}`];
+    const voted           = option.get('voted') || (poll.get('own_votes') && poll.get('own_votes').includes(optionIndex));
+
+    const title = option.getIn(['translation', 'title']) || option.get('title');
+    let titleHtml = option.getIn(['translation', 'titleHtml']) || option.get('titleHtml');
+
+    if (!titleHtml) {
+      const emojiMap = makeEmojiMap(poll);
+      titleHtml = emojify(escapeTextContentForBrowser(title), emojiMap);
+    }
+
+    return (
+      <li key={option.get('title')}>
+        <label className={classNames('poll__option', { selectable: !showResults })}>
+          <input
+            name='vote-options'
+            type={poll.get('multiple') ? 'checkbox' : 'radio'}
+            value={optionIndex}
+            checked={active}
+            onChange={this.handleOptionChange}
+            disabled={disabled}
+          />
+
+          {!showResults && (
+            <span
+              className={classNames('poll__input', { checkbox: poll.get('multiple'), active })}
+              tabIndex={0}
+              role={poll.get('multiple') ? 'checkbox' : 'radio'}
+              onKeyPress={this.handleOptionKeyPress}
+              aria-checked={active}
+              aria-label={title}
+              lang={lang}
+              data-index={optionIndex}
+            />
+          )}
+          {showResults && (
+            <span
+              className='poll__number'
+              title={intl.formatMessage(messages.votes, {
+                votes: option.get('votes_count'),
+              })}
+            >
+              {Math.round(percent)}%
+            </span>
+          )}
+
+          <span
+            className='poll__option__text translate'
+            lang={lang}
+            dangerouslySetInnerHTML={{ __html: titleHtml }}
+          />
+
+          {!!voted && <span className='poll__voted'>
+            <Icon id='check' className='poll__voted__mark' title={intl.formatMessage(messages.voted)} />
+          </span>}
+        </label>
+
+        {showResults && (
+          <Motion defaultStyle={{ width: 0 }} style={{ width: spring(percent, { stiffness: 180, damping: 12 }) }}>
+            {({ width }) =>
+              <span className={classNames('poll__chart', { leading })} style={{ width: `${width}%` }} />
+            }
+          </Motion>
+        )}
+      </li>
+    );
+  }
+
+  render () {
+    const { poll, intl } = this.props;
+    const { revealed, expired } = this.state;
+
+    if (!poll) {
+      return null;
+    }
+
+    const timeRemaining = expired ? intl.formatMessage(messages.closed) : <RelativeTimestamp timestamp={poll.get('expires_at')} futureDate />;
+    const showResults   = poll.get('voted') || revealed || expired;
+    const disabled      = this.props.disabled || Object.entries(this.state.selected).every(item => !item);
+
+    let votesCount = null;
+
+    if (poll.get('voters_count') !== null && poll.get('voters_count') !== undefined) {
+      votesCount = <FormattedMessage id='poll.total_people' defaultMessage='{count, plural, one {# person} other {# people}}' values={{ count: poll.get('voters_count') }} />;
+    } else {
+      votesCount = <FormattedMessage id='poll.total_votes' defaultMessage='{count, plural, one {# vote} other {# votes}}' values={{ count: poll.get('votes_count') }} />;
+    }
+
+    return (
+      <div className='poll'>
+        <ul>
+          {poll.get('options').map((option, i) => this.renderOption(option, i, showResults))}
+        </ul>
+
+        <div className='poll__footer'>
+          {!showResults && <button className='button button-secondary' disabled={disabled || !this.context.identity.signedIn} onClick={this.handleVote}><FormattedMessage id='poll.vote' defaultMessage='Vote' /></button>}
+          {!showResults && <><button className='poll__link' onClick={this.handleReveal}><FormattedMessage id='poll.reveal' defaultMessage='See results' /></button> · </>}
+          {showResults && !this.props.disabled && <><button className='poll__link' onClick={this.handleRefresh}><FormattedMessage id='poll.refresh' defaultMessage='Refresh' /></button> · </>}
+          {votesCount}
+          {poll.get('expires_at') && <> · {timeRemaining}</>}
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(Poll);
diff --git a/app/javascript/flavours/blobfox/components/radio_button.tsx b/app/javascript/flavours/blobfox/components/radio_button.tsx
new file mode 100644
index 00000000000000..d0a565b9e65aa3
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/radio_button.tsx
@@ -0,0 +1,33 @@
+import classNames from 'classnames';
+
+interface Props {
+  value: string;
+  checked: boolean;
+  name: string;
+  onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
+  label: React.ReactNode;
+}
+
+export const RadioButton: React.FC<Props> = ({
+  name,
+  value,
+  checked,
+  onChange,
+  label,
+}) => {
+  return (
+    <label className='radio-button'>
+      <input
+        name={name}
+        type='radio'
+        value={value}
+        checked={checked}
+        onChange={onChange}
+      />
+
+      <span className={classNames('radio-button__input', { checked })} />
+
+      <span>{label}</span>
+    </label>
+  );
+};
diff --git a/app/javascript/flavours/blobfox/components/regeneration_indicator.jsx b/app/javascript/flavours/blobfox/components/regeneration_indicator.jsx
new file mode 100644
index 00000000000000..caaa67ca1dbbf6
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/regeneration_indicator.jsx
@@ -0,0 +1,18 @@
+import { FormattedMessage } from 'react-intl';
+
+import illustration from 'flavours/blobfox/images/elephant_ui_working.svg';
+
+const RegenerationIndicator = () => (
+  <div className='regeneration-indicator'>
+    <div className='regeneration-indicator__figure'>
+      <img src={illustration} alt='' />
+    </div>
+
+    <div className='regeneration-indicator__label'>
+      <FormattedMessage id='regeneration_indicator.label' tagName='strong' defaultMessage='Loading&hellip;' />
+      <FormattedMessage id='regeneration_indicator.sublabel' defaultMessage='Your home feed is being prepared!' />
+    </div>
+  </div>
+);
+
+export default RegenerationIndicator;
diff --git a/app/javascript/flavours/blobfox/components/relative_timestamp.tsx b/app/javascript/flavours/blobfox/components/relative_timestamp.tsx
new file mode 100644
index 00000000000000..ac3ab0fb4d48f6
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/relative_timestamp.tsx
@@ -0,0 +1,282 @@
+import { Component } from 'react';
+
+import type { IntlShape } from 'react-intl';
+import { injectIntl, defineMessages } from 'react-intl';
+
+const messages = defineMessages({
+  today: { id: 'relative_time.today', defaultMessage: 'today' },
+  just_now: { id: 'relative_time.just_now', defaultMessage: 'now' },
+  just_now_full: {
+    id: 'relative_time.full.just_now',
+    defaultMessage: 'just now',
+  },
+  seconds: { id: 'relative_time.seconds', defaultMessage: '{number}s' },
+  seconds_full: {
+    id: 'relative_time.full.seconds',
+    defaultMessage: '{number, plural, one {# second} other {# seconds}} ago',
+  },
+  minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' },
+  minutes_full: {
+    id: 'relative_time.full.minutes',
+    defaultMessage: '{number, plural, one {# minute} other {# minutes}} ago',
+  },
+  hours: { id: 'relative_time.hours', defaultMessage: '{number}h' },
+  hours_full: {
+    id: 'relative_time.full.hours',
+    defaultMessage: '{number, plural, one {# hour} other {# hours}} ago',
+  },
+  days: { id: 'relative_time.days', defaultMessage: '{number}d' },
+  days_full: {
+    id: 'relative_time.full.days',
+    defaultMessage: '{number, plural, one {# day} other {# days}} ago',
+  },
+  moments_remaining: {
+    id: 'time_remaining.moments',
+    defaultMessage: 'Moments remaining',
+  },
+  seconds_remaining: {
+    id: 'time_remaining.seconds',
+    defaultMessage: '{number, plural, one {# second} other {# seconds}} left',
+  },
+  minutes_remaining: {
+    id: 'time_remaining.minutes',
+    defaultMessage: '{number, plural, one {# minute} other {# minutes}} left',
+  },
+  hours_remaining: {
+    id: 'time_remaining.hours',
+    defaultMessage: '{number, plural, one {# hour} other {# hours}} left',
+  },
+  days_remaining: {
+    id: 'time_remaining.days',
+    defaultMessage: '{number, plural, one {# day} other {# days}} left',
+  },
+});
+
+const dateFormatOptions = {
+  hour12: false,
+  year: 'numeric',
+  month: 'short',
+  day: '2-digit',
+  hour: '2-digit',
+  minute: '2-digit',
+} as const;
+
+const shortDateFormatOptions = {
+  month: 'short',
+  day: 'numeric',
+} as const;
+
+const SECOND = 1000;
+const MINUTE = 1000 * 60;
+const HOUR = 1000 * 60 * 60;
+const DAY = 1000 * 60 * 60 * 24;
+
+const MAX_DELAY = 2147483647;
+
+const selectUnits = (delta: number) => {
+  const absDelta = Math.abs(delta);
+
+  if (absDelta < MINUTE) {
+    return 'second';
+  } else if (absDelta < HOUR) {
+    return 'minute';
+  } else if (absDelta < DAY) {
+    return 'hour';
+  }
+
+  return 'day';
+};
+
+const getUnitDelay = (units: string) => {
+  switch (units) {
+    case 'second':
+      return SECOND;
+    case 'minute':
+      return MINUTE;
+    case 'hour':
+      return HOUR;
+    case 'day':
+      return DAY;
+    default:
+      return MAX_DELAY;
+  }
+};
+
+export const timeAgoString = (
+  intl: IntlShape,
+  date: Date,
+  now: number,
+  year: number,
+  timeGiven: boolean,
+  short?: boolean,
+) => {
+  const delta = now - date.getTime();
+
+  let relativeTime;
+
+  if (delta < DAY && !timeGiven) {
+    relativeTime = intl.formatMessage(messages.today);
+  } else if (delta < 10 * SECOND) {
+    relativeTime = intl.formatMessage(
+      short ? messages.just_now : messages.just_now_full,
+    );
+  } else if (delta < 7 * DAY) {
+    if (delta < MINUTE) {
+      relativeTime = intl.formatMessage(
+        short ? messages.seconds : messages.seconds_full,
+        { number: Math.floor(delta / SECOND) },
+      );
+    } else if (delta < HOUR) {
+      relativeTime = intl.formatMessage(
+        short ? messages.minutes : messages.minutes_full,
+        { number: Math.floor(delta / MINUTE) },
+      );
+    } else if (delta < DAY) {
+      relativeTime = intl.formatMessage(
+        short ? messages.hours : messages.hours_full,
+        { number: Math.floor(delta / HOUR) },
+      );
+    } else {
+      relativeTime = intl.formatMessage(
+        short ? messages.days : messages.days_full,
+        { number: Math.floor(delta / DAY) },
+      );
+    }
+  } else if (date.getFullYear() === year) {
+    relativeTime = intl.formatDate(date, shortDateFormatOptions);
+  } else {
+    relativeTime = intl.formatDate(date, {
+      ...shortDateFormatOptions,
+      year: 'numeric',
+    });
+  }
+
+  return relativeTime;
+};
+
+const timeRemainingString = (
+  intl: IntlShape,
+  date: Date,
+  now: number,
+  timeGiven = true,
+) => {
+  const delta = date.getTime() - now;
+
+  let relativeTime;
+
+  if (delta < DAY && !timeGiven) {
+    relativeTime = intl.formatMessage(messages.today);
+  } else if (delta < 10 * SECOND) {
+    relativeTime = intl.formatMessage(messages.moments_remaining);
+  } else if (delta < MINUTE) {
+    relativeTime = intl.formatMessage(messages.seconds_remaining, {
+      number: Math.floor(delta / SECOND),
+    });
+  } else if (delta < HOUR) {
+    relativeTime = intl.formatMessage(messages.minutes_remaining, {
+      number: Math.floor(delta / MINUTE),
+    });
+  } else if (delta < DAY) {
+    relativeTime = intl.formatMessage(messages.hours_remaining, {
+      number: Math.floor(delta / HOUR),
+    });
+  } else {
+    relativeTime = intl.formatMessage(messages.days_remaining, {
+      number: Math.floor(delta / DAY),
+    });
+  }
+
+  return relativeTime;
+};
+
+interface Props {
+  intl: IntlShape;
+  timestamp: string;
+  year: number;
+  futureDate?: boolean;
+  short?: boolean;
+}
+interface States {
+  now: number;
+}
+class RelativeTimestamp extends Component<Props, States> {
+  state = {
+    now: Date.now(),
+  };
+
+  static defaultProps = {
+    year: new Date().getFullYear(),
+    short: true,
+  };
+
+  _timer: number | undefined;
+
+  shouldComponentUpdate(nextProps: Props, nextState: States) {
+    // As of right now the locale doesn't change without a new page load,
+    // but we might as well check in case that ever changes.
+    return (
+      this.props.timestamp !== nextProps.timestamp ||
+      this.props.intl.locale !== nextProps.intl.locale ||
+      this.state.now !== nextState.now
+    );
+  }
+
+  UNSAFE_componentWillReceiveProps(nextProps: Props) {
+    if (this.props.timestamp !== nextProps.timestamp) {
+      this.setState({ now: Date.now() });
+    }
+  }
+
+  componentDidMount() {
+    this._scheduleNextUpdate(this.props, this.state);
+  }
+
+  UNSAFE_componentWillUpdate(nextProps: Props, nextState: States) {
+    this._scheduleNextUpdate(nextProps, nextState);
+  }
+
+  componentWillUnmount() {
+    window.clearTimeout(this._timer);
+  }
+
+  _scheduleNextUpdate(props: Props, state: States) {
+    window.clearTimeout(this._timer);
+
+    const { timestamp } = props;
+    const delta = new Date(timestamp).getTime() - state.now;
+    const unitDelay = getUnitDelay(selectUnits(delta));
+    const unitRemainder = Math.abs(delta % unitDelay);
+    const updateInterval = 1000 * 10;
+    const delay =
+      delta < 0
+        ? Math.max(updateInterval, unitDelay - unitRemainder)
+        : Math.max(updateInterval, unitRemainder);
+
+    this._timer = window.setTimeout(() => {
+      this.setState({ now: Date.now() });
+    }, delay);
+  }
+
+  render() {
+    const { timestamp, intl, year, futureDate, short } = this.props;
+
+    const timeGiven = timestamp.includes('T');
+    const date = new Date(timestamp);
+    const relativeTime = futureDate
+      ? timeRemainingString(intl, date, this.state.now, timeGiven)
+      : timeAgoString(intl, date, this.state.now, year, timeGiven, short);
+
+    return (
+      <time
+        dateTime={timestamp}
+        title={intl.formatDate(date, dateFormatOptions)}
+      >
+        {relativeTime}
+      </time>
+    );
+  }
+}
+
+const RelativeTimestampWithIntl = injectIntl(RelativeTimestamp);
+
+export { RelativeTimestampWithIntl as RelativeTimestamp };
diff --git a/app/javascript/flavours/blobfox/components/router.tsx b/app/javascript/flavours/blobfox/components/router.tsx
new file mode 100644
index 00000000000000..188f3b483865cb
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/router.tsx
@@ -0,0 +1,80 @@
+import type { PropsWithChildren } from 'react';
+import React from 'react';
+
+import { Router as OriginalRouter } from 'react-router';
+
+import type {
+  LocationDescriptor,
+  LocationDescriptorObject,
+  Path,
+} from 'history';
+import { createBrowserHistory } from 'history';
+
+import { layoutFromWindow } from 'flavours/blobfox/is_mobile';
+
+interface MastodonLocationState {
+  fromMastodon?: boolean;
+  mastodonModalKey?: string;
+}
+type HistoryPath = Path | LocationDescriptor<MastodonLocationState>;
+
+const browserHistory = createBrowserHistory<
+  MastodonLocationState | undefined
+>();
+const originalPush = browserHistory.push.bind(browserHistory);
+const originalReplace = browserHistory.replace.bind(browserHistory);
+
+function normalizePath(
+  path: HistoryPath,
+  state?: MastodonLocationState,
+): LocationDescriptorObject<MastodonLocationState> {
+  const location = typeof path === 'string' ? { pathname: path } : { ...path };
+
+  if (location.state === undefined && state !== undefined) {
+    location.state = state;
+  } else if (
+    location.state !== undefined &&
+    state !== undefined &&
+    process.env.NODE_ENV === 'development'
+  ) {
+    // eslint-disable-next-line no-console
+    console.log(
+      'You should avoid providing a 2nd state argument to push when the 1st argument is a location-like object that already has state; it is ignored',
+    );
+  }
+
+  if (
+    layoutFromWindow() === 'multi-column' &&
+    !location.pathname?.startsWith('/deck')
+  ) {
+    location.pathname = `/deck${location.pathname}`;
+  }
+
+  return location;
+}
+
+browserHistory.push = (path: HistoryPath, state?: MastodonLocationState) => {
+  const location = normalizePath(path, state);
+
+  location.state = location.state ?? {};
+  location.state.fromMastodon = true;
+
+  originalPush(location);
+};
+
+browserHistory.replace = (path: HistoryPath, state?: MastodonLocationState) => {
+  const location = normalizePath(path, state);
+
+  if (!location.pathname) return;
+
+  if (browserHistory.location.state?.fromMastodon) {
+    location.state = location.state ?? {};
+    location.state.fromMastodon = true;
+  }
+
+  originalReplace(location);
+};
+
+export const Router: React.FC<PropsWithChildren> = ({ children }) => {
+  return <OriginalRouter history={browserHistory}>{children}</OriginalRouter>;
+};
diff --git a/app/javascript/flavours/blobfox/components/scrollable_list.jsx b/app/javascript/flavours/blobfox/components/scrollable_list.jsx
new file mode 100644
index 00000000000000..f8061aad96b9c8
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/scrollable_list.jsx
@@ -0,0 +1,405 @@
+import PropTypes from 'prop-types';
+import { Children, cloneElement, PureComponent } from 'react';
+
+import classNames from 'classnames';
+import { useLocation } from 'react-router-dom';
+
+import { List as ImmutableList } from 'immutable';
+import { connect } from 'react-redux';
+
+import { supportsPassiveEvents } from 'detect-passive-events';
+import { throttle } from 'lodash';
+
+import ScrollContainer from 'flavours/blobfox/containers/scroll_container';
+
+import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
+import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen';
+import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
+
+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;
+
+/**
+ *
+ * @param {import('flavours/blobfox/store').RootState} state
+ * @param {*} props
+ */
+const mapStateToProps = (state, { scrollKey }) => {
+  return {
+    preventScroll: scrollKey === state.dropdownMenu.scrollKey,
+  };
+};
+
+// This component only exists to be able to call useLocation()
+const IOArticleContainerWrapper = ({id, index, listLength, intersectionObserverWrapper, trackScroll, scrollKey, children}) => {
+  const location = useLocation();
+
+  return (<IntersectionObserverArticleContainer
+    id={id}
+    index={index}
+    listLength={listLength}
+    intersectionObserverWrapper={intersectionObserverWrapper}
+    saveHeightKey={trackScroll ? `${location.key}:${scrollKey}` : null}
+  >
+    {children}
+  </IntersectionObserverArticleContainer>);
+};
+
+IOArticleContainerWrapper.propTypes =  {
+  id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+  index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+  listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+  scrollKey: PropTypes.string.isRequired,
+  intersectionObserverWrapper: PropTypes.object.isRequired,
+  trackScroll: PropTypes.bool.isRequired,
+  children: PropTypes.node,
+};
+
+class ScrollableList extends PureComponent {
+
+  static propTypes = {
+    scrollKey: PropTypes.string.isRequired,
+    onLoadMore: PropTypes.func,
+    onLoadPending: PropTypes.func,
+    onScrollToTop: PropTypes.func,
+    onScroll: PropTypes.func,
+    trackScroll: PropTypes.bool,
+    isLoading: PropTypes.bool,
+    showLoading: PropTypes.bool,
+    hasMore: PropTypes.bool,
+    numPending: PropTypes.number,
+    prepend: PropTypes.node,
+    append: PropTypes.node,
+    alwaysPrepend: PropTypes.bool,
+    emptyMessage: PropTypes.node,
+    children: PropTypes.node,
+    bindToDocument: PropTypes.bool,
+    preventScroll: PropTypes.bool,
+  };
+
+  static defaultProps = {
+    trackScroll: true,
+  };
+
+  state = {
+    fullscreen: null,
+    cachedMediaWidth: 300,
+  };
+
+  intersectionObserverWrapper = new IntersectionObserverWrapper();
+
+  handleScroll = throttle(() => {
+    if (this.node) {
+      const scrollTop = this.getScrollTop();
+      const scrollHeight = this.getScrollHeight();
+      const clientHeight = this.getClientHeight();
+      const offset = scrollHeight - scrollTop - clientHeight;
+
+      if (scrollTop > 0 && offset < 400 && this.props.onLoadMore && this.props.hasMore && !this.props.isLoading) {
+        this.props.onLoadMore();
+      }
+
+      if (scrollTop < 100 && this.props.onScrollToTop) {
+        this.props.onScrollToTop();
+      } else if (this.props.onScroll) {
+        this.props.onScroll();
+      }
+
+      if (!this.lastScrollWasSynthetic) {
+        // If the last scroll wasn't caused by setScrollTop(), assume it was
+        // intentional and cancel any pending scroll reset on mouse idle
+        this.scrollToTopOnMouseIdle = false;
+      }
+      this.lastScrollWasSynthetic = false;
+    }
+  }, 150, {
+    trailing: true,
+  });
+
+  mouseIdleTimer = null;
+  mouseMovedRecently = false;
+  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;
+    }
+  };
+
+  clearMouseIdleTimer = () => {
+    if (this.mouseIdleTimer === null) {
+      return;
+    }
+
+    clearTimeout(this.mouseIdleTimer);
+    this.mouseIdleTimer = null;
+  };
+
+  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);
+
+    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);
+
+  handleWheel = throttle(() => {
+    this.scrollToTopOnMouseIdle = false;
+  }, 150, {
+    trailing: true,
+  });
+
+  handleMouseIdle = () => {
+    if (this.scrollToTopOnMouseIdle && !this.props.preventScroll) {
+      this.setScrollTop(0);
+    }
+
+    this.mouseMovedRecently = false;
+    this.scrollToTopOnMouseIdle = false;
+  };
+
+  componentDidMount () {
+    this.attachScrollListener();
+    this.attachIntersectionObserver();
+
+    attachFullscreenListener(this.onFullScreenChange);
+
+    // Handle initial scroll position
+    this.handleScroll();
+  }
+
+  getScrollPosition = () => {
+    if (this.node && (this.getScrollTop() > 0 || this.mouseMovedRecently)) {
+      return { height: this.getScrollHeight(), top: this.getScrollTop() };
+    } else {
+      return null;
+    }
+  };
+
+  getScrollTop = () => {
+    return this._getScrollingElement().scrollTop;
+  };
+
+  getScrollHeight = () => {
+    return this._getScrollingElement().scrollHeight;
+  };
+
+  getClientHeight = () => {
+    return this._getScrollingElement().clientHeight;
+  };
+
+  updateScrollBottom = (snapshot) => {
+    const newScrollTop = this.getScrollHeight() - snapshot;
+
+    this.setScrollTop(newScrollTop);
+  };
+
+  getSnapshotBeforeUpdate (prevProps) {
+    const someItemInserted = Children.count(prevProps.children) > 0 &&
+      Children.count(prevProps.children) < Children.count(this.props.children) &&
+      this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props);
+    const pendingChanged = (prevProps.numPending > 0) !== (this.props.numPending > 0);
+
+    if (pendingChanged || someItemInserted && (this.getScrollTop() > 0 || this.mouseMovedRecently || this.props.preventScroll)) {
+      return this.getScrollHeight() - this.getScrollTop();
+    } else {
+      return null;
+    }
+  }
+
+  componentDidUpdate (prevProps, prevState, snapshot) {
+    // Reset the scroll position when a new child comes in in order not to
+    // jerk the scrollbar around if you're already scrolled down the page.
+    if (snapshot !== null) {
+      this.updateScrollBottom(snapshot);
+    }
+  }
+
+  cacheMediaWidth = (width) => {
+    if (width && this.state.cachedMediaWidth !== width) {
+      this.setState({ cachedMediaWidth: width });
+    }
+  };
+
+  componentWillUnmount () {
+    this.clearMouseIdleTimer();
+    this.detachScrollListener();
+    this.detachIntersectionObserver();
+
+    detachFullscreenListener(this.onFullScreenChange);
+  }
+
+  onFullScreenChange = () => {
+    this.setState({ fullscreen: isFullscreen() });
+  };
+
+  attachIntersectionObserver () {
+    let nodeOptions = {
+      root: this.node,
+      rootMargin: '300% 0px',
+    };
+
+    this.intersectionObserverWrapper
+      .connect(this.props.bindToDocument ? {} : nodeOptions);
+  }
+
+  detachIntersectionObserver () {
+    this.intersectionObserverWrapper.disconnect();
+  }
+
+  attachScrollListener () {
+    if (this.props.bindToDocument) {
+      document.addEventListener('scroll', this.handleScroll);
+      document.addEventListener('wheel', this.handleWheel,  listenerOptions);
+    } else {
+      this.node.addEventListener('scroll', this.handleScroll);
+      this.node.addEventListener('wheel', this.handleWheel, listenerOptions);
+    }
+  }
+
+  detachScrollListener () {
+    if (this.props.bindToDocument) {
+      document.removeEventListener('scroll', this.handleScroll);
+      document.removeEventListener('wheel', this.handleWheel, listenerOptions);
+    } else {
+      this.node.removeEventListener('scroll', this.handleScroll);
+      this.node.removeEventListener('wheel', this.handleWheel, listenerOptions);
+    }
+  }
+
+  getFirstChildKey (props) {
+    const { children } = props;
+    let firstChild     = children;
+
+    if (children instanceof ImmutableList) {
+      firstChild = children.get(0);
+    } else if (Array.isArray(children)) {
+      firstChild = children[0];
+    }
+
+    return firstChild && firstChild.key;
+  }
+
+  setRef = (c) => {
+    this.node = c;
+  };
+
+  handleLoadMore = e => {
+    e.preventDefault();
+    this.props.onLoadMore();
+  };
+
+  handleLoadPending = e => {
+    e.preventDefault();
+    this.props.onLoadPending();
+    // Prevent the weird scroll-jumping behavior, as we explicitly don't want to
+    // scroll to top, and we know the scroll height is going to change
+    this.scrollToTopOnMouseIdle = false;
+    this.lastScrollWasSynthetic = false;
+    this.clearMouseIdleTimer();
+    this.mouseIdleTimer = setTimeout(this.handleMouseIdle, MOUSE_IDLE_DELAY);
+    this.mouseMovedRecently = true;
+  };
+
+  render () {
+    const { children, scrollKey, trackScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, emptyMessage, onLoadMore } = this.props;
+    const { fullscreen } = this.state;
+    const childrenCount = Children.count(children);
+
+    const loadMore     = (hasMore && onLoadMore) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null;
+    const loadPending  = (numPending > 0) ? <LoadPending count={numPending} onClick={this.handleLoadPending} /> : null;
+    let scrollableArea = null;
+
+    if (showLoading) {
+      scrollableArea = (
+        <div className='scrollable scrollable--flex' ref={this.setRef}>
+          <div role='feed' className='item-list'>
+            {prepend}
+          </div>
+
+          <div className='scrollable__append'>
+            <LoadingIndicator />
+          </div>
+        </div>
+      );
+    } else if (isLoading || childrenCount > 0 || hasMore || !emptyMessage) {
+      scrollableArea = (
+        <div className={classNames('scrollable', { fullscreen })} ref={this.setRef} onMouseMove={this.handleMouseMove}>
+          <div role='feed' className='item-list'>
+            {prepend}
+
+            {loadPending}
+
+            {Children.map(this.props.children, (child, index) => (
+              <IOArticleContainerWrapper
+                key={child.key}
+                id={child.key}
+                index={index}
+                listLength={childrenCount}
+                intersectionObserverWrapper={this.intersectionObserverWrapper}
+                trackScroll={trackScroll}
+                scrollKey={scrollKey}
+              >
+                {cloneElement(child, {
+                  getScrollPosition: this.getScrollPosition,
+                  updateScrollBottom: this.updateScrollBottom,
+                  cachedMediaWidth: this.state.cachedMediaWidth,
+                  cacheMediaWidth: this.cacheMediaWidth,
+                })}
+              </IOArticleContainerWrapper>
+            ))}
+
+            {loadMore}
+
+            {!hasMore && append}
+          </div>
+        </div>
+      );
+    } else {
+      scrollableArea = (
+        <div className={classNames('scrollable scrollable--flex', { fullscreen })} ref={this.setRef}>
+          {alwaysPrepend && prepend}
+
+          <div className='empty-column-indicator'>
+            {emptyMessage}
+          </div>
+        </div>
+      );
+    }
+
+    if (trackScroll) {
+      return (
+        <ScrollContainer scrollKey={scrollKey}>
+          {scrollableArea}
+        </ScrollContainer>
+      );
+    } else {
+      return scrollableArea;
+    }
+  }
+
+}
+
+export default connect(mapStateToProps, null, null, { forwardRef: true })(ScrollableList);
diff --git a/app/javascript/flavours/blobfox/components/server_banner.jsx b/app/javascript/flavours/blobfox/components/server_banner.jsx
new file mode 100644
index 00000000000000..396ea67016b3c9
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/server_banner.jsx
@@ -0,0 +1,97 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
+
+import { Link } from 'react-router-dom';
+
+import { connect } from 'react-redux';
+
+import { fetchServer } from 'flavours/blobfox/actions/server';
+import { ServerHeroImage } from 'flavours/blobfox/components/server_hero_image';
+import { ShortNumber } from 'flavours/blobfox/components/short_number';
+import { Skeleton } from 'flavours/blobfox/components/skeleton';
+import Account from 'flavours/blobfox/containers/account_container';
+import { domain } from 'flavours/blobfox/initial_state';
+
+const messages = defineMessages({
+  aboutActiveUsers: { id: 'server_banner.about_active_users', defaultMessage: 'People using this server during the last 30 days (Monthly Active Users)' },
+});
+
+const mapStateToProps = state => ({
+  server: state.getIn(['server', 'server']),
+});
+
+class ServerBanner extends PureComponent {
+
+  static propTypes = {
+    server: PropTypes.object,
+    dispatch: PropTypes.func,
+    intl: PropTypes.object,
+  };
+
+  componentDidMount () {
+    const { dispatch } = this.props;
+    dispatch(fetchServer());
+  }
+
+  render () {
+    const { server, intl } = this.props;
+    const isLoading = server.get('isLoading');
+
+    return (
+      <div className='server-banner'>
+        <div className='server-banner__introduction'>
+          <FormattedMessage id='server_banner.introduction' defaultMessage='{domain} is part of the decentralized social network powered by {mastodon}.' values={{ domain: <strong>{domain}</strong>, mastodon: <a href='https://joinmastodon.org' target='_blank'>Mastodon</a> }} />
+        </div>
+
+        <ServerHeroImage blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} className='server-banner__hero' />
+
+        <div className='server-banner__description'>
+          {isLoading ? (
+            <>
+              <Skeleton width='100%' />
+              <br />
+              <Skeleton width='100%' />
+              <br />
+              <Skeleton width='70%' />
+            </>
+          ) : server.get('description')}
+        </div>
+
+        <div className='server-banner__meta'>
+          <div className='server-banner__meta__column'>
+            <h4><FormattedMessage id='server_banner.administered_by' defaultMessage='Administered by:' /></h4>
+
+            <Account id={server.getIn(['contact', 'account', 'id'])} size={36} minimal />
+          </div>
+
+          <div className='server-banner__meta__column'>
+            <h4><FormattedMessage id='server_banner.server_stats' defaultMessage='Server stats:' /></h4>
+
+            {isLoading ? (
+              <>
+                <strong className='server-banner__number'><Skeleton width='10ch' /></strong>
+                <br />
+                <span className='server-banner__number-label'><Skeleton width='5ch' /></span>
+              </>
+            ) : (
+              <>
+                <strong className='server-banner__number'><ShortNumber value={server.getIn(['usage', 'users', 'active_month'])} /></strong>
+                <br />
+                <span className='server-banner__number-label' title={intl.formatMessage(messages.aboutActiveUsers)}><FormattedMessage id='server_banner.active_users' defaultMessage='active users' /></span>
+              </>
+            )}
+          </div>
+        </div>
+
+        <hr className='spacer' />
+
+        <Link className='button button--block button-secondary' to='/about'><FormattedMessage id='server_banner.learn_more' defaultMessage='Learn more' /></Link>
+      </div>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(injectIntl(ServerBanner));
diff --git a/app/javascript/flavours/blobfox/components/server_hero_image.tsx b/app/javascript/flavours/blobfox/components/server_hero_image.tsx
new file mode 100644
index 00000000000000..68b7f03df39744
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/server_hero_image.tsx
@@ -0,0 +1,35 @@
+import { useCallback, useState } from 'react';
+
+import classNames from 'classnames';
+
+import { Blurhash } from './blurhash';
+
+interface Props {
+  src: string;
+  srcSet?: string;
+  blurhash?: string;
+  className?: string;
+}
+
+export const ServerHeroImage: React.FC<Props> = ({
+  src,
+  srcSet,
+  blurhash,
+  className,
+}) => {
+  const [loaded, setLoaded] = useState(false);
+
+  const handleLoad = useCallback(() => {
+    setLoaded(true);
+  }, [setLoaded]);
+
+  return (
+    <div
+      className={classNames('image', { loaded }, className)}
+      role='presentation'
+    >
+      {blurhash && <Blurhash hash={blurhash} className='image__preview' />}
+      <img src={src} srcSet={srcSet} alt='' onLoad={handleLoad} />
+    </div>
+  );
+};
diff --git a/app/javascript/flavours/blobfox/components/setting_text.jsx b/app/javascript/flavours/blobfox/components/setting_text.jsx
new file mode 100644
index 00000000000000..79d4bf8ea315e2
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/setting_text.jsx
@@ -0,0 +1,35 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
+export default class SettingText extends PureComponent {
+
+  static propTypes = {
+    settings: ImmutablePropTypes.map.isRequired,
+    settingPath: PropTypes.array.isRequired,
+    label: PropTypes.string.isRequired,
+    onChange: PropTypes.func.isRequired,
+  };
+
+  handleChange = (e) => {
+    this.props.onChange(this.props.settingPath, e.target.value);
+  };
+
+  render () {
+    const { settings, settingPath, label } = this.props;
+
+    return (
+      <label>
+        <span style={{ display: 'none' }}>{label}</span>
+        <input
+          className='setting-text'
+          value={settings.getIn(settingPath)}
+          onChange={this.handleChange}
+          placeholder={label}
+        />
+      </label>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/components/short_number.tsx b/app/javascript/flavours/blobfox/components/short_number.tsx
new file mode 100644
index 00000000000000..74c3c5d75e71cf
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/short_number.tsx
@@ -0,0 +1,90 @@
+import { memo } from 'react';
+
+import { FormattedMessage, FormattedNumber } from 'react-intl';
+
+import { toShortNumber, pluralReady, DECIMAL_UNITS } from '../utils/numbers';
+
+type ShortNumberRenderer = (
+  displayNumber: JSX.Element,
+  pluralReady: number,
+) => JSX.Element;
+
+interface ShortNumberProps {
+  value: number;
+  renderer?: ShortNumberRenderer;
+  children?: ShortNumberRenderer;
+}
+
+export const ShortNumberRenderer: React.FC<ShortNumberProps> = ({
+  value,
+  renderer,
+  children,
+}) => {
+  const shortNumber = toShortNumber(value);
+  const [, division] = shortNumber;
+
+  if (children && renderer) {
+    console.warn(
+      'Both renderer prop and renderer as a child provided. This is a mistake and you really should fix that. Only renderer passed as a child will be used.',
+    );
+  }
+
+  const customRenderer = children ?? renderer ?? null;
+
+  const displayNumber = <ShortNumberCounter value={shortNumber} />;
+
+  return (
+    customRenderer?.(displayNumber, pluralReady(value, division)) ??
+    displayNumber
+  );
+};
+export const ShortNumber = memo(ShortNumberRenderer);
+
+interface ShortNumberCounterProps {
+  value: number[];
+}
+const ShortNumberCounter: React.FC<ShortNumberCounterProps> = ({ value }) => {
+  const [rawNumber, unit, maxFractionDigits = 0] = value;
+
+  const count = (
+    <FormattedNumber
+      value={rawNumber}
+      maximumFractionDigits={maxFractionDigits}
+    />
+  );
+
+  const values = { count, rawNumber };
+
+  switch (unit) {
+    case DECIMAL_UNITS.THOUSAND: {
+      return (
+        <FormattedMessage
+          id='units.short.thousand'
+          defaultMessage='{count}K'
+          values={values}
+        />
+      );
+    }
+    case DECIMAL_UNITS.MILLION: {
+      return (
+        <FormattedMessage
+          id='units.short.million'
+          defaultMessage='{count}M'
+          values={values}
+        />
+      );
+    }
+    case DECIMAL_UNITS.BILLION: {
+      return (
+        <FormattedMessage
+          id='units.short.billion'
+          defaultMessage='{count}B'
+          values={values}
+        />
+      );
+    }
+    // Not sure if we should go farther - @Sasha-Sorokin
+    default:
+      return count;
+  }
+};
diff --git a/app/javascript/flavours/blobfox/components/skeleton.tsx b/app/javascript/flavours/blobfox/components/skeleton.tsx
new file mode 100644
index 00000000000000..d6f1aed7238f87
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/skeleton.tsx
@@ -0,0 +1,10 @@
+interface Props {
+  width?: number | string;
+  height?: number | string;
+}
+
+export const Skeleton: React.FC<Props> = ({ width, height }) => (
+  <span className='skeleton' style={{ width, height }}>
+    &zwnj;
+  </span>
+);
diff --git a/app/javascript/flavours/blobfox/components/status.jsx b/app/javascript/flavours/blobfox/components/status.jsx
new file mode 100644
index 00000000000000..f4c4d0d5e387c8
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/status.jsx
@@ -0,0 +1,879 @@
+import PropTypes from 'prop-types';
+
+import { injectIntl, FormattedMessage } from 'react-intl';
+
+import classNames from 'classnames';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import { HotKeys } from 'react-hotkeys';
+
+import PictureInPicturePlaceholder from 'flavours/blobfox/components/picture_in_picture_placeholder';
+import PollContainer from 'flavours/blobfox/containers/poll_container';
+import NotificationOverlayContainer from 'flavours/blobfox/features/notifications/containers/overlay_container';
+import { autoUnfoldCW } from 'flavours/blobfox/utils/content_warning';
+import { withOptionalRouter, WithOptionalRouterPropTypes } from 'flavours/blobfox/utils/react_router';
+
+import Card from '../features/status/components/card';
+// 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';
+import { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
+import { displayMedia, visibleReactions } from '../initial_state';
+
+import AttachmentList from './attachment_list';
+import { getHashtagBarForStatus } from './hashtag_bar';
+import StatusActionBar from './status_action_bar';
+import StatusContent from './status_content';
+import StatusHeader from './status_header';
+import StatusIcons from './status_icons';
+import StatusPrepend from './status_prepend';
+import StatusReactions from './status_reactions';
+
+const domParser = new DOMParser();
+
+export const textForScreenReader = (intl, status, rebloggedByText = false, expanded = false) => {
+  const displayName = status.getIn(['account', 'display_name']);
+
+  const spoilerText = status.getIn(['translation', 'spoiler_text']) || status.get('spoiler_text');
+  const contentHtml = status.getIn(['translation', 'contentHtml']) || status.get('contentHtml');
+  const contentText = domParser.parseFromString(contentHtml, 'text/html').documentElement.textContent;
+
+  const values = [
+    displayName.length === 0 ? status.getIn(['account', 'acct']).split('@')[0] : displayName,
+    spoilerText && !expanded ? spoilerText : contentText,
+    intl.formatDate(status.get('created_at'), { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }),
+    status.getIn(['account', 'acct']),
+  ];
+
+  if (rebloggedByText) {
+    values.push(rebloggedByText);
+  }
+
+  return values.join(', ');
+};
+
+export const defaultMediaVisibility = (status, settings) => {
+  if (!status) {
+    return undefined;
+  }
+
+  if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
+    status = status.get('reblog');
+  }
+
+  if (settings.getIn(['media', 'reveal_behind_cw']) && !!status.get('spoiler_text')) {
+    return true;
+  }
+
+  return (displayMedia !== 'hide_all' && !status.get('sensitive') || displayMedia === 'show_all');
+};
+
+class Status extends ImmutablePureComponent {
+
+  static contextTypes = {
+    identity: PropTypes.object,
+  };
+
+  static propTypes = {
+    containerId: PropTypes.string,
+    id: PropTypes.string,
+    status: ImmutablePropTypes.map,
+    account: ImmutablePropTypes.record,
+    previousId: PropTypes.string,
+    nextInReplyToId: PropTypes.string,
+    rootId: PropTypes.string,
+    onClick: PropTypes.func,
+    onReply: PropTypes.func,
+    onFavourite: PropTypes.func,
+    onReblog: PropTypes.func,
+    onBookmark: PropTypes.func,
+    onDelete: PropTypes.func,
+    onDirect: PropTypes.func,
+    onMention: PropTypes.func,
+    onReactionAdd: PropTypes.func,
+    onReactionRemove: PropTypes.func,
+    onPin: PropTypes.func,
+    onOpenMedia: PropTypes.func,
+    onOpenVideo: PropTypes.func,
+    onBlock: PropTypes.func,
+    onAddFilter: PropTypes.func,
+    onEmbed: PropTypes.func,
+    onHeightChange: PropTypes.func,
+    onToggleHidden: PropTypes.func,
+    onTranslate: PropTypes.func,
+    onInteractionModal: PropTypes.func,
+    muted: PropTypes.bool,
+    hidden: PropTypes.bool,
+    unread: PropTypes.bool,
+    prepend: PropTypes.string,
+    withDismiss: PropTypes.bool,
+    onMoveUp: PropTypes.func,
+    onMoveDown: PropTypes.func,
+    getScrollPosition: PropTypes.func,
+    updateScrollBottom: PropTypes.func,
+    expanded: PropTypes.bool,
+    intl: PropTypes.object.isRequired,
+    cacheMediaWidth: PropTypes.func,
+    cachedMediaWidth: PropTypes.number,
+    scrollKey: PropTypes.string,
+    deployPictureInPicture: PropTypes.func,
+    settings: ImmutablePropTypes.map.isRequired,
+    pictureInPicture: ImmutablePropTypes.contains({
+      inUse: PropTypes.bool,
+      available: PropTypes.bool,
+    }),
+    ...WithOptionalRouterPropTypes,
+  };
+
+  state = {
+    isCollapsed: false,
+    autoCollapsed: false,
+    isExpanded: undefined,
+    showMedia: undefined,
+    statusId: undefined,
+    revealBehindCW: undefined,
+    showCard: false,
+    forceFilter: undefined,
+  };
+
+  // Avoid checking props that are functions (and whose equality will always
+  // evaluate to false. See react-immutable-pure-component for usage.
+  updateOnProps = [
+    'status',
+    'account',
+    'settings',
+    'prepend',
+    'muted',
+    'notification',
+    'hidden',
+    'expanded',
+    'unread',
+    'pictureInPicture',
+    'previousId',
+    'nextInReplyToId',
+    'rootId',
+  ];
+
+  updateOnStates = [
+    'isExpanded',
+    'isCollapsed',
+    'showMedia',
+    'forceFilter',
+  ];
+
+  //  If our settings have changed to disable collapsed statuses, then we
+  //  need to make sure that we uncollapse every one. We do that by watching
+  //  for changes to `settings.collapsed.enabled` in
+  //  `getderivedStateFromProps()`.
+
+  //  We also need to watch for changes on the `collapse` prop---if this
+  //  changes to anything other than `undefined`, then we need to collapse or
+  //  uncollapse our status accordingly.
+  static getDerivedStateFromProps(nextProps, prevState) {
+    let update = {};
+    let updated = false;
+
+    // Make sure the state mirrors props we track…
+    if (nextProps.expanded !== prevState.expandedProp) {
+      update.expandedProp = nextProps.expanded;
+      updated = true;
+    }
+    if (nextProps.status?.get('hidden') !== prevState.statusPropHidden) {
+      update.statusPropHidden = nextProps.status?.get('hidden');
+      updated = true;
+    }
+
+    // Update state based on new props
+    if (!nextProps.settings.getIn(['collapsed', 'enabled'])) {
+      if (prevState.isCollapsed) {
+        update.isCollapsed = false;
+        updated = true;
+      }
+    }
+
+    // Handle uncollapsing toots when the shared CW state is expanded
+    if (nextProps.settings.getIn(['content_warnings', 'shared_state']) &&
+      nextProps.status?.get('spoiler_text')?.length && nextProps.status?.get('hidden') === false &&
+      prevState.statusPropHidden !== false && prevState.isCollapsed
+    ) {
+      update.isCollapsed = false;
+      updated = true;
+    }
+
+    // The “expanded” prop is used to one-off change the local state.
+    // It's used in the thread view when unfolding/re-folding all CWs at once.
+    if (nextProps.expanded !== prevState.expandedProp &&
+      nextProps.expanded !== undefined
+    ) {
+      update.isExpanded = nextProps.expanded;
+      if (nextProps.expanded) update.isCollapsed = false;
+      updated = true;
+    }
+
+    if (prevState.isExpanded === undefined && update.isExpanded === undefined) {
+      update.isExpanded = autoUnfoldCW(nextProps.settings, nextProps.status);
+      updated = true;
+    }
+
+    if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) {
+      update.showMedia = defaultMediaVisibility(nextProps.status, nextProps.settings);
+      update.statusId = nextProps.status.get('id');
+      updated = true;
+    }
+
+    if (nextProps.settings.getIn(['media', 'reveal_behind_cw']) !== prevState.revealBehindCW) {
+      update.revealBehindCW = nextProps.settings.getIn(['media', 'reveal_behind_cw']);
+      if (update.revealBehindCW) {
+        update.showMedia = defaultMediaVisibility(nextProps.status, nextProps.settings);
+      }
+      updated = true;
+    }
+
+    return updated ? update : null;
+  }
+
+  //  When mounting, we just check to see if our status should be collapsed,
+  //  and collapse it if so. We don't need to worry about whether collapsing
+  //  is enabled here, because `setCollapsed()` already takes that into
+  //  account.
+
+  //  The cases where a status should be collapsed are:
+  //
+  //   -  The `collapse` prop has been set to `true`
+  //   -  The user has decided in local settings to collapse all statuses.
+  //   -  The user has decided to collapse all notifications ('muted'
+  //      statuses).
+  //   -  The user has decided to collapse long statuses and the status is
+  //      over the user set value (default 400 without media, or 610px with).
+  //   -  The status is a reply and the user has decided to collapse all
+  //      replies.
+  //   -  The status contains media and the user has decided to collapse all
+  //      statuses with media.
+  //   -  The status is a reblog the user has decided to collapse all
+  //      statuses which are reblogs.
+  componentDidMount () {
+    const { node } = this;
+    const {
+      status,
+      settings,
+      collapse,
+      muted,
+      prepend,
+    } = this.props;
+
+    // Prevent a crash when node is undefined. Not completely sure why this
+    // happens, might be because status === null.
+    if (node === undefined) return;
+
+    const autoCollapseSettings = settings.getIn(['collapsed', 'auto']);
+
+    // Don't autocollapse if CW state is shared and status is explicitly revealed,
+    // as it could cause surprising changes when receiving notifications
+    if (settings.getIn(['content_warnings', 'shared_state']) && status.get('spoiler_text').length && !status.get('hidden')) return;
+
+    let autoCollapseHeight = parseInt(autoCollapseSettings.get('height'));
+    if (status.get('media_attachments').size && !muted) {
+      autoCollapseHeight += 210;
+    }
+
+    if (collapse ||
+      autoCollapseSettings.get('all') ||
+      (autoCollapseSettings.get('notifications') && muted) ||
+      (autoCollapseSettings.get('lengthy') && node.clientHeight > autoCollapseHeight) ||
+      (autoCollapseSettings.get('reblogs') && prepend === 'reblogged_by') ||
+      (autoCollapseSettings.get('replies') && status.get('in_reply_to_id', null) !== null) ||
+      (autoCollapseSettings.get('media') && !(status.get('spoiler_text').length) && status.get('media_attachments').size > 0)
+    ) {
+      this.setCollapsed(true);
+      // Hack to fix timeline jumps on second rendering when auto-collapsing
+      this.setState({ autoCollapsed: true });
+    }
+
+    // Hack to fix timeline jumps when a preview card is fetched
+    this.setState({
+      showCard: !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card') && this.props.settings.get('inline_preview_cards'),
+    });
+  }
+
+  //  Hack to fix timeline jumps on second rendering when auto-collapsing
+  //  or on subsequent rendering when a preview card has been fetched
+  getSnapshotBeforeUpdate() {
+    if (!this.props.getScrollPosition) return null;
+
+    const { muted, hidden, status, settings } = this.props;
+
+    const doShowCard = !muted && !hidden && status && status.get('card') && settings.get('inline_preview_cards');
+    if (this.state.autoCollapsed || (doShowCard && !this.state.showCard)) {
+      if (doShowCard) this.setState({ showCard: true });
+      if (this.state.autoCollapsed) this.setState({ autoCollapsed: false });
+      return this.props.getScrollPosition();
+    } else {
+      return null;
+    }
+  }
+
+  componentDidUpdate(prevProps, prevState, snapshot) {
+    if (snapshot !== null && this.props.updateScrollBottom && this.node.offsetTop < snapshot.top) {
+      this.props.updateScrollBottom(snapshot.height - snapshot.top);
+    }
+  }
+
+  componentWillUnmount() {
+    if (this.node && this.props.getScrollPosition) {
+      const position = this.props.getScrollPosition();
+      if (position !== null && this.node.offsetTop < position.top) {
+        requestAnimationFrame(() => {
+          this.props.updateScrollBottom(position.height - position.top);
+        });
+      }
+    }
+  }
+
+  //  `setCollapsed()` sets the value of `isCollapsed` in our state, that is,
+  //  whether the toot is collapsed or not.
+
+  //  `setCollapsed()` automatically checks for us whether toot collapsing
+  //  is enabled, so we don't have to.
+  setCollapsed = (value) => {
+    if (this.props.settings.getIn(['collapsed', 'enabled'])) {
+      if (value) {
+        this.setExpansion(false);
+      }
+      this.setState({ isCollapsed: value });
+    } else {
+      this.setState({ isCollapsed: false });
+    }
+  };
+
+  setExpansion = (value) => {
+    if (this.props.settings.getIn(['content_warnings', 'shared_state']) && this.props.status.get('hidden') === value) {
+      this.props.onToggleHidden(this.props.status);
+    }
+
+    this.setState({ isExpanded: value });
+    if (value) {
+      this.setCollapsed(false);
+    }
+  };
+
+  //  `parseClick()` takes a click event and responds appropriately.
+  //  If our status is collapsed, then clicking on it should uncollapse it.
+  //  If `Shift` is held, then clicking on it should collapse it.
+  //  Otherwise, we open the url handed to us in `destination`, if
+  //  applicable.
+  parseClick = (e, destination) => {
+    const { status, history } = this.props;
+    const { isCollapsed } = this.state;
+    if (!history) return;
+
+    if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey)) {
+      if (isCollapsed) this.setCollapsed(false);
+      else if (e.shiftKey) {
+        this.setCollapsed(true);
+        document.getSelection().removeAllRanges();
+      } else if (this.props.onClick) {
+        this.props.onClick();
+        return;
+      } else {
+        if (destination === undefined) {
+          destination = `/@${
+            status.getIn(['reblog', 'account', 'acct'], status.getIn(['account', 'acct']))
+          }/${
+            status.getIn(['reblog', 'id'], status.get('id'))
+          }`;
+        }
+        history.push(destination);
+      }
+      e.preventDefault();
+    }
+  };
+
+  handleToggleMediaVisibility = () => {
+    this.setState({ showMedia: !this.state.showMedia });
+  };
+
+  handleExpandedToggle = () => {
+    if (this.props.settings.getIn(['content_warnings', 'shared_state'])) {
+      this.props.onToggleHidden(this.props.status);
+    } else if (this.props.status.get('spoiler_text')) {
+      this.setExpansion(!this.state.isExpanded);
+    }
+  };
+
+  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);
+  };
+
+  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);
+  };
+
+  handleHotkeyOpenMedia = e => {
+    const { status, onOpenMedia, onOpenVideo } = this.props;
+    const statusId = status.get('id');
+
+    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 });
+      } else {
+        onOpenMedia(statusId, status.get('media_attachments'), 0, lang);
+      }
+    }
+  };
+
+  handleDeployPictureInPicture = (type, mediaProps) => {
+    const { deployPictureInPicture, status } = this.props;
+
+    deployPictureInPicture(status, type, mediaProps);
+  };
+
+  handleHotkeyReply = e => {
+    e.preventDefault();
+    this.props.onReply(this.props.status, this.props.history);
+  };
+
+  handleHotkeyFavourite = (e) => {
+    this.props.onFavourite(this.props.status, e);
+  };
+
+  handleHotkeyBoost = e => {
+    this.props.onReblog(this.props.status, e);
+  };
+
+  handleHotkeyBookmark = e => {
+    this.props.onBookmark(this.props.status, e);
+  };
+
+  handleHotkeyMention = e => {
+    e.preventDefault();
+    this.props.onMention(this.props.status.get('account'), this.props.history);
+  };
+
+  handleHotkeyOpen = () => {
+    const status = this.props.status;
+    this.props.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
+  };
+
+  handleHotkeyOpenProfile = () => {
+    this.props.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
+  };
+
+  handleHotkeyMoveUp = e => {
+    this.props.onMoveUp(this.props.containerId || this.props.id, e.target.getAttribute('data-featured'));
+  };
+
+  handleHotkeyMoveDown = e => {
+    this.props.onMoveDown(this.props.containerId || this.props.id, e.target.getAttribute('data-featured'));
+  };
+
+  handleHotkeyCollapse = () => {
+    if (!this.props.settings.getIn(['collapsed', 'enabled']))
+      return;
+
+    this.setCollapsed(!this.state.isCollapsed);
+  };
+
+  handleHotkeyToggleSensitive = () => {
+    this.handleToggleMediaVisibility();
+  };
+
+  handleUnfilterClick = e => {
+    this.setState({ forceFilter: false });
+    e.preventDefault();
+  };
+
+  handleFilterClick = () => {
+    this.setState({ forceFilter: true });
+  };
+
+  handleRef = c => {
+    this.node = c;
+  };
+
+  handleTranslate = () => {
+    this.props.onTranslate(this.props.status);
+  };
+
+  renderLoadingMediaGallery () {
+    return <div className='media-gallery' style={{ height: '110px' }} />;
+  }
+
+  renderLoadingVideoPlayer () {
+    return <div className='video-player' style={{ height: '110px' }} />;
+  }
+
+  renderLoadingAudioPlayer () {
+    return <div className='audio-player' style={{ height: '110px' }} />;
+  }
+
+  render () {
+    const { signedIn } = this.context.identity;
+    const {
+      handleRef,
+      parseClick,
+      setCollapsed,
+    } = this;
+    const {
+      intl,
+      status,
+      account,
+      settings,
+      collapsed,
+      muted,
+      intersectionObserverWrapper,
+      onOpenVideo,
+      onOpenMedia,
+      notification,
+      hidden,
+      unread,
+      featured,
+      pictureInPicture,
+      previousId,
+      nextInReplyToId,
+      rootId,
+      history,
+      ...other
+    } = this.props;
+    const { isCollapsed } = this.state;
+    let background = null;
+    let attachments = null;
+
+    //  Depending on user settings, some media are considered as parts of the
+    //  contents (affected by CW) while other will be displayed outside of the
+    //  CW.
+    let contentMedia = [];
+    let contentMediaIcons = [];
+    let extraMedia = [];
+    let extraMediaIcons = [];
+    let media = contentMedia;
+    let mediaIcons = contentMediaIcons;
+
+    if (settings.getIn(['content_warnings', 'media_outside'])) {
+      media = extraMedia;
+      mediaIcons = extraMediaIcons;
+    }
+
+    if (status === null) {
+      return null;
+    }
+
+    const isExpanded = settings.getIn(['content_warnings', 'shared_state']) ? !status.get('hidden') : this.state.isExpanded;
+
+    const handlers = {
+      reply: this.handleHotkeyReply,
+      favourite: this.handleHotkeyFavourite,
+      boost: this.handleHotkeyBoost,
+      mention: this.handleHotkeyMention,
+      open: this.handleHotkeyOpen,
+      openProfile: this.handleHotkeyOpenProfile,
+      moveUp: this.handleHotkeyMoveUp,
+      moveDown: this.handleHotkeyMoveDown,
+      toggleHidden: this.handleExpandedToggle,
+      bookmark: this.handleHotkeyBookmark,
+      toggleCollapse: this.handleHotkeyCollapse,
+      toggleSensitive: this.handleHotkeyToggleSensitive,
+      openMedia: this.handleHotkeyOpenMedia,
+    };
+
+    let prepend, rebloggedByText;
+
+    if (hidden) {
+      return (
+        <HotKeys handlers={handlers}>
+          <div ref={this.handleRef} className='status focusable' tabIndex={0}>
+            <span>{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}</span>
+            <span>{status.get('content')}</span>
+          </div>
+        </HotKeys>
+      );
+    }
+
+    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,
+        moveDown: this.handleHotkeyMoveDown,
+      };
+
+      return (
+        <HotKeys handlers={minHandlers}>
+          <div className='status__wrapper status__wrapper--filtered focusable' tabIndex={0} ref={this.handleRef}>
+            <FormattedMessage id='status.filtered' defaultMessage='Filtered' />: {matchedFilters.join(', ')}.
+            {' '}
+            <button className='status__wrapper--filtered__button' onClick={this.handleUnfilterClick}>
+              <FormattedMessage id='status.show_filter_reason' defaultMessage='Show anyway' />
+            </button>
+          </div>
+        </HotKeys>
+      );
+    }
+
+    //  If user backgrounds for collapsed statuses are enabled, then we
+    //  initialize our background accordingly. This will only be rendered if
+    //  the status is collapsed.
+    if (settings.getIn(['collapsed', 'backgrounds', 'user_backgrounds'])) {
+      background = status.getIn(['account', 'header']);
+    }
+
+    //  This handles our media attachments.
+    //  If a media file is of unknwon type or if the status is muted
+    //  (notification), we show a list of links instead of embedded media.
+
+    //  After we have generated our appropriate media element and stored it in
+    //  `media`, we snatch the thumbnail to use as our `background` if media
+    //  backgrounds for collapsed statuses are enabled.
+
+    attachments = status.get('media_attachments');
+
+    if (pictureInPicture.get('inUse')) {
+      media.push(<PictureInPicturePlaceholder />);
+      mediaIcons.push('video-camera');
+    } else if (attachments.size > 0) {
+      const language = status.getIn(['translation', 'language']) || status.get('language');
+
+      if (muted || attachments.some(item => item.get('type') === 'unknown')) {
+        media.push(
+          <AttachmentList
+            compact
+            media={status.get('media_attachments')}
+          />,
+        );
+      } else if (attachments.getIn([0, 'type']) === 'audio') {
+        const attachment = status.getIn(['media_attachments', 0]);
+        const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
+
+        media.push(
+          <Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
+            {Component => (
+              <Component
+                src={attachment.get('url')}
+                alt={description}
+                lang={language}
+                poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
+                backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
+                foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
+                accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
+                duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
+                width={this.props.cachedMediaWidth}
+                height={110}
+                cacheWidth={this.props.cacheMediaWidth}
+                deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
+                sensitive={status.get('sensitive')}
+                blurhash={attachment.get('blurhash')}
+                visible={this.state.showMedia}
+                onToggleVisibility={this.handleToggleMediaVisibility}
+              />
+            )}
+          </Bundle>,
+        );
+        mediaIcons.push('music');
+      } else if (attachments.getIn([0, 'type']) === 'video') {
+        const attachment = status.getIn(['media_attachments', 0]);
+        const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
+
+        media.push(
+          <Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
+            {Component => (<Component
+              preview={attachment.get('preview_url')}
+              frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
+              blurhash={attachment.get('blurhash')}
+              src={attachment.get('url')}
+              alt={description}
+              lang={language}
+              inline
+              sensitive={status.get('sensitive')}
+              letterbox={settings.getIn(['media', 'letterbox'])}
+              fullwidth={!rootId && settings.getIn(['media', 'fullwidth'])}
+              preventPlayback={isCollapsed || !isExpanded}
+              onOpenVideo={this.handleOpenVideo}
+              deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
+              visible={this.state.showMedia}
+              onToggleVisibility={this.handleToggleMediaVisibility}
+            />)}
+          </Bundle>,
+        );
+        mediaIcons.push('video-camera');
+      } else {  //  Media type is 'image' or 'gifv'
+        media.push(
+          <Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
+            {Component => (
+              <Component
+                media={attachments}
+                lang={language}
+                sensitive={status.get('sensitive')}
+                letterbox={settings.getIn(['media', 'letterbox'])}
+                fullwidth={!rootId && settings.getIn(['media', 'fullwidth'])}
+                hidden={isCollapsed || !isExpanded}
+                onOpenMedia={this.handleOpenMedia}
+                cacheWidth={this.props.cacheMediaWidth}
+                defaultWidth={this.props.cachedMediaWidth}
+                visible={this.state.showMedia}
+                onToggleVisibility={this.handleToggleMediaVisibility}
+              />
+            )}
+          </Bundle>,
+        );
+        mediaIcons.push('picture-o');
+      }
+
+      if (!status.get('sensitive') && !(status.get('spoiler_text').length > 0) && settings.getIn(['collapsed', 'backgrounds', 'preview_images'])) {
+        background = attachments.getIn([0, 'preview_url']);
+      }
+    } else if (status.get('card') && settings.get('inline_preview_cards') && !this.props.muted) {
+      media.push(
+        <Card
+          onOpenMedia={this.handleOpenMedia}
+          card={status.get('card')}
+          compact
+          sensitive={status.get('sensitive')}
+        />,
+      );
+      mediaIcons.push('link');
+    }
+
+    if (status.get('poll')) {
+      const language = status.getIn(['translation', 'language']) || status.get('language');
+      contentMedia.push(<PollContainer pollId={status.get('poll')} lang={language} />);
+      contentMediaIcons.push('tasks');
+    }
+
+    //  Here we prepare extra data-* attributes for CSS selectors.
+    //  Users can use those for theming, hiding avatars etc via UserStyle
+    const selectorAttribs = {
+      'data-status-by': `@${status.getIn(['account', 'acct'])}`,
+    };
+
+    if (this.props.prepend && account) {
+      const notifKind = {
+        favourite: 'favourited',
+        reaction: 'reacted',
+        reblog: 'boosted',
+        reblogged_by: 'boosted',
+        status: 'posted',
+      }[this.props.prepend];
+
+      selectorAttribs[`data-${notifKind}-by`] = `@${account.get('acct')}`;
+
+      prepend = (
+        <StatusPrepend
+          type={this.props.prepend}
+          account={account}
+          parseClick={parseClick}
+          notificationId={this.props.notificationId}
+        />
+      );
+    }
+
+    if (this.props.prepend === 'reblog') {
+      rebloggedByText = intl.formatMessage({ id: 'status.reblogged_by', defaultMessage: '{name} boosted' }, { name: account.get('acct') });
+    }
+
+    const computedClass = classNames('status', `status-${status.get('visibility')}`, {
+      collapsed: isCollapsed,
+      'has-background': isCollapsed && background,
+      'status__wrapper-reply': !!status.get('in_reply_to_id'),
+      'status--in-thread': !!rootId,
+      'status--first-in-thread': previousId && (!connectUp || connectToRoot),
+      unread,
+      muted,
+    }, 'focusable');
+
+    const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
+    contentMedia.push(hashtagBar);
+
+    return (
+      <HotKeys handlers={handlers}>
+        <div
+          className={computedClass}
+          style={isCollapsed && background ? { backgroundImage: `url(${background})` } : null}
+          {...selectorAttribs}
+          ref={handleRef}
+          tabIndex={0}
+          data-featured={featured ? 'true' : null}
+          aria-label={textForScreenReader(intl, status, rebloggedByText, !status.get('hidden'))}
+          data-nosnippet={status.getIn(['account', 'noindex'], true) || undefined}
+        >
+          {!muted && prepend}
+
+          {(connectReply || connectUp || connectToRoot) && <div className={classNames('status__line', { 'status__line--full': connectReply, 'status__line--first': !status.get('in_reply_to_id') && !connectToRoot })} />}
+
+          <header className='status__info'>
+            <span>
+              {muted && prepend}
+              {!muted || !isCollapsed ? (
+                <StatusHeader
+                  status={status}
+                  friend={account}
+                  collapsed={isCollapsed}
+                  parseClick={parseClick}
+                />
+              ) : null}
+            </span>
+            <StatusIcons
+              status={status}
+              mediaIcons={contentMediaIcons.concat(extraMediaIcons)}
+              collapsible={settings.getIn(['collapsed', 'enabled'])}
+              collapsed={isCollapsed}
+              setCollapsed={setCollapsed}
+              settings={settings.get('status_icons')}
+            />
+          </header>
+          <StatusContent
+            status={status}
+            media={contentMedia}
+            extraMedia={extraMedia}
+            mediaIcons={contentMediaIcons}
+            expanded={isExpanded}
+            onExpandedToggle={this.handleExpandedToggle}
+            onTranslate={this.handleTranslate}
+            parseClick={parseClick}
+            disabled={!history}
+            tagLinks={settings.get('tag_misleading_links')}
+            rewriteMentions={settings.get('rewrite_mentions')}
+            {...statusContentProps}
+          />
+
+          <StatusReactions
+            statusId={status.get('id')}
+            reactions={status.get('reactions')}
+            numVisible={visibleReactions}
+            addReaction={this.props.onReactionAdd}
+            removeReaction={this.props.onReactionRemove}
+            canReact={this.context.identity.signedIn}
+          />
+
+          {!isCollapsed || !(muted || !settings.getIn(['collapsed', 'show_action_bar'])) ? (
+            <StatusActionBar
+              status={status}
+              account={status.get('account')}
+              showReplyCount={settings.get('show_reply_count')}
+              onFilter={matchedFilters ? this.handleFilterClick : null}
+              {...other}
+            />
+          ) : null}
+          {notification ? (
+            <NotificationOverlayContainer
+              notification={notification}
+            />
+          ) : null}
+        </div>
+      </HotKeys>
+    );
+  }
+
+}
+
+export default withOptionalRouter(injectIntl(Status));
diff --git a/app/javascript/flavours/blobfox/components/status_action_bar.jsx b/app/javascript/flavours/blobfox/components/status_action_bar.jsx
new file mode 100644
index 00000000000000..e5f79c84ca3a90
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/status_action_bar.jsx
@@ -0,0 +1,371 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl } from 'react-intl';
+
+import classNames from 'classnames';
+import { withRouter } from 'react-router-dom';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/blobfox/permissions';
+import { accountAdminLink, statusAdminLink } from 'flavours/blobfox/utils/backend_links';
+import { WithRouterPropTypes } from 'flavours/blobfox/utils/react_router';
+
+import DropdownMenuContainer from '../containers/dropdown_menu_container';
+import EmojiPickerDropdown from '../features/compose/containers/emoji_picker_dropdown_container';
+import { me, maxReactions } from '../initial_state';
+
+import { IconButton } from './icon_button';
+import { RelativeTimestamp } from './relative_timestamp';
+
+const messages = defineMessages({
+  delete: { id: 'status.delete', defaultMessage: 'Delete' },
+  redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
+  edit: { id: 'status.edit', defaultMessage: 'Edit' },
+  direct: { id: 'status.direct', defaultMessage: 'Privately mention @{name}' },
+  mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
+  mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
+  block: { id: 'account.block', defaultMessage: 'Block @{name}' },
+  reply: { id: 'status.reply', defaultMessage: 'Reply' },
+  share: { id: 'status.share', defaultMessage: 'Share' },
+  more: { id: 'status.more', defaultMessage: 'More' },
+  replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
+  reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
+  reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
+  cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
+  cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
+  favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
+  react: { id: 'status.react', defaultMessage: 'React' },
+  bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
+  open: { id: 'status.open', defaultMessage: 'Expand this status' },
+  report: { id: 'status.report', defaultMessage: 'Report @{name}' },
+  muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
+  unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
+  pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
+  unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
+  embed: { id: 'status.embed', defaultMessage: 'Embed' },
+  admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
+  admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' },
+  admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },
+  copy: { id: 'status.copy', defaultMessage: 'Copy link to post' },
+  hide: { id: 'status.hide', defaultMessage: 'Hide post' },
+  edited: { id: 'status.edited', defaultMessage: 'Edited {date}' },
+  filter: { id: 'status.filter', defaultMessage: 'Filter this post' },
+  openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' },
+});
+
+class StatusActionBar extends ImmutablePureComponent {
+
+  static contextTypes = {
+    identity: PropTypes.object,
+  };
+
+  static propTypes = {
+    status: ImmutablePropTypes.map.isRequired,
+    onReply: PropTypes.func,
+    onFavourite: PropTypes.func,
+    onReactionAdd: PropTypes.func,
+    onReblog: PropTypes.func,
+    onDelete: PropTypes.func,
+    onDirect: PropTypes.func,
+    onMention: PropTypes.func,
+    onMute: PropTypes.func,
+    onBlock: PropTypes.func,
+    onReport: PropTypes.func,
+    onEmbed: PropTypes.func,
+    onMuteConversation: PropTypes.func,
+    onPin: PropTypes.func,
+    onBookmark: PropTypes.func,
+    onFilter: PropTypes.func,
+    onAddFilter: PropTypes.func,
+    onInteractionModal: PropTypes.func,
+    withDismiss: PropTypes.bool,
+    withCounters: PropTypes.bool,
+    showReplyCount: PropTypes.bool,
+    scrollKey: PropTypes.string,
+    intl: PropTypes.object.isRequired,
+    ...WithRouterPropTypes,
+  };
+
+  // Avoid checking props that are functions (and whose equality will always
+  // evaluate to false. See react-immutable-pure-component for usage.
+  updateOnProps = [
+    'status',
+    'showReplyCount',
+    'withCounters',
+    'withDismiss',
+  ];
+
+  handleReplyClick = () => {
+    const { signedIn } = this.context.identity;
+
+    if (signedIn) {
+      this.props.onReply(this.props.status, this.props.history);
+    } else {
+      this.props.onInteractionModal('reply', this.props.status);
+    }
+  };
+
+  handleShareClick = () => {
+    navigator.share({
+      url: this.props.status.get('url'),
+    });
+  };
+
+  handleFavouriteClick = (e) => {
+    const { signedIn } = this.context.identity;
+
+    if (signedIn) {
+      this.props.onFavourite(this.props.status, e);
+    } else {
+      this.props.onInteractionModal('favourite', this.props.status);
+    }
+  };
+
+  handleEmojiPick = data => {
+    this.props.onReactionAdd(this.props.status.get('id'), data.native.replace(/:/g, ''), data.imageUrl);
+  };
+
+  handleReblogClick = e => {
+    const { signedIn } = this.context.identity;
+
+    if (signedIn) {
+      this.props.onReblog(this.props.status, e);
+    } else {
+      this.props.onInteractionModal('reblog', this.props.status);
+    }
+  };
+
+  handleBookmarkClick = (e) => {
+    this.props.onBookmark(this.props.status, e);
+  };
+
+  handleDeleteClick = () => {
+    this.props.onDelete(this.props.status, this.props.history);
+  };
+
+  handleRedraftClick = () => {
+    this.props.onDelete(this.props.status, this.props.history, true);
+  };
+
+  handleEditClick = () => {
+    this.props.onEdit(this.props.status, this.props.history);
+  };
+
+  handlePinClick = () => {
+    this.props.onPin(this.props.status);
+  };
+
+  handleMentionClick = () => {
+    this.props.onMention(this.props.status.get('account'), this.props.history);
+  };
+
+  handleDirectClick = () => {
+    this.props.onDirect(this.props.status.get('account'), this.props.history);
+  };
+
+  handleMuteClick = () => {
+    this.props.onMute(this.props.status.get('account'));
+  };
+
+  handleBlockClick = () => {
+    this.props.onBlock(this.props.status);
+  };
+
+  handleOpen = () => {
+    this.props.history.push(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}`);
+  };
+
+  handleEmbed = () => {
+    this.props.onEmbed(this.props.status);
+  };
+
+  handleReport = () => {
+    this.props.onReport(this.props.status);
+  };
+
+  handleConversationMuteClick = () => {
+    this.props.onMuteConversation(this.props.status);
+  };
+
+  handleCopy = () => {
+    const url = this.props.status.get('url');
+    navigator.clipboard.writeText(url);
+  };
+
+  handleHideClick = () => {
+    this.props.onFilter();
+  };
+
+  handleFilterClick = () => {
+    this.props.onAddFilter(this.props.status);
+  };
+
+  handleNoOp = () => {}; // hack for reaction add button
+
+  render () {
+    const { status, intl, withDismiss, withCounters, showReplyCount, scrollKey } = this.props;
+    const { permissions, signedIn } = this.context.identity;
+
+    const mutingConversation = status.get('muted');
+    const publicStatus       = ['public', 'unlisted'].includes(status.get('visibility'));
+    const pinnableStatus     = ['public', 'unlisted', 'private'].includes(status.get('visibility'));
+    const writtenByMe        = status.getIn(['account', 'id']) === me;
+    const isRemote           = status.getIn(['account', 'username']) !== status.getIn(['account', 'acct']);
+
+    let menu = [];
+    let reblogIcon = 'retweet';
+    let replyIcon;
+    let replyTitle;
+
+    menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
+
+    if (publicStatus && isRemote) {
+      menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: status.get('url') });
+    }
+
+    menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy });
+
+    if (publicStatus && 'share' in navigator) {
+      menu.push({ text: intl.formatMessage(messages.share), action: this.handleShareClick });
+    }
+
+    if (publicStatus && (signedIn || !isRemote)) {
+      menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
+    }
+
+    if (signedIn) {
+      menu.push(null);
+
+      if (writtenByMe && pinnableStatus) {
+        menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
+        menu.push(null);
+      }
+
+      if (writtenByMe || withDismiss) {
+        menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
+        menu.push(null);
+      }
+
+      if (writtenByMe) {
+        menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
+        menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick, dangerous: true });
+        menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick, dangerous: true });
+      } else {
+        menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
+        menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick });
+        menu.push(null);
+
+        if (!this.props.onFilter) {
+          menu.push({ text: intl.formatMessage(messages.filter), action: this.handleFilterClick, dangerous: true });
+          menu.push(null);
+        }
+
+        menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick, dangerous: true });
+        menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick, dangerous: true });
+        menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport, dangerous: true });
+
+        if (((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && (accountAdminLink || statusAdminLink)) || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) {
+          menu.push(null);
+          if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) {
+            if (accountAdminLink !== undefined) {
+              menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: accountAdminLink(status.getIn(['account', 'id'])) });
+            }
+            if (statusAdminLink !== undefined) {
+              menu.push({ text: intl.formatMessage(messages.admin_status), href: statusAdminLink(status.getIn(['account', 'id']), status.get('id')) });
+            }
+          }
+          if (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) {
+            const domain = status.getIn(['account', 'acct']).split('@')[1];
+            menu.push({ text: intl.formatMessage(messages.admin_domain, { domain: domain }), href: `/admin/instances/${domain}` });
+          }
+        }
+      }
+    }
+
+    if (status.get('in_reply_to_id', null) === null) {
+      replyIcon = 'reply';
+      replyTitle = intl.formatMessage(messages.reply);
+    } else {
+      replyIcon = 'reply-all';
+      replyTitle = intl.formatMessage(messages.replyAll);
+    }
+
+    const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
+
+    let reblogTitle = '';
+    if (status.get('reblogged')) {
+      reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
+    } else if (publicStatus) {
+      reblogTitle = intl.formatMessage(messages.reblog);
+    } else if (reblogPrivate) {
+      reblogTitle = intl.formatMessage(messages.reblog_private);
+    } else {
+      reblogTitle = intl.formatMessage(messages.cannot_reblog);
+    }
+
+    const filterButton = this.props.onFilter && (
+      <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.hide)} icon='eye' onClick={this.handleHideClick} />
+    );
+
+    const canReact = permissions && status.get('reactions').filter(r => r.get('count') > 0 && r.get('me')).size < maxReactions;
+    const reactButton = (
+      <IconButton
+        className='status__action-bar-button'
+        onClick={this.handleNoOp} // EmojiPickerDropdown handles that
+        title={intl.formatMessage(messages.react)}
+        disabled={!canReact}
+        icon='plus'
+      />
+    );
+
+    return (
+      <div className='status__action-bar'>
+        <IconButton
+          className='status__action-bar-button'
+          title={replyTitle}
+          icon={replyIcon}
+          onClick={this.handleReplyClick}
+          counter={showReplyCount ? status.get('replies_count') : undefined}
+          obfuscateCount
+        />
+        <IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon={reblogIcon} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
+        <IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
+        {
+          permissions
+            ? <EmojiPickerDropdown className='status__action-bar-button' onPickEmoji={this.handleEmojiPick} button={reactButton} disabled={!canReact} />
+            : reactButton
+        }
+        <IconButton className='status__action-bar-button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />
+        {
+          permissions
+            ? <EmojiPickerDropdown className='status__action-bar-button' onPickEmoji={this.handleEmojiPick} button={reactButton} disabled={!canReact} />
+            : reactButton
+        }
+
+        {filterButton}
+
+        <div className='status__action-bar-dropdown'>
+          <DropdownMenuContainer
+            scrollKey={scrollKey}
+            status={status}
+            items={menu}
+            icon='ellipsis-h'
+            size={18}
+            direction='right'
+            ariaLabel={intl.formatMessage(messages.more)}
+          />
+        </div>
+
+        <div className='status__action-bar-spacer' />
+        <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'>
+          <RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { hour12: false, year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>}
+        </a>
+      </div>
+    );
+  }
+
+}
+
+export default withRouter(injectIntl(StatusActionBar));
diff --git a/app/javascript/flavours/blobfox/components/status_content.jsx b/app/javascript/flavours/blobfox/components/status_content.jsx
new file mode 100644
index 00000000000000..85df1b58385828
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/status_content.jsx
@@ -0,0 +1,491 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { FormattedMessage, injectIntl } from 'react-intl';
+
+import classnames from 'classnames';
+import { withRouter } from 'react-router-dom';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
+
+import { Icon } from 'flavours/blobfox/components/icon';
+import { autoPlayGif, languages as preloadedLanguages } from 'flavours/blobfox/initial_state';
+import { decode as decodeIDNA } from 'flavours/blobfox/utils/idna';
+
+import Permalink from './permalink';
+
+const textMatchesTarget = (text, origin, host) => {
+  return (text === origin || text === host
+          || text.startsWith(origin + '/') || text.startsWith(host + '/')
+          || 'www.' + text === host || ('www.' + text).startsWith(host + '/'));
+};
+
+const isLinkMisleading = (link) => {
+  let linkTextParts = [];
+
+  // Reconstruct visible text, as we do not have much control over how links
+  // from remote software look, and we can't rely on `innerText` because the
+  // `invisible` class does not set `display` to `none`.
+
+  const walk = (node) => {
+    switch (node.nodeType) {
+    case Node.TEXT_NODE:
+      linkTextParts.push(node.textContent);
+      break;
+    case Node.ELEMENT_NODE:
+      if (node.classList.contains('invisible')) return;
+      const children = node.childNodes;
+      for (let i = 0; i < children.length; i++) {
+        walk(children[i]);
+      }
+      break;
+    }
+  };
+
+  walk(link);
+
+  const linkText = linkTextParts.join('');
+  const targetURL = new URL(link.href);
+
+  if (targetURL.protocol === 'magnet:') {
+    return !linkText.startsWith('magnet:');
+  }
+
+  if (targetURL.protocol === 'xmpp:') {
+    return !(linkText === targetURL.href || 'xmpp:' + linkText === targetURL.href);
+  }
+
+  // The following may not work with international domain names
+  if (textMatchesTarget(linkText, targetURL.origin, targetURL.host) || textMatchesTarget(linkText.toLowerCase(), targetURL.origin, targetURL.host)) {
+    return false;
+  }
+
+  // The link hasn't been recognized, maybe it features an international domain name
+  const hostname = decodeIDNA(targetURL.hostname).normalize('NFKC');
+  const host = targetURL.host.replace(targetURL.hostname, hostname);
+  const origin = targetURL.origin.replace(targetURL.host, host);
+  const text = linkText.normalize('NFKC');
+  return !(textMatchesTarget(text, origin, host) || textMatchesTarget(text.toLowerCase(), origin, host));
+};
+
+/**
+ *
+ * @param {any} status
+ * @returns {string}
+ */
+export function getStatusContent(status) {
+  return status.getIn(['translation', 'contentHtml']) || status.get('contentHtml');
+}
+
+class TranslateButton extends PureComponent {
+
+  static propTypes = {
+    translation: ImmutablePropTypes.map,
+    onClick: PropTypes.func,
+  };
+
+  render () {
+    const { translation, onClick } = this.props;
+
+    if (translation) {
+      const language     = preloadedLanguages.find(lang => lang[0] === translation.get('detected_source_language'));
+      const languageName = language ? language[2] : translation.get('detected_source_language');
+      const provider     = translation.get('provider');
+
+      return (
+        <div className='translate-button'>
+          <div className='translate-button__meta'>
+            <FormattedMessage id='status.translated_from_with' defaultMessage='Translated from {lang} using {provider}' values={{ lang: languageName, provider }} />
+          </div>
+
+          <button className='link-button' onClick={onClick}>
+            <FormattedMessage id='status.show_original' defaultMessage='Show original' />
+          </button>
+        </div>
+      );
+    }
+
+    return (
+      <button className='status__content__translate-button' onClick={onClick}>
+        <FormattedMessage id='status.translate' defaultMessage='Translate' />
+      </button>
+    );
+  }
+
+}
+
+const mapStateToProps = state => ({
+  languages: state.getIn(['server', 'translationLanguages', 'items']),
+});
+
+class StatusContent extends PureComponent {
+
+  static contextTypes = {
+    identity: PropTypes.object,
+  };
+
+  static propTypes = {
+    status: ImmutablePropTypes.map.isRequired,
+    statusContent: PropTypes.string,
+    expanded: PropTypes.bool,
+    collapsed: PropTypes.bool,
+    onExpandedToggle: PropTypes.func,
+    onTranslate: PropTypes.func,
+    media: PropTypes.node,
+    extraMedia: PropTypes.node,
+    mediaIcons: PropTypes.arrayOf(PropTypes.string),
+    parseClick: PropTypes.func,
+    disabled: PropTypes.bool,
+    onUpdate: PropTypes.func,
+    tagLinks: PropTypes.bool,
+    rewriteMentions: PropTypes.string,
+    languages: ImmutablePropTypes.map,
+    intl: PropTypes.object,
+    // from react-router
+    match: PropTypes.object.isRequired,
+    location: PropTypes.object.isRequired,
+    history: PropTypes.object.isRequired
+  };
+
+  static defaultProps = {
+    tagLinks: true,
+    rewriteMentions: 'no',
+  };
+
+  state = {
+    hidden: true,
+  };
+
+  _updateStatusLinks () {
+    const node = this.contentsNode;
+    const { tagLinks, rewriteMentions } = this.props;
+
+    if (!node) {
+      return;
+    }
+
+    const links = node.querySelectorAll('a');
+
+    for (var i = 0; i < links.length; ++i) {
+      let link = links[i];
+      if (link.classList.contains('status-link')) {
+        continue;
+      }
+      link.classList.add('status-link');
+
+      let mention = this.props.status.get('mentions').find(item => link.href === item.get('url'));
+
+      if (mention) {
+        link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
+        link.setAttribute('title', `@${mention.get('acct')}`);
+        if (rewriteMentions !== 'no') {
+          while (link.firstChild) link.removeChild(link.firstChild);
+          link.appendChild(document.createTextNode('@'));
+          const acctSpan = document.createElement('span');
+          acctSpan.textContent = rewriteMentions === 'acct' ? mention.get('acct') : mention.get('username');
+          link.appendChild(acctSpan);
+        }
+      } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
+        link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
+      } else {
+        link.addEventListener('click', this.onLinkClick.bind(this), false);
+        link.setAttribute('title', link.href);
+        link.classList.add('unhandled-link');
+
+        link.setAttribute('target', '_blank');
+        link.setAttribute('rel', 'noopener nofollow noreferrer');
+
+        try {
+          if (tagLinks && isLinkMisleading(link)) {
+            // Add a tag besides the link to display its origin
+
+            const url = new URL(link.href);
+            const tag = document.createElement('span');
+            tag.classList.add('link-origin-tag');
+            switch (url.protocol) {
+            case 'xmpp:':
+              tag.textContent = `[${url.href}]`;
+              break;
+            case 'magnet:':
+              tag.textContent = '(magnet)';
+              break;
+            default:
+              tag.textContent = `[${url.host}]`;
+            }
+            link.insertAdjacentText('beforeend', ' ');
+            link.insertAdjacentElement('beforeend', tag);
+          }
+        } catch (e) {
+          // The URL is invalid, remove the href just to be safe
+          if (tagLinks && e instanceof TypeError) link.removeAttribute('href');
+        }
+      }
+    }
+  }
+
+  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');
+    }
+  };
+
+  componentDidMount () {
+    this._updateStatusLinks();
+  }
+
+  componentDidUpdate () {
+    this._updateStatusLinks();
+    if (this.props.onUpdate) this.props.onUpdate();
+  }
+
+  onLinkClick = (e) => {
+    if (this.props.collapsed) {
+      if (this.props.parseClick) this.props.parseClick(e);
+    }
+  };
+
+  onMentionClick = (mention, e) => {
+    if (this.props.parseClick) {
+      this.props.parseClick(e, `/@${mention.get('acct')}`);
+    }
+  };
+
+  onHashtagClick = (hashtag, e) => {
+    hashtag = hashtag.replace(/^#/, '');
+
+    if (this.props.parseClick) {
+      this.props.parseClick(e, `/tags/${hashtag}`);
+    }
+  };
+
+  handleMouseDown = (e) => {
+    this.startXY = [e.clientX, e.clientY];
+  };
+
+  handleMouseUp = (e) => {
+    const { parseClick, disabled } = this.props;
+
+    if (disabled || !this.startXY) {
+      return;
+    }
+
+    const [ startX, startY ] = this.startXY;
+    const [ deltaX, deltaY ] = [Math.abs(e.clientX - startX), Math.abs(e.clientY - startY)];
+
+    let element = e.target;
+    while (element !== e.currentTarget) {
+      if (['button', 'video', 'a', 'label', 'canvas'].includes(element.localName) || element.getAttribute('role') === 'button') {
+        return;
+      }
+      element = element.parentNode;
+    }
+
+    if (deltaX + deltaY < 5 && e.button === 0 && parseClick) {
+      parseClick(e);
+    }
+
+    this.startXY = null;
+  };
+
+  handleSpoilerClick = (e) => {
+    e.preventDefault();
+
+    if (this.props.onExpandedToggle) {
+      this.props.onExpandedToggle();
+    } else {
+      this.setState({ hidden: !this.state.hidden });
+    }
+  };
+
+  handleTranslate = () => {
+    this.props.onTranslate();
+  };
+
+  setContentsRef = (c) => {
+    this.contentsNode = c;
+  };
+
+  render () {
+    const {
+      status,
+      media,
+      extraMedia,
+      mediaIcons,
+      parseClick,
+      disabled,
+      tagLinks,
+      rewriteMentions,
+      intl,
+      statusContent,
+    } = this.props;
+
+    const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
+    const contentLocale = intl.locale.replace(/[_-].*/, '');
+    const targetLanguages = this.props.languages?.get(status.get('language') || 'und');
+    const renderTranslate = this.props.onTranslate && this.context.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('search_index').trim().length > 0 && targetLanguages?.includes(contentLocale);
+
+    const content = { __html: statusContent ?? getStatusContent(status) };
+    const spoilerContent = { __html: status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml') };
+    const language = status.getIn(['translation', 'language']) || status.get('language');
+    const classNames = classnames('status__content', {
+      'status__content--with-action': parseClick && !disabled,
+      'status__content--with-spoiler': status.get('spoiler_text').length > 0,
+    });
+
+    const translateButton = renderTranslate && (
+      <TranslateButton onClick={this.handleTranslate} translation={status.get('translation')} />
+    );
+
+    if (status.get('spoiler_text').length > 0) {
+      let mentionsPlaceholder = '';
+
+      const mentionLinks = status.get('mentions').map(item => (
+        <Permalink
+          to={`/@${item.get('acct')}`}
+          href={item.get('url')}
+          key={item.get('id')}
+          className='mention'
+        >
+          @<span>{item.get('username')}</span>
+        </Permalink>
+      )).reduce((aggregate, item) => [...aggregate, item, ' '], []);
+
+      let toggleText = null;
+      if (hidden) {
+        toggleText = [
+          <FormattedMessage
+            id='status.show_more'
+            defaultMessage='Show more'
+            key='0'
+          />,
+        ];
+        if (mediaIcons) {
+          mediaIcons.forEach((mediaIcon, idx) => {
+            toggleText.push(
+              <Icon
+                fixedWidth
+                className='status__content__spoiler-icon'
+                id={mediaIcon}
+                aria-hidden='true'
+                key={`icon-${idx}`}
+              />,
+            );
+          });
+        }
+      } else {
+        toggleText = (
+          <FormattedMessage
+            id='status.show_less'
+            defaultMessage='Show less'
+            key='0'
+          />
+        );
+      }
+
+      if (hidden) {
+        mentionsPlaceholder = <div>{mentionLinks}</div>;
+      }
+
+      return (
+        <div className={classNames} tabIndex={0} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
+          <p
+            style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}
+          >
+            <span dangerouslySetInnerHTML={spoilerContent} className='translate' lang={language} />
+            {' '}
+            <button type='button' className='status__content__spoiler-link' onClick={this.handleSpoilerClick} aria-expanded={!hidden}>
+              {toggleText}
+            </button>
+          </p>
+
+          {mentionsPlaceholder}
+
+          <div className={`status__content__spoiler ${!hidden ? 'status__content__spoiler--visible' : ''}`}>
+            <div
+              ref={this.setContentsRef}
+              key={`contents-${tagLinks}`}
+              tabIndex={!hidden ? 0 : null}
+              dangerouslySetInnerHTML={content}
+              className='status__content__text translate'
+              onMouseEnter={this.handleMouseEnter}
+              onMouseLeave={this.handleMouseLeave}
+              lang={language}
+            />
+            {!hidden && translateButton}
+            {media}
+          </div>
+
+          {extraMedia}
+        </div>
+      );
+    } else if (parseClick) {
+      return (
+        <div
+          className={classNames}
+          onMouseDown={this.handleMouseDown}
+          onMouseUp={this.handleMouseUp}
+          tabIndex={0}
+        >
+          <div
+            ref={this.setContentsRef}
+            key={`contents-${tagLinks}-${rewriteMentions}`}
+            dangerouslySetInnerHTML={content}
+            className='status__content__text translate'
+            tabIndex={0}
+            onMouseEnter={this.handleMouseEnter}
+            onMouseLeave={this.handleMouseLeave}
+            lang={language}
+          />
+          {translateButton}
+          {media}
+          {extraMedia}
+        </div>
+      );
+    } else {
+      return (
+        <div
+          className='status__content'
+          tabIndex={0}
+        >
+          <div
+            ref={this.setContentsRef}
+            key={`contents-${tagLinks}`}
+            className='status__content__text translate'
+            dangerouslySetInnerHTML={content}
+            tabIndex={0}
+            onMouseEnter={this.handleMouseEnter}
+            onMouseLeave={this.handleMouseLeave}
+            lang={language}
+          />
+          {translateButton}
+          {media}
+          {extraMedia}
+        </div>
+      );
+    }
+  }
+
+}
+
+export default withRouter(connect(mapStateToProps)(injectIntl(StatusContent)));
diff --git a/app/javascript/flavours/blobfox/components/status_header.jsx b/app/javascript/flavours/blobfox/components/status_header.jsx
new file mode 100644
index 00000000000000..1c51707cefaa9c
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/status_header.jsx
@@ -0,0 +1,71 @@
+//  Package imports.
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
+//  Mastodon imports.
+import { Avatar } from './avatar';
+import { AvatarOverlay } from './avatar_overlay';
+import { DisplayName } from './display_name';
+
+export default class StatusHeader extends PureComponent {
+
+  static propTypes = {
+    status: ImmutablePropTypes.map.isRequired,
+    friend: ImmutablePropTypes.map,
+    parseClick: PropTypes.func.isRequired,
+  };
+
+  //  Handles clicks on account name/image
+  handleClick = (acct, e) => {
+    const { parseClick } = this.props;
+    parseClick(e, `/@${acct}`);
+  };
+
+  handleAccountClick = (e) => {
+    const { status } = this.props;
+    this.handleClick(status.getIn(['account', 'acct']), e);
+  };
+
+  //  Rendering.
+  render () {
+    const {
+      status,
+      friend,
+    } = this.props;
+
+    const account = status.get('account');
+
+    let statusAvatar;
+    if (friend === undefined || friend === null) {
+      statusAvatar = <Avatar account={account} size={46} />;
+    } else {
+      statusAvatar = <AvatarOverlay account={account} friend={friend} />;
+    }
+
+    return (
+      <div className='status__info__account'>
+        <a
+          href={account.get('url')}
+          target='_blank'
+          className='status__avatar'
+          onClick={this.handleAccountClick}
+          rel='noopener noreferrer'
+        >
+          {statusAvatar}
+        </a>
+        <a
+          href={account.get('url')}
+          target='_blank'
+          className='status__display-name'
+          onClick={this.handleAccountClick}
+          rel='noopener noreferrer'
+        >
+          <DisplayName account={account} />
+        </a>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/components/status_icons.jsx b/app/javascript/flavours/blobfox/components/status_icons.jsx
new file mode 100644
index 00000000000000..34b4ad6e5885a4
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/status_icons.jsx
@@ -0,0 +1,147 @@
+//  Package imports.
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, injectIntl } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
+
+//  Mastodon imports.
+import { Icon } from 'flavours/blobfox/components/icon';
+import { languages } from 'flavours/blobfox/initial_state';
+
+import { IconButton } from './icon_button';
+import VisibilityIcon from './status_visibility_icon';
+
+//  Messages for use with internationalization stuff.
+const messages = defineMessages({
+  collapse: { id: 'status.collapse', defaultMessage: 'Collapse' },
+  uncollapse: { id: 'status.uncollapse', defaultMessage: 'Uncollapse' },
+  inReplyTo: { id: 'status.in_reply_to', defaultMessage: 'This toot is a reply' },
+  previewCard: { id: 'status.has_preview_card', defaultMessage: 'Features an attached preview card' },
+  pictures: { id: 'status.has_pictures', defaultMessage: 'Features attached pictures' },
+  poll: { id: 'status.is_poll', defaultMessage: 'This toot is a poll' },
+  video: { id: 'status.has_video', defaultMessage: 'Features attached videos' },
+  audio: { id: 'status.has_audio', defaultMessage: 'Features attached audio files' },
+  localOnly: { id: 'status.local_only', defaultMessage: 'Only visible from your instance' },
+});
+
+const LanguageIcon = ({ language }) => {
+  if (!languages) return null;
+
+  const lang = languages.find((lang) => lang[0] === language);
+  if (!lang) return null;
+
+  return (
+    <span className='text-icon' title={`${lang[2]} (${lang[1]})`} aria-hidden='true'>
+      {lang[0].toUpperCase()}
+    </span>
+  );
+};
+
+LanguageIcon.propTypes = {
+  language: PropTypes.string.isRequired,
+};
+
+class StatusIcons extends PureComponent {
+
+  static propTypes = {
+    status: ImmutablePropTypes.map.isRequired,
+    mediaIcons: PropTypes.arrayOf(PropTypes.string),
+    collapsible: PropTypes.bool,
+    collapsed: PropTypes.bool,
+    setCollapsed: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+    settings: ImmutablePropTypes.map.isRequired,
+  };
+
+  //  Handles clicks on collapsed button
+  handleCollapsedClick = (e) => {
+    const { collapsed, setCollapsed } = this.props;
+    if (e.button === 0) {
+      setCollapsed(!collapsed);
+      e.preventDefault();
+    }
+  };
+
+  mediaIconTitleText (mediaIcon) {
+    const { intl } = this.props;
+
+    const message = {
+      'link': messages.previewCard,
+      'picture-o': messages.pictures,
+      'tasks': messages.poll,
+      'video-camera': messages.video,
+      'music': messages.audio,
+    }[mediaIcon];
+
+    return message && intl.formatMessage(message);
+  }
+
+  renderIcon (mediaIcon) {
+    return (
+      <Icon
+        fixedWidth
+        className='status__media-icon'
+        key={`media-icon--${mediaIcon}`}
+        id={mediaIcon}
+        aria-hidden='true'
+        title={this.mediaIconTitleText(mediaIcon)}
+      />
+    );
+  }
+
+  //  Rendering.
+  render () {
+    const {
+      status,
+      mediaIcons,
+      collapsible,
+      collapsed,
+      settings,
+      intl,
+    } = this.props;
+
+    return (
+      <div className='status__info__icons'>
+        {settings.get('language') && status.get('language') && <LanguageIcon language={status.get('language')} />}
+        {settings.get('reply') && status.get('in_reply_to_id', null) !== null ? (
+          <Icon
+            className='status__reply-icon'
+            fixedWidth
+            id='comment'
+            aria-hidden='true'
+            title={intl.formatMessage(messages.inReplyTo)}
+          />
+        ) : null}
+        {settings.get('local_only') && status.get('local_only') &&
+          <Icon
+            fixedWidth
+            id='home'
+            aria-hidden='true'
+            title={intl.formatMessage(messages.localOnly)}
+          />}
+        {settings.get('media') && !!mediaIcons && mediaIcons.map(icon => this.renderIcon(icon))}
+        {settings.get('visibility') && <VisibilityIcon visibility={status.get('visibility')} />}
+        {collapsible && (
+          <IconButton
+            className='status__collapse-button'
+            animate
+            active={collapsed}
+            title={
+              collapsed ?
+                intl.formatMessage(messages.uncollapse) :
+                intl.formatMessage(messages.collapse)
+            }
+            icon='angle-double-up'
+            onClick={this.handleCollapsedClick}
+          />
+        )}
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(StatusIcons);
diff --git a/app/javascript/flavours/blobfox/components/status_list.jsx b/app/javascript/flavours/blobfox/components/status_list.jsx
new file mode 100644
index 00000000000000..3f103742f23037
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/status_list.jsx
@@ -0,0 +1,137 @@
+import PropTypes from 'prop-types';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import { debounce } from 'lodash';
+
+import RegenerationIndicator from 'flavours/blobfox/components/regeneration_indicator';
+
+import StatusContainer from '../containers/status_container';
+
+import { LoadGap } from './load_gap';
+import ScrollableList from './scrollable_list';
+
+export default class StatusList extends ImmutablePureComponent {
+
+  static propTypes = {
+    scrollKey: PropTypes.string.isRequired,
+    statusIds: ImmutablePropTypes.list.isRequired,
+    featuredStatusIds: ImmutablePropTypes.list,
+    onLoadMore: PropTypes.func,
+    onScrollToTop: PropTypes.func,
+    onScroll: PropTypes.func,
+    trackScroll: PropTypes.bool,
+    isLoading: PropTypes.bool,
+    isPartial: PropTypes.bool,
+    hasMore: PropTypes.bool,
+    prepend: PropTypes.node,
+    emptyMessage: PropTypes.node,
+    alwaysPrepend: PropTypes.bool,
+    withCounters: PropTypes.bool,
+    timelineId: PropTypes.string.isRequired,
+    lastId: PropTypes.string,
+    regex: PropTypes.string,
+  };
+
+  static defaultProps = {
+    trackScroll: true,
+  };
+
+  getFeaturedStatusCount = () => {
+    return this.props.featuredStatusIds ? this.props.featuredStatusIds.size : 0;
+  };
+
+  getCurrentStatusIndex = (id, featured) => {
+    if (featured) {
+      return this.props.featuredStatusIds.indexOf(id);
+    } else {
+      return this.props.statusIds.indexOf(id) + this.getFeaturedStatusCount();
+    }
+  };
+
+  handleMoveUp = (id, featured) => {
+    const elementIndex = this.getCurrentStatusIndex(id, featured) - 1;
+    this._selectChild(elementIndex, true);
+  };
+
+  handleMoveDown = (id, featured) => {
+    const elementIndex = this.getCurrentStatusIndex(id, featured) + 1;
+    this._selectChild(elementIndex, false);
+  };
+
+  handleLoadOlder = debounce(() => {
+    const { statusIds, lastId, onLoadMore } = this.props;
+    onLoadMore(lastId || (statusIds.size > 0 ? statusIds.last() : undefined));
+  }, 300, { leading: true });
+
+  _selectChild (index, align_top) {
+    const container = this.node.node;
+    const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
+
+    if (element) {
+      if (align_top && container.scrollTop > element.offsetTop) {
+        element.scrollIntoView(true);
+      } else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
+        element.scrollIntoView(false);
+      }
+      element.focus();
+    }
+  }
+
+  setRef = c => {
+    this.node = c;
+  };
+
+  render () {
+    const { statusIds, featuredStatusIds, onLoadMore, timelineId, ...other }  = this.props;
+    const { isLoading, isPartial } = other;
+
+    if (isPartial) {
+      return <RegenerationIndicator />;
+    }
+
+    let scrollableContent = (isLoading || statusIds.size > 0) ? (
+      statusIds.map((statusId, index) => statusId === null ? (
+        <LoadGap
+          key={'gap:' + statusIds.get(index + 1)}
+          disabled={isLoading}
+          maxId={index > 0 ? statusIds.get(index - 1) : null}
+          onClick={onLoadMore}
+        />
+      ) : (
+        <StatusContainer
+          key={statusId}
+          id={statusId}
+          onMoveUp={this.handleMoveUp}
+          onMoveDown={this.handleMoveDown}
+          contextType={timelineId}
+          scrollKey={this.props.scrollKey}
+          withCounters={this.props.withCounters}
+        />
+      ))
+    ) : null;
+
+    if (scrollableContent && featuredStatusIds) {
+      scrollableContent = featuredStatusIds.map(statusId => (
+        <StatusContainer
+          key={`f-${statusId}`}
+          id={statusId}
+          featured
+          onMoveUp={this.handleMoveUp}
+          onMoveDown={this.handleMoveDown}
+          contextType={timelineId}
+          scrollKey={this.props.scrollKey}
+          withCounters={this.props.withCounters}
+        />
+      )).concat(scrollableContent);
+    }
+
+    return (
+      <ScrollableList {...other} showLoading={isLoading && statusIds.size === 0} onLoadMore={onLoadMore && this.handleLoadOlder} ref={this.setRef}>
+        {scrollableContent}
+      </ScrollableList>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/components/status_prepend.jsx b/app/javascript/flavours/blobfox/components/status_prepend.jsx
new file mode 100644
index 00000000000000..017ca66a19bae7
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/status_prepend.jsx
@@ -0,0 +1,158 @@
+//  Package imports  //
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
+import { Icon } from 'flavours/blobfox/components/icon';
+import { me } from 'flavours/blobfox/initial_state';
+
+export default class StatusPrepend extends PureComponent {
+
+  static propTypes = {
+    type: PropTypes.string.isRequired,
+    account: ImmutablePropTypes.map.isRequired,
+    parseClick: PropTypes.func.isRequired,
+    notificationId: PropTypes.number,
+  };
+
+  handleClick = (e) => {
+    const { account, parseClick } = this.props;
+    parseClick(e, `/@${account.get('acct')}`);
+  };
+
+  Message = () => {
+    const { type, account } = this.props;
+    let link = (
+      <a
+        onClick={this.handleClick}
+        href={account.get('url')}
+        className='status__display-name'
+      >
+        <b
+          dangerouslySetInnerHTML={{
+            __html : account.get('display_name_html') || account.get('username'),
+          }}
+        />
+      </a>
+    );
+    switch (type) {
+    case 'featured':
+      return (
+        <FormattedMessage id='status.pinned' defaultMessage='Pinned post' />
+      );
+    case 'reblogged_by':
+      return (
+        <FormattedMessage
+          id='status.reblogged_by'
+          defaultMessage='{name} boosted'
+          values={{ name : link }}
+        />
+      );
+    case 'favourite':
+      return (
+        <FormattedMessage
+          id='notification.favourite'
+          defaultMessage='{name} favorited your status'
+          values={{ name : link }}
+        />
+      );
+    case 'reaction':
+      return (
+        <FormattedMessage
+          id='notification.reaction'
+          defaultMessage='{name} reacted to your status'
+          values={{ name: link }}
+        />
+      );
+    case 'reblog':
+      return (
+        <FormattedMessage
+          id='notification.reblog'
+          defaultMessage='{name} boosted your status'
+          values={{ name : link }}
+        />
+      );
+    case 'status':
+      return (
+        <FormattedMessage
+          id='notification.status'
+          defaultMessage='{name} just posted'
+          values={{ name: link }}
+        />
+      );
+    case 'poll':
+      if (me === account.get('id')) {
+        return (
+          <FormattedMessage
+            id='notification.own_poll'
+            defaultMessage='Your poll has ended'
+          />
+        );
+      } else {
+        return (
+          <FormattedMessage
+            id='notification.poll'
+            defaultMessage='A poll you have voted in has ended'
+          />
+        );
+      }
+    case 'update':
+      return (
+        <FormattedMessage
+          id='notification.update'
+          defaultMessage='{name} edited a post'
+          values={{ name: link }}
+        />
+      );
+    }
+    return null;
+  };
+
+  render () {
+    const { Message } = this;
+    const { type } = this.props;
+
+    let iconId;
+
+    switch(type) {
+    case 'favourite':
+      iconId = 'star';
+      break;
+    case 'reaction':
+      iconId = 'plus';
+      break;
+    case 'featured':
+      iconId = 'thumb-tack';
+      break;
+    case 'poll':
+      iconId = 'tasks';
+      break;
+    case 'reblog':
+    case 'reblogged_by':
+      iconId = 'retweet';
+      break;
+    case 'status':
+      iconId = 'bell';
+      break;
+    case 'update':
+      iconId = 'pencil';
+      break;
+    }
+
+    return !type ? null : (
+      <aside className={type === 'reblogged_by' || type === 'featured' ? 'status__prepend' : 'notification__message'}>
+        <div className={type === 'reblogged_by' || type === 'featured' ? 'status__prepend-icon-wrapper' : 'notification__favourite-icon-wrapper'}>
+          <Icon
+            className={`status__prepend-icon ${type === 'favourite' ? 'star-icon' : ''}`}
+            id={iconId}
+          />
+        </div>
+        <Message />
+      </aside>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/components/status_reactions.jsx b/app/javascript/flavours/blobfox/components/status_reactions.jsx
new file mode 100644
index 00000000000000..81443d20555e14
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/status_reactions.jsx
@@ -0,0 +1,175 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import classNames from 'classnames';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import TransitionMotion from 'react-motion/lib/TransitionMotion';
+import spring from 'react-motion/lib/spring';
+
+import { unicodeMapping } from '../features/emoji/emoji_unicode_mapping_light';
+import { autoPlayGif, reduceMotion } from '../initial_state';
+import { assetHost } from '../utils/config';
+
+import { AnimatedNumber } from './animated_number';
+
+export default class StatusReactions extends ImmutablePureComponent {
+
+  static propTypes = {
+    statusId: PropTypes.string.isRequired,
+    reactions: ImmutablePropTypes.list.isRequired,
+    numVisible: PropTypes.number,
+    addReaction: PropTypes.func.isRequired,
+    canReact: PropTypes.bool.isRequired,
+    removeReaction: PropTypes.func.isRequired,
+  };
+
+  willEnter() {
+    return { scale: reduceMotion ? 1 : 0 };
+  }
+
+  willLeave() {
+    return { scale: reduceMotion ? 0 : spring(0, { stiffness: 170, damping: 26 }) };
+  }
+
+  render() {
+    const { reactions, numVisible } = this.props;
+    let visibleReactions = reactions
+      .filter(x => x.get('count') > 0)
+      .sort((a, b) => b.get('count') - a.get('count'));
+
+    if (numVisible >= 0) {
+      visibleReactions = visibleReactions.filter((_, i) => i < numVisible);
+    }
+
+    const styles = visibleReactions.map(reaction => ({
+      key: reaction.get('name'),
+      data: reaction,
+      style: { scale: reduceMotion ? 1 : spring(1, { stiffness: 150, damping: 13 }) },
+    })).toArray();
+
+    return (
+      <TransitionMotion styles={styles} willEnter={this.willEnter} willLeave={this.willLeave}>
+        {items => (
+          <div className={classNames('reactions-bar', { 'reactions-bar--empty': visibleReactions.isEmpty() })}>
+            {items.map(({ key, data, style }) => (
+              <Reaction
+                key={key}
+                statusId={this.props.statusId}
+                reaction={data}
+                style={{ transform: `scale(${style.scale})`, position: style.scale < 0.5 ? 'absolute' : 'static' }}
+                addReaction={this.props.addReaction}
+                removeReaction={this.props.removeReaction}
+                canReact={this.props.canReact}
+              />
+            ))}
+          </div>
+        )}
+      </TransitionMotion>
+    );
+  }
+
+}
+
+class Reaction extends ImmutablePureComponent {
+
+  static propTypes = {
+    statusId: PropTypes.string,
+    reaction: ImmutablePropTypes.map.isRequired,
+    addReaction: PropTypes.func.isRequired,
+    removeReaction: PropTypes.func.isRequired,
+    canReact: PropTypes.bool.isRequired,
+    style: PropTypes.object,
+  };
+
+  state = {
+    hovered: false,
+  };
+
+  handleClick = () => {
+    const { reaction, statusId, addReaction, removeReaction } = this.props;
+
+    if (reaction.get('me')) {
+      removeReaction(statusId, reaction.get('name'));
+    } else {
+      addReaction(statusId, reaction.get('name'));
+    }
+  };
+
+  handleMouseEnter = () => this.setState({ hovered: true });
+
+  handleMouseLeave = () => this.setState({ hovered: false });
+
+  render() {
+    const { reaction } = this.props;
+
+    return (
+      <button
+        className={classNames('reactions-bar__item', { active: reaction.get('me') })}
+        onClick={this.handleClick}
+        onMouseEnter={this.handleMouseEnter}
+        onMouseLeave={this.handleMouseLeave}
+        disabled={!this.props.canReact}
+        style={this.props.style}
+      >
+        <span className='reactions-bar__item__emoji'>
+          <Emoji
+            hovered={this.state.hovered}
+            emoji={reaction.get('name')}
+            url={reaction.get('url')}
+            staticUrl={reaction.get('static_url')}
+          />
+        </span>
+        <span className='reactions-bar__item__count'>
+          <AnimatedNumber value={reaction.get('count')} />
+        </span>
+      </button>
+    );
+  }
+
+}
+
+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 (
+        <img
+          draggable='false'
+          className='emojione'
+          alt={emoji}
+          title={title}
+          src={`${assetHost}/emoji/${filename}.svg`}
+        />
+      );
+    } else {
+      const filename = (autoPlayGif || hovered) ? url : staticUrl;
+      const shortCode = `:${emoji}:`;
+
+      return (
+        <img
+          draggable='false'
+          className='emojione custom-emoji'
+          alt={shortCode}
+          title={shortCode}
+          src={filename}
+        />
+      );
+    }
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/components/status_visibility_icon.jsx b/app/javascript/flavours/blobfox/components/status_visibility_icon.jsx
new file mode 100644
index 00000000000000..f2d7bd562b726d
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/status_visibility_icon.jsx
@@ -0,0 +1,54 @@
+//  Package imports  //
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl } from 'react-intl';
+
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import { Icon } from 'flavours/blobfox/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' },
+  direct: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
+});
+
+class VisibilityIcon extends ImmutablePureComponent {
+
+  static propTypes = {
+    visibility: PropTypes.string,
+    intl: PropTypes.object.isRequired,
+    withLabel: PropTypes.bool,
+  };
+
+  render() {
+    const { withLabel, visibility, intl } = this.props;
+
+    const visibilityIcon = {
+      public: 'globe',
+      unlisted: 'unlock',
+      private: 'lock',
+      direct: 'envelope',
+    }[visibility];
+
+    const label = intl.formatMessage(messages[visibility]);
+
+    const icon = (<Icon
+      className='status__visibility-icon'
+      fixedWidth
+      id={visibilityIcon}
+      title={label}
+      aria-hidden='true'
+    />);
+
+    if (withLabel) {
+      return (<span style={{ whiteSpace: 'nowrap' }}>{icon} {label}</span>);
+    } else {
+      return icon;
+    }
+  }
+
+}
+
+export default injectIntl(VisibilityIcon);
diff --git a/app/javascript/flavours/blobfox/components/timeline_hint.tsx b/app/javascript/flavours/blobfox/components/timeline_hint.tsx
new file mode 100644
index 00000000000000..bf2a2d8bbaeda7
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/timeline_hint.tsx
@@ -0,0 +1,25 @@
+import { FormattedMessage } from 'react-intl';
+
+interface Props {
+  resource: JSX.Element;
+  url: string;
+}
+
+export const TimelineHint: React.FC<Props> = ({ resource, url }) => (
+  <div className='timeline-hint'>
+    <strong>
+      <FormattedMessage
+        id='timeline_hint.remote_resource_not_displayed'
+        defaultMessage='{resource} from other servers are not displayed.'
+        values={{ resource }}
+      />
+    </strong>
+    <br />
+    <a href={url} target='_blank' rel='noopener noreferrer'>
+      <FormattedMessage
+        id='account.browse_more_on_origin_server'
+        defaultMessage='Browse more on the original profile'
+      />
+    </a>
+  </div>
+);
diff --git a/app/javascript/flavours/blobfox/components/verified_badge.tsx b/app/javascript/flavours/blobfox/components/verified_badge.tsx
new file mode 100644
index 00000000000000..9a6adcfa8601c9
--- /dev/null
+++ b/app/javascript/flavours/blobfox/components/verified_badge.tsx
@@ -0,0 +1,27 @@
+import { Icon } from './icon';
+
+const domParser = new DOMParser();
+
+const stripRelMe = (html: string) => {
+  const document = domParser.parseFromString(html, 'text/html').documentElement;
+
+  document.querySelectorAll<HTMLAnchorElement>('a[rel]').forEach((link) => {
+    link.rel = link.rel
+      .split(' ')
+      .filter((x: string) => x !== 'me')
+      .join(' ');
+  });
+
+  const body = document.querySelector('body');
+  return body ? { __html: body.innerHTML } : undefined;
+};
+
+interface Props {
+  link: string;
+}
+export const VerifiedBadge: React.FC<Props> = ({ link }) => (
+  <span className='verified-badge'>
+    <Icon id='check' className='verified-badge__mark' />
+    <span dangerouslySetInnerHTML={stripRelMe(link)} />
+  </span>
+);
diff --git a/app/javascript/flavours/blobfox/containers/account_container.jsx b/app/javascript/flavours/blobfox/containers/account_container.jsx
new file mode 100644
index 00000000000000..a134452e77210f
--- /dev/null
+++ b/app/javascript/flavours/blobfox/containers/account_container.jsx
@@ -0,0 +1,76 @@
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import { connect } from 'react-redux';
+
+import {
+  followAccount,
+  unfollowAccount,
+  blockAccount,
+  unblockAccount,
+  muteAccount,
+  unmuteAccount,
+} from '../actions/accounts';
+import { openModal } from '../actions/modal';
+import { initMuteModal } from '../actions/mutes';
+import Account from '../components/account';
+import { unfollowModal } from '../initial_state';
+import { makeGetAccount } from '../selectors';
+
+const messages = defineMessages({
+  unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
+});
+
+const makeMapStateToProps = () => {
+  const getAccount = makeGetAccount();
+
+  const mapStateToProps = (state, props) => ({
+    account: getAccount(state, props.id),
+  });
+
+  return mapStateToProps;
+};
+
+const mapDispatchToProps = (dispatch, { intl }) => ({
+
+  onFollow (account) {
+    if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
+      if (unfollowModal) {
+        dispatch(openModal({
+          modalType: 'CONFIRM',
+          modalProps: {
+            message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
+            confirm: intl.formatMessage(messages.unfollowConfirm),
+            onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
+          },
+        }));
+      } else {
+        dispatch(unfollowAccount(account.get('id')));
+      }
+    } else {
+      dispatch(followAccount(account.get('id')));
+    }
+  },
+
+  onBlock (account) {
+    if (account.getIn(['relationship', 'blocking'])) {
+      dispatch(unblockAccount(account.get('id')));
+    } else {
+      dispatch(blockAccount(account.get('id')));
+    }
+  },
+
+  onMute (account) {
+    if (account.getIn(['relationship', 'muting'])) {
+      dispatch(unmuteAccount(account.get('id')));
+    } else {
+      dispatch(initMuteModal(account));
+    }
+  },
+
+
+  onMuteNotifications (account, notifications) {
+    dispatch(muteAccount(account.get('id'), notifications));
+  },
+});
+
+export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Account));
diff --git a/app/javascript/flavours/blobfox/containers/admin_component.jsx b/app/javascript/flavours/blobfox/containers/admin_component.jsx
new file mode 100644
index 00000000000000..ee0bc2f316beb1
--- /dev/null
+++ b/app/javascript/flavours/blobfox/containers/admin_component.jsx
@@ -0,0 +1,22 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { IntlProvider } from 'flavours/blobfox/locales';
+
+export default class AdminComponent extends PureComponent {
+
+  static propTypes = {
+    children: PropTypes.node.isRequired,
+  };
+
+  render () {
+    const { children } = this.props;
+
+    return (
+      <IntlProvider>
+        {children}
+      </IntlProvider>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/containers/compose_container.jsx b/app/javascript/flavours/blobfox/containers/compose_container.jsx
new file mode 100644
index 00000000000000..f76550678ed2e1
--- /dev/null
+++ b/app/javascript/flavours/blobfox/containers/compose_container.jsx
@@ -0,0 +1,31 @@
+import { PureComponent } from 'react';
+
+import { Provider } from 'react-redux';
+
+import { fetchCustomEmojis } from '../actions/custom_emojis';
+import { hydrateStore } from '../actions/store';
+import Compose from '../features/standalone/compose';
+import initialState from '../initial_state';
+import { IntlProvider } from '../locales';
+import { store } from '../store';
+
+
+if (initialState) {
+  store.dispatch(hydrateStore(initialState));
+}
+
+store.dispatch(fetchCustomEmojis());
+
+export default class ComposeContainer extends PureComponent {
+
+  render () {
+    return (
+      <IntlProvider>
+        <Provider store={store}>
+          <Compose />
+        </Provider>
+      </IntlProvider>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/containers/domain_container.jsx b/app/javascript/flavours/blobfox/containers/domain_container.jsx
new file mode 100644
index 00000000000000..c719a5775c7c16
--- /dev/null
+++ b/app/javascript/flavours/blobfox/containers/domain_container.jsx
@@ -0,0 +1,36 @@
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import { connect } from 'react-redux';
+
+import { blockDomain, unblockDomain } from '../actions/domain_blocks';
+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 = () => ({});
+
+  return mapStateToProps;
+};
+
+const mapDispatchToProps = (dispatch, { intl }) => ({
+  onBlockDomain (domain) {
+    dispatch(openModal({
+      modalType: 'CONFIRM',
+      modalProps: {
+        message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />,
+        confirm: intl.formatMessage(messages.blockDomainConfirm),
+        onConfirm: () => dispatch(blockDomain(domain)),
+      },
+    }));
+  },
+
+  onUnblockDomain (domain) {
+    dispatch(unblockDomain(domain));
+  },
+});
+
+export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Domain));
diff --git a/app/javascript/flavours/blobfox/containers/dropdown_menu_container.js b/app/javascript/flavours/blobfox/containers/dropdown_menu_container.js
new file mode 100644
index 00000000000000..501bafdc53c67f
--- /dev/null
+++ b/app/javascript/flavours/blobfox/containers/dropdown_menu_container.js
@@ -0,0 +1,37 @@
+import { connect } from 'react-redux';
+
+import { openDropdownMenu, closeDropdownMenu } from '../actions/dropdown_menu';
+import { openModal, closeModal } from '../actions/modal';
+import DropdownMenu from '../components/dropdown_menu';
+import { isUserTouching } from '../is_mobile';
+
+/**
+ * @param {import('flavours/blobfox/store').RootState} state
+ */
+const mapStateToProps = state => ({
+  openDropdownId: state.dropdownMenu.openId,
+  openedViaKeyboard: state.dropdownMenu.keyboard,
+});
+
+const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({
+  onOpen(id, onItemClick, keyboard) {
+    dispatch(isUserTouching() ? openModal({
+      modalType: 'ACTIONS',
+      modalProps: {
+        status,
+        actions: items,
+        onClick: onItemClick,
+      },
+    }) : openDropdownMenu({ id, keyboard, scrollKey }));
+  },
+
+  onClose(id) {
+    dispatch(closeModal({
+      modalType: 'ACTIONS',
+      ignoreFocus: false,
+    }));
+    dispatch(closeDropdownMenu({ id }));
+  },
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(DropdownMenu);
diff --git a/app/javascript/flavours/blobfox/containers/intersection_observer_article_container.js b/app/javascript/flavours/blobfox/containers/intersection_observer_article_container.js
new file mode 100644
index 00000000000000..8d9bda6704326f
--- /dev/null
+++ b/app/javascript/flavours/blobfox/containers/intersection_observer_article_container.js
@@ -0,0 +1,18 @@
+import { connect } from 'react-redux';
+
+import { setHeight } from '../actions/height_cache';
+import IntersectionObserverArticle from '../components/intersection_observer_article';
+
+const makeMapStateToProps = (state, props) => ({
+  cachedHeight: state.getIn(['height_cache', props.saveHeightKey, props.id]),
+});
+
+const mapDispatchToProps = (dispatch) => ({
+
+  onHeightChange (key, id, height) {
+    dispatch(setHeight(key, id, height));
+  },
+
+});
+
+export default connect(makeMapStateToProps, mapDispatchToProps)(IntersectionObserverArticle);
diff --git a/app/javascript/flavours/blobfox/containers/mastodon.jsx b/app/javascript/flavours/blobfox/containers/mastodon.jsx
new file mode 100644
index 00000000000000..fc0361808eee95
--- /dev/null
+++ b/app/javascript/flavours/blobfox/containers/mastodon.jsx
@@ -0,0 +1,97 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { Helmet } from 'react-helmet';
+import { Route } from 'react-router-dom';
+
+import { Provider as ReduxProvider } from 'react-redux';
+
+import { ScrollContext } from 'react-router-scroll-4';
+
+import { fetchCustomEmojis } from 'flavours/blobfox/actions/custom_emojis';
+import { checkDeprecatedLocalSettings } from 'flavours/blobfox/actions/local_settings';
+import { hydrateStore } from 'flavours/blobfox/actions/store';
+import { connectUserStream } from 'flavours/blobfox/actions/streaming';
+import ErrorBoundary from 'flavours/blobfox/components/error_boundary';
+import { Router } from 'flavours/blobfox/components/router';
+import UI from 'flavours/blobfox/features/ui';
+import initialState, { title as siteTitle } from 'flavours/blobfox/initial_state';
+import { IntlProvider } from 'flavours/blobfox/locales';
+import { store } from 'flavours/blobfox/store';
+
+const title = process.env.NODE_ENV === 'production' ? siteTitle : `${siteTitle} (Dev)`;
+
+const hydrateAction = hydrateStore(initialState);
+
+store.dispatch(hydrateAction);
+
+// check for deprecated local settings
+store.dispatch(checkDeprecatedLocalSettings());
+
+if (initialState.meta.me) {
+  store.dispatch(fetchCustomEmojis());
+}
+
+const createIdentityContext = state => ({
+  signedIn: !!state.meta.me,
+  accountId: state.meta.me,
+  disabledAccountId: state.meta.disabled_account_id,
+  accessToken: state.meta.access_token,
+  permissions: state.role ? state.role.permissions : 0,
+});
+
+export default class Mastodon extends PureComponent {
+
+  static childContextTypes = {
+    identity: PropTypes.shape({
+      signedIn: PropTypes.bool.isRequired,
+      accountId: PropTypes.string,
+      disabledAccountId: PropTypes.string,
+      accessToken: PropTypes.string,
+    }).isRequired,
+  };
+
+  identity = createIdentityContext(initialState);
+
+  getChildContext() {
+    return {
+      identity: this.identity,
+    };
+  }
+
+  componentDidMount() {
+    if (this.identity.signedIn) {
+      this.disconnect = store.dispatch(connectUserStream());
+    }
+  }
+
+  componentWillUnmount () {
+    if (this.disconnect) {
+      this.disconnect();
+      this.disconnect = null;
+    }
+  }
+
+  shouldUpdateScroll (prevRouterProps, { location }) {
+    return !(location.state?.mastodonModalKey && location.state?.mastodonModalKey !== prevRouterProps?.location?.state?.mastodonModalKey);
+  }
+
+  render () {
+    return (
+      <IntlProvider>
+        <ReduxProvider store={store}>
+          <ErrorBoundary>
+            <Router>
+              <ScrollContext shouldUpdateScroll={this.shouldUpdateScroll}>
+                <Route path='/' component={UI} />
+              </ScrollContext>
+            </Router>
+
+            <Helmet defaultTitle={title} titleTemplate={`%s - ${title}`} />
+          </ErrorBoundary>
+        </ReduxProvider>
+      </IntlProvider>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/containers/media_container.jsx b/app/javascript/flavours/blobfox/containers/media_container.jsx
new file mode 100644
index 00000000000000..84a9a2c9cd186b
--- /dev/null
+++ b/app/javascript/flavours/blobfox/containers/media_container.jsx
@@ -0,0 +1,127 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+import { createPortal } from 'react-dom';
+
+import { fromJS } from 'immutable';
+
+import { ImmutableHashtag as Hashtag } from 'flavours/blobfox/components/hashtag';
+import MediaGallery from 'flavours/blobfox/components/media_gallery';
+import ModalRoot from 'flavours/blobfox/components/modal_root';
+import Poll from 'flavours/blobfox/components/poll';
+import Audio from 'flavours/blobfox/features/audio';
+import Card from 'flavours/blobfox/features/status/components/card';
+import MediaModal from 'flavours/blobfox/features/ui/components/media_modal';
+import Video from 'flavours/blobfox/features/video';
+import { IntlProvider } from 'flavours/blobfox/locales';
+import { getScrollbarWidth } from 'flavours/blobfox/utils/scrollbar';
+
+const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll, Hashtag, Audio };
+
+export default class MediaContainer extends PureComponent {
+
+  static propTypes = {
+    components: PropTypes.object.isRequired,
+  };
+
+  state = {
+    media: null,
+    index: null,
+    lang: null,
+    time: null,
+    backgroundColor: null,
+    options: null,
+  };
+
+  handleOpenMedia = (media, index, lang) => {
+    document.body.classList.add('with-modals--active');
+    document.documentElement.style.marginRight = `${getScrollbarWidth()}px`;
+
+    this.setState({ media, index, lang });
+  };
+
+  handleOpenVideo = (lang, options) => {
+    const { components } = this.props;
+    const { media } = JSON.parse(components[options.componentIndex].getAttribute('data-props'));
+    const mediaList = fromJS(media);
+
+    document.body.classList.add('with-modals--active');
+    document.documentElement.style.marginRight = `${getScrollbarWidth()}px`;
+
+    this.setState({ media: mediaList, lang, options });
+  };
+
+  handleCloseMedia = () => {
+    document.body.classList.remove('with-modals--active');
+    document.documentElement.style.marginRight = '0';
+
+    this.setState({
+      media: null,
+      index: null,
+      time: null,
+      backgroundColor: null,
+      options: null,
+    });
+  };
+
+  setBackgroundColor = color => {
+    this.setState({ backgroundColor: color });
+  };
+
+  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;
+    }
+
+    return (
+      <IntlProvider>
+        <>
+          {[].map.call(components, (component, i) => {
+            const componentName = component.getAttribute('data-component');
+            const Component = MEDIA_COMPONENTS[componentName];
+            const { media, card, poll, hashtag, ...props } = JSON.parse(component.getAttribute('data-props'));
+
+            Object.assign(props, {
+              ...(media   ? { media:   fromJS(media)   } : {}),
+              ...(card    ? { card:    fromJS(card)    } : {}),
+              ...(poll    ? { poll:    fromJS(poll)    } : {}),
+              ...(hashtag ? { hashtag: fromJS(hashtag) } : {}),
+
+              ...(componentName === 'Video' ? {
+                componentIndex: i,
+                onOpenVideo: handleOpenVideo,
+              } : {
+                onOpenMedia: this.handleOpenMedia,
+              }),
+            });
+
+            return createPortal(
+              <Component {...props} key={`media-${i}`} />,
+              component,
+            );
+          })}
+
+          <ModalRoot backgroundColor={this.state.backgroundColor} onClose={this.handleCloseMedia}>
+            {this.state.media && (
+              <MediaModal
+                media={this.state.media}
+                index={this.state.index || 0}
+                lang={this.state.lang}
+                currentTime={this.state.options?.startTime}
+                autoPlay={this.state.options?.autoPlay}
+                volume={this.state.options?.defaultVolume}
+                onClose={this.handleCloseMedia}
+                onChangeBackgroundColor={this.setBackgroundColor}
+              />
+            )}
+          </ModalRoot>
+        </>
+      </IntlProvider>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/containers/notification_purge_buttons_container.js b/app/javascript/flavours/blobfox/containers/notification_purge_buttons_container.js
new file mode 100644
index 00000000000000..22bf98f7f182a1
--- /dev/null
+++ b/app/javascript/flavours/blobfox/containers/notification_purge_buttons_container.js
@@ -0,0 +1,53 @@
+//  Package imports.
+import { defineMessages, injectIntl } from 'react-intl';
+
+import { connect } from 'react-redux';
+
+//  Our imports.
+import { openModal } from 'flavours/blobfox/actions/modal';
+import {
+  deleteMarkedNotifications,
+  enterNotificationClearingMode,
+  markAllNotifications,
+} from 'flavours/blobfox/actions/notifications';
+import NotificationPurgeButtons from 'flavours/blobfox/components/notification_purge_buttons';
+
+const messages = defineMessages({
+  clearMessage: { id: 'notifications.marked_clear_confirmation', defaultMessage: 'Are you sure you want to permanently clear all selected notifications?' },
+  clearConfirm: { id: 'notifications.marked_clear', defaultMessage: 'Clear selected notifications' },
+});
+
+const mapDispatchToProps = (dispatch, { intl }) => ({
+  onEnterCleaningMode(yes) {
+    dispatch(enterNotificationClearingMode(yes));
+  },
+
+  onDeleteMarked() {
+    dispatch(openModal({
+      modalType: 'CONFIRM',
+      modalProps: {
+        message: intl.formatMessage(messages.clearMessage),
+        confirm: intl.formatMessage(messages.clearConfirm),
+        onConfirm: () => dispatch(deleteMarkedNotifications()),
+      },
+    }));
+  },
+
+  onMarkAll() {
+    dispatch(markAllNotifications(true));
+  },
+
+  onMarkNone() {
+    dispatch(markAllNotifications(false));
+  },
+
+  onInvert() {
+    dispatch(markAllNotifications(null));
+  },
+});
+
+const mapStateToProps = state => ({
+  markNewForDelete: state.getIn(['notifications', 'markNewForDelete']),
+});
+
+export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NotificationPurgeButtons));
diff --git a/app/javascript/flavours/blobfox/containers/poll_container.js b/app/javascript/flavours/blobfox/containers/poll_container.js
new file mode 100644
index 00000000000000..a67055ec344177
--- /dev/null
+++ b/app/javascript/flavours/blobfox/containers/poll_container.js
@@ -0,0 +1,26 @@
+import { connect } from 'react-redux';
+
+import { debounce } from 'lodash';
+
+import { fetchPoll, vote } from 'flavours/blobfox/actions/polls';
+import Poll from 'flavours/blobfox/components/poll';
+
+const mapDispatchToProps = (dispatch, { pollId }) => ({
+  refresh: debounce(
+    () => {
+      dispatch(fetchPoll(pollId));
+    },
+    1000,
+    { leading: true },
+  ),
+
+  onVote (choices) {
+    dispatch(vote(pollId, choices));
+  },
+});
+
+const mapStateToProps = (state, { pollId }) => ({
+  poll: state.getIn(['polls', pollId]),
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(Poll);
diff --git a/app/javascript/flavours/blobfox/containers/scroll_container.js b/app/javascript/flavours/blobfox/containers/scroll_container.js
new file mode 100644
index 00000000000000..d21ff63687dbfd
--- /dev/null
+++ b/app/javascript/flavours/blobfox/containers/scroll_container.js
@@ -0,0 +1,18 @@
+import { ScrollContainer as OriginalScrollContainer } from 'react-router-scroll-4';
+
+// ScrollContainer is used to automatically scroll to the top when pushing a
+// new history state and remembering the scroll position when going back.
+// There are a few things we need to do differently, though.
+const defaultShouldUpdateScroll = (prevRouterProps, { location }) => {
+  // If the change is caused by opening a modal, do not scroll to top
+  return !(location.state?.mastodonModalKey && location.state?.mastodonModalKey !== prevRouterProps?.location?.state?.mastodonModalKey);
+};
+
+export default
+class ScrollContainer extends OriginalScrollContainer {
+
+  static defaultProps = {
+    shouldUpdateScroll: defaultShouldUpdateScroll,
+  };
+
+}
diff --git a/app/javascript/flavours/blobfox/containers/status_container.js b/app/javascript/flavours/blobfox/containers/status_container.js
new file mode 100644
index 00000000000000..f12d95c68e849a
--- /dev/null
+++ b/app/javascript/flavours/blobfox/containers/status_container.js
@@ -0,0 +1,313 @@
+import { defineMessages, injectIntl } from 'react-intl';
+
+import { connect } from 'react-redux';
+
+import { initBlockModal } from 'flavours/blobfox/actions/blocks';
+import { initBoostModal } from 'flavours/blobfox/actions/boosts';
+import {
+  replyCompose,
+  mentionCompose,
+  directCompose,
+} from 'flavours/blobfox/actions/compose';
+import {
+  initAddFilter,
+} from 'flavours/blobfox/actions/filters';
+import {
+  reblog,
+  favourite,
+  bookmark,
+  unreblog,
+  unfavourite,
+  unbookmark,
+  pin,
+  unpin,
+  addReaction,
+  removeReaction,
+} from 'flavours/blobfox/actions/interactions';
+import { changeLocalSetting } from 'flavours/blobfox/actions/local_settings';
+import { openModal } from 'flavours/blobfox/actions/modal';
+import { initMuteModal } from 'flavours/blobfox/actions/mutes';
+import { deployPictureInPicture } from 'flavours/blobfox/actions/picture_in_picture';
+import { initReport } from 'flavours/blobfox/actions/reports';
+import {
+  muteStatus,
+  unmuteStatus,
+  deleteStatus,
+  hideStatus,
+  revealStatus,
+  editStatus,
+  translateStatus,
+  undoStatusTranslation,
+} from 'flavours/blobfox/actions/statuses';
+import Status from 'flavours/blobfox/components/status';
+import { boostModal, favouriteModal, deleteModal } from 'flavours/blobfox/initial_state';
+import { makeGetStatus, makeGetPictureInPicture } from 'flavours/blobfox/selectors';
+
+import { showAlertForError } from '../actions/alerts';
+
+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? Favorites and boosts will be lost, and replies to the original post will be orphaned.' },
+  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?' },
+  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}}' },
+  editFilter: { id: 'confirmations.unfilter.edit_filter', defaultMessage: 'Edit filter' },
+});
+
+const makeMapStateToProps = () => {
+  const getStatus = makeGetStatus();
+  const getPictureInPicture = makeGetPictureInPicture();
+
+  const mapStateToProps = (state, props) => {
+
+    let status = getStatus(state, props);
+    let reblogStatus = status ? status.get('reblog', null) : null;
+    let account = undefined;
+    let prepend = undefined;
+
+    if (props.featured && status) {
+      account = status.get('account');
+      prepend = 'featured';
+    } else if (reblogStatus !== null && typeof reblogStatus === 'object') {
+      account = status.get('account');
+      status = reblogStatus;
+      prepend = 'reblogged_by';
+    }
+
+    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,
+      settings: state.get('local_settings'),
+      prepend: prepend || props.prepend,
+      pictureInPicture: getPictureInPicture(state, props),
+    };
+  };
+
+  return mapStateToProps;
+};
+
+const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
+
+  onReply (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({
+          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)),
+          },
+        }));
+      } else {
+        dispatch(replyCompose(status, router));
+      }
+    });
+  },
+
+  onModalReblog (status, privacy) {
+    if (status.get('reblogged')) {
+      dispatch(unreblog(status));
+    } else {
+      dispatch(reblog(status, privacy));
+    }
+  },
+
+  onReblog (status, e) {
+    dispatch((_, getState) => {
+      let state = getState();
+      if (state.getIn(['local_settings', 'confirm_boost_missing_media_description']) && status.get('media_attachments').some(item => !item.get('description')) && !status.get('reblogged')) {
+        dispatch(initBoostModal({ status, onReblog: this.onModalReblog, missingMediaDescription: true }));
+      } else if (e.shiftKey || !boostModal) {
+        this.onModalReblog(status);
+      } else {
+        dispatch(initBoostModal({ status, onReblog: this.onModalReblog }));
+      }
+    });
+  },
+
+  onBookmark (status) {
+    if (status.get('bookmarked')) {
+      dispatch(unbookmark(status));
+    } else {
+      dispatch(bookmark(status));
+    }
+  },
+
+  onModalFavourite (status) {
+    dispatch(favourite(status));
+  },
+
+  onFavourite (status, e) {
+    if (status.get('favourited')) {
+      dispatch(unfavourite(status));
+    } else {
+      if (e.shiftKey || !favouriteModal) {
+        this.onModalFavourite(status);
+      } else {
+        dispatch(openModal({
+          modalType: 'FAVOURITE',
+          modalProps: {
+            status,
+            onFavourite: this.onModalFavourite,
+          },
+        }));
+      }
+    }
+  },
+
+  onPin (status) {
+    if (status.get('pinned')) {
+      dispatch(unpin(status));
+    } else {
+      dispatch(pin(status));
+    }
+  },
+
+  onReactionAdd (statusId, name, url) {
+    dispatch(addReaction(statusId, name, url));
+  },
+
+  onReactionRemove (statusId, name) {
+    dispatch(removeReaction(statusId, name));
+  },
+
+  onEmbed (status) {
+    dispatch(openModal({
+      modalType: 'EMBED',
+      modalProps: {
+        id: status.get('id'),
+        onError: error => dispatch(showAlertForError(error)),
+      },
+    }));
+  },
+
+  onDelete (status, history, withRedraft = false) {
+    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)),
+        },
+      }));
+    }
+  },
+
+  onEdit (status, history) {
+    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)),
+          },
+        }));
+      } else {
+        dispatch(editStatus(status.get('id'), history));
+      }
+    });
+  },
+
+  onTranslate (status) {
+    if (status.get('translation')) {
+      dispatch(undoStatusTranslation(status.get('id'), status.get('poll')));
+    } else {
+      dispatch(translateStatus(status.get('id')));
+    }
+  },
+
+  onDirect (account, router) {
+    dispatch(directCompose(account, router));
+  },
+
+  onMention (account, router) {
+    dispatch(mentionCompose(account, router));
+  },
+
+  onOpenMedia (statusId, media, index, lang) {
+    dispatch(openModal({
+      modalType: 'MEDIA',
+      modalProps: { statusId, media, index, lang },
+    }));
+  },
+
+  onOpenVideo (statusId, media, lang, options) {
+    dispatch(openModal({
+      modalType: 'VIDEO',
+      modalProps: { statusId, media, lang, options },
+    }));
+  },
+
+  onBlock (status) {
+    const account = status.get('account');
+    dispatch(initBlockModal(account));
+  },
+
+  onReport (status) {
+    dispatch(initReport(status.get('account'), status));
+  },
+
+  onAddFilter (status) {
+    dispatch(initAddFilter(status, { contextType }));
+  },
+
+  onMute (account) {
+    dispatch(initMuteModal(account));
+  },
+
+  onMuteConversation (status) {
+    if (status.get('muted')) {
+      dispatch(unmuteStatus(status.get('id')));
+    } else {
+      dispatch(muteStatus(status.get('id')));
+    }
+  },
+
+  onToggleHidden (status) {
+    if (status.get('hidden')) {
+      dispatch(revealStatus(status.get('id')));
+    } else {
+      dispatch(hideStatus(status.get('id')));
+    }
+  },
+
+  deployPictureInPicture (status, type, mediaProps) {
+    dispatch((_, getState) => {
+      if (getState().getIn(['local_settings', 'media', 'pop_in_player'])) {
+        dispatch(deployPictureInPicture(status.get('id'), status.getIn(['account', 'id']), type, mediaProps));
+      }
+    });
+  },
+
+  onInteractionModal (type, status) {
+    dispatch(openModal({
+      modalType: 'INTERACTION',
+      modalProps: {
+        type,
+        accountId: status.getIn(['account', 'id']),
+        url: status.get('uri'),
+      },
+    }));
+  },
+
+});
+
+export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));
diff --git a/app/javascript/flavours/blobfox/features/about/index.jsx b/app/javascript/flavours/blobfox/features/about/index.jsx
new file mode 100644
index 00000000000000..f15b0e45ef716d
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/about/index.jsx
@@ -0,0 +1,225 @@
+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 { List as ImmutableList } from 'immutable';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
+
+import { fetchServer, fetchExtendedDescription, fetchDomainBlocks  } from 'flavours/blobfox/actions/server';
+import Column from 'flavours/blobfox/components/column';
+import { Icon  }  from 'flavours/blobfox/components/icon';
+import { ServerHeroImage } from 'flavours/blobfox/components/server_hero_image';
+import { Skeleton } from 'flavours/blobfox/components/skeleton';
+import Account from 'flavours/blobfox/containers/account_container';
+import LinkFooter from 'flavours/blobfox/features/ui/components/link_footer';
+
+const messages = defineMessages({
+  title: { id: 'column.about', defaultMessage: 'About' },
+  rules: { id: 'about.rules', defaultMessage: 'Server rules' },
+  blocks: { id: 'about.blocks', defaultMessage: 'Moderated servers' },
+  silenced: { id: 'about.domain_blocks.silenced.title', defaultMessage: 'Limited' },
+  silencedExplanation: { id: 'about.domain_blocks.silenced.explanation', defaultMessage: 'You will generally not see profiles and content from this server, unless you explicitly look it up or opt into it by following.' },
+  suspended: { id: 'about.domain_blocks.suspended.title', defaultMessage: 'Suspended' },
+  suspendedExplanation: { id: 'about.domain_blocks.suspended.explanation', defaultMessage: 'No data from this server will be processed, stored or exchanged, making any interaction or communication with users from this server impossible.' },
+});
+
+const severityMessages = {
+  silence: {
+    title: messages.silenced,
+    explanation: messages.silencedExplanation,
+  },
+
+  suspend: {
+    title: messages.suspended,
+    explanation: messages.suspendedExplanation,
+  },
+};
+
+const mapStateToProps = state => ({
+  server: state.getIn(['server', 'server']),
+  extendedDescription: state.getIn(['server', 'extendedDescription']),
+  domainBlocks: state.getIn(['server', 'domainBlocks']),
+});
+
+class Section extends PureComponent {
+
+  static propTypes = {
+    title: PropTypes.string,
+    children: PropTypes.node,
+    open: PropTypes.bool,
+    onOpen: PropTypes.func,
+  };
+
+  state = {
+    collapsed: !this.props.open,
+  };
+
+  handleClick = () => {
+    const { onOpen } = this.props;
+    const { collapsed } = this.state;
+
+    this.setState({ collapsed: !collapsed }, () => onOpen && onOpen());
+  };
+
+  render () {
+    const { title, children } = this.props;
+    const { collapsed } = this.state;
+
+    return (
+      <div className={classNames('about__section', { active: !collapsed })}>
+        <div className='about__section__title' role='button' tabIndex={0} onClick={this.handleClick}>
+          <Icon id={collapsed ? 'chevron-right' : 'chevron-down'} fixedWidth /> {title}
+        </div>
+
+        {!collapsed && (
+          <div className='about__section__body'>{children}</div>
+        )}
+      </div>
+    );
+  }
+
+}
+
+class About extends PureComponent {
+
+  static propTypes = {
+    server: ImmutablePropTypes.map,
+    extendedDescription: ImmutablePropTypes.map,
+    domainBlocks: ImmutablePropTypes.contains({
+      isLoading: PropTypes.bool,
+      isAvailable: PropTypes.bool,
+      items: ImmutablePropTypes.list,
+    }),
+    dispatch: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+    multiColumn: PropTypes.bool,
+  };
+
+  componentDidMount () {
+    const { dispatch } = this.props;
+    dispatch(fetchServer());
+    dispatch(fetchExtendedDescription());
+  }
+
+  handleDomainBlocksOpen = () => {
+    const { dispatch } = this.props;
+    dispatch(fetchDomainBlocks());
+  };
+
+  render () {
+    const { multiColumn, intl, server, extendedDescription, domainBlocks } = this.props;
+    const isLoading = server.get('isLoading');
+
+    return (
+      <Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.title)}>
+        <div className='scrollable about'>
+          <div className='about__header'>
+            <ServerHeroImage blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} srcSet={server.getIn(['thumbnail', 'versions'])?.map((value, key) => `${value} ${key.replace('@', '')}`).join(', ')} className='about__header__hero' />
+            <h1>{isLoading ? <Skeleton width='10ch' /> : server.get('domain')}</h1>
+            <p><FormattedMessage id='about.powered_by' defaultMessage='Decentralized social media powered by {mastodon}' values={{ mastodon: <a href='https://joinmastodon.org' className='about__mail' target='_blank'>Mastodon</a> }} /></p>
+          </div>
+
+          <div className='about__meta'>
+            <div className='about__meta__column'>
+              <h4><FormattedMessage id='server_banner.administered_by' defaultMessage='Administered by:' /></h4>
+
+              <Account id={server.getIn(['contact', 'account', 'id'])} size={36} minimal />
+            </div>
+
+            <hr className='about__meta__divider' />
+
+            <div className='about__meta__column'>
+              <h4><FormattedMessage id='about.contact' defaultMessage='Contact:' /></h4>
+
+              {isLoading ? <Skeleton width='10ch' /> : <a className='about__mail' href={`mailto:${server.getIn(['contact', 'email'])}`}>{server.getIn(['contact', 'email'])}</a>}
+            </div>
+          </div>
+
+          <Section open title={intl.formatMessage(messages.title)}>
+            {extendedDescription.get('isLoading') ? (
+              <>
+                <Skeleton width='100%' />
+                <br />
+                <Skeleton width='100%' />
+                <br />
+                <Skeleton width='100%' />
+                <br />
+                <Skeleton width='70%' />
+              </>
+            ) : (extendedDescription.get('content')?.length > 0 ? (
+              <div
+                className='prose'
+                dangerouslySetInnerHTML={{ __html: extendedDescription.get('content') }}
+              />
+            ) : (
+              <p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>
+            ))}
+          </Section>
+
+          <Section title={intl.formatMessage(messages.rules)}>
+            {!isLoading && (server.get('rules', ImmutableList()).isEmpty() ? (
+              <p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>
+            ) : (
+              <ol className='rules-list'>
+                {server.get('rules').map(rule => (
+                  <li key={rule.get('id')}>
+                    <span className='rules-list__text'>{rule.get('text')}</span>
+                  </li>
+                ))}
+              </ol>
+            ))}
+          </Section>
+
+          <Section title={intl.formatMessage(messages.blocks)} onOpen={this.handleDomainBlocksOpen}>
+            {domainBlocks.get('isLoading') ? (
+              <>
+                <Skeleton width='100%' />
+                <br />
+                <Skeleton width='70%' />
+              </>
+            ) : (domainBlocks.get('isAvailable') ? (
+              <>
+                <p><FormattedMessage id='about.domain_blocks.preamble' defaultMessage='Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server.' /></p>
+
+                <div className='about__domain-blocks'>
+                  {domainBlocks.get('items').map(block => (
+                    <div className='about__domain-blocks__domain' key={block.get('domain')}>
+                      <div className='about__domain-blocks__domain__header'>
+                        <h6><span title={`SHA-256: ${block.get('digest')}`}>{block.get('domain')}</span></h6>
+                        <span className='about__domain-blocks__domain__type' title={intl.formatMessage(severityMessages[block.get('severity')].explanation)}>{intl.formatMessage(severityMessages[block.get('severity')].title)}</span>
+                      </div>
+
+                      <p>{(block.get('comment') || '').length > 0 ? block.get('comment') : <FormattedMessage id='about.domain_blocks.no_reason_available' defaultMessage='Reason not available' />}</p>
+                    </div>
+                  ))}
+                </div>
+              </>
+            ) : (
+              <p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>
+            ))}
+          </Section>
+
+          <LinkFooter />
+
+          <div className='about__footer'>
+            <p><FormattedMessage id='about.fork_disclaimer' defaultMessage='blobfox-soc is free open source software forked from Mastodon.' /></p>
+            <p><FormattedMessage id='about.disclaimer' defaultMessage='Mastodon is free, open-source software, and a trademark of Mastodon gGmbH.' /></p>
+          </div>
+        </div>
+
+        <Helmet>
+          <title>{intl.formatMessage(messages.title)}</title>
+          <meta name='robots' content='all' />
+        </Helmet>
+      </Column>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(injectIntl(About));
diff --git a/app/javascript/flavours/blobfox/features/account/components/account_note.jsx b/app/javascript/flavours/blobfox/features/account/components/account_note.jsx
new file mode 100644
index 00000000000000..272a4ee312c08d
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/account/components/account_note.jsx
@@ -0,0 +1,174 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import { is } from 'immutable';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import Textarea from 'react-textarea-autosize';
+
+const messages = defineMessages({
+  placeholder: { id: 'account_note.placeholder', defaultMessage: 'Click to add a note' },
+});
+
+class InlineAlert extends PureComponent {
+
+  static propTypes = {
+    show: PropTypes.bool,
+  };
+
+  state = {
+    mountMessage: false,
+  };
+
+  static TRANSITION_DELAY = 200;
+
+  UNSAFE_componentWillReceiveProps (nextProps) {
+    if (!this.props.show && nextProps.show) {
+      this.setState({ mountMessage: true });
+    } else if (this.props.show && !nextProps.show) {
+      setTimeout(() => this.setState({ mountMessage: false }), InlineAlert.TRANSITION_DELAY);
+    }
+  }
+
+  render () {
+    const { show } = this.props;
+    const { mountMessage } = this.state;
+
+    return (
+      <span aria-live='polite' role='status' className='inline-alert' style={{ opacity: show ? 1 : 0 }}>
+        {mountMessage && <FormattedMessage id='generic.saved' defaultMessage='Saved' />}
+      </span>
+    );
+  }
+
+}
+
+class AccountNote extends ImmutablePureComponent {
+
+  static propTypes = {
+    account: ImmutablePropTypes.record.isRequired,
+    value: PropTypes.string,
+    onSave: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  state = {
+    value: null,
+    saving: false,
+    saved: false,
+  };
+
+  UNSAFE_componentWillMount () {
+    this._reset();
+  }
+
+  UNSAFE_componentWillReceiveProps (nextProps) {
+    const accountWillChange = !is(this.props.account, nextProps.account);
+    const newState = {};
+
+    if (accountWillChange && this._isDirty()) {
+      this._save(false);
+    }
+
+    if (accountWillChange || nextProps.value === this.state.value) {
+      newState.saving = false;
+    }
+
+    if (this.props.value !== nextProps.value) {
+      newState.value = nextProps.value;
+    }
+
+    this.setState(newState);
+  }
+
+  componentWillUnmount () {
+    if (this._isDirty()) {
+      this._save(false);
+    }
+  }
+
+  setTextareaRef = c => {
+    this.textarea = c;
+  };
+
+  handleChange = e => {
+    this.setState({ value: e.target.value, saving: false });
+  };
+
+  handleKeyDown = e => {
+    if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
+      e.preventDefault();
+
+      this._save();
+
+      if (this.textarea) {
+        this.textarea.blur();
+      }
+    } else if (e.keyCode === 27) {
+      e.preventDefault();
+
+      this._reset(() => {
+        if (this.textarea) {
+          this.textarea.blur();
+        }
+      });
+    }
+  };
+
+  handleBlur = () => {
+    if (this._isDirty()) {
+      this._save();
+    }
+  };
+
+  _save (showMessage = true) {
+    this.setState({ saving: true }, () => this.props.onSave(this.state.value));
+
+    if (showMessage) {
+      this.setState({ saved: true }, () => setTimeout(() => this.setState({ saved: false }), 2000));
+    }
+  }
+
+  _reset (callback) {
+    this.setState({ value: this.props.value }, callback);
+  }
+
+  _isDirty () {
+    return !this.state.saving && this.props.value !== null && this.state.value !== null && this.state.value !== this.props.value;
+  }
+
+  render () {
+    const { account, intl } = this.props;
+    const { value, saved } = this.state;
+
+    if (!account) {
+      return null;
+    }
+
+    return (
+      <div className='account__header__account-note'>
+        <label htmlFor={`account-note-${account.get('id')}`}>
+          <FormattedMessage id='account.account_note_header' defaultMessage='Note' /> <InlineAlert show={saved} />
+        </label>
+
+        <Textarea
+          id={`account-note-${account.get('id')}`}
+          className='account__header__account-note__content'
+          disabled={this.props.value === null || value === null}
+          placeholder={intl.formatMessage(messages.placeholder)}
+          value={value || ''}
+          onChange={this.handleChange}
+          onKeyDown={this.handleKeyDown}
+          onBlur={this.handleBlur}
+          ref={this.setTextareaRef}
+        />
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(AccountNote);
diff --git a/app/javascript/flavours/blobfox/features/account/components/action_bar.jsx b/app/javascript/flavours/blobfox/features/account/components/action_bar.jsx
new file mode 100644
index 00000000000000..8730f8fe2669f7
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/account/components/action_bar.jsx
@@ -0,0 +1,85 @@
+import { PureComponent } from 'react';
+
+import { FormattedMessage, FormattedNumber } from 'react-intl';
+
+import { NavLink } from 'react-router-dom';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
+import { Icon } from 'flavours/blobfox/components/icon';
+
+class ActionBar extends PureComponent {
+
+  static propTypes = {
+    account: ImmutablePropTypes.map.isRequired,
+  };
+
+  isStatusesPageActive = (match, location) => {
+    if (!match) {
+      return false;
+    }
+    return !location.pathname.match(/\/(followers|following)\/?$/);
+  };
+
+  render () {
+    const { account } = this.props;
+
+    if (account.get('suspended')) {
+      return (
+        <div>
+          <div className='account__disclaimer'>
+            <Icon id='info-circle' fixedWidth /> <FormattedMessage
+              id='account.suspended_disclaimer_full'
+              defaultMessage='This user has been suspended by a moderator.'
+            />
+          </div>
+        </div>
+      );
+    }
+
+    let extraInfo = '';
+
+    if (account.get('acct') !== account.get('username')) {
+      extraInfo = (
+        <div className='account__disclaimer'>
+          <Icon id='info-circle' fixedWidth /> <FormattedMessage
+            id='account.disclaimer_full'
+            defaultMessage="Information below may reflect the user's profile incompletely."
+          />
+          {' '}
+          <a target='_blank' rel='noopener' href={account.get('url')}>
+            <FormattedMessage id='account.view_full_profile' defaultMessage='View full profile' />
+          </a>
+        </div>
+      );
+    }
+
+    return (
+      <div>
+        {extraInfo}
+
+        <div className='account__action-bar'>
+          <div className='account__action-bar-links'>
+            <NavLink isActive={this.isStatusesPageActive} activeClassName='active' className='account__action-bar__tab' to={`/@${account.get('acct')}`}>
+              <FormattedMessage id='account.posts' defaultMessage='Posts' />
+              <strong><FormattedNumber value={account.get('statuses_count')} /></strong>
+            </NavLink>
+
+            <NavLink exact activeClassName='active' className='account__action-bar__tab' to={`/@${account.get('acct')}/following`}>
+              <FormattedMessage id='account.follows' defaultMessage='Follows' />
+              <strong><FormattedNumber value={account.get('following_count')} /></strong>
+            </NavLink>
+
+            <NavLink exact activeClassName='active' className='account__action-bar__tab' to={`/@${account.get('acct')}/followers`}>
+              <FormattedMessage id='account.followers' defaultMessage='Followers' />
+              <strong>{ account.get('followers_count') < 0 ? '-' : <FormattedNumber value={account.get('followers_count')} /> }</strong>
+            </NavLink>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default ActionBar;
diff --git a/app/javascript/flavours/blobfox/features/account/components/featured_tags.jsx b/app/javascript/flavours/blobfox/features/account/components/featured_tags.jsx
new file mode 100644
index 00000000000000..4a1219cf984203
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/account/components/featured_tags.jsx
@@ -0,0 +1,52 @@
+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 Hashtag from 'flavours/blobfox/components/hashtag';
+
+const messages = defineMessages({
+  lastStatusAt: { id: 'account.featured_tags.last_status_at', defaultMessage: 'Last post on {date}' },
+  empty: { id: 'account.featured_tags.last_status_never', defaultMessage: 'No posts' },
+});
+
+class FeaturedTags extends ImmutablePureComponent {
+
+  static propTypes = {
+    account: ImmutablePropTypes.record,
+    featuredTags: ImmutablePropTypes.list,
+    tagged: PropTypes.string,
+    intl: PropTypes.object.isRequired,
+  };
+
+  render () {
+    const { account, featuredTags, intl } = this.props;
+
+    if (!account || account.get('suspended') || featuredTags.isEmpty()) {
+      return null;
+    }
+
+    return (
+      <div className='getting-started__trends'>
+        <h4><FormattedMessage id='account.featured_tags.title' defaultMessage="{name}'s featured hashtags" values={{ name: <bdi dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /> }} /></h4>
+
+        {featuredTags.take(3).map(featuredTag => (
+          <Hashtag
+            key={featuredTag.get('name')}
+            name={featuredTag.get('name')}
+            href={featuredTag.get('url')}
+            to={`/@${account.get('acct')}/tagged/${featuredTag.get('name')}`}
+            uses={featuredTag.get('statuses_count') * 1}
+            withGraph={false}
+            description={((featuredTag.get('statuses_count') * 1) > 0) ? intl.formatMessage(messages.lastStatusAt, { date: intl.formatDate(featuredTag.get('last_status_at'), { month: 'short', day: '2-digit' }) }) : intl.formatMessage(messages.empty)}
+          />
+        ))}
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(FeaturedTags);
diff --git a/app/javascript/flavours/blobfox/features/account/components/follow_request_note.jsx b/app/javascript/flavours/blobfox/features/account/components/follow_request_note.jsx
new file mode 100644
index 00000000000000..c830ca3d64eafd
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/account/components/follow_request_note.jsx
@@ -0,0 +1,38 @@
+import { FormattedMessage } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import { Icon } from 'flavours/blobfox/components/icon';
+
+export default class FollowRequestNote extends ImmutablePureComponent {
+
+  static propTypes = {
+    account: ImmutablePropTypes.record.isRequired,
+  };
+
+  render () {
+    const { account, onAuthorize, onReject } = this.props;
+
+    return (
+      <div className='follow-request-banner'>
+        <div className='follow-request-banner__message'>
+          <FormattedMessage id='account.requested_follow' defaultMessage='{name} has requested to follow you' values={{ name: <bdi><strong dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi> }} />
+        </div>
+
+        <div className='follow-request-banner__action'>
+          <button type='button' className='button button-tertiary button--confirmation' onClick={onAuthorize}>
+            <Icon id='check' fixedWidth />
+            <FormattedMessage id='follow_request.authorize' defaultMessage='Authorize' />
+          </button>
+
+          <button type='button' className='button button-tertiary button--destructive' onClick={onReject}>
+            <Icon id='times' fixedWidth />
+            <FormattedMessage id='follow_request.reject' defaultMessage='Reject' />
+          </button>
+        </div>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/account/components/header.jsx b/app/javascript/flavours/blobfox/features/account/components/header.jsx
new file mode 100644
index 00000000000000..35b3650825f855
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/account/components/header.jsx
@@ -0,0 +1,404 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import classNames from 'classnames';
+import { Helmet } from 'react-helmet';
+import { withRouter } from 'react-router-dom';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import { Avatar } from 'flavours/blobfox/components/avatar';
+import { Button } from 'flavours/blobfox/components/button';
+import { Icon }  from 'flavours/blobfox/components/icon';
+import { IconButton } from 'flavours/blobfox/components/icon_button';
+import DropdownMenuContainer from 'flavours/blobfox/containers/dropdown_menu_container';
+import { autoPlayGif, me, domain } from 'flavours/blobfox/initial_state';
+import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/blobfox/permissions';
+import { preferencesLink, profileLink, accountAdminLink } from 'flavours/blobfox/utils/backend_links';
+import { WithRouterPropTypes } from 'flavours/blobfox/utils/react_router';
+
+import AccountNoteContainer from '../containers/account_note_container';
+import FollowRequestNoteContainer from '../containers/follow_request_note_container';
+
+const messages = defineMessages({
+  unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
+  follow: { id: 'account.follow', defaultMessage: 'Follow' },
+  cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Withdraw follow request' },
+  requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
+  unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
+  edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
+  linkVerifiedOn: { id: 'account.link_verified_on', defaultMessage: 'Ownership of this link was checked on {date}' },
+  account_locked: { id: 'account.locked_info', defaultMessage: 'This account privacy status is set to locked. The owner manually reviews who can follow them.' },
+  mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' },
+  direct: { id: 'account.direct', defaultMessage: 'Privately mention @{name}' },
+  unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
+  block: { id: 'account.block', defaultMessage: 'Block @{name}' },
+  mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
+  report: { id: 'account.report', defaultMessage: 'Report @{name}' },
+  share: { id: 'account.share', defaultMessage: 'Share @{name}\'s profile' },
+  media: { id: 'account.media', defaultMessage: 'Media' },
+  blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' },
+  unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
+  hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' },
+  showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' },
+  enableNotifications: { id: 'account.enable_notifications', defaultMessage: 'Notify me when @{name} posts' },
+  disableNotifications: { id: 'account.disable_notifications', defaultMessage: 'Stop notifying me when @{name} posts' },
+  pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' },
+  preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
+  follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
+  favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' },
+  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' },
+  mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
+  endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' },
+  unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' },
+  add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' },
+  admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
+  admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },
+  languages: { id: 'account.languages', defaultMessage: 'Change subscribed languages' },
+  openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' },
+});
+
+const titleFromAccount = account => {
+  const displayName = account.get('display_name');
+  const acct = account.get('acct') === account.get('username') ? `${account.get('username')}@${domain}` : account.get('acct');
+  const prefix = displayName.trim().length === 0 ? account.get('username') : displayName;
+
+  return `${prefix} (@${acct})`;
+};
+
+const dateFormatOptions = {
+  month: 'short',
+  day: 'numeric',
+  year: 'numeric',
+  hour12: false,
+  hour: '2-digit',
+  minute: '2-digit',
+};
+
+class Header extends ImmutablePureComponent {
+
+  static propTypes = {
+    account: ImmutablePropTypes.record,
+    identity_props: ImmutablePropTypes.list,
+    onFollow: PropTypes.func.isRequired,
+    onBlock: PropTypes.func.isRequired,
+    onMention: PropTypes.func.isRequired,
+    onDirect: PropTypes.func.isRequired,
+    onReblogToggle: PropTypes.func.isRequired,
+    onNotifyToggle: PropTypes.func.isRequired,
+    onReport: PropTypes.func.isRequired,
+    onMute: PropTypes.func.isRequired,
+    onBlockDomain: PropTypes.func.isRequired,
+    onUnblockDomain: PropTypes.func.isRequired,
+    onEndorseToggle: PropTypes.func.isRequired,
+    onAddToList: PropTypes.func.isRequired,
+    onChangeLanguages: PropTypes.func.isRequired,
+    onInteractionModal: PropTypes.func.isRequired,
+    onOpenAvatar: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+    domain: PropTypes.string.isRequired,
+    hidden: PropTypes.bool,
+    ...WithRouterPropTypes,
+  };
+
+  static contextTypes = {
+    identity: PropTypes.object,
+  };
+
+  openEditProfile = () => {
+    window.open(profileLink, '_blank');
+  };
+
+  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');
+    }
+  };
+
+  handleAvatarClick = e => {
+    if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
+      e.preventDefault();
+      this.props.onOpenAvatar();
+    }
+  };
+
+  handleShare = () => {
+    const { account } = this.props;
+
+    navigator.share({
+      url: account.get('url'),
+    }).catch((e) => {
+      if (e.name !== 'AbortError') console.error(e);
+    });
+  };
+
+  render () {
+    const { account, hidden, intl, domain } = this.props;
+    const { signedIn, permissions } = this.context.identity;
+
+    if (!account) {
+      return null;
+    }
+
+    const suspended    = account.get('suspended');
+    const isRemote     = account.get('acct') !== account.get('username');
+    const remoteDomain = isRemote ? account.get('acct').split('@')[1] : null;
+
+    let info        = [];
+    let actionBtn   = '';
+    let bellBtn     = '';
+    let lockedIcon  = '';
+    let menu        = [];
+
+    if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) {
+      info.push(<span className='relationship-tag'><FormattedMessage id='account.follows_you' defaultMessage='Follows you' /></span>);
+    } else if (me !== account.get('id') && account.getIn(['relationship', 'blocking'])) {
+      info.push(<span className='relationship-tag'><FormattedMessage id='account.blocked' defaultMessage='Blocked' /></span>);
+    }
+
+    if (me !== account.get('id') && account.getIn(['relationship', 'muting'])) {
+      info.push(<span className='relationship-tag'><FormattedMessage id='account.muted' defaultMessage='Muted' /></span>);
+    } else if (me !== account.get('id') && account.getIn(['relationship', 'domain_blocking'])) {
+      info.push(<span className='relationship-tag'><FormattedMessage id='account.domain_blocked' defaultMessage='Domain blocked' /></span>);
+    }
+
+    if (account.getIn(['relationship', 'requested']) || account.getIn(['relationship', 'following'])) {
+      bellBtn = <IconButton icon={account.getIn(['relationship', 'notifying']) ? 'bell' : 'bell-o'} size={24} active={account.getIn(['relationship', 'notifying'])} title={intl.formatMessage(account.getIn(['relationship', 'notifying']) ? messages.disableNotifications : messages.enableNotifications, { name: account.get('username') })} onClick={this.props.onNotifyToggle} />;
+    }
+
+    if (me !== account.get('id')) {
+      if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded
+        actionBtn = '';
+      } else if (account.getIn(['relationship', 'requested'])) {
+        actionBtn = <Button text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
+      } else if (!account.getIn(['relationship', 'blocking'])) {
+        actionBtn = <Button className={classNames({ 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={signedIn ? this.props.onFollow : this.props.onInteractionModal} />;
+      } else if (account.getIn(['relationship', 'blocking'])) {
+        actionBtn = <Button text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
+      }
+    } else if (profileLink) {
+      actionBtn = <Button text={intl.formatMessage(messages.edit_profile)} onClick={this.openEditProfile} />;
+    }
+
+    if (account.get('moved') && !account.getIn(['relationship', 'following'])) {
+      actionBtn = '';
+    }
+
+    if (account.get('suspended') && !account.getIn(['relationship', 'following'])) {
+      actionBtn = '';
+    }
+
+    if (account.get('locked')) {
+      lockedIcon = <Icon id='lock' title={intl.formatMessage(messages.account_locked)} />;
+    }
+
+    if (signedIn && account.get('id') !== me && !account.get('suspended')) {
+      menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention });
+      menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.props.onDirect });
+      menu.push(null);
+    }
+
+    if (isRemote) {
+      menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: account.get('url') });
+      menu.push(null);
+    }
+
+    if ('share' in navigator && !account.get('suspended')) {
+      menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare });
+      menu.push(null);
+    }
+
+    if (account.get('id') === me) {
+      if (profileLink) menu.push({ text: intl.formatMessage(messages.edit_profile), href: profileLink });
+      if (preferencesLink) menu.push({ text: intl.formatMessage(messages.preferences), href: preferencesLink });
+      menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' });
+      menu.push(null);
+      menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
+      menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' });
+      menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' });
+      menu.push({ text: intl.formatMessage(messages.followed_tags), to: '/followed_tags' });
+      menu.push(null);
+      menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
+      menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
+      menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
+    } else if (signedIn) {
+      if (account.getIn(['relationship', 'following'])) {
+        if (!account.getIn(['relationship', 'muting'])) {
+          if (account.getIn(['relationship', 'showing_reblogs'])) {
+            menu.push({ text: intl.formatMessage(messages.hideReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
+          } else {
+            menu.push({ text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
+          }
+
+          menu.push({ text: intl.formatMessage(messages.languages), action: this.props.onChangeLanguages });
+          menu.push(null);
+        }
+
+        menu.push({ text: intl.formatMessage(account.getIn(['relationship', 'endorsed']) ? messages.unendorse : messages.endorse), action: this.props.onEndorseToggle });
+        menu.push({ text: intl.formatMessage(messages.add_or_remove_from_list), action: this.props.onAddToList });
+        menu.push(null);
+      }
+
+      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 });
+      }
+
+      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 });
+      }
+
+      if (!account.get('suspended')) {
+        menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport, dangerous: true });
+      }
+    }
+
+    if (signedIn && isRemote) {
+      menu.push(null);
+
+      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 });
+      }
+    }
+
+    if (account.get('id') !== me && ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && accountAdminLink) || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) {
+      menu.push(null);
+      if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && accountAdminLink) {
+        menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: accountAdminLink(account.get('id')) });
+      }
+      if (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) {
+        menu.push({ text: intl.formatMessage(messages.admin_domain, { domain: remoteDomain }), href: `/admin/instances/${remoteDomain}` });
+      }
+    }
+
+    const content          = { __html: account.get('note_emojified') };
+    const displayNameHtml = { __html: account.get('display_name_html') };
+    const fields          = account.get('fields');
+    const isLocal         = account.get('acct').indexOf('@') === -1;
+    const acct            = isLocal && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
+    const isIndexable     = !account.get('noindex');
+
+    let badge;
+
+    if (account.get('bot')) {
+      badge = (<div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Automated' /></div>);
+    } else if (account.get('group')) {
+      badge = (<div className='account-role group'><FormattedMessage id='account.badges.group' defaultMessage='Group' /></div>);
+    } else {
+      badge = null;
+    }
+
+    let role = null;
+    if (account.getIn(['roles', 0])) {
+      role = (<div key='role' className={`account-role user-role-${account.getIn(['roles', 0, 'id'])}`}>{account.getIn(['roles', 0, 'name'])}</div>);
+    }
+
+    return (
+      <div className={classNames('account__header', { inactive: !!account.get('moved') })} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
+        {!(suspended || hidden || account.get('moved')) && account.getIn(['relationship', 'requested_by']) && <FollowRequestNoteContainer account={account} />}
+
+        <div className='account__header__image'>
+          <div className='account__header__info'>
+            {info}
+          </div>
+
+          {!(suspended || hidden) && <img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />}
+        </div>
+
+        <div className='account__header__bar'>
+          <div className='account__header__tabs'>
+            <a className='avatar' href={account.get('avatar')} rel='noopener noreferrer' target='_blank' onClick={this.handleAvatarClick}>
+              <Avatar account={suspended || hidden ? undefined : account} size={90} />
+              {role}
+            </a>
+
+            <div className='account__header__tabs__buttons'>
+              {!hidden && (
+                <>
+                  {actionBtn}
+                  {bellBtn}
+                </>
+              )}
+
+              <DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' size={24} direction='right' />
+            </div>
+          </div>
+
+          <div className='account__header__tabs__name'>
+            <h1>
+              <span dangerouslySetInnerHTML={displayNameHtml} /> {badge}
+              <small>
+                <span>@{acct}</span> {lockedIcon}
+              </small>
+            </h1>
+          </div>
+
+          {signedIn && <AccountNoteContainer account={account} />}
+
+          {!(suspended || hidden) && (
+            <div className='account__header__extra'>
+              <div className='account__header__bio'>
+                { fields.size > 0 && (
+                  <div className='account__header__fields'>
+                    {fields.map((pair, i) => (
+                      <dl key={i}>
+                        <dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} />
+
+                        <dd className={pair.get('verified_at') && 'verified'} title={pair.get('value_plain')}>
+                          {pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><Icon id='check' className='verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} className='translate' />
+                        </dd>
+                      </dl>
+                    ))}
+                  </div>
+                )}
+
+                {account.get('note').length > 0 && account.get('note') !== '<p></p>' && <div className='account__header__content translate' dangerouslySetInnerHTML={content} />}
+
+                <div className='account__header__joined'><FormattedMessage id='account.joined' defaultMessage='Joined {date}' values={{ date: intl.formatDate(account.get('created_at'), { year: 'numeric', month: 'short', day: '2-digit' }) }} /></div>
+              </div>
+            </div>
+          )}
+        </div>
+
+        <Helmet>
+          <title>{titleFromAccount(account)}</title>
+          <meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} />
+          <link rel='canonical' href={account.get('url')} />
+        </Helmet>
+      </div>
+    );
+  }
+
+}
+
+export default withRouter(injectIntl(Header));
diff --git a/app/javascript/flavours/blobfox/features/account/components/profile_column_header.jsx b/app/javascript/flavours/blobfox/features/account/components/profile_column_header.jsx
new file mode 100644
index 00000000000000..2dc4216bdd1001
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/account/components/profile_column_header.jsx
@@ -0,0 +1,36 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { injectIntl, defineMessages } from 'react-intl';
+
+import ColumnHeader from '../../../components/column_header';
+
+const messages = defineMessages({
+  profile: { id: 'column_header.profile', defaultMessage: 'Profile' },
+});
+
+class ProfileColumnHeader extends PureComponent {
+
+  static propTypes = {
+    onClick: PropTypes.func,
+    multiColumn: PropTypes.bool,
+    intl: PropTypes.object.isRequired,
+  };
+
+  render() {
+    const { onClick, intl, multiColumn } = this.props;
+
+    return (
+      <ColumnHeader
+        icon='user-circle'
+        title={intl.formatMessage(messages.profile)}
+        onClick={onClick}
+        showBackButton
+        multiColumn={multiColumn}
+      />
+    );
+  }
+
+}
+
+export default injectIntl(ProfileColumnHeader);
diff --git a/app/javascript/flavours/blobfox/features/account/containers/account_note_container.js b/app/javascript/flavours/blobfox/features/account/containers/account_note_container.js
new file mode 100644
index 00000000000000..3d24e158b34e77
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/account/containers/account_note_container.js
@@ -0,0 +1,19 @@
+import { connect } from 'react-redux';
+
+import { submitAccountNote } from 'flavours/blobfox/actions/account_notes';
+
+import AccountNote from '../components/account_note';
+
+const mapStateToProps = (state, { account }) => ({
+  value: account.getIn(['relationship', 'note']),
+});
+
+const mapDispatchToProps = (dispatch, { account }) => ({
+
+  onSave (value) {
+    dispatch(submitAccountNote({ id: account.get('id'), value}));
+  },
+
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(AccountNote);
diff --git a/app/javascript/flavours/blobfox/features/account/containers/featured_tags_container.js b/app/javascript/flavours/blobfox/features/account/containers/featured_tags_container.js
new file mode 100644
index 00000000000000..5d4344086653bb
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/account/containers/featured_tags_container.js
@@ -0,0 +1,17 @@
+import { List as ImmutableList } from 'immutable';
+import { connect } from 'react-redux';
+
+import { makeGetAccount } from 'flavours/blobfox/selectors';
+
+import FeaturedTags from '../components/featured_tags';
+
+const mapStateToProps = () => {
+  const getAccount = makeGetAccount();
+
+  return (state, { accountId }) => ({
+    account: getAccount(state, accountId),
+    featuredTags: state.getIn(['user_lists', 'featured_tags', accountId, 'items'], ImmutableList()),
+  });
+};
+
+export default connect(mapStateToProps)(FeaturedTags);
diff --git a/app/javascript/flavours/blobfox/features/account/containers/follow_request_note_container.js b/app/javascript/flavours/blobfox/features/account/containers/follow_request_note_container.js
new file mode 100644
index 00000000000000..1771a78fecfae0
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/account/containers/follow_request_note_container.js
@@ -0,0 +1,17 @@
+import { connect } from 'react-redux';
+
+import { authorizeFollowRequest, rejectFollowRequest } from 'flavours/blobfox/actions/accounts';
+
+import FollowRequestNote from '../components/follow_request_note';
+
+const mapDispatchToProps = (dispatch, { account }) => ({
+  onAuthorize () {
+    dispatch(authorizeFollowRequest(account.get('id')));
+  },
+
+  onReject () {
+    dispatch(rejectFollowRequest(account.get('id')));
+  },
+});
+
+export default connect(null, mapDispatchToProps)(FollowRequestNote);
diff --git a/app/javascript/flavours/blobfox/features/account/navigation.jsx b/app/javascript/flavours/blobfox/features/account/navigation.jsx
new file mode 100644
index 00000000000000..ddb8a835275c05
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/account/navigation.jsx
@@ -0,0 +1,55 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { connect } from 'react-redux';
+
+import FeaturedTags from 'flavours/blobfox/features/account/containers/featured_tags_container';
+import { normalizeForLookup } from 'flavours/blobfox/reducers/accounts_map';
+
+const mapStateToProps = (state, { match: { params: { acct } } }) => {
+  const accountId = state.getIn(['accounts_map', normalizeForLookup(acct)]);
+
+  if (!accountId) {
+    return {
+      isLoading: true,
+    };
+  }
+
+  return {
+    accountId,
+    isLoading: false,
+  };
+};
+
+class AccountNavigation extends PureComponent {
+
+  static propTypes = {
+    match: PropTypes.shape({
+      params: PropTypes.shape({
+        acct: PropTypes.string,
+        tagged: PropTypes.string,
+      }).isRequired,
+    }).isRequired,
+
+    accountId: PropTypes.string,
+    isLoading: PropTypes.bool,
+  };
+
+  render () {
+    const { accountId, isLoading, match: { params: { tagged } } } = this.props;
+
+    if (isLoading) {
+      return null;
+    }
+
+    return (
+      <>
+        <div className='flex-spacer' />
+        <FeaturedTags accountId={accountId} tagged={tagged} />
+      </>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(AccountNavigation);
diff --git a/app/javascript/flavours/blobfox/features/account_gallery/components/media_item.jsx b/app/javascript/flavours/blobfox/features/account_gallery/components/media_item.jsx
new file mode 100644
index 00000000000000..cda7fd5e00be10
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/account_gallery/components/media_item.jsx
@@ -0,0 +1,155 @@
+import PropTypes from 'prop-types';
+
+import classNames from 'classnames';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import { Blurhash } from 'flavours/blobfox/components/blurhash';
+import { Icon }  from 'flavours/blobfox/components/icon';
+import { autoPlayGif, displayMedia, useBlurhash } from 'flavours/blobfox/initial_state';
+
+export default class MediaItem extends ImmutablePureComponent {
+
+  static propTypes = {
+    attachment: ImmutablePropTypes.map.isRequired,
+    displayWidth: PropTypes.number.isRequired,
+    onOpenMedia: PropTypes.func.isRequired,
+  };
+
+  state = {
+    visible: displayMedia !== 'hide_all' && !this.props.attachment.getIn(['status', 'sensitive']) || displayMedia === 'show_all',
+    loaded: false,
+  };
+
+  handleImageLoad = () => {
+    this.setState({ loaded: true });
+  };
+
+  handleMouseEnter = e => {
+    if (this.hoverToPlay()) {
+      e.target.play();
+    }
+  };
+
+  handleMouseLeave = e => {
+    if (this.hoverToPlay()) {
+      e.target.pause();
+      e.target.currentTime = 0;
+    }
+  };
+
+  hoverToPlay () {
+    return !autoPlayGif && ['gifv', 'video'].indexOf(this.props.attachment.get('type')) !== -1;
+  }
+
+  handleClick = e => {
+    if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
+      e.preventDefault();
+
+      if (this.state.visible) {
+        this.props.onOpenMedia(this.props.attachment);
+      } else {
+        this.setState({ visible: true });
+      }
+    }
+  };
+
+  render () {
+    const { attachment, displayWidth } = this.props;
+    const { visible, loaded } = this.state;
+
+    const width  = `${Math.floor((displayWidth - 4) / 3) - 4}px`;
+    const height = width;
+    const status = attachment.get('status');
+    const title  = status.get('spoiler_text') || attachment.get('description');
+
+    let thumbnail, label, icon, content;
+
+    if (!visible) {
+      icon = (
+        <span className='account-gallery__item__icons'>
+          <Icon id='eye-slash' />
+        </span>
+      );
+    } else {
+      if (['audio', 'video'].includes(attachment.get('type'))) {
+        content = (
+          <img
+            src={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
+            alt={attachment.get('description')}
+            lang={status.get('language')}
+            onLoad={this.handleImageLoad}
+          />
+        );
+
+        if (attachment.get('type') === 'audio') {
+          label = <Icon id='music' />;
+        } else {
+          label = <Icon id='play' />;
+        }
+      } else if (attachment.get('type') === 'image') {
+        const focusX = attachment.getIn(['meta', 'focus', 'x']) || 0;
+        const focusY = attachment.getIn(['meta', 'focus', 'y']) || 0;
+        const x      = ((focusX /  2) + .5) * 100;
+        const y      = ((focusY / -2) + .5) * 100;
+
+        content = (
+          <img
+            src={attachment.get('preview_url')}
+            alt={attachment.get('description')}
+            lang={status.get('language')}
+            style={{ objectPosition: `${x}% ${y}%` }}
+            onLoad={this.handleImageLoad}
+          />
+        );
+      } else if (attachment.get('type') === 'gifv') {
+        content = (
+          <video
+            className='media-gallery__item-gifv-thumbnail'
+            aria-label={attachment.get('description')}
+            title={attachment.get('description')}
+            lang={status.get('language')}
+            role='application'
+            src={attachment.get('url')}
+            onMouseEnter={this.handleMouseEnter}
+            onMouseLeave={this.handleMouseLeave}
+            autoPlay={autoPlayGif}
+            playsInline
+            loop
+            muted
+          />
+        );
+
+        label = 'GIF';
+      }
+
+      thumbnail = (
+        <div className='media-gallery__gifv'>
+          {content}
+
+          {label && (
+            <div className='media-gallery__item__badges'>
+              <span className='media-gallery__gifv__label'>{label}</span>
+            </div>
+          )}
+        </div>
+      );
+    }
+
+    return (
+      <div className='account-gallery__item' style={{ width, height }}>
+        <a className='media-gallery__item-thumbnail' href={status.get('url')} onClick={this.handleClick} title={title} target='_blank' rel='noopener noreferrer'>
+          <Blurhash
+            hash={attachment.get('blurhash')}
+            className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })}
+            dummy={!useBlurhash}
+          />
+
+          {visible ? thumbnail : icon}
+        </a>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/account_gallery/index.jsx b/app/javascript/flavours/blobfox/features/account_gallery/index.jsx
new file mode 100644
index 00000000000000..7ef3bc688c6d39
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/account_gallery/index.jsx
@@ -0,0 +1,239 @@
+import PropTypes from 'prop-types';
+
+import { FormattedMessage } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
+import { lookupAccount, fetchAccount } from 'flavours/blobfox/actions/accounts';
+import { openModal } from 'flavours/blobfox/actions/modal';
+import { LoadMore } from 'flavours/blobfox/components/load_more';
+import { LoadingIndicator } from 'flavours/blobfox/components/loading_indicator';
+import ScrollContainer from 'flavours/blobfox/containers/scroll_container';
+import ProfileColumnHeader from 'flavours/blobfox/features/account/components/profile_column_header';
+import BundleColumnError from 'flavours/blobfox/features/ui/components/bundle_column_error';
+import { normalizeForLookup } from 'flavours/blobfox/reducers/accounts_map';
+import { getAccountGallery } from 'flavours/blobfox/selectors';
+
+import { expandAccountMediaTimeline } from '../../actions/timelines';
+import HeaderContainer from '../account_timeline/containers/header_container';
+import Column from '../ui/components/column';
+
+import MediaItem from './components/media_item';
+
+const mapStateToProps = (state, { params: { acct, id } }) => {
+  const accountId = id || state.getIn(['accounts_map', normalizeForLookup(acct)]);
+
+  if (!accountId) {
+    return {
+      isLoading: true,
+    };
+  }
+
+  return {
+    accountId,
+    isAccount: !!state.getIn(['accounts', accountId]),
+    attachments: getAccountGallery(state, accountId),
+    isLoading: state.getIn(['timelines', `account:${accountId}:media`, 'isLoading']),
+    hasMore: state.getIn(['timelines', `account:${accountId}:media`, 'hasMore']),
+    suspended: state.getIn(['accounts', accountId, 'suspended'], false),
+  };
+};
+
+class LoadMoreMedia extends ImmutablePureComponent {
+
+  static propTypes = {
+    maxId: PropTypes.string,
+    onLoadMore: PropTypes.func.isRequired,
+  };
+
+  handleLoadMore = () => {
+    this.props.onLoadMore(this.props.maxId);
+  };
+
+  render () {
+    return (
+      <LoadMore
+        disabled={this.props.disabled}
+        onClick={this.handleLoadMore}
+      />
+    );
+  }
+
+}
+
+class AccountGallery extends ImmutablePureComponent {
+
+  static propTypes = {
+    params: PropTypes.shape({
+      acct: PropTypes.string,
+      id: PropTypes.string,
+    }).isRequired,
+    accountId: PropTypes.string,
+    dispatch: PropTypes.func.isRequired,
+    attachments: ImmutablePropTypes.list.isRequired,
+    isLoading: PropTypes.bool,
+    hasMore: PropTypes.bool,
+    isAccount: PropTypes.bool,
+    suspended: PropTypes.bool,
+    multiColumn: PropTypes.bool,
+  };
+
+  state = {
+    width: 323,
+  };
+
+  _load () {
+    const { accountId, isAccount, dispatch } = this.props;
+
+    if (!isAccount) dispatch(fetchAccount(accountId));
+    dispatch(expandAccountMediaTimeline(accountId));
+  }
+
+  componentDidMount () {
+    const { params: { acct }, accountId, dispatch } = this.props;
+
+    if (accountId) {
+      this._load();
+    } else {
+      dispatch(lookupAccount(acct));
+    }
+  }
+
+  componentDidUpdate (prevProps) {
+    const { params: { acct }, accountId, dispatch } = this.props;
+
+    if (prevProps.accountId !== accountId && accountId) {
+      this._load();
+    } else if (prevProps.params.acct !== acct) {
+      dispatch(lookupAccount(acct));
+    }
+  }
+
+  handleHeaderClick = () => {
+    this.column.scrollTop();
+  };
+
+  handleScrollToBottom = () => {
+    if (this.props.hasMore) {
+      this.handleLoadMore(this.props.attachments.size > 0 ? this.props.attachments.last().getIn(['status', 'id']) : undefined);
+    }
+  };
+
+  handleScroll = e => {
+    const { scrollTop, scrollHeight, clientHeight } = e.target;
+    const offset = scrollHeight - scrollTop - clientHeight;
+
+    if (150 > offset && !this.props.isLoading) {
+      this.handleScrollToBottom();
+    }
+  };
+
+  handleLoadMore = maxId => {
+    this.props.dispatch(expandAccountMediaTimeline(this.props.accountId, { maxId }));
+  };
+
+  handleLoadOlder = e => {
+    e.preventDefault();
+    this.handleScrollToBottom();
+  };
+
+  setColumnRef = c => {
+    this.column = c;
+  };
+
+  handleOpenMedia = attachment => {
+    const { dispatch } = this.props;
+    const statusId = attachment.getIn(['status', 'id']);
+    const lang = attachment.getIn(['status', 'language']);
+
+    if (attachment.get('type') === 'video') {
+      dispatch(openModal({
+        modalType: 'VIDEO',
+        modalProps: { media: attachment, statusId, lang, options: { autoPlay: true } },
+      }));
+    } else if (attachment.get('type') === 'audio') {
+      dispatch(openModal({
+        modalType: 'AUDIO',
+        modalProps: { media: attachment, statusId, lang, options: { autoPlay: true } },
+      }));
+    } else {
+      const media = attachment.getIn(['status', 'media_attachments']);
+      const index = media.findIndex(x => x.get('id') === attachment.get('id'));
+
+      dispatch(openModal({
+        modalType: 'MEDIA',
+        modalProps: { media, index, statusId, lang },
+      }));
+    }
+  };
+
+  handleRef = c => {
+    if (c) {
+      this.setState({ width: c.offsetWidth });
+    }
+  };
+
+  render () {
+    const { attachments, isLoading, hasMore, isAccount, multiColumn, suspended } = this.props;
+    const { width } = this.state;
+
+    if (!isAccount) {
+      return (
+        <BundleColumnError multiColumn={multiColumn} errorType='routing' />
+      );
+    }
+
+    if (!attachments && isLoading) {
+      return (
+        <Column>
+          <LoadingIndicator />
+        </Column>
+      );
+    }
+
+    let loadOlder = null;
+
+    if (hasMore && !(isLoading && attachments.size === 0)) {
+      loadOlder = <LoadMore visible={!isLoading} onClick={this.handleLoadOlder} />;
+    }
+
+    return (
+      <Column ref={this.setColumnRef}>
+        <ProfileColumnHeader onClick={this.handleHeaderClick} multiColumn={multiColumn} />
+
+        <ScrollContainer scrollKey='account_gallery'>
+          <div className='scrollable scrollable--flex' onScroll={this.handleScroll}>
+            <HeaderContainer accountId={this.props.accountId} />
+
+            {suspended ? (
+              <div className='empty-column-indicator'>
+                <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />
+              </div>
+            ) : (
+              <div role='feed' className='account-gallery__container' ref={this.handleRef}>
+                {attachments.map((attachment, index) => attachment === null ? (
+                  <LoadMoreMedia key={'more:' + attachments.getIn(index + 1, 'id')} maxId={index > 0 ? attachments.getIn(index - 1, 'id') : null} onLoadMore={this.handleLoadMore} />
+                ) : (
+                  <MediaItem key={attachment.get('id')} attachment={attachment} displayWidth={width} onOpenMedia={this.handleOpenMedia} />
+                ))}
+
+                {loadOlder}
+              </div>
+            )}
+
+            {isLoading && attachments.size === 0 && (
+              <div className='scrollable__append'>
+                <LoadingIndicator />
+              </div>
+            )}
+          </div>
+        </ScrollContainer>
+      </Column>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(AccountGallery);
diff --git a/app/javascript/flavours/blobfox/features/account_timeline/components/header.jsx b/app/javascript/flavours/blobfox/features/account_timeline/components/header.jsx
new file mode 100644
index 00000000000000..cb9fc5b87f4650
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/account_timeline/components/header.jsx
@@ -0,0 +1,165 @@
+import PropTypes from 'prop-types';
+
+import { FormattedMessage } from 'react-intl';
+
+import { NavLink, withRouter } from 'react-router-dom';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import { WithRouterPropTypes } from 'flavours/blobfox/utils/react_router';
+
+import ActionBar from '../../account/components/action_bar';
+import InnerHeader from '../../account/components/header';
+
+import MemorialNote from './memorial_note';
+import MovedNote from './moved_note';
+
+class Header extends ImmutablePureComponent {
+
+  static propTypes = {
+    account: ImmutablePropTypes.record,
+    onFollow: PropTypes.func.isRequired,
+    onBlock: PropTypes.func.isRequired,
+    onMention: PropTypes.func.isRequired,
+    onDirect: PropTypes.func.isRequired,
+    onReblogToggle: PropTypes.func.isRequired,
+    onReport: PropTypes.func.isRequired,
+    onMute: PropTypes.func.isRequired,
+    onBlockDomain: PropTypes.func.isRequired,
+    onUnblockDomain: PropTypes.func.isRequired,
+    onEndorseToggle: PropTypes.func.isRequired,
+    onAddToList: PropTypes.func.isRequired,
+    onChangeLanguages: PropTypes.func.isRequired,
+    onInteractionModal: PropTypes.func.isRequired,
+    onOpenAvatar: PropTypes.func.isRequired,
+    hideTabs: PropTypes.bool,
+    domain: PropTypes.string.isRequired,
+    hidden: PropTypes.bool,
+    ...WithRouterPropTypes,
+  };
+
+  handleFollow = () => {
+    this.props.onFollow(this.props.account);
+  };
+
+  handleBlock = () => {
+    this.props.onBlock(this.props.account);
+  };
+
+  handleMention = () => {
+    this.props.onMention(this.props.account, this.props.history);
+  };
+
+  handleDirect = () => {
+    this.props.onDirect(this.props.account, this.props.history);
+  };
+
+  handleReport = () => {
+    this.props.onReport(this.props.account);
+  };
+
+  handleReblogToggle = () => {
+    this.props.onReblogToggle(this.props.account);
+  };
+
+  handleNotifyToggle = () => {
+    this.props.onNotifyToggle(this.props.account);
+  };
+
+  handleMute = () => {
+    this.props.onMute(this.props.account);
+  };
+
+  handleBlockDomain = () => {
+    const domain = this.props.account.get('acct').split('@')[1];
+
+    if (!domain) return;
+
+    this.props.onBlockDomain(domain);
+  };
+
+  handleUnblockDomain = () => {
+    const domain = this.props.account.get('acct').split('@')[1];
+
+    if (!domain) return;
+
+    this.props.onUnblockDomain(domain);
+  };
+
+  handleEndorseToggle = () => {
+    this.props.onEndorseToggle(this.props.account);
+  };
+
+  handleAddToList = () => {
+    this.props.onAddToList(this.props.account);
+  };
+
+  handleEditAccountNote = () => {
+    this.props.onEditAccountNote(this.props.account);
+  };
+
+  handleChangeLanguages = () => {
+    this.props.onChangeLanguages(this.props.account);
+  };
+
+  handleInteractionModal = () => {
+    this.props.onInteractionModal(this.props.account);
+  };
+
+  handleOpenAvatar = () => {
+    this.props.onOpenAvatar(this.props.account);
+  };
+
+  render () {
+    const { account, hidden, hideTabs } = this.props;
+
+    if (account === null) {
+      return null;
+    }
+
+    return (
+      <div className='account-timeline__header'>
+        {(!hidden && account.get('memorial')) && <MemorialNote />}
+        {(!hidden && account.get('moved')) && <MovedNote from={account} to={account.get('moved')} />}
+
+        <InnerHeader
+          account={account}
+          onFollow={this.handleFollow}
+          onBlock={this.handleBlock}
+          onMention={this.handleMention}
+          onDirect={this.handleDirect}
+          onReblogToggle={this.handleReblogToggle}
+          onNotifyToggle={this.handleNotifyToggle}
+          onReport={this.handleReport}
+          onMute={this.handleMute}
+          onBlockDomain={this.handleBlockDomain}
+          onUnblockDomain={this.handleUnblockDomain}
+          onEndorseToggle={this.handleEndorseToggle}
+          onAddToList={this.handleAddToList}
+          onEditAccountNote={this.handleEditAccountNote}
+          onChangeLanguages={this.handleChangeLanguages}
+          onInteractionModal={this.handleInteractionModal}
+          onOpenAvatar={this.handleOpenAvatar}
+          domain={this.props.domain}
+          hidden={hidden}
+        />
+
+        <ActionBar
+          account={account}
+        />
+
+        {!(hideTabs || hidden) && (
+          <div className='account__section-headline'>
+            <NavLink exact to={`/@${account.get('acct')}`}><FormattedMessage id='account.posts' defaultMessage='Posts' /></NavLink>
+            <NavLink exact to={`/@${account.get('acct')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Posts and replies' /></NavLink>
+            <NavLink exact to={`/@${account.get('acct')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink>
+          </div>
+        )}
+      </div>
+    );
+  }
+
+}
+
+export default withRouter(Header);
diff --git a/app/javascript/flavours/blobfox/features/account_timeline/components/limited_account_hint.tsx b/app/javascript/flavours/blobfox/features/account_timeline/components/limited_account_hint.tsx
new file mode 100644
index 00000000000000..8096dff4996146
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/account_timeline/components/limited_account_hint.tsx
@@ -0,0 +1,35 @@
+import { useCallback } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import { revealAccount } from 'flavours/blobfox/actions/accounts_typed';
+import { Button } from 'flavours/blobfox/components/button';
+import { domain } from 'flavours/blobfox/initial_state';
+import { useAppDispatch } from 'flavours/blobfox/store';
+
+export const LimitedAccountHint: React.FC<{ accountId: string }> = ({
+  accountId,
+}) => {
+  const dispatch = useAppDispatch();
+  const reveal = useCallback(() => {
+    dispatch(revealAccount({ id: accountId }));
+  }, [dispatch, accountId]);
+
+  return (
+    <div className='limited-account-hint'>
+      <p>
+        <FormattedMessage
+          id='limited_account_hint.title'
+          defaultMessage='This profile has been hidden by the moderators of {domain}.'
+          values={{ domain }}
+        />
+      </p>
+      <Button onClick={reveal}>
+        <FormattedMessage
+          id='limited_account_hint.action'
+          defaultMessage='Show profile anyway'
+        />
+      </Button>
+    </div>
+  );
+};
diff --git a/app/javascript/flavours/blobfox/features/account_timeline/components/memorial_note.jsx b/app/javascript/flavours/blobfox/features/account_timeline/components/memorial_note.jsx
new file mode 100644
index 00000000000000..a04808f1caf737
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/account_timeline/components/memorial_note.jsx
@@ -0,0 +1,11 @@
+import { FormattedMessage } from 'react-intl';
+
+const MemorialNote = () => (
+  <div className='account-memorial-banner'>
+    <div className='account-memorial-banner__message'>
+      <FormattedMessage id='account.in_memoriam' defaultMessage='In Memoriam.' />
+    </div>
+  </div>
+);
+
+export default MemorialNote;
diff --git a/app/javascript/flavours/blobfox/features/account_timeline/components/moved_note.jsx b/app/javascript/flavours/blobfox/features/account_timeline/components/moved_note.jsx
new file mode 100644
index 00000000000000..aecb30827a9e32
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/account_timeline/components/moved_note.jsx
@@ -0,0 +1,52 @@
+import { FormattedMessage } from 'react-intl';
+
+import { withRouter } from 'react-router-dom';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import { Icon } from 'flavours/blobfox/components/icon';
+import { WithRouterPropTypes } from 'flavours/blobfox/utils/react_router';
+
+import { AvatarOverlay } from '../../../components/avatar_overlay';
+import { DisplayName } from '../../../components/display_name';
+
+class MovedNote extends ImmutablePureComponent {
+
+  static propTypes = {
+    from: ImmutablePropTypes.map.isRequired,
+    to: ImmutablePropTypes.map.isRequired,
+    ...WithRouterPropTypes,
+  };
+
+  handleAccountClick = e => {
+    if (e.button === 0) {
+      e.preventDefault();
+      this.props.history.push(`/@${this.props.to.get('acct')}`);
+    }
+
+    e.stopPropagation();
+  };
+
+  render () {
+    const { from, to } = this.props;
+    const displayNameHtml = { __html: from.get('display_name_html') };
+
+    return (
+      <div className='account__moved-note'>
+        <div className='account__moved-note__message'>
+          <div className='account__moved-note__icon-wrapper'><Icon id='suitcase' className='account__moved-note__icon' fixedWidth /></div>
+          <FormattedMessage id='account.moved_to' defaultMessage='{name} has indicated that their new account is now:' values={{ name: <bdi><strong dangerouslySetInnerHTML={displayNameHtml} /></bdi> }} />
+        </div>
+
+        <a href={to.get('url')} onClick={this.handleAccountClick} className='detailed-status__display-name'>
+          <div className='detailed-status__display-avatar'><AvatarOverlay account={to} friend={from} /></div>
+          <DisplayName account={to} />
+        </a>
+      </div>
+    );
+  }
+
+}
+
+export default withRouter(MovedNote);
diff --git a/app/javascript/flavours/blobfox/features/account_timeline/containers/header_container.jsx b/app/javascript/flavours/blobfox/features/account_timeline/containers/header_container.jsx
new file mode 100644
index 00000000000000..c3a3de71ed9c3b
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/account_timeline/containers/header_container.jsx
@@ -0,0 +1,186 @@
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import { connect } from 'react-redux';
+
+import {
+  followAccount,
+  unfollowAccount,
+  unblockAccount,
+  unmuteAccount,
+  pinAccount,
+  unpinAccount,
+} from '../../../actions/accounts';
+import { initBlockModal } from '../../../actions/blocks';
+import {
+  mentionCompose,
+  directCompose,
+} from '../../../actions/compose';
+import { blockDomain, unblockDomain } from '../../../actions/domain_blocks';
+import { openModal } from '../../../actions/modal';
+import { initMuteModal } from '../../../actions/mutes';
+import { initReport } from '../../../actions/reports';
+import { unfollowModal } from '../../../initial_state';
+import { makeGetAccount, getAccountHidden } from '../../../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' },
+});
+
+const makeMapStateToProps = () => {
+  const getAccount = makeGetAccount();
+
+  const mapStateToProps = (state, { accountId }) => ({
+    account: getAccount(state, accountId),
+    domain: state.getIn(['meta', 'domain']),
+    hidden: getAccountHidden(state, accountId),
+  });
+
+  return mapStateToProps;
+};
+
+const mapDispatchToProps = (dispatch, { intl }) => ({
+
+  onFollow (account) {
+    if (account.getIn(['relationship', 'following'])) {
+      if (unfollowModal) {
+        dispatch(openModal({
+          modalType: 'CONFIRM',
+          modalProps: {
+            message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
+            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: <FormattedMessage id='confirmations.cancel_follow_request.message' defaultMessage='Are you sure you want to withdraw your request to follow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
+            confirm: intl.formatMessage(messages.cancelFollowRequestConfirm),
+            onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
+          },
+        }));
+      } else {
+        dispatch(unfollowAccount(account.get('id')));
+      }
+    } else {
+      dispatch(followAccount(account.get('id')));
+    }
+  },
+
+  onInteractionModal (account) {
+    dispatch(openModal({
+      modalType: 'INTERACTION',
+      modalProps: {
+        type: 'follow',
+        accountId: account.get('id'),
+        url: account.get('uri'),
+      },
+    }));
+  },
+
+  onBlock (account) {
+    if (account.getIn(['relationship', 'blocking'])) {
+      dispatch(unblockAccount(account.get('id')));
+    } else {
+      dispatch(initBlockModal(account));
+    }
+  },
+
+  onMention (account, router) {
+    dispatch(mentionCompose(account, router));
+  },
+
+  onDirect (account, router) {
+    dispatch(directCompose(account, router));
+  },
+
+  onReblogToggle (account) {
+    if (account.getIn(['relationship', 'showing_reblogs'])) {
+      dispatch(followAccount(account.get('id'), { reblogs: false }));
+    } else {
+      dispatch(followAccount(account.get('id'), { reblogs: true }));
+    }
+  },
+
+  onEndorseToggle (account) {
+    if (account.getIn(['relationship', 'endorsed'])) {
+      dispatch(unpinAccount(account.get('id')));
+    } else {
+      dispatch(pinAccount(account.get('id')));
+    }
+  },
+
+  onNotifyToggle (account) {
+    if (account.getIn(['relationship', 'notifying'])) {
+      dispatch(followAccount(account.get('id'), { notify: false }));
+    } else {
+      dispatch(followAccount(account.get('id'), { notify: true }));
+    }
+  },
+
+  onReport (account) {
+    dispatch(initReport(account));
+  },
+
+  onMute (account) {
+    if (account.getIn(['relationship', 'muting'])) {
+      dispatch(unmuteAccount(account.get('id')));
+    } else {
+      dispatch(initMuteModal(account));
+    }
+  },
+
+  onBlockDomain (domain) {
+    dispatch(openModal({
+      modalType: 'CONFIRM',
+      modalProps: {
+        message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />,
+        confirm: intl.formatMessage(messages.blockDomainConfirm),
+        onConfirm: () => dispatch(blockDomain(domain)),
+      },
+    }));
+  },
+
+  onUnblockDomain (domain) {
+    dispatch(unblockDomain(domain));
+  },
+
+  onAddToList (account) {
+    dispatch(openModal({
+      modalType: 'LIST_ADDER',
+      modalProps: {
+        accountId: account.get('id'),
+      },
+    }));
+  },
+
+  onChangeLanguages (account) {
+    dispatch(openModal({
+      modalType: 'SUBSCRIBED_LANGUAGES',
+      modalProps: {
+        accountId: account.get('id'),
+      },
+    }));
+  },
+
+  onOpenAvatar (account) {
+    dispatch(openModal({
+      modalType: 'IMAGE',
+      modalProps: {
+        src: account.get('avatar'),
+        alt: account.get('acct'),
+      },
+    }));
+  },
+
+});
+
+export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header));
diff --git a/app/javascript/flavours/blobfox/features/account_timeline/index.jsx b/app/javascript/flavours/blobfox/features/account_timeline/index.jsx
new file mode 100644
index 00000000000000..4ecd55dff43228
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/account_timeline/index.jsx
@@ -0,0 +1,210 @@
+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 { connect } from 'react-redux';
+
+import { TimelineHint } from 'flavours/blobfox/components/timeline_hint';
+import ProfileColumnHeader from 'flavours/blobfox/features/account/components/profile_column_header';
+import BundleColumnError from 'flavours/blobfox/features/ui/components/bundle_column_error';
+import { normalizeForLookup } from 'flavours/blobfox/reducers/accounts_map';
+import { getAccountHidden } from 'flavours/blobfox/selectors';
+
+import { lookupAccount, fetchAccount } from '../../actions/accounts';
+import { fetchFeaturedTags } from '../../actions/featured_tags';
+import { expandAccountFeaturedTimeline, expandAccountTimeline } from '../../actions/timelines';
+import { LoadingIndicator } from '../../components/loading_indicator';
+import StatusList from '../../components/status_list';
+import Column from '../ui/components/column';
+
+import { LimitedAccountHint } from './components/limited_account_hint';
+import HeaderContainer from './containers/header_container';
+
+const emptyList = ImmutableList();
+
+const mapStateToProps = (state, { params: { acct, id, tagged }, withReplies = false }) => {
+  const accountId = id || state.getIn(['accounts_map', normalizeForLookup(acct)]);
+
+  if (accountId === null) {
+    return {
+      isLoading: false,
+      isAccount: false,
+      statusIds: emptyList,
+    };
+  } else if (!accountId) {
+    return {
+      isLoading: true,
+      statusIds: emptyList,
+    };
+  }
+
+  const path = withReplies ? `${accountId}:with_replies` : `${accountId}${tagged ? `:${tagged}` : ''}`;
+
+  return {
+    accountId,
+    remote: !!(state.getIn(['accounts', accountId, 'acct']) !== state.getIn(['accounts', accountId, 'username'])),
+    remoteUrl: state.getIn(['accounts', accountId, 'url']),
+    isAccount: !!state.getIn(['accounts', accountId]),
+    statusIds: state.getIn(['timelines', `account:${path}`, 'items'], ImmutableList()),
+    featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned${tagged ? `:${tagged}` : ''}`, 'items'], ImmutableList()),
+    isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
+    hasMore:   state.getIn(['timelines', `account:${path}`, 'hasMore']),
+    suspended: state.getIn(['accounts', accountId, 'suspended'], false),
+    hidden: getAccountHidden(state, accountId),
+  };
+};
+
+const RemoteHint = ({ url }) => (
+  <TimelineHint url={url} resource={<FormattedMessage id='timeline_hint.resources.statuses' defaultMessage='Older posts' />} />
+);
+
+RemoteHint.propTypes = {
+  url: PropTypes.string.isRequired,
+};
+
+class AccountTimeline extends ImmutablePureComponent {
+
+  static propTypes = {
+    params: PropTypes.shape({
+      acct: PropTypes.string,
+      id: PropTypes.string,
+      tagged: PropTypes.string,
+    }).isRequired,
+    accountId: PropTypes.string,
+    dispatch: PropTypes.func.isRequired,
+    statusIds: ImmutablePropTypes.list,
+    featuredStatusIds: ImmutablePropTypes.list,
+    isLoading: PropTypes.bool,
+    hasMore: PropTypes.bool,
+    withReplies: PropTypes.bool,
+    isAccount: PropTypes.bool,
+    suspended: PropTypes.bool,
+    hidden: PropTypes.bool,
+    remote: PropTypes.bool,
+    remoteUrl: PropTypes.string,
+    multiColumn: PropTypes.bool,
+  };
+
+  _load () {
+    const { accountId, withReplies, params: { tagged }, dispatch } = this.props;
+
+    dispatch(fetchAccount(accountId));
+
+    if (!withReplies) {
+      dispatch(expandAccountFeaturedTimeline(accountId, { tagged }));
+    }
+
+    dispatch(fetchFeaturedTags(accountId));
+    dispatch(expandAccountTimeline(accountId, { withReplies, tagged }));
+  }
+
+  componentDidMount () {
+    const { params: { acct }, accountId, dispatch } = this.props;
+
+    if (accountId) {
+      this._load();
+    } else {
+      dispatch(lookupAccount(acct));
+    }
+  }
+
+  componentDidUpdate (prevProps) {
+    const { params: { acct, tagged }, accountId, withReplies, dispatch } = this.props;
+
+    if (prevProps.accountId !== accountId && accountId) {
+      this._load();
+    } else if (prevProps.params.acct !== acct) {
+      dispatch(lookupAccount(acct));
+    } else if (prevProps.params.tagged !== tagged) {
+      if (!withReplies) {
+        dispatch(expandAccountFeaturedTimeline(accountId, { tagged }));
+      }
+      dispatch(expandAccountTimeline(accountId, { withReplies, tagged }));
+    }
+  }
+
+  UNSAFE_componentWillReceiveProps (nextProps) {
+    const { dispatch } = this.props;
+
+    if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) {
+      dispatch(fetchAccount(nextProps.params.accountId));
+
+      if (!nextProps.withReplies) {
+        dispatch(expandAccountFeaturedTimeline(nextProps.params.accountId));
+      }
+
+      dispatch(expandAccountTimeline(nextProps.params.accountId, { withReplies: nextProps.params.withReplies }));
+    }
+  }
+
+  handleHeaderClick = () => {
+    this.column.scrollTop();
+  };
+
+  handleLoadMore = maxId => {
+    this.props.dispatch(expandAccountTimeline(this.props.accountId, { maxId, withReplies: this.props.withReplies, tagged: this.props.params.tagged }));
+  };
+
+  setRef = c => {
+    this.column = c;
+  };
+
+  render () {
+    const { accountId, statusIds, featuredStatusIds, isLoading, hasMore, suspended, isAccount, hidden, multiColumn, remote, remoteUrl } = this.props;
+
+    if (isLoading && statusIds.isEmpty()) {
+      return (
+        <Column>
+          <LoadingIndicator />
+        </Column>
+      );
+    } else if (!isLoading && !isAccount) {
+      return (
+        <BundleColumnError multiColumn={multiColumn} errorType='routing' />
+      );
+    }
+
+    let emptyMessage;
+
+    const forceEmptyState = suspended || hidden;
+
+    if (suspended) {
+      emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
+    } else if (hidden) {
+      emptyMessage = <LimitedAccountHint accountId={accountId} />;
+    } else if (remote && statusIds.isEmpty()) {
+      emptyMessage = <RemoteHint url={remoteUrl} />;
+    } else {
+      emptyMessage = <FormattedMessage id='empty_column.account_timeline' defaultMessage='No posts found' />;
+    }
+
+    const remoteMessage = remote ? <RemoteHint url={remoteUrl} /> : null;
+
+    return (
+      <Column ref={this.setRef}>
+        <ProfileColumnHeader onClick={this.handleHeaderClick} multiColumn={multiColumn} />
+
+        <StatusList
+          prepend={<HeaderContainer accountId={this.props.accountId} hideTabs={forceEmptyState} tagged={this.props.params.tagged} />}
+          alwaysPrepend
+          append={remoteMessage}
+          scrollKey='account_timeline'
+          statusIds={forceEmptyState ? emptyList : statusIds}
+          featuredStatusIds={featuredStatusIds}
+          isLoading={isLoading}
+          hasMore={!forceEmptyState && hasMore}
+          onLoadMore={this.handleLoadMore}
+          emptyMessage={emptyMessage}
+          bindToDocument={!multiColumn}
+          timelineId='account'
+        />
+      </Column>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(AccountTimeline);
diff --git a/app/javascript/flavours/blobfox/features/audio/index.jsx b/app/javascript/flavours/blobfox/features/audio/index.jsx
new file mode 100644
index 00000000000000..199eda8a7eb633
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/audio/index.jsx
@@ -0,0 +1,600 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
+
+import classNames from 'classnames';
+
+import { is } from 'immutable';
+
+import { throttle, debounce } from 'lodash';
+
+import { Icon }  from 'flavours/blobfox/components/icon';
+import { formatTime, getPointerPosition, fileNameFromURL } from 'flavours/blobfox/features/video';
+
+import { Blurhash } from '../../components/blurhash';
+import { displayMedia, useBlurhash } from '../../initial_state';
+
+import Visualizer from './visualizer';
+
+const messages = defineMessages({
+  play: { id: 'video.play', defaultMessage: 'Play' },
+  pause: { id: 'video.pause', defaultMessage: 'Pause' },
+  mute: { id: 'video.mute', defaultMessage: 'Mute sound' },
+  unmute: { id: 'video.unmute', defaultMessage: 'Unmute sound' },
+  download: { id: 'video.download', defaultMessage: 'Download file' },
+  hide: { id: 'audio.hide', defaultMessage: 'Hide audio' },
+});
+
+const TICK_SIZE = 10;
+const PADDING   = 180;
+
+class Audio extends PureComponent {
+
+  static propTypes = {
+    src: PropTypes.string.isRequired,
+    alt: PropTypes.string,
+    lang: PropTypes.string,
+    poster: PropTypes.string,
+    duration: PropTypes.number,
+    width: PropTypes.number,
+    height: PropTypes.number,
+    sensitive: PropTypes.bool,
+    editable: PropTypes.bool,
+    fullscreen: PropTypes.bool,
+    intl: PropTypes.object.isRequired,
+    blurhash: PropTypes.string,
+    cacheWidth: PropTypes.func,
+    visible: PropTypes.bool,
+    onToggleVisibility: PropTypes.func,
+    backgroundColor: PropTypes.string,
+    foregroundColor: PropTypes.string,
+    accentColor: PropTypes.string,
+    currentTime: PropTypes.number,
+    autoPlay: PropTypes.bool,
+    volume: PropTypes.number,
+    muted: PropTypes.bool,
+    deployPictureInPicture: PropTypes.func,
+  };
+
+  state = {
+    width: this.props.width,
+    currentTime: 0,
+    buffer: 0,
+    duration: null,
+    paused: true,
+    muted: false,
+    volume: 1,
+    dragging: false,
+    revealed: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'),
+  };
+
+  constructor (props) {
+    super(props);
+    this.visualizer = new Visualizer(TICK_SIZE);
+  }
+
+  setPlayerRef = c => {
+    this.player = c;
+
+    if (this.player) {
+      this._setDimensions();
+    }
+  };
+
+  _pack() {
+    return {
+      src: this.props.src,
+      volume: this.state.volume,
+      muted: this.state.muted,
+      currentTime: this.audio.currentTime,
+      poster: this.props.poster,
+      backgroundColor: this.props.backgroundColor,
+      foregroundColor: this.props.foregroundColor,
+      accentColor: this.props.accentColor,
+      sensitive: this.props.sensitive,
+      visible: this.props.visible,
+    };
+  }
+
+  _setDimensions () {
+    const width  = this.player.offsetWidth;
+    const height = this.props.fullscreen ? this.player.offsetHeight : (width / (16/9));
+
+    if (width && width !== this.state.containerWidth) {
+      if (this.props.cacheWidth) {
+        this.props.cacheWidth(width);
+      }
+
+      this.setState({ width, height });
+    }
+  }
+
+  setSeekRef = c => {
+    this.seek = c;
+  };
+
+  setVolumeRef = c => {
+    this.volume = c;
+  };
+
+  setAudioRef = c => {
+    this.audio = c;
+
+    if (this.audio) {
+      this.audio.volume = 1;
+      this.audio.muted = false;
+    }
+  };
+
+  setCanvasRef = c => {
+    this.canvas = c;
+
+    this.visualizer.setCanvas(c);
+  };
+
+  componentDidMount () {
+    window.addEventListener('scroll', this.handleScroll);
+    window.addEventListener('resize', this.handleResize, { passive: true });
+  }
+
+  componentDidUpdate (prevProps, prevState) {
+    if (this.player) {
+      this._setDimensions();
+    }
+
+    if (prevProps.src !== this.props.src || this.state.width !== prevState.width || this.state.height !== prevState.height || prevProps.accentColor !== this.props.accentColor) {
+      this._clear();
+      this._draw();
+    }
+  }
+
+  UNSAFE_componentWillReceiveProps (nextProps) {
+    if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) {
+      this.setState({ revealed: nextProps.visible });
+    }
+  }
+
+  componentWillUnmount () {
+    window.removeEventListener('scroll', this.handleScroll);
+    window.removeEventListener('resize', this.handleResize);
+
+    if (!this.state.paused && this.audio && this.props.deployPictureInPicture) {
+      this.props.deployPictureInPicture('audio', this._pack());
+    }
+  }
+
+  togglePlay = () => {
+    if (!this.audioContext) {
+      this._initAudioContext();
+    }
+
+    if (this.state.paused) {
+      this.setState({ paused: false }, () => this.audio.play());
+    } else {
+      this.setState({ paused: true }, () => this.audio.pause());
+    }
+  };
+
+  handleResize = debounce(() => {
+    if (this.player) {
+      this._setDimensions();
+    }
+  }, 250, {
+    trailing: true,
+  });
+
+  handlePlay = () => {
+    this.setState({ paused: false });
+
+    if (this.audioContext && this.audioContext.state === 'suspended') {
+      this.audioContext.resume();
+    }
+
+    this._renderCanvas();
+  };
+
+  handlePause = () => {
+    this.setState({ paused: true });
+
+    if (this.audioContext) {
+      this.audioContext.suspend();
+    }
+  };
+
+  handleProgress = () => {
+    const lastTimeRange = this.audio.buffered.length - 1;
+
+    if (lastTimeRange > -1) {
+      this.setState({ buffer: Math.ceil(this.audio.buffered.end(lastTimeRange) / this.audio.duration * 100) });
+    }
+  };
+
+  toggleMute = () => {
+    const muted = !(this.state.muted || this.state.volume === 0);
+
+    this.setState((state) => ({ muted, volume: Math.max(state.volume || 0.5, 0.05) }), () => {
+      if (this.gainNode) {
+        this.gainNode.gain.value = this.state.muted ? 0 : this.state.volume;
+      }
+    });
+  };
+
+  toggleReveal = () => {
+    if (this.props.onToggleVisibility) {
+      this.props.onToggleVisibility();
+    } else {
+      this.setState({ revealed: !this.state.revealed });
+    }
+  };
+
+  handleVolumeMouseDown = e => {
+    document.addEventListener('mousemove', this.handleMouseVolSlide, true);
+    document.addEventListener('mouseup', this.handleVolumeMouseUp, true);
+    document.addEventListener('touchmove', this.handleMouseVolSlide, true);
+    document.addEventListener('touchend', this.handleVolumeMouseUp, true);
+
+    this.handleMouseVolSlide(e);
+
+    e.preventDefault();
+    e.stopPropagation();
+  };
+
+  handleVolumeMouseUp = () => {
+    document.removeEventListener('mousemove', this.handleMouseVolSlide, true);
+    document.removeEventListener('mouseup', this.handleVolumeMouseUp, true);
+    document.removeEventListener('touchmove', this.handleMouseVolSlide, true);
+    document.removeEventListener('touchend', this.handleVolumeMouseUp, true);
+  };
+
+  handleMouseDown = e => {
+    document.addEventListener('mousemove', this.handleMouseMove, true);
+    document.addEventListener('mouseup', this.handleMouseUp, true);
+    document.addEventListener('touchmove', this.handleMouseMove, true);
+    document.addEventListener('touchend', this.handleMouseUp, true);
+
+    this.setState({ dragging: true });
+    this.audio.pause();
+    this.handleMouseMove(e);
+
+    e.preventDefault();
+    e.stopPropagation();
+  };
+
+  handleMouseUp = () => {
+    document.removeEventListener('mousemove', this.handleMouseMove, true);
+    document.removeEventListener('mouseup', this.handleMouseUp, true);
+    document.removeEventListener('touchmove', this.handleMouseMove, true);
+    document.removeEventListener('touchend', this.handleMouseUp, true);
+
+    this.setState({ dragging: false });
+    this.audio.play();
+  };
+
+  handleMouseMove = throttle(e => {
+    const { x } = getPointerPosition(this.seek, e);
+    const currentTime = this.audio.duration * x;
+
+    if (!isNaN(currentTime)) {
+      this.setState({ currentTime }, () => {
+        this.audio.currentTime = currentTime;
+      });
+    }
+  }, 15);
+
+  handleTimeUpdate = () => {
+    this.setState({
+      currentTime: this.audio.currentTime,
+      duration: this.audio.duration,
+    });
+  };
+
+  handleMouseVolSlide = throttle(e => {
+    const { x } = getPointerPosition(this.volume, e);
+
+    if(!isNaN(x)) {
+      this.setState((state) => ({ volume: x, muted: state.muted && x === 0 }), () => {
+        if (this.gainNode) {
+          this.gainNode.gain.value = this.state.muted ? 0 : x;
+        }
+      });
+    }
+  }, 15);
+
+  handleScroll = throttle(() => {
+    if (!this.canvas || !this.audio) {
+      return;
+    }
+
+    const { top, height } = this.canvas.getBoundingClientRect();
+    const inView = (top <= (window.innerHeight || document.documentElement.clientHeight)) && (top + height >= 0);
+
+    if (!this.state.paused && !inView) {
+      this.audio.pause();
+
+      if (this.props.deployPictureInPicture) {
+        this.props.deployPictureInPicture('audio', this._pack());
+      }
+
+      this.setState({ paused: true });
+    }
+  }, 150, { trailing: true });
+
+  handleMouseEnter = () => {
+    this.setState({ hovered: true });
+  };
+
+  handleMouseLeave = () => {
+    this.setState({ hovered: false });
+  };
+
+  handleLoadedData = () => {
+    const { autoPlay, currentTime } = this.props;
+
+    if (currentTime) {
+      this.audio.currentTime = currentTime;
+    }
+
+    if (autoPlay) {
+      this.togglePlay();
+    }
+  };
+
+  _initAudioContext () {
+    const AudioContext = window.AudioContext || window.webkitAudioContext;
+    const context      = new AudioContext();
+    const source       = context.createMediaElementSource(this.audio);
+    const gainNode     = context.createGain();
+
+    gainNode.gain.value = this.state.muted ? 0 : this.state.volume;
+
+    this.visualizer.setAudioContext(context, source);
+    source.connect(gainNode);
+    gainNode.connect(context.destination);
+
+    this.audioContext = context;
+    this.gainNode = gainNode;
+  }
+
+  handleDownload = () => {
+    fetch(this.props.src).then(res => res.blob()).then(blob => {
+      const element   = document.createElement('a');
+      const objectURL = URL.createObjectURL(blob);
+
+      element.setAttribute('href', objectURL);
+      element.setAttribute('download', fileNameFromURL(this.props.src));
+
+      document.body.appendChild(element);
+      element.click();
+      document.body.removeChild(element);
+
+      URL.revokeObjectURL(objectURL);
+    }).catch(err => {
+      console.error(err);
+    });
+  };
+
+  _renderCanvas () {
+    requestAnimationFrame(() => {
+      if (!this.audio) return;
+
+      this.handleTimeUpdate();
+      this._clear();
+      this._draw();
+
+      if (!this.state.paused) {
+        this._renderCanvas();
+      }
+    });
+  }
+
+  _clear() {
+    this.visualizer.clear(this.state.width, this.state.height);
+  }
+
+  _draw() {
+    this.visualizer.draw(this._getCX(), this._getCY(), this._getAccentColor(), this._getRadius(), this._getScaleCoefficient());
+  }
+
+  _getRadius () {
+    return parseInt((this.state.height || this.props.height) / 2 - PADDING * this._getScaleCoefficient());
+  }
+
+  _getScaleCoefficient () {
+    return (this.state.height || this.props.height) / 982;
+  }
+
+  _getCX() {
+    return Math.floor(this.state.width / 2);
+  }
+
+  _getCY() {
+    return Math.floor((this.state.height || this.props.height) / 2);
+  }
+
+  _getAccentColor () {
+    return this.props.accentColor || '#ffffff';
+  }
+
+  _getBackgroundColor () {
+    return this.props.backgroundColor || '#000000';
+  }
+
+  _getForegroundColor () {
+    return this.props.foregroundColor || '#ffffff';
+  }
+
+  seekBy (time) {
+    const currentTime = this.audio.currentTime + time;
+
+    if (!isNaN(currentTime)) {
+      this.setState({ currentTime }, () => {
+        this.audio.currentTime = currentTime;
+      });
+    }
+  }
+
+  handleAudioKeyDown = e => {
+    // On the audio element or the seek bar, we can safely use the space bar
+    // for playback control because there are no buttons to press
+
+    if (e.key === ' ') {
+      e.preventDefault();
+      e.stopPropagation();
+      this.togglePlay();
+    }
+  };
+
+  handleKeyDown = e => {
+    switch(e.key) {
+    case 'k':
+      e.preventDefault();
+      e.stopPropagation();
+      this.togglePlay();
+      break;
+    case 'm':
+      e.preventDefault();
+      e.stopPropagation();
+      this.toggleMute();
+      break;
+    case 'j':
+      e.preventDefault();
+      e.stopPropagation();
+      this.seekBy(-10);
+      break;
+    case 'l':
+      e.preventDefault();
+      e.stopPropagation();
+      this.seekBy(10);
+      break;
+    }
+  };
+
+  render () {
+    const { src, intl, alt, lang, editable, autoPlay, sensitive, blurhash } = this.props;
+    const { paused, volume, currentTime, duration, buffer, dragging, revealed } = this.state;
+    const progress = Math.min((currentTime / duration) * 100, 100);
+    const muted = this.state.muted || volume === 0;
+
+    let warning;
+
+    if (sensitive) {
+      warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />;
+    } else {
+      warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />;
+    }
+
+    return (
+      <div className={classNames('audio-player', { editable, inactive: !revealed })} ref={this.setPlayerRef} style={{ backgroundColor: this._getBackgroundColor(), color: this._getForegroundColor(), aspectRatio: '16 / 9' }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} tabIndex={0} onKeyDown={this.handleKeyDown}>
+
+        <Blurhash
+          hash={blurhash}
+          className={classNames('media-gallery__preview', {
+            'media-gallery__preview--hidden': revealed,
+          })}
+          dummy={!useBlurhash}
+        />
+
+        {(revealed || editable) && <audio
+          ref={this.setAudioRef}
+          preload={autoPlay ? 'auto' : 'none'}
+          onPlay={this.handlePlay}
+          onPause={this.handlePause}
+          onProgress={this.handleProgress}
+          onLoadedData={this.handleLoadedData}
+          crossOrigin='anonymous'
+        >
+          <source src={src.replace("/original/", "/opus/").replace(".mp3", ".webm")} type="audio/webm; codecs=opus"/>
+          <source src={src} type="audio/mpeg; codecs=mp3"/>
+        </audio>
+        }
+
+        <canvas
+          role='button'
+          tabIndex={0}
+          className='audio-player__canvas'
+          width={this.state.width}
+          height={this.state.height}
+          style={{ width: '100%', position: 'absolute', top: 0, left: 0 }}
+          ref={this.setCanvasRef}
+          onClick={this.togglePlay}
+          onKeyDown={this.handleAudioKeyDown}
+          title={alt}
+          aria-label={alt}
+          lang={lang}
+        />
+
+        <div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed || editable })}>
+          <button type='button' className='spoiler-button__overlay' onClick={this.toggleReveal}>
+            <span className='spoiler-button__overlay__label'>
+              {warning}
+              <span className='spoiler-button__overlay__action'><FormattedMessage id='status.media.show' defaultMessage='Click to show' /></span>
+            </span>
+          </button>
+        </div>
+
+        {(revealed || editable) && <img
+          src={this.props.poster}
+          alt=''
+          style={{
+            position: 'absolute',
+            left: '50%',
+            top: '50%',
+            height: `calc(${(100 - 2 * 100 * PADDING / 982)}% - ${TICK_SIZE * 2}px)`,
+            aspectRatio: '1',
+            transform: 'translate(-50%, -50%)',
+            borderRadius: '50%',
+            pointerEvents: 'none',
+          }}
+        />}
+
+        <div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}>
+          <div className='video-player__seek__buffer' style={{ width: `${buffer}%` }} />
+          <div className='video-player__seek__progress' style={{ width: `${progress}%`, backgroundColor: this._getAccentColor() }} />
+
+          <span
+            className={classNames('video-player__seek__handle', { active: dragging })}
+            tabIndex={0}
+            style={{ left: `${progress}%`, backgroundColor: this._getAccentColor() }}
+            onKeyDown={this.handleAudioKeyDown}
+          />
+        </div>
+
+        <div className='video-player__controls active'>
+          <div className='video-player__buttons-bar'>
+            <div className='video-player__buttons left'>
+              <button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} className='player-button' onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
+              <button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} className='player-button' onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
+
+              <div className={classNames('video-player__volume', { active: this.state.hovered })} ref={this.setVolumeRef} onMouseDown={this.handleVolumeMouseDown}>
+                <div className='video-player__volume__current' style={{ width: `${muted ? 0 : volume * 100}%`, backgroundColor: this._getAccentColor() }} />
+
+                <span
+                  className='video-player__volume__handle'
+                  tabIndex={0}
+                  style={{ left: `${muted ? 0 : volume * 100}%`, backgroundColor: this._getAccentColor() }}
+                />
+              </div>
+
+              <span className='video-player__time'>
+                <span className='video-player__time-current'>{formatTime(Math.floor(currentTime))}</span>
+                <span className='video-player__time-sep'>/</span>
+                <span className='video-player__time-total'>{formatTime(Math.floor(this.state.duration || this.props.duration))}</span>
+              </span>
+            </div>
+
+            <div className='video-player__buttons right'>
+              {!editable && <button type='button' title={intl.formatMessage(messages.hide)} aria-label={intl.formatMessage(messages.hide)} className='player-button' onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>}
+              <a title={intl.formatMessage(messages.download)} aria-label={intl.formatMessage(messages.download)} className='video-player__download__icon player-button' href={this.props.src} download>
+                <Icon id={'download'} fixedWidth />
+              </a>
+            </div>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(Audio);
diff --git a/app/javascript/flavours/blobfox/features/audio/visualizer.js b/app/javascript/flavours/blobfox/features/audio/visualizer.js
new file mode 100644
index 00000000000000..77d5b5a65cdce1
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/audio/visualizer.js
@@ -0,0 +1,136 @@
+/*
+Copyright (c) 2020 by Alex Permyakov (https://codepen.io/alexdevp/pen/RNELPV)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+const hex2rgba = (hex, alpha = 1) => {
+  const [r, g, b] = hex.match(/\w\w/g).map(x => parseInt(x, 16));
+  return `rgba(${r}, ${g}, ${b}, ${alpha})`;
+};
+
+export default class Visualizer {
+
+  constructor (tickSize) {
+    this.tickSize = tickSize;
+  }
+
+  setCanvas(canvas) {
+    this.canvas = canvas;
+    if (canvas) {
+      this.context = canvas.getContext('2d');
+    }
+  }
+
+  setAudioContext(context, source) {
+    const analyser = context.createAnalyser();
+
+    analyser.smoothingTimeConstant = 0.6;
+    analyser.fftSize = 2048;
+
+    source.connect(analyser);
+
+    this.analyser = analyser;
+  }
+
+  getTickPoints (count) {
+    const coords = [];
+
+    for(let i = 0; i < count; i++) {
+      const rad = Math.PI * 2 * i / count;
+      coords.push({ x: Math.cos(rad), y: -Math.sin(rad) });
+    }
+
+    return coords;
+  }
+
+  drawTick (cx, cy, mainColor, x1, y1, x2, y2) {
+    const dx1 = Math.ceil(cx + x1);
+    const dy1 = Math.ceil(cy + y1);
+    const dx2 = Math.ceil(cx + x2);
+    const dy2 = Math.ceil(cy + y2);
+
+    const gradient = this.context.createLinearGradient(dx1, dy1, dx2, dy2);
+
+    const lastColor = hex2rgba(mainColor, 0);
+
+    gradient.addColorStop(0, mainColor);
+    gradient.addColorStop(0.6, mainColor);
+    gradient.addColorStop(1, lastColor);
+
+    this.context.beginPath();
+    this.context.strokeStyle = gradient;
+    this.context.lineWidth = 2;
+    this.context.moveTo(dx1, dy1);
+    this.context.lineTo(dx2, dy2);
+    this.context.stroke();
+  }
+
+  getTicks (count, size, radius, scaleCoefficient) {
+    const ticks = this.getTickPoints(count);
+    const lesser = 200;
+    const m = [];
+    const bufferLength = this.analyser ? this.analyser.frequencyBinCount : 0;
+    const frequencyData = new Uint8Array(bufferLength);
+    const allScales = [];
+
+    if (this.analyser) {
+      this.analyser.getByteFrequencyData(frequencyData);
+    }
+
+    ticks.forEach((tick, i) => {
+      const coef = 1 - i / (ticks.length * 2.5);
+
+      let delta = ((frequencyData[i] || 0) - lesser * coef) * scaleCoefficient;
+
+      if (delta < 0) {
+        delta = 0;
+      }
+
+      const k = radius / (radius - (size + delta));
+
+      const x1 = tick.x * (radius - size);
+      const y1 = tick.y * (radius - size);
+      const x2 = x1 * k;
+      const y2 = y1 * k;
+
+      m.push({ x1, y1, x2, y2 });
+
+      if (i < 20) {
+        let scale = delta / (200 * scaleCoefficient);
+        scale = scale < 1 ? 1 : scale;
+        allScales.push(scale);
+      }
+    });
+
+    const scale = allScales.reduce((pv, cv) => pv + cv, 0) / allScales.length;
+
+    return m.map(({ x1, y1, x2, y2 }) => ({
+      x1: x1,
+      y1: y1,
+      x2: x2 * scale,
+      y2: y2 * scale,
+    }));
+  }
+
+  clear (width, height) {
+    this.context.clearRect(0, 0, width, height);
+  }
+
+  draw (cx, cy, color, radius, coefficient) {
+    this.context.save();
+
+    const ticks = this.getTicks(parseInt(360 * coefficient), this.tickSize, radius, coefficient);
+
+    ticks.forEach(tick => {
+      this.drawTick(cx, cy, color, tick.x1, tick.y1, tick.x2, tick.y2);
+    });
+
+    this.context.restore();
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/blocks/index.jsx b/app/javascript/flavours/blobfox/features/blocks/index.jsx
new file mode 100644
index 00000000000000..d976174ce0c2b0
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/blocks/index.jsx
@@ -0,0 +1,82 @@
+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 { connect } from 'react-redux';
+
+import { debounce } from 'lodash';
+
+import { fetchBlocks, expandBlocks } from '../../actions/blocks';
+import ColumnBackButtonSlim from '../../components/column_back_button_slim';
+import { LoadingIndicator } from '../../components/loading_indicator';
+import ScrollableList from '../../components/scrollable_list';
+import AccountContainer from '../../containers/account_container';
+import Column from '../ui/components/column';
+
+const messages = defineMessages({
+  heading: { id: 'column.blocks', defaultMessage: 'Blocked users' },
+});
+
+const mapStateToProps = state => ({
+  accountIds: state.getIn(['user_lists', 'blocks', 'items']),
+  hasMore: !!state.getIn(['user_lists', 'blocks', 'next']),
+  isLoading: state.getIn(['user_lists', 'blocks', 'isLoading'], true),
+});
+
+class Blocks extends ImmutablePureComponent {
+
+  static propTypes = {
+    params: PropTypes.object.isRequired,
+    dispatch: PropTypes.func.isRequired,
+    accountIds: ImmutablePropTypes.list,
+    hasMore: PropTypes.bool,
+    isLoading: PropTypes.bool,
+    intl: PropTypes.object.isRequired,
+    multiColumn: PropTypes.bool,
+  };
+
+  UNSAFE_componentWillMount () {
+    this.props.dispatch(fetchBlocks());
+  }
+
+  handleLoadMore = debounce(() => {
+    this.props.dispatch(expandBlocks());
+  }, 300, { leading: true });
+
+  render () {
+    const { intl, accountIds, hasMore, multiColumn, isLoading } = this.props;
+
+    if (!accountIds) {
+      return (
+        <Column>
+          <LoadingIndicator />
+        </Column>
+      );
+    }
+
+    const emptyMessage = <FormattedMessage id='empty_column.blocks' defaultMessage="You haven't blocked any users yet." />;
+
+    return (
+      <Column bindToDocument={!multiColumn} icon='ban' heading={intl.formatMessage(messages.heading)}>
+        <ColumnBackButtonSlim />
+        <ScrollableList
+          scrollKey='blocks'
+          onLoadMore={this.handleLoadMore}
+          hasMore={hasMore}
+          isLoading={isLoading}
+          emptyMessage={emptyMessage}
+          bindToDocument={!multiColumn}
+        >
+          {accountIds.map(id =>
+            <AccountContainer key={id} id={id} defaultAction='block' />,
+          )}
+        </ScrollableList>
+      </Column>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(injectIntl(Blocks));
diff --git a/app/javascript/flavours/blobfox/features/bookmarked_statuses/index.jsx b/app/javascript/flavours/blobfox/features/bookmarked_statuses/index.jsx
new file mode 100644
index 00000000000000..d61b97022df25f
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/bookmarked_statuses/index.jsx
@@ -0,0 +1,113 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
+import { debounce } from 'lodash';
+
+import { fetchBookmarkedStatuses, expandBookmarkedStatuses } from 'flavours/blobfox/actions/bookmarks';
+import { addColumn, removeColumn, moveColumn } from 'flavours/blobfox/actions/columns';
+import ColumnHeader from 'flavours/blobfox/components/column_header';
+import StatusList from 'flavours/blobfox/components/status_list';
+import Column from 'flavours/blobfox/features/ui/components/column';
+import { getStatusList } from 'flavours/blobfox/selectors';
+
+const messages = defineMessages({
+  heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
+});
+
+const mapStateToProps = state => ({
+  statusIds: getStatusList(state, 'bookmarks'),
+  isLoading: state.getIn(['status_lists', 'bookmarks', 'isLoading'], true),
+  hasMore: !!state.getIn(['status_lists', 'bookmarks', 'next']),
+});
+
+class Bookmarks extends ImmutablePureComponent {
+
+  static propTypes = {
+    dispatch: PropTypes.func.isRequired,
+    statusIds: ImmutablePropTypes.list.isRequired,
+    intl: PropTypes.object.isRequired,
+    columnId: PropTypes.string,
+    multiColumn: PropTypes.bool,
+    hasMore: PropTypes.bool,
+    isLoading: PropTypes.bool,
+  };
+
+  UNSAFE_componentWillMount () {
+    this.props.dispatch(fetchBookmarkedStatuses());
+  }
+
+  handlePin = () => {
+    const { columnId, dispatch } = this.props;
+
+    if (columnId) {
+      dispatch(removeColumn(columnId));
+    } else {
+      dispatch(addColumn('BOOKMARKS', {}));
+    }
+  };
+
+  handleMove = (dir) => {
+    const { columnId, dispatch } = this.props;
+    dispatch(moveColumn(columnId, dir));
+  };
+
+  handleHeaderClick = () => {
+    this.column.scrollTop();
+  };
+
+  setRef = c => {
+    this.column = c;
+  };
+
+  handleLoadMore = debounce(() => {
+    this.props.dispatch(expandBookmarkedStatuses());
+  }, 300, { leading: true });
+
+  render () {
+    const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
+    const pinned = !!columnId;
+
+    const emptyMessage = <FormattedMessage id='empty_column.bookmarked_statuses' defaultMessage="You don't have any bookmarked posts yet. When you bookmark one, it will show up here." />;
+
+    return (
+      <Column bindToDocument={!multiColumn} ref={this.setRef}>
+        <ColumnHeader
+          icon='bookmark'
+          title={intl.formatMessage(messages.heading)}
+          onPin={this.handlePin}
+          onMove={this.handleMove}
+          onClick={this.handleHeaderClick}
+          pinned={pinned}
+          multiColumn={multiColumn}
+          showBackButton
+        />
+
+        <StatusList
+          trackScroll={!pinned}
+          statusIds={statusIds}
+          scrollKey={`bookmarked_statuses-${columnId}`}
+          hasMore={hasMore}
+          isLoading={isLoading}
+          onLoadMore={this.handleLoadMore}
+          emptyMessage={emptyMessage}
+          bindToDocument={!multiColumn}
+        />
+
+        <Helmet>
+          <title>{intl.formatMessage(messages.heading)}</title>
+          <meta name='robots' content='noindex' />
+        </Helmet>
+      </Column>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(injectIntl(Bookmarks));
diff --git a/app/javascript/flavours/blobfox/features/closed_registrations_modal/index.jsx b/app/javascript/flavours/blobfox/features/closed_registrations_modal/index.jsx
new file mode 100644
index 00000000000000..b3742a30fa638b
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/closed_registrations_modal/index.jsx
@@ -0,0 +1,77 @@
+import { FormattedMessage } from 'react-intl';
+
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
+import { fetchServer } from 'flavours/blobfox/actions/server';
+import { domain } from 'flavours/blobfox/initial_state';
+
+const mapStateToProps = state => ({
+  message: state.getIn(['server', 'server', 'registrations', 'message']),
+});
+
+class ClosedRegistrationsModal extends ImmutablePureComponent {
+
+  componentDidMount () {
+    const { dispatch } = this.props;
+    dispatch(fetchServer());
+  }
+
+  render () {
+    let closedRegistrationsMessage;
+
+    if (this.props.message) {
+      closedRegistrationsMessage = (
+        <p
+          className='prose'
+          dangerouslySetInnerHTML={{ __html: this.props.message }}
+        />
+      );
+    } else {
+      closedRegistrationsMessage = (
+        <p className='prose'>
+          <FormattedMessage
+            id='closed_registrations_modal.description'
+            defaultMessage='Creating an account on {domain} is currently not possible, but please keep in mind that you do not need an account specifically on {domain} to use Mastodon.'
+            values={{ domain: <strong>{domain}</strong> }}
+          />
+        </p>
+      );
+    }
+
+    return (
+      <div className='modal-root__modal interaction-modal'>
+        <div className='interaction-modal__lead'>
+          <h3><FormattedMessage id='closed_registrations_modal.title' defaultMessage='Signing up on Mastodon' /></h3>
+          <p>
+            <FormattedMessage
+              id='closed_registrations_modal.preamble'
+              defaultMessage='Mastodon is decentralized, so no matter where you create your account, you will be able to follow and interact with anyone on this server. You can even self-host it!'
+            />
+          </p>
+        </div>
+
+        <div className='interaction-modal__choices'>
+          <div className='interaction-modal__choices__choice'>
+            <h3><FormattedMessage id='interaction_modal.on_this_server' defaultMessage='On this server' /></h3>
+            {closedRegistrationsMessage}
+          </div>
+
+          <div className='interaction-modal__choices__choice'>
+            <h3><FormattedMessage id='interaction_modal.on_another_server' defaultMessage='On a different server' /></h3>
+            <p className='prose'>
+              <FormattedMessage
+                id='closed_registrations.other_server_instructions'
+                defaultMessage='Since Mastodon is decentralized, you can create an account on another server and still interact with this one.'
+              />
+            </p>
+            <a href='https://joinmastodon.org/servers' className='button button--block'><FormattedMessage id='closed_registrations_modal.find_another_server' defaultMessage='Find another server' /></a>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(ClosedRegistrationsModal);
diff --git a/app/javascript/flavours/blobfox/features/community_timeline/components/column_settings.jsx b/app/javascript/flavours/blobfox/features/community_timeline/components/column_settings.jsx
new file mode 100644
index 00000000000000..b31d490ba2593c
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/community_timeline/components/column_settings.jsx
@@ -0,0 +1,45 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
+import SettingText from 'flavours/blobfox/components/setting_text';
+import SettingToggle from 'flavours/blobfox/features/notifications/components/setting_toggle';
+
+const messages = defineMessages({
+  filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' },
+  settings: { id: 'home.settings', defaultMessage: 'Column settings' },
+});
+
+class ColumnSettings extends PureComponent {
+
+  static propTypes = {
+    settings: ImmutablePropTypes.map.isRequired,
+    onChange: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+    columnId: PropTypes.string,
+  };
+
+  render () {
+    const { settings, onChange, intl } = this.props;
+
+    return (
+      <div>
+        <div className='column-settings__row'>
+          <SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
+        </div>
+
+        <span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
+
+        <div className='column-settings__row'>
+          <SettingText settings={settings} settingPath={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} />
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(ColumnSettings);
diff --git a/app/javascript/flavours/blobfox/features/community_timeline/containers/column_settings_container.js b/app/javascript/flavours/blobfox/features/community_timeline/containers/column_settings_container.js
new file mode 100644
index 00000000000000..1e9f1213945a03
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/community_timeline/containers/column_settings_container.js
@@ -0,0 +1,29 @@
+import { connect } from 'react-redux';
+
+import { changeColumnParams } from '../../../actions/columns';
+import { changeSetting } from '../../../actions/settings';
+import ColumnSettings from '../components/column_settings';
+
+const mapStateToProps = (state, { columnId }) => {
+  const uuid = columnId;
+  const columns = state.getIn(['settings', 'columns']);
+  const index = columns.findIndex(c => c.get('uuid') === uuid);
+
+  return {
+    settings: (uuid && index >= 0) ? columns.get(index).get('params') : state.getIn(['settings', 'community']),
+  };
+};
+
+const mapDispatchToProps = (dispatch, { columnId }) => {
+  return {
+    onChange (key, checked) {
+      if (columnId) {
+        dispatch(changeColumnParams(columnId, key, checked));
+      } else {
+        dispatch(changeSetting(['community', ...key], checked));
+      }
+    },
+  };
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);
diff --git a/app/javascript/flavours/blobfox/features/community_timeline/index.jsx b/app/javascript/flavours/blobfox/features/community_timeline/index.jsx
new file mode 100644
index 00000000000000..3f715b644e6fd7
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/community_timeline/index.jsx
@@ -0,0 +1,166 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+
+import { connect } from 'react-redux';
+
+import { DismissableBanner } from 'flavours/blobfox/components/dismissable_banner';
+import { domain } from 'flavours/blobfox/initial_state';
+
+import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
+import { connectCommunityStream } from '../../actions/streaming';
+import { expandCommunityTimeline } from '../../actions/timelines';
+import Column from '../../components/column';
+import ColumnHeader from '../../components/column_header';
+import StatusListContainer from '../ui/containers/status_list_container';
+
+import ColumnSettingsContainer from './containers/column_settings_container';
+
+const messages = defineMessages({
+  title: { id: 'column.community', defaultMessage: 'Local timeline' },
+});
+
+const mapStateToProps = (state, { columnId }) => {
+  const uuid = columnId;
+  const columns = state.getIn(['settings', 'columns']);
+  const index = columns.findIndex(c => c.get('uuid') === uuid);
+  const onlyMedia = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'community', 'other', 'onlyMedia']);
+  const regex = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'regex', 'body']) : state.getIn(['settings', 'community', 'regex', 'body']);
+  const timelineState = state.getIn(['timelines', `community${onlyMedia ? ':media' : ''}`]);
+
+  return {
+    hasUnread: !!timelineState && timelineState.get('unread') > 0,
+    onlyMedia,
+    regex,
+  };
+};
+
+class CommunityTimeline extends PureComponent {
+
+  static contextTypes = {
+    identity: PropTypes.object,
+  };
+
+  static defaultProps = {
+    onlyMedia: false,
+  };
+
+  static propTypes = {
+    dispatch: PropTypes.func.isRequired,
+    columnId: PropTypes.string,
+    intl: PropTypes.object.isRequired,
+    hasUnread: PropTypes.bool,
+    multiColumn: PropTypes.bool,
+    onlyMedia: PropTypes.bool,
+    regex: PropTypes.string,
+  };
+
+  handlePin = () => {
+    const { columnId, dispatch, onlyMedia } = this.props;
+
+    if (columnId) {
+      dispatch(removeColumn(columnId));
+    } else {
+      dispatch(addColumn('COMMUNITY', { other: { onlyMedia } }));
+    }
+  };
+
+  handleMove = (dir) => {
+    const { columnId, dispatch } = this.props;
+    dispatch(moveColumn(columnId, dir));
+  };
+
+  handleHeaderClick = () => {
+    this.column.scrollTop();
+  };
+
+  componentDidMount () {
+    const { dispatch, onlyMedia } = this.props;
+    const { signedIn } = this.context.identity;
+
+    dispatch(expandCommunityTimeline({ onlyMedia }));
+
+    if (signedIn) {
+      this.disconnect = dispatch(connectCommunityStream({ onlyMedia }));
+    }
+  }
+
+  componentDidUpdate (prevProps) {
+    const { signedIn } = this.context.identity;
+
+    if (prevProps.onlyMedia !== this.props.onlyMedia) {
+      const { dispatch, onlyMedia } = this.props;
+
+      if (this.disconnect) {
+        this.disconnect();
+      }
+
+      dispatch(expandCommunityTimeline({ onlyMedia }));
+
+      if (signedIn) {
+        this.disconnect = dispatch(connectCommunityStream({ onlyMedia }));
+      }
+    }
+  }
+
+  componentWillUnmount () {
+    if (this.disconnect) {
+      this.disconnect();
+      this.disconnect = null;
+    }
+  }
+
+  setRef = c => {
+    this.column = c;
+  };
+
+  handleLoadMore = maxId => {
+    const { dispatch, onlyMedia } = this.props;
+
+    dispatch(expandCommunityTimeline({ maxId, onlyMedia }));
+  };
+
+  render () {
+    const { intl, hasUnread, columnId, multiColumn, onlyMedia } = this.props;
+    const pinned = !!columnId;
+
+    return (
+      <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
+        <ColumnHeader
+          icon='users'
+          active={hasUnread}
+          title={intl.formatMessage(messages.title)}
+          onPin={this.handlePin}
+          onMove={this.handleMove}
+          onClick={this.handleHeaderClick}
+          pinned={pinned}
+          multiColumn={multiColumn}
+        >
+          <ColumnSettingsContainer columnId={columnId} />
+        </ColumnHeader>
+
+        <StatusListContainer
+          prepend={<DismissableBanner id='community_timeline'><FormattedMessage id='dismissable_banner.community_timeline' defaultMessage='These are the most recent public posts from people whose accounts are hosted by {domain}.' values={{ domain }} /></DismissableBanner>}
+          trackScroll={!pinned}
+          scrollKey={`community_timeline-${columnId}`}
+          timelineId={`community${onlyMedia ? ':media' : ''}`}
+          onLoadMore={this.handleLoadMore}
+          emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />}
+          bindToDocument={!multiColumn}
+          regex={this.props.regex}
+        />
+
+        <Helmet>
+          <title>{intl.formatMessage(messages.title)}</title>
+          <meta name='robots' content='noindex' />
+        </Helmet>
+      </Column>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(injectIntl(CommunityTimeline));
diff --git a/app/javascript/flavours/blobfox/features/compose/components/action_bar.jsx b/app/javascript/flavours/blobfox/features/compose/components/action_bar.jsx
new file mode 100644
index 00000000000000..2da781b792df0b
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/components/action_bar.jsx
@@ -0,0 +1,73 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, injectIntl } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
+import { preferencesLink, profileLink } from 'flavours/blobfox/utils/backend_links';
+
+import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
+
+const messages = defineMessages({
+  edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
+  pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' },
+  preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
+  follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
+  favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' },
+  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' },
+  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 {
+
+  static propTypes = {
+    account: ImmutablePropTypes.record.isRequired,
+    onLogout: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  handleLogout = () => {
+    this.props.onLogout();
+  };
+
+  render () {
+    const { intl } = this.props;
+
+    let menu = [];
+
+    menu.push({ text: intl.formatMessage(messages.edit_profile), href: profileLink });
+    menu.push({ text: intl.formatMessage(messages.preferences), href: preferencesLink });
+    menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' });
+    menu.push(null);
+    menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
+    menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' });
+    menu.push({ text: intl.formatMessage(messages.bookmarks), to: '/bookmarks' });
+    menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' });
+    menu.push({ text: intl.formatMessage(messages.followed_tags), to: '/followed_tags' });
+    menu.push(null);
+    menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
+    menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
+    menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
+    menu.push({ text: intl.formatMessage(messages.filters), href: '/filters' });
+    menu.push(null);
+    menu.push({ text: intl.formatMessage(messages.logout), action: this.handleLogout });
+
+    return (
+      <div className='compose__action-bar'>
+        <div className='compose__action-bar-dropdown'>
+          <DropdownMenuContainer items={menu} icon='bars' size={18} direction='right' />
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(ActionBar);
diff --git a/app/javascript/flavours/blobfox/features/compose/components/autosuggest_account.jsx b/app/javascript/flavours/blobfox/features/compose/components/autosuggest_account.jsx
new file mode 100644
index 00000000000000..8ffc9062e5c9fd
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/components/autosuggest_account.jsx
@@ -0,0 +1,24 @@
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import { Avatar } from '../../../components/avatar';
+import { DisplayName } from '../../../components/display_name';
+
+export default class AutosuggestAccount extends ImmutablePureComponent {
+
+  static propTypes = {
+    account: ImmutablePropTypes.record.isRequired,
+  };
+
+  render () {
+    const { account } = this.props;
+
+    return (
+      <div className='autosuggest-account' title={account.get('acct')}>
+        <div className='autosuggest-account-icon'><Avatar account={account} size={24} /></div>
+        <DisplayName account={account} inline />
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/compose/components/character_counter.jsx b/app/javascript/flavours/blobfox/features/compose/components/character_counter.jsx
new file mode 100644
index 00000000000000..42452b30f6af55
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/components/character_counter.jsx
@@ -0,0 +1,26 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { length } from 'stringz';
+
+export default class CharacterCounter extends PureComponent {
+
+  static propTypes = {
+    text: PropTypes.string.isRequired,
+    max: PropTypes.number.isRequired,
+  };
+
+  checkRemainingText (diff) {
+    if (diff < 0) {
+      return <span className='character-counter character-counter--over'>{diff}</span>;
+    }
+
+    return <span className='character-counter'>{diff}</span>;
+  }
+
+  render () {
+    const diff = this.props.max - length(this.props.text);
+    return this.checkRemainingText(diff);
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/compose/components/compose_form.jsx b/app/javascript/flavours/blobfox/features/compose/components/compose_form.jsx
new file mode 100644
index 00000000000000..282a28ba711fa1
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/components/compose_form.jsx
@@ -0,0 +1,356 @@
+import PropTypes from 'prop-types';
+import { createRef } from 'react';
+
+import { defineMessages, injectIntl } from 'react-intl';
+
+import classNames from 'classnames';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import { length } from 'stringz';
+
+import { maxChars } from 'flavours/blobfox/initial_state';
+import { isMobile } from 'flavours/blobfox/is_mobile';
+import { WithOptionalRouterPropTypes, withOptionalRouter } from 'flavours/blobfox/utils/react_router';
+
+import AutosuggestInput from '../../../components/autosuggest_input';
+import AutosuggestTextarea from '../../../components/autosuggest_textarea';
+import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
+import OptionsContainer from '../containers/options_container';
+import PollFormContainer from '../containers/poll_form_container';
+import ReplyIndicatorContainer from '../containers/reply_indicator_container';
+import UploadFormContainer from '../containers/upload_form_container';
+import WarningContainer from '../containers/warning_container';
+import { countableText } from '../util/counter';
+
+import CharacterCounter from './character_counter';
+import Publisher from './publisher';
+import TextareaIcons from './textarea_icons';
+
+const messages = defineMessages({
+  placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
+  missingDescriptionMessage: {
+    id: 'confirmations.missing_media_description.message',
+    defaultMessage: 'At least one media attachment is lacking a description. Consider describing all media attachments for the visually impaired before sending your toot.',
+  },
+  missingDescriptionConfirm: {
+    id: 'confirmations.missing_media_description.confirm',
+    defaultMessage: 'Send anyway',
+  },
+  spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' },
+});
+
+class ComposeForm extends ImmutablePureComponent {
+  static propTypes = {
+    intl: PropTypes.object.isRequired,
+    text: PropTypes.string.isRequired,
+    suggestions: ImmutablePropTypes.list,
+    spoiler: PropTypes.bool,
+    privacy: PropTypes.string,
+    spoilerText: PropTypes.string,
+    focusDate: PropTypes.instanceOf(Date),
+    caretPosition: PropTypes.number,
+    preselectDate: PropTypes.instanceOf(Date),
+    isSubmitting: PropTypes.bool,
+    isChangingUpload: PropTypes.bool,
+    isEditing: PropTypes.bool,
+    isUploading: PropTypes.bool,
+    onChange: PropTypes.func.isRequired,
+    onSubmit: PropTypes.func.isRequired,
+    onClearSuggestions: PropTypes.func.isRequired,
+    onFetchSuggestions: PropTypes.func.isRequired,
+    onSuggestionSelected: PropTypes.func.isRequired,
+    onChangeSpoilerText: PropTypes.func.isRequired,
+    onPaste: PropTypes.func.isRequired,
+    onPickEmoji: PropTypes.func.isRequired,
+    showSearch: PropTypes.bool,
+    anyMedia: PropTypes.bool,
+    isInReply: PropTypes.bool,
+    singleColumn: PropTypes.bool,
+    lang: PropTypes.string,
+    advancedOptions: ImmutablePropTypes.map,
+    layout: PropTypes.string,
+    media: ImmutablePropTypes.list,
+    sideArm: PropTypes.string,
+    sensitive: PropTypes.bool,
+    spoilersAlwaysOn: PropTypes.bool,
+    mediaDescriptionConfirmation: PropTypes.bool,
+    preselectOnReply: PropTypes.bool,
+    onChangeSpoilerness: PropTypes.func.isRequired,
+    onChangeVisibility: PropTypes.func.isRequired,
+    onMediaDescriptionConfirm: PropTypes.func.isRequired,
+    ...WithOptionalRouterPropTypes
+  };
+
+  static defaultProps = {
+    showSearch: false,
+  };
+
+  state = {
+    highlighted: false,
+  };
+
+  constructor(props) {
+    super(props);
+    this.textareaRef = createRef(null);
+  }
+
+  handleChange = (e) => {
+    this.props.onChange(e.target.value);
+  };
+
+  handleKeyDown = (e) => {
+    if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
+      this.handleSubmit();
+    }
+
+    if (e.keyCode === 13 && e.altKey) {
+      this.handleSecondarySubmit();
+    }
+  };
+
+  getFulltextForCharacterCounting = () => {
+    return [
+      this.props.spoiler? this.props.spoilerText: '',
+      countableText(this.props.text),
+      this.props.advancedOptions && this.props.advancedOptions.get('do_not_federate') ? ' 👁️' : '',
+    ].join('');
+  };
+
+  canSubmit = () => {
+    const { isSubmitting, isChangingUpload, isUploading, anyMedia } = this.props;
+    const fulltext = this.getFulltextForCharacterCounting();
+
+    return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > maxChars || (!fulltext.trim().length && !anyMedia));
+  };
+
+  handleSubmit = (e, overriddenVisibility = null) => {
+    if (this.props.text !== this.textareaRef.current.value) {
+      // Something changed the text inside the textarea (e.g. browser extensions like Grammarly)
+      // Update the state to match the current text
+      this.props.onChange(this.textareaRef.current.value);
+    }
+
+    if (!this.canSubmit()) {
+      return;
+    }
+
+    if (e) {
+      e.preventDefault();
+    }
+
+    // Submit unless there are media with missing descriptions
+    if (this.props.mediaDescriptionConfirmation && this.props.media && this.props.media.some(item => !item.get('description'))) {
+      const firstWithoutDescription = this.props.media.find(item => !item.get('description'));
+      this.props.onMediaDescriptionConfirm(this.props.history || null, firstWithoutDescription.get('id'), overriddenVisibility);
+    } else {
+      if (overriddenVisibility) {
+        this.props.onChangeVisibility(overriddenVisibility);
+      }
+      this.props.onSubmit(this.props.history || null);
+    }
+  };
+
+  //  Handles the secondary submit button.
+  handleSecondarySubmit = () => {
+    const { sideArm } = this.props;
+    this.handleSubmit(null, sideArm === 'none' ? null : sideArm);
+  };
+
+  onSuggestionsClearRequested = () => {
+    this.props.onClearSuggestions();
+  };
+
+  onSuggestionsFetchRequested = (token) => {
+    this.props.onFetchSuggestions(token);
+  };
+
+  onSuggestionSelected = (tokenStart, token, value) => {
+    this.props.onSuggestionSelected(tokenStart, token, value, ['text']);
+  };
+
+  onSpoilerSuggestionSelected = (tokenStart, token, value) => {
+    this.props.onSuggestionSelected(tokenStart, token, value, ['spoiler_text']);
+  };
+
+  handleChangeSpoilerText = (e) => {
+    this.props.onChangeSpoilerText(e.target.value);
+  };
+
+  handleFocus = () => {
+    if (this.composeForm && !this.props.singleColumn) {
+      const { left, right } = this.composeForm.getBoundingClientRect();
+      if (left < 0 || right > (window.innerWidth || document.documentElement.clientWidth)) {
+        this.composeForm.scrollIntoView();
+      }
+    }
+  };
+
+  componentDidMount () {
+    this._updateFocusAndSelection({ });
+  }
+
+  componentWillUnmount () {
+    if (this.timeout) clearTimeout(this.timeout);
+  }
+
+  componentDidUpdate (prevProps) {
+    this._updateFocusAndSelection(prevProps);
+  }
+
+  _updateFocusAndSelection = (prevProps) => {
+    // This statement does several things:
+    // - If we're beginning a reply, and,
+    //     - Replying to zero or one users, places the cursor at the end of the textbox.
+    //     - Replying to more than one user, selects any usernames past the first;
+    //       this provides a convenient shortcut to drop everyone else from the conversation.
+    if (this.props.focusDate && this.props.focusDate !== prevProps.focusDate) {
+      let selectionEnd, selectionStart;
+
+      if (this.props.preselectDate !== prevProps.preselectDate && this.props.isInReply && this.props.preselectOnReply) {
+        selectionEnd   = this.props.text.length;
+        selectionStart = this.props.text.search(/\s/) + 1;
+      } else if (typeof this.props.caretPosition === 'number') {
+        selectionStart = this.props.caretPosition;
+        selectionEnd   = this.props.caretPosition;
+      } else {
+        selectionEnd   = this.props.text.length;
+        selectionStart = selectionEnd;
+      }
+
+      // Because of the wicg-inert polyfill, the activeElement may not be
+      // immediately selectable, we have to wait for observers to run, as
+      // described in https://github.com/WICG/inert#performance-and-gotchas
+      Promise.resolve().then(() => {
+        this.textareaRef.current.setSelectionRange(selectionStart, selectionEnd);
+        this.textareaRef.current.focus();
+        if (!this.props.singleColumn) this.textareaRef.current.scrollIntoView();
+        this.setState({ highlighted: true });
+        this.timeout = setTimeout(() => this.setState({ highlighted: false }), 700);
+      }).catch(console.error);
+    } else if(prevProps.isSubmitting && !this.props.isSubmitting) {
+      this.textareaRef.current.focus();
+    } else if (this.props.spoiler !== prevProps.spoiler) {
+      if (this.props.spoiler) {
+        this.spoilerText.input.focus();
+      } else if (prevProps.spoiler) {
+        this.textareaRef.current.focus();
+      }
+    }
+  };
+
+  setSpoilerText = (c) => {
+    this.spoilerText = c;
+  };
+
+  setRef = c => {
+    this.composeForm = c;
+  };
+
+  handleEmojiPick = (data) => {
+    const position = this.textareaRef.current.selectionStart;
+
+    this.props.onPickEmoji(position, data);
+  };
+
+  render () {
+    const {
+      intl,
+      advancedOptions,
+      isSubmitting,
+      layout,
+      onChangeSpoilerness,
+      onPaste,
+      privacy,
+      sensitive,
+      showSearch,
+      sideArm,
+      spoilersAlwaysOn,
+      isEditing,
+    } = this.props;
+    const { highlighted } = this.state;
+    const disabled = this.props.isSubmitting;
+
+    return (
+      <form className='compose-form' onSubmit={this.handleSubmit}>
+        <WarningContainer />
+
+        <ReplyIndicatorContainer />
+
+        <div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`} ref={this.setRef} aria-hidden={!this.props.spoiler}>
+          <AutosuggestInput
+            placeholder={intl.formatMessage(messages.spoiler_placeholder)}
+            value={this.props.spoilerText}
+            onChange={this.handleChangeSpoilerText}
+            onKeyDown={this.handleKeyDown}
+            disabled={!this.props.spoiler}
+            ref={this.setSpoilerText}
+            suggestions={this.props.suggestions}
+            onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
+            onSuggestionsClearRequested={this.onSuggestionsClearRequested}
+            onSuggestionSelected={this.onSpoilerSuggestionSelected}
+            searchTokens={[':']}
+            id='cw-spoiler-input'
+            className='spoiler-input__input'
+            lang={this.props.lang}
+            autoFocus={false}
+            spellCheck
+          />
+        </div>
+
+        <div className={classNames('compose-form__highlightable', { active: highlighted })}>
+          <AutosuggestTextarea
+            ref={this.textareaRef}
+            placeholder={intl.formatMessage(messages.placeholder)}
+            disabled={disabled}
+            value={this.props.text}
+            onChange={this.handleChange}
+            suggestions={this.props.suggestions}
+            onFocus={this.handleFocus}
+            onKeyDown={this.handleKeyDown}
+            onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
+            onSuggestionsClearRequested={this.onSuggestionsClearRequested}
+            onSuggestionSelected={this.onSuggestionSelected}
+            onPaste={onPaste}
+            autoFocus={!showSearch && !isMobile(window.innerWidth, layout)}
+            lang={this.props.lang}
+          >
+            <TextareaIcons advancedOptions={advancedOptions} />
+            <div className='compose-form__modifiers'>
+              <UploadFormContainer />
+              <PollFormContainer />
+            </div>
+          </AutosuggestTextarea>
+          <EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} />
+
+          <div className='compose-form__buttons-wrapper'>
+            <OptionsContainer
+              advancedOptions={advancedOptions}
+              disabled={isSubmitting}
+              onToggleSpoiler={this.props.spoilersAlwaysOn ? null : onChangeSpoilerness}
+              onUpload={onPaste}
+              isEditing={isEditing}
+              sensitive={sensitive || (spoilersAlwaysOn && this.props.spoilerText && this.props.spoilerText.length > 0)}
+              spoiler={spoilersAlwaysOn ? (this.props.spoilerText && this.props.spoilerText.length > 0) : this.props.spoiler}
+            />
+            <div className='character-counter__wrapper'>
+              <CharacterCounter max={maxChars} text={this.getFulltextForCharacterCounting()} />
+            </div>
+          </div>
+        </div>
+
+        <Publisher
+          disabled={!this.canSubmit()}
+          isEditing={isEditing}
+          onSecondarySubmit={this.handleSecondarySubmit}
+          privacy={privacy}
+          sideArm={sideArm}
+        />
+      </form>
+    );
+  }
+
+}
+
+export default withOptionalRouter(injectIntl(ComposeForm));
diff --git a/app/javascript/flavours/blobfox/features/compose/components/dropdown.jsx b/app/javascript/flavours/blobfox/features/compose/components/dropdown.jsx
new file mode 100644
index 00000000000000..5f2de1189bfe25
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/components/dropdown.jsx
@@ -0,0 +1,243 @@
+//  Package imports.
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import classNames from 'classnames';
+
+import Overlay from 'react-overlays/Overlay';
+
+//  Components.
+import { IconButton } from 'flavours/blobfox/components/icon_button';
+
+import DropdownMenu from './dropdown_menu';
+
+//  The component.
+export default class ComposerOptionsDropdown extends PureComponent {
+
+  static propTypes = {
+    isUserTouching: PropTypes.func,
+    disabled: PropTypes.bool,
+    icon: PropTypes.string,
+    items: PropTypes.arrayOf(PropTypes.shape({
+      icon: PropTypes.string,
+      meta: PropTypes.string,
+      name: PropTypes.string.isRequired,
+      text: PropTypes.string,
+    })).isRequired,
+    onModalOpen: PropTypes.func,
+    onModalClose: PropTypes.func,
+    title: PropTypes.string,
+    value: PropTypes.string,
+    onChange: PropTypes.func,
+    container: PropTypes.func,
+    renderItemContents: PropTypes.func,
+    closeOnChange: PropTypes.bool,
+  };
+
+  static defaultProps = {
+    closeOnChange: true,
+  };
+
+  state = {
+    open: false,
+    openedViaKeyboard: undefined,
+    placement: 'bottom',
+  };
+
+  //  Toggles opening and closing the dropdown.
+  handleToggle = ({ type }) => {
+    const { onModalOpen } = this.props;
+    const { open } = this.state;
+
+    if (this.props.isUserTouching && this.props.isUserTouching()) {
+      if (open) {
+        this.props.onModalClose();
+      } else {
+        const modal = this.handleMakeModal();
+        if (modal && onModalOpen) {
+          onModalOpen(modal);
+        }
+      }
+    } else {
+      if (open && this.activeElement) {
+        this.activeElement.focus({ preventScroll: true });
+      }
+      this.setState({ open: !open, openedViaKeyboard: type !== 'click' });
+    }
+  };
+
+  handleKeyDown = (e) => {
+    switch (e.key) {
+    case 'Escape':
+      this.handleClose();
+      break;
+    }
+  };
+
+  handleMouseDown = () => {
+    if (!this.state.open) {
+      this.activeElement = document.activeElement;
+    }
+  };
+
+  handleButtonKeyDown = (e) => {
+    switch(e.key) {
+    case ' ':
+    case 'Enter':
+      this.handleMouseDown();
+      break;
+    }
+  };
+
+  handleKeyPress = (e) => {
+    switch(e.key) {
+    case ' ':
+    case 'Enter':
+      this.handleToggle(e);
+      e.stopPropagation();
+      e.preventDefault();
+      break;
+    }
+  };
+
+  handleClose = () => {
+    if (this.state.open && this.activeElement) {
+      this.activeElement.focus({ preventScroll: true });
+    }
+    this.setState({ open: false });
+  };
+
+  handleItemClick = (e) => {
+    const {
+      items,
+      onChange,
+      onModalClose,
+      closeOnChange,
+    } = this.props;
+
+    const i = Number(e.currentTarget.getAttribute('data-index'));
+
+    const { name } = items[i];
+
+    e.preventDefault();  //  Prevents focus from changing
+    if (closeOnChange) onModalClose();
+    onChange(name);
+  };
+
+  //  Creates an action modal object.
+  handleMakeModal = () => {
+    const {
+      items,
+      onChange,
+      onModalOpen,
+      onModalClose,
+      value,
+    } = this.props;
+
+    //  Required props.
+    if (!(onChange && onModalOpen && onModalClose && items)) {
+      return null;
+    }
+
+    //  The object.
+    return {
+      renderItemContents: this.props.renderItemContents,
+      onClick: this.handleItemClick,
+      actions: items.map(
+        ({
+          name,
+          ...rest
+        }) => ({
+          ...rest,
+          active: value && name === value,
+          name,
+        }),
+      ),
+    };
+  };
+
+  setTargetRef = c => {
+    this.target = c;
+  };
+
+  findTarget = () => {
+    return this.target;
+  };
+
+  handleOverlayEnter = (state) => {
+    this.setState({ placement: state.placement });
+  };
+
+  //  Rendering.
+  render () {
+    const {
+      disabled,
+      title,
+      icon,
+      items,
+      onChange,
+      value,
+      container,
+      renderItemContents,
+      closeOnChange,
+    } = this.props;
+    const { open, placement } = this.state;
+
+    const active = value && items.findIndex(({ name }) => name === value) === (placement === 'bottom' ? 0 : (items.length - 1));
+
+    return (
+      <div
+        className={classNames('privacy-dropdown', placement, { active: open })}
+        onKeyDown={this.handleKeyDown}
+        ref={this.setTargetRef}
+      >
+        <div className={classNames('privacy-dropdown__value', { active })}>
+          <IconButton
+            active={open}
+            className='privacy-dropdown__value-icon'
+            disabled={disabled}
+            icon={icon}
+            inverted
+            onClick={this.handleToggle}
+            onMouseDown={this.handleMouseDown}
+            onKeyDown={this.handleButtonKeyDown}
+            onKeyPress={this.handleKeyPress}
+            size={18}
+            style={{
+              height: null,
+              lineHeight: '27px',
+            }}
+            title={title}
+          />
+        </div>
+
+        <Overlay
+          containerPadding={20}
+          placement={placement}
+          show={open}
+          flip
+          target={this.findTarget}
+          container={container}
+          popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}
+        >
+          {({ props, placement }) => (
+            <div {...props}>
+              <div className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}>
+                <DropdownMenu
+                  items={items}
+                  renderItemContents={renderItemContents}
+                  onChange={onChange}
+                  onClose={this.handleClose}
+                  value={value}
+                  openedViaKeyboard={this.state.openedViaKeyboard}
+                  closeOnChange={closeOnChange}
+                />
+              </div>
+            </div>
+          )}
+        </Overlay>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/compose/components/dropdown_menu.jsx b/app/javascript/flavours/blobfox/features/compose/components/dropdown_menu.jsx
new file mode 100644
index 00000000000000..45b94dedee902c
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/components/dropdown_menu.jsx
@@ -0,0 +1,200 @@
+//  Package imports.
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import classNames from 'classnames';
+
+import { supportsPassiveEvents } from 'detect-passive-events';
+
+//  Components.
+import { Icon } from 'flavours/blobfox/components/icon';
+
+const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
+
+//  The component.
+export default class ComposerOptionsDropdownContent extends PureComponent {
+
+  static propTypes = {
+    items: PropTypes.arrayOf(PropTypes.shape({
+      icon: PropTypes.string,
+      meta: PropTypes.node,
+      name: PropTypes.string.isRequired,
+      text: PropTypes.node,
+    })),
+    onChange: PropTypes.func.isRequired,
+    onClose: PropTypes.func.isRequired,
+    style: PropTypes.object,
+    value: PropTypes.string,
+    renderItemContents: PropTypes.func,
+    openedViaKeyboard: PropTypes.bool,
+    closeOnChange: PropTypes.bool,
+  };
+
+  static defaultProps = {
+    style: {},
+    closeOnChange: true,
+  };
+
+  state = {
+    value: this.props.openedViaKeyboard ? this.props.items[0].name : undefined,
+  };
+
+  //  When the document is clicked elsewhere, we close the dropdown.
+  handleDocumentClick = (e) => {
+    if (this.node && !this.node.contains(e.target)) {
+      this.props.onClose();
+      e.stopPropagation();
+    }
+  };
+
+  //  Stores our node in `this.node`.
+  setRef = (node) => {
+    this.node = node;
+  };
+
+  //  On mounting, we add our listeners.
+  componentDidMount () {
+    document.addEventListener('click', this.handleDocumentClick, { capture: true });
+    document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
+    if (this.focusedItem) {
+      this.focusedItem.focus({ preventScroll: true });
+    } else {
+      this.node.firstChild.focus({ preventScroll: true });
+    }
+  }
+
+  //  On unmounting, we remove our listeners.
+  componentWillUnmount () {
+    document.removeEventListener('click', this.handleDocumentClick, { capture: true });
+    document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
+  }
+
+  handleClick = (e) => {
+    const i = Number(e.currentTarget.getAttribute('data-index'));
+
+    const {
+      onChange,
+      onClose,
+      closeOnChange,
+      items,
+    } = this.props;
+
+    const { name } = items[i];
+
+    e.preventDefault();  //  Prevents change in focus on click
+    if (closeOnChange) {
+      onClose();
+    }
+    onChange(name);
+  };
+
+  // Handle changes differently whether the dropdown is a list of options or actions
+  handleChange = (name) => {
+    if (this.props.value) {
+      this.props.onChange(name);
+    } else {
+      this.setState({ value: name });
+    }
+  };
+
+  handleKeyDown = (e) => {
+    const index = Number(e.currentTarget.getAttribute('data-index'));
+    const { items } = this.props;
+    let element = null;
+
+    switch(e.key) {
+    case 'Escape':
+      this.props.onClose();
+      break;
+    case 'Enter':
+    case ' ':
+      this.handleClick(e);
+      break;
+    case 'ArrowDown':
+      element = this.node.childNodes[index + 1] || this.node.firstChild;
+      break;
+    case 'ArrowUp':
+      element = this.node.childNodes[index - 1] || this.node.lastChild;
+      break;
+    case 'Tab':
+      if (e.shiftKey) {
+        element = this.node.childNodes[index - 1] || this.node.lastChild;
+      } else {
+        element = this.node.childNodes[index + 1] || this.node.firstChild;
+      }
+      break;
+    case 'Home':
+      element = this.node.firstChild;
+      break;
+    case 'End':
+      element = this.node.lastChild;
+      break;
+    }
+
+    if (element) {
+      element.focus();
+      this.handleChange(items[Number(element.getAttribute('data-index'))].name);
+      e.preventDefault();
+      e.stopPropagation();
+    }
+  };
+
+  setFocusRef = c => {
+    this.focusedItem = c;
+  };
+
+  renderItem = (item, i) => {
+    const { name, icon, meta, text } = item;
+
+    const active = (name === (this.props.value || this.state.value));
+
+    const computedClass = classNames('privacy-dropdown__option', { active });
+
+    let contents = this.props.renderItemContents && this.props.renderItemContents(item, i);
+
+    if (!contents) {
+      contents = (
+        <>
+          {icon && <Icon className='icon' fixedWidth id={icon} />}
+
+          <div className='privacy-dropdown__option__content'>
+            <strong>{text}</strong>
+            {meta}
+          </div>
+        </>
+      );
+    }
+
+    return (
+      <div
+        className={computedClass}
+        onClick={this.handleClick}
+        onKeyDown={this.handleKeyDown}
+        role='option'
+        aria-selected={active}
+        tabIndex={0}
+        key={name}
+        data-index={i}
+        ref={active ? this.setFocusRef : null}
+      >
+        {contents}
+      </div>
+    );
+  };
+
+  //  Rendering.
+  render () {
+    const {
+      items,
+      style,
+    } = this.props;
+
+    //  The result.
+    return (
+      <div style={{ ...style }} role='listbox' ref={this.setRef}>
+        {!!items && items.map((item, i) => this.renderItem(item, i))}
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/compose/components/emoji_picker_dropdown.jsx b/app/javascript/flavours/blobfox/features/compose/components/emoji_picker_dropdown.jsx
new file mode 100644
index 00000000000000..e228f9c205f922
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/components/emoji_picker_dropdown.jsx
@@ -0,0 +1,422 @@
+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 Overlay from 'react-overlays/Overlay';
+
+import { useSystemEmojiFont } from 'flavours/blobfox/initial_state';
+import { assetHost } from 'flavours/blobfox/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...' },
+  custom: { id: 'emoji_button.custom', defaultMessage: 'Custom' },
+  recent: { id: 'emoji_button.recent', defaultMessage: 'Frequently used' },
+  search_results: { id: 'emoji_button.search_results', defaultMessage: 'Search results' },
+  people: { id: 'emoji_button.people', defaultMessage: 'People' },
+  nature: { id: 'emoji_button.nature', defaultMessage: 'Nature' },
+  food: { id: 'emoji_button.food', defaultMessage: 'Food & Drink' },
+  activity: { id: 'emoji_button.activity', defaultMessage: 'Activity' },
+  travel: { id: 'emoji_button.travel', defaultMessage: 'Travel & Places' },
+  objects: { id: 'emoji_button.objects', defaultMessage: 'Objects' },
+  symbols: { id: 'emoji_button.symbols', defaultMessage: 'Symbols' },
+  flags: { id: 'emoji_button.flags', defaultMessage: 'Flags' },
+});
+
+let EmojiPicker, Emoji; // load asynchronously
+
+const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
+
+const backgroundImageFn = () => `${assetHost}/emoji/sheet_13.png`;
+
+const notFoundFn = () => (
+  <div className='emoji-mart-no-results'>
+    <Emoji
+      emoji='sleuth_or_spy'
+      set='twitter'
+      size={32}
+      sheetSize={32}
+      backgroundImageFn={backgroundImageFn}
+    />
+
+    <div className='emoji-mart-no-results-label'>
+      <FormattedMessage id='emoji_button.not_found' defaultMessage='No matching emojis found' />
+    </div>
+  </div>
+);
+
+class ModifierPickerMenu extends PureComponent {
+
+  static propTypes = {
+    active: PropTypes.bool,
+    onSelect: PropTypes.func.isRequired,
+    onClose: PropTypes.func.isRequired,
+  };
+
+  handleClick = e => {
+    this.props.onSelect(e.currentTarget.getAttribute('data-index') * 1);
+  };
+
+  UNSAFE_componentWillReceiveProps (nextProps) {
+    if (nextProps.active) {
+      this.attachListeners();
+    } else {
+      this.removeListeners();
+    }
+  }
+
+  componentWillUnmount () {
+    this.removeListeners();
+  }
+
+  handleDocumentClick = e => {
+    if (this.node && !this.node.contains(e.target)) {
+      this.props.onClose();
+    }
+  };
+
+  attachListeners () {
+    document.addEventListener('click', this.handleDocumentClick, { capture: true });
+    document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
+  }
+
+  removeListeners () {
+    document.removeEventListener('click', this.handleDocumentClick, { capture: true });
+    document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
+  }
+
+  setRef = c => {
+    this.node = c;
+  };
+
+  render () {
+    const { active } = this.props;
+
+    return (
+      <div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}>
+        <button type='button' onClick={this.handleClick} data-index={1}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
+        <button type='button' onClick={this.handleClick} data-index={2}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
+        <button type='button' onClick={this.handleClick} data-index={3}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
+        <button type='button' onClick={this.handleClick} data-index={4}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
+        <button type='button' onClick={this.handleClick} data-index={5}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
+        <button type='button' onClick={this.handleClick} data-index={6}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
+      </div>
+    );
+  }
+
+}
+
+class ModifierPicker extends PureComponent {
+
+  static propTypes = {
+    active: PropTypes.bool,
+    modifier: PropTypes.number,
+    onChange: PropTypes.func,
+    onClose: PropTypes.func,
+    onOpen: PropTypes.func,
+  };
+
+  handleClick = () => {
+    if (this.props.active) {
+      this.props.onClose();
+    } else {
+      this.props.onOpen();
+    }
+  };
+
+  handleSelect = modifier => {
+    this.props.onChange(modifier);
+    this.props.onClose();
+  };
+
+  render () {
+    const { active, modifier } = this.props;
+
+    return (
+      <div className='emoji-picker-dropdown__modifiers'>
+        <Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} />
+        <ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} />
+      </div>
+    );
+  }
+
+}
+
+class EmojiPickerMenuImpl extends PureComponent {
+
+  static propTypes = {
+    custom_emojis: ImmutablePropTypes.list,
+    frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string),
+    loading: PropTypes.bool,
+    onClose: PropTypes.func.isRequired,
+    onPick: PropTypes.func.isRequired,
+    style: PropTypes.object,
+    intl: PropTypes.object.isRequired,
+    skinTone: PropTypes.number.isRequired,
+    onSkinTone: PropTypes.func.isRequired,
+  };
+
+  static defaultProps = {
+    style: {},
+    loading: true,
+    frequentlyUsedEmojis: [],
+  };
+
+  state = {
+    modifierOpen: false,
+    readyToFocus: false,
+  };
+
+  handleDocumentClick = e => {
+    if (this.node && !this.node.contains(e.target)) {
+      this.props.onClose();
+    }
+  };
+
+  componentDidMount () {
+    document.addEventListener('click', this.handleDocumentClick, { capture: true });
+    document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
+
+    // Because of https://github.com/react-bootstrap/react-bootstrap/issues/2614 we need
+    // to wait for a frame before focusing
+    requestAnimationFrame(() => {
+      this.setState({ readyToFocus: true });
+      if (this.node) {
+        const element = this.node.querySelector('input[type="search"]');
+        if (element) element.focus();
+      }
+    });
+  }
+
+  componentWillUnmount () {
+    document.removeEventListener('click', this.handleDocumentClick, { capture: true });
+    document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
+  }
+
+  setRef = c => {
+    this.node = c;
+  };
+
+  getI18n = () => {
+    const { intl } = this.props;
+
+    return {
+      search: intl.formatMessage(messages.emoji_search),
+      categories: {
+        search: intl.formatMessage(messages.search_results),
+        recent: intl.formatMessage(messages.recent),
+        people: intl.formatMessage(messages.people),
+        nature: intl.formatMessage(messages.nature),
+        foods: intl.formatMessage(messages.food),
+        activity: intl.formatMessage(messages.activity),
+        places: intl.formatMessage(messages.travel),
+        objects: intl.formatMessage(messages.objects),
+        symbols: intl.formatMessage(messages.symbols),
+        flags: intl.formatMessage(messages.flags),
+        custom: intl.formatMessage(messages.custom),
+      },
+    };
+  };
+
+  handleClick = (emoji, event) => {
+    if (!emoji.native) {
+      emoji.native = emoji.colons;
+    }
+    if (!(event.ctrlKey || event.metaKey)) {
+      this.props.onClose();
+    }
+    this.props.onPick(emoji);
+  };
+
+  handleModifierOpen = () => {
+    this.setState({ modifierOpen: true });
+  };
+
+  handleModifierClose = () => {
+    this.setState({ modifierOpen: false });
+  };
+
+  handleModifierChange = modifier => {
+    this.props.onSkinTone(modifier);
+  };
+
+  render () {
+    const { loading, style, intl, custom_emojis, skinTone, frequentlyUsedEmojis } = this.props;
+
+    if (loading) {
+      return <div style={{ width: 299 }} />;
+    }
+
+    const title = intl.formatMessage(messages.emoji);
+
+    const { modifierOpen } = this.state;
+
+    const categoriesSort = [
+      'recent',
+      'people',
+      'nature',
+      'foods',
+      'activity',
+      'places',
+      'objects',
+      'symbols',
+      'flags',
+    ];
+
+    categoriesSort.splice(1, 0, ...Array.from(categoriesFromEmojis(custom_emojis)).sort());
+
+    return (
+      <div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}>
+        <EmojiPicker
+          perLine={8}
+          emojiSize={22}
+          sheetSize={32}
+          custom={buildCustomEmojis(custom_emojis)}
+          color=''
+          emoji=''
+          set='twitter'
+          title={title}
+          i18n={this.getI18n()}
+          onClick={this.handleClick}
+          include={categoriesSort}
+          recent={frequentlyUsedEmojis}
+          skin={skinTone}
+          showPreview={false}
+          showSkinTones={false}
+          backgroundImageFn={backgroundImageFn}
+          notFound={notFoundFn}
+          autoFocus={this.state.readyToFocus}
+          emojiTooltip
+          native={useSystemEmojiFont}
+        />
+
+        <ModifierPicker
+          active={modifierOpen}
+          modifier={skinTone}
+          onOpen={this.handleModifierOpen}
+          onClose={this.handleModifierClose}
+          onChange={this.handleModifierChange}
+        />
+      </div>
+    );
+  }
+
+}
+
+const EmojiPickerMenu = injectIntl(EmojiPickerMenuImpl);
+
+class EmojiPickerDropdown extends PureComponent {
+
+  static propTypes = {
+    custom_emojis: ImmutablePropTypes.list,
+    frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string),
+    intl: PropTypes.object.isRequired,
+    onPickEmoji: PropTypes.func.isRequired,
+    onSkinTone: PropTypes.func.isRequired,
+    skinTone: PropTypes.number.isRequired,
+    button: PropTypes.node,
+    disabled: PropTypes.bool,
+  };
+
+  state = {
+    active: false,
+    loading: false,
+  };
+
+  setRef = (c) => {
+    this.dropdown = c;
+  };
+
+  onShowDropdown = () => {
+    this.setState({ active: true });
+
+    if (!EmojiPicker) {
+      this.setState({ loading: true });
+
+      EmojiPickerAsync().then(EmojiMart => {
+        EmojiPicker = EmojiMart.Picker;
+        Emoji       = EmojiMart.Emoji;
+
+        this.setState({ loading: false });
+      }).catch(() => {
+        this.setState({ loading: false, active: false });
+      });
+    }
+  };
+
+  onHideDropdown = () => {
+    this.setState({ active: false });
+  };
+
+  onToggle = (e) => {
+    if (!this.state.disabled && !this.state.loading && (!e.key || e.key === 'Enter')) {
+      if (this.state.active) {
+        this.onHideDropdown();
+      } else {
+        this.onShowDropdown(e);
+      }
+    }
+  };
+
+  handleKeyDown = e => {
+    if (e.key === 'Escape') {
+      this.onHideDropdown();
+    }
+  };
+
+  setTargetRef = c => {
+    this.target = c;
+  };
+
+  findTarget = () => {
+    return this.target;
+  };
+
+  render () {
+    const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis, button } = this.props;
+    const title = intl.formatMessage(messages.emoji);
+    const { active, loading } = this.state;
+
+    return (
+      <div className='emoji-picker-dropdown' onKeyDown={this.handleKeyDown}>
+        <div ref={this.setTargetRef} className='emoji-button' title={title} aria-label={title} aria-expanded={active} role='button' onClick={this.onToggle} onKeyDown={this.onToggle} tabIndex={0}>
+          {button || <img
+            className={classNames('emojione', { 'pulse-loading': active && loading })}
+            alt='🙂'
+            src={`${assetHost}/emoji/1f642.svg`}
+          />}
+        </div>
+
+        <Overlay show={active} placement={'bottom'} flip target={this.findTarget} popperConfig={{ strategy: 'fixed' }}>
+          {({ props, placement })=> (
+            <div {...props} style={{ ...props.style, width: 299 }}>
+              <div className={`dropdown-animation ${placement}`}>
+                <EmojiPickerMenu
+                  custom_emojis={this.props.custom_emojis}
+                  loading={loading}
+                  onClose={this.onHideDropdown}
+                  onPick={onPickEmoji}
+                  onSkinTone={onSkinTone}
+                  skinTone={skinTone}
+                  frequentlyUsedEmojis={frequentlyUsedEmojis}
+                />
+              </div>
+            </div>
+          )}
+        </Overlay>
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(EmojiPickerDropdown);
diff --git a/app/javascript/flavours/blobfox/features/compose/components/header.jsx b/app/javascript/flavours/blobfox/features/compose/components/header.jsx
new file mode 100644
index 00000000000000..a8d3e080a04b7b
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/components/header.jsx
@@ -0,0 +1,134 @@
+import PropTypes from 'prop-types';
+
+import { injectIntl, defineMessages } from 'react-intl';
+
+import { Link } from 'react-router-dom';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import { Icon } from 'flavours/blobfox/components/icon';
+import { signOutLink } from 'flavours/blobfox/utils/backend_links';
+import { conditionalRender } from 'flavours/blobfox/utils/react_helpers';
+
+const messages = defineMessages({
+  community: {
+    defaultMessage: 'Local timeline',
+    id: 'navigation_bar.community_timeline',
+  },
+  home_timeline: {
+    defaultMessage: 'Home',
+    id: 'tabs_bar.home',
+  },
+  logout: {
+    defaultMessage: 'Logout',
+    id: 'navigation_bar.logout',
+  },
+  notifications: {
+    defaultMessage: 'Notifications',
+    id: 'tabs_bar.notifications',
+  },
+  public: {
+    defaultMessage: 'Federated timeline',
+    id: 'navigation_bar.public_timeline',
+  },
+  settings: {
+    defaultMessage: 'App settings',
+    id: 'navigation_bar.app_settings',
+  },
+  start: {
+    defaultMessage: 'Getting started',
+    id: 'getting_started.heading',
+  },
+});
+
+class Header extends ImmutablePureComponent {
+
+  static propTypes = {
+    columns: ImmutablePropTypes.list,
+    unreadNotifications: PropTypes.number,
+    showNotificationsBadge: PropTypes.bool,
+    intl: PropTypes.object,
+    onSettingsClick: PropTypes.func,
+    onLogout: PropTypes.func.isRequired,
+  };
+
+  handleLogoutClick = e => {
+    e.preventDefault();
+    e.stopPropagation();
+
+    this.props.onLogout();
+
+    return false;
+  };
+
+  render () {
+    const { intl, columns, unreadNotifications, showNotificationsBadge, onSettingsClick } = this.props;
+
+    //  Only renders the component if the column isn't being shown.
+    const renderForColumn = conditionalRender.bind(null,
+      columnId => !columns || !columns.some(
+        column => column.get('id') === columnId,
+      ),
+    );
+
+    //  The result.
+    return (
+      <nav className='drawer__header'>
+        <Link
+          aria-label={intl.formatMessage(messages.start)}
+          title={intl.formatMessage(messages.start)}
+          to='/getting-started'
+        ><Icon id='asterisk' /></Link>
+        {renderForColumn('HOME', (
+          <Link
+            aria-label={intl.formatMessage(messages.home_timeline)}
+            title={intl.formatMessage(messages.home_timeline)}
+            to='/home'
+          ><Icon id='home' /></Link>
+        ))}
+        {renderForColumn('NOTIFICATIONS', (
+          <Link
+            aria-label={intl.formatMessage(messages.notifications)}
+            title={intl.formatMessage(messages.notifications)}
+            to='/notifications'
+          >
+            <span className='icon-badge-wrapper'>
+              <Icon id='bell' />
+              { showNotificationsBadge && unreadNotifications > 0 && <div className='icon-badge' />}
+            </span>
+          </Link>
+        ))}
+        {renderForColumn('COMMUNITY', (
+          <Link
+            aria-label={intl.formatMessage(messages.community)}
+            title={intl.formatMessage(messages.community)}
+            to='/public/local'
+          ><Icon id='users' /></Link>
+        ))}
+        {renderForColumn('PUBLIC', (
+          <Link
+            aria-label={intl.formatMessage(messages.public)}
+            title={intl.formatMessage(messages.public)}
+            to='/public'
+          ><Icon id='globe' /></Link>
+        ))}
+        <a
+          aria-label={intl.formatMessage(messages.settings)}
+          onClick={onSettingsClick}
+          href='/settings/preferences'
+          title={intl.formatMessage(messages.settings)}
+        ><Icon id='cogs' /></a>
+        <a
+          aria-label={intl.formatMessage(messages.logout)}
+          onClick={this.handleLogoutClick}
+          href={signOutLink}
+          title={intl.formatMessage(messages.logout)}
+        ><Icon id='sign-out' /></a>
+      </nav>
+    );
+  }
+
+}
+
+export default injectIntl(Header);
diff --git a/app/javascript/flavours/blobfox/features/compose/components/language_dropdown.jsx b/app/javascript/flavours/blobfox/features/compose/components/language_dropdown.jsx
new file mode 100644
index 00000000000000..53ef6ebd63716b
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/components/language_dropdown.jsx
@@ -0,0 +1,334 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { injectIntl, defineMessages } from 'react-intl';
+
+import classNames from 'classnames';
+
+import { supportsPassiveEvents } from 'detect-passive-events';
+import fuzzysort from 'fuzzysort';
+import Overlay from 'react-overlays/Overlay';
+
+import { languages as preloadedLanguages } from 'flavours/blobfox/initial_state';
+import { loupeIcon, deleteIcon } from 'flavours/blobfox/utils/icons';
+
+import TextIconButton from './text_icon_button';
+
+const messages = defineMessages({
+  changeLanguage: { id: 'compose.language.change', defaultMessage: 'Change language' },
+  search: { id: 'compose.language.search', defaultMessage: 'Search languages...' },
+  clear: { id: 'emoji_button.clear', defaultMessage: 'Clear' },
+});
+
+const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
+
+class LanguageDropdownMenu extends PureComponent {
+
+  static propTypes = {
+    value: PropTypes.string.isRequired,
+    frequentlyUsedLanguages: PropTypes.arrayOf(PropTypes.string).isRequired,
+    onClose: PropTypes.func.isRequired,
+    onChange: PropTypes.func.isRequired,
+    languages: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)),
+    intl: PropTypes.object,
+  };
+
+  static defaultProps = {
+    languages: preloadedLanguages,
+  };
+
+  state = {
+    searchValue: '',
+  };
+
+  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('touchend', this.handleDocumentClick, listenerOptions);
+
+    // Because of https://github.com/react-bootstrap/react-bootstrap/issues/2614 we need
+    // to wait for a frame before focusing
+    requestAnimationFrame(() => {
+      if (this.node) {
+        const element = this.node.querySelector('input[type="search"]');
+        if (element) element.focus();
+      }
+    });
+  }
+
+  componentWillUnmount () {
+    document.removeEventListener('click', this.handleDocumentClick, { capture: true });
+    document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
+  }
+
+  setRef = c => {
+    this.node = c;
+  };
+
+  setListRef = c => {
+    this.listNode = c;
+  };
+
+  handleSearchChange = ({ target }) => {
+    this.setState({ searchValue: target.value });
+  };
+
+  search () {
+    const { languages, value, frequentlyUsedLanguages } = this.props;
+    const { searchValue } = this.state;
+
+    if (searchValue === '') {
+      return [...languages].sort((a, b) => {
+        // Push current selection to the top of the list
+
+        if (a[0] === value) {
+          return -1;
+        } else if (b[0] === value) {
+          return 1;
+        } else {
+          // Sort according to frequently used languages
+
+          const indexOfA = frequentlyUsedLanguages.indexOf(a[0]);
+          const indexOfB = frequentlyUsedLanguages.indexOf(b[0]);
+
+          return ((indexOfA > -1 ? indexOfA : Infinity) - (indexOfB > -1 ? indexOfB : Infinity));
+        }
+      });
+    }
+
+    return fuzzysort.go(searchValue, languages, {
+      keys: ['0', '1', '2'],
+      limit: 5,
+      threshold: -10000,
+    }).map(result => result.obj);
+  }
+
+  frequentlyUsed () {
+    const { languages, value } = this.props;
+    const current = languages.find(lang => lang[0] === value);
+    const results = [];
+
+    if (current) {
+      results.push(current);
+    }
+
+    return results;
+  }
+
+  handleClick = e => {
+    const value = e.currentTarget.getAttribute('data-index');
+
+    e.preventDefault();
+
+    this.props.onClose();
+    this.props.onChange(value);
+  };
+
+  handleKeyDown = e => {
+    const { onClose } = this.props;
+    const index = Array.from(this.listNode.childNodes).findIndex(node => node === e.currentTarget);
+
+    let element = null;
+
+    switch(e.key) {
+    case 'Escape':
+      onClose();
+      break;
+    case 'Enter':
+      this.handleClick(e);
+      break;
+    case 'ArrowDown':
+      element = this.listNode.childNodes[index + 1] || this.listNode.firstChild;
+      break;
+    case 'ArrowUp':
+      element = this.listNode.childNodes[index - 1] || this.listNode.lastChild;
+      break;
+    case 'Tab':
+      if (e.shiftKey) {
+        element = this.listNode.childNodes[index - 1] || this.listNode.lastChild;
+      } else {
+        element = this.listNode.childNodes[index + 1] || this.listNode.firstChild;
+      }
+      break;
+    case 'Home':
+      element = this.listNode.firstChild;
+      break;
+    case 'End':
+      element = this.listNode.lastChild;
+      break;
+    }
+
+    if (element) {
+      element.focus();
+      e.preventDefault();
+      e.stopPropagation();
+    }
+  };
+
+  handleSearchKeyDown = e => {
+    const { onChange, onClose } = this.props;
+    const { searchValue } = this.state;
+
+    let element = null;
+
+    switch(e.key) {
+    case 'Tab':
+    case 'ArrowDown':
+      element = this.listNode.firstChild;
+
+      if (element) {
+        element.focus();
+        e.preventDefault();
+        e.stopPropagation();
+      }
+
+      break;
+    case 'Enter':
+      element = this.listNode.firstChild;
+
+      if (element) {
+        onChange(element.getAttribute('data-index'));
+        onClose();
+      }
+      break;
+    case 'Escape':
+      if (searchValue !== '') {
+        e.preventDefault();
+        this.handleClear();
+      }
+
+      break;
+    }
+  };
+
+  handleClear = () => {
+    this.setState({ searchValue: '' });
+  };
+
+  renderItem = lang => {
+    const { value } = this.props;
+
+    return (
+      <div key={lang[0]} role='option' tabIndex={0} data-index={lang[0]} className={classNames('language-dropdown__dropdown__results__item', { active: lang[0] === value })} aria-selected={lang[0] === value} onClick={this.handleClick} onKeyDown={this.handleKeyDown}>
+        <span className='language-dropdown__dropdown__results__item__native-name' lang={lang[0]}>{lang[2]}</span> <span className='language-dropdown__dropdown__results__item__common-name'>({lang[1]})</span>
+      </div>
+    );
+  };
+
+  render () {
+    const { intl } = this.props;
+    const { searchValue } = this.state;
+    const isSearching = searchValue !== '';
+    const results = this.search();
+
+    return (
+      <div ref={this.setRef}>
+        <div className='emoji-mart-search'>
+          <input type='search' value={searchValue} onChange={this.handleSearchChange} onKeyDown={this.handleSearchKeyDown} placeholder={intl.formatMessage(messages.search)} />
+          <button type='button' className='emoji-mart-search-icon' disabled={!isSearching} aria-label={intl.formatMessage(messages.clear)} onClick={this.handleClear}>{!isSearching ? loupeIcon : deleteIcon}</button>
+        </div>
+
+        <div className='language-dropdown__dropdown__results emoji-mart-scroll' role='listbox' ref={this.setListRef}>
+          {results.map(this.renderItem)}
+        </div>
+      </div>
+    );
+  }
+
+}
+
+class LanguageDropdown extends PureComponent {
+
+  static propTypes = {
+    value: PropTypes.string,
+    frequentlyUsedLanguages: PropTypes.arrayOf(PropTypes.string),
+    intl: PropTypes.object.isRequired,
+    onChange: PropTypes.func,
+    onClose: PropTypes.func,
+  };
+
+  state = {
+    open: false,
+    placement: 'bottom',
+  };
+
+  handleToggle = () => {
+    if (this.state.open && this.activeElement) {
+      this.activeElement.focus({ preventScroll: true });
+    }
+
+    this.setState({ open: !this.state.open });
+  };
+
+  handleClose = () => {
+    const { value, onClose } = this.props;
+
+    if (this.state.open && this.activeElement) {
+      this.activeElement.focus({ preventScroll: true });
+    }
+
+    this.setState({ open: false });
+    onClose(value);
+  };
+
+  handleChange = value => {
+    const { onChange } = this.props;
+    onChange(value);
+  };
+
+  setTargetRef = c => {
+    this.target = c;
+  };
+
+  findTarget = () => {
+    return this.target;
+  };
+
+  handleOverlayEnter = (state) => {
+    this.setState({ placement: state.placement });
+  };
+
+  render () {
+    const { value, intl, frequentlyUsedLanguages } = this.props;
+    const { open, placement } = this.state;
+
+    return (
+      <div className={classNames('privacy-dropdown', placement, { active: open })}>
+        <div className='privacy-dropdown__value' ref={this.setTargetRef} >
+          <TextIconButton
+            className='privacy-dropdown__value-icon'
+            label={value && value.toUpperCase()}
+            title={intl.formatMessage(messages.changeLanguage)}
+            active={open}
+            onClick={this.handleToggle}
+          />
+        </div>
+
+        <Overlay show={open} placement={'bottom'} flip target={this.findTarget} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}>
+          {({ props, placement }) => (
+            <div {...props}>
+              <div className={`dropdown-animation language-dropdown__dropdown ${placement}`} >
+                <LanguageDropdownMenu
+                  value={value}
+                  frequentlyUsedLanguages={frequentlyUsedLanguages}
+                  onClose={this.handleClose}
+                  onChange={this.handleChange}
+                  intl={intl}
+                />
+              </div>
+            </div>
+          )}
+        </Overlay>
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(LanguageDropdown);
diff --git a/app/javascript/flavours/blobfox/features/compose/components/navigation_bar.jsx b/app/javascript/flavours/blobfox/features/compose/components/navigation_bar.jsx
new file mode 100644
index 00000000000000..b9601dd59ac49a
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/components/navigation_bar.jsx
@@ -0,0 +1,54 @@
+import PropTypes from 'prop-types';
+
+import { FormattedMessage } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import Permalink from 'flavours/blobfox/components/permalink';
+import { profileLink } from 'flavours/blobfox/utils/backend_links';
+
+import { Avatar } from '../../../components/avatar';
+
+import ActionBar from './action_bar';
+
+export default class NavigationBar extends ImmutablePureComponent {
+
+  static propTypes = {
+    account: ImmutablePropTypes.record.isRequired,
+    onLogout: PropTypes.func.isRequired,
+    onClose: PropTypes.func,
+  };
+
+  render () {
+    const username = this.props.account.get('acct');
+    return (
+      <div className='navigation-bar'>
+        <Permalink className='avatar' href={this.props.account.get('url')} to={`/@${username}`}>
+          <span style={{ display: 'none' }}>{username}</span>
+          <Avatar account={this.props.account} size={46} />
+        </Permalink>
+
+        <div className='navigation-bar__profile'>
+          <span>
+            <Permalink className='acct' href={this.props.account.get('url')} to={`/@${username}`}>
+              <strong className='navigation-bar__profile-account'>@{username}</strong>
+            </Permalink>
+          </span>
+
+          { profileLink !== undefined && (
+            <a
+              className='edit'
+              href={profileLink}
+            ><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a>
+          )}
+        </div>
+
+        <div className='navigation-bar__actions'>
+          <ActionBar account={this.props.account} onLogout={this.props.onLogout} />
+        </div>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/compose/components/options.jsx b/app/javascript/flavours/blobfox/features/compose/components/options.jsx
new file mode 100644
index 00000000000000..e3f46d97486b03
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/components/options.jsx
@@ -0,0 +1,311 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
+import Toggle from 'react-toggle';
+
+import { IconButton } from 'flavours/blobfox/components/icon_button';
+import { pollLimits } from 'flavours/blobfox/initial_state';
+
+import DropdownContainer from '../containers/dropdown_container';
+import LanguageDropdown from '../containers/language_dropdown_container';
+import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
+
+import TextIconButton from './text_icon_button';
+
+const messages = defineMessages({
+  advanced_options_icon_title: {
+    defaultMessage: 'Advanced options',
+    id: 'advanced_options.icon_title',
+  },
+  attach: {
+    defaultMessage: 'Attach...',
+    id: 'compose.attach',
+  },
+  content_type: {
+    defaultMessage: 'Content type',
+    id: 'content-type.change',
+  },
+  doodle: {
+    defaultMessage: 'Draw something',
+    id: 'compose.attach.doodle',
+  },
+  html: {
+    defaultMessage: 'HTML',
+    id: 'compose.content-type.html',
+  },
+  local_only_long: {
+    defaultMessage: 'Do not post to other instances',
+    id: 'advanced_options.local-only.long',
+  },
+  local_only_short: {
+    defaultMessage: 'Local-only',
+    id: 'advanced_options.local-only.short',
+  },
+  markdown: {
+    defaultMessage: 'Markdown',
+    id: 'compose.content-type.markdown',
+  },
+  plain: {
+    defaultMessage: 'Plain text',
+    id: 'compose.content-type.plain',
+  },
+  spoiler: {
+    defaultMessage: 'Hide text behind warning',
+    id: 'compose_form.spoiler',
+  },
+  threaded_mode_long: {
+    defaultMessage: 'Automatically opens a reply on posting',
+    id: 'advanced_options.threaded_mode.long',
+  },
+  threaded_mode_short: {
+    defaultMessage: 'Threaded mode',
+    id: 'advanced_options.threaded_mode.short',
+  },
+  upload: {
+    defaultMessage: 'Upload a file',
+    id: 'compose.attach.upload',
+  },
+  add_poll: {
+    defaultMessage: 'Add a poll',
+    id: 'poll_button.add_poll',
+  },
+  remove_poll: {
+    defaultMessage: 'Remove poll',
+    id: 'poll_button.remove_poll',
+  },
+});
+
+const mapStateToProps = (state, { name }) => ({
+  checked: state.getIn(['compose', 'advanced_options', name]),
+});
+
+class ToggleOptionImpl extends ImmutablePureComponent {
+
+  static propTypes = {
+    name: PropTypes.string.isRequired,
+    checked: PropTypes.bool,
+    onChangeAdvancedOption: PropTypes.func.isRequired,
+  };
+
+  handleChange = () => {
+    this.props.onChangeAdvancedOption(this.props.name);
+  };
+
+  render() {
+    const { meta, text, checked } = this.props;
+
+    return (
+      <>
+        <Toggle checked={checked} onChange={this.handleChange} />
+
+        <div className='privacy-dropdown__option__content'>
+          <strong>{text}</strong>
+          {meta}
+        </div>
+      </>
+    );
+  }
+
+}
+
+const ToggleOption = connect(mapStateToProps)(ToggleOptionImpl);
+
+class ComposerOptions extends ImmutablePureComponent {
+
+  static propTypes = {
+    acceptContentTypes: PropTypes.string,
+    advancedOptions: ImmutablePropTypes.map,
+    disabled: PropTypes.bool,
+    allowMedia: PropTypes.bool,
+    allowPoll: PropTypes.bool,
+    hasPoll: PropTypes.bool,
+    intl: PropTypes.object.isRequired,
+    onChangeAdvancedOption: PropTypes.func.isRequired,
+    onChangeContentType: PropTypes.func.isRequired,
+    onTogglePoll: PropTypes.func.isRequired,
+    onDoodleOpen: PropTypes.func.isRequired,
+    onToggleSpoiler: PropTypes.func,
+    onUpload: PropTypes.func.isRequired,
+    contentType: PropTypes.string,
+    resetFileKey: PropTypes.number,
+    spoiler: PropTypes.bool,
+    showContentTypeChoice: PropTypes.bool,
+    isEditing: PropTypes.bool,
+  };
+
+  handleChangeFiles = ({ target: { files } }) => {
+    const { onUpload } = this.props;
+    if (files.length) {
+      onUpload(files);
+    }
+  };
+
+  handleClickAttach = (name) => {
+    const { fileElement } = this;
+    const { onDoodleOpen } = this.props;
+
+    switch (name) {
+    case 'upload':
+      if (fileElement) {
+        fileElement.click();
+      }
+      return;
+    case 'doodle':
+      onDoodleOpen();
+      return;
+    }
+  };
+
+  handleRefFileElement = (fileElement) => {
+    this.fileElement = fileElement;
+  };
+
+  renderToggleItemContents = (item) => {
+    const { onChangeAdvancedOption } = this.props;
+    const { name, meta, text } = item;
+
+    return <ToggleOption name={name} text={text} meta={meta} onChangeAdvancedOption={onChangeAdvancedOption} />;
+  };
+
+  render () {
+    const {
+      acceptContentTypes,
+      advancedOptions,
+      contentType,
+      disabled,
+      allowMedia,
+      allowPoll,
+      hasPoll,
+      onChangeAdvancedOption,
+      onChangeContentType,
+      onTogglePoll,
+      onToggleSpoiler,
+      resetFileKey,
+      spoiler,
+      showContentTypeChoice,
+      isEditing,
+      intl: { formatMessage },
+    } = this.props;
+
+    const contentTypeItems = {
+      plain: {
+        icon: 'file-text',
+        name: 'text/plain',
+        text: formatMessage(messages.plain),
+      },
+      html: {
+        icon: 'code',
+        name: 'text/html',
+        text: formatMessage(messages.html),
+      },
+      markdown: {
+        icon: 'arrow-circle-down',
+        name: 'text/markdown',
+        text: formatMessage(messages.markdown),
+      },
+    };
+
+    //  The result.
+    return (
+      <div className='compose-form__buttons'>
+        <input
+          accept={acceptContentTypes}
+          disabled={disabled || !allowMedia}
+          key={resetFileKey}
+          onChange={this.handleChangeFiles}
+          ref={this.handleRefFileElement}
+          type='file'
+          multiple
+          style={{ display: 'none' }}
+        />
+        <DropdownContainer
+          disabled={disabled || !allowMedia}
+          icon='paperclip'
+          items={[
+            {
+              icon: 'cloud-upload',
+              name: 'upload',
+              text: formatMessage(messages.upload),
+            },
+            {
+              icon: 'paint-brush',
+              name: 'doodle',
+              text: formatMessage(messages.doodle),
+            },
+          ]}
+          onChange={this.handleClickAttach}
+          title={formatMessage(messages.attach)}
+        />
+        {!!pollLimits && (
+          <IconButton
+            active={hasPoll}
+            disabled={disabled || !allowPoll}
+            icon='tasks'
+            inverted
+            onClick={onTogglePoll}
+            size={18}
+            style={{
+              height: null,
+              lineHeight: null,
+            }}
+            title={formatMessage(hasPoll ? messages.remove_poll : messages.add_poll)}
+          />
+        )}
+        <hr />
+        <PrivacyDropdownContainer disabled={disabled || isEditing} />
+        {showContentTypeChoice && (
+          <DropdownContainer
+            disabled={disabled}
+            icon={(contentTypeItems[contentType.split('/')[1]] || {}).icon}
+            items={[
+              contentTypeItems.plain,
+              contentTypeItems.html,
+              contentTypeItems.markdown,
+            ]}
+            onChange={onChangeContentType}
+            title={formatMessage(messages.content_type)}
+            value={contentType}
+          />
+        )}
+        {onToggleSpoiler && (
+          <TextIconButton
+            active={spoiler}
+            ariaControls='cw-spoiler-input'
+            label='CW'
+            onClick={onToggleSpoiler}
+            title={formatMessage(messages.spoiler)}
+          />
+        )}
+        <LanguageDropdown />
+        <DropdownContainer
+          disabled={disabled || isEditing}
+          icon='ellipsis-h'
+          items={advancedOptions ? [
+            {
+              meta: formatMessage(messages.local_only_long),
+              name: 'do_not_federate',
+              text: formatMessage(messages.local_only_short),
+            },
+            {
+              meta: formatMessage(messages.threaded_mode_long),
+              name: 'threaded_mode',
+              text: formatMessage(messages.threaded_mode_short),
+            },
+          ] : null}
+          onChange={onChangeAdvancedOption}
+          renderItemContents={this.renderToggleItemContents}
+          title={formatMessage(messages.advanced_options_icon_title)}
+          closeOnChange={false}
+        />
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(ComposerOptions);
diff --git a/app/javascript/flavours/blobfox/features/compose/components/poll_form.jsx b/app/javascript/flavours/blobfox/features/compose/components/poll_form.jsx
new file mode 100644
index 00000000000000..008888265091d2
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/components/poll_form.jsx
@@ -0,0 +1,181 @@
+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 ImmutablePureComponent from 'react-immutable-pure-component';
+
+import AutosuggestInput from 'flavours/blobfox/components/autosuggest_input';
+import { Icon }  from 'flavours/blobfox/components/icon';
+import { IconButton } from 'flavours/blobfox/components/icon_button';
+import { pollLimits } from 'flavours/blobfox/initial_state';
+
+const messages = defineMessages({
+  option_placeholder: { id: 'compose_form.poll.option_placeholder', defaultMessage: 'Choice {number}' },
+  add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add a choice' },
+  remove_option: { id: 'compose_form.poll.remove_option', defaultMessage: 'Remove this choice' },
+  poll_duration: { id: 'compose_form.poll.duration', defaultMessage: 'Poll duration' },
+  single_choice: { id: 'compose_form.poll.single_choice', defaultMessage: 'Allow one choice' },
+  multiple_choices: { id: 'compose_form.poll.multiple_choices', defaultMessage: 'Allow multiple choices' },
+  minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
+  hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
+  days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' },
+});
+
+class OptionIntl extends PureComponent {
+
+  static propTypes = {
+    title: PropTypes.string.isRequired,
+    lang: PropTypes.string,
+    index: PropTypes.number.isRequired,
+    isPollMultiple: PropTypes.bool,
+    autoFocus: PropTypes.bool,
+    onChange: PropTypes.func.isRequired,
+    onRemove: PropTypes.func.isRequired,
+    suggestions: ImmutablePropTypes.list,
+    onClearSuggestions: PropTypes.func.isRequired,
+    onFetchSuggestions: PropTypes.func.isRequired,
+    onSuggestionSelected: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  handleOptionTitleChange = e => {
+    this.props.onChange(this.props.index, e.target.value);
+  };
+
+  handleOptionRemove = () => {
+    this.props.onRemove(this.props.index);
+  };
+
+  onSuggestionsClearRequested = () => {
+    this.props.onClearSuggestions();
+  };
+
+  onSuggestionsFetchRequested = (token) => {
+    this.props.onFetchSuggestions(token);
+  };
+
+  onSuggestionSelected = (tokenStart, token, value) => {
+    this.props.onSuggestionSelected(tokenStart, token, value, ['poll', 'options', this.props.index]);
+  };
+
+  render () {
+    const { isPollMultiple, title, lang, index, autoFocus, intl } = this.props;
+
+    return (
+      <li>
+        <label className='poll__option editable'>
+          <span className={classNames('poll__input', { checkbox: isPollMultiple })} />
+
+          <AutosuggestInput
+            placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })}
+            maxLength={pollLimits.max_option_chars}
+            value={title}
+            lang={lang}
+            spellCheck
+            onChange={this.handleOptionTitleChange}
+            suggestions={this.props.suggestions}
+            onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
+            onSuggestionsClearRequested={this.onSuggestionsClearRequested}
+            onSuggestionSelected={this.onSuggestionSelected}
+            searchTokens={[':']}
+            autoFocus={autoFocus}
+          />
+        </label>
+
+        <div className='poll__cancel'>
+          <IconButton disabled={index < 1} title={intl.formatMessage(messages.remove_option)} icon='times' onClick={this.handleOptionRemove} />
+        </div>
+      </li>
+    );
+  }
+
+}
+
+const Option = injectIntl(OptionIntl);
+
+class PollForm extends ImmutablePureComponent {
+
+  static propTypes = {
+    options: ImmutablePropTypes.list,
+    lang: PropTypes.string,
+    expiresIn: PropTypes.number,
+    isMultiple: PropTypes.bool,
+    onChangeOption: PropTypes.func.isRequired,
+    onAddOption: PropTypes.func.isRequired,
+    onRemoveOption: PropTypes.func.isRequired,
+    onChangeSettings: PropTypes.func.isRequired,
+    suggestions: ImmutablePropTypes.list,
+    onClearSuggestions: PropTypes.func.isRequired,
+    onFetchSuggestions: PropTypes.func.isRequired,
+    onSuggestionSelected: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  handleAddOption = () => {
+    this.props.onAddOption('');
+  };
+
+  handleSelectDuration = e => {
+    this.props.onChangeSettings(e.target.value, this.props.isMultiple);
+  };
+
+  handleSelectMultiple = e => {
+    this.props.onChangeSettings(this.props.expiresIn, e.target.value === 'true');
+  };
+
+  render () {
+    const { options, lang, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl, ...other } = this.props;
+
+    if (!options) {
+      return null;
+    }
+
+    const autoFocusIndex = options.indexOf('');
+
+    return (
+      <div className='compose-form__poll-wrapper'>
+        <ul>
+          {options.map((title, i) => <Option title={title} lang={lang} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} autoFocus={i === autoFocusIndex} {...other} />)}
+          {options.size < pollLimits.max_options && (
+            <label className='poll__text editable'>
+              <span className={classNames('poll__input')} style={{ opacity: 0 }} />
+              <button className='button button-secondary' onClick={this.handleAddOption} type='button'><Icon id='plus' /> <FormattedMessage {...messages.add_option} /></button>
+            </label>
+          )}
+        </ul>
+
+        <div className='poll__footer'>
+          {/* eslint-disable-next-line jsx-a11y/no-onchange */}
+          <select value={isMultiple ? 'true' : 'false'} onChange={this.handleSelectMultiple}>
+            <option value='false'>{intl.formatMessage(messages.single_choice)}</option>
+            <option value='true'>{intl.formatMessage(messages.multiple_choices)}</option>
+          </select>
+
+          {/* eslint-disable-next-line jsx-a11y/no-onchange */}
+          <select value={expiresIn} onChange={this.handleSelectDuration}>
+            <option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option>
+            <option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option>
+            <option value={3600}>{intl.formatMessage(messages.hours, { number: 1 })}</option>
+            <option value={21600}>{intl.formatMessage(messages.hours, { number: 6 })}</option>
+            <option value={43200}>{intl.formatMessage(messages.hours, { number: 12 })}</option>
+            <option value={86400}>{intl.formatMessage(messages.days, { number: 1 })}</option>
+            <option value={259200}>{intl.formatMessage(messages.days, { number: 3 })}</option>
+            <option value={604800}>{intl.formatMessage(messages.days, { number: 7 })}</option>
+            <option value={1209600}>{intl.formatMessage(messages.days, { number: 14 })}</option>
+            <option value={2629746}>{intl.formatMessage(messages.days, { number: 30 })}</option>
+            <option value={7889238}>{intl.formatMessage(messages.days, { number: 91 })}</option>
+            <option value={2629746}>{intl.formatMessage(messages.days, { number: 182 })}</option>
+            <option value={31536000}>{intl.formatMessage(messages.days, { number: 365 })}</option>
+          </select>
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(PollForm);
diff --git a/app/javascript/flavours/blobfox/features/compose/components/privacy_dropdown.jsx b/app/javascript/flavours/blobfox/features/compose/components/privacy_dropdown.jsx
new file mode 100644
index 00000000000000..06775230fe40d0
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/components/privacy_dropdown.jsx
@@ -0,0 +1,90 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { injectIntl, defineMessages } from 'react-intl';
+
+import Dropdown from './dropdown';
+
+const messages = defineMessages({
+  public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
+  public_long: { id: 'privacy.public.long', defaultMessage: 'Visible for all' },
+  unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
+  unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Visible for all, but opted-out of discovery features' },
+  private_short: { id: 'privacy.private.short', defaultMessage: 'Followers only' },
+  private_long: { id: 'privacy.private.long', defaultMessage: 'Visible for followers only' },
+  direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
+  direct_long: { id: 'privacy.direct.long', defaultMessage: 'Visible for mentioned users only' },
+  change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' },
+});
+
+class PrivacyDropdown extends PureComponent {
+
+  static propTypes = {
+    isUserTouching: PropTypes.func,
+    onModalOpen: PropTypes.func,
+    onModalClose: PropTypes.func,
+    value: PropTypes.string.isRequired,
+    onChange: PropTypes.func.isRequired,
+    noDirect: PropTypes.bool,
+    container: PropTypes.func,
+    disabled: PropTypes.bool,
+    intl: PropTypes.object.isRequired,
+  };
+
+  render () {
+    const { value, onChange, onModalOpen, onModalClose, disabled, noDirect, container, isUserTouching, intl: { formatMessage } } = this.props;
+
+    //  We predefine our privacy items so that we can easily pick the
+    //  dropdown icon later.
+    const privacyItems = {
+      direct: {
+        icon: 'envelope',
+        meta: formatMessage(messages.direct_long),
+        name: 'direct',
+        text: formatMessage(messages.direct_short),
+      },
+      private: {
+        icon: 'lock',
+        meta: formatMessage(messages.private_long),
+        name: 'private',
+        text: formatMessage(messages.private_short),
+      },
+      public: {
+        icon: 'globe',
+        meta: formatMessage(messages.public_long),
+        name: 'public',
+        text: formatMessage(messages.public_short),
+      },
+      unlisted: {
+        icon: 'unlock',
+        meta: formatMessage(messages.unlisted_long),
+        name: 'unlisted',
+        text: formatMessage(messages.unlisted_short),
+      },
+    };
+
+    const items = [privacyItems.public, privacyItems.unlisted, privacyItems.private];
+
+    if (!noDirect) {
+      items.push(privacyItems.direct);
+    }
+
+    return (
+      <Dropdown
+        disabled={disabled}
+        icon={(privacyItems[value] || {}).icon}
+        items={items}
+        onChange={onChange}
+        isUserTouching={isUserTouching}
+        onModalClose={onModalClose}
+        onModalOpen={onModalOpen}
+        title={formatMessage(messages.change_privacy)}
+        container={container}
+        value={value}
+      />
+    );
+  }
+
+}
+
+export default injectIntl(PrivacyDropdown);
diff --git a/app/javascript/flavours/blobfox/features/compose/components/publisher.jsx b/app/javascript/flavours/blobfox/features/compose/components/publisher.jsx
new file mode 100644
index 00000000000000..df71df09818aa1
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/components/publisher.jsx
@@ -0,0 +1,92 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl } from 'react-intl';
+
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import { Button } from 'flavours/blobfox/components/button';
+import { Icon } from 'flavours/blobfox/components/icon';
+
+const messages = defineMessages({
+  publish: {
+    defaultMessage: 'Publish',
+    id: 'compose_form.publish',
+  },
+  publishLoud: {
+    defaultMessage: '{publish}!',
+    id: 'compose_form.publish_loud',
+  },
+  saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Save changes' },
+  public: { id: 'privacy.public.short', defaultMessage: 'Public' },
+  unlisted: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
+  private: { id: 'privacy.private.short', defaultMessage: 'Followers only' },
+  direct: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
+});
+
+class Publisher extends ImmutablePureComponent {
+
+  static propTypes = {
+    disabled: PropTypes.bool,
+    intl: PropTypes.object.isRequired,
+    onSecondarySubmit: PropTypes.func,
+    privacy: PropTypes.oneOf(['direct', 'private', 'unlisted', 'public']),
+    sideArm: PropTypes.oneOf(['none', 'direct', 'private', 'unlisted', 'public']),
+    isEditing: PropTypes.bool,
+  };
+
+  render () {
+    const { disabled, intl, onSecondarySubmit, privacy, sideArm, isEditing } = this.props;
+
+    const privacyIcons = { direct: 'envelope', private: 'lock', public: 'globe', unlisted: 'unlock' };
+
+    let publishText;
+    if (isEditing) {
+      publishText = intl.formatMessage(messages.saveChanges);
+    } else if (privacy === 'private' || privacy === 'direct') {
+      const iconId = privacyIcons[privacy];
+      publishText = (
+        <span>
+          <Icon id={iconId} /> {intl.formatMessage(messages.publish)}
+        </span>
+      );
+    } else {
+      publishText = privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish);
+    }
+
+    const privacyNames = {
+      public: messages.public,
+      unlisted: messages.unlisted,
+      private: messages.private,
+      direct: messages.direct,
+    };
+
+    return (
+      <div className='compose-form__publish'>
+        {sideArm && !isEditing && sideArm !== 'none' && (
+          <div className='compose-form__publish-button-wrapper'>
+            <Button
+              className='side_arm'
+              disabled={disabled}
+              onClick={onSecondarySubmit}
+              style={{ padding: null }}
+              text={<Icon id={privacyIcons[sideArm]} />}
+              title={`${intl.formatMessage(messages.publish)}: ${intl.formatMessage(privacyNames[sideArm])}`}
+            />
+          </div>
+        )}
+        <div className='compose-form__publish-button-wrapper'>
+          <Button
+            className='primary'
+            type='submit'
+            text={publishText}
+            title={`${intl.formatMessage(messages.publish)}: ${intl.formatMessage(privacyNames[privacy])}`}
+            disabled={disabled}
+          />
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(Publisher);
diff --git a/app/javascript/flavours/blobfox/features/compose/components/reply_indicator.jsx b/app/javascript/flavours/blobfox/features/compose/components/reply_indicator.jsx
new file mode 100644
index 00000000000000..a3b42eea1c62bb
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/components/reply_indicator.jsx
@@ -0,0 +1,70 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import AttachmentList from 'flavours/blobfox/components/attachment_list';
+
+import { IconButton } from '../../../components/icon_button';
+import AccountContainer from '../../../containers/account_container';
+
+const messages = defineMessages({
+  cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
+});
+
+class ReplyIndicator extends ImmutablePureComponent {
+
+  static propTypes = {
+    status: ImmutablePropTypes.map,
+    onCancel: PropTypes.func,
+    intl: PropTypes.object.isRequired,
+  };
+
+  handleClick = () => {
+    const { onCancel } = this.props;
+    if (onCancel) {
+      onCancel();
+    }
+  };
+
+  render () {
+    const { status, intl } = this.props;
+
+    if (!status) {
+      return null;
+    }
+
+    const content = { __html: status.get('contentHtml') };
+
+    const account     = status.get('account');
+
+    return (
+      <div className='reply-indicator'>
+        <div className='reply-indicator__header'>
+          <div className='reply-indicator__cancel'><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} inverted /></div>
+
+          {account && (
+            <AccountContainer
+              id={account}
+              small
+            />
+          )}
+        </div>
+
+        <div className='reply-indicator__content translate' dangerouslySetInnerHTML={content} />
+
+        {status.get('media_attachments').size > 0 && (
+          <AttachmentList
+            compact
+            media={status.get('media_attachments')}
+          />
+        )}
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(ReplyIndicator);
diff --git a/app/javascript/flavours/blobfox/features/compose/components/search.jsx b/app/javascript/flavours/blobfox/features/compose/components/search.jsx
new file mode 100644
index 00000000000000..76debfd7fb3b7c
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/components/search.jsx
@@ -0,0 +1,400 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, injectIntl, FormattedMessage, FormattedList } from 'react-intl';
+
+import classNames from 'classnames';
+import { withRouter } from 'react-router-dom';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
+import { Icon }  from 'flavours/blobfox/components/icon';
+import { domain, searchEnabled } from 'flavours/blobfox/initial_state';
+import { HASHTAG_REGEX } from 'flavours/blobfox/utils/hashtags';
+import { WithRouterPropTypes } from 'flavours/blobfox/utils/react_router';
+
+const messages = defineMessages({
+  placeholder: { id: 'search.placeholder', defaultMessage: 'Search' },
+  placeholderSignedIn: { id: 'search.search_or_paste', defaultMessage: 'Search or paste URL' },
+});
+
+const labelForRecentSearch = search => {
+  switch(search.get('type')) {
+  case 'account':
+    return `@${search.get('q')}`;
+  case 'hashtag':
+    return `#${search.get('q')}`;
+  default:
+    return search.get('q');
+  }
+};
+
+class Search extends PureComponent {
+
+  static contextTypes = {
+    identity: PropTypes.object.isRequired,
+  };
+
+  static propTypes = {
+    value: PropTypes.string.isRequired,
+    recent: ImmutablePropTypes.orderedSet,
+    submitted: PropTypes.bool,
+    onChange: PropTypes.func.isRequired,
+    onSubmit: PropTypes.func.isRequired,
+    onOpenURL: PropTypes.func.isRequired,
+    onClickSearchResult: PropTypes.func.isRequired,
+    onForgetSearchResult: PropTypes.func.isRequired,
+    onClear: PropTypes.func.isRequired,
+    onShow: PropTypes.func.isRequired,
+    openInRoute: PropTypes.bool,
+    intl: PropTypes.object.isRequired,
+    singleColumn: PropTypes.bool,
+    ...WithRouterPropTypes,
+  };
+
+  state = {
+    expanded: false,
+    selectedOption: -1,
+    options: [],
+  };
+
+  defaultOptions = [
+    { label: <><mark>has:</mark> <FormattedList type='disjunction' value={['media', 'poll', 'embed']} /></>, action: e => { e.preventDefault(); this._insertText('has:'); } },
+    { label: <><mark>is:</mark> <FormattedList type='disjunction' value={['reply', 'sensitive']} /></>, action: e => { e.preventDefault(); this._insertText('is:'); } },
+    { label: <><mark>language:</mark> <FormattedMessage id='search_popout.language_code' defaultMessage='ISO language code' /></>, action: e => { e.preventDefault(); this._insertText('language:'); } },
+    { label: <><mark>from:</mark> <FormattedMessage id='search_popout.user' defaultMessage='user' /></>, action: e => { e.preventDefault(); this._insertText('from:'); } },
+    { label: <><mark>before:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('before:'); } },
+    { label: <><mark>during:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('during:'); } },
+    { label: <><mark>after:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('after:'); } },
+    { label: <><mark>in:</mark> <FormattedList type='disjunction' value={['all', 'library', 'public']} /></>, action: e => { e.preventDefault(); this._insertText('in:'); } }
+  ];
+
+  setRef = c => {
+    this.searchForm = c;
+  };
+
+  handleChange = ({ target }) => {
+    const { onChange } = this.props;
+
+    onChange(target.value);
+
+    this._calculateOptions(target.value);
+  };
+
+  handleClear = e => {
+    const { value, submitted, onClear } = this.props;
+
+    e.preventDefault();
+
+    if (value.length > 0 || submitted) {
+      onClear();
+      this.setState({ options: [], selectedOption: -1 });
+    }
+  };
+
+  handleKeyDown = (e) => {
+    const { selectedOption } = this.state;
+    const options = searchEnabled ? this._getOptions().concat(this.defaultOptions) : this._getOptions();
+
+    switch(e.key) {
+    case 'Escape':
+      e.preventDefault();
+      this._unfocus();
+
+      break;
+    case 'ArrowDown':
+      e.preventDefault();
+
+      if (options.length > 0) {
+        this.setState({ selectedOption: Math.min(selectedOption + 1, options.length - 1) });
+      }
+
+      break;
+    case 'ArrowUp':
+      e.preventDefault();
+
+      if (options.length > 0) {
+        this.setState({ selectedOption: Math.max(selectedOption - 1, -1) });
+      }
+
+      break;
+    case 'Enter':
+      e.preventDefault();
+
+      if (selectedOption === -1) {
+        this._submit();
+      } else if (options.length > 0) {
+        options[selectedOption].action(e);
+      }
+
+      break;
+    case 'Delete':
+      if (selectedOption > -1 && options.length > 0) {
+        const search = options[selectedOption];
+
+        if (typeof search.forget === 'function') {
+          e.preventDefault();
+          search.forget(e);
+        }
+      }
+
+      break;
+    }
+  };
+
+  handleFocus = () => {
+    const { onShow, singleColumn } = this.props;
+
+    this.setState({ expanded: true, selectedOption: -1 });
+    onShow();
+
+    if (this.searchForm && !singleColumn) {
+      const { left, right } = this.searchForm.getBoundingClientRect();
+
+      if (left < 0 || right > (window.innerWidth || document.documentElement.clientWidth)) {
+        this.searchForm.scrollIntoView();
+      }
+    }
+  };
+
+  handleBlur = () => {
+    this.setState({ expanded: false, selectedOption: -1 });
+  };
+
+  handleHashtagClick = () => {
+    const { value, onClickSearchResult, history } = this.props;
+
+    const query = value.trim().replace(/^#/, '');
+
+    history.push(`/tags/${query}`);
+    onClickSearchResult(query, 'hashtag');
+    this._unfocus();
+  };
+
+  handleAccountClick = () => {
+    const { value, onClickSearchResult, history } = this.props;
+
+    const query = value.trim().replace(/^@/, '');
+
+    history.push(`/@${query}`);
+    onClickSearchResult(query, 'account');
+    this._unfocus();
+  };
+
+  handleURLClick = () => {
+    const { onOpenURL, history } = this.props;
+
+    onOpenURL(history);
+    this._unfocus();
+  };
+
+  handleStatusSearch = () => {
+    this._submit('statuses');
+  };
+
+  handleAccountSearch = () => {
+    this._submit('accounts');
+  };
+
+  handleRecentSearchClick = search => {
+    const { onChange, history } = this.props;
+
+    if (search.get('type') === 'account') {
+      history.push(`/@${search.get('q')}`);
+    } else if (search.get('type') === 'hashtag') {
+      history.push(`/tags/${search.get('q')}`);
+    } else {
+      onChange(search.get('q'));
+      this._submit(search.get('type'));
+    }
+
+    this._unfocus();
+  };
+
+  handleForgetRecentSearchClick = search => {
+    const { onForgetSearchResult } = this.props;
+
+    onForgetSearchResult(search.get('q'));
+  };
+
+  _unfocus () {
+    document.querySelector('.ui').parentElement.focus();
+  }
+
+  _insertText (text) {
+    const { value, onChange } = this.props;
+
+    if (value === '') {
+      onChange(text);
+    } else if (value[value.length - 1] === ' ') {
+      onChange(`${value}${text}`);
+    } else {
+      onChange(`${value} ${text}`);
+    }
+  }
+
+  _submit (type) {
+    const { onSubmit, openInRoute, value, onClickSearchResult, history } = this.props;
+
+    onSubmit(type);
+
+    if (value) {
+      onClickSearchResult(value, type);
+    }
+
+    if (openInRoute) {
+      history.push('/search');
+    }
+
+    this._unfocus();
+  }
+
+  _getOptions () {
+    const { options } = this.state;
+
+    if (options.length > 0) {
+      return options;
+    }
+
+    const { recent } = this.props;
+
+    return recent.toArray().map(search => ({
+      label: labelForRecentSearch(search),
+
+      action: () => this.handleRecentSearchClick(search),
+
+      forget: e => {
+        e.stopPropagation();
+        this.handleForgetRecentSearchClick(search);
+      },
+    }));
+  }
+
+  _calculateOptions (value) {
+    const { signedIn } = this.context.identity;
+    const trimmedValue = value.trim();
+    const options = [];
+
+    if (trimmedValue.length > 0) {
+      const couldBeURL = trimmedValue.startsWith('https://') && !trimmedValue.includes(' ');
+
+      if (couldBeURL) {
+        options.push({ key: 'open-url', label: <FormattedMessage id='search.quick_action.open_url' defaultMessage='Open URL in Mastodon' />, action: this.handleURLClick });
+      }
+
+      const couldBeHashtag = (trimmedValue.startsWith('#') && trimmedValue.length > 1) || trimmedValue.match(HASHTAG_REGEX);
+
+      if (couldBeHashtag) {
+        options.push({ key: 'go-to-hashtag', label: <FormattedMessage id='search.quick_action.go_to_hashtag' defaultMessage='Go to hashtag {x}' values={{ x: <mark>#{trimmedValue.replace(/^#/, '')}</mark> }} />, action: this.handleHashtagClick });
+      }
+
+      const couldBeUsername = trimmedValue.match(/^@?[a-z0-9_-]+(@[^\s]+)?$/i);
+
+      if (couldBeUsername) {
+        options.push({ key: 'go-to-account', label: <FormattedMessage id='search.quick_action.go_to_account' defaultMessage='Go to profile {x}' values={{ x: <mark>@{trimmedValue.replace(/^@/, '')}</mark> }} />, action: this.handleAccountClick });
+      }
+
+      const couldBeStatusSearch = searchEnabled;
+
+      if (couldBeStatusSearch && signedIn) {
+        options.push({ key: 'status-search', label: <FormattedMessage id='search.quick_action.status_search' defaultMessage='Posts matching {x}' values={{ x: <mark>{trimmedValue}</mark> }} />, action: this.handleStatusSearch });
+      }
+
+      const couldBeUserSearch = true;
+
+      if (couldBeUserSearch) {
+        options.push({ key: 'account-search', label: <FormattedMessage id='search.quick_action.account_search' defaultMessage='Profiles matching {x}' values={{ x: <mark>{trimmedValue}</mark> }} />, action: this.handleAccountSearch });
+      }
+    }
+
+    this.setState({ options });
+  }
+
+  render () {
+    const { intl, value, submitted, recent } = this.props;
+    const { expanded, options, selectedOption } = this.state;
+    const { signedIn } = this.context.identity;
+
+    const hasValue = value.length > 0 || submitted;
+
+    return (
+      <div className={classNames('search', { active: expanded })}>
+        <input
+          ref={this.setRef}
+          className='search__input'
+          type='text'
+          placeholder={intl.formatMessage(signedIn ? messages.placeholderSignedIn : messages.placeholder)}
+          aria-label={intl.formatMessage(signedIn ? messages.placeholderSignedIn : messages.placeholder)}
+          value={value || ''}
+          onChange={this.handleChange}
+          onKeyDown={this.handleKeyDown}
+          onFocus={this.handleFocus}
+          onBlur={this.handleBlur}
+        />
+
+        <div role='button' tabIndex={0} className='search__icon' onClick={this.handleClear}>
+          <Icon id='search' className={hasValue ? '' : 'active'} />
+          <Icon id='times-circle' className={hasValue ? 'active' : ''} />
+        </div>
+
+        <div className='search__popout'>
+          {options.length === 0 && (
+            <>
+              <h4><FormattedMessage id='search_popout.recent' defaultMessage='Recent searches' /></h4>
+
+              <div className='search__popout__menu'>
+                {recent.size > 0 ? this._getOptions().map(({ label, action, forget }, i) => (
+                  <button key={label} onMouseDown={action} className={classNames('search__popout__menu__item search__popout__menu__item--flex', { selected: selectedOption === i })}>
+                    <span>{label}</span>
+                    <button className='icon-button' onMouseDown={forget}><Icon id='times' /></button>
+                  </button>
+                )) : (
+                  <div className='search__popout__menu__message'>
+                    <FormattedMessage id='search.no_recent_searches' defaultMessage='No recent searches' />
+                  </div>
+                )}
+              </div>
+            </>
+          )}
+
+          {options.length > 0 && (
+            <>
+              <h4><FormattedMessage id='search_popout.quick_actions' defaultMessage='Quick actions' /></h4>
+
+              <div className='search__popout__menu'>
+                {options.map(({ key, label, action }, i) => (
+                  <button key={key} onMouseDown={action} className={classNames('search__popout__menu__item', { selected: selectedOption === i })}>
+                    {label}
+                  </button>
+                ))}
+              </div>
+            </>
+          )}
+
+          <h4><FormattedMessage id='search_popout.options' defaultMessage='Search options' /></h4>
+
+          {searchEnabled && signedIn ? (
+            <div className='search__popout__menu'>
+              {this.defaultOptions.map(({ key, label, action }, i) => (
+                <button key={key} onMouseDown={action} className={classNames('search__popout__menu__item', { selected: selectedOption === ((options.length || recent.size) + i) })}>
+                  {label}
+                </button>
+              ))}
+            </div>
+          ) : (
+            <div className='search__popout__menu__message'>
+              {searchEnabled ? (
+                <FormattedMessage id='search_popout.full_text_search_logged_out_message' defaultMessage='Only available when logged in.' />
+              ) : (
+                <FormattedMessage id='search_popout.full_text_search_disabled_message' defaultMessage='Not available on {domain}.' values={{ domain }} />
+              )}
+            </div>
+          )}
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default withRouter(injectIntl(Search));
diff --git a/app/javascript/flavours/blobfox/features/compose/components/search_results.jsx b/app/javascript/flavours/blobfox/features/compose/components/search_results.jsx
new file mode 100644
index 00000000000000..dd383dec41d9c5
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/components/search_results.jsx
@@ -0,0 +1,89 @@
+import PropTypes from 'prop-types';
+
+import { FormattedMessage } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import { Icon }  from 'flavours/blobfox/components/icon';
+import { LoadMore } from 'flavours/blobfox/components/load_more';
+import { SearchSection } from 'flavours/blobfox/features/explore/components/search_section';
+
+import { ImmutableHashtag as Hashtag } from '../../../components/hashtag';
+import AccountContainer from '../../../containers/account_container';
+import StatusContainer from '../../../containers/status_container';
+
+const INITIAL_PAGE_LIMIT = 10;
+
+const withoutLastResult = list => {
+  if (list.size > INITIAL_PAGE_LIMIT && list.size % INITIAL_PAGE_LIMIT === 1) {
+    return list.skipLast(1);
+  } else {
+    return list;
+  }
+};
+
+class SearchResults extends ImmutablePureComponent {
+
+  static propTypes = {
+    results: ImmutablePropTypes.map.isRequired,
+    expandSearch: PropTypes.func.isRequired,
+    searchTerm: PropTypes.string,
+  };
+
+  handleLoadMoreAccounts = () => this.props.expandSearch('accounts');
+
+  handleLoadMoreStatuses = () => this.props.expandSearch('statuses');
+
+  handleLoadMoreHashtags = () => this.props.expandSearch('hashtags');
+
+  render () {
+    const { results } = this.props;
+
+    let accounts, statuses, hashtags;
+
+    if (results.get('accounts') && results.get('accounts').size > 0) {
+      accounts = (
+        <SearchSection title={<><Icon id='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></>}>
+          {withoutLastResult(results.get('accounts')).map(accountId => <AccountContainer key={accountId} id={accountId} />)}
+          {(results.get('accounts').size > INITIAL_PAGE_LIMIT && results.get('accounts').size % INITIAL_PAGE_LIMIT === 1) && <LoadMore visible onClick={this.handleLoadMoreAccounts} />}
+        </SearchSection>
+      );
+    }
+
+    if (results.get('hashtags') && results.get('hashtags').size > 0) {
+      hashtags = (
+        <SearchSection title={<><Icon id='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></>}>
+          {withoutLastResult(results.get('hashtags')).map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
+          {(results.get('hashtags').size > INITIAL_PAGE_LIMIT && results.get('hashtags').size % INITIAL_PAGE_LIMIT === 1) && <LoadMore visible onClick={this.handleLoadMoreHashtags} />}
+        </SearchSection>
+      );
+    }
+
+    if (results.get('statuses') && results.get('statuses').size > 0) {
+      statuses = (
+        <SearchSection title={<><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></>}>
+          {withoutLastResult(results.get('statuses')).map(statusId => <StatusContainer key={statusId} id={statusId} />)}
+          {(results.get('statuses').size > INITIAL_PAGE_LIMIT && results.get('statuses').size % INITIAL_PAGE_LIMIT === 1) && <LoadMore visible onClick={this.handleLoadMoreStatuses} />}
+        </SearchSection>
+      );
+    }
+
+
+    return (
+      <div className='drawer--results'>
+        <header className='search-results__header'>
+          <Icon id='search' fixedWidth />
+          <FormattedMessage id='explore.search_results' defaultMessage='Search results' />
+        </header>
+
+        {accounts}
+        {hashtags}
+        {statuses}
+      </div>
+    );
+  }
+
+}
+
+export default SearchResults;
diff --git a/app/javascript/flavours/blobfox/features/compose/components/text_icon_button.jsx b/app/javascript/flavours/blobfox/features/compose/components/text_icon_button.jsx
new file mode 100644
index 00000000000000..166d022b88ac79
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/components/text_icon_button.jsx
@@ -0,0 +1,38 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+const iconStyle = {
+  height: null,
+  lineHeight: '27px',
+  minWidth: `${18 * 1.28571429}px`,
+};
+
+export default class TextIconButton extends PureComponent {
+
+  static propTypes = {
+    label: PropTypes.string.isRequired,
+    title: PropTypes.string,
+    active: PropTypes.bool,
+    onClick: PropTypes.func.isRequired,
+    ariaControls: PropTypes.string,
+  };
+
+  render () {
+    const { label, title, active, ariaControls } = this.props;
+
+    return (
+      <button
+        type='button'
+        title={title}
+        aria-label={title}
+        className={`text-icon-button ${active ? 'active' : ''}`}
+        aria-expanded={active}
+        onClick={this.props.onClick}
+        aria-controls={ariaControls} style={iconStyle}
+      >
+        {label}
+      </button>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/compose/components/textarea_icons.jsx b/app/javascript/flavours/blobfox/features/compose/components/textarea_icons.jsx
new file mode 100644
index 00000000000000..b54f1256956da6
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/components/textarea_icons.jsx
@@ -0,0 +1,61 @@
+//  Package imports.
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+//  Components.
+import { Icon } from 'flavours/blobfox/components/icon';
+//  Messages.
+const messages = defineMessages({
+  localOnly: {
+    defaultMessage: 'This post is local-only',
+    id: 'advanced_options.local-only.tooltip',
+  },
+  threadedMode: {
+    defaultMessage: 'Threaded mode enabled',
+    id: 'advanced_options.threaded_mode.tooltip',
+  },
+});
+
+//  We use an array of tuples here instead of an object because it
+//  preserves order.
+const iconMap = [
+  ['do_not_federate', 'home', messages.localOnly],
+  ['threaded_mode', 'comments', messages.threadedMode],
+];
+
+class TextareaIcons extends ImmutablePureComponent {
+
+  static propTypes = {
+    advancedOptions: ImmutablePropTypes.map,
+    intl: PropTypes.object.isRequired,
+  };
+
+  render () {
+    const { advancedOptions, intl } = this.props;
+    return (
+      <div className='compose-form__textarea-icons'>
+        {advancedOptions ? iconMap.map(
+          ([key, icon, message]) => advancedOptions.get(key) ? (
+            <span
+              className='textarea_icon'
+              key={key}
+              title={intl.formatMessage(message)}
+            >
+              <Icon
+                fixedWidth
+                id={icon}
+              />
+            </span>
+          ) : null,
+        ) : null}
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(TextareaIcons);
diff --git a/app/javascript/flavours/blobfox/features/compose/components/upload.jsx b/app/javascript/flavours/blobfox/features/compose/components/upload.jsx
new file mode 100644
index 00000000000000..5af24570baf806
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/components/upload.jsx
@@ -0,0 +1,66 @@
+import PropTypes from 'prop-types';
+
+import { FormattedMessage } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import spring from 'react-motion/lib/spring';
+
+import { Icon }  from 'flavours/blobfox/components/icon';
+
+import Motion from '../../ui/util/optional_motion';
+
+export default class Upload extends ImmutablePureComponent {
+
+  static propTypes = {
+    media: ImmutablePropTypes.map.isRequired,
+    onUndo: PropTypes.func.isRequired,
+    onOpenFocalPoint: PropTypes.func.isRequired,
+  };
+
+  handleUndoClick = e => {
+    e.stopPropagation();
+    this.props.onUndo(this.props.media.get('id'));
+  };
+
+  handleFocalPointClick = e => {
+    e.stopPropagation();
+    this.props.onOpenFocalPoint(this.props.media.get('id'));
+  };
+
+  render () {
+    const { media } = this.props;
+
+    if (!media) {
+      return null;
+    }
+
+    const focusX = media.getIn(['meta', 'focus', 'x']);
+    const focusY = media.getIn(['meta', 'focus', 'y']);
+    const x = ((focusX /  2) + .5) * 100;
+    const y = ((focusY / -2) + .5) * 100;
+
+    return (
+      <div className='compose-form__upload'>
+        <Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}>
+          {({ scale }) => (
+            <div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}>
+              <div className='compose-form__upload__actions'>
+                <button type='button' className='icon-button' onClick={this.handleUndoClick}><Icon id='times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button>
+                <button type='button' className='icon-button' onClick={this.handleFocalPointClick}><Icon id='pencil' /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>
+              </div>
+
+              {(media.get('description') || '').length === 0 && (
+                <div className='compose-form__upload__warning'>
+                  <button type='button' className='icon-button' onClick={this.handleFocalPointClick}><Icon id='info-circle' /> <FormattedMessage id='upload_form.description_missing' defaultMessage='No description added' /></button>
+                </div>
+              )}
+            </div>
+          )}
+        </Motion>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/compose/components/upload_form.jsx b/app/javascript/flavours/blobfox/features/compose/components/upload_form.jsx
new file mode 100644
index 00000000000000..cf2e53ad905f0d
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/components/upload_form.jsx
@@ -0,0 +1,32 @@
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import SensitiveButtonContainer from '../containers/sensitive_button_container';
+import UploadContainer from '../containers/upload_container';
+import UploadProgressContainer from '../containers/upload_progress_container';
+
+export default class UploadForm extends ImmutablePureComponent {
+
+  static propTypes = {
+    mediaIds: ImmutablePropTypes.list.isRequired,
+  };
+
+  render () {
+    const { mediaIds } = this.props;
+
+    return (
+      <div className='compose-form__upload-wrapper'>
+        <UploadProgressContainer />
+
+        <div className='compose-form__uploads-wrapper'>
+          {mediaIds.map(id => (
+            <UploadContainer id={id} key={id} />
+          ))}
+        </div>
+
+        {!mediaIds.isEmpty() && <SensitiveButtonContainer />}
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/compose/components/upload_progress.jsx b/app/javascript/flavours/blobfox/features/compose/components/upload_progress.jsx
new file mode 100644
index 00000000000000..906093d7821533
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/components/upload_progress.jsx
@@ -0,0 +1,56 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import spring from 'react-motion/lib/spring';
+
+import { Icon }  from 'flavours/blobfox/components/icon';
+
+import Motion from '../../ui/util/optional_motion';
+
+export default class UploadProgress extends PureComponent {
+
+  static propTypes = {
+    active: PropTypes.bool,
+    progress: PropTypes.number,
+    isProcessing: PropTypes.bool,
+  };
+
+  render () {
+    const { active, progress, isProcessing } = this.props;
+
+    if (!active) {
+      return null;
+    }
+
+    let message;
+
+    if (isProcessing) {
+      message = <FormattedMessage id='upload_progress.processing' defaultMessage='Processing…' />;
+    } else {
+      message = <FormattedMessage id='upload_progress.label' defaultMessage='Uploading…' />;
+    }
+
+    return (
+      <div className='upload-progress'>
+        <div className='upload-progress__icon'>
+          <Icon id='upload' />
+        </div>
+
+        <div className='upload-progress__message'>
+          {message}
+
+          <div className='upload-progress__backdrop'>
+            <Motion defaultStyle={{ width: 0 }} style={{ width: spring(progress) }}>
+              {({ width }) =>
+                <div className='upload-progress__tracker' style={{ width: `${width}%` }} />
+              }
+            </Motion>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/compose/components/warning.jsx b/app/javascript/flavours/blobfox/features/compose/components/warning.jsx
new file mode 100644
index 00000000000000..c5babc30a5a1ad
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/components/warning.jsx
@@ -0,0 +1,28 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import spring from 'react-motion/lib/spring';
+
+import Motion from '../../ui/util/optional_motion';
+
+export default class Warning extends PureComponent {
+
+  static propTypes = {
+    message: PropTypes.node.isRequired,
+  };
+
+  render () {
+    const { message } = this.props;
+
+    return (
+      <Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
+        {({ opacity, scaleX, scaleY }) => (
+          <div className='compose-form__warning' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}>
+            {message}
+          </div>
+        )}
+      </Motion>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/compose/containers/autosuggest_account_container.js b/app/javascript/flavours/blobfox/features/compose/containers/autosuggest_account_container.js
new file mode 100644
index 00000000000000..f86f01bd97e38c
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/containers/autosuggest_account_container.js
@@ -0,0 +1,16 @@
+import { connect } from 'react-redux';
+
+import { makeGetAccount } from '../../../selectors';
+import AutosuggestAccount from '../components/autosuggest_account';
+
+const makeMapStateToProps = () => {
+  const getAccount = makeGetAccount();
+
+  const mapStateToProps = (state, { id }) => ({
+    account: getAccount(state, id),
+  });
+
+  return mapStateToProps;
+};
+
+export default connect(makeMapStateToProps)(AutosuggestAccount);
diff --git a/app/javascript/flavours/blobfox/features/compose/containers/compose_form_container.js b/app/javascript/flavours/blobfox/features/compose/containers/compose_form_container.js
new file mode 100644
index 00000000000000..e2713a22138661
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/containers/compose_form_container.js
@@ -0,0 +1,150 @@
+import { defineMessages, injectIntl } from 'react-intl';
+
+import { connect } from 'react-redux';
+
+import { privacyPreference } from 'flavours/blobfox/utils/privacy_preference';
+
+import {
+  changeCompose,
+  submitCompose,
+  clearComposeSuggestions,
+  fetchComposeSuggestions,
+  selectComposeSuggestion,
+  changeComposeSpoilerText,
+  changeComposeSpoilerness,
+  changeComposeVisibility,
+  insertEmojiCompose,
+  uploadCompose,
+} from '../../../actions/compose';
+import { changeLocalSetting } from '../../../actions/local_settings';
+import {
+  openModal,
+} from '../../../actions/modal';
+import ComposeForm from '../components/compose_form';
+
+const messages = defineMessages({
+  missingDescriptionMessage: {
+    id: 'confirmations.missing_media_description.message',
+    defaultMessage: 'At least one media attachment is lacking a description. Consider describing all media attachments for the visually impaired before sending your toot.',
+  },
+  missingDescriptionConfirm: {
+    id: 'confirmations.missing_media_description.confirm',
+    defaultMessage: 'Send anyway',
+  },
+  missingDescriptionEdit: {
+    id: 'confirmations.missing_media_description.edit',
+    defaultMessage: 'Edit media',
+  },
+});
+
+const sideArmPrivacy = state => {
+  const inReplyTo = state.getIn(['compose', 'in_reply_to']);
+  const replyPrivacy = inReplyTo ? state.getIn(['statuses', inReplyTo, 'visibility']) : null;
+  const sideArmBasePrivacy = state.getIn(['local_settings', 'side_arm']);
+  const sideArmRestrictedPrivacy = replyPrivacy ? privacyPreference(replyPrivacy, sideArmBasePrivacy) : null;
+  let sideArmPrivacy = null;
+  switch (state.getIn(['local_settings', 'side_arm_reply_mode'])) {
+  case 'copy':
+    sideArmPrivacy = replyPrivacy;
+    break;
+  case 'restrict':
+    sideArmPrivacy = sideArmRestrictedPrivacy;
+    break;
+  }
+  return sideArmPrivacy || sideArmBasePrivacy;
+};
+
+const mapStateToProps = state => ({
+  text: state.getIn(['compose', 'text']),
+  suggestions: state.getIn(['compose', 'suggestions']),
+  spoiler: state.getIn(['local_settings', 'always_show_spoilers_field']) || state.getIn(['compose', 'spoiler']),
+  spoilerText: state.getIn(['compose', 'spoiler_text']),
+  privacy: state.getIn(['compose', 'privacy']),
+  focusDate: state.getIn(['compose', 'focusDate']),
+  caretPosition: state.getIn(['compose', 'caretPosition']),
+  preselectDate: state.getIn(['compose', 'preselectDate']),
+  isSubmitting: state.getIn(['compose', 'is_submitting']),
+  isEditing: state.getIn(['compose', 'id']) !== null,
+  isChangingUpload: state.getIn(['compose', 'is_changing_upload']),
+  isUploading: state.getIn(['compose', 'is_uploading']),
+  anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
+  isInReply: state.getIn(['compose', 'in_reply_to']) !== null,
+  lang: state.getIn(['compose', 'language']),
+  advancedOptions: state.getIn(['compose', 'advanced_options']),
+  layout: state.getIn(['local_settings', 'layout']),
+  media: state.getIn(['compose', 'media_attachments']),
+  sideArm: sideArmPrivacy(state),
+  sensitive: state.getIn(['compose', 'sensitive']),
+  showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
+  spoilersAlwaysOn: state.getIn(['local_settings', 'always_show_spoilers_field']),
+  mediaDescriptionConfirmation: state.getIn(['local_settings', 'confirm_missing_media_description']),
+  preselectOnReply: state.getIn(['local_settings', 'preselect_on_reply']),
+});
+
+const mapDispatchToProps = (dispatch, { intl }) => ({
+
+  onChange (text) {
+    dispatch(changeCompose(text));
+  },
+
+  onSubmit (router) {
+    dispatch(submitCompose(router));
+  },
+
+  onClearSuggestions () {
+    dispatch(clearComposeSuggestions());
+  },
+
+  onFetchSuggestions (token) {
+    dispatch(fetchComposeSuggestions(token));
+  },
+
+  onSuggestionSelected (position, token, suggestion, path) {
+    dispatch(selectComposeSuggestion(position, token, suggestion, path));
+  },
+
+  onChangeSpoilerText (text) {
+    dispatch(changeComposeSpoilerText(text));
+  },
+
+  onPaste (files) {
+    dispatch(uploadCompose(files));
+  },
+
+  onPickEmoji (position, emoji) {
+    dispatch(insertEmojiCompose(position, emoji));
+  },
+
+  onChangeSpoilerness() {
+    dispatch(changeComposeSpoilerness());
+  },
+
+  onChangeVisibility(value) {
+    dispatch(changeComposeVisibility(value));
+  },
+
+  onMediaDescriptionConfirm(routerHistory, mediaId, overriddenVisibility = null) {
+    dispatch(openModal({
+      modalType: 'CONFIRM',
+      modalProps: {
+        message: intl.formatMessage(messages.missingDescriptionMessage),
+        confirm: intl.formatMessage(messages.missingDescriptionConfirm),
+        onConfirm: () => {
+          if (overriddenVisibility) {
+            dispatch(changeComposeVisibility(overriddenVisibility));
+          }
+          dispatch(submitCompose(routerHistory));
+        },
+        secondary: intl.formatMessage(messages.missingDescriptionEdit),
+        onSecondary: () => dispatch(openModal({
+          modalType: 'FOCAL_POINT',
+          modalProps: { id: mediaId },
+        })),
+        onDoNotAsk: () => dispatch(changeLocalSetting(['confirm_missing_media_description'], false)),
+      },
+    }));
+  },
+
+});
+
+export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ComposeForm));
diff --git a/app/javascript/flavours/blobfox/features/compose/containers/dropdown_container.js b/app/javascript/flavours/blobfox/features/compose/containers/dropdown_container.js
new file mode 100644
index 00000000000000..25d60d9328c8cb
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/containers/dropdown_container.js
@@ -0,0 +1,14 @@
+import { connect } from 'react-redux';
+
+import { openModal, closeModal } from 'flavours/blobfox/actions/modal';
+import { isUserTouching } from 'flavours/blobfox/is_mobile';
+
+import Dropdown from '../components/dropdown';
+
+const mapDispatchToProps = dispatch => ({
+  isUserTouching,
+  onModalOpen: props => dispatch(openModal({ modalType: 'ACTIONS', modalProps: props })),
+  onModalClose: () => dispatch(closeModal({ modalType: undefined, ignoreFocus: false })),
+});
+
+export default connect(null, mapDispatchToProps)(Dropdown);
diff --git a/app/javascript/flavours/blobfox/features/compose/containers/emoji_picker_dropdown_container.js b/app/javascript/flavours/blobfox/features/compose/containers/emoji_picker_dropdown_container.js
new file mode 100644
index 00000000000000..a0e50029dfa303
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/containers/emoji_picker_dropdown_container.js
@@ -0,0 +1,85 @@
+import { Map as ImmutableMap } from 'immutable';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+
+import { useEmoji } from '../../../actions/emojis';
+import { changeSetting } from '../../../actions/settings';
+import EmojiPickerDropdown from '../components/emoji_picker_dropdown';
+
+const perLine = 8;
+const lines   = 2;
+
+const DEFAULTS = [
+  '+1',
+  'grinning',
+  'kissing_heart',
+  'heart_eyes',
+  'laughing',
+  'stuck_out_tongue_winking_eye',
+  'sweat_smile',
+  'joy',
+  'yum',
+  'disappointed',
+  'thinking_face',
+  'weary',
+  'sob',
+  'sunglasses',
+  'heart',
+  'ok_hand',
+];
+
+const getFrequentlyUsedEmojis = createSelector([
+  state => state.getIn(['settings', 'frequentlyUsedEmojis'], ImmutableMap()),
+], emojiCounters => {
+  let emojis = emojiCounters
+    .keySeq()
+    .sort((a, b) => emojiCounters.get(a) - emojiCounters.get(b))
+    .reverse()
+    .slice(0, perLine * lines)
+    .toArray();
+
+  if (emojis.length < DEFAULTS.length) {
+    let uniqueDefaults = DEFAULTS.filter(emoji => !emojis.includes(emoji));
+    emojis = emojis.concat(uniqueDefaults.slice(0, DEFAULTS.length - emojis.length));
+  }
+
+  return emojis;
+});
+
+const getCustomEmojis = createSelector([
+  state => state.get('custom_emojis'),
+], emojis => emojis.filter(e => e.get('visible_in_picker')).sort((a, b) => {
+  const aShort = a.get('shortcode').toLowerCase();
+  const bShort = b.get('shortcode').toLowerCase();
+
+  if (aShort < bShort) {
+    return -1;
+  } else if (aShort > bShort ) {
+    return 1;
+  } else {
+    return 0;
+  }
+}));
+
+const mapStateToProps = state => ({
+  custom_emojis: getCustomEmojis(state),
+  skinTone: state.getIn(['settings', 'skinTone']),
+  frequentlyUsedEmojis: getFrequentlyUsedEmojis(state),
+});
+
+const mapDispatchToProps = (dispatch, { onPickEmoji }) => ({
+  onSkinTone: skinTone => {
+    dispatch(changeSetting(['skinTone'], skinTone));
+  },
+
+  onPickEmoji: emoji => {
+    // eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a react hook
+    dispatch(useEmoji(emoji));
+
+    if (onPickEmoji) {
+      onPickEmoji(emoji);
+    }
+  },
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(EmojiPickerDropdown);
diff --git a/app/javascript/flavours/blobfox/features/compose/containers/header_container.js b/app/javascript/flavours/blobfox/features/compose/containers/header_container.js
new file mode 100644
index 00000000000000..b7931ffa895d89
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/containers/header_container.js
@@ -0,0 +1,42 @@
+import { defineMessages, injectIntl } from 'react-intl';
+
+import { connect }   from 'react-redux';
+
+import { openModal } from 'flavours/blobfox/actions/modal';
+import { logOut } from 'flavours/blobfox/utils/log_out';
+
+import Header from '../components/header';
+
+const messages = defineMessages({
+  logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
+  logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
+});
+
+const mapStateToProps = state => {
+  return {
+    columns: state.getIn(['settings', 'columns']),
+    unreadNotifications: state.getIn(['notifications', 'unread']),
+    showNotificationsBadge: state.getIn(['local_settings', 'notifications', 'tab_badge']),
+  };
+};
+
+const mapDispatchToProps = (dispatch, { intl }) => ({
+  onSettingsClick (e) {
+    e.preventDefault();
+    e.stopPropagation();
+    dispatch(openModal({ modalType: 'SETTINGS', modalProps: {} }));
+  },
+  onLogout () {
+    dispatch(openModal({
+      modalType: 'CONFIRM',
+      modalProps: {
+        message: intl.formatMessage(messages.logoutMessage),
+        confirm: intl.formatMessage(messages.logoutConfirm),
+        closeWhenConfirm: false,
+        onConfirm: () => logOut(),
+      },
+    }));
+  },
+});
+
+export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(Header));
diff --git a/app/javascript/flavours/blobfox/features/compose/containers/language_dropdown_container.js b/app/javascript/flavours/blobfox/features/compose/containers/language_dropdown_container.js
new file mode 100644
index 00000000000000..7344097e229a2c
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/containers/language_dropdown_container.js
@@ -0,0 +1,37 @@
+import { Map as ImmutableMap } from 'immutable';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+
+import { changeComposeLanguage } from 'flavours/blobfox/actions/compose';
+import { useLanguage } from 'flavours/blobfox/actions/languages';
+
+import LanguageDropdown from '../components/language_dropdown';
+
+const getFrequentlyUsedLanguages = createSelector([
+  state => state.getIn(['settings', 'frequentlyUsedLanguages'], ImmutableMap()),
+], languageCounters => (
+  languageCounters.keySeq()
+    .sort((a, b) => languageCounters.get(a) - languageCounters.get(b))
+    .reverse()
+    .toArray()
+));
+
+const mapStateToProps = state => ({
+  frequentlyUsedLanguages: getFrequentlyUsedLanguages(state),
+  value: state.getIn(['compose', 'language']),
+});
+
+const mapDispatchToProps = dispatch => ({
+
+  onChange (value) {
+    dispatch(changeComposeLanguage(value));
+  },
+
+  onClose (value) {
+    // eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a react hook
+    dispatch(useLanguage(value));
+  },
+
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(LanguageDropdown);
diff --git a/app/javascript/flavours/blobfox/features/compose/containers/navigation_container.js b/app/javascript/flavours/blobfox/features/compose/containers/navigation_container.js
new file mode 100644
index 00000000000000..8b8d2f57a2a7b8
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/containers/navigation_container.js
@@ -0,0 +1,36 @@
+import { defineMessages, injectIntl } from 'react-intl';
+
+import { connect }   from 'react-redux';
+
+import { openModal } from 'flavours/blobfox/actions/modal';
+import { logOut } from 'flavours/blobfox/utils/log_out';
+
+import { me } from '../../../initial_state';
+import NavigationBar from '../components/navigation_bar';
+
+const messages = defineMessages({
+  logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
+  logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
+});
+
+const mapStateToProps = state => {
+  return {
+    account: state.getIn(['accounts', me]),
+  };
+};
+
+const mapDispatchToProps = (dispatch, { intl }) => ({
+  onLogout () {
+    dispatch(openModal({
+      modalType: 'CONFIRM',
+      modalProps: {
+        message: intl.formatMessage(messages.logoutMessage),
+        confirm: intl.formatMessage(messages.logoutConfirm),
+        closeWhenConfirm: false,
+        onConfirm: () => logOut(),
+      },
+    }));
+  },
+});
+
+export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NavigationBar));
diff --git a/app/javascript/flavours/blobfox/features/compose/containers/options_container.js b/app/javascript/flavours/blobfox/features/compose/containers/options_container.js
new file mode 100644
index 00000000000000..12ded500f4c34c
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/containers/options_container.js
@@ -0,0 +1,56 @@
+import { connect } from 'react-redux';
+
+import {
+  changeComposeAdvancedOption,
+  changeComposeContentType,
+  addPoll,
+  removePoll,
+} from 'flavours/blobfox/actions/compose';
+import { openModal } from 'flavours/blobfox/actions/modal';
+
+import Options from '../components/options';
+
+function mapStateToProps (state) {
+  const poll = state.getIn(['compose', 'poll']);
+  const media = state.getIn(['compose', 'media_attachments']);
+  const pending_media = state.getIn(['compose', 'pending_media_attachments']);
+  return {
+    acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']).toArray().join(','),
+    resetFileKey: state.getIn(['compose', 'resetFileKey']),
+    hasPoll: !!poll,
+    allowMedia: !poll && (media ? media.size + pending_media < 4 && !media.some(item => ['video', 'audio'].includes(item.get('type'))) : pending_media < 4),
+    allowPoll: !(media && !!media.size),
+    showContentTypeChoice: state.getIn(['local_settings', 'show_content_type_choice']),
+    contentType: state.getIn(['compose', 'content_type']),
+  };
+}
+
+const mapDispatchToProps = (dispatch) => ({
+
+  onChangeAdvancedOption(option, value) {
+    dispatch(changeComposeAdvancedOption(option, value));
+  },
+
+  onChangeContentType(value) {
+    dispatch(changeComposeContentType(value));
+  },
+
+  onTogglePoll() {
+    dispatch((_, getState) => {
+      if (getState().getIn(['compose', 'poll'])) {
+        dispatch(removePoll());
+      } else {
+        dispatch(addPoll());
+      }
+    });
+  },
+
+  onDoodleOpen() {
+    dispatch(openModal({
+      modalType: 'DOODLE',
+      modalProps: { noEsc: true, noClose: true },
+    }));
+  },
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(Options);
diff --git a/app/javascript/flavours/blobfox/features/compose/containers/poll_form_container.js b/app/javascript/flavours/blobfox/features/compose/containers/poll_form_container.js
new file mode 100644
index 00000000000000..177ffcea6acb4b
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/containers/poll_form_container.js
@@ -0,0 +1,53 @@
+import { connect } from 'react-redux';
+
+import {
+  addPollOption,
+  removePollOption,
+  changePollOption,
+  changePollSettings,
+  clearComposeSuggestions,
+  fetchComposeSuggestions,
+  selectComposeSuggestion,
+} from '../../../actions/compose';
+import PollForm from '../components/poll_form';
+
+const mapStateToProps = state => ({
+  suggestions: state.getIn(['compose', 'suggestions']),
+  options: state.getIn(['compose', 'poll', 'options']),
+  lang: state.getIn(['compose', 'language']),
+  expiresIn: state.getIn(['compose', 'poll', 'expires_in']),
+  isMultiple: state.getIn(['compose', 'poll', 'multiple']),
+});
+
+const mapDispatchToProps = dispatch => ({
+  onAddOption(title) {
+    dispatch(addPollOption(title));
+  },
+
+  onRemoveOption(index) {
+    dispatch(removePollOption(index));
+  },
+
+  onChangeOption(index, title) {
+    dispatch(changePollOption(index, title));
+  },
+
+  onChangeSettings(expiresIn, isMultiple) {
+    dispatch(changePollSettings(expiresIn, isMultiple));
+  },
+
+  onClearSuggestions () {
+    dispatch(clearComposeSuggestions());
+  },
+
+  onFetchSuggestions (token) {
+    dispatch(fetchComposeSuggestions(token));
+  },
+
+  onSuggestionSelected (position, token, accountId, path) {
+    dispatch(selectComposeSuggestion(position, token, accountId, path));
+  },
+
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(PollForm);
diff --git a/app/javascript/flavours/blobfox/features/compose/containers/privacy_dropdown_container.js b/app/javascript/flavours/blobfox/features/compose/containers/privacy_dropdown_container.js
new file mode 100644
index 00000000000000..6d26abf4f6623b
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/containers/privacy_dropdown_container.js
@@ -0,0 +1,30 @@
+import { connect } from 'react-redux';
+
+import { changeComposeVisibility } from '../../../actions/compose';
+import { openModal, closeModal } from '../../../actions/modal';
+import { isUserTouching } from '../../../is_mobile';
+import PrivacyDropdown from '../components/privacy_dropdown';
+
+const mapStateToProps = state => ({
+  value: state.getIn(['compose', 'privacy']),
+});
+
+const mapDispatchToProps = dispatch => ({
+
+  onChange (value) {
+    dispatch(changeComposeVisibility(value));
+  },
+
+  isUserTouching,
+  onModalOpen: props => dispatch(openModal({
+    modalType: 'ACTIONS',
+    modalProps: props,
+  })),
+  onModalClose: () => dispatch(closeModal({
+    modalType: undefined,
+    ignoreFocus: false,
+  })),
+
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(PrivacyDropdown);
diff --git a/app/javascript/flavours/blobfox/features/compose/containers/reply_indicator_container.js b/app/javascript/flavours/blobfox/features/compose/containers/reply_indicator_container.js
new file mode 100644
index 00000000000000..678124b2a8d93c
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/containers/reply_indicator_container.js
@@ -0,0 +1,33 @@
+import { connect } from 'react-redux';
+
+import { cancelReplyCompose } from '../../../actions/compose';
+import ReplyIndicator from '../components/reply_indicator';
+
+const makeMapStateToProps = () => {
+  const mapStateToProps = state => {
+    let statusId = state.getIn(['compose', 'id'], null);
+    let editing  = true;
+
+    if (statusId === null) {
+      statusId = state.getIn(['compose', 'in_reply_to']);
+      editing  = false;
+    }
+
+    return {
+      status: state.getIn(['statuses', statusId]),
+      editing,
+    };
+  };
+
+  return mapStateToProps;
+};
+
+const mapDispatchToProps = dispatch => ({
+
+  onCancel () {
+    dispatch(cancelReplyCompose());
+  },
+
+});
+
+export default connect(makeMapStateToProps, mapDispatchToProps)(ReplyIndicator);
diff --git a/app/javascript/flavours/blobfox/features/compose/containers/search_container.js b/app/javascript/flavours/blobfox/features/compose/containers/search_container.js
new file mode 100644
index 00000000000000..a0217e3bd0febc
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/containers/search_container.js
@@ -0,0 +1,53 @@
+import { connect } from 'react-redux';
+
+import {
+  changeSearch,
+  clearSearch,
+  submitSearch,
+  showSearch,
+  openURL,
+  clickSearchResult,
+  forgetSearchResult,
+} from 'flavours/blobfox/actions/search';
+
+import Search from '../components/search';
+
+const mapStateToProps = state => ({
+  value: state.getIn(['search', 'value']),
+  submitted: state.getIn(['search', 'submitted']),
+  recent: state.getIn(['search', 'recent']).reverse(),
+});
+
+const mapDispatchToProps = dispatch => ({
+
+  onChange (value) {
+    dispatch(changeSearch(value));
+  },
+
+  onClear () {
+    dispatch(clearSearch());
+  },
+
+  onSubmit (type) {
+    dispatch(submitSearch(type));
+  },
+
+  onShow () {
+    dispatch(showSearch());
+  },
+
+  onOpenURL (routerHistory) {
+    dispatch(openURL(routerHistory));
+  },
+
+  onClickSearchResult (q, type) {
+    dispatch(clickSearchResult(q, type));
+  },
+
+  onForgetSearchResult (q) {
+    dispatch(forgetSearchResult(q));
+  },
+
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(Search);
diff --git a/app/javascript/flavours/blobfox/features/compose/containers/search_results_container.js b/app/javascript/flavours/blobfox/features/compose/containers/search_results_container.js
new file mode 100644
index 00000000000000..3906b08a0190f4
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/containers/search_results_container.js
@@ -0,0 +1,20 @@
+import { connect } from 'react-redux';
+
+import { expandSearch } from 'flavours/blobfox/actions/search';
+import { fetchSuggestions, dismissSuggestion } from 'flavours/blobfox/actions/suggestions';
+
+import SearchResults from '../components/search_results';
+
+const mapStateToProps = state => ({
+  results: state.getIn(['search', 'results']),
+  suggestions: state.getIn(['suggestions', 'items']),
+  searchTerm: state.getIn(['search', 'searchTerm']),
+});
+
+const mapDispatchToProps = dispatch => ({
+  fetchSuggestions: () => dispatch(fetchSuggestions()),
+  expandSearch: type => dispatch(expandSearch(type)),
+  dismissSuggestion: account => dispatch(dismissSuggestion(account.get('id'))),
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(SearchResults);
diff --git a/app/javascript/flavours/blobfox/features/compose/containers/sensitive_button_container.jsx b/app/javascript/flavours/blobfox/features/compose/containers/sensitive_button_container.jsx
new file mode 100644
index 00000000000000..96bcdc9bad7aa9
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/containers/sensitive_button_container.jsx
@@ -0,0 +1,77 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
+
+import classNames from 'classnames';
+
+import { connect } from 'react-redux';
+
+import { changeComposeSensitivity } from 'flavours/blobfox/actions/compose';
+
+const messages = defineMessages({
+  marked: {
+    id: 'compose_form.sensitive.marked',
+    defaultMessage: '{count, plural, one {Media is marked as sensitive} other {Media is marked as sensitive}}',
+  },
+  unmarked: {
+    id: 'compose_form.sensitive.unmarked',
+    defaultMessage: '{count, plural, one {Media is not marked as sensitive} other {Media is not marked as sensitive}}',
+  },
+});
+
+const mapStateToProps = state => {
+  const spoilersAlwaysOn = state.getIn(['local_settings', 'always_show_spoilers_field']);
+  const spoilerText = state.getIn(['compose', 'spoiler_text']);
+  return {
+    active: state.getIn(['compose', 'sensitive']) || (spoilersAlwaysOn && spoilerText && spoilerText.length > 0),
+    disabled: state.getIn(['compose', 'spoiler']),
+    mediaCount: state.getIn(['compose', 'media_attachments']).size,
+  };
+};
+
+const mapDispatchToProps = dispatch => ({
+
+  onClick () {
+    dispatch(changeComposeSensitivity());
+  },
+
+});
+
+class SensitiveButton extends PureComponent {
+
+  static propTypes = {
+    active: PropTypes.bool,
+    disabled: PropTypes.bool,
+    mediaCount: PropTypes.number,
+    onClick: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  render () {
+    const { active, disabled, mediaCount, onClick, intl } = this.props;
+
+    return (
+      <div className='compose-form__sensitive-button'>
+        <label className={classNames('icon-button', { active })} title={intl.formatMessage(active ? messages.marked : messages.unmarked, { count: mediaCount })}>
+          <input
+            name='mark-sensitive'
+            type='checkbox'
+            checked={active}
+            onChange={onClick}
+            disabled={disabled}
+          />
+
+          <FormattedMessage
+            id='compose_form.sensitive.hide'
+            defaultMessage='{count, plural, one {Mark media as sensitive} other {Mark media as sensitive}}'
+            values={{ count: mediaCount }}
+          />
+        </label>
+      </div>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(SensitiveButton));
diff --git a/app/javascript/flavours/blobfox/features/compose/containers/upload_container.js b/app/javascript/flavours/blobfox/features/compose/containers/upload_container.js
new file mode 100644
index 00000000000000..77bb90db87b91a
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/containers/upload_container.js
@@ -0,0 +1,26 @@
+import { connect } from 'react-redux';
+
+import { undoUploadCompose, initMediaEditModal, submitCompose } from '../../../actions/compose';
+import Upload from '../components/upload';
+
+const mapStateToProps = (state, { id }) => ({
+  media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
+});
+
+const mapDispatchToProps = dispatch => ({
+
+  onUndo: id => {
+    dispatch(undoUploadCompose(id));
+  },
+
+  onOpenFocalPoint: id => {
+    dispatch(initMediaEditModal(id));
+  },
+
+  onSubmit (router) {
+    dispatch(submitCompose(router));
+  },
+
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(Upload);
diff --git a/app/javascript/flavours/blobfox/features/compose/containers/upload_form_container.js b/app/javascript/flavours/blobfox/features/compose/containers/upload_form_container.js
new file mode 100644
index 00000000000000..336525cf5399d6
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/containers/upload_form_container.js
@@ -0,0 +1,9 @@
+import { connect } from 'react-redux';
+
+import UploadForm from '../components/upload_form';
+
+const mapStateToProps = state => ({
+  mediaIds: state.getIn(['compose', 'media_attachments']).map(item => item.get('id')),
+});
+
+export default connect(mapStateToProps)(UploadForm);
diff --git a/app/javascript/flavours/blobfox/features/compose/containers/upload_progress_container.js b/app/javascript/flavours/blobfox/features/compose/containers/upload_progress_container.js
new file mode 100644
index 00000000000000..ffff321c3fcdf1
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/containers/upload_progress_container.js
@@ -0,0 +1,11 @@
+import { connect } from 'react-redux';
+
+import UploadProgress from '../components/upload_progress';
+
+const mapStateToProps = state => ({
+  active: state.getIn(['compose', 'is_uploading']),
+  progress: state.getIn(['compose', 'progress']),
+  isProcessing: state.getIn(['compose', 'is_processing']),
+});
+
+export default connect(mapStateToProps)(UploadProgress);
diff --git a/app/javascript/flavours/blobfox/features/compose/containers/warning_container.jsx b/app/javascript/flavours/blobfox/features/compose/containers/warning_container.jsx
new file mode 100644
index 00000000000000..4908e0634e2bc0
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/containers/warning_container.jsx
@@ -0,0 +1,47 @@
+import PropTypes from 'prop-types';
+
+import { FormattedMessage } from 'react-intl';
+
+import { connect } from 'react-redux';
+
+import { me } from 'flavours/blobfox/initial_state';
+import { profileLink, privacyPolicyLink } from 'flavours/blobfox/utils/backend_links';
+import { HASHTAG_PATTERN_REGEX } from 'flavours/blobfox/utils/hashtags';
+
+import Warning from '../components/warning';
+
+const mapStateToProps = state => ({
+  needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']),
+  hashtagWarning: state.getIn(['compose', 'privacy']) !== 'public' && HASHTAG_PATTERN_REGEX.test(state.getIn(['compose', 'text'])),
+  directMessageWarning: state.getIn(['compose', 'privacy']) === 'direct',
+});
+
+const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning }) => {
+  if (needsLockWarning) {
+    return <Warning message={<FormattedMessage id='compose_form.lock_disclaimer' defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.' values={{ locked: <a href={profileLink}><FormattedMessage id='compose_form.lock_disclaimer.lock' defaultMessage='locked' /></a> }} />} />;
+  }
+
+  if (hashtagWarning) {
+    return <Warning message={<FormattedMessage id='compose_form.hashtag_warning' defaultMessage="This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag." />} />;
+  }
+
+  if (directMessageWarning) {
+    const message = (
+      <span>
+        <FormattedMessage id='compose_form.encryption_warning' defaultMessage='Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.' /> {!!privacyPolicyLink && <a href={privacyPolicyLink} target='_blank'><FormattedMessage id='compose_form.direct_message_warning_learn_more' defaultMessage='Learn more' /></a>}
+      </span>
+    );
+
+    return <Warning message={message} />;
+  }
+
+  return null;
+};
+
+WarningWrapper.propTypes = {
+  needsLockWarning: PropTypes.bool,
+  hashtagWarning: PropTypes.bool,
+  directMessageWarning: PropTypes.bool,
+};
+
+export default connect(mapStateToProps)(WarningWrapper);
diff --git a/app/javascript/flavours/blobfox/features/compose/index.jsx b/app/javascript/flavours/blobfox/features/compose/index.jsx
new file mode 100644
index 00000000000000..34da2b82676c1c
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/index.jsx
@@ -0,0 +1,122 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { injectIntl, defineMessages } from 'react-intl';
+
+import classNames from 'classnames';
+import { Helmet } from 'react-helmet';
+
+import { connect } from 'react-redux';
+
+import spring from 'react-motion/lib/spring';
+
+import { mountCompose, unmountCompose, cycleElefriendCompose } from 'flavours/blobfox/actions/compose';
+import Column from 'flavours/blobfox/components/column';
+
+import { mascot } from '../../initial_state';
+import Motion from '../ui/util/optional_motion';
+
+import ComposeFormContainer from './containers/compose_form_container';
+import HeaderContainer from './containers/header_container';
+import NavigationContainer from './containers/navigation_container';
+import SearchContainer from './containers/search_container';
+import SearchResultsContainer from './containers/search_results_container';
+
+const messages = defineMessages({
+  compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new post' },
+});
+
+const mapStateToProps = (state, ownProps) => ({
+  elefriend: state.getIn(['compose', 'elefriend']),
+  showSearch: ownProps.multiColumn ? state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']) : false,
+});
+
+const mapDispatchToProps = (dispatch) => ({
+  onClickElefriend () {
+    dispatch(cycleElefriendCompose());
+  },
+
+  onMount () {
+    dispatch(mountCompose());
+  },
+
+  onUnmount () {
+    dispatch(unmountCompose());
+  },
+});
+
+class Compose extends PureComponent {
+
+  static propTypes = {
+    multiColumn: PropTypes.bool,
+    showSearch: PropTypes.bool,
+    elefriend: PropTypes.number,
+    onClickElefriend: PropTypes.func,
+    onMount: PropTypes.func,
+    onUnmount: PropTypes.func,
+    intl: PropTypes.object.isRequired,
+  };
+
+  componentDidMount () {
+    this.props.onMount();
+  }
+
+  componentWillUnmount () {
+    this.props.onUnmount();
+  }
+
+  render () {
+    const {
+      elefriend,
+      intl,
+      multiColumn,
+      onClickElefriend,
+      showSearch,
+    } = this.props;
+    const computedClass = classNames('drawer', `mbstobon-${elefriend}`);
+
+    if (multiColumn) {
+      return (
+        <div className={computedClass} role='region' aria-label={intl.formatMessage(messages.compose)}>
+          <HeaderContainer />
+
+          {multiColumn && <SearchContainer />}
+
+          <div className='drawer__pager'>
+            <div className='drawer__inner'>
+              <NavigationContainer />
+
+              <ComposeFormContainer />
+
+              <div className='drawer__inner__mastodon'>
+                {mascot ? <img alt='' draggable='false' src={mascot} /> : <button className='mastodon' onClick={onClickElefriend} />}
+              </div>
+            </div>
+
+            <Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
+              {({ x }) => (
+                <div className='drawer__inner darker' style={{ transform: `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}>
+                  <SearchResultsContainer />
+                </div>
+              )}
+            </Motion>
+          </div>
+        </div>
+      );
+    }
+
+    return (
+      <Column>
+        <NavigationContainer />
+        <ComposeFormContainer />
+
+        <Helmet>
+          <meta name='robots' content='noindex' />
+        </Helmet>
+      </Column>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(Compose));
diff --git a/app/javascript/flavours/blobfox/features/compose/util/counter.js b/app/javascript/flavours/blobfox/features/compose/util/counter.js
new file mode 100644
index 00000000000000..ec2431096b5a46
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/util/counter.js
@@ -0,0 +1,9 @@
+import { urlRegex } from './url_regex';
+
+const urlPlaceholder = '$2xxxxxxxxxxxxxxxxxxxxxxx';
+
+export function countableText(inputText) {
+  return inputText
+    .replace(urlRegex, urlPlaceholder)
+    .replace(/(^|[^/\w])@(([a-z0-9_]+)@[a-z0-9.-]+[a-z0-9]+)/ig, '$1@$3');
+}
diff --git a/app/javascript/flavours/blobfox/features/compose/util/url_regex.js b/app/javascript/flavours/blobfox/features/compose/util/url_regex.js
new file mode 100644
index 00000000000000..887612ae306765
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/compose/util/url_regex.js
@@ -0,0 +1,30 @@
+import regexSupplant from 'twitter-text/dist/lib/regexSupplant';
+import validDomain from 'twitter-text/dist/regexp/validDomain';
+import validPortNumber from 'twitter-text/dist/regexp/validPortNumber';
+import validUrlPath from 'twitter-text/dist/regexp/validUrlPath';
+import validUrlPrecedingChars from 'twitter-text/dist/regexp/validUrlPrecedingChars';
+import validUrlQueryChars from 'twitter-text/dist/regexp/validUrlQueryChars';
+import validUrlQueryEndingChars from 'twitter-text/dist/regexp/validUrlQueryEndingChars';
+
+// The difference with twitter-text's extractURL is that the protocol isn't
+// optional.
+
+export const urlRegex = regexSupplant(
+  '('                                                          + // $1 URL
+    '(#{validUrlPrecedingChars})'                              + // $2
+    '(https?:\\/\\/)'                                          + // $3 Protocol
+    '(#{validDomain})'                                         + // $4 Domain(s)
+    '(?::(#{validPortNumber}))?'                               + // $5 Port number (optional)
+    '(\\/#{validUrlPath}*)?'                                   + // $6 URL Path
+    '(\\?#{validUrlQueryChars}*#{validUrlQueryEndingChars})?'  + // $7 Query String
+  ')',
+  {
+    validUrlPrecedingChars,
+    validDomain,
+    validPortNumber,
+    validUrlPath,
+    validUrlQueryChars,
+    validUrlQueryEndingChars,
+  },
+  'gi',
+);
diff --git a/app/javascript/flavours/blobfox/features/direct_timeline/components/column_settings.jsx b/app/javascript/flavours/blobfox/features/direct_timeline/components/column_settings.jsx
new file mode 100644
index 00000000000000..94168d1d920a90
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/direct_timeline/components/column_settings.jsx
@@ -0,0 +1,47 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
+import SettingToggle from 'flavours/blobfox/features/notifications/components/setting_toggle';
+
+import SettingText from '../../../components/setting_text';
+
+const messages = defineMessages({
+  filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' },
+  settings: { id: 'home.settings', defaultMessage: 'Column settings' },
+});
+
+class ColumnSettings extends PureComponent {
+
+  static propTypes = {
+    settings: ImmutablePropTypes.map.isRequired,
+    onChange: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  render () {
+    const { settings, onChange, intl } = this.props;
+
+    return (
+      <div>
+        <span className='column-settings__section'><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span>
+
+        <div className='column-settings__row'>
+          <SettingToggle settings={settings} settingPath={['conversations']} onChange={onChange} label={<FormattedMessage id='direct.group_by_conversations' defaultMessage='Group by conversation' />} />
+        </div>
+
+        <span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
+
+        <div className='column-settings__row'>
+          <SettingText settings={settings} settingPath={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} />
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(ColumnSettings);
diff --git a/app/javascript/flavours/blobfox/features/direct_timeline/components/conversation.jsx b/app/javascript/flavours/blobfox/features/direct_timeline/components/conversation.jsx
new file mode 100644
index 00000000000000..cdac53539e8944
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/direct_timeline/components/conversation.jsx
@@ -0,0 +1,233 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import classNames from 'classnames';
+import { withRouter } from 'react-router-dom';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import { HotKeys } from 'react-hotkeys';
+
+import AttachmentList from 'flavours/blobfox/components/attachment_list';
+import AvatarComposite from 'flavours/blobfox/components/avatar_composite';
+import { IconButton } from 'flavours/blobfox/components/icon_button';
+import Permalink from 'flavours/blobfox/components/permalink';
+import { RelativeTimestamp } from 'flavours/blobfox/components/relative_timestamp';
+import StatusContent from 'flavours/blobfox/components/status_content';
+import DropdownMenuContainer from 'flavours/blobfox/containers/dropdown_menu_container';
+import { autoPlayGif } from 'flavours/blobfox/initial_state';
+import { WithRouterPropTypes } from 'flavours/blobfox/utils/react_router';
+
+const messages = defineMessages({
+  more: { id: 'status.more', defaultMessage: 'More' },
+  open: { id: 'conversation.open', defaultMessage: 'View conversation' },
+  reply: { id: 'status.reply', defaultMessage: 'Reply' },
+  markAsRead: { id: 'conversation.mark_as_read', defaultMessage: 'Mark as read' },
+  delete: { id: 'conversation.delete', defaultMessage: 'Delete conversation' },
+  muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
+  unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
+});
+
+class Conversation extends ImmutablePureComponent {
+
+  static propTypes = {
+    conversationId: PropTypes.string.isRequired,
+    accounts: ImmutablePropTypes.list.isRequired,
+    lastStatus: ImmutablePropTypes.map,
+    unread:PropTypes.bool.isRequired,
+    scrollKey: PropTypes.string,
+    onMoveUp: PropTypes.func,
+    onMoveDown: PropTypes.func,
+    markRead: PropTypes.func.isRequired,
+    delete: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+    ...WithRouterPropTypes,
+  };
+
+  state = {
+    isExpanded: undefined,
+  };
+
+  parseClick = (e, destination) => {
+    const { history, lastStatus, unread, markRead } = this.props;
+    if (!history) return;
+
+    if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey)) {
+      if (destination === undefined) {
+        if (unread) {
+          markRead();
+        }
+        destination = `/statuses/${lastStatus.get('id')}`;
+      }
+      history.push(destination);
+      e.preventDefault();
+    }
+  };
+
+  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');
+    }
+  };
+
+  handleClick = () => {
+    if (!this.props.history) {
+      return;
+    }
+
+    const { lastStatus, unread, markRead } = this.props;
+
+    if (unread) {
+      markRead();
+    }
+
+    this.props.history.push(`/@${lastStatus.getIn(['account', 'acct'])}/${lastStatus.get('id')}`);
+  };
+
+  handleMarkAsRead = () => {
+    this.props.markRead();
+  };
+
+  handleReply = () => {
+    this.props.reply(this.props.lastStatus, this.props.history);
+  };
+
+  handleDelete = () => {
+    this.props.delete();
+  };
+
+  handleHotkeyMoveUp = () => {
+    this.props.onMoveUp(this.props.conversationId);
+  };
+
+  handleHotkeyMoveDown = () => {
+    this.props.onMoveDown(this.props.conversationId);
+  };
+
+  handleConversationMute = () => {
+    this.props.onMute(this.props.lastStatus);
+  };
+
+  handleShowMore = () => {
+    this.props.onToggleHidden(this.props.lastStatus);
+
+    if (this.props.lastStatus.get('spoiler_text')) {
+      this.setExpansion(!this.state.isExpanded);
+    }
+  };
+
+  setExpansion = value => {
+    this.setState({ isExpanded: value });
+  };
+
+  render () {
+    const { accounts, lastStatus, unread, scrollKey, intl } = this.props;
+
+    if (lastStatus === null) {
+      return null;
+    }
+
+    const isExpanded = this.props.settings.getIn(['content_warnings', 'shared_state']) ? !lastStatus.get('hidden') : this.state.isExpanded;
+
+    const menu = [
+      { text: intl.formatMessage(messages.open), action: this.handleClick },
+      null,
+    ];
+
+    menu.push({ text: intl.formatMessage(lastStatus.get('muted') ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMute });
+
+    if (unread) {
+      menu.push({ text: intl.formatMessage(messages.markAsRead), action: this.handleMarkAsRead });
+      menu.push(null);
+    }
+
+    menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDelete });
+
+    const names = accounts.map(a => <Permalink to={`/@${a.get('acct')}`} href={a.get('url')} key={a.get('id')} title={a.get('acct')}><bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi></Permalink>).reduce((prev, cur) => [prev, ', ', cur]);
+
+    const handlers = {
+      reply: this.handleReply,
+      open: this.handleClick,
+      moveUp: this.handleHotkeyMoveUp,
+      moveDown: this.handleHotkeyMoveDown,
+      toggleHidden: this.handleShowMore,
+    };
+
+    let media = null;
+    if (lastStatus.get('media_attachments').size > 0) {
+      media = <AttachmentList compact media={lastStatus.get('media_attachments')} />;
+    }
+
+    return (
+      <HotKeys handlers={handlers}>
+        <div className={classNames('conversation focusable muted', { 'conversation--unread': unread })} tabIndex={0}>
+          <div className='conversation__avatar' onClick={this.handleClick} role='presentation'>
+            <AvatarComposite accounts={accounts} size={48} />
+          </div>
+
+          <div className='conversation__content'>
+            <div className='conversation__content__info'>
+              <div className='conversation__content__relative-time'>
+                {unread && <span className='conversation__unread' />} <RelativeTimestamp timestamp={lastStatus.get('created_at')} />
+              </div>
+
+              <div className='conversation__content__names' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
+                <FormattedMessage id='conversation.with' defaultMessage='With {names}' values={{ names: <span>{names}</span> }} />
+              </div>
+            </div>
+
+            <StatusContent
+              status={lastStatus}
+              parseClick={this.parseClick}
+              expanded={isExpanded}
+              onExpandedToggle={this.handleShowMore}
+              collapsible
+              media={media}
+            />
+
+            <div className='status__action-bar'>
+              <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.reply)} icon='reply' onClick={this.handleReply} />
+
+              <div className='status__action-bar-dropdown'>
+                <DropdownMenuContainer
+                  scrollKey={scrollKey}
+                  status={lastStatus}
+                  items={menu}
+                  icon='ellipsis-h'
+                  size={18}
+                  direction='right'
+                  title={intl.formatMessage(messages.more)}
+                />
+              </div>
+            </div>
+          </div>
+        </div>
+      </HotKeys>
+    );
+  }
+
+}
+
+export default withRouter(injectIntl(Conversation));
diff --git a/app/javascript/flavours/blobfox/features/direct_timeline/components/conversations_list.jsx b/app/javascript/flavours/blobfox/features/direct_timeline/components/conversations_list.jsx
new file mode 100644
index 00000000000000..8c12ea9e5f68a2
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/direct_timeline/components/conversations_list.jsx
@@ -0,0 +1,77 @@
+import PropTypes from 'prop-types';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import { debounce } from 'lodash';
+
+import ScrollableList from '../../../components/scrollable_list';
+import ConversationContainer from '../containers/conversation_container';
+
+export default class ConversationsList extends ImmutablePureComponent {
+
+  static propTypes = {
+    conversations: ImmutablePropTypes.list.isRequired,
+    scrollKey: PropTypes.string.isRequired,
+    hasMore: PropTypes.bool,
+    isLoading: PropTypes.bool,
+    onLoadMore: PropTypes.func,
+  };
+
+  getCurrentIndex = id => this.props.conversations.findIndex(x => x.get('id') === id);
+
+  handleMoveUp = id => {
+    const elementIndex = this.getCurrentIndex(id) - 1;
+    this._selectChild(elementIndex, true);
+  };
+
+  handleMoveDown = id => {
+    const elementIndex = this.getCurrentIndex(id) + 1;
+    this._selectChild(elementIndex, false);
+  };
+
+  _selectChild (index, align_top) {
+    const container = this.node.node;
+    const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
+
+    if (element) {
+      if (align_top && container.scrollTop > element.offsetTop) {
+        element.scrollIntoView(true);
+      } else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
+        element.scrollIntoView(false);
+      }
+      element.focus();
+    }
+  }
+
+  setRef = c => {
+    this.node = c;
+  };
+
+  handleLoadOlder = debounce(() => {
+    const last = this.props.conversations.last();
+
+    if (last && last.get('last_status')) {
+      this.props.onLoadMore(last.get('last_status'));
+    }
+  }, 300, { leading: true });
+
+  render () {
+    const { conversations, isLoading, onLoadMore, ...other } = this.props;
+
+    return (
+      <ScrollableList {...other} isLoading={isLoading} showLoading={isLoading && conversations.isEmpty()} onLoadMore={onLoadMore && this.handleLoadOlder} ref={this.setRef}>
+        {conversations.map(item => (
+          <ConversationContainer
+            key={item.get('id')}
+            conversationId={item.get('id')}
+            onMoveUp={this.handleMoveUp}
+            onMoveDown={this.handleMoveDown}
+            scrollKey={this.props.scrollKey}
+          />
+        ))}
+      </ScrollableList>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/direct_timeline/containers/column_settings_container.js b/app/javascript/flavours/blobfox/features/direct_timeline/containers/column_settings_container.js
new file mode 100644
index 00000000000000..e7cdbf60187b96
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/direct_timeline/containers/column_settings_container.js
@@ -0,0 +1,19 @@
+import { connect } from 'react-redux';
+
+import { changeSetting } from 'flavours/blobfox/actions/settings';
+
+import ColumnSettings from '../components/column_settings';
+
+const mapStateToProps = state => ({
+  settings: state.getIn(['settings', 'direct']),
+});
+
+const mapDispatchToProps = dispatch => ({
+
+  onChange (path, checked) {
+    dispatch(changeSetting(['direct', ...path], checked));
+  },
+
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);
diff --git a/app/javascript/flavours/blobfox/features/direct_timeline/containers/conversation_container.js b/app/javascript/flavours/blobfox/features/direct_timeline/containers/conversation_container.js
new file mode 100644
index 00000000000000..ab80d0629635d4
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/direct_timeline/containers/conversation_container.js
@@ -0,0 +1,81 @@
+import { defineMessages, injectIntl } from 'react-intl';
+
+import { connect } from 'react-redux';
+
+import { replyCompose } from 'flavours/blobfox/actions/compose';
+import { markConversationRead, deleteConversation } from 'flavours/blobfox/actions/conversations';
+import { openModal } from 'flavours/blobfox/actions/modal';
+import { muteStatus, unmuteStatus, hideStatus, revealStatus } from 'flavours/blobfox/actions/statuses';
+import { makeGetStatus } from 'flavours/blobfox/selectors';
+
+import Conversation from '../components/conversation';
+
+const messages = defineMessages({
+  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?' },
+});
+
+const mapStateToProps = () => {
+  const getStatus = makeGetStatus();
+
+  return (state, { conversationId }) => {
+    const conversation = state.getIn(['conversations', 'items']).find(x => x.get('id') === conversationId);
+    const lastStatusId = conversation.get('last_status', null);
+
+    return {
+      accounts: conversation.get('accounts').map(accountId => state.getIn(['accounts', accountId], null)),
+      unread: conversation.get('unread'),
+      lastStatus: lastStatusId && getStatus(state, { id: lastStatusId }),
+      settings: state.get('local_settings'),
+    };
+  };
+};
+
+const mapDispatchToProps = (dispatch, { intl, conversationId }) => ({
+
+  markRead () {
+    dispatch(markConversationRead(conversationId));
+  },
+
+  reply (status, router) {
+    dispatch((_, getState) => {
+      let state = getState();
+
+      if (state.getIn(['compose', 'text']).trim().length !== 0) {
+        dispatch(openModal({
+          modalType: 'CONFIRM',
+          modalProps: {
+            message: intl.formatMessage(messages.replyMessage),
+            confirm: intl.formatMessage(messages.replyConfirm),
+            onConfirm: () => dispatch(replyCompose(status, router)),
+          },
+        }));
+      } else {
+        dispatch(replyCompose(status, router));
+      }
+    });
+  },
+
+  delete () {
+    dispatch(deleteConversation(conversationId));
+  },
+
+  onMute (status) {
+    if (status.get('muted')) {
+      dispatch(unmuteStatus(status.get('id')));
+    } else {
+      dispatch(muteStatus(status.get('id')));
+    }
+  },
+
+  onToggleHidden (status) {
+    if (status.get('hidden')) {
+      dispatch(revealStatus(status.get('id')));
+    } else {
+      dispatch(hideStatus(status.get('id')));
+    }
+  },
+
+});
+
+export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(Conversation));
diff --git a/app/javascript/flavours/blobfox/features/direct_timeline/containers/conversations_list_container.js b/app/javascript/flavours/blobfox/features/direct_timeline/containers/conversations_list_container.js
new file mode 100644
index 00000000000000..1dcd3ec1bd4ad3
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/direct_timeline/containers/conversations_list_container.js
@@ -0,0 +1,16 @@
+import { connect } from 'react-redux';
+
+import { expandConversations } from '../../../actions/conversations';
+import ConversationsList from '../components/conversations_list';
+
+const mapStateToProps = state => ({
+  conversations: state.getIn(['conversations', 'items']),
+  isLoading: state.getIn(['conversations', 'isLoading'], true),
+  hasMore: state.getIn(['conversations', 'hasMore'], false),
+});
+
+const mapDispatchToProps = dispatch => ({
+  onLoadMore: maxId => dispatch(expandConversations({ maxId })),
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(ConversationsList);
diff --git a/app/javascript/flavours/blobfox/features/direct_timeline/index.jsx b/app/javascript/flavours/blobfox/features/direct_timeline/index.jsx
new file mode 100644
index 00000000000000..d47464f0720d7e
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/direct_timeline/index.jsx
@@ -0,0 +1,165 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+
+import { connect } from 'react-redux';
+
+import { addColumn, removeColumn, moveColumn } from 'flavours/blobfox/actions/columns';
+import { mountConversations, unmountConversations, expandConversations } from 'flavours/blobfox/actions/conversations';
+import { connectDirectStream } from 'flavours/blobfox/actions/streaming';
+import { expandDirectTimeline } from 'flavours/blobfox/actions/timelines';
+import Column from 'flavours/blobfox/components/column';
+import ColumnHeader from 'flavours/blobfox/components/column_header';
+import StatusListContainer from 'flavours/blobfox/features/ui/containers/status_list_container';
+
+import ColumnSettingsContainer from './containers/column_settings_container';
+import ConversationsListContainer from './containers/conversations_list_container';
+
+const messages = defineMessages({
+  title: { id: 'column.direct', defaultMessage: 'Private mentions' },
+});
+
+const mapStateToProps = state => ({
+  hasUnread: state.getIn(['timelines', 'direct', 'unread']) > 0,
+  conversationsMode: state.getIn(['settings', 'direct', 'conversations']),
+});
+
+class DirectTimeline extends PureComponent {
+
+  static propTypes = {
+    dispatch: PropTypes.func.isRequired,
+    columnId: PropTypes.string,
+    intl: PropTypes.object.isRequired,
+    hasUnread: PropTypes.bool,
+    multiColumn: PropTypes.bool,
+    conversationsMode: PropTypes.bool,
+  };
+
+  handlePin = () => {
+    const { columnId, dispatch } = this.props;
+
+    if (columnId) {
+      dispatch(removeColumn(columnId));
+    } else {
+      dispatch(addColumn('DIRECT', {}));
+    }
+  };
+
+  handleMove = (dir) => {
+    const { columnId, dispatch } = this.props;
+    dispatch(moveColumn(columnId, dir));
+  };
+
+  handleHeaderClick = () => {
+    this.column.scrollTop();
+  };
+
+  componentDidMount () {
+    const { dispatch, conversationsMode } = this.props;
+
+    dispatch(mountConversations());
+
+    if (conversationsMode) {
+      dispatch(expandConversations());
+    } else {
+      dispatch(expandDirectTimeline());
+    }
+
+    this.disconnect = dispatch(connectDirectStream());
+  }
+
+  componentDidUpdate(prevProps) {
+    const { dispatch, conversationsMode } = this.props;
+
+    if (prevProps.conversationsMode && !conversationsMode) {
+      dispatch(expandDirectTimeline());
+    } else if (!prevProps.conversationsMode && conversationsMode) {
+      dispatch(expandConversations());
+    }
+  }
+
+  componentWillUnmount () {
+    this.props.dispatch(unmountConversations());
+
+    if (this.disconnect) {
+      this.disconnect();
+      this.disconnect = null;
+    }
+  }
+
+  setRef = c => {
+    this.column = c;
+  };
+
+  handleLoadMoreTimeline = maxId => {
+    this.props.dispatch(expandDirectTimeline({ maxId }));
+  };
+
+  handleLoadMoreConversations = maxId => {
+    this.props.dispatch(expandConversations({ maxId }));
+  };
+
+  render () {
+    const { intl, hasUnread, columnId, multiColumn, conversationsMode } = this.props;
+    const pinned = !!columnId;
+
+    let contents;
+    if (conversationsMode) {
+      contents = (
+        <ConversationsListContainer
+          trackScroll={!pinned}
+          scrollKey={`direct_timeline-${columnId}`}
+          timelineId='direct'
+          bindToDocument={!multiColumn}
+          onLoadMore={this.handleLoadMore}
+          prepend={<div className='follow_requests-unlocked_explanation'><span><FormattedMessage id='compose_form.encryption_warning' defaultMessage='Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.' /> <a href='/terms' target='_blank'><FormattedMessage id='compose_form.direct_message_warning_learn_more' defaultMessage='Learn more' /></a></span></div>}
+          alwaysPrepend
+          emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any private mentions yet. When you send or receive one, it will show up here." />}
+        />
+      );
+    } else {
+      contents = (
+        <StatusListContainer
+          trackScroll={!pinned}
+          scrollKey={`direct_timeline-${columnId}`}
+          timelineId='direct'
+          bindToDocument={!multiColumn}
+          onLoadMore={this.handleLoadMoreTimeline}
+          prepend={<div className='follow_requests-unlocked_explanation'><span><FormattedMessage id='compose_form.encryption_warning' defaultMessage='Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.' /> <a href='/terms' target='_blank'><FormattedMessage id='compose_form.direct_message_warning_learn_more' defaultMessage='Learn more' /></a></span></div>}
+          alwaysPrepend
+          emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any private mentions yet. When you send or receive one, it will show up here." />}
+        />
+      );
+    }
+
+    return (
+      <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
+        <ColumnHeader
+          icon='envelope'
+          active={hasUnread}
+          title={intl.formatMessage(messages.title)}
+          onPin={this.handlePin}
+          onMove={this.handleMove}
+          onClick={this.handleHeaderClick}
+          pinned={pinned}
+          multiColumn={multiColumn}
+        >
+          <ColumnSettingsContainer />
+        </ColumnHeader>
+
+        {contents}
+
+        <Helmet>
+          <title>{intl.formatMessage(messages.title)}</title>
+          <meta name='robots' content='noindex' />
+        </Helmet>
+      </Column>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(injectIntl(DirectTimeline));
diff --git a/app/javascript/flavours/blobfox/features/directory/components/account_card.jsx b/app/javascript/flavours/blobfox/features/directory/components/account_card.jsx
new file mode 100644
index 00000000000000..cdf38e06a972cc
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/directory/components/account_card.jsx
@@ -0,0 +1,255 @@
+import PropTypes from 'prop-types';
+
+import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
+
+import classNames from 'classnames';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
+import {
+  followAccount,
+  unfollowAccount,
+  unblockAccount,
+  unmuteAccount,
+} from 'flavours/blobfox/actions/accounts';
+import { openModal } from 'flavours/blobfox/actions/modal';
+import { Avatar } from 'flavours/blobfox/components/avatar';
+import { Button } from 'flavours/blobfox/components/button';
+import { DisplayName } from 'flavours/blobfox/components/display_name';
+import { IconButton } from 'flavours/blobfox/components/icon_button';
+import Permalink from 'flavours/blobfox/components/permalink';
+import { ShortNumber } from 'flavours/blobfox/components/short_number';
+import { autoPlayGif, me, unfollowModal } from 'flavours/blobfox/initial_state';
+import { makeGetAccount } from 'flavours/blobfox/selectors';
+
+const messages = defineMessages({
+  unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
+  follow: { id: 'account.follow', defaultMessage: 'Follow' },
+  cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Withdraw follow request' },
+  cancelFollowRequestConfirm: { id: 'confirmations.cancel_follow_request.confirm', defaultMessage: 'Withdraw request' },
+  requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
+  unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
+  unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
+  unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
+  edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
+  dismissSuggestion: { id: 'suggestions.dismiss', defaultMessage: 'Dismiss suggestion' },
+});
+
+const makeMapStateToProps = () => {
+  const getAccount = makeGetAccount();
+
+  const mapStateToProps = (state, { id }) => ({
+    account: getAccount(state, id),
+  });
+
+  return mapStateToProps;
+};
+
+const mapDispatchToProps = (dispatch, { intl }) => ({
+  onFollow(account) {
+    if (account.getIn(['relationship', 'following'])) {
+      if (unfollowModal) {
+        dispatch(
+          openModal({
+            modalType: 'CONFIRM',
+            modalProps: {
+              message: (
+                <FormattedMessage
+                  id='confirmations.unfollow.message'
+                  defaultMessage='Are you sure you want to unfollow {name}?'
+                  values={{ name: <strong>@{account.get('acct')}</strong> }}
+                />
+              ),
+              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: <FormattedMessage id='confirmations.cancel_follow_request.message' defaultMessage='Are you sure you want to withdraw your request to follow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
+            confirm: intl.formatMessage(messages.cancelFollowRequestConfirm),
+            onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
+          },
+        }));
+      } else {
+        dispatch(unfollowAccount(account.get('id')));
+      }
+    } else {
+      dispatch(followAccount(account.get('id')));
+    }
+  },
+
+  onBlock(account) {
+    if (account.getIn(['relationship', 'blocking'])) {
+      dispatch(unblockAccount(account.get('id')));
+    }
+  },
+
+  onMute(account) {
+    if (account.getIn(['relationship', 'muting'])) {
+      dispatch(unmuteAccount(account.get('id')));
+    }
+  },
+
+});
+
+class AccountCard extends ImmutablePureComponent {
+
+  static propTypes = {
+    account: ImmutablePropTypes.record.isRequired,
+    intl: PropTypes.object.isRequired,
+    onFollow: PropTypes.func.isRequired,
+    onBlock: PropTypes.func.isRequired,
+    onMute: PropTypes.func.isRequired,
+    onDismiss: 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');
+    }
+  };
+
+  handleFollow = () => {
+    this.props.onFollow(this.props.account);
+  };
+
+  handleBlock = () => {
+    this.props.onBlock(this.props.account);
+  };
+
+  handleMute = () => {
+    this.props.onMute(this.props.account);
+  };
+
+  handleEditProfile = () => {
+    window.open('/settings/profile', '_blank');
+  };
+
+  handleDismiss = (e) => {
+    const { account, onDismiss } = this.props;
+    onDismiss(account.get('id'));
+
+    e.preventDefault();
+    e.stopPropagation();
+  };
+
+  render() {
+    const { account, intl } = this.props;
+
+    let actionBtn;
+
+    if (me !== account.get('id')) {
+      if (!account.get('relationship')) { // Wait until the relationship is loaded
+        actionBtn = '';
+      } else if (account.getIn(['relationship', 'requested'])) {
+        actionBtn = <Button  text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.handleFollow} />;
+      } else if (account.getIn(['relationship', 'muting'])) {
+        actionBtn = <Button  text={intl.formatMessage(messages.unmute)} onClick={this.handleMute} />;
+      } else if (!account.getIn(['relationship', 'blocking'])) {
+        actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames({ 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />;
+      } else if (account.getIn(['relationship', 'blocking'])) {
+        actionBtn = <Button  text={intl.formatMessage(messages.unblock)} onClick={this.handleBlock} />;
+      }
+    } else {
+      actionBtn = <Button  text={intl.formatMessage(messages.edit_profile)} onClick={this.handleEditProfile} />;
+    }
+
+    return (
+      <div className='account-card'>
+        <Permalink href={account.get('url')} to={`/@${account.get('acct')}`} className='account-card__permalink'>
+          <div className='account-card__header'>
+            {this.props.onDismiss && <IconButton className='media-modal__close' title={intl.formatMessage(messages.dismissSuggestion)} icon='times' onClick={this.handleDismiss} size={20} />}
+
+            <img
+              src={
+                autoPlayGif ? account.get('header') : account.get('header_static')
+              }
+              alt=''
+            />
+          </div>
+
+          <div className='account-card__title'>
+            <div className='account-card__title__avatar'><Avatar account={account} size={56} /></div>
+            <DisplayName account={account} />
+          </div>
+        </Permalink>
+
+        {account.get('note').length > 0 && (
+          <div
+            className='account-card__bio translate'
+            onMouseEnter={this.handleMouseEnter}
+            onMouseLeave={this.handleMouseLeave}
+            dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
+          />
+        )}
+
+        <div className='account-card__actions'>
+          <div className='account-card__counters'>
+            <div className='account-card__counters__item'>
+              <ShortNumber value={account.get('statuses_count')} />
+              <small>
+                <FormattedMessage id='account.posts' defaultMessage='Posts' />
+              </small>
+            </div>
+
+            <div className='account-card__counters__item'>
+              {account.get('followers_count') < 0 ? '-' : <ShortNumber value={account.get('followers_count')} />}{' '}
+              <small>
+                <FormattedMessage
+                  id='account.followers'
+                  defaultMessage='Followers'
+                />
+              </small>
+            </div>
+
+            <div className='account-card__counters__item'>
+              <ShortNumber value={account.get('following_count')} />{' '}
+              <small>
+                <FormattedMessage
+                  id='account.following'
+                  defaultMessage='Following'
+                />
+              </small>
+            </div>
+          </div>
+
+          <div className='account-card__actions__button'>
+            {actionBtn}
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(AccountCard));
diff --git a/app/javascript/flavours/blobfox/features/directory/index.jsx b/app/javascript/flavours/blobfox/features/directory/index.jsx
new file mode 100644
index 00000000000000..95caaae1211629
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/directory/index.jsx
@@ -0,0 +1,179 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, injectIntl } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+
+import { List as ImmutableList } from 'immutable';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
+
+import { addColumn, removeColumn, moveColumn, changeColumnParams } from 'flavours/blobfox/actions/columns';
+import { fetchDirectory, expandDirectory } from 'flavours/blobfox/actions/directory';
+import Column from 'flavours/blobfox/components/column';
+import ColumnHeader from 'flavours/blobfox/components/column_header';
+import { LoadMore } from 'flavours/blobfox/components/load_more';
+import { LoadingIndicator } from 'flavours/blobfox/components/loading_indicator';
+import { RadioButton } from 'flavours/blobfox/components/radio_button';
+import ScrollContainer from 'flavours/blobfox/containers/scroll_container';
+
+import AccountCard from './components/account_card';
+
+const messages = defineMessages({
+  title: { id: 'column.directory', defaultMessage: 'Browse profiles' },
+  recentlyActive: { id: 'directory.recently_active', defaultMessage: 'Recently active' },
+  newArrivals: { id: 'directory.new_arrivals', defaultMessage: 'New arrivals' },
+  local: { id: 'directory.local', defaultMessage: 'From {domain} only' },
+  federated: { id: 'directory.federated', defaultMessage: 'From known fediverse' },
+});
+
+const mapStateToProps = state => ({
+  accountIds: state.getIn(['user_lists', 'directory', 'items'], ImmutableList()),
+  isLoading: state.getIn(['user_lists', 'directory', 'isLoading'], true),
+  domain: state.getIn(['meta', 'domain']),
+});
+
+class Directory extends PureComponent {
+
+  static propTypes = {
+    isLoading: PropTypes.bool,
+    accountIds: ImmutablePropTypes.list.isRequired,
+    dispatch: PropTypes.func.isRequired,
+    columnId: PropTypes.string,
+    intl: PropTypes.object.isRequired,
+    multiColumn: PropTypes.bool,
+    domain: PropTypes.string.isRequired,
+    params: PropTypes.shape({
+      order: PropTypes.string,
+      local: PropTypes.bool,
+    }),
+  };
+
+  state = {
+    order: null,
+    local: null,
+  };
+
+  handlePin = () => {
+    const { columnId, dispatch } = this.props;
+
+    if (columnId) {
+      dispatch(removeColumn(columnId));
+    } else {
+      dispatch(addColumn('DIRECTORY', this.getParams(this.props, this.state)));
+    }
+  };
+
+  getParams = (props, state) => ({
+    order: state.order === null ? (props.params.order || 'active') : state.order,
+    local: state.local === null ? (props.params.local || false) : state.local,
+  });
+
+  handleMove = dir => {
+    const { columnId, dispatch } = this.props;
+    dispatch(moveColumn(columnId, dir));
+  };
+
+  handleHeaderClick = () => {
+    this.column.scrollTop();
+  };
+
+  componentDidMount () {
+    const { dispatch } = this.props;
+    dispatch(fetchDirectory(this.getParams(this.props, this.state)));
+  }
+
+  componentDidUpdate (prevProps, prevState) {
+    const { dispatch } = this.props;
+    const paramsOld = this.getParams(prevProps, prevState);
+    const paramsNew = this.getParams(this.props, this.state);
+
+    if (paramsOld.order !== paramsNew.order || paramsOld.local !== paramsNew.local) {
+      dispatch(fetchDirectory(paramsNew));
+    }
+  }
+
+  setRef = c => {
+    this.column = c;
+  };
+
+  handleChangeOrder = e => {
+    const { dispatch, columnId } = this.props;
+
+    if (columnId) {
+      dispatch(changeColumnParams(columnId, ['order'], e.target.value));
+    } else {
+      this.setState({ order: e.target.value });
+    }
+  };
+
+  handleChangeLocal = e => {
+    const { dispatch, columnId } = this.props;
+
+    if (columnId) {
+      dispatch(changeColumnParams(columnId, ['local'], e.target.value === '1'));
+    } else {
+      this.setState({ local: e.target.value === '1' });
+    }
+  };
+
+  handleLoadMore = () => {
+    const { dispatch } = this.props;
+    dispatch(expandDirectory(this.getParams(this.props, this.state)));
+  };
+
+  render () {
+    const { isLoading, accountIds, intl, columnId, multiColumn, domain } = this.props;
+    const { order, local }  = this.getParams(this.props, this.state);
+    const pinned = !!columnId;
+
+    const scrollableArea = (
+      <div className='scrollable'>
+        <div className='filter-form'>
+          <div className='filter-form__column' role='group'>
+            <RadioButton name='order' value='active' label={intl.formatMessage(messages.recentlyActive)} checked={order === 'active'} onChange={this.handleChangeOrder} />
+            <RadioButton name='order' value='new' label={intl.formatMessage(messages.newArrivals)} checked={order === 'new'} onChange={this.handleChangeOrder} />
+          </div>
+
+          <div className='filter-form__column' role='group'>
+            <RadioButton name='local' value='1' label={intl.formatMessage(messages.local, { domain })} checked={local} onChange={this.handleChangeLocal} />
+            <RadioButton name='local' value='0' label={intl.formatMessage(messages.federated)} checked={!local} onChange={this.handleChangeLocal} />
+          </div>
+        </div>
+
+        <div className='directory__list'>
+          {isLoading ? <LoadingIndicator /> : accountIds.map(accountId => (
+            <AccountCard id={accountId} key={accountId} />
+          ))}
+        </div>
+
+        <LoadMore onClick={this.handleLoadMore} visible={!isLoading} />
+      </div>
+    );
+
+    return (
+      <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
+        <ColumnHeader
+          icon='address-book-o'
+          title={intl.formatMessage(messages.title)}
+          onPin={this.handlePin}
+          onMove={this.handleMove}
+          onClick={this.handleHeaderClick}
+          pinned={pinned}
+          multiColumn={multiColumn}
+        />
+
+        {multiColumn && !pinned ? <ScrollContainer scrollKey='directory'>{scrollableArea}</ScrollContainer> : scrollableArea}
+
+        <Helmet>
+          <title>{intl.formatMessage(messages.title)}</title>
+          <meta name='robots' content='noindex' />
+        </Helmet>
+      </Column>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(injectIntl(Directory));
diff --git a/app/javascript/flavours/blobfox/features/domain_blocks/index.jsx b/app/javascript/flavours/blobfox/features/domain_blocks/index.jsx
new file mode 100644
index 00000000000000..9e63b2f8170570
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/domain_blocks/index.jsx
@@ -0,0 +1,87 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
+import { debounce } from 'lodash';
+
+import { fetchDomainBlocks, expandDomainBlocks } from '../../actions/domain_blocks';
+import ColumnBackButtonSlim from '../../components/column_back_button_slim';
+import { LoadingIndicator } from '../../components/loading_indicator';
+import ScrollableList from '../../components/scrollable_list';
+import DomainContainer from '../../containers/domain_container';
+import Column from '../ui/components/column';
+
+const messages = defineMessages({
+  heading: { id: 'column.domain_blocks', defaultMessage: 'Blocked domains' },
+  unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
+});
+
+const mapStateToProps = state => ({
+  domains: state.getIn(['domain_lists', 'blocks', 'items']),
+  hasMore: !!state.getIn(['domain_lists', 'blocks', 'next']),
+});
+
+class Blocks extends ImmutablePureComponent {
+
+  static propTypes = {
+    params: PropTypes.object.isRequired,
+    dispatch: PropTypes.func.isRequired,
+    hasMore: PropTypes.bool,
+    domains: ImmutablePropTypes.orderedSet,
+    intl: PropTypes.object.isRequired,
+    multiColumn: PropTypes.bool,
+  };
+
+  UNSAFE_componentWillMount () {
+    this.props.dispatch(fetchDomainBlocks());
+  }
+
+  handleLoadMore = debounce(() => {
+    this.props.dispatch(expandDomainBlocks());
+  }, 300, { leading: true });
+
+  render () {
+    const { intl, domains, hasMore, multiColumn } = this.props;
+
+    if (!domains) {
+      return (
+        <Column>
+          <LoadingIndicator />
+        </Column>
+      );
+    }
+
+    const emptyMessage = <FormattedMessage id='empty_column.domain_blocks' defaultMessage='There are no blocked domains yet.' />;
+
+    return (
+      <Column bindToDocument={!multiColumn} icon='minus-circle' heading={intl.formatMessage(messages.heading)}>
+        <ColumnBackButtonSlim />
+
+        <ScrollableList
+          scrollKey='domain_blocks'
+          onLoadMore={this.handleLoadMore}
+          hasMore={hasMore}
+          emptyMessage={emptyMessage}
+          bindToDocument={!multiColumn}
+        >
+          {domains.map(domain =>
+            <DomainContainer key={domain} domain={domain} />,
+          )}
+        </ScrollableList>
+
+        <Helmet>
+          <meta name='robots' content='noindex' />
+        </Helmet>
+      </Column>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(injectIntl(Blocks));
diff --git a/app/javascript/flavours/blobfox/features/emoji/emoji.js b/app/javascript/flavours/blobfox/features/emoji/emoji.js
new file mode 100644
index 00000000000000..d283871211f104
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/emoji/emoji.js
@@ -0,0 +1,166 @@
+import Trie from 'substring-trie';
+
+import { assetHost } from 'flavours/blobfox/utils/config';
+
+import { autoPlayGif, useSystemEmojiFont } from '../../initial_state';
+
+import { unicodeMapping } from './emoji_unicode_mapping_light';
+
+const trie = new Trie(Object.keys(unicodeMapping));
+
+// Convert to file names from emojis. (For different variation selector emojis)
+const emojiFilenames = (emojis) => {
+  return emojis.map(v => unicodeMapping[v].filename);
+};
+
+// Emoji requiring extra borders depending on theme
+const darkEmoji = emojiFilenames(['🎱', '🐜', '⚫', '🖤', '⬛', '◼️', '◾', '◼️', '✒️', '▪️', '💣', '🎳', '📷', '📸', '♣️', '🕶️', '✴️', '🔌', '💂‍♀️', '📽️', '🍳', '🦍', '💂', '🔪', '🕳️', '🕹️', '🕋', '🖊️', '🖋️', '💂‍♂️', '🎤', '🎓', '🎥', '🎼', '♠️', '🎩', '🦃', '📼', '📹', '🎮', '🐃', '🏴', '🐞', '🕺', '📱', '📲', '🚲']);
+const lightEmoji = emojiFilenames(['👽', '⚾', '🐔', '☁️', '💨', '🕊️', '👀', '🍥', '👻', '🐐', '❕', '❔', '⛸️', '🌩️', '🔊', '🔇', '📃', '🌧️', '🐏', '🍚', '🍙', '🐓', '🐑', '💀', '☠️', '🌨️', '🔉', '🔈', '💬', '💭', '🏐', '🏳️', '⚪', '⬜', '◽', '◻️', '▫️']);
+
+const emojiFilename = (filename) => {
+  const borderedEmoji = (document.body && document.body.classList.contains('skin-mastodon-light')) ? lightEmoji : darkEmoji;
+  return borderedEmoji.includes(filename) ? (filename + '_border') : filename;
+};
+
+const emojifyTextNode = (node, customEmojis) => {
+  const VS15 = 0xFE0E;
+  const VS16 = 0xFE0F;
+
+  let str = node.textContent;
+
+  const fragment = new DocumentFragment();
+  let i = 0;
+
+  for (;;) {
+    let unicode_emoji;
+
+    // Skip to the next potential emoji to replace (either custom emoji or custom emoji :shortcode:)
+    if (customEmojis === null) {
+      while (i < str.length && (useSystemEmojiFont || !(unicode_emoji = trie.search(str.slice(i))))) {
+        i += str.codePointAt(i) < 65536 ? 1 : 2;
+      }
+    } else {
+      while (i < str.length && str[i] !== ':' && (useSystemEmojiFont || !(unicode_emoji = trie.search(str.slice(i))))) {
+        i += str.codePointAt(i) < 65536 ? 1 : 2;
+      }
+    }
+
+    // We reached the end of the string, nothing to replace
+    if (i === str.length) {
+      break;
+    }
+
+    let rend, replacement = null;
+    if (str[i] === ':') { // Potentially the start of a custom emoji :shortcode:
+      rend = str.indexOf(':', i + 1) + 1;
+
+      // no matching ending ':', skip
+      if (!rend) {
+        i++;
+        continue;
+      }
+
+      const shortcode = str.slice(i, rend);
+      const custom_emoji = customEmojis[shortcode];
+
+      // not a recognized shortcode, skip
+      if (!custom_emoji) {
+        i++;
+        continue;
+      }
+
+      // now got a replacee as ':shortcode:'
+      // if you want additional emoji handler, add statements below which set replacement and return true.
+      const filename = autoPlayGif ? custom_emoji.url : custom_emoji.static_url;
+      replacement = document.createElement('img');
+      replacement.setAttribute('draggable', 'false');
+      replacement.setAttribute('class', 'emojione custom-emoji');
+      replacement.setAttribute('alt', shortcode);
+      replacement.setAttribute('title', shortcode);
+      replacement.setAttribute('src', filename);
+      replacement.setAttribute('data-original', custom_emoji.url);
+      replacement.setAttribute('data-static', custom_emoji.static_url);
+    } else { // start of an unicode emoji
+      rend = i + unicode_emoji.length;
+
+      // If the matched character was followed by VS15 (for selecting text presentation), skip it.
+      if (str.codePointAt(rend - 1) !== VS16 && str.codePointAt(rend) === VS15) {
+        i = rend + 1;
+        continue;
+      }
+
+      const { filename, shortCode } = unicodeMapping[unicode_emoji];
+      const title = shortCode ? `:${shortCode}:` : '';
+
+      replacement = document.createElement('img');
+      replacement.setAttribute('draggable', 'false');
+      replacement.setAttribute('class', 'emojione');
+      replacement.setAttribute('alt', unicode_emoji);
+      replacement.setAttribute('title', title);
+      replacement.setAttribute('src', `${assetHost}/emoji/${emojiFilename(filename)}.svg`);
+    }
+
+    // Add the processed-up-to-now string and the emoji replacement
+    fragment.append(document.createTextNode(str.slice(0, i)));
+    fragment.append(replacement);
+    str = str.slice(rend);
+    i = 0;
+  }
+
+  fragment.append(document.createTextNode(str));
+  node.parentElement.replaceChild(fragment, node);
+};
+
+const emojifyNode = (node, customEmojis) => {
+  for (const child of node.childNodes) {
+    switch(child.nodeType) {
+    case Node.TEXT_NODE:
+      emojifyTextNode(child, customEmojis);
+      break;
+    case Node.ELEMENT_NODE:
+      if (!child.classList.contains('invisible'))
+        emojifyNode(child, customEmojis);
+      break;
+    }
+  }
+};
+
+const emojify = (str, customEmojis = {}) => {
+  const wrapper = document.createElement('div');
+  wrapper.innerHTML = str;
+
+  if (!Object.keys(customEmojis).length)
+    customEmojis = null;
+
+  emojifyNode(wrapper, customEmojis);
+
+  return wrapper.innerHTML;
+};
+
+export default emojify;
+
+export const buildCustomEmojis = (customEmojis) => {
+  const emojis = [];
+
+  customEmojis.forEach(emoji => {
+    const shortcode = emoji.get('shortcode');
+    const url       = autoPlayGif ? emoji.get('url') : emoji.get('static_url');
+    const name      = shortcode.replace(':', '');
+
+    emojis.push({
+      id: name,
+      name,
+      short_names: [name],
+      text: '',
+      emoticons: [],
+      keywords: [name],
+      imageUrl: url,
+      custom: true,
+      customCategory: emoji.get('category'),
+    });
+  });
+
+  return emojis;
+};
+
+export const categoriesFromEmojis = customEmojis => customEmojis.reduce((set, emoji) => set.add(emoji.get('category') ? `custom-${emoji.get('category')}` : 'custom'), new Set(['custom']));
diff --git a/app/javascript/flavours/blobfox/features/emoji/emoji_compressed.d.ts b/app/javascript/flavours/blobfox/features/emoji/emoji_compressed.d.ts
new file mode 100644
index 00000000000000..3b627325a09c95
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/emoji/emoji_compressed.d.ts
@@ -0,0 +1,57 @@
+import type { BaseEmoji, EmojiData, NimbleEmojiIndex } from 'emoji-mart';
+import type { Category, Data, Emoji } from 'emoji-mart/dist-es/utils/data';
+
+/*
+ * The 'search' property, although not defined in the [`Emoji`]{@link node_modules/@types/emoji-mart/dist-es/utils/data.d.ts#Emoji} type,
+ * is used in the application.
+ * This could be due to an oversight by the library maintainer.
+ * The `search` property is defined and used [here]{@link node_modules/emoji-mart/dist/utils/data.js#uncompress}.
+ */
+export type Search = string;
+/*
+ * The 'skins' property does not exist in the application data.
+ * This could be a potential area of refactoring or error handling.
+ * The non-existence of 'skins' property is evident at [this location]{@link app/javascript/flavours/blobfox/features/emoji/emoji_compressed.js:121}.
+ */
+type Skins = null;
+
+type Filename = string;
+type UnicodeFilename = string;
+export type FilenameData = [
+  filename: Filename,
+  unicodeFilename?: UnicodeFilename,
+][];
+export type ShortCodesToEmojiDataKey =
+  | EmojiData['id']
+  | BaseEmoji['native']
+  | keyof NimbleEmojiIndex['emojis'];
+
+type SearchData = [
+  BaseEmoji['native'],
+  Emoji['short_names'],
+  Search,
+  Emoji['unified'],
+];
+
+export type ShortCodesToEmojiData = Record<
+  ShortCodesToEmojiDataKey,
+  [FilenameData, SearchData]
+>;
+type EmojisWithoutShortCodes = FilenameData;
+
+type EmojiCompressed = [
+  ShortCodesToEmojiData,
+  Skins,
+  Category[],
+  Data['aliases'],
+  EmojisWithoutShortCodes,
+];
+
+/*
+ * `emoji_compressed.js` uses `babel-plugin-preval`, which makes it difficult to convert to TypeScript.
+ * As a temporary solution, we are allowing a default export here to apply the TypeScript type `EmojiCompressed` to the JS file export.
+ * - {@link app/javascript/flavours/blobfox/features/emoji/emoji_compressed.js}
+ */
+declare const emojiCompressed: EmojiCompressed;
+
+export default emojiCompressed; // eslint-disable-line import/no-default-export
diff --git a/app/javascript/flavours/blobfox/features/emoji/emoji_compressed.js b/app/javascript/flavours/blobfox/features/emoji/emoji_compressed.js
new file mode 100644
index 00000000000000..40cf707a03e51f
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/emoji/emoji_compressed.js
@@ -0,0 +1,135 @@
+/* eslint-disable import/no-commonjs --
+   We need to use CommonJS here due to preval */
+// @preval
+// http://www.unicode.org/Public/emoji/5.0/emoji-test.txt
+// This file contains the compressed version of the emoji data from
+// both emoji_map.json and from emoji-mart's emojiIndex and data objects.
+// It's designed to be emitted in an array format to take up less space
+// over the wire.
+
+const { emojiIndex } = require('emoji-mart');
+let data = require('emoji-mart/data/all.json');
+const { uncompress: emojiMartUncompress } = require('emoji-mart/dist/utils/data');
+
+const emojiMap = require('./emoji_map.json');
+const { unicodeToFilename } = require('./unicode_to_filename');
+const { unicodeToUnifiedName } = require('./unicode_to_unified_name');
+
+
+if(data.compressed) {
+  data = emojiMartUncompress(data);
+}
+
+const emojiMartData = data;
+
+const excluded       = ['®', '©', '™'];
+const skinTones      = ['🏻', '🏼', '🏽', '🏾', '🏿'];
+const shortcodeMap   = {};
+
+const shortCodesToEmojiData = {};
+const emojisWithoutShortCodes = [];
+
+Object.keys(emojiIndex.emojis).forEach(key => {
+  let emoji = emojiIndex.emojis[key];
+
+  // Emojis with skin tone modifiers are stored like this
+  if (Object.prototype.hasOwnProperty.call(emoji, '1')) {
+    emoji = emoji['1'];
+  }
+
+  shortcodeMap[emoji.native] = emoji.id;
+});
+
+const stripModifiers = unicode => {
+  skinTones.forEach(tone => {
+    unicode = unicode.replace(tone, '');
+  });
+
+  return unicode;
+};
+
+Object.keys(emojiMap).forEach(key => {
+  if (excluded.includes(key)) {
+    delete emojiMap[key];
+    return;
+  }
+
+  const normalizedKey = stripModifiers(key);
+  let shortcode       = shortcodeMap[normalizedKey];
+
+  if (!shortcode) {
+    shortcode = shortcodeMap[normalizedKey + '\uFE0F'];
+  }
+
+  const filename = emojiMap[key];
+
+  const filenameData = [key];
+
+  if (unicodeToFilename(key) !== filename) {
+    // filename can't be derived using unicodeToFilename
+    filenameData.push(filename);
+  }
+
+  if (typeof shortcode === 'undefined') {
+    emojisWithoutShortCodes.push(filenameData);
+  } else {
+    if (!Array.isArray(shortCodesToEmojiData[shortcode])) {
+      shortCodesToEmojiData[shortcode] = [[]];
+    }
+
+    shortCodesToEmojiData[shortcode][0].push(filenameData);
+  }
+});
+
+Object.keys(emojiIndex.emojis).forEach(key => {
+  let emoji = emojiIndex.emojis[key];
+
+  // Emojis with skin tone modifiers are stored like this
+  if (Object.prototype.hasOwnProperty.call(emoji, '1')) {
+    emoji = emoji['1'];
+  }
+
+  const { native } = emoji;
+  let { short_names, search, unified } = emojiMartData.emojis[key];
+
+  if (short_names[0] !== key) {
+    throw new Error('The compressor expects the first short_code to be the ' +
+      'key. It may need to be rewritten if the emoji change such that this ' +
+      'is no longer the case.');
+  }
+
+  short_names = short_names.slice(1); // first short name can be inferred from the key
+
+  const searchData = [native, short_names, search];
+
+  if (unicodeToUnifiedName(native) !== unified) {
+    // unified name can't be derived from unicodeToUnifiedName
+    searchData.push(unified);
+  }
+
+  if (!Array.isArray(shortCodesToEmojiData[key])) {
+    shortCodesToEmojiData[key] = [[]];
+  }
+
+  shortCodesToEmojiData[key].push(searchData);
+});
+
+// JSON.parse/stringify is to emulate what @preval is doing and avoid any
+// inconsistent behavior in dev mode
+module.exports = JSON.parse(JSON.stringify([
+  shortCodesToEmojiData,
+  /*
+   * The property `skins` is not found in the current context.
+   * This could potentially lead to issues when interacting with modules or data structures
+   * that expect the presence of `skins` property.
+   * Currently, no definitions or references to `skins` property can be found in:
+   * - {@link node_modules/emoji-mart/dist/utils/data.js}
+   * - {@link node_modules/emoji-mart/data/all.json}
+   * - {@link app/javascript/flavours/blobfox/features/emoji/emoji_compressed.d.ts#Skins}
+   * Future refactorings or updates should consider adding definitions or handling for `skins` property.
+   */
+  emojiMartData.skins,
+  emojiMartData.categories,
+  emojiMartData.aliases,
+  emojisWithoutShortCodes,
+]));
diff --git a/app/javascript/flavours/blobfox/features/emoji/emoji_map.json b/app/javascript/flavours/blobfox/features/emoji/emoji_map.json
new file mode 100644
index 00000000000000..64f6615b7932f4
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/emoji/emoji_map.json
@@ -0,0 +1 @@
+{"😀":"1f600","😃":"1f603","😄":"1f604","😁":"1f601","😆":"1f606","😅":"1f605","🤣":"1f923","😂":"1f602","🙂":"1f642","🙃":"1f643","🫠":"1fae0","😉":"1f609","😊":"1f60a","😇":"1f607","🥰":"1f970","😍":"1f60d","🤩":"1f929","😘":"1f618","😗":"1f617","☺":"263a","😚":"1f61a","😙":"1f619","🥲":"1f972","😋":"1f60b","😛":"1f61b","😜":"1f61c","🤪":"1f92a","😝":"1f61d","🤑":"1f911","🤗":"1f917","🤭":"1f92d","🫢":"1fae2","🫣":"1fae3","🤫":"1f92b","🤔":"1f914","🫡":"1fae1","🤐":"1f910","🤨":"1f928","😐":"1f610","😑":"1f611","😶":"1f636","🫥":"1fae5","😏":"1f60f","😒":"1f612","🙄":"1f644","😬":"1f62c","🤥":"1f925","😌":"1f60c","😔":"1f614","😪":"1f62a","🤤":"1f924","😴":"1f634","😷":"1f637","🤒":"1f912","🤕":"1f915","🤢":"1f922","🤮":"1f92e","🤧":"1f927","🥵":"1f975","🥶":"1f976","🥴":"1f974","😵":"1f635","🤯":"1f92f","🤠":"1f920","🥳":"1f973","🥸":"1f978","😎":"1f60e","🤓":"1f913","🧐":"1f9d0","😕":"1f615","🫤":"1fae4","😟":"1f61f","🙁":"1f641","☹":"2639","😮":"1f62e","😯":"1f62f","😲":"1f632","😳":"1f633","🥺":"1f97a","🥹":"1f979","😦":"1f626","😧":"1f627","😨":"1f628","😰":"1f630","😥":"1f625","😢":"1f622","😭":"1f62d","😱":"1f631","😖":"1f616","😣":"1f623","😞":"1f61e","😓":"1f613","😩":"1f629","😫":"1f62b","🥱":"1f971","😤":"1f624","😡":"1f621","😠":"1f620","🤬":"1f92c","😈":"1f608","👿":"1f47f","💀":"1f480","☠":"2620","💩":"1f4a9","🤡":"1f921","👹":"1f479","👺":"1f47a","👻":"1f47b","👽":"1f47d","👾":"1f47e","🤖":"1f916","😺":"1f63a","😸":"1f638","😹":"1f639","😻":"1f63b","😼":"1f63c","😽":"1f63d","🙀":"1f640","😿":"1f63f","😾":"1f63e","🙈":"1f648","🙉":"1f649","🙊":"1f64a","💋":"1f48b","💌":"1f48c","💘":"1f498","💝":"1f49d","💖":"1f496","💗":"1f497","💓":"1f493","💞":"1f49e","💕":"1f495","💟":"1f49f","❣":"2763","💔":"1f494","❤":"2764","🧡":"1f9e1","💛":"1f49b","💚":"1f49a","💙":"1f499","💜":"1f49c","🤎":"1f90e","🖤":"1f5a4","🤍":"1f90d","💯":"1f4af","💢":"1f4a2","💥":"1f4a5","💫":"1f4ab","💦":"1f4a6","💨":"1f4a8","🕳":"1f573","💣":"1f4a3","💬":"1f4ac","🗨":"1f5e8","🗯":"1f5ef","💭":"1f4ad","💤":"1f4a4","👋":"1f44b","🤚":"1f91a","🖐":"1f590","✋":"270b","🖖":"1f596","🫱":"1faf1","🫲":"1faf2","🫳":"1faf3","🫴":"1faf4","👌":"1f44c","🤌":"1f90c","🤏":"1f90f","✌":"270c","🤞":"1f91e","🫰":"1faf0","🤟":"1f91f","🤘":"1f918","🤙":"1f919","👈":"1f448","👉":"1f449","👆":"1f446","🖕":"1f595","👇":"1f447","☝":"261d","🫵":"1faf5","👍":"1f44d","👎":"1f44e","✊":"270a","👊":"1f44a","🤛":"1f91b","🤜":"1f91c","👏":"1f44f","🙌":"1f64c","🫶":"1faf6","👐":"1f450","🤲":"1f932","🤝":"1f91d","🙏":"1f64f","✍":"270d","💅":"1f485","🤳":"1f933","💪":"1f4aa","🦾":"1f9be","🦿":"1f9bf","🦵":"1f9b5","🦶":"1f9b6","👂":"1f442","🦻":"1f9bb","👃":"1f443","🧠":"1f9e0","🫀":"1fac0","🫁":"1fac1","🦷":"1f9b7","🦴":"1f9b4","👀":"1f440","👁":"1f441","👅":"1f445","👄":"1f444","🫦":"1fae6","👶":"1f476","🧒":"1f9d2","👦":"1f466","👧":"1f467","🧑":"1f9d1","👱":"1f471","👨":"1f468","🧔":"1f9d4","👩":"1f469","🧓":"1f9d3","👴":"1f474","👵":"1f475","🙍":"1f64d","🙎":"1f64e","🙅":"1f645","🙆":"1f646","💁":"1f481","🙋":"1f64b","🧏":"1f9cf","🙇":"1f647","🤦":"1f926","🤷":"1f937","👮":"1f46e","🕵":"1f575","💂":"1f482","🥷":"1f977","👷":"1f477","🫅":"1fac5","🤴":"1f934","👸":"1f478","👳":"1f473","👲":"1f472","🧕":"1f9d5","🤵":"1f935","👰":"1f470","🤰":"1f930","🫃":"1fac3","🫄":"1fac4","🤱":"1f931","👼":"1f47c","🎅":"1f385","🤶":"1f936","🦸":"1f9b8","🦹":"1f9b9","🧙":"1f9d9","🧚":"1f9da","🧛":"1f9db","🧜":"1f9dc","🧝":"1f9dd","🧞":"1f9de","🧟":"1f9df","🧌":"1f9cc","💆":"1f486","💇":"1f487","🚶":"1f6b6","🧍":"1f9cd","🧎":"1f9ce","🏃":"1f3c3","💃":"1f483","🕺":"1f57a","🕴":"1f574","👯":"1f46f","🧖":"1f9d6","🧗":"1f9d7","🤺":"1f93a","🏇":"1f3c7","⛷":"26f7","🏂":"1f3c2","🏌":"1f3cc","🏄":"1f3c4","🚣":"1f6a3","🏊":"1f3ca","⛹":"26f9","🏋":"1f3cb","🚴":"1f6b4","🚵":"1f6b5","🤸":"1f938","🤼":"1f93c","🤽":"1f93d","🤾":"1f93e","🤹":"1f939","🧘":"1f9d8","🛀":"1f6c0","🛌":"1f6cc","👭":"1f46d","👫":"1f46b","👬":"1f46c","💏":"1f48f","💑":"1f491","👪":"1f46a","🗣":"1f5e3","👤":"1f464","👥":"1f465","🫂":"1fac2","👣":"1f463","🏻":"1f463","🏼":"1f463","🏽":"1f463","🏾":"1f463","🏿":"1f463","🦰":"1f463","🦱":"1f463","🦳":"1f463","🦲":"1f463","🐵":"1f435","🐒":"1f412","🦍":"1f98d","🦧":"1f9a7","🐶":"1f436","🐕":"1f415","🦮":"1f9ae","🐩":"1f429","🐺":"1f43a","🦊":"1f98a","🦝":"1f99d","🐱":"1f431","🐈":"1f408","🦁":"1f981","🐯":"1f42f","🐅":"1f405","🐆":"1f406","🐴":"1f434","🐎":"1f40e","🦄":"1f984","🦓":"1f993","🦌":"1f98c","🦬":"1f9ac","🐮":"1f42e","🐂":"1f402","🐃":"1f403","🐄":"1f404","🐷":"1f437","🐖":"1f416","🐗":"1f417","🐽":"1f43d","🐏":"1f40f","🐑":"1f411","🐐":"1f410","🐪":"1f42a","🐫":"1f42b","🦙":"1f999","🦒":"1f992","🐘":"1f418","🦣":"1f9a3","🦏":"1f98f","🦛":"1f99b","🐭":"1f42d","🐁":"1f401","🐀":"1f400","🐹":"1f439","🐰":"1f430","🐇":"1f407","🐿":"1f43f","🦫":"1f9ab","🦔":"1f994","🦇":"1f987","🐻":"1f43b","🐨":"1f428","🐼":"1f43c","🦥":"1f9a5","🦦":"1f9a6","🦨":"1f9a8","🦘":"1f998","🦡":"1f9a1","🐾":"1f43e","🦃":"1f983","🐔":"1f414","🐓":"1f413","🐣":"1f423","🐤":"1f424","🐥":"1f425","🐦":"1f426","🐧":"1f427","🕊":"1f54a","🦅":"1f985","🦆":"1f986","🦢":"1f9a2","🦉":"1f989","🦤":"1f9a4","🪶":"1fab6","🦩":"1f9a9","🦚":"1f99a","🦜":"1f99c","🐸":"1f438","🐊":"1f40a","🐢":"1f422","🦎":"1f98e","🐍":"1f40d","🐲":"1f432","🐉":"1f409","🦕":"1f995","🦖":"1f996","🐳":"1f433","🐋":"1f40b","🐬":"1f42c","🦭":"1f9ad","🐟":"1f41f","🐠":"1f420","🐡":"1f421","🦈":"1f988","🐙":"1f419","🐚":"1f41a","🪸":"1fab8","🐌":"1f40c","🦋":"1f98b","🐛":"1f41b","🐜":"1f41c","🐝":"1f41d","🪲":"1fab2","🐞":"1f41e","🦗":"1f997","🪳":"1fab3","🕷":"1f577","🕸":"1f578","🦂":"1f982","🦟":"1f99f","🪰":"1fab0","🪱":"1fab1","🦠":"1f9a0","💐":"1f490","🌸":"1f338","💮":"1f4ae","🪷":"1fab7","🏵":"1f3f5","🌹":"1f339","🥀":"1f940","🌺":"1f33a","🌻":"1f33b","🌼":"1f33c","🌷":"1f337","🌱":"1f331","🪴":"1fab4","🌲":"1f332","🌳":"1f333","🌴":"1f334","🌵":"1f335","🌾":"1f33e","🌿":"1f33f","☘":"2618","🍀":"1f340","🍁":"1f341","🍂":"1f342","🍃":"1f343","🪹":"1fab9","🪺":"1faba","🍇":"1f347","🍈":"1f348","🍉":"1f349","🍊":"1f34a","🍋":"1f34b","🍌":"1f34c","🍍":"1f34d","🥭":"1f96d","🍎":"1f34e","🍏":"1f34f","🍐":"1f350","🍑":"1f351","🍒":"1f352","🍓":"1f353","🫐":"1fad0","🥝":"1f95d","🍅":"1f345","🫒":"1fad2","🥥":"1f965","🥑":"1f951","🍆":"1f346","🥔":"1f954","🥕":"1f955","🌽":"1f33d","🌶":"1f336","🫑":"1fad1","🥒":"1f952","🥬":"1f96c","🥦":"1f966","🧄":"1f9c4","🧅":"1f9c5","🍄":"1f344","🥜":"1f95c","🫘":"1fad8","🌰":"1f330","🍞":"1f35e","🥐":"1f950","🥖":"1f956","🫓":"1fad3","🥨":"1f968","🥯":"1f96f","🥞":"1f95e","🧇":"1f9c7","🧀":"1f9c0","🍖":"1f356","🍗":"1f357","🥩":"1f969","🥓":"1f953","🍔":"1f354","🍟":"1f35f","🍕":"1f355","🌭":"1f32d","🥪":"1f96a","🌮":"1f32e","🌯":"1f32f","🫔":"1fad4","🥙":"1f959","🧆":"1f9c6","🥚":"1f95a","🍳":"1f373","🥘":"1f958","🍲":"1f372","🫕":"1fad5","🥣":"1f963","🥗":"1f957","🍿":"1f37f","🧈":"1f9c8","🧂":"1f9c2","🥫":"1f96b","🍱":"1f371","🍘":"1f358","🍙":"1f359","🍚":"1f35a","🍛":"1f35b","🍜":"1f35c","🍝":"1f35d","🍠":"1f360","🍢":"1f362","🍣":"1f363","🍤":"1f364","🍥":"1f365","🥮":"1f96e","🍡":"1f361","🥟":"1f95f","🥠":"1f960","🥡":"1f961","🦀":"1f980","🦞":"1f99e","🦐":"1f990","🦑":"1f991","🦪":"1f9aa","🍦":"1f366","🍧":"1f367","🍨":"1f368","🍩":"1f369","🍪":"1f36a","🎂":"1f382","🍰":"1f370","🧁":"1f9c1","🥧":"1f967","🍫":"1f36b","🍬":"1f36c","🍭":"1f36d","🍮":"1f36e","🍯":"1f36f","🍼":"1f37c","🥛":"1f95b","☕":"2615","🫖":"1fad6","🍵":"1f375","🍶":"1f376","🍾":"1f37e","🍷":"1f377","🍸":"1f378","🍹":"1f379","🍺":"1f37a","🍻":"1f37b","🥂":"1f942","🥃":"1f943","🫗":"1fad7","🥤":"1f964","🧋":"1f9cb","🧃":"1f9c3","🧉":"1f9c9","🧊":"1f9ca","🥢":"1f962","🍽":"1f37d","🍴":"1f374","🥄":"1f944","🔪":"1f52a","🫙":"1fad9","🏺":"1f3fa","🌍":"1f30d","🌎":"1f30e","🌏":"1f30f","🌐":"1f310","🗺":"1f5fa","🗾":"1f5fe","🧭":"1f9ed","🏔":"1f3d4","⛰":"26f0","🌋":"1f30b","🗻":"1f5fb","🏕":"1f3d5","🏖":"1f3d6","🏜":"1f3dc","🏝":"1f3dd","🏞":"1f3de","🏟":"1f3df","🏛":"1f3db","🏗":"1f3d7","🧱":"1f9f1","🪨":"1faa8","🪵":"1fab5","🛖":"1f6d6","🏘":"1f3d8","🏚":"1f3da","🏠":"1f3e0","🏡":"1f3e1","🏢":"1f3e2","🏣":"1f3e3","🏤":"1f3e4","🏥":"1f3e5","🏦":"1f3e6","🏨":"1f3e8","🏩":"1f3e9","🏪":"1f3ea","🏫":"1f3eb","🏬":"1f3ec","🏭":"1f3ed","🏯":"1f3ef","🏰":"1f3f0","💒":"1f492","🗼":"1f5fc","🗽":"1f5fd","⛪":"26ea","🕌":"1f54c","🛕":"1f6d5","🕍":"1f54d","⛩":"26e9","🕋":"1f54b","⛲":"26f2","⛺":"26fa","🌁":"1f301","🌃":"1f303","🏙":"1f3d9","🌄":"1f304","🌅":"1f305","🌆":"1f306","🌇":"1f307","🌉":"1f309","♨":"2668","🎠":"1f3a0","🛝":"1f6dd","🎡":"1f3a1","🎢":"1f3a2","💈":"1f488","🎪":"1f3aa","🚂":"1f682","🚃":"1f683","🚄":"1f684","🚅":"1f685","🚆":"1f686","🚇":"1f687","🚈":"1f688","🚉":"1f689","🚊":"1f68a","🚝":"1f69d","🚞":"1f69e","🚋":"1f68b","🚌":"1f68c","🚍":"1f68d","🚎":"1f68e","🚐":"1f690","🚑":"1f691","🚒":"1f692","🚓":"1f693","🚔":"1f694","🚕":"1f695","🚖":"1f696","🚗":"1f697","🚘":"1f698","🚙":"1f699","🛻":"1f6fb","🚚":"1f69a","🚛":"1f69b","🚜":"1f69c","🏎":"1f3ce","🏍":"1f3cd","🛵":"1f6f5","🦽":"1f9bd","🦼":"1f9bc","🛺":"1f6fa","🚲":"1f6b2","🛴":"1f6f4","🛹":"1f6f9","🛼":"1f6fc","🚏":"1f68f","🛣":"1f6e3","🛤":"1f6e4","🛢":"1f6e2","⛽":"26fd","🛞":"1f6de","🚨":"1f6a8","🚥":"1f6a5","🚦":"1f6a6","🛑":"1f6d1","🚧":"1f6a7","⚓":"2693","🛟":"1f6df","⛵":"26f5","🛶":"1f6f6","🚤":"1f6a4","🛳":"1f6f3","⛴":"26f4","🛥":"1f6e5","🚢":"1f6a2","✈":"2708","🛩":"1f6e9","🛫":"1f6eb","🛬":"1f6ec","🪂":"1fa82","💺":"1f4ba","🚁":"1f681","🚟":"1f69f","🚠":"1f6a0","🚡":"1f6a1","🛰":"1f6f0","🚀":"1f680","🛸":"1f6f8","🛎":"1f6ce","🧳":"1f9f3","⌛":"231b","⏳":"23f3","⌚":"231a","⏰":"23f0","⏱":"23f1","⏲":"23f2","🕰":"1f570","🕛":"1f55b","🕧":"1f567","🕐":"1f550","🕜":"1f55c","🕑":"1f551","🕝":"1f55d","🕒":"1f552","🕞":"1f55e","🕓":"1f553","🕟":"1f55f","🕔":"1f554","🕠":"1f560","🕕":"1f555","🕡":"1f561","🕖":"1f556","🕢":"1f562","🕗":"1f557","🕣":"1f563","🕘":"1f558","🕤":"1f564","🕙":"1f559","🕥":"1f565","🕚":"1f55a","🕦":"1f566","🌑":"1f311","🌒":"1f312","🌓":"1f313","🌔":"1f314","🌕":"1f315","🌖":"1f316","🌗":"1f317","🌘":"1f318","🌙":"1f319","🌚":"1f31a","🌛":"1f31b","🌜":"1f31c","🌡":"1f321","☀":"2600","🌝":"1f31d","🌞":"1f31e","🪐":"1fa90","⭐":"2b50","🌟":"1f31f","🌠":"1f320","🌌":"1f30c","☁":"2601","⛅":"26c5","⛈":"26c8","🌤":"1f324","🌥":"1f325","🌦":"1f326","🌧":"1f327","🌨":"1f328","🌩":"1f329","🌪":"1f32a","🌫":"1f32b","🌬":"1f32c","🌀":"1f300","🌈":"1f308","🌂":"1f302","☂":"2602","☔":"2614","⛱":"26f1","⚡":"26a1","❄":"2744","☃":"2603","⛄":"26c4","☄":"2604","🔥":"1f525","💧":"1f4a7","🌊":"1f30a","🎃":"1f383","🎄":"1f384","🎆":"1f386","🎇":"1f387","🧨":"1f9e8","✨":"2728","🎈":"1f388","🎉":"1f389","🎊":"1f38a","🎋":"1f38b","🎍":"1f38d","🎎":"1f38e","🎏":"1f38f","🎐":"1f390","🎑":"1f391","🧧":"1f9e7","🎀":"1f380","🎁":"1f381","🎗":"1f397","🎟":"1f39f","🎫":"1f3ab","🎖":"1f396","🏆":"1f3c6","🏅":"1f3c5","🥇":"1f947","🥈":"1f948","🥉":"1f949","⚽":"26bd","⚾":"26be","🥎":"1f94e","🏀":"1f3c0","🏐":"1f3d0","🏈":"1f3c8","🏉":"1f3c9","🎾":"1f3be","🥏":"1f94f","🎳":"1f3b3","🏏":"1f3cf","🏑":"1f3d1","🏒":"1f3d2","🥍":"1f94d","🏓":"1f3d3","🏸":"1f3f8","🥊":"1f94a","🥋":"1f94b","🥅":"1f945","⛳":"26f3","⛸":"26f8","🎣":"1f3a3","🤿":"1f93f","🎽":"1f3bd","🎿":"1f3bf","🛷":"1f6f7","🥌":"1f94c","🎯":"1f3af","🪀":"1fa80","🪁":"1fa81","🎱":"1f3b1","🔮":"1f52e","🪄":"1fa84","🧿":"1f9ff","🪬":"1faac","🎮":"1f3ae","🕹":"1f579","🎰":"1f3b0","🎲":"1f3b2","🧩":"1f9e9","🧸":"1f9f8","🪅":"1fa85","🪩":"1faa9","🪆":"1fa86","♠":"2660","♥":"2665","♦":"2666","♣":"2663","♟":"265f","🃏":"1f0cf","🀄":"1f004","🎴":"1f3b4","🎭":"1f3ad","🖼":"1f5bc","🎨":"1f3a8","🧵":"1f9f5","🪡":"1faa1","🧶":"1f9f6","🪢":"1faa2","👓":"1f453","🕶":"1f576","🥽":"1f97d","🥼":"1f97c","🦺":"1f9ba","👔":"1f454","👕":"1f455","👖":"1f456","🧣":"1f9e3","🧤":"1f9e4","🧥":"1f9e5","🧦":"1f9e6","👗":"1f457","👘":"1f458","🥻":"1f97b","🩱":"1fa71","🩲":"1fa72","🩳":"1fa73","👙":"1f459","👚":"1f45a","👛":"1f45b","👜":"1f45c","👝":"1f45d","🛍":"1f6cd","🎒":"1f392","🩴":"1fa74","👞":"1f45e","👟":"1f45f","🥾":"1f97e","🥿":"1f97f","👠":"1f460","👡":"1f461","🩰":"1fa70","👢":"1f462","👑":"1f451","👒":"1f452","🎩":"1f3a9","🎓":"1f393","🧢":"1f9e2","🪖":"1fa96","⛑":"26d1","📿":"1f4ff","💄":"1f484","💍":"1f48d","💎":"1f48e","🔇":"1f507","🔈":"1f508","🔉":"1f509","🔊":"1f50a","📢":"1f4e2","📣":"1f4e3","📯":"1f4ef","🔔":"1f514","🔕":"1f515","🎼":"1f3bc","🎵":"1f3b5","🎶":"1f3b6","🎙":"1f399","🎚":"1f39a","🎛":"1f39b","🎤":"1f3a4","🎧":"1f3a7","📻":"1f4fb","🎷":"1f3b7","🪗":"1fa97","🎸":"1f3b8","🎹":"1f3b9","🎺":"1f3ba","🎻":"1f3bb","🪕":"1fa95","🥁":"1f941","🪘":"1fa98","📱":"1f4f1","📲":"1f4f2","☎":"260e","📞":"1f4de","📟":"1f4df","📠":"1f4e0","🔋":"1f50b","🪫":"1faab","🔌":"1f50c","💻":"1f4bb","🖥":"1f5a5","🖨":"1f5a8","⌨":"2328","🖱":"1f5b1","🖲":"1f5b2","💽":"1f4bd","💾":"1f4be","💿":"1f4bf","📀":"1f4c0","🧮":"1f9ee","🎥":"1f3a5","🎞":"1f39e","📽":"1f4fd","🎬":"1f3ac","📺":"1f4fa","📷":"1f4f7","📸":"1f4f8","📹":"1f4f9","📼":"1f4fc","🔍":"1f50d","🔎":"1f50e","🕯":"1f56f","💡":"1f4a1","🔦":"1f526","🏮":"1f3ee","🪔":"1fa94","📔":"1f4d4","📕":"1f4d5","📖":"1f4d6","📗":"1f4d7","📘":"1f4d8","📙":"1f4d9","📚":"1f4da","📓":"1f4d3","📒":"1f4d2","📃":"1f4c3","📜":"1f4dc","📄":"1f4c4","📰":"1f4f0","🗞":"1f5de","📑":"1f4d1","🔖":"1f516","🏷":"1f3f7","💰":"1f4b0","🪙":"1fa99","💴":"1f4b4","💵":"1f4b5","💶":"1f4b6","💷":"1f4b7","💸":"1f4b8","💳":"1f4b3","🧾":"1f9fe","💹":"1f4b9","✉":"2709","📧":"1f4e7","📨":"1f4e8","📩":"1f4e9","📤":"1f4e4","📥":"1f4e5","📦":"1f4e6","📫":"1f4eb","📪":"1f4ea","📬":"1f4ec","📭":"1f4ed","📮":"1f4ee","🗳":"1f5f3","✏":"270f","✒":"2712","🖋":"1f58b","🖊":"1f58a","🖌":"1f58c","🖍":"1f58d","📝":"1f4dd","💼":"1f4bc","📁":"1f4c1","📂":"1f4c2","🗂":"1f5c2","📅":"1f4c5","📆":"1f4c6","🗒":"1f5d2","🗓":"1f5d3","📇":"1f4c7","📈":"1f4c8","📉":"1f4c9","📊":"1f4ca","📋":"1f4cb","📌":"1f4cc","📍":"1f4cd","📎":"1f4ce","🖇":"1f587","📏":"1f4cf","📐":"1f4d0","✂":"2702","🗃":"1f5c3","🗄":"1f5c4","🗑":"1f5d1","🔒":"1f512","🔓":"1f513","🔏":"1f50f","🔐":"1f510","🔑":"1f511","🗝":"1f5dd","🔨":"1f528","🪓":"1fa93","⛏":"26cf","⚒":"2692","🛠":"1f6e0","🗡":"1f5e1","⚔":"2694","🔫":"1f52b","🪃":"1fa83","🏹":"1f3f9","🛡":"1f6e1","🪚":"1fa9a","🔧":"1f527","🪛":"1fa9b","🔩":"1f529","⚙":"2699","🗜":"1f5dc","⚖":"2696","🦯":"1f9af","🔗":"1f517","⛓":"26d3","🪝":"1fa9d","🧰":"1f9f0","🧲":"1f9f2","🪜":"1fa9c","⚗":"2697","🧪":"1f9ea","🧫":"1f9eb","🧬":"1f9ec","🔬":"1f52c","🔭":"1f52d","📡":"1f4e1","💉":"1f489","🩸":"1fa78","💊":"1f48a","🩹":"1fa79","🩼":"1fa7c","🩺":"1fa7a","🩻":"1fa7b","🚪":"1f6aa","🛗":"1f6d7","🪞":"1fa9e","🪟":"1fa9f","🛏":"1f6cf","🛋":"1f6cb","🪑":"1fa91","🚽":"1f6bd","🪠":"1faa0","🚿":"1f6bf","🛁":"1f6c1","🪤":"1faa4","🪒":"1fa92","🧴":"1f9f4","🧷":"1f9f7","🧹":"1f9f9","🧺":"1f9fa","🧻":"1f9fb","🪣":"1faa3","🧼":"1f9fc","🫧":"1fae7","🪥":"1faa5","🧽":"1f9fd","🧯":"1f9ef","🛒":"1f6d2","🚬":"1f6ac","⚰":"26b0","🪦":"1faa6","⚱":"26b1","🗿":"1f5ff","🪧":"1faa7","🪪":"1faaa","🏧":"1f3e7","🚮":"1f6ae","🚰":"1f6b0","♿":"267f","🚹":"1f6b9","🚺":"1f6ba","🚻":"1f6bb","🚼":"1f6bc","🚾":"1f6be","🛂":"1f6c2","🛃":"1f6c3","🛄":"1f6c4","🛅":"1f6c5","⚠":"26a0","🚸":"1f6b8","⛔":"26d4","🚫":"1f6ab","🚳":"1f6b3","🚭":"1f6ad","🚯":"1f6af","🚱":"1f6b1","🚷":"1f6b7","📵":"1f4f5","🔞":"1f51e","☢":"2622","☣":"2623","⬆":"2b06","↗":"2197","➡":"27a1","↘":"2198","⬇":"2b07","↙":"2199","⬅":"2b05","↖":"2196","↕":"2195","↔":"2194","↩":"21a9","↪":"21aa","⤴":"2934","⤵":"2935","🔃":"1f503","🔄":"1f504","🔙":"1f519","🔚":"1f51a","🔛":"1f51b","🔜":"1f51c","🔝":"1f51d","🛐":"1f6d0","⚛":"269b","🕉":"1f549","✡":"2721","☸":"2638","☯":"262f","✝":"271d","☦":"2626","☪":"262a","☮":"262e","🕎":"1f54e","🔯":"1f52f","♈":"2648","♉":"2649","♊":"264a","♋":"264b","♌":"264c","♍":"264d","♎":"264e","♏":"264f","♐":"2650","♑":"2651","♒":"2652","♓":"2653","⛎":"26ce","🔀":"1f500","🔁":"1f501","🔂":"1f502","▶":"25b6","⏩":"23e9","⏭":"23ed","⏯":"23ef","◀":"25c0","⏪":"23ea","⏮":"23ee","🔼":"1f53c","⏫":"23eb","🔽":"1f53d","⏬":"23ec","⏸":"23f8","⏹":"23f9","⏺":"23fa","⏏":"23cf","🎦":"1f3a6","🔅":"1f505","🔆":"1f506","📶":"1f4f6","📳":"1f4f3","📴":"1f4f4","♀":"2640","♂":"2642","⚧":"26a7","✖":"2716","➕":"2795","➖":"2796","➗":"2797","🟰":"1f7f0","♾":"267e","‼":"203c","⁉":"2049","❓":"2753","❔":"2754","❕":"2755","❗":"2757","〰":"3030","💱":"1f4b1","💲":"1f4b2","⚕":"2695","♻":"267b","⚜":"269c","🔱":"1f531","📛":"1f4db","🔰":"1f530","⭕":"2b55","✅":"2705","☑":"2611","✔":"2714","❌":"274c","❎":"274e","➰":"27b0","➿":"27bf","〽":"303d","✳":"2733","✴":"2734","❇":"2747","©":"a9","®":"ae","™":"2122","🔟":"1f51f","🔠":"1f520","🔡":"1f521","🔢":"1f522","🔣":"1f523","🔤":"1f524","🅰":"1f170","🆎":"1f18e","🅱":"1f171","🆑":"1f191","🆒":"1f192","🆓":"1f193","ℹ":"2139","🆔":"1f194","Ⓜ":"24c2","🆕":"1f195","🆖":"1f196","🅾":"1f17e","🆗":"1f197","🅿":"1f17f","🆘":"1f198","🆙":"1f199","🆚":"1f19a","🈁":"1f201","🈂":"1f202","🈷":"1f237","🈶":"1f236","🈯":"1f22f","🉐":"1f250","🈹":"1f239","🈚":"1f21a","🈲":"1f232","🉑":"1f251","🈸":"1f238","🈴":"1f234","🈳":"1f233","㊗":"3297","㊙":"3299","🈺":"1f23a","🈵":"1f235","🔴":"1f534","🟠":"1f7e0","🟡":"1f7e1","🟢":"1f7e2","🔵":"1f535","🟣":"1f7e3","🟤":"1f7e4","⚫":"26ab","⚪":"26aa","🟥":"1f7e5","🟧":"1f7e7","🟨":"1f7e8","🟩":"1f7e9","🟦":"1f7e6","🟪":"1f7ea","🟫":"1f7eb","⬛":"2b1b","⬜":"2b1c","◼":"25fc","◻":"25fb","◾":"25fe","◽":"25fd","▪":"25aa","▫":"25ab","🔶":"1f536","🔷":"1f537","🔸":"1f538","🔹":"1f539","🔺":"1f53a","🔻":"1f53b","💠":"1f4a0","🔘":"1f518","🔳":"1f533","🔲":"1f532","🏁":"1f3c1","🚩":"1f6a9","🎌":"1f38c","🏴":"1f3f4","🏳":"1f3f3","☺️":"263a","☹️":"2639","☠️":"2620","❣️":"2763","❤️":"2764","🕳️":"1f573","🗨️":"1f5e8","🗯️":"1f5ef","👋🏻":"1f44b-1f3fb","👋🏼":"1f44b-1f3fc","👋🏽":"1f44b-1f3fd","👋🏾":"1f44b-1f3fe","👋🏿":"1f44b-1f3ff","🤚🏻":"1f91a-1f3fb","🤚🏼":"1f91a-1f3fc","🤚🏽":"1f91a-1f3fd","🤚🏾":"1f91a-1f3fe","🤚🏿":"1f91a-1f3ff","🖐️":"1f590","🖐🏻":"1f590-1f3fb","🖐🏼":"1f590-1f3fc","🖐🏽":"1f590-1f3fd","🖐🏾":"1f590-1f3fe","🖐🏿":"1f590-1f3ff","✋🏻":"270b-1f3fb","✋🏼":"270b-1f3fc","✋🏽":"270b-1f3fd","✋🏾":"270b-1f3fe","✋🏿":"270b-1f3ff","🖖🏻":"1f596-1f3fb","🖖🏼":"1f596-1f3fc","🖖🏽":"1f596-1f3fd","🖖🏾":"1f596-1f3fe","🖖🏿":"1f596-1f3ff","🫱🏻":"1faf1-1f3fb","🫱🏼":"1faf1-1f3fc","🫱🏽":"1faf1-1f3fd","🫱🏾":"1faf1-1f3fe","🫱🏿":"1faf1-1f3ff","🫲🏻":"1faf2-1f3fb","🫲🏼":"1faf2-1f3fc","🫲🏽":"1faf2-1f3fd","🫲🏾":"1faf2-1f3fe","🫲🏿":"1faf2-1f3ff","🫳🏻":"1faf3-1f3fb","🫳🏼":"1faf3-1f3fc","🫳🏽":"1faf3-1f3fd","🫳🏾":"1faf3-1f3fe","🫳🏿":"1faf3-1f3ff","🫴🏻":"1faf4-1f3fb","🫴🏼":"1faf4-1f3fc","🫴🏽":"1faf4-1f3fd","🫴🏾":"1faf4-1f3fe","🫴🏿":"1faf4-1f3ff","👌🏻":"1f44c-1f3fb","👌🏼":"1f44c-1f3fc","👌🏽":"1f44c-1f3fd","👌🏾":"1f44c-1f3fe","👌🏿":"1f44c-1f3ff","🤌🏻":"1f90c-1f3fb","🤌🏼":"1f90c-1f3fc","🤌🏽":"1f90c-1f3fd","🤌🏾":"1f90c-1f3fe","🤌🏿":"1f90c-1f3ff","🤏🏻":"1f90f-1f3fb","🤏🏼":"1f90f-1f3fc","🤏🏽":"1f90f-1f3fd","🤏🏾":"1f90f-1f3fe","🤏🏿":"1f90f-1f3ff","✌️":"270c","✌🏻":"270c-1f3fb","✌🏼":"270c-1f3fc","✌🏽":"270c-1f3fd","✌🏾":"270c-1f3fe","✌🏿":"270c-1f3ff","🤞🏻":"1f91e-1f3fb","🤞🏼":"1f91e-1f3fc","🤞🏽":"1f91e-1f3fd","🤞🏾":"1f91e-1f3fe","🤞🏿":"1f91e-1f3ff","🫰🏻":"1faf0-1f3fb","🫰🏼":"1faf0-1f3fc","🫰🏽":"1faf0-1f3fd","🫰🏾":"1faf0-1f3fe","🫰🏿":"1faf0-1f3ff","🤟🏻":"1f91f-1f3fb","🤟🏼":"1f91f-1f3fc","🤟🏽":"1f91f-1f3fd","🤟🏾":"1f91f-1f3fe","🤟🏿":"1f91f-1f3ff","🤘🏻":"1f918-1f3fb","🤘🏼":"1f918-1f3fc","🤘🏽":"1f918-1f3fd","🤘🏾":"1f918-1f3fe","🤘🏿":"1f918-1f3ff","🤙🏻":"1f919-1f3fb","🤙🏼":"1f919-1f3fc","🤙🏽":"1f919-1f3fd","🤙🏾":"1f919-1f3fe","🤙🏿":"1f919-1f3ff","👈🏻":"1f448-1f3fb","👈🏼":"1f448-1f3fc","👈🏽":"1f448-1f3fd","👈🏾":"1f448-1f3fe","👈🏿":"1f448-1f3ff","👉🏻":"1f449-1f3fb","👉🏼":"1f449-1f3fc","👉🏽":"1f449-1f3fd","👉🏾":"1f449-1f3fe","👉🏿":"1f449-1f3ff","👆🏻":"1f446-1f3fb","👆🏼":"1f446-1f3fc","👆🏽":"1f446-1f3fd","👆🏾":"1f446-1f3fe","👆🏿":"1f446-1f3ff","🖕🏻":"1f595-1f3fb","🖕🏼":"1f595-1f3fc","🖕🏽":"1f595-1f3fd","🖕🏾":"1f595-1f3fe","🖕🏿":"1f595-1f3ff","👇🏻":"1f447-1f3fb","👇🏼":"1f447-1f3fc","👇🏽":"1f447-1f3fd","👇🏾":"1f447-1f3fe","👇🏿":"1f447-1f3ff","☝️":"261d","☝🏻":"261d-1f3fb","☝🏼":"261d-1f3fc","☝🏽":"261d-1f3fd","☝🏾":"261d-1f3fe","☝🏿":"261d-1f3ff","🫵🏻":"1faf5-1f3fb","🫵🏼":"1faf5-1f3fc","🫵🏽":"1faf5-1f3fd","🫵🏾":"1faf5-1f3fe","🫵🏿":"1faf5-1f3ff","👍🏻":"1f44d-1f3fb","👍🏼":"1f44d-1f3fc","👍🏽":"1f44d-1f3fd","👍🏾":"1f44d-1f3fe","👍🏿":"1f44d-1f3ff","👎🏻":"1f44e-1f3fb","👎🏼":"1f44e-1f3fc","👎🏽":"1f44e-1f3fd","👎🏾":"1f44e-1f3fe","👎🏿":"1f44e-1f3ff","✊🏻":"270a-1f3fb","✊🏼":"270a-1f3fc","✊🏽":"270a-1f3fd","✊🏾":"270a-1f3fe","✊🏿":"270a-1f3ff","👊🏻":"1f44a-1f3fb","👊🏼":"1f44a-1f3fc","👊🏽":"1f44a-1f3fd","👊🏾":"1f44a-1f3fe","👊🏿":"1f44a-1f3ff","🤛🏻":"1f91b-1f3fb","🤛🏼":"1f91b-1f3fc","🤛🏽":"1f91b-1f3fd","🤛🏾":"1f91b-1f3fe","🤛🏿":"1f91b-1f3ff","🤜🏻":"1f91c-1f3fb","🤜🏼":"1f91c-1f3fc","🤜🏽":"1f91c-1f3fd","🤜🏾":"1f91c-1f3fe","🤜🏿":"1f91c-1f3ff","👏🏻":"1f44f-1f3fb","👏🏼":"1f44f-1f3fc","👏🏽":"1f44f-1f3fd","👏🏾":"1f44f-1f3fe","👏🏿":"1f44f-1f3ff","🙌🏻":"1f64c-1f3fb","🙌🏼":"1f64c-1f3fc","🙌🏽":"1f64c-1f3fd","🙌🏾":"1f64c-1f3fe","🙌🏿":"1f64c-1f3ff","🫶🏻":"1faf6-1f3fb","🫶🏼":"1faf6-1f3fc","🫶🏽":"1faf6-1f3fd","🫶🏾":"1faf6-1f3fe","🫶🏿":"1faf6-1f3ff","👐🏻":"1f450-1f3fb","👐🏼":"1f450-1f3fc","👐🏽":"1f450-1f3fd","👐🏾":"1f450-1f3fe","👐🏿":"1f450-1f3ff","🤲🏻":"1f932-1f3fb","🤲🏼":"1f932-1f3fc","🤲🏽":"1f932-1f3fd","🤲🏾":"1f932-1f3fe","🤲🏿":"1f932-1f3ff","🤝🏻":"1f91d-1f3fb","🤝🏼":"1f91d-1f3fc","🤝🏽":"1f91d-1f3fd","🤝🏾":"1f91d-1f3fe","🤝🏿":"1f91d-1f3ff","🙏🏻":"1f64f-1f3fb","🙏🏼":"1f64f-1f3fc","🙏🏽":"1f64f-1f3fd","🙏🏾":"1f64f-1f3fe","🙏🏿":"1f64f-1f3ff","✍️":"270d","✍🏻":"270d-1f3fb","✍🏼":"270d-1f3fc","✍🏽":"270d-1f3fd","✍🏾":"270d-1f3fe","✍🏿":"270d-1f3ff","💅🏻":"1f485-1f3fb","💅🏼":"1f485-1f3fc","💅🏽":"1f485-1f3fd","💅🏾":"1f485-1f3fe","💅🏿":"1f485-1f3ff","🤳🏻":"1f933-1f3fb","🤳🏼":"1f933-1f3fc","🤳🏽":"1f933-1f3fd","🤳🏾":"1f933-1f3fe","🤳🏿":"1f933-1f3ff","💪🏻":"1f4aa-1f3fb","💪🏼":"1f4aa-1f3fc","💪🏽":"1f4aa-1f3fd","💪🏾":"1f4aa-1f3fe","💪🏿":"1f4aa-1f3ff","🦵🏻":"1f9b5-1f3fb","🦵🏼":"1f9b5-1f3fc","🦵🏽":"1f9b5-1f3fd","🦵🏾":"1f9b5-1f3fe","🦵🏿":"1f9b5-1f3ff","🦶🏻":"1f9b6-1f3fb","🦶🏼":"1f9b6-1f3fc","🦶🏽":"1f9b6-1f3fd","🦶🏾":"1f9b6-1f3fe","🦶🏿":"1f9b6-1f3ff","👂🏻":"1f442-1f3fb","👂🏼":"1f442-1f3fc","👂🏽":"1f442-1f3fd","👂🏾":"1f442-1f3fe","👂🏿":"1f442-1f3ff","🦻🏻":"1f9bb-1f3fb","🦻🏼":"1f9bb-1f3fc","🦻🏽":"1f9bb-1f3fd","🦻🏾":"1f9bb-1f3fe","🦻🏿":"1f9bb-1f3ff","👃🏻":"1f443-1f3fb","👃🏼":"1f443-1f3fc","👃🏽":"1f443-1f3fd","👃🏾":"1f443-1f3fe","👃🏿":"1f443-1f3ff","👁️":"1f441","👶🏻":"1f476-1f3fb","👶🏼":"1f476-1f3fc","👶🏽":"1f476-1f3fd","👶🏾":"1f476-1f3fe","👶🏿":"1f476-1f3ff","🧒🏻":"1f9d2-1f3fb","🧒🏼":"1f9d2-1f3fc","🧒🏽":"1f9d2-1f3fd","🧒🏾":"1f9d2-1f3fe","🧒🏿":"1f9d2-1f3ff","👦🏻":"1f466-1f3fb","👦🏼":"1f466-1f3fc","👦🏽":"1f466-1f3fd","👦🏾":"1f466-1f3fe","👦🏿":"1f466-1f3ff","👧🏻":"1f467-1f3fb","👧🏼":"1f467-1f3fc","👧🏽":"1f467-1f3fd","👧🏾":"1f467-1f3fe","👧🏿":"1f467-1f3ff","🧑🏻":"1f9d1-1f3fb","🧑🏼":"1f9d1-1f3fc","🧑🏽":"1f9d1-1f3fd","🧑🏾":"1f9d1-1f3fe","🧑🏿":"1f9d1-1f3ff","👱🏻":"1f471-1f3fb","👱🏼":"1f471-1f3fc","👱🏽":"1f471-1f3fd","👱🏾":"1f471-1f3fe","👱🏿":"1f471-1f3ff","👨🏻":"1f468-1f3fb","👨🏼":"1f468-1f3fc","👨🏽":"1f468-1f3fd","👨🏾":"1f468-1f3fe","👨🏿":"1f468-1f3ff","🧔🏻":"1f9d4-1f3fb","🧔🏼":"1f9d4-1f3fc","🧔🏽":"1f9d4-1f3fd","🧔🏾":"1f9d4-1f3fe","🧔🏿":"1f9d4-1f3ff","👩🏻":"1f469-1f3fb","👩🏼":"1f469-1f3fc","👩🏽":"1f469-1f3fd","👩🏾":"1f469-1f3fe","👩🏿":"1f469-1f3ff","🧓🏻":"1f9d3-1f3fb","🧓🏼":"1f9d3-1f3fc","🧓🏽":"1f9d3-1f3fd","🧓🏾":"1f9d3-1f3fe","🧓🏿":"1f9d3-1f3ff","👴🏻":"1f474-1f3fb","👴🏼":"1f474-1f3fc","👴🏽":"1f474-1f3fd","👴🏾":"1f474-1f3fe","👴🏿":"1f474-1f3ff","👵🏻":"1f475-1f3fb","👵🏼":"1f475-1f3fc","👵🏽":"1f475-1f3fd","👵🏾":"1f475-1f3fe","👵🏿":"1f475-1f3ff","🙍🏻":"1f64d-1f3fb","🙍🏼":"1f64d-1f3fc","🙍🏽":"1f64d-1f3fd","🙍🏾":"1f64d-1f3fe","🙍🏿":"1f64d-1f3ff","🙎🏻":"1f64e-1f3fb","🙎🏼":"1f64e-1f3fc","🙎🏽":"1f64e-1f3fd","🙎🏾":"1f64e-1f3fe","🙎🏿":"1f64e-1f3ff","🙅🏻":"1f645-1f3fb","🙅🏼":"1f645-1f3fc","🙅🏽":"1f645-1f3fd","🙅🏾":"1f645-1f3fe","🙅🏿":"1f645-1f3ff","🙆🏻":"1f646-1f3fb","🙆🏼":"1f646-1f3fc","🙆🏽":"1f646-1f3fd","🙆🏾":"1f646-1f3fe","🙆🏿":"1f646-1f3ff","💁🏻":"1f481-1f3fb","💁🏼":"1f481-1f3fc","💁🏽":"1f481-1f3fd","💁🏾":"1f481-1f3fe","💁🏿":"1f481-1f3ff","🙋🏻":"1f64b-1f3fb","🙋🏼":"1f64b-1f3fc","🙋🏽":"1f64b-1f3fd","🙋🏾":"1f64b-1f3fe","🙋🏿":"1f64b-1f3ff","🧏🏻":"1f9cf-1f3fb","🧏🏼":"1f9cf-1f3fc","🧏🏽":"1f9cf-1f3fd","🧏🏾":"1f9cf-1f3fe","🧏🏿":"1f9cf-1f3ff","🙇🏻":"1f647-1f3fb","🙇🏼":"1f647-1f3fc","🙇🏽":"1f647-1f3fd","🙇🏾":"1f647-1f3fe","🙇🏿":"1f647-1f3ff","🤦🏻":"1f926-1f3fb","🤦🏼":"1f926-1f3fc","🤦🏽":"1f926-1f3fd","🤦🏾":"1f926-1f3fe","🤦🏿":"1f926-1f3ff","🤷🏻":"1f937-1f3fb","🤷🏼":"1f937-1f3fc","🤷🏽":"1f937-1f3fd","🤷🏾":"1f937-1f3fe","🤷🏿":"1f937-1f3ff","👮🏻":"1f46e-1f3fb","👮🏼":"1f46e-1f3fc","👮🏽":"1f46e-1f3fd","👮🏾":"1f46e-1f3fe","👮🏿":"1f46e-1f3ff","🕵️":"1f575","🕵🏻":"1f575-1f3fb","🕵🏼":"1f575-1f3fc","🕵🏽":"1f575-1f3fd","🕵🏾":"1f575-1f3fe","🕵🏿":"1f575-1f3ff","💂🏻":"1f482-1f3fb","💂🏼":"1f482-1f3fc","💂🏽":"1f482-1f3fd","💂🏾":"1f482-1f3fe","💂🏿":"1f482-1f3ff","🥷🏻":"1f977-1f3fb","🥷🏼":"1f977-1f3fc","🥷🏽":"1f977-1f3fd","🥷🏾":"1f977-1f3fe","🥷🏿":"1f977-1f3ff","👷🏻":"1f477-1f3fb","👷🏼":"1f477-1f3fc","👷🏽":"1f477-1f3fd","👷🏾":"1f477-1f3fe","👷🏿":"1f477-1f3ff","🫅🏻":"1fac5-1f3fb","🫅🏼":"1fac5-1f3fc","🫅🏽":"1fac5-1f3fd","🫅🏾":"1fac5-1f3fe","🫅🏿":"1fac5-1f3ff","🤴🏻":"1f934-1f3fb","🤴🏼":"1f934-1f3fc","🤴🏽":"1f934-1f3fd","🤴🏾":"1f934-1f3fe","🤴🏿":"1f934-1f3ff","👸🏻":"1f478-1f3fb","👸🏼":"1f478-1f3fc","👸🏽":"1f478-1f3fd","👸🏾":"1f478-1f3fe","👸🏿":"1f478-1f3ff","👳🏻":"1f473-1f3fb","👳🏼":"1f473-1f3fc","👳🏽":"1f473-1f3fd","👳🏾":"1f473-1f3fe","👳🏿":"1f473-1f3ff","👲🏻":"1f472-1f3fb","👲🏼":"1f472-1f3fc","👲🏽":"1f472-1f3fd","👲🏾":"1f472-1f3fe","👲🏿":"1f472-1f3ff","🧕🏻":"1f9d5-1f3fb","🧕🏼":"1f9d5-1f3fc","🧕🏽":"1f9d5-1f3fd","🧕🏾":"1f9d5-1f3fe","🧕🏿":"1f9d5-1f3ff","🤵🏻":"1f935-1f3fb","🤵🏼":"1f935-1f3fc","🤵🏽":"1f935-1f3fd","🤵🏾":"1f935-1f3fe","🤵🏿":"1f935-1f3ff","👰🏻":"1f470-1f3fb","👰🏼":"1f470-1f3fc","👰🏽":"1f470-1f3fd","👰🏾":"1f470-1f3fe","👰🏿":"1f470-1f3ff","🤰🏻":"1f930-1f3fb","🤰🏼":"1f930-1f3fc","🤰🏽":"1f930-1f3fd","🤰🏾":"1f930-1f3fe","🤰🏿":"1f930-1f3ff","🫃🏻":"1fac3-1f3fb","🫃🏼":"1fac3-1f3fc","🫃🏽":"1fac3-1f3fd","🫃🏾":"1fac3-1f3fe","🫃🏿":"1fac3-1f3ff","🫄🏻":"1fac4-1f3fb","🫄🏼":"1fac4-1f3fc","🫄🏽":"1fac4-1f3fd","🫄🏾":"1fac4-1f3fe","🫄🏿":"1fac4-1f3ff","🤱🏻":"1f931-1f3fb","🤱🏼":"1f931-1f3fc","🤱🏽":"1f931-1f3fd","🤱🏾":"1f931-1f3fe","🤱🏿":"1f931-1f3ff","👼🏻":"1f47c-1f3fb","👼🏼":"1f47c-1f3fc","👼🏽":"1f47c-1f3fd","👼🏾":"1f47c-1f3fe","👼🏿":"1f47c-1f3ff","🎅🏻":"1f385-1f3fb","🎅🏼":"1f385-1f3fc","🎅🏽":"1f385-1f3fd","🎅🏾":"1f385-1f3fe","🎅🏿":"1f385-1f3ff","🤶🏻":"1f936-1f3fb","🤶🏼":"1f936-1f3fc","🤶🏽":"1f936-1f3fd","🤶🏾":"1f936-1f3fe","🤶🏿":"1f936-1f3ff","🦸🏻":"1f9b8-1f3fb","🦸🏼":"1f9b8-1f3fc","🦸🏽":"1f9b8-1f3fd","🦸🏾":"1f9b8-1f3fe","🦸🏿":"1f9b8-1f3ff","🦹🏻":"1f9b9-1f3fb","🦹🏼":"1f9b9-1f3fc","🦹🏽":"1f9b9-1f3fd","🦹🏾":"1f9b9-1f3fe","🦹🏿":"1f9b9-1f3ff","🧙🏻":"1f9d9-1f3fb","🧙🏼":"1f9d9-1f3fc","🧙🏽":"1f9d9-1f3fd","🧙🏾":"1f9d9-1f3fe","🧙🏿":"1f9d9-1f3ff","🧚🏻":"1f9da-1f3fb","🧚🏼":"1f9da-1f3fc","🧚🏽":"1f9da-1f3fd","🧚🏾":"1f9da-1f3fe","🧚🏿":"1f9da-1f3ff","🧛🏻":"1f9db-1f3fb","🧛🏼":"1f9db-1f3fc","🧛🏽":"1f9db-1f3fd","🧛🏾":"1f9db-1f3fe","🧛🏿":"1f9db-1f3ff","🧜🏻":"1f9dc-1f3fb","🧜🏼":"1f9dc-1f3fc","🧜🏽":"1f9dc-1f3fd","🧜🏾":"1f9dc-1f3fe","🧜🏿":"1f9dc-1f3ff","🧝🏻":"1f9dd-1f3fb","🧝🏼":"1f9dd-1f3fc","🧝🏽":"1f9dd-1f3fd","🧝🏾":"1f9dd-1f3fe","🧝🏿":"1f9dd-1f3ff","💆🏻":"1f486-1f3fb","💆🏼":"1f486-1f3fc","💆🏽":"1f486-1f3fd","💆🏾":"1f486-1f3fe","💆🏿":"1f486-1f3ff","💇🏻":"1f487-1f3fb","💇🏼":"1f487-1f3fc","💇🏽":"1f487-1f3fd","💇🏾":"1f487-1f3fe","💇🏿":"1f487-1f3ff","🚶🏻":"1f6b6-1f3fb","🚶🏼":"1f6b6-1f3fc","🚶🏽":"1f6b6-1f3fd","🚶🏾":"1f6b6-1f3fe","🚶🏿":"1f6b6-1f3ff","🧍🏻":"1f9cd-1f3fb","🧍🏼":"1f9cd-1f3fc","🧍🏽":"1f9cd-1f3fd","🧍🏾":"1f9cd-1f3fe","🧍🏿":"1f9cd-1f3ff","🧎🏻":"1f9ce-1f3fb","🧎🏼":"1f9ce-1f3fc","🧎🏽":"1f9ce-1f3fd","🧎🏾":"1f9ce-1f3fe","🧎🏿":"1f9ce-1f3ff","🏃🏻":"1f3c3-1f3fb","🏃🏼":"1f3c3-1f3fc","🏃🏽":"1f3c3-1f3fd","🏃🏾":"1f3c3-1f3fe","🏃🏿":"1f3c3-1f3ff","💃🏻":"1f483-1f3fb","💃🏼":"1f483-1f3fc","💃🏽":"1f483-1f3fd","💃🏾":"1f483-1f3fe","💃🏿":"1f483-1f3ff","🕺🏻":"1f57a-1f3fb","🕺🏼":"1f57a-1f3fc","🕺🏽":"1f57a-1f3fd","🕺🏾":"1f57a-1f3fe","🕺🏿":"1f57a-1f3ff","🕴️":"1f574","🕴🏻":"1f574-1f3fb","🕴🏼":"1f574-1f3fc","🕴🏽":"1f574-1f3fd","🕴🏾":"1f574-1f3fe","🕴🏿":"1f574-1f3ff","🧖🏻":"1f9d6-1f3fb","🧖🏼":"1f9d6-1f3fc","🧖🏽":"1f9d6-1f3fd","🧖🏾":"1f9d6-1f3fe","🧖🏿":"1f9d6-1f3ff","🧗🏻":"1f9d7-1f3fb","🧗🏼":"1f9d7-1f3fc","🧗🏽":"1f9d7-1f3fd","🧗🏾":"1f9d7-1f3fe","🧗🏿":"1f9d7-1f3ff","🏇🏻":"1f3c7-1f3fb","🏇🏼":"1f3c7-1f3fc","🏇🏽":"1f3c7-1f3fd","🏇🏾":"1f3c7-1f3fe","🏇🏿":"1f3c7-1f3ff","⛷️":"26f7","🏂🏻":"1f3c2-1f3fb","🏂🏼":"1f3c2-1f3fc","🏂🏽":"1f3c2-1f3fd","🏂🏾":"1f3c2-1f3fe","🏂🏿":"1f3c2-1f3ff","🏌️":"1f3cc","🏌🏻":"1f3cc-1f3fb","🏌🏼":"1f3cc-1f3fc","🏌🏽":"1f3cc-1f3fd","🏌🏾":"1f3cc-1f3fe","🏌🏿":"1f3cc-1f3ff","🏄🏻":"1f3c4-1f3fb","🏄🏼":"1f3c4-1f3fc","🏄🏽":"1f3c4-1f3fd","🏄🏾":"1f3c4-1f3fe","🏄🏿":"1f3c4-1f3ff","🚣🏻":"1f6a3-1f3fb","🚣🏼":"1f6a3-1f3fc","🚣🏽":"1f6a3-1f3fd","🚣🏾":"1f6a3-1f3fe","🚣🏿":"1f6a3-1f3ff","🏊🏻":"1f3ca-1f3fb","🏊🏼":"1f3ca-1f3fc","🏊🏽":"1f3ca-1f3fd","🏊🏾":"1f3ca-1f3fe","🏊🏿":"1f3ca-1f3ff","⛹️":"26f9","⛹🏻":"26f9-1f3fb","⛹🏼":"26f9-1f3fc","⛹🏽":"26f9-1f3fd","⛹🏾":"26f9-1f3fe","⛹🏿":"26f9-1f3ff","🏋️":"1f3cb","🏋🏻":"1f3cb-1f3fb","🏋🏼":"1f3cb-1f3fc","🏋🏽":"1f3cb-1f3fd","🏋🏾":"1f3cb-1f3fe","🏋🏿":"1f3cb-1f3ff","🚴🏻":"1f6b4-1f3fb","🚴🏼":"1f6b4-1f3fc","🚴🏽":"1f6b4-1f3fd","🚴🏾":"1f6b4-1f3fe","🚴🏿":"1f6b4-1f3ff","🚵🏻":"1f6b5-1f3fb","🚵🏼":"1f6b5-1f3fc","🚵🏽":"1f6b5-1f3fd","🚵🏾":"1f6b5-1f3fe","🚵🏿":"1f6b5-1f3ff","🤸🏻":"1f938-1f3fb","🤸🏼":"1f938-1f3fc","🤸🏽":"1f938-1f3fd","🤸🏾":"1f938-1f3fe","🤸🏿":"1f938-1f3ff","🤽🏻":"1f93d-1f3fb","🤽🏼":"1f93d-1f3fc","🤽🏽":"1f93d-1f3fd","🤽🏾":"1f93d-1f3fe","🤽🏿":"1f93d-1f3ff","🤾🏻":"1f93e-1f3fb","🤾🏼":"1f93e-1f3fc","🤾🏽":"1f93e-1f3fd","🤾🏾":"1f93e-1f3fe","🤾🏿":"1f93e-1f3ff","🤹🏻":"1f939-1f3fb","🤹🏼":"1f939-1f3fc","🤹🏽":"1f939-1f3fd","🤹🏾":"1f939-1f3fe","🤹🏿":"1f939-1f3ff","🧘🏻":"1f9d8-1f3fb","🧘🏼":"1f9d8-1f3fc","🧘🏽":"1f9d8-1f3fd","🧘🏾":"1f9d8-1f3fe","🧘🏿":"1f9d8-1f3ff","🛀🏻":"1f6c0-1f3fb","🛀🏼":"1f6c0-1f3fc","🛀🏽":"1f6c0-1f3fd","🛀🏾":"1f6c0-1f3fe","🛀🏿":"1f6c0-1f3ff","🛌🏻":"1f6cc-1f3fb","🛌🏼":"1f6cc-1f3fc","🛌🏽":"1f6cc-1f3fd","🛌🏾":"1f6cc-1f3fe","🛌🏿":"1f6cc-1f3ff","👭🏻":"1f46d-1f3fb","👭🏼":"1f46d-1f3fc","👭🏽":"1f46d-1f3fd","👭🏾":"1f46d-1f3fe","👭🏿":"1f46d-1f3ff","👫🏻":"1f46b-1f3fb","👫🏼":"1f46b-1f3fc","👫🏽":"1f46b-1f3fd","👫🏾":"1f46b-1f3fe","👫🏿":"1f46b-1f3ff","👬🏻":"1f46c-1f3fb","👬🏼":"1f46c-1f3fc","👬🏽":"1f46c-1f3fd","👬🏾":"1f46c-1f3fe","👬🏿":"1f46c-1f3ff","💏🏻":"1f48f-1f3fb","💏🏼":"1f48f-1f3fc","💏🏽":"1f48f-1f3fd","💏🏾":"1f48f-1f3fe","💏🏿":"1f48f-1f3ff","💑🏻":"1f491-1f3fb","💑🏼":"1f491-1f3fc","💑🏽":"1f491-1f3fd","💑🏾":"1f491-1f3fe","💑🏿":"1f491-1f3ff","🗣️":"1f5e3","🐿️":"1f43f","🕊️":"1f54a","🕷️":"1f577","🕸️":"1f578","🏵️":"1f3f5","☘️":"2618","🌶️":"1f336","🍽️":"1f37d","🗺️":"1f5fa","🏔️":"1f3d4","⛰️":"26f0","🏕️":"1f3d5","🏖️":"1f3d6","🏜️":"1f3dc","🏝️":"1f3dd","🏞️":"1f3de","🏟️":"1f3df","🏛️":"1f3db","🏗️":"1f3d7","🏘️":"1f3d8","🏚️":"1f3da","⛩️":"26e9","🏙️":"1f3d9","♨️":"2668","🏎️":"1f3ce","🏍️":"1f3cd","🛣️":"1f6e3","🛤️":"1f6e4","🛢️":"1f6e2","🛳️":"1f6f3","⛴️":"26f4","🛥️":"1f6e5","✈️":"2708","🛩️":"1f6e9","🛰️":"1f6f0","🛎️":"1f6ce","⏱️":"23f1","⏲️":"23f2","🕰️":"1f570","🌡️":"1f321","☀️":"2600","☁️":"2601","⛈️":"26c8","🌤️":"1f324","🌥️":"1f325","🌦️":"1f326","🌧️":"1f327","🌨️":"1f328","🌩️":"1f329","🌪️":"1f32a","🌫️":"1f32b","🌬️":"1f32c","☂️":"2602","⛱️":"26f1","❄️":"2744","☃️":"2603","☄️":"2604","🎗️":"1f397","🎟️":"1f39f","🎖️":"1f396","⛸️":"26f8","🕹️":"1f579","♠️":"2660","♥️":"2665","♦️":"2666","♣️":"2663","♟️":"265f","🖼️":"1f5bc","🕶️":"1f576","🛍️":"1f6cd","⛑️":"26d1","🎙️":"1f399","🎚️":"1f39a","🎛️":"1f39b","☎️":"260e","🖥️":"1f5a5","🖨️":"1f5a8","⌨️":"2328","🖱️":"1f5b1","🖲️":"1f5b2","🎞️":"1f39e","📽️":"1f4fd","🕯️":"1f56f","🗞️":"1f5de","🏷️":"1f3f7","✉️":"2709","🗳️":"1f5f3","✏️":"270f","✒️":"2712","🖋️":"1f58b","🖊️":"1f58a","🖌️":"1f58c","🖍️":"1f58d","🗂️":"1f5c2","🗒️":"1f5d2","🗓️":"1f5d3","🖇️":"1f587","✂️":"2702","🗃️":"1f5c3","🗄️":"1f5c4","🗑️":"1f5d1","🗝️":"1f5dd","⛏️":"26cf","⚒️":"2692","🛠️":"1f6e0","🗡️":"1f5e1","⚔️":"2694","🛡️":"1f6e1","⚙️":"2699","🗜️":"1f5dc","⚖️":"2696","⛓️":"26d3","⚗️":"2697","🛏️":"1f6cf","🛋️":"1f6cb","⚰️":"26b0","⚱️":"26b1","⚠️":"26a0","☢️":"2622","☣️":"2623","⬆️":"2b06","↗️":"2197","➡️":"27a1","↘️":"2198","⬇️":"2b07","↙️":"2199","⬅️":"2b05","↖️":"2196","↕️":"2195","↔️":"2194","↩️":"21a9","↪️":"21aa","⤴️":"2934","⤵️":"2935","⚛️":"269b","🕉️":"1f549","✡️":"2721","☸️":"2638","☯️":"262f","✝️":"271d","☦️":"2626","☪️":"262a","☮️":"262e","▶️":"25b6","⏭️":"23ed","⏯️":"23ef","◀️":"25c0","⏮️":"23ee","⏸️":"23f8","⏹️":"23f9","⏺️":"23fa","⏏️":"23cf","♀️":"2640","♂️":"2642","⚧️":"26a7","✖️":"2716","♾️":"267e","‼️":"203c","⁉️":"2049","〰️":"3030","⚕️":"2695","♻️":"267b","⚜️":"269c","☑️":"2611","✔️":"2714","〽️":"303d","✳️":"2733","✴️":"2734","❇️":"2747","©️":"a9","®️":"ae","™️":"2122","#⃣":"23-20e3","*⃣":"2a-20e3","0⃣":"30-20e3","1⃣":"31-20e3","2⃣":"32-20e3","3⃣":"33-20e3","4⃣":"34-20e3","5⃣":"35-20e3","6⃣":"36-20e3","7⃣":"37-20e3","8⃣":"38-20e3","9⃣":"39-20e3","🅰️":"1f170","🅱️":"1f171","ℹ️":"2139","Ⓜ️":"24c2","🅾️":"1f17e","🅿️":"1f17f","🈂️":"1f202","🈷️":"1f237","㊗️":"3297","㊙️":"3299","◼️":"25fc","◻️":"25fb","▪️":"25aa","▫️":"25ab","🏳️":"1f3f3","🇦🇨":"1f1e6-1f1e8","🇦🇩":"1f1e6-1f1e9","🇦🇪":"1f1e6-1f1ea","🇦🇫":"1f1e6-1f1eb","🇦🇬":"1f1e6-1f1ec","🇦🇮":"1f1e6-1f1ee","🇦🇱":"1f1e6-1f1f1","🇦🇲":"1f1e6-1f1f2","🇦🇴":"1f1e6-1f1f4","🇦🇶":"1f1e6-1f1f6","🇦🇷":"1f1e6-1f1f7","🇦🇸":"1f1e6-1f1f8","🇦🇹":"1f1e6-1f1f9","🇦🇺":"1f1e6-1f1fa","🇦🇼":"1f1e6-1f1fc","🇦🇽":"1f1e6-1f1fd","🇦🇿":"1f1e6-1f1ff","🇧🇦":"1f1e7-1f1e6","🇧🇧":"1f1e7-1f1e7","🇧🇩":"1f1e7-1f1e9","🇧🇪":"1f1e7-1f1ea","🇧🇫":"1f1e7-1f1eb","🇧🇬":"1f1e7-1f1ec","🇧🇭":"1f1e7-1f1ed","🇧🇮":"1f1e7-1f1ee","🇧🇯":"1f1e7-1f1ef","🇧🇱":"1f1e7-1f1f1","🇧🇲":"1f1e7-1f1f2","🇧🇳":"1f1e7-1f1f3","🇧🇴":"1f1e7-1f1f4","🇧🇶":"1f1e7-1f1f6","🇧🇷":"1f1e7-1f1f7","🇧🇸":"1f1e7-1f1f8","🇧🇹":"1f1e7-1f1f9","🇧🇻":"1f1e7-1f1fb","🇧🇼":"1f1e7-1f1fc","🇧🇾":"1f1e7-1f1fe","🇧🇿":"1f1e7-1f1ff","🇨🇦":"1f1e8-1f1e6","🇨🇨":"1f1e8-1f1e8","🇨🇩":"1f1e8-1f1e9","🇨🇫":"1f1e8-1f1eb","🇨🇬":"1f1e8-1f1ec","🇨🇭":"1f1e8-1f1ed","🇨🇮":"1f1e8-1f1ee","🇨🇰":"1f1e8-1f1f0","🇨🇱":"1f1e8-1f1f1","🇨🇲":"1f1e8-1f1f2","🇨🇳":"1f1e8-1f1f3","🇨🇴":"1f1e8-1f1f4","🇨🇵":"1f1e8-1f1f5","🇨🇷":"1f1e8-1f1f7","🇨🇺":"1f1e8-1f1fa","🇨🇻":"1f1e8-1f1fb","🇨🇼":"1f1e8-1f1fc","🇨🇽":"1f1e8-1f1fd","🇨🇾":"1f1e8-1f1fe","🇨🇿":"1f1e8-1f1ff","🇩🇪":"1f1e9-1f1ea","🇩🇬":"1f1e9-1f1ec","🇩🇯":"1f1e9-1f1ef","🇩🇰":"1f1e9-1f1f0","🇩🇲":"1f1e9-1f1f2","🇩🇴":"1f1e9-1f1f4","🇩🇿":"1f1e9-1f1ff","🇪🇦":"1f1ea-1f1e6","🇪🇨":"1f1ea-1f1e8","🇪🇪":"1f1ea-1f1ea","🇪🇬":"1f1ea-1f1ec","🇪🇭":"1f1ea-1f1ed","🇪🇷":"1f1ea-1f1f7","🇪🇸":"1f1ea-1f1f8","🇪🇹":"1f1ea-1f1f9","🇪🇺":"1f1ea-1f1fa","🇫🇮":"1f1eb-1f1ee","🇫🇯":"1f1eb-1f1ef","🇫🇰":"1f1eb-1f1f0","🇫🇲":"1f1eb-1f1f2","🇫🇴":"1f1eb-1f1f4","🇫🇷":"1f1eb-1f1f7","🇬🇦":"1f1ec-1f1e6","🇬🇧":"1f1ec-1f1e7","🇬🇩":"1f1ec-1f1e9","🇬🇪":"1f1ec-1f1ea","🇬🇫":"1f1ec-1f1eb","🇬🇬":"1f1ec-1f1ec","🇬🇭":"1f1ec-1f1ed","🇬🇮":"1f1ec-1f1ee","🇬🇱":"1f1ec-1f1f1","🇬🇲":"1f1ec-1f1f2","🇬🇳":"1f1ec-1f1f3","🇬🇵":"1f1ec-1f1f5","🇬🇶":"1f1ec-1f1f6","🇬🇷":"1f1ec-1f1f7","🇬🇸":"1f1ec-1f1f8","🇬🇹":"1f1ec-1f1f9","🇬🇺":"1f1ec-1f1fa","🇬🇼":"1f1ec-1f1fc","🇬🇾":"1f1ec-1f1fe","🇭🇰":"1f1ed-1f1f0","🇭🇲":"1f1ed-1f1f2","🇭🇳":"1f1ed-1f1f3","🇭🇷":"1f1ed-1f1f7","🇭🇹":"1f1ed-1f1f9","🇭🇺":"1f1ed-1f1fa","🇮🇨":"1f1ee-1f1e8","🇮🇩":"1f1ee-1f1e9","🇮🇪":"1f1ee-1f1ea","🇮🇱":"1f1ee-1f1f1","🇮🇲":"1f1ee-1f1f2","🇮🇳":"1f1ee-1f1f3","🇮🇴":"1f1ee-1f1f4","🇮🇶":"1f1ee-1f1f6","🇮🇷":"1f1ee-1f1f7","🇮🇸":"1f1ee-1f1f8","🇮🇹":"1f1ee-1f1f9","🇯🇪":"1f1ef-1f1ea","🇯🇲":"1f1ef-1f1f2","🇯🇴":"1f1ef-1f1f4","🇯🇵":"1f1ef-1f1f5","🇰🇪":"1f1f0-1f1ea","🇰🇬":"1f1f0-1f1ec","🇰🇭":"1f1f0-1f1ed","🇰🇮":"1f1f0-1f1ee","🇰🇲":"1f1f0-1f1f2","🇰🇳":"1f1f0-1f1f3","🇰🇵":"1f1f0-1f1f5","🇰🇷":"1f1f0-1f1f7","🇰🇼":"1f1f0-1f1fc","🇰🇾":"1f1f0-1f1fe","🇰🇿":"1f1f0-1f1ff","🇱🇦":"1f1f1-1f1e6","🇱🇧":"1f1f1-1f1e7","🇱🇨":"1f1f1-1f1e8","🇱🇮":"1f1f1-1f1ee","🇱🇰":"1f1f1-1f1f0","🇱🇷":"1f1f1-1f1f7","🇱🇸":"1f1f1-1f1f8","🇱🇹":"1f1f1-1f1f9","🇱🇺":"1f1f1-1f1fa","🇱🇻":"1f1f1-1f1fb","🇱🇾":"1f1f1-1f1fe","🇲🇦":"1f1f2-1f1e6","🇲🇨":"1f1f2-1f1e8","🇲🇩":"1f1f2-1f1e9","🇲🇪":"1f1f2-1f1ea","🇲🇫":"1f1f2-1f1eb","🇲🇬":"1f1f2-1f1ec","🇲🇭":"1f1f2-1f1ed","🇲🇰":"1f1f2-1f1f0","🇲🇱":"1f1f2-1f1f1","🇲🇲":"1f1f2-1f1f2","🇲🇳":"1f1f2-1f1f3","🇲🇴":"1f1f2-1f1f4","🇲🇵":"1f1f2-1f1f5","🇲🇶":"1f1f2-1f1f6","🇲🇷":"1f1f2-1f1f7","🇲🇸":"1f1f2-1f1f8","🇲🇹":"1f1f2-1f1f9","🇲🇺":"1f1f2-1f1fa","🇲🇻":"1f1f2-1f1fb","🇲🇼":"1f1f2-1f1fc","🇲🇽":"1f1f2-1f1fd","🇲🇾":"1f1f2-1f1fe","🇲🇿":"1f1f2-1f1ff","🇳🇦":"1f1f3-1f1e6","🇳🇨":"1f1f3-1f1e8","🇳🇪":"1f1f3-1f1ea","🇳🇫":"1f1f3-1f1eb","🇳🇬":"1f1f3-1f1ec","🇳🇮":"1f1f3-1f1ee","🇳🇱":"1f1f3-1f1f1","🇳🇴":"1f1f3-1f1f4","🇳🇵":"1f1f3-1f1f5","🇳🇷":"1f1f3-1f1f7","🇳🇺":"1f1f3-1f1fa","🇳🇿":"1f1f3-1f1ff","🇴🇲":"1f1f4-1f1f2","🇵🇦":"1f1f5-1f1e6","🇵🇪":"1f1f5-1f1ea","🇵🇫":"1f1f5-1f1eb","🇵🇬":"1f1f5-1f1ec","🇵🇭":"1f1f5-1f1ed","🇵🇰":"1f1f5-1f1f0","🇵🇱":"1f1f5-1f1f1","🇵🇲":"1f1f5-1f1f2","🇵🇳":"1f1f5-1f1f3","🇵🇷":"1f1f5-1f1f7","🇵🇸":"1f1f5-1f1f8","🇵🇹":"1f1f5-1f1f9","🇵🇼":"1f1f5-1f1fc","🇵🇾":"1f1f5-1f1fe","🇶🇦":"1f1f6-1f1e6","🇷🇪":"1f1f7-1f1ea","🇷🇴":"1f1f7-1f1f4","🇷🇸":"1f1f7-1f1f8","🇷🇺":"1f1f7-1f1fa","🇷🇼":"1f1f7-1f1fc","🇸🇦":"1f1f8-1f1e6","🇸🇧":"1f1f8-1f1e7","🇸🇨":"1f1f8-1f1e8","🇸🇩":"1f1f8-1f1e9","🇸🇪":"1f1f8-1f1ea","🇸🇬":"1f1f8-1f1ec","🇸🇭":"1f1f8-1f1ed","🇸🇮":"1f1f8-1f1ee","🇸🇯":"1f1f8-1f1ef","🇸🇰":"1f1f8-1f1f0","🇸🇱":"1f1f8-1f1f1","🇸🇲":"1f1f8-1f1f2","🇸🇳":"1f1f8-1f1f3","🇸🇴":"1f1f8-1f1f4","🇸🇷":"1f1f8-1f1f7","🇸🇸":"1f1f8-1f1f8","🇸🇹":"1f1f8-1f1f9","🇸🇻":"1f1f8-1f1fb","🇸🇽":"1f1f8-1f1fd","🇸🇾":"1f1f8-1f1fe","🇸🇿":"1f1f8-1f1ff","🇹🇦":"1f1f9-1f1e6","🇹🇨":"1f1f9-1f1e8","🇹🇩":"1f1f9-1f1e9","🇹🇫":"1f1f9-1f1eb","🇹🇬":"1f1f9-1f1ec","🇹🇭":"1f1f9-1f1ed","🇹🇯":"1f1f9-1f1ef","🇹🇰":"1f1f9-1f1f0","🇹🇱":"1f1f9-1f1f1","🇹🇲":"1f1f9-1f1f2","🇹🇳":"1f1f9-1f1f3","🇹🇴":"1f1f9-1f1f4","🇹🇷":"1f1f9-1f1f7","🇹🇹":"1f1f9-1f1f9","🇹🇻":"1f1f9-1f1fb","🇹🇼":"1f1f9-1f1fc","🇹🇿":"1f1f9-1f1ff","🇺🇦":"1f1fa-1f1e6","🇺🇬":"1f1fa-1f1ec","🇺🇲":"1f1fa-1f1f2","🇺🇳":"1f1fa-1f1f3","🇺🇸":"1f1fa-1f1f8","🇺🇾":"1f1fa-1f1fe","🇺🇿":"1f1fa-1f1ff","🇻🇦":"1f1fb-1f1e6","🇻🇨":"1f1fb-1f1e8","🇻🇪":"1f1fb-1f1ea","🇻🇬":"1f1fb-1f1ec","🇻🇮":"1f1fb-1f1ee","🇻🇳":"1f1fb-1f1f3","🇻🇺":"1f1fb-1f1fa","🇼🇫":"1f1fc-1f1eb","🇼🇸":"1f1fc-1f1f8","🇽🇰":"1f1fd-1f1f0","🇾🇪":"1f1fe-1f1ea","🇾🇹":"1f1fe-1f1f9","🇿🇦":"1f1ff-1f1e6","🇿🇲":"1f1ff-1f1f2","🇿🇼":"1f1ff-1f1fc","😶‍🌫":"1f636-200d-1f32b-fe0f","😮‍💨":"1f62e-200d-1f4a8","😵‍💫":"1f635-200d-1f4ab","❤‍🔥":"2764-fe0f-200d-1f525","❤‍🩹":"2764-fe0f-200d-1fa79","👁‍🗨":"1f441-200d-1f5e8","🧔‍♂":"1f9d4-200d-2642-fe0f","🧔‍♀":"1f9d4-200d-2640-fe0f","👨‍🦰":"1f468-200d-1f9b0","👨‍🦱":"1f468-200d-1f9b1","👨‍🦳":"1f468-200d-1f9b3","👨‍🦲":"1f468-200d-1f9b2","👩‍🦰":"1f469-200d-1f9b0","🧑‍🦰":"1f9d1-200d-1f9b0","👩‍🦱":"1f469-200d-1f9b1","🧑‍🦱":"1f9d1-200d-1f9b1","👩‍🦳":"1f469-200d-1f9b3","🧑‍🦳":"1f9d1-200d-1f9b3","👩‍🦲":"1f469-200d-1f9b2","🧑‍🦲":"1f9d1-200d-1f9b2","👱‍♀":"1f471-200d-2640-fe0f","👱‍♂":"1f471-200d-2642-fe0f","🙍‍♂":"1f64d-200d-2642-fe0f","🙍‍♀":"1f64d-200d-2640-fe0f","🙎‍♂":"1f64e-200d-2642-fe0f","🙎‍♀":"1f64e-200d-2640-fe0f","🙅‍♂":"1f645-200d-2642-fe0f","🙅‍♀":"1f645-200d-2640-fe0f","🙆‍♂":"1f646-200d-2642-fe0f","🙆‍♀":"1f646-200d-2640-fe0f","💁‍♂":"1f481-200d-2642-fe0f","💁‍♀":"1f481-200d-2640-fe0f","🙋‍♂":"1f64b-200d-2642-fe0f","🙋‍♀":"1f64b-200d-2640-fe0f","🧏‍♂":"1f9cf-200d-2642-fe0f","🧏‍♀":"1f9cf-200d-2640-fe0f","🙇‍♂":"1f647-200d-2642-fe0f","🙇‍♀":"1f647-200d-2640-fe0f","🤦‍♂":"1f926-200d-2642-fe0f","🤦‍♀":"1f926-200d-2640-fe0f","🤷‍♂":"1f937-200d-2642-fe0f","🤷‍♀":"1f937-200d-2640-fe0f","🧑‍⚕":"1f9d1-200d-2695-fe0f","👨‍⚕":"1f468-200d-2695-fe0f","👩‍⚕":"1f469-200d-2695-fe0f","🧑‍🎓":"1f9d1-200d-1f393","👨‍🎓":"1f468-200d-1f393","👩‍🎓":"1f469-200d-1f393","🧑‍🏫":"1f9d1-200d-1f3eb","👨‍🏫":"1f468-200d-1f3eb","👩‍🏫":"1f469-200d-1f3eb","🧑‍⚖":"1f9d1-200d-2696-fe0f","👨‍⚖":"1f468-200d-2696-fe0f","👩‍⚖":"1f469-200d-2696-fe0f","🧑‍🌾":"1f9d1-200d-1f33e","👨‍🌾":"1f468-200d-1f33e","👩‍🌾":"1f469-200d-1f33e","🧑‍🍳":"1f9d1-200d-1f373","👨‍🍳":"1f468-200d-1f373","👩‍🍳":"1f469-200d-1f373","🧑‍🔧":"1f9d1-200d-1f527","👨‍🔧":"1f468-200d-1f527","👩‍🔧":"1f469-200d-1f527","🧑‍🏭":"1f9d1-200d-1f3ed","👨‍🏭":"1f468-200d-1f3ed","👩‍🏭":"1f469-200d-1f3ed","🧑‍💼":"1f9d1-200d-1f4bc","👨‍💼":"1f468-200d-1f4bc","👩‍💼":"1f469-200d-1f4bc","🧑‍🔬":"1f9d1-200d-1f52c","👨‍🔬":"1f468-200d-1f52c","👩‍🔬":"1f469-200d-1f52c","🧑‍💻":"1f9d1-200d-1f4bb","👨‍💻":"1f468-200d-1f4bb","👩‍💻":"1f469-200d-1f4bb","🧑‍🎤":"1f9d1-200d-1f3a4","👨‍🎤":"1f468-200d-1f3a4","👩‍🎤":"1f469-200d-1f3a4","🧑‍🎨":"1f9d1-200d-1f3a8","👨‍🎨":"1f468-200d-1f3a8","👩‍🎨":"1f469-200d-1f3a8","🧑‍✈":"1f9d1-200d-2708-fe0f","👨‍✈":"1f468-200d-2708-fe0f","👩‍✈":"1f469-200d-2708-fe0f","🧑‍🚀":"1f9d1-200d-1f680","👨‍🚀":"1f468-200d-1f680","👩‍🚀":"1f469-200d-1f680","🧑‍🚒":"1f9d1-200d-1f692","👨‍🚒":"1f468-200d-1f692","👩‍🚒":"1f469-200d-1f692","👮‍♂":"1f46e-200d-2642-fe0f","👮‍♀":"1f46e-200d-2640-fe0f","🕵‍♂":"1f575-fe0f-200d-2642-fe0f","🕵‍♀":"1f575-fe0f-200d-2640-fe0f","💂‍♂":"1f482-200d-2642-fe0f","💂‍♀":"1f482-200d-2640-fe0f","👷‍♂":"1f477-200d-2642-fe0f","👷‍♀":"1f477-200d-2640-fe0f","👳‍♂":"1f473-200d-2642-fe0f","👳‍♀":"1f473-200d-2640-fe0f","🤵‍♂":"1f935-200d-2642-fe0f","🤵‍♀":"1f935-200d-2640-fe0f","👰‍♂":"1f470-200d-2642-fe0f","👰‍♀":"1f470-200d-2640-fe0f","👩‍🍼":"1f469-200d-1f37c","👨‍🍼":"1f468-200d-1f37c","🧑‍🍼":"1f9d1-200d-1f37c","🧑‍🎄":"1f9d1-200d-1f384","🦸‍♂":"1f9b8-200d-2642-fe0f","🦸‍♀":"1f9b8-200d-2640-fe0f","🦹‍♂":"1f9b9-200d-2642-fe0f","🦹‍♀":"1f9b9-200d-2640-fe0f","🧙‍♂":"1f9d9-200d-2642-fe0f","🧙‍♀":"1f9d9-200d-2640-fe0f","🧚‍♂":"1f9da-200d-2642-fe0f","🧚‍♀":"1f9da-200d-2640-fe0f","🧛‍♂":"1f9db-200d-2642-fe0f","🧛‍♀":"1f9db-200d-2640-fe0f","🧜‍♂":"1f9dc-200d-2642-fe0f","🧜‍♀":"1f9dc-200d-2640-fe0f","🧝‍♂":"1f9dd-200d-2642-fe0f","🧝‍♀":"1f9dd-200d-2640-fe0f","🧞‍♂":"1f9de-200d-2642-fe0f","🧞‍♀":"1f9de-200d-2640-fe0f","🧟‍♂":"1f9df-200d-2642-fe0f","🧟‍♀":"1f9df-200d-2640-fe0f","💆‍♂":"1f486-200d-2642-fe0f","💆‍♀":"1f486-200d-2640-fe0f","💇‍♂":"1f487-200d-2642-fe0f","💇‍♀":"1f487-200d-2640-fe0f","🚶‍♂":"1f6b6-200d-2642-fe0f","🚶‍♀":"1f6b6-200d-2640-fe0f","🧍‍♂":"1f9cd-200d-2642-fe0f","🧍‍♀":"1f9cd-200d-2640-fe0f","🧎‍♂":"1f9ce-200d-2642-fe0f","🧎‍♀":"1f9ce-200d-2640-fe0f","🧑‍🦯":"1f9d1-200d-1f9af","👨‍🦯":"1f468-200d-1f9af","👩‍🦯":"1f469-200d-1f9af","🧑‍🦼":"1f9d1-200d-1f9bc","👨‍🦼":"1f468-200d-1f9bc","👩‍🦼":"1f469-200d-1f9bc","🧑‍🦽":"1f9d1-200d-1f9bd","👨‍🦽":"1f468-200d-1f9bd","👩‍🦽":"1f469-200d-1f9bd","🏃‍♂":"1f3c3-200d-2642-fe0f","🏃‍♀":"1f3c3-200d-2640-fe0f","👯‍♂":"1f46f-200d-2642-fe0f","👯‍♀":"1f46f-200d-2640-fe0f","🧖‍♂":"1f9d6-200d-2642-fe0f","🧖‍♀":"1f9d6-200d-2640-fe0f","🧗‍♂":"1f9d7-200d-2642-fe0f","🧗‍♀":"1f9d7-200d-2640-fe0f","🏌‍♂":"1f3cc-fe0f-200d-2642-fe0f","🏌‍♀":"1f3cc-fe0f-200d-2640-fe0f","🏄‍♂":"1f3c4-200d-2642-fe0f","🏄‍♀":"1f3c4-200d-2640-fe0f","🚣‍♂":"1f6a3-200d-2642-fe0f","🚣‍♀":"1f6a3-200d-2640-fe0f","🏊‍♂":"1f3ca-200d-2642-fe0f","🏊‍♀":"1f3ca-200d-2640-fe0f","⛹‍♂":"26f9-fe0f-200d-2642-fe0f","⛹‍♀":"26f9-fe0f-200d-2640-fe0f","🏋‍♂":"1f3cb-fe0f-200d-2642-fe0f","🏋‍♀":"1f3cb-fe0f-200d-2640-fe0f","🚴‍♂":"1f6b4-200d-2642-fe0f","🚴‍♀":"1f6b4-200d-2640-fe0f","🚵‍♂":"1f6b5-200d-2642-fe0f","🚵‍♀":"1f6b5-200d-2640-fe0f","🤸‍♂":"1f938-200d-2642-fe0f","🤸‍♀":"1f938-200d-2640-fe0f","🤼‍♂":"1f93c-200d-2642-fe0f","🤼‍♀":"1f93c-200d-2640-fe0f","🤽‍♂":"1f93d-200d-2642-fe0f","🤽‍♀":"1f93d-200d-2640-fe0f","🤾‍♂":"1f93e-200d-2642-fe0f","🤾‍♀":"1f93e-200d-2640-fe0f","🤹‍♂":"1f939-200d-2642-fe0f","🤹‍♀":"1f939-200d-2640-fe0f","🧘‍♂":"1f9d8-200d-2642-fe0f","🧘‍♀":"1f9d8-200d-2640-fe0f","👨‍👦":"1f468-200d-1f466","👨‍👧":"1f468-200d-1f467","👩‍👦":"1f469-200d-1f466","👩‍👧":"1f469-200d-1f467","🐕‍🦺":"1f415-200d-1f9ba","🐈‍⬛":"1f408-200d-2b1b","🐻‍❄":"1f43b-200d-2744-fe0f","#️⃣":"23-20e3","*️⃣":"2a-20e3","0️⃣":"30-20e3","1️⃣":"31-20e3","2️⃣":"32-20e3","3️⃣":"33-20e3","4️⃣":"34-20e3","5️⃣":"35-20e3","6️⃣":"36-20e3","7️⃣":"37-20e3","8️⃣":"38-20e3","9️⃣":"39-20e3","🏳‍🌈":"1f3f3-fe0f-200d-1f308","🏳‍⚧":"1f3f3-fe0f-200d-26a7-fe0f","🏴‍☠":"1f3f4-200d-2620-fe0f","😶‍🌫️":"1f636-200d-1f32b-fe0f","❤️‍🔥":"2764-fe0f-200d-1f525","❤️‍🩹":"2764-fe0f-200d-1fa79","👁‍🗨️":"1f441-200d-1f5e8","👁️‍🗨":"1f441-200d-1f5e8","🧔‍♂️":"1f9d4-200d-2642-fe0f","🧔🏻‍♂":"1f9d4-1f3fb-200d-2642-fe0f","🧔🏼‍♂":"1f9d4-1f3fc-200d-2642-fe0f","🧔🏽‍♂":"1f9d4-1f3fd-200d-2642-fe0f","🧔🏾‍♂":"1f9d4-1f3fe-200d-2642-fe0f","🧔🏿‍♂":"1f9d4-1f3ff-200d-2642-fe0f","🧔‍♀️":"1f9d4-200d-2640-fe0f","🧔🏻‍♀":"1f9d4-1f3fb-200d-2640-fe0f","🧔🏼‍♀":"1f9d4-1f3fc-200d-2640-fe0f","🧔🏽‍♀":"1f9d4-1f3fd-200d-2640-fe0f","🧔🏾‍♀":"1f9d4-1f3fe-200d-2640-fe0f","🧔🏿‍♀":"1f9d4-1f3ff-200d-2640-fe0f","👨🏻‍🦰":"1f468-1f3fb-200d-1f9b0","👨🏼‍🦰":"1f468-1f3fc-200d-1f9b0","👨🏽‍🦰":"1f468-1f3fd-200d-1f9b0","👨🏾‍🦰":"1f468-1f3fe-200d-1f9b0","👨🏿‍🦰":"1f468-1f3ff-200d-1f9b0","👨🏻‍🦱":"1f468-1f3fb-200d-1f9b1","👨🏼‍🦱":"1f468-1f3fc-200d-1f9b1","👨🏽‍🦱":"1f468-1f3fd-200d-1f9b1","👨🏾‍🦱":"1f468-1f3fe-200d-1f9b1","👨🏿‍🦱":"1f468-1f3ff-200d-1f9b1","👨🏻‍🦳":"1f468-1f3fb-200d-1f9b3","👨🏼‍🦳":"1f468-1f3fc-200d-1f9b3","👨🏽‍🦳":"1f468-1f3fd-200d-1f9b3","👨🏾‍🦳":"1f468-1f3fe-200d-1f9b3","👨🏿‍🦳":"1f468-1f3ff-200d-1f9b3","👨🏻‍🦲":"1f468-1f3fb-200d-1f9b2","👨🏼‍🦲":"1f468-1f3fc-200d-1f9b2","👨🏽‍🦲":"1f468-1f3fd-200d-1f9b2","👨🏾‍🦲":"1f468-1f3fe-200d-1f9b2","👨🏿‍🦲":"1f468-1f3ff-200d-1f9b2","👩🏻‍🦰":"1f469-1f3fb-200d-1f9b0","👩🏼‍🦰":"1f469-1f3fc-200d-1f9b0","👩🏽‍🦰":"1f469-1f3fd-200d-1f9b0","👩🏾‍🦰":"1f469-1f3fe-200d-1f9b0","👩🏿‍🦰":"1f469-1f3ff-200d-1f9b0","🧑🏻‍🦰":"1f9d1-1f3fb-200d-1f9b0","🧑🏼‍🦰":"1f9d1-1f3fc-200d-1f9b0","🧑🏽‍🦰":"1f9d1-1f3fd-200d-1f9b0","🧑🏾‍🦰":"1f9d1-1f3fe-200d-1f9b0","🧑🏿‍🦰":"1f9d1-1f3ff-200d-1f9b0","👩🏻‍🦱":"1f469-1f3fb-200d-1f9b1","👩🏼‍🦱":"1f469-1f3fc-200d-1f9b1","👩🏽‍🦱":"1f469-1f3fd-200d-1f9b1","👩🏾‍🦱":"1f469-1f3fe-200d-1f9b1","👩🏿‍🦱":"1f469-1f3ff-200d-1f9b1","🧑🏻‍🦱":"1f9d1-1f3fb-200d-1f9b1","🧑🏼‍🦱":"1f9d1-1f3fc-200d-1f9b1","🧑🏽‍🦱":"1f9d1-1f3fd-200d-1f9b1","🧑🏾‍🦱":"1f9d1-1f3fe-200d-1f9b1","🧑🏿‍🦱":"1f9d1-1f3ff-200d-1f9b1","👩🏻‍🦳":"1f469-1f3fb-200d-1f9b3","👩🏼‍🦳":"1f469-1f3fc-200d-1f9b3","👩🏽‍🦳":"1f469-1f3fd-200d-1f9b3","👩🏾‍🦳":"1f469-1f3fe-200d-1f9b3","👩🏿‍🦳":"1f469-1f3ff-200d-1f9b3","🧑🏻‍🦳":"1f9d1-1f3fb-200d-1f9b3","🧑🏼‍🦳":"1f9d1-1f3fc-200d-1f9b3","🧑🏽‍🦳":"1f9d1-1f3fd-200d-1f9b3","🧑🏾‍🦳":"1f9d1-1f3fe-200d-1f9b3","🧑🏿‍🦳":"1f9d1-1f3ff-200d-1f9b3","👩🏻‍🦲":"1f469-1f3fb-200d-1f9b2","👩🏼‍🦲":"1f469-1f3fc-200d-1f9b2","👩🏽‍🦲":"1f469-1f3fd-200d-1f9b2","👩🏾‍🦲":"1f469-1f3fe-200d-1f9b2","👩🏿‍🦲":"1f469-1f3ff-200d-1f9b2","🧑🏻‍🦲":"1f9d1-1f3fb-200d-1f9b2","🧑🏼‍🦲":"1f9d1-1f3fc-200d-1f9b2","🧑🏽‍🦲":"1f9d1-1f3fd-200d-1f9b2","🧑🏾‍🦲":"1f9d1-1f3fe-200d-1f9b2","🧑🏿‍🦲":"1f9d1-1f3ff-200d-1f9b2","👱‍♀️":"1f471-200d-2640-fe0f","👱🏻‍♀":"1f471-1f3fb-200d-2640-fe0f","👱🏼‍♀":"1f471-1f3fc-200d-2640-fe0f","👱🏽‍♀":"1f471-1f3fd-200d-2640-fe0f","👱🏾‍♀":"1f471-1f3fe-200d-2640-fe0f","👱🏿‍♀":"1f471-1f3ff-200d-2640-fe0f","👱‍♂️":"1f471-200d-2642-fe0f","👱🏻‍♂":"1f471-1f3fb-200d-2642-fe0f","👱🏼‍♂":"1f471-1f3fc-200d-2642-fe0f","👱🏽‍♂":"1f471-1f3fd-200d-2642-fe0f","👱🏾‍♂":"1f471-1f3fe-200d-2642-fe0f","👱🏿‍♂":"1f471-1f3ff-200d-2642-fe0f","🙍‍♂️":"1f64d-200d-2642-fe0f","🙍🏻‍♂":"1f64d-1f3fb-200d-2642-fe0f","🙍🏼‍♂":"1f64d-1f3fc-200d-2642-fe0f","🙍🏽‍♂":"1f64d-1f3fd-200d-2642-fe0f","🙍🏾‍♂":"1f64d-1f3fe-200d-2642-fe0f","🙍🏿‍♂":"1f64d-1f3ff-200d-2642-fe0f","🙍‍♀️":"1f64d-200d-2640-fe0f","🙍🏻‍♀":"1f64d-1f3fb-200d-2640-fe0f","🙍🏼‍♀":"1f64d-1f3fc-200d-2640-fe0f","🙍🏽‍♀":"1f64d-1f3fd-200d-2640-fe0f","🙍🏾‍♀":"1f64d-1f3fe-200d-2640-fe0f","🙍🏿‍♀":"1f64d-1f3ff-200d-2640-fe0f","🙎‍♂️":"1f64e-200d-2642-fe0f","🙎🏻‍♂":"1f64e-1f3fb-200d-2642-fe0f","🙎🏼‍♂":"1f64e-1f3fc-200d-2642-fe0f","🙎🏽‍♂":"1f64e-1f3fd-200d-2642-fe0f","🙎🏾‍♂":"1f64e-1f3fe-200d-2642-fe0f","🙎🏿‍♂":"1f64e-1f3ff-200d-2642-fe0f","🙎‍♀️":"1f64e-200d-2640-fe0f","🙎🏻‍♀":"1f64e-1f3fb-200d-2640-fe0f","🙎🏼‍♀":"1f64e-1f3fc-200d-2640-fe0f","🙎🏽‍♀":"1f64e-1f3fd-200d-2640-fe0f","🙎🏾‍♀":"1f64e-1f3fe-200d-2640-fe0f","🙎🏿‍♀":"1f64e-1f3ff-200d-2640-fe0f","🙅‍♂️":"1f645-200d-2642-fe0f","🙅🏻‍♂":"1f645-1f3fb-200d-2642-fe0f","🙅🏼‍♂":"1f645-1f3fc-200d-2642-fe0f","🙅🏽‍♂":"1f645-1f3fd-200d-2642-fe0f","🙅🏾‍♂":"1f645-1f3fe-200d-2642-fe0f","🙅🏿‍♂":"1f645-1f3ff-200d-2642-fe0f","🙅‍♀️":"1f645-200d-2640-fe0f","🙅🏻‍♀":"1f645-1f3fb-200d-2640-fe0f","🙅🏼‍♀":"1f645-1f3fc-200d-2640-fe0f","🙅🏽‍♀":"1f645-1f3fd-200d-2640-fe0f","🙅🏾‍♀":"1f645-1f3fe-200d-2640-fe0f","🙅🏿‍♀":"1f645-1f3ff-200d-2640-fe0f","🙆‍♂️":"1f646-200d-2642-fe0f","🙆🏻‍♂":"1f646-1f3fb-200d-2642-fe0f","🙆🏼‍♂":"1f646-1f3fc-200d-2642-fe0f","🙆🏽‍♂":"1f646-1f3fd-200d-2642-fe0f","🙆🏾‍♂":"1f646-1f3fe-200d-2642-fe0f","🙆🏿‍♂":"1f646-1f3ff-200d-2642-fe0f","🙆‍♀️":"1f646-200d-2640-fe0f","🙆🏻‍♀":"1f646-1f3fb-200d-2640-fe0f","🙆🏼‍♀":"1f646-1f3fc-200d-2640-fe0f","🙆🏽‍♀":"1f646-1f3fd-200d-2640-fe0f","🙆🏾‍♀":"1f646-1f3fe-200d-2640-fe0f","🙆🏿‍♀":"1f646-1f3ff-200d-2640-fe0f","💁‍♂️":"1f481-200d-2642-fe0f","💁🏻‍♂":"1f481-1f3fb-200d-2642-fe0f","💁🏼‍♂":"1f481-1f3fc-200d-2642-fe0f","💁🏽‍♂":"1f481-1f3fd-200d-2642-fe0f","💁🏾‍♂":"1f481-1f3fe-200d-2642-fe0f","💁🏿‍♂":"1f481-1f3ff-200d-2642-fe0f","💁‍♀️":"1f481-200d-2640-fe0f","💁🏻‍♀":"1f481-1f3fb-200d-2640-fe0f","💁🏼‍♀":"1f481-1f3fc-200d-2640-fe0f","💁🏽‍♀":"1f481-1f3fd-200d-2640-fe0f","💁🏾‍♀":"1f481-1f3fe-200d-2640-fe0f","💁🏿‍♀":"1f481-1f3ff-200d-2640-fe0f","🙋‍♂️":"1f64b-200d-2642-fe0f","🙋🏻‍♂":"1f64b-1f3fb-200d-2642-fe0f","🙋🏼‍♂":"1f64b-1f3fc-200d-2642-fe0f","🙋🏽‍♂":"1f64b-1f3fd-200d-2642-fe0f","🙋🏾‍♂":"1f64b-1f3fe-200d-2642-fe0f","🙋🏿‍♂":"1f64b-1f3ff-200d-2642-fe0f","🙋‍♀️":"1f64b-200d-2640-fe0f","🙋🏻‍♀":"1f64b-1f3fb-200d-2640-fe0f","🙋🏼‍♀":"1f64b-1f3fc-200d-2640-fe0f","🙋🏽‍♀":"1f64b-1f3fd-200d-2640-fe0f","🙋🏾‍♀":"1f64b-1f3fe-200d-2640-fe0f","🙋🏿‍♀":"1f64b-1f3ff-200d-2640-fe0f","🧏‍♂️":"1f9cf-200d-2642-fe0f","🧏🏻‍♂":"1f9cf-1f3fb-200d-2642-fe0f","🧏🏼‍♂":"1f9cf-1f3fc-200d-2642-fe0f","🧏🏽‍♂":"1f9cf-1f3fd-200d-2642-fe0f","🧏🏾‍♂":"1f9cf-1f3fe-200d-2642-fe0f","🧏🏿‍♂":"1f9cf-1f3ff-200d-2642-fe0f","🧏‍♀️":"1f9cf-200d-2640-fe0f","🧏🏻‍♀":"1f9cf-1f3fb-200d-2640-fe0f","🧏🏼‍♀":"1f9cf-1f3fc-200d-2640-fe0f","🧏🏽‍♀":"1f9cf-1f3fd-200d-2640-fe0f","🧏🏾‍♀":"1f9cf-1f3fe-200d-2640-fe0f","🧏🏿‍♀":"1f9cf-1f3ff-200d-2640-fe0f","🙇‍♂️":"1f647-200d-2642-fe0f","🙇🏻‍♂":"1f647-1f3fb-200d-2642-fe0f","🙇🏼‍♂":"1f647-1f3fc-200d-2642-fe0f","🙇🏽‍♂":"1f647-1f3fd-200d-2642-fe0f","🙇🏾‍♂":"1f647-1f3fe-200d-2642-fe0f","🙇🏿‍♂":"1f647-1f3ff-200d-2642-fe0f","🙇‍♀️":"1f647-200d-2640-fe0f","🙇🏻‍♀":"1f647-1f3fb-200d-2640-fe0f","🙇🏼‍♀":"1f647-1f3fc-200d-2640-fe0f","🙇🏽‍♀":"1f647-1f3fd-200d-2640-fe0f","🙇🏾‍♀":"1f647-1f3fe-200d-2640-fe0f","🙇🏿‍♀":"1f647-1f3ff-200d-2640-fe0f","🤦‍♂️":"1f926-200d-2642-fe0f","🤦🏻‍♂":"1f926-1f3fb-200d-2642-fe0f","🤦🏼‍♂":"1f926-1f3fc-200d-2642-fe0f","🤦🏽‍♂":"1f926-1f3fd-200d-2642-fe0f","🤦🏾‍♂":"1f926-1f3fe-200d-2642-fe0f","🤦🏿‍♂":"1f926-1f3ff-200d-2642-fe0f","🤦‍♀️":"1f926-200d-2640-fe0f","🤦🏻‍♀":"1f926-1f3fb-200d-2640-fe0f","🤦🏼‍♀":"1f926-1f3fc-200d-2640-fe0f","🤦🏽‍♀":"1f926-1f3fd-200d-2640-fe0f","🤦🏾‍♀":"1f926-1f3fe-200d-2640-fe0f","🤦🏿‍♀":"1f926-1f3ff-200d-2640-fe0f","🤷‍♂️":"1f937-200d-2642-fe0f","🤷🏻‍♂":"1f937-1f3fb-200d-2642-fe0f","🤷🏼‍♂":"1f937-1f3fc-200d-2642-fe0f","🤷🏽‍♂":"1f937-1f3fd-200d-2642-fe0f","🤷🏾‍♂":"1f937-1f3fe-200d-2642-fe0f","🤷🏿‍♂":"1f937-1f3ff-200d-2642-fe0f","🤷‍♀️":"1f937-200d-2640-fe0f","🤷🏻‍♀":"1f937-1f3fb-200d-2640-fe0f","🤷🏼‍♀":"1f937-1f3fc-200d-2640-fe0f","🤷🏽‍♀":"1f937-1f3fd-200d-2640-fe0f","🤷🏾‍♀":"1f937-1f3fe-200d-2640-fe0f","🤷🏿‍♀":"1f937-1f3ff-200d-2640-fe0f","🧑‍⚕️":"1f9d1-200d-2695-fe0f","🧑🏻‍⚕":"1f9d1-1f3fb-200d-2695-fe0f","🧑🏼‍⚕":"1f9d1-1f3fc-200d-2695-fe0f","🧑🏽‍⚕":"1f9d1-1f3fd-200d-2695-fe0f","🧑🏾‍⚕":"1f9d1-1f3fe-200d-2695-fe0f","🧑🏿‍⚕":"1f9d1-1f3ff-200d-2695-fe0f","👨‍⚕️":"1f468-200d-2695-fe0f","👨🏻‍⚕":"1f468-1f3fb-200d-2695-fe0f","👨🏼‍⚕":"1f468-1f3fc-200d-2695-fe0f","👨🏽‍⚕":"1f468-1f3fd-200d-2695-fe0f","👨🏾‍⚕":"1f468-1f3fe-200d-2695-fe0f","👨🏿‍⚕":"1f468-1f3ff-200d-2695-fe0f","👩‍⚕️":"1f469-200d-2695-fe0f","👩🏻‍⚕":"1f469-1f3fb-200d-2695-fe0f","👩🏼‍⚕":"1f469-1f3fc-200d-2695-fe0f","👩🏽‍⚕":"1f469-1f3fd-200d-2695-fe0f","👩🏾‍⚕":"1f469-1f3fe-200d-2695-fe0f","👩🏿‍⚕":"1f469-1f3ff-200d-2695-fe0f","🧑🏻‍🎓":"1f9d1-1f3fb-200d-1f393","🧑🏼‍🎓":"1f9d1-1f3fc-200d-1f393","🧑🏽‍🎓":"1f9d1-1f3fd-200d-1f393","🧑🏾‍🎓":"1f9d1-1f3fe-200d-1f393","🧑🏿‍🎓":"1f9d1-1f3ff-200d-1f393","👨🏻‍🎓":"1f468-1f3fb-200d-1f393","👨🏼‍🎓":"1f468-1f3fc-200d-1f393","👨🏽‍🎓":"1f468-1f3fd-200d-1f393","👨🏾‍🎓":"1f468-1f3fe-200d-1f393","👨🏿‍🎓":"1f468-1f3ff-200d-1f393","👩🏻‍🎓":"1f469-1f3fb-200d-1f393","👩🏼‍🎓":"1f469-1f3fc-200d-1f393","👩🏽‍🎓":"1f469-1f3fd-200d-1f393","👩🏾‍🎓":"1f469-1f3fe-200d-1f393","👩🏿‍🎓":"1f469-1f3ff-200d-1f393","🧑🏻‍🏫":"1f9d1-1f3fb-200d-1f3eb","🧑🏼‍🏫":"1f9d1-1f3fc-200d-1f3eb","🧑🏽‍🏫":"1f9d1-1f3fd-200d-1f3eb","🧑🏾‍🏫":"1f9d1-1f3fe-200d-1f3eb","🧑🏿‍🏫":"1f9d1-1f3ff-200d-1f3eb","👨🏻‍🏫":"1f468-1f3fb-200d-1f3eb","👨🏼‍🏫":"1f468-1f3fc-200d-1f3eb","👨🏽‍🏫":"1f468-1f3fd-200d-1f3eb","👨🏾‍🏫":"1f468-1f3fe-200d-1f3eb","👨🏿‍🏫":"1f468-1f3ff-200d-1f3eb","👩🏻‍🏫":"1f469-1f3fb-200d-1f3eb","👩🏼‍🏫":"1f469-1f3fc-200d-1f3eb","👩🏽‍🏫":"1f469-1f3fd-200d-1f3eb","👩🏾‍🏫":"1f469-1f3fe-200d-1f3eb","👩🏿‍🏫":"1f469-1f3ff-200d-1f3eb","🧑‍⚖️":"1f9d1-200d-2696-fe0f","🧑🏻‍⚖":"1f9d1-1f3fb-200d-2696-fe0f","🧑🏼‍⚖":"1f9d1-1f3fc-200d-2696-fe0f","🧑🏽‍⚖":"1f9d1-1f3fd-200d-2696-fe0f","🧑🏾‍⚖":"1f9d1-1f3fe-200d-2696-fe0f","🧑🏿‍⚖":"1f9d1-1f3ff-200d-2696-fe0f","👨‍⚖️":"1f468-200d-2696-fe0f","👨🏻‍⚖":"1f468-1f3fb-200d-2696-fe0f","👨🏼‍⚖":"1f468-1f3fc-200d-2696-fe0f","👨🏽‍⚖":"1f468-1f3fd-200d-2696-fe0f","👨🏾‍⚖":"1f468-1f3fe-200d-2696-fe0f","👨🏿‍⚖":"1f468-1f3ff-200d-2696-fe0f","👩‍⚖️":"1f469-200d-2696-fe0f","👩🏻‍⚖":"1f469-1f3fb-200d-2696-fe0f","👩🏼‍⚖":"1f469-1f3fc-200d-2696-fe0f","👩🏽‍⚖":"1f469-1f3fd-200d-2696-fe0f","👩🏾‍⚖":"1f469-1f3fe-200d-2696-fe0f","👩🏿‍⚖":"1f469-1f3ff-200d-2696-fe0f","🧑🏻‍🌾":"1f9d1-1f3fb-200d-1f33e","🧑🏼‍🌾":"1f9d1-1f3fc-200d-1f33e","🧑🏽‍🌾":"1f9d1-1f3fd-200d-1f33e","🧑🏾‍🌾":"1f9d1-1f3fe-200d-1f33e","🧑🏿‍🌾":"1f9d1-1f3ff-200d-1f33e","👨🏻‍🌾":"1f468-1f3fb-200d-1f33e","👨🏼‍🌾":"1f468-1f3fc-200d-1f33e","👨🏽‍🌾":"1f468-1f3fd-200d-1f33e","👨🏾‍🌾":"1f468-1f3fe-200d-1f33e","👨🏿‍🌾":"1f468-1f3ff-200d-1f33e","👩🏻‍🌾":"1f469-1f3fb-200d-1f33e","👩🏼‍🌾":"1f469-1f3fc-200d-1f33e","👩🏽‍🌾":"1f469-1f3fd-200d-1f33e","👩🏾‍🌾":"1f469-1f3fe-200d-1f33e","👩🏿‍🌾":"1f469-1f3ff-200d-1f33e","🧑🏻‍🍳":"1f9d1-1f3fb-200d-1f373","🧑🏼‍🍳":"1f9d1-1f3fc-200d-1f373","🧑🏽‍🍳":"1f9d1-1f3fd-200d-1f373","🧑🏾‍🍳":"1f9d1-1f3fe-200d-1f373","🧑🏿‍🍳":"1f9d1-1f3ff-200d-1f373","👨🏻‍🍳":"1f468-1f3fb-200d-1f373","👨🏼‍🍳":"1f468-1f3fc-200d-1f373","👨🏽‍🍳":"1f468-1f3fd-200d-1f373","👨🏾‍🍳":"1f468-1f3fe-200d-1f373","👨🏿‍🍳":"1f468-1f3ff-200d-1f373","👩🏻‍🍳":"1f469-1f3fb-200d-1f373","👩🏼‍🍳":"1f469-1f3fc-200d-1f373","👩🏽‍🍳":"1f469-1f3fd-200d-1f373","👩🏾‍🍳":"1f469-1f3fe-200d-1f373","👩🏿‍🍳":"1f469-1f3ff-200d-1f373","🧑🏻‍🔧":"1f9d1-1f3fb-200d-1f527","🧑🏼‍🔧":"1f9d1-1f3fc-200d-1f527","🧑🏽‍🔧":"1f9d1-1f3fd-200d-1f527","🧑🏾‍🔧":"1f9d1-1f3fe-200d-1f527","🧑🏿‍🔧":"1f9d1-1f3ff-200d-1f527","👨🏻‍🔧":"1f468-1f3fb-200d-1f527","👨🏼‍🔧":"1f468-1f3fc-200d-1f527","👨🏽‍🔧":"1f468-1f3fd-200d-1f527","👨🏾‍🔧":"1f468-1f3fe-200d-1f527","👨🏿‍🔧":"1f468-1f3ff-200d-1f527","👩🏻‍🔧":"1f469-1f3fb-200d-1f527","👩🏼‍🔧":"1f469-1f3fc-200d-1f527","👩🏽‍🔧":"1f469-1f3fd-200d-1f527","👩🏾‍🔧":"1f469-1f3fe-200d-1f527","👩🏿‍🔧":"1f469-1f3ff-200d-1f527","🧑🏻‍🏭":"1f9d1-1f3fb-200d-1f3ed","🧑🏼‍🏭":"1f9d1-1f3fc-200d-1f3ed","🧑🏽‍🏭":"1f9d1-1f3fd-200d-1f3ed","🧑🏾‍🏭":"1f9d1-1f3fe-200d-1f3ed","🧑🏿‍🏭":"1f9d1-1f3ff-200d-1f3ed","👨🏻‍🏭":"1f468-1f3fb-200d-1f3ed","👨🏼‍🏭":"1f468-1f3fc-200d-1f3ed","👨🏽‍🏭":"1f468-1f3fd-200d-1f3ed","👨🏾‍🏭":"1f468-1f3fe-200d-1f3ed","👨🏿‍🏭":"1f468-1f3ff-200d-1f3ed","👩🏻‍🏭":"1f469-1f3fb-200d-1f3ed","👩🏼‍🏭":"1f469-1f3fc-200d-1f3ed","👩🏽‍🏭":"1f469-1f3fd-200d-1f3ed","👩🏾‍🏭":"1f469-1f3fe-200d-1f3ed","👩🏿‍🏭":"1f469-1f3ff-200d-1f3ed","🧑🏻‍💼":"1f9d1-1f3fb-200d-1f4bc","🧑🏼‍💼":"1f9d1-1f3fc-200d-1f4bc","🧑🏽‍💼":"1f9d1-1f3fd-200d-1f4bc","🧑🏾‍💼":"1f9d1-1f3fe-200d-1f4bc","🧑🏿‍💼":"1f9d1-1f3ff-200d-1f4bc","👨🏻‍💼":"1f468-1f3fb-200d-1f4bc","👨🏼‍💼":"1f468-1f3fc-200d-1f4bc","👨🏽‍💼":"1f468-1f3fd-200d-1f4bc","👨🏾‍💼":"1f468-1f3fe-200d-1f4bc","👨🏿‍💼":"1f468-1f3ff-200d-1f4bc","👩🏻‍💼":"1f469-1f3fb-200d-1f4bc","👩🏼‍💼":"1f469-1f3fc-200d-1f4bc","👩🏽‍💼":"1f469-1f3fd-200d-1f4bc","👩🏾‍💼":"1f469-1f3fe-200d-1f4bc","👩🏿‍💼":"1f469-1f3ff-200d-1f4bc","🧑🏻‍🔬":"1f9d1-1f3fb-200d-1f52c","🧑🏼‍🔬":"1f9d1-1f3fc-200d-1f52c","🧑🏽‍🔬":"1f9d1-1f3fd-200d-1f52c","🧑🏾‍🔬":"1f9d1-1f3fe-200d-1f52c","🧑🏿‍🔬":"1f9d1-1f3ff-200d-1f52c","👨🏻‍🔬":"1f468-1f3fb-200d-1f52c","👨🏼‍🔬":"1f468-1f3fc-200d-1f52c","👨🏽‍🔬":"1f468-1f3fd-200d-1f52c","👨🏾‍🔬":"1f468-1f3fe-200d-1f52c","👨🏿‍🔬":"1f468-1f3ff-200d-1f52c","👩🏻‍🔬":"1f469-1f3fb-200d-1f52c","👩🏼‍🔬":"1f469-1f3fc-200d-1f52c","👩🏽‍🔬":"1f469-1f3fd-200d-1f52c","👩🏾‍🔬":"1f469-1f3fe-200d-1f52c","👩🏿‍🔬":"1f469-1f3ff-200d-1f52c","🧑🏻‍💻":"1f9d1-1f3fb-200d-1f4bb","🧑🏼‍💻":"1f9d1-1f3fc-200d-1f4bb","🧑🏽‍💻":"1f9d1-1f3fd-200d-1f4bb","🧑🏾‍💻":"1f9d1-1f3fe-200d-1f4bb","🧑🏿‍💻":"1f9d1-1f3ff-200d-1f4bb","👨🏻‍💻":"1f468-1f3fb-200d-1f4bb","👨🏼‍💻":"1f468-1f3fc-200d-1f4bb","👨🏽‍💻":"1f468-1f3fd-200d-1f4bb","👨🏾‍💻":"1f468-1f3fe-200d-1f4bb","👨🏿‍💻":"1f468-1f3ff-200d-1f4bb","👩🏻‍💻":"1f469-1f3fb-200d-1f4bb","👩🏼‍💻":"1f469-1f3fc-200d-1f4bb","👩🏽‍💻":"1f469-1f3fd-200d-1f4bb","👩🏾‍💻":"1f469-1f3fe-200d-1f4bb","👩🏿‍💻":"1f469-1f3ff-200d-1f4bb","🧑🏻‍🎤":"1f9d1-1f3fb-200d-1f3a4","🧑🏼‍🎤":"1f9d1-1f3fc-200d-1f3a4","🧑🏽‍🎤":"1f9d1-1f3fd-200d-1f3a4","🧑🏾‍🎤":"1f9d1-1f3fe-200d-1f3a4","🧑🏿‍🎤":"1f9d1-1f3ff-200d-1f3a4","👨🏻‍🎤":"1f468-1f3fb-200d-1f3a4","👨🏼‍🎤":"1f468-1f3fc-200d-1f3a4","👨🏽‍🎤":"1f468-1f3fd-200d-1f3a4","👨🏾‍🎤":"1f468-1f3fe-200d-1f3a4","👨🏿‍🎤":"1f468-1f3ff-200d-1f3a4","👩🏻‍🎤":"1f469-1f3fb-200d-1f3a4","👩🏼‍🎤":"1f469-1f3fc-200d-1f3a4","👩🏽‍🎤":"1f469-1f3fd-200d-1f3a4","👩🏾‍🎤":"1f469-1f3fe-200d-1f3a4","👩🏿‍🎤":"1f469-1f3ff-200d-1f3a4","🧑🏻‍🎨":"1f9d1-1f3fb-200d-1f3a8","🧑🏼‍🎨":"1f9d1-1f3fc-200d-1f3a8","🧑🏽‍🎨":"1f9d1-1f3fd-200d-1f3a8","🧑🏾‍🎨":"1f9d1-1f3fe-200d-1f3a8","🧑🏿‍🎨":"1f9d1-1f3ff-200d-1f3a8","👨🏻‍🎨":"1f468-1f3fb-200d-1f3a8","👨🏼‍🎨":"1f468-1f3fc-200d-1f3a8","👨🏽‍🎨":"1f468-1f3fd-200d-1f3a8","👨🏾‍🎨":"1f468-1f3fe-200d-1f3a8","👨🏿‍🎨":"1f468-1f3ff-200d-1f3a8","👩🏻‍🎨":"1f469-1f3fb-200d-1f3a8","👩🏼‍🎨":"1f469-1f3fc-200d-1f3a8","👩🏽‍🎨":"1f469-1f3fd-200d-1f3a8","👩🏾‍🎨":"1f469-1f3fe-200d-1f3a8","👩🏿‍🎨":"1f469-1f3ff-200d-1f3a8","🧑‍✈️":"1f9d1-200d-2708-fe0f","🧑🏻‍✈":"1f9d1-1f3fb-200d-2708-fe0f","🧑🏼‍✈":"1f9d1-1f3fc-200d-2708-fe0f","🧑🏽‍✈":"1f9d1-1f3fd-200d-2708-fe0f","🧑🏾‍✈":"1f9d1-1f3fe-200d-2708-fe0f","🧑🏿‍✈":"1f9d1-1f3ff-200d-2708-fe0f","👨‍✈️":"1f468-200d-2708-fe0f","👨🏻‍✈":"1f468-1f3fb-200d-2708-fe0f","👨🏼‍✈":"1f468-1f3fc-200d-2708-fe0f","👨🏽‍✈":"1f468-1f3fd-200d-2708-fe0f","👨🏾‍✈":"1f468-1f3fe-200d-2708-fe0f","👨🏿‍✈":"1f468-1f3ff-200d-2708-fe0f","👩‍✈️":"1f469-200d-2708-fe0f","👩🏻‍✈":"1f469-1f3fb-200d-2708-fe0f","👩🏼‍✈":"1f469-1f3fc-200d-2708-fe0f","👩🏽‍✈":"1f469-1f3fd-200d-2708-fe0f","👩🏾‍✈":"1f469-1f3fe-200d-2708-fe0f","👩🏿‍✈":"1f469-1f3ff-200d-2708-fe0f","🧑🏻‍🚀":"1f9d1-1f3fb-200d-1f680","🧑🏼‍🚀":"1f9d1-1f3fc-200d-1f680","🧑🏽‍🚀":"1f9d1-1f3fd-200d-1f680","🧑🏾‍🚀":"1f9d1-1f3fe-200d-1f680","🧑🏿‍🚀":"1f9d1-1f3ff-200d-1f680","👨🏻‍🚀":"1f468-1f3fb-200d-1f680","👨🏼‍🚀":"1f468-1f3fc-200d-1f680","👨🏽‍🚀":"1f468-1f3fd-200d-1f680","👨🏾‍🚀":"1f468-1f3fe-200d-1f680","👨🏿‍🚀":"1f468-1f3ff-200d-1f680","👩🏻‍🚀":"1f469-1f3fb-200d-1f680","👩🏼‍🚀":"1f469-1f3fc-200d-1f680","👩🏽‍🚀":"1f469-1f3fd-200d-1f680","👩🏾‍🚀":"1f469-1f3fe-200d-1f680","👩🏿‍🚀":"1f469-1f3ff-200d-1f680","🧑🏻‍🚒":"1f9d1-1f3fb-200d-1f692","🧑🏼‍🚒":"1f9d1-1f3fc-200d-1f692","🧑🏽‍🚒":"1f9d1-1f3fd-200d-1f692","🧑🏾‍🚒":"1f9d1-1f3fe-200d-1f692","🧑🏿‍🚒":"1f9d1-1f3ff-200d-1f692","👨🏻‍🚒":"1f468-1f3fb-200d-1f692","👨🏼‍🚒":"1f468-1f3fc-200d-1f692","👨🏽‍🚒":"1f468-1f3fd-200d-1f692","👨🏾‍🚒":"1f468-1f3fe-200d-1f692","👨🏿‍🚒":"1f468-1f3ff-200d-1f692","👩🏻‍🚒":"1f469-1f3fb-200d-1f692","👩🏼‍🚒":"1f469-1f3fc-200d-1f692","👩🏽‍🚒":"1f469-1f3fd-200d-1f692","👩🏾‍🚒":"1f469-1f3fe-200d-1f692","👩🏿‍🚒":"1f469-1f3ff-200d-1f692","👮‍♂️":"1f46e-200d-2642-fe0f","👮🏻‍♂":"1f46e-1f3fb-200d-2642-fe0f","👮🏼‍♂":"1f46e-1f3fc-200d-2642-fe0f","👮🏽‍♂":"1f46e-1f3fd-200d-2642-fe0f","👮🏾‍♂":"1f46e-1f3fe-200d-2642-fe0f","👮🏿‍♂":"1f46e-1f3ff-200d-2642-fe0f","👮‍♀️":"1f46e-200d-2640-fe0f","👮🏻‍♀":"1f46e-1f3fb-200d-2640-fe0f","👮🏼‍♀":"1f46e-1f3fc-200d-2640-fe0f","👮🏽‍♀":"1f46e-1f3fd-200d-2640-fe0f","👮🏾‍♀":"1f46e-1f3fe-200d-2640-fe0f","👮🏿‍♀":"1f46e-1f3ff-200d-2640-fe0f","🕵‍♂️":"1f575-fe0f-200d-2642-fe0f","🕵️‍♂":"1f575-fe0f-200d-2642-fe0f","🕵🏻‍♂":"1f575-1f3fb-200d-2642-fe0f","🕵🏼‍♂":"1f575-1f3fc-200d-2642-fe0f","🕵🏽‍♂":"1f575-1f3fd-200d-2642-fe0f","🕵🏾‍♂":"1f575-1f3fe-200d-2642-fe0f","🕵🏿‍♂":"1f575-1f3ff-200d-2642-fe0f","🕵‍♀️":"1f575-fe0f-200d-2640-fe0f","🕵️‍♀":"1f575-fe0f-200d-2640-fe0f","🕵🏻‍♀":"1f575-1f3fb-200d-2640-fe0f","🕵🏼‍♀":"1f575-1f3fc-200d-2640-fe0f","🕵🏽‍♀":"1f575-1f3fd-200d-2640-fe0f","🕵🏾‍♀":"1f575-1f3fe-200d-2640-fe0f","🕵🏿‍♀":"1f575-1f3ff-200d-2640-fe0f","💂‍♂️":"1f482-200d-2642-fe0f","💂🏻‍♂":"1f482-1f3fb-200d-2642-fe0f","💂🏼‍♂":"1f482-1f3fc-200d-2642-fe0f","💂🏽‍♂":"1f482-1f3fd-200d-2642-fe0f","💂🏾‍♂":"1f482-1f3fe-200d-2642-fe0f","💂🏿‍♂":"1f482-1f3ff-200d-2642-fe0f","💂‍♀️":"1f482-200d-2640-fe0f","💂🏻‍♀":"1f482-1f3fb-200d-2640-fe0f","💂🏼‍♀":"1f482-1f3fc-200d-2640-fe0f","💂🏽‍♀":"1f482-1f3fd-200d-2640-fe0f","💂🏾‍♀":"1f482-1f3fe-200d-2640-fe0f","💂🏿‍♀":"1f482-1f3ff-200d-2640-fe0f","👷‍♂️":"1f477-200d-2642-fe0f","👷🏻‍♂":"1f477-1f3fb-200d-2642-fe0f","👷🏼‍♂":"1f477-1f3fc-200d-2642-fe0f","👷🏽‍♂":"1f477-1f3fd-200d-2642-fe0f","👷🏾‍♂":"1f477-1f3fe-200d-2642-fe0f","👷🏿‍♂":"1f477-1f3ff-200d-2642-fe0f","👷‍♀️":"1f477-200d-2640-fe0f","👷🏻‍♀":"1f477-1f3fb-200d-2640-fe0f","👷🏼‍♀":"1f477-1f3fc-200d-2640-fe0f","👷🏽‍♀":"1f477-1f3fd-200d-2640-fe0f","👷🏾‍♀":"1f477-1f3fe-200d-2640-fe0f","👷🏿‍♀":"1f477-1f3ff-200d-2640-fe0f","👳‍♂️":"1f473-200d-2642-fe0f","👳🏻‍♂":"1f473-1f3fb-200d-2642-fe0f","👳🏼‍♂":"1f473-1f3fc-200d-2642-fe0f","👳🏽‍♂":"1f473-1f3fd-200d-2642-fe0f","👳🏾‍♂":"1f473-1f3fe-200d-2642-fe0f","👳🏿‍♂":"1f473-1f3ff-200d-2642-fe0f","👳‍♀️":"1f473-200d-2640-fe0f","👳🏻‍♀":"1f473-1f3fb-200d-2640-fe0f","👳🏼‍♀":"1f473-1f3fc-200d-2640-fe0f","👳🏽‍♀":"1f473-1f3fd-200d-2640-fe0f","👳🏾‍♀":"1f473-1f3fe-200d-2640-fe0f","👳🏿‍♀":"1f473-1f3ff-200d-2640-fe0f","🤵‍♂️":"1f935-200d-2642-fe0f","🤵🏻‍♂":"1f935-1f3fb-200d-2642-fe0f","🤵🏼‍♂":"1f935-1f3fc-200d-2642-fe0f","🤵🏽‍♂":"1f935-1f3fd-200d-2642-fe0f","🤵🏾‍♂":"1f935-1f3fe-200d-2642-fe0f","🤵🏿‍♂":"1f935-1f3ff-200d-2642-fe0f","🤵‍♀️":"1f935-200d-2640-fe0f","🤵🏻‍♀":"1f935-1f3fb-200d-2640-fe0f","🤵🏼‍♀":"1f935-1f3fc-200d-2640-fe0f","🤵🏽‍♀":"1f935-1f3fd-200d-2640-fe0f","🤵🏾‍♀":"1f935-1f3fe-200d-2640-fe0f","🤵🏿‍♀":"1f935-1f3ff-200d-2640-fe0f","👰‍♂️":"1f470-200d-2642-fe0f","👰🏻‍♂":"1f470-1f3fb-200d-2642-fe0f","👰🏼‍♂":"1f470-1f3fc-200d-2642-fe0f","👰🏽‍♂":"1f470-1f3fd-200d-2642-fe0f","👰🏾‍♂":"1f470-1f3fe-200d-2642-fe0f","👰🏿‍♂":"1f470-1f3ff-200d-2642-fe0f","👰‍♀️":"1f470-200d-2640-fe0f","👰🏻‍♀":"1f470-1f3fb-200d-2640-fe0f","👰🏼‍♀":"1f470-1f3fc-200d-2640-fe0f","👰🏽‍♀":"1f470-1f3fd-200d-2640-fe0f","👰🏾‍♀":"1f470-1f3fe-200d-2640-fe0f","👰🏿‍♀":"1f470-1f3ff-200d-2640-fe0f","👩🏻‍🍼":"1f469-1f3fb-200d-1f37c","👩🏼‍🍼":"1f469-1f3fc-200d-1f37c","👩🏽‍🍼":"1f469-1f3fd-200d-1f37c","👩🏾‍🍼":"1f469-1f3fe-200d-1f37c","👩🏿‍🍼":"1f469-1f3ff-200d-1f37c","👨🏻‍🍼":"1f468-1f3fb-200d-1f37c","👨🏼‍🍼":"1f468-1f3fc-200d-1f37c","👨🏽‍🍼":"1f468-1f3fd-200d-1f37c","👨🏾‍🍼":"1f468-1f3fe-200d-1f37c","👨🏿‍🍼":"1f468-1f3ff-200d-1f37c","🧑🏻‍🍼":"1f9d1-1f3fb-200d-1f37c","🧑🏼‍🍼":"1f9d1-1f3fc-200d-1f37c","🧑🏽‍🍼":"1f9d1-1f3fd-200d-1f37c","🧑🏾‍🍼":"1f9d1-1f3fe-200d-1f37c","🧑🏿‍🍼":"1f9d1-1f3ff-200d-1f37c","🧑🏻‍🎄":"1f9d1-1f3fb-200d-1f384","🧑🏼‍🎄":"1f9d1-1f3fc-200d-1f384","🧑🏽‍🎄":"1f9d1-1f3fd-200d-1f384","🧑🏾‍🎄":"1f9d1-1f3fe-200d-1f384","🧑🏿‍🎄":"1f9d1-1f3ff-200d-1f384","🦸‍♂️":"1f9b8-200d-2642-fe0f","🦸🏻‍♂":"1f9b8-1f3fb-200d-2642-fe0f","🦸🏼‍♂":"1f9b8-1f3fc-200d-2642-fe0f","🦸🏽‍♂":"1f9b8-1f3fd-200d-2642-fe0f","🦸🏾‍♂":"1f9b8-1f3fe-200d-2642-fe0f","🦸🏿‍♂":"1f9b8-1f3ff-200d-2642-fe0f","🦸‍♀️":"1f9b8-200d-2640-fe0f","🦸🏻‍♀":"1f9b8-1f3fb-200d-2640-fe0f","🦸🏼‍♀":"1f9b8-1f3fc-200d-2640-fe0f","🦸🏽‍♀":"1f9b8-1f3fd-200d-2640-fe0f","🦸🏾‍♀":"1f9b8-1f3fe-200d-2640-fe0f","🦸🏿‍♀":"1f9b8-1f3ff-200d-2640-fe0f","🦹‍♂️":"1f9b9-200d-2642-fe0f","🦹🏻‍♂":"1f9b9-1f3fb-200d-2642-fe0f","🦹🏼‍♂":"1f9b9-1f3fc-200d-2642-fe0f","🦹🏽‍♂":"1f9b9-1f3fd-200d-2642-fe0f","🦹🏾‍♂":"1f9b9-1f3fe-200d-2642-fe0f","🦹🏿‍♂":"1f9b9-1f3ff-200d-2642-fe0f","🦹‍♀️":"1f9b9-200d-2640-fe0f","🦹🏻‍♀":"1f9b9-1f3fb-200d-2640-fe0f","🦹🏼‍♀":"1f9b9-1f3fc-200d-2640-fe0f","🦹🏽‍♀":"1f9b9-1f3fd-200d-2640-fe0f","🦹🏾‍♀":"1f9b9-1f3fe-200d-2640-fe0f","🦹🏿‍♀":"1f9b9-1f3ff-200d-2640-fe0f","🧙‍♂️":"1f9d9-200d-2642-fe0f","🧙🏻‍♂":"1f9d9-1f3fb-200d-2642-fe0f","🧙🏼‍♂":"1f9d9-1f3fc-200d-2642-fe0f","🧙🏽‍♂":"1f9d9-1f3fd-200d-2642-fe0f","🧙🏾‍♂":"1f9d9-1f3fe-200d-2642-fe0f","🧙🏿‍♂":"1f9d9-1f3ff-200d-2642-fe0f","🧙‍♀️":"1f9d9-200d-2640-fe0f","🧙🏻‍♀":"1f9d9-1f3fb-200d-2640-fe0f","🧙🏼‍♀":"1f9d9-1f3fc-200d-2640-fe0f","🧙🏽‍♀":"1f9d9-1f3fd-200d-2640-fe0f","🧙🏾‍♀":"1f9d9-1f3fe-200d-2640-fe0f","🧙🏿‍♀":"1f9d9-1f3ff-200d-2640-fe0f","🧚‍♂️":"1f9da-200d-2642-fe0f","🧚🏻‍♂":"1f9da-1f3fb-200d-2642-fe0f","🧚🏼‍♂":"1f9da-1f3fc-200d-2642-fe0f","🧚🏽‍♂":"1f9da-1f3fd-200d-2642-fe0f","🧚🏾‍♂":"1f9da-1f3fe-200d-2642-fe0f","🧚🏿‍♂":"1f9da-1f3ff-200d-2642-fe0f","🧚‍♀️":"1f9da-200d-2640-fe0f","🧚🏻‍♀":"1f9da-1f3fb-200d-2640-fe0f","🧚🏼‍♀":"1f9da-1f3fc-200d-2640-fe0f","🧚🏽‍♀":"1f9da-1f3fd-200d-2640-fe0f","🧚🏾‍♀":"1f9da-1f3fe-200d-2640-fe0f","🧚🏿‍♀":"1f9da-1f3ff-200d-2640-fe0f","🧛‍♂️":"1f9db-200d-2642-fe0f","🧛🏻‍♂":"1f9db-1f3fb-200d-2642-fe0f","🧛🏼‍♂":"1f9db-1f3fc-200d-2642-fe0f","🧛🏽‍♂":"1f9db-1f3fd-200d-2642-fe0f","🧛🏾‍♂":"1f9db-1f3fe-200d-2642-fe0f","🧛🏿‍♂":"1f9db-1f3ff-200d-2642-fe0f","🧛‍♀️":"1f9db-200d-2640-fe0f","🧛🏻‍♀":"1f9db-1f3fb-200d-2640-fe0f","🧛🏼‍♀":"1f9db-1f3fc-200d-2640-fe0f","🧛🏽‍♀":"1f9db-1f3fd-200d-2640-fe0f","🧛🏾‍♀":"1f9db-1f3fe-200d-2640-fe0f","🧛🏿‍♀":"1f9db-1f3ff-200d-2640-fe0f","🧜‍♂️":"1f9dc-200d-2642-fe0f","🧜🏻‍♂":"1f9dc-1f3fb-200d-2642-fe0f","🧜🏼‍♂":"1f9dc-1f3fc-200d-2642-fe0f","🧜🏽‍♂":"1f9dc-1f3fd-200d-2642-fe0f","🧜🏾‍♂":"1f9dc-1f3fe-200d-2642-fe0f","🧜🏿‍♂":"1f9dc-1f3ff-200d-2642-fe0f","🧜‍♀️":"1f9dc-200d-2640-fe0f","🧜🏻‍♀":"1f9dc-1f3fb-200d-2640-fe0f","🧜🏼‍♀":"1f9dc-1f3fc-200d-2640-fe0f","🧜🏽‍♀":"1f9dc-1f3fd-200d-2640-fe0f","🧜🏾‍♀":"1f9dc-1f3fe-200d-2640-fe0f","🧜🏿‍♀":"1f9dc-1f3ff-200d-2640-fe0f","🧝‍♂️":"1f9dd-200d-2642-fe0f","🧝🏻‍♂":"1f9dd-1f3fb-200d-2642-fe0f","🧝🏼‍♂":"1f9dd-1f3fc-200d-2642-fe0f","🧝🏽‍♂":"1f9dd-1f3fd-200d-2642-fe0f","🧝🏾‍♂":"1f9dd-1f3fe-200d-2642-fe0f","🧝🏿‍♂":"1f9dd-1f3ff-200d-2642-fe0f","🧝‍♀️":"1f9dd-200d-2640-fe0f","🧝🏻‍♀":"1f9dd-1f3fb-200d-2640-fe0f","🧝🏼‍♀":"1f9dd-1f3fc-200d-2640-fe0f","🧝🏽‍♀":"1f9dd-1f3fd-200d-2640-fe0f","🧝🏾‍♀":"1f9dd-1f3fe-200d-2640-fe0f","🧝🏿‍♀":"1f9dd-1f3ff-200d-2640-fe0f","🧞‍♂️":"1f9de-200d-2642-fe0f","🧞‍♀️":"1f9de-200d-2640-fe0f","🧟‍♂️":"1f9df-200d-2642-fe0f","🧟‍♀️":"1f9df-200d-2640-fe0f","💆‍♂️":"1f486-200d-2642-fe0f","💆🏻‍♂":"1f486-1f3fb-200d-2642-fe0f","💆🏼‍♂":"1f486-1f3fc-200d-2642-fe0f","💆🏽‍♂":"1f486-1f3fd-200d-2642-fe0f","💆🏾‍♂":"1f486-1f3fe-200d-2642-fe0f","💆🏿‍♂":"1f486-1f3ff-200d-2642-fe0f","💆‍♀️":"1f486-200d-2640-fe0f","💆🏻‍♀":"1f486-1f3fb-200d-2640-fe0f","💆🏼‍♀":"1f486-1f3fc-200d-2640-fe0f","💆🏽‍♀":"1f486-1f3fd-200d-2640-fe0f","💆🏾‍♀":"1f486-1f3fe-200d-2640-fe0f","💆🏿‍♀":"1f486-1f3ff-200d-2640-fe0f","💇‍♂️":"1f487-200d-2642-fe0f","💇🏻‍♂":"1f487-1f3fb-200d-2642-fe0f","💇🏼‍♂":"1f487-1f3fc-200d-2642-fe0f","💇🏽‍♂":"1f487-1f3fd-200d-2642-fe0f","💇🏾‍♂":"1f487-1f3fe-200d-2642-fe0f","💇🏿‍♂":"1f487-1f3ff-200d-2642-fe0f","💇‍♀️":"1f487-200d-2640-fe0f","💇🏻‍♀":"1f487-1f3fb-200d-2640-fe0f","💇🏼‍♀":"1f487-1f3fc-200d-2640-fe0f","💇🏽‍♀":"1f487-1f3fd-200d-2640-fe0f","💇🏾‍♀":"1f487-1f3fe-200d-2640-fe0f","💇🏿‍♀":"1f487-1f3ff-200d-2640-fe0f","🚶‍♂️":"1f6b6-200d-2642-fe0f","🚶🏻‍♂":"1f6b6-1f3fb-200d-2642-fe0f","🚶🏼‍♂":"1f6b6-1f3fc-200d-2642-fe0f","🚶🏽‍♂":"1f6b6-1f3fd-200d-2642-fe0f","🚶🏾‍♂":"1f6b6-1f3fe-200d-2642-fe0f","🚶🏿‍♂":"1f6b6-1f3ff-200d-2642-fe0f","🚶‍♀️":"1f6b6-200d-2640-fe0f","🚶🏻‍♀":"1f6b6-1f3fb-200d-2640-fe0f","🚶🏼‍♀":"1f6b6-1f3fc-200d-2640-fe0f","🚶🏽‍♀":"1f6b6-1f3fd-200d-2640-fe0f","🚶🏾‍♀":"1f6b6-1f3fe-200d-2640-fe0f","🚶🏿‍♀":"1f6b6-1f3ff-200d-2640-fe0f","🧍‍♂️":"1f9cd-200d-2642-fe0f","🧍🏻‍♂":"1f9cd-1f3fb-200d-2642-fe0f","🧍🏼‍♂":"1f9cd-1f3fc-200d-2642-fe0f","🧍🏽‍♂":"1f9cd-1f3fd-200d-2642-fe0f","🧍🏾‍♂":"1f9cd-1f3fe-200d-2642-fe0f","🧍🏿‍♂":"1f9cd-1f3ff-200d-2642-fe0f","🧍‍♀️":"1f9cd-200d-2640-fe0f","🧍🏻‍♀":"1f9cd-1f3fb-200d-2640-fe0f","🧍🏼‍♀":"1f9cd-1f3fc-200d-2640-fe0f","🧍🏽‍♀":"1f9cd-1f3fd-200d-2640-fe0f","🧍🏾‍♀":"1f9cd-1f3fe-200d-2640-fe0f","🧍🏿‍♀":"1f9cd-1f3ff-200d-2640-fe0f","🧎‍♂️":"1f9ce-200d-2642-fe0f","🧎🏻‍♂":"1f9ce-1f3fb-200d-2642-fe0f","🧎🏼‍♂":"1f9ce-1f3fc-200d-2642-fe0f","🧎🏽‍♂":"1f9ce-1f3fd-200d-2642-fe0f","🧎🏾‍♂":"1f9ce-1f3fe-200d-2642-fe0f","🧎🏿‍♂":"1f9ce-1f3ff-200d-2642-fe0f","🧎‍♀️":"1f9ce-200d-2640-fe0f","🧎🏻‍♀":"1f9ce-1f3fb-200d-2640-fe0f","🧎🏼‍♀":"1f9ce-1f3fc-200d-2640-fe0f","🧎🏽‍♀":"1f9ce-1f3fd-200d-2640-fe0f","🧎🏾‍♀":"1f9ce-1f3fe-200d-2640-fe0f","🧎🏿‍♀":"1f9ce-1f3ff-200d-2640-fe0f","🧑🏻‍🦯":"1f9d1-1f3fb-200d-1f9af","🧑🏼‍🦯":"1f9d1-1f3fc-200d-1f9af","🧑🏽‍🦯":"1f9d1-1f3fd-200d-1f9af","🧑🏾‍🦯":"1f9d1-1f3fe-200d-1f9af","🧑🏿‍🦯":"1f9d1-1f3ff-200d-1f9af","👨🏻‍🦯":"1f468-1f3fb-200d-1f9af","👨🏼‍🦯":"1f468-1f3fc-200d-1f9af","👨🏽‍🦯":"1f468-1f3fd-200d-1f9af","👨🏾‍🦯":"1f468-1f3fe-200d-1f9af","👨🏿‍🦯":"1f468-1f3ff-200d-1f9af","👩🏻‍🦯":"1f469-1f3fb-200d-1f9af","👩🏼‍🦯":"1f469-1f3fc-200d-1f9af","👩🏽‍🦯":"1f469-1f3fd-200d-1f9af","👩🏾‍🦯":"1f469-1f3fe-200d-1f9af","👩🏿‍🦯":"1f469-1f3ff-200d-1f9af","🧑🏻‍🦼":"1f9d1-1f3fb-200d-1f9bc","🧑🏼‍🦼":"1f9d1-1f3fc-200d-1f9bc","🧑🏽‍🦼":"1f9d1-1f3fd-200d-1f9bc","🧑🏾‍🦼":"1f9d1-1f3fe-200d-1f9bc","🧑🏿‍🦼":"1f9d1-1f3ff-200d-1f9bc","👨🏻‍🦼":"1f468-1f3fb-200d-1f9bc","👨🏼‍🦼":"1f468-1f3fc-200d-1f9bc","👨🏽‍🦼":"1f468-1f3fd-200d-1f9bc","👨🏾‍🦼":"1f468-1f3fe-200d-1f9bc","👨🏿‍🦼":"1f468-1f3ff-200d-1f9bc","👩🏻‍🦼":"1f469-1f3fb-200d-1f9bc","👩🏼‍🦼":"1f469-1f3fc-200d-1f9bc","👩🏽‍🦼":"1f469-1f3fd-200d-1f9bc","👩🏾‍🦼":"1f469-1f3fe-200d-1f9bc","👩🏿‍🦼":"1f469-1f3ff-200d-1f9bc","🧑🏻‍🦽":"1f9d1-1f3fb-200d-1f9bd","🧑🏼‍🦽":"1f9d1-1f3fc-200d-1f9bd","🧑🏽‍🦽":"1f9d1-1f3fd-200d-1f9bd","🧑🏾‍🦽":"1f9d1-1f3fe-200d-1f9bd","🧑🏿‍🦽":"1f9d1-1f3ff-200d-1f9bd","👨🏻‍🦽":"1f468-1f3fb-200d-1f9bd","👨🏼‍🦽":"1f468-1f3fc-200d-1f9bd","👨🏽‍🦽":"1f468-1f3fd-200d-1f9bd","👨🏾‍🦽":"1f468-1f3fe-200d-1f9bd","👨🏿‍🦽":"1f468-1f3ff-200d-1f9bd","👩🏻‍🦽":"1f469-1f3fb-200d-1f9bd","👩🏼‍🦽":"1f469-1f3fc-200d-1f9bd","👩🏽‍🦽":"1f469-1f3fd-200d-1f9bd","👩🏾‍🦽":"1f469-1f3fe-200d-1f9bd","👩🏿‍🦽":"1f469-1f3ff-200d-1f9bd","🏃‍♂️":"1f3c3-200d-2642-fe0f","🏃🏻‍♂":"1f3c3-1f3fb-200d-2642-fe0f","🏃🏼‍♂":"1f3c3-1f3fc-200d-2642-fe0f","🏃🏽‍♂":"1f3c3-1f3fd-200d-2642-fe0f","🏃🏾‍♂":"1f3c3-1f3fe-200d-2642-fe0f","🏃🏿‍♂":"1f3c3-1f3ff-200d-2642-fe0f","🏃‍♀️":"1f3c3-200d-2640-fe0f","🏃🏻‍♀":"1f3c3-1f3fb-200d-2640-fe0f","🏃🏼‍♀":"1f3c3-1f3fc-200d-2640-fe0f","🏃🏽‍♀":"1f3c3-1f3fd-200d-2640-fe0f","🏃🏾‍♀":"1f3c3-1f3fe-200d-2640-fe0f","🏃🏿‍♀":"1f3c3-1f3ff-200d-2640-fe0f","👯‍♂️":"1f46f-200d-2642-fe0f","👯‍♀️":"1f46f-200d-2640-fe0f","🧖‍♂️":"1f9d6-200d-2642-fe0f","🧖🏻‍♂":"1f9d6-1f3fb-200d-2642-fe0f","🧖🏼‍♂":"1f9d6-1f3fc-200d-2642-fe0f","🧖🏽‍♂":"1f9d6-1f3fd-200d-2642-fe0f","🧖🏾‍♂":"1f9d6-1f3fe-200d-2642-fe0f","🧖🏿‍♂":"1f9d6-1f3ff-200d-2642-fe0f","🧖‍♀️":"1f9d6-200d-2640-fe0f","🧖🏻‍♀":"1f9d6-1f3fb-200d-2640-fe0f","🧖🏼‍♀":"1f9d6-1f3fc-200d-2640-fe0f","🧖🏽‍♀":"1f9d6-1f3fd-200d-2640-fe0f","🧖🏾‍♀":"1f9d6-1f3fe-200d-2640-fe0f","🧖🏿‍♀":"1f9d6-1f3ff-200d-2640-fe0f","🧗‍♂️":"1f9d7-200d-2642-fe0f","🧗🏻‍♂":"1f9d7-1f3fb-200d-2642-fe0f","🧗🏼‍♂":"1f9d7-1f3fc-200d-2642-fe0f","🧗🏽‍♂":"1f9d7-1f3fd-200d-2642-fe0f","🧗🏾‍♂":"1f9d7-1f3fe-200d-2642-fe0f","🧗🏿‍♂":"1f9d7-1f3ff-200d-2642-fe0f","🧗‍♀️":"1f9d7-200d-2640-fe0f","🧗🏻‍♀":"1f9d7-1f3fb-200d-2640-fe0f","🧗🏼‍♀":"1f9d7-1f3fc-200d-2640-fe0f","🧗🏽‍♀":"1f9d7-1f3fd-200d-2640-fe0f","🧗🏾‍♀":"1f9d7-1f3fe-200d-2640-fe0f","🧗🏿‍♀":"1f9d7-1f3ff-200d-2640-fe0f","🏌‍♂️":"1f3cc-fe0f-200d-2642-fe0f","🏌️‍♂":"1f3cc-fe0f-200d-2642-fe0f","🏌🏻‍♂":"1f3cc-1f3fb-200d-2642-fe0f","🏌🏼‍♂":"1f3cc-1f3fc-200d-2642-fe0f","🏌🏽‍♂":"1f3cc-1f3fd-200d-2642-fe0f","🏌🏾‍♂":"1f3cc-1f3fe-200d-2642-fe0f","🏌🏿‍♂":"1f3cc-1f3ff-200d-2642-fe0f","🏌‍♀️":"1f3cc-fe0f-200d-2640-fe0f","🏌️‍♀":"1f3cc-fe0f-200d-2640-fe0f","🏌🏻‍♀":"1f3cc-1f3fb-200d-2640-fe0f","🏌🏼‍♀":"1f3cc-1f3fc-200d-2640-fe0f","🏌🏽‍♀":"1f3cc-1f3fd-200d-2640-fe0f","🏌🏾‍♀":"1f3cc-1f3fe-200d-2640-fe0f","🏌🏿‍♀":"1f3cc-1f3ff-200d-2640-fe0f","🏄‍♂️":"1f3c4-200d-2642-fe0f","🏄🏻‍♂":"1f3c4-1f3fb-200d-2642-fe0f","🏄🏼‍♂":"1f3c4-1f3fc-200d-2642-fe0f","🏄🏽‍♂":"1f3c4-1f3fd-200d-2642-fe0f","🏄🏾‍♂":"1f3c4-1f3fe-200d-2642-fe0f","🏄🏿‍♂":"1f3c4-1f3ff-200d-2642-fe0f","🏄‍♀️":"1f3c4-200d-2640-fe0f","🏄🏻‍♀":"1f3c4-1f3fb-200d-2640-fe0f","🏄🏼‍♀":"1f3c4-1f3fc-200d-2640-fe0f","🏄🏽‍♀":"1f3c4-1f3fd-200d-2640-fe0f","🏄🏾‍♀":"1f3c4-1f3fe-200d-2640-fe0f","🏄🏿‍♀":"1f3c4-1f3ff-200d-2640-fe0f","🚣‍♂️":"1f6a3-200d-2642-fe0f","🚣🏻‍♂":"1f6a3-1f3fb-200d-2642-fe0f","🚣🏼‍♂":"1f6a3-1f3fc-200d-2642-fe0f","🚣🏽‍♂":"1f6a3-1f3fd-200d-2642-fe0f","🚣🏾‍♂":"1f6a3-1f3fe-200d-2642-fe0f","🚣🏿‍♂":"1f6a3-1f3ff-200d-2642-fe0f","🚣‍♀️":"1f6a3-200d-2640-fe0f","🚣🏻‍♀":"1f6a3-1f3fb-200d-2640-fe0f","🚣🏼‍♀":"1f6a3-1f3fc-200d-2640-fe0f","🚣🏽‍♀":"1f6a3-1f3fd-200d-2640-fe0f","🚣🏾‍♀":"1f6a3-1f3fe-200d-2640-fe0f","🚣🏿‍♀":"1f6a3-1f3ff-200d-2640-fe0f","🏊‍♂️":"1f3ca-200d-2642-fe0f","🏊🏻‍♂":"1f3ca-1f3fb-200d-2642-fe0f","🏊🏼‍♂":"1f3ca-1f3fc-200d-2642-fe0f","🏊🏽‍♂":"1f3ca-1f3fd-200d-2642-fe0f","🏊🏾‍♂":"1f3ca-1f3fe-200d-2642-fe0f","🏊🏿‍♂":"1f3ca-1f3ff-200d-2642-fe0f","🏊‍♀️":"1f3ca-200d-2640-fe0f","🏊🏻‍♀":"1f3ca-1f3fb-200d-2640-fe0f","🏊🏼‍♀":"1f3ca-1f3fc-200d-2640-fe0f","🏊🏽‍♀":"1f3ca-1f3fd-200d-2640-fe0f","🏊🏾‍♀":"1f3ca-1f3fe-200d-2640-fe0f","🏊🏿‍♀":"1f3ca-1f3ff-200d-2640-fe0f","⛹‍♂️":"26f9-fe0f-200d-2642-fe0f","⛹️‍♂":"26f9-fe0f-200d-2642-fe0f","⛹🏻‍♂":"26f9-1f3fb-200d-2642-fe0f","⛹🏼‍♂":"26f9-1f3fc-200d-2642-fe0f","⛹🏽‍♂":"26f9-1f3fd-200d-2642-fe0f","⛹🏾‍♂":"26f9-1f3fe-200d-2642-fe0f","⛹🏿‍♂":"26f9-1f3ff-200d-2642-fe0f","⛹‍♀️":"26f9-fe0f-200d-2640-fe0f","⛹️‍♀":"26f9-fe0f-200d-2640-fe0f","⛹🏻‍♀":"26f9-1f3fb-200d-2640-fe0f","⛹🏼‍♀":"26f9-1f3fc-200d-2640-fe0f","⛹🏽‍♀":"26f9-1f3fd-200d-2640-fe0f","⛹🏾‍♀":"26f9-1f3fe-200d-2640-fe0f","⛹🏿‍♀":"26f9-1f3ff-200d-2640-fe0f","🏋‍♂️":"1f3cb-fe0f-200d-2642-fe0f","🏋️‍♂":"1f3cb-fe0f-200d-2642-fe0f","🏋🏻‍♂":"1f3cb-1f3fb-200d-2642-fe0f","🏋🏼‍♂":"1f3cb-1f3fc-200d-2642-fe0f","🏋🏽‍♂":"1f3cb-1f3fd-200d-2642-fe0f","🏋🏾‍♂":"1f3cb-1f3fe-200d-2642-fe0f","🏋🏿‍♂":"1f3cb-1f3ff-200d-2642-fe0f","🏋‍♀️":"1f3cb-fe0f-200d-2640-fe0f","🏋️‍♀":"1f3cb-fe0f-200d-2640-fe0f","🏋🏻‍♀":"1f3cb-1f3fb-200d-2640-fe0f","🏋🏼‍♀":"1f3cb-1f3fc-200d-2640-fe0f","🏋🏽‍♀":"1f3cb-1f3fd-200d-2640-fe0f","🏋🏾‍♀":"1f3cb-1f3fe-200d-2640-fe0f","🏋🏿‍♀":"1f3cb-1f3ff-200d-2640-fe0f","🚴‍♂️":"1f6b4-200d-2642-fe0f","🚴🏻‍♂":"1f6b4-1f3fb-200d-2642-fe0f","🚴🏼‍♂":"1f6b4-1f3fc-200d-2642-fe0f","🚴🏽‍♂":"1f6b4-1f3fd-200d-2642-fe0f","🚴🏾‍♂":"1f6b4-1f3fe-200d-2642-fe0f","🚴🏿‍♂":"1f6b4-1f3ff-200d-2642-fe0f","🚴‍♀️":"1f6b4-200d-2640-fe0f","🚴🏻‍♀":"1f6b4-1f3fb-200d-2640-fe0f","🚴🏼‍♀":"1f6b4-1f3fc-200d-2640-fe0f","🚴🏽‍♀":"1f6b4-1f3fd-200d-2640-fe0f","🚴🏾‍♀":"1f6b4-1f3fe-200d-2640-fe0f","🚴🏿‍♀":"1f6b4-1f3ff-200d-2640-fe0f","🚵‍♂️":"1f6b5-200d-2642-fe0f","🚵🏻‍♂":"1f6b5-1f3fb-200d-2642-fe0f","🚵🏼‍♂":"1f6b5-1f3fc-200d-2642-fe0f","🚵🏽‍♂":"1f6b5-1f3fd-200d-2642-fe0f","🚵🏾‍♂":"1f6b5-1f3fe-200d-2642-fe0f","🚵🏿‍♂":"1f6b5-1f3ff-200d-2642-fe0f","🚵‍♀️":"1f6b5-200d-2640-fe0f","🚵🏻‍♀":"1f6b5-1f3fb-200d-2640-fe0f","🚵🏼‍♀":"1f6b5-1f3fc-200d-2640-fe0f","🚵🏽‍♀":"1f6b5-1f3fd-200d-2640-fe0f","🚵🏾‍♀":"1f6b5-1f3fe-200d-2640-fe0f","🚵🏿‍♀":"1f6b5-1f3ff-200d-2640-fe0f","🤸‍♂️":"1f938-200d-2642-fe0f","🤸🏻‍♂":"1f938-1f3fb-200d-2642-fe0f","🤸🏼‍♂":"1f938-1f3fc-200d-2642-fe0f","🤸🏽‍♂":"1f938-1f3fd-200d-2642-fe0f","🤸🏾‍♂":"1f938-1f3fe-200d-2642-fe0f","🤸🏿‍♂":"1f938-1f3ff-200d-2642-fe0f","🤸‍♀️":"1f938-200d-2640-fe0f","🤸🏻‍♀":"1f938-1f3fb-200d-2640-fe0f","🤸🏼‍♀":"1f938-1f3fc-200d-2640-fe0f","🤸🏽‍♀":"1f938-1f3fd-200d-2640-fe0f","🤸🏾‍♀":"1f938-1f3fe-200d-2640-fe0f","🤸🏿‍♀":"1f938-1f3ff-200d-2640-fe0f","🤼‍♂️":"1f93c-200d-2642-fe0f","🤼‍♀️":"1f93c-200d-2640-fe0f","🤽‍♂️":"1f93d-200d-2642-fe0f","🤽🏻‍♂":"1f93d-1f3fb-200d-2642-fe0f","🤽🏼‍♂":"1f93d-1f3fc-200d-2642-fe0f","🤽🏽‍♂":"1f93d-1f3fd-200d-2642-fe0f","🤽🏾‍♂":"1f93d-1f3fe-200d-2642-fe0f","🤽🏿‍♂":"1f93d-1f3ff-200d-2642-fe0f","🤽‍♀️":"1f93d-200d-2640-fe0f","🤽🏻‍♀":"1f93d-1f3fb-200d-2640-fe0f","🤽🏼‍♀":"1f93d-1f3fc-200d-2640-fe0f","🤽🏽‍♀":"1f93d-1f3fd-200d-2640-fe0f","🤽🏾‍♀":"1f93d-1f3fe-200d-2640-fe0f","🤽🏿‍♀":"1f93d-1f3ff-200d-2640-fe0f","🤾‍♂️":"1f93e-200d-2642-fe0f","🤾🏻‍♂":"1f93e-1f3fb-200d-2642-fe0f","🤾🏼‍♂":"1f93e-1f3fc-200d-2642-fe0f","🤾🏽‍♂":"1f93e-1f3fd-200d-2642-fe0f","🤾🏾‍♂":"1f93e-1f3fe-200d-2642-fe0f","🤾🏿‍♂":"1f93e-1f3ff-200d-2642-fe0f","🤾‍♀️":"1f93e-200d-2640-fe0f","🤾🏻‍♀":"1f93e-1f3fb-200d-2640-fe0f","🤾🏼‍♀":"1f93e-1f3fc-200d-2640-fe0f","🤾🏽‍♀":"1f93e-1f3fd-200d-2640-fe0f","🤾🏾‍♀":"1f93e-1f3fe-200d-2640-fe0f","🤾🏿‍♀":"1f93e-1f3ff-200d-2640-fe0f","🤹‍♂️":"1f939-200d-2642-fe0f","🤹🏻‍♂":"1f939-1f3fb-200d-2642-fe0f","🤹🏼‍♂":"1f939-1f3fc-200d-2642-fe0f","🤹🏽‍♂":"1f939-1f3fd-200d-2642-fe0f","🤹🏾‍♂":"1f939-1f3fe-200d-2642-fe0f","🤹🏿‍♂":"1f939-1f3ff-200d-2642-fe0f","🤹‍♀️":"1f939-200d-2640-fe0f","🤹🏻‍♀":"1f939-1f3fb-200d-2640-fe0f","🤹🏼‍♀":"1f939-1f3fc-200d-2640-fe0f","🤹🏽‍♀":"1f939-1f3fd-200d-2640-fe0f","🤹🏾‍♀":"1f939-1f3fe-200d-2640-fe0f","🤹🏿‍♀":"1f939-1f3ff-200d-2640-fe0f","🧘‍♂️":"1f9d8-200d-2642-fe0f","🧘🏻‍♂":"1f9d8-1f3fb-200d-2642-fe0f","🧘🏼‍♂":"1f9d8-1f3fc-200d-2642-fe0f","🧘🏽‍♂":"1f9d8-1f3fd-200d-2642-fe0f","🧘🏾‍♂":"1f9d8-1f3fe-200d-2642-fe0f","🧘🏿‍♂":"1f9d8-1f3ff-200d-2642-fe0f","🧘‍♀️":"1f9d8-200d-2640-fe0f","🧘🏻‍♀":"1f9d8-1f3fb-200d-2640-fe0f","🧘🏼‍♀":"1f9d8-1f3fc-200d-2640-fe0f","🧘🏽‍♀":"1f9d8-1f3fd-200d-2640-fe0f","🧘🏾‍♀":"1f9d8-1f3fe-200d-2640-fe0f","🧘🏿‍♀":"1f9d8-1f3ff-200d-2640-fe0f","🐻‍❄️":"1f43b-200d-2744-fe0f","🏳️‍🌈":"1f3f3-fe0f-200d-1f308","🏳‍⚧️":"1f3f3-fe0f-200d-26a7-fe0f","🏳️‍⚧":"1f3f3-fe0f-200d-26a7-fe0f","🏴‍☠️":"1f3f4-200d-2620-fe0f","👁️‍🗨️":"1f441-200d-1f5e8","🫱🏻‍🫲🏼":"1faf1-1f3fb-200d-1faf2-1f3fc","🫱🏻‍🫲🏽":"1faf1-1f3fb-200d-1faf2-1f3fd","🫱🏻‍🫲🏾":"1faf1-1f3fb-200d-1faf2-1f3fe","🫱🏻‍🫲🏿":"1faf1-1f3fb-200d-1faf2-1f3ff","🫱🏼‍🫲🏻":"1faf1-1f3fc-200d-1faf2-1f3fb","🫱🏼‍🫲🏽":"1faf1-1f3fc-200d-1faf2-1f3fd","🫱🏼‍🫲🏾":"1faf1-1f3fc-200d-1faf2-1f3fe","🫱🏼‍🫲🏿":"1faf1-1f3fc-200d-1faf2-1f3ff","🫱🏽‍🫲🏻":"1faf1-1f3fd-200d-1faf2-1f3fb","🫱🏽‍🫲🏼":"1faf1-1f3fd-200d-1faf2-1f3fc","🫱🏽‍🫲🏾":"1faf1-1f3fd-200d-1faf2-1f3fe","🫱🏽‍🫲🏿":"1faf1-1f3fd-200d-1faf2-1f3ff","🫱🏾‍🫲🏻":"1faf1-1f3fe-200d-1faf2-1f3fb","🫱🏾‍🫲🏼":"1faf1-1f3fe-200d-1faf2-1f3fc","🫱🏾‍🫲🏽":"1faf1-1f3fe-200d-1faf2-1f3fd","🫱🏾‍🫲🏿":"1faf1-1f3fe-200d-1faf2-1f3ff","🫱🏿‍🫲🏻":"1faf1-1f3ff-200d-1faf2-1f3fb","🫱🏿‍🫲🏼":"1faf1-1f3ff-200d-1faf2-1f3fc","🫱🏿‍🫲🏽":"1faf1-1f3ff-200d-1faf2-1f3fd","🫱🏿‍🫲🏾":"1faf1-1f3ff-200d-1faf2-1f3fe","🧔🏻‍♂️":"1f9d4-1f3fb-200d-2642-fe0f","🧔🏼‍♂️":"1f9d4-1f3fc-200d-2642-fe0f","🧔🏽‍♂️":"1f9d4-1f3fd-200d-2642-fe0f","🧔🏾‍♂️":"1f9d4-1f3fe-200d-2642-fe0f","🧔🏿‍♂️":"1f9d4-1f3ff-200d-2642-fe0f","🧔🏻‍♀️":"1f9d4-1f3fb-200d-2640-fe0f","🧔🏼‍♀️":"1f9d4-1f3fc-200d-2640-fe0f","🧔🏽‍♀️":"1f9d4-1f3fd-200d-2640-fe0f","🧔🏾‍♀️":"1f9d4-1f3fe-200d-2640-fe0f","🧔🏿‍♀️":"1f9d4-1f3ff-200d-2640-fe0f","👱🏻‍♀️":"1f471-1f3fb-200d-2640-fe0f","👱🏼‍♀️":"1f471-1f3fc-200d-2640-fe0f","👱🏽‍♀️":"1f471-1f3fd-200d-2640-fe0f","👱🏾‍♀️":"1f471-1f3fe-200d-2640-fe0f","👱🏿‍♀️":"1f471-1f3ff-200d-2640-fe0f","👱🏻‍♂️":"1f471-1f3fb-200d-2642-fe0f","👱🏼‍♂️":"1f471-1f3fc-200d-2642-fe0f","👱🏽‍♂️":"1f471-1f3fd-200d-2642-fe0f","👱🏾‍♂️":"1f471-1f3fe-200d-2642-fe0f","👱🏿‍♂️":"1f471-1f3ff-200d-2642-fe0f","🙍🏻‍♂️":"1f64d-1f3fb-200d-2642-fe0f","🙍🏼‍♂️":"1f64d-1f3fc-200d-2642-fe0f","🙍🏽‍♂️":"1f64d-1f3fd-200d-2642-fe0f","🙍🏾‍♂️":"1f64d-1f3fe-200d-2642-fe0f","🙍🏿‍♂️":"1f64d-1f3ff-200d-2642-fe0f","🙍🏻‍♀️":"1f64d-1f3fb-200d-2640-fe0f","🙍🏼‍♀️":"1f64d-1f3fc-200d-2640-fe0f","🙍🏽‍♀️":"1f64d-1f3fd-200d-2640-fe0f","🙍🏾‍♀️":"1f64d-1f3fe-200d-2640-fe0f","🙍🏿‍♀️":"1f64d-1f3ff-200d-2640-fe0f","🙎🏻‍♂️":"1f64e-1f3fb-200d-2642-fe0f","🙎🏼‍♂️":"1f64e-1f3fc-200d-2642-fe0f","🙎🏽‍♂️":"1f64e-1f3fd-200d-2642-fe0f","🙎🏾‍♂️":"1f64e-1f3fe-200d-2642-fe0f","🙎🏿‍♂️":"1f64e-1f3ff-200d-2642-fe0f","🙎🏻‍♀️":"1f64e-1f3fb-200d-2640-fe0f","🙎🏼‍♀️":"1f64e-1f3fc-200d-2640-fe0f","🙎🏽‍♀️":"1f64e-1f3fd-200d-2640-fe0f","🙎🏾‍♀️":"1f64e-1f3fe-200d-2640-fe0f","🙎🏿‍♀️":"1f64e-1f3ff-200d-2640-fe0f","🙅🏻‍♂️":"1f645-1f3fb-200d-2642-fe0f","🙅🏼‍♂️":"1f645-1f3fc-200d-2642-fe0f","🙅🏽‍♂️":"1f645-1f3fd-200d-2642-fe0f","🙅🏾‍♂️":"1f645-1f3fe-200d-2642-fe0f","🙅🏿‍♂️":"1f645-1f3ff-200d-2642-fe0f","🙅🏻‍♀️":"1f645-1f3fb-200d-2640-fe0f","🙅🏼‍♀️":"1f645-1f3fc-200d-2640-fe0f","🙅🏽‍♀️":"1f645-1f3fd-200d-2640-fe0f","🙅🏾‍♀️":"1f645-1f3fe-200d-2640-fe0f","🙅🏿‍♀️":"1f645-1f3ff-200d-2640-fe0f","🙆🏻‍♂️":"1f646-1f3fb-200d-2642-fe0f","🙆🏼‍♂️":"1f646-1f3fc-200d-2642-fe0f","🙆🏽‍♂️":"1f646-1f3fd-200d-2642-fe0f","🙆🏾‍♂️":"1f646-1f3fe-200d-2642-fe0f","🙆🏿‍♂️":"1f646-1f3ff-200d-2642-fe0f","🙆🏻‍♀️":"1f646-1f3fb-200d-2640-fe0f","🙆🏼‍♀️":"1f646-1f3fc-200d-2640-fe0f","🙆🏽‍♀️":"1f646-1f3fd-200d-2640-fe0f","🙆🏾‍♀️":"1f646-1f3fe-200d-2640-fe0f","🙆🏿‍♀️":"1f646-1f3ff-200d-2640-fe0f","💁🏻‍♂️":"1f481-1f3fb-200d-2642-fe0f","💁🏼‍♂️":"1f481-1f3fc-200d-2642-fe0f","💁🏽‍♂️":"1f481-1f3fd-200d-2642-fe0f","💁🏾‍♂️":"1f481-1f3fe-200d-2642-fe0f","💁🏿‍♂️":"1f481-1f3ff-200d-2642-fe0f","💁🏻‍♀️":"1f481-1f3fb-200d-2640-fe0f","💁🏼‍♀️":"1f481-1f3fc-200d-2640-fe0f","💁🏽‍♀️":"1f481-1f3fd-200d-2640-fe0f","💁🏾‍♀️":"1f481-1f3fe-200d-2640-fe0f","💁🏿‍♀️":"1f481-1f3ff-200d-2640-fe0f","🙋🏻‍♂️":"1f64b-1f3fb-200d-2642-fe0f","🙋🏼‍♂️":"1f64b-1f3fc-200d-2642-fe0f","🙋🏽‍♂️":"1f64b-1f3fd-200d-2642-fe0f","🙋🏾‍♂️":"1f64b-1f3fe-200d-2642-fe0f","🙋🏿‍♂️":"1f64b-1f3ff-200d-2642-fe0f","🙋🏻‍♀️":"1f64b-1f3fb-200d-2640-fe0f","🙋🏼‍♀️":"1f64b-1f3fc-200d-2640-fe0f","🙋🏽‍♀️":"1f64b-1f3fd-200d-2640-fe0f","🙋🏾‍♀️":"1f64b-1f3fe-200d-2640-fe0f","🙋🏿‍♀️":"1f64b-1f3ff-200d-2640-fe0f","🧏🏻‍♂️":"1f9cf-1f3fb-200d-2642-fe0f","🧏🏼‍♂️":"1f9cf-1f3fc-200d-2642-fe0f","🧏🏽‍♂️":"1f9cf-1f3fd-200d-2642-fe0f","🧏🏾‍♂️":"1f9cf-1f3fe-200d-2642-fe0f","🧏🏿‍♂️":"1f9cf-1f3ff-200d-2642-fe0f","🧏🏻‍♀️":"1f9cf-1f3fb-200d-2640-fe0f","🧏🏼‍♀️":"1f9cf-1f3fc-200d-2640-fe0f","🧏🏽‍♀️":"1f9cf-1f3fd-200d-2640-fe0f","🧏🏾‍♀️":"1f9cf-1f3fe-200d-2640-fe0f","🧏🏿‍♀️":"1f9cf-1f3ff-200d-2640-fe0f","🙇🏻‍♂️":"1f647-1f3fb-200d-2642-fe0f","🙇🏼‍♂️":"1f647-1f3fc-200d-2642-fe0f","🙇🏽‍♂️":"1f647-1f3fd-200d-2642-fe0f","🙇🏾‍♂️":"1f647-1f3fe-200d-2642-fe0f","🙇🏿‍♂️":"1f647-1f3ff-200d-2642-fe0f","🙇🏻‍♀️":"1f647-1f3fb-200d-2640-fe0f","🙇🏼‍♀️":"1f647-1f3fc-200d-2640-fe0f","🙇🏽‍♀️":"1f647-1f3fd-200d-2640-fe0f","🙇🏾‍♀️":"1f647-1f3fe-200d-2640-fe0f","🙇🏿‍♀️":"1f647-1f3ff-200d-2640-fe0f","🤦🏻‍♂️":"1f926-1f3fb-200d-2642-fe0f","🤦🏼‍♂️":"1f926-1f3fc-200d-2642-fe0f","🤦🏽‍♂️":"1f926-1f3fd-200d-2642-fe0f","🤦🏾‍♂️":"1f926-1f3fe-200d-2642-fe0f","🤦🏿‍♂️":"1f926-1f3ff-200d-2642-fe0f","🤦🏻‍♀️":"1f926-1f3fb-200d-2640-fe0f","🤦🏼‍♀️":"1f926-1f3fc-200d-2640-fe0f","🤦🏽‍♀️":"1f926-1f3fd-200d-2640-fe0f","🤦🏾‍♀️":"1f926-1f3fe-200d-2640-fe0f","🤦🏿‍♀️":"1f926-1f3ff-200d-2640-fe0f","🤷🏻‍♂️":"1f937-1f3fb-200d-2642-fe0f","🤷🏼‍♂️":"1f937-1f3fc-200d-2642-fe0f","🤷🏽‍♂️":"1f937-1f3fd-200d-2642-fe0f","🤷🏾‍♂️":"1f937-1f3fe-200d-2642-fe0f","🤷🏿‍♂️":"1f937-1f3ff-200d-2642-fe0f","🤷🏻‍♀️":"1f937-1f3fb-200d-2640-fe0f","🤷🏼‍♀️":"1f937-1f3fc-200d-2640-fe0f","🤷🏽‍♀️":"1f937-1f3fd-200d-2640-fe0f","🤷🏾‍♀️":"1f937-1f3fe-200d-2640-fe0f","🤷🏿‍♀️":"1f937-1f3ff-200d-2640-fe0f","🧑🏻‍⚕️":"1f9d1-1f3fb-200d-2695-fe0f","🧑🏼‍⚕️":"1f9d1-1f3fc-200d-2695-fe0f","🧑🏽‍⚕️":"1f9d1-1f3fd-200d-2695-fe0f","🧑🏾‍⚕️":"1f9d1-1f3fe-200d-2695-fe0f","🧑🏿‍⚕️":"1f9d1-1f3ff-200d-2695-fe0f","👨🏻‍⚕️":"1f468-1f3fb-200d-2695-fe0f","👨🏼‍⚕️":"1f468-1f3fc-200d-2695-fe0f","👨🏽‍⚕️":"1f468-1f3fd-200d-2695-fe0f","👨🏾‍⚕️":"1f468-1f3fe-200d-2695-fe0f","👨🏿‍⚕️":"1f468-1f3ff-200d-2695-fe0f","👩🏻‍⚕️":"1f469-1f3fb-200d-2695-fe0f","👩🏼‍⚕️":"1f469-1f3fc-200d-2695-fe0f","👩🏽‍⚕️":"1f469-1f3fd-200d-2695-fe0f","👩🏾‍⚕️":"1f469-1f3fe-200d-2695-fe0f","👩🏿‍⚕️":"1f469-1f3ff-200d-2695-fe0f","🧑🏻‍⚖️":"1f9d1-1f3fb-200d-2696-fe0f","🧑🏼‍⚖️":"1f9d1-1f3fc-200d-2696-fe0f","🧑🏽‍⚖️":"1f9d1-1f3fd-200d-2696-fe0f","🧑🏾‍⚖️":"1f9d1-1f3fe-200d-2696-fe0f","🧑🏿‍⚖️":"1f9d1-1f3ff-200d-2696-fe0f","👨🏻‍⚖️":"1f468-1f3fb-200d-2696-fe0f","👨🏼‍⚖️":"1f468-1f3fc-200d-2696-fe0f","👨🏽‍⚖️":"1f468-1f3fd-200d-2696-fe0f","👨🏾‍⚖️":"1f468-1f3fe-200d-2696-fe0f","👨🏿‍⚖️":"1f468-1f3ff-200d-2696-fe0f","👩🏻‍⚖️":"1f469-1f3fb-200d-2696-fe0f","👩🏼‍⚖️":"1f469-1f3fc-200d-2696-fe0f","👩🏽‍⚖️":"1f469-1f3fd-200d-2696-fe0f","👩🏾‍⚖️":"1f469-1f3fe-200d-2696-fe0f","👩🏿‍⚖️":"1f469-1f3ff-200d-2696-fe0f","🧑🏻‍✈️":"1f9d1-1f3fb-200d-2708-fe0f","🧑🏼‍✈️":"1f9d1-1f3fc-200d-2708-fe0f","🧑🏽‍✈️":"1f9d1-1f3fd-200d-2708-fe0f","🧑🏾‍✈️":"1f9d1-1f3fe-200d-2708-fe0f","🧑🏿‍✈️":"1f9d1-1f3ff-200d-2708-fe0f","👨🏻‍✈️":"1f468-1f3fb-200d-2708-fe0f","👨🏼‍✈️":"1f468-1f3fc-200d-2708-fe0f","👨🏽‍✈️":"1f468-1f3fd-200d-2708-fe0f","👨🏾‍✈️":"1f468-1f3fe-200d-2708-fe0f","👨🏿‍✈️":"1f468-1f3ff-200d-2708-fe0f","👩🏻‍✈️":"1f469-1f3fb-200d-2708-fe0f","👩🏼‍✈️":"1f469-1f3fc-200d-2708-fe0f","👩🏽‍✈️":"1f469-1f3fd-200d-2708-fe0f","👩🏾‍✈️":"1f469-1f3fe-200d-2708-fe0f","👩🏿‍✈️":"1f469-1f3ff-200d-2708-fe0f","👮🏻‍♂️":"1f46e-1f3fb-200d-2642-fe0f","👮🏼‍♂️":"1f46e-1f3fc-200d-2642-fe0f","👮🏽‍♂️":"1f46e-1f3fd-200d-2642-fe0f","👮🏾‍♂️":"1f46e-1f3fe-200d-2642-fe0f","👮🏿‍♂️":"1f46e-1f3ff-200d-2642-fe0f","👮🏻‍♀️":"1f46e-1f3fb-200d-2640-fe0f","👮🏼‍♀️":"1f46e-1f3fc-200d-2640-fe0f","👮🏽‍♀️":"1f46e-1f3fd-200d-2640-fe0f","👮🏾‍♀️":"1f46e-1f3fe-200d-2640-fe0f","👮🏿‍♀️":"1f46e-1f3ff-200d-2640-fe0f","🕵️‍♂️":"1f575-fe0f-200d-2642-fe0f","🕵🏻‍♂️":"1f575-1f3fb-200d-2642-fe0f","🕵🏼‍♂️":"1f575-1f3fc-200d-2642-fe0f","🕵🏽‍♂️":"1f575-1f3fd-200d-2642-fe0f","🕵🏾‍♂️":"1f575-1f3fe-200d-2642-fe0f","🕵🏿‍♂️":"1f575-1f3ff-200d-2642-fe0f","🕵️‍♀️":"1f575-fe0f-200d-2640-fe0f","🕵🏻‍♀️":"1f575-1f3fb-200d-2640-fe0f","🕵🏼‍♀️":"1f575-1f3fc-200d-2640-fe0f","🕵🏽‍♀️":"1f575-1f3fd-200d-2640-fe0f","🕵🏾‍♀️":"1f575-1f3fe-200d-2640-fe0f","🕵🏿‍♀️":"1f575-1f3ff-200d-2640-fe0f","💂🏻‍♂️":"1f482-1f3fb-200d-2642-fe0f","💂🏼‍♂️":"1f482-1f3fc-200d-2642-fe0f","💂🏽‍♂️":"1f482-1f3fd-200d-2642-fe0f","💂🏾‍♂️":"1f482-1f3fe-200d-2642-fe0f","💂🏿‍♂️":"1f482-1f3ff-200d-2642-fe0f","💂🏻‍♀️":"1f482-1f3fb-200d-2640-fe0f","💂🏼‍♀️":"1f482-1f3fc-200d-2640-fe0f","💂🏽‍♀️":"1f482-1f3fd-200d-2640-fe0f","💂🏾‍♀️":"1f482-1f3fe-200d-2640-fe0f","💂🏿‍♀️":"1f482-1f3ff-200d-2640-fe0f","👷🏻‍♂️":"1f477-1f3fb-200d-2642-fe0f","👷🏼‍♂️":"1f477-1f3fc-200d-2642-fe0f","👷🏽‍♂️":"1f477-1f3fd-200d-2642-fe0f","👷🏾‍♂️":"1f477-1f3fe-200d-2642-fe0f","👷🏿‍♂️":"1f477-1f3ff-200d-2642-fe0f","👷🏻‍♀️":"1f477-1f3fb-200d-2640-fe0f","👷🏼‍♀️":"1f477-1f3fc-200d-2640-fe0f","👷🏽‍♀️":"1f477-1f3fd-200d-2640-fe0f","👷🏾‍♀️":"1f477-1f3fe-200d-2640-fe0f","👷🏿‍♀️":"1f477-1f3ff-200d-2640-fe0f","👳🏻‍♂️":"1f473-1f3fb-200d-2642-fe0f","👳🏼‍♂️":"1f473-1f3fc-200d-2642-fe0f","👳🏽‍♂️":"1f473-1f3fd-200d-2642-fe0f","👳🏾‍♂️":"1f473-1f3fe-200d-2642-fe0f","👳🏿‍♂️":"1f473-1f3ff-200d-2642-fe0f","👳🏻‍♀️":"1f473-1f3fb-200d-2640-fe0f","👳🏼‍♀️":"1f473-1f3fc-200d-2640-fe0f","👳🏽‍♀️":"1f473-1f3fd-200d-2640-fe0f","👳🏾‍♀️":"1f473-1f3fe-200d-2640-fe0f","👳🏿‍♀️":"1f473-1f3ff-200d-2640-fe0f","🤵🏻‍♂️":"1f935-1f3fb-200d-2642-fe0f","🤵🏼‍♂️":"1f935-1f3fc-200d-2642-fe0f","🤵🏽‍♂️":"1f935-1f3fd-200d-2642-fe0f","🤵🏾‍♂️":"1f935-1f3fe-200d-2642-fe0f","🤵🏿‍♂️":"1f935-1f3ff-200d-2642-fe0f","🤵🏻‍♀️":"1f935-1f3fb-200d-2640-fe0f","🤵🏼‍♀️":"1f935-1f3fc-200d-2640-fe0f","🤵🏽‍♀️":"1f935-1f3fd-200d-2640-fe0f","🤵🏾‍♀️":"1f935-1f3fe-200d-2640-fe0f","🤵🏿‍♀️":"1f935-1f3ff-200d-2640-fe0f","👰🏻‍♂️":"1f470-1f3fb-200d-2642-fe0f","👰🏼‍♂️":"1f470-1f3fc-200d-2642-fe0f","👰🏽‍♂️":"1f470-1f3fd-200d-2642-fe0f","👰🏾‍♂️":"1f470-1f3fe-200d-2642-fe0f","👰🏿‍♂️":"1f470-1f3ff-200d-2642-fe0f","👰🏻‍♀️":"1f470-1f3fb-200d-2640-fe0f","👰🏼‍♀️":"1f470-1f3fc-200d-2640-fe0f","👰🏽‍♀️":"1f470-1f3fd-200d-2640-fe0f","👰🏾‍♀️":"1f470-1f3fe-200d-2640-fe0f","👰🏿‍♀️":"1f470-1f3ff-200d-2640-fe0f","🦸🏻‍♂️":"1f9b8-1f3fb-200d-2642-fe0f","🦸🏼‍♂️":"1f9b8-1f3fc-200d-2642-fe0f","🦸🏽‍♂️":"1f9b8-1f3fd-200d-2642-fe0f","🦸🏾‍♂️":"1f9b8-1f3fe-200d-2642-fe0f","🦸🏿‍♂️":"1f9b8-1f3ff-200d-2642-fe0f","🦸🏻‍♀️":"1f9b8-1f3fb-200d-2640-fe0f","🦸🏼‍♀️":"1f9b8-1f3fc-200d-2640-fe0f","🦸🏽‍♀️":"1f9b8-1f3fd-200d-2640-fe0f","🦸🏾‍♀️":"1f9b8-1f3fe-200d-2640-fe0f","🦸🏿‍♀️":"1f9b8-1f3ff-200d-2640-fe0f","🦹🏻‍♂️":"1f9b9-1f3fb-200d-2642-fe0f","🦹🏼‍♂️":"1f9b9-1f3fc-200d-2642-fe0f","🦹🏽‍♂️":"1f9b9-1f3fd-200d-2642-fe0f","🦹🏾‍♂️":"1f9b9-1f3fe-200d-2642-fe0f","🦹🏿‍♂️":"1f9b9-1f3ff-200d-2642-fe0f","🦹🏻‍♀️":"1f9b9-1f3fb-200d-2640-fe0f","🦹🏼‍♀️":"1f9b9-1f3fc-200d-2640-fe0f","🦹🏽‍♀️":"1f9b9-1f3fd-200d-2640-fe0f","🦹🏾‍♀️":"1f9b9-1f3fe-200d-2640-fe0f","🦹🏿‍♀️":"1f9b9-1f3ff-200d-2640-fe0f","🧙🏻‍♂️":"1f9d9-1f3fb-200d-2642-fe0f","🧙🏼‍♂️":"1f9d9-1f3fc-200d-2642-fe0f","🧙🏽‍♂️":"1f9d9-1f3fd-200d-2642-fe0f","🧙🏾‍♂️":"1f9d9-1f3fe-200d-2642-fe0f","🧙🏿‍♂️":"1f9d9-1f3ff-200d-2642-fe0f","🧙🏻‍♀️":"1f9d9-1f3fb-200d-2640-fe0f","🧙🏼‍♀️":"1f9d9-1f3fc-200d-2640-fe0f","🧙🏽‍♀️":"1f9d9-1f3fd-200d-2640-fe0f","🧙🏾‍♀️":"1f9d9-1f3fe-200d-2640-fe0f","🧙🏿‍♀️":"1f9d9-1f3ff-200d-2640-fe0f","🧚🏻‍♂️":"1f9da-1f3fb-200d-2642-fe0f","🧚🏼‍♂️":"1f9da-1f3fc-200d-2642-fe0f","🧚🏽‍♂️":"1f9da-1f3fd-200d-2642-fe0f","🧚🏾‍♂️":"1f9da-1f3fe-200d-2642-fe0f","🧚🏿‍♂️":"1f9da-1f3ff-200d-2642-fe0f","🧚🏻‍♀️":"1f9da-1f3fb-200d-2640-fe0f","🧚🏼‍♀️":"1f9da-1f3fc-200d-2640-fe0f","🧚🏽‍♀️":"1f9da-1f3fd-200d-2640-fe0f","🧚🏾‍♀️":"1f9da-1f3fe-200d-2640-fe0f","🧚🏿‍♀️":"1f9da-1f3ff-200d-2640-fe0f","🧛🏻‍♂️":"1f9db-1f3fb-200d-2642-fe0f","🧛🏼‍♂️":"1f9db-1f3fc-200d-2642-fe0f","🧛🏽‍♂️":"1f9db-1f3fd-200d-2642-fe0f","🧛🏾‍♂️":"1f9db-1f3fe-200d-2642-fe0f","🧛🏿‍♂️":"1f9db-1f3ff-200d-2642-fe0f","🧛🏻‍♀️":"1f9db-1f3fb-200d-2640-fe0f","🧛🏼‍♀️":"1f9db-1f3fc-200d-2640-fe0f","🧛🏽‍♀️":"1f9db-1f3fd-200d-2640-fe0f","🧛🏾‍♀️":"1f9db-1f3fe-200d-2640-fe0f","🧛🏿‍♀️":"1f9db-1f3ff-200d-2640-fe0f","🧜🏻‍♂️":"1f9dc-1f3fb-200d-2642-fe0f","🧜🏼‍♂️":"1f9dc-1f3fc-200d-2642-fe0f","🧜🏽‍♂️":"1f9dc-1f3fd-200d-2642-fe0f","🧜🏾‍♂️":"1f9dc-1f3fe-200d-2642-fe0f","🧜🏿‍♂️":"1f9dc-1f3ff-200d-2642-fe0f","🧜🏻‍♀️":"1f9dc-1f3fb-200d-2640-fe0f","🧜🏼‍♀️":"1f9dc-1f3fc-200d-2640-fe0f","🧜🏽‍♀️":"1f9dc-1f3fd-200d-2640-fe0f","🧜🏾‍♀️":"1f9dc-1f3fe-200d-2640-fe0f","🧜🏿‍♀️":"1f9dc-1f3ff-200d-2640-fe0f","🧝🏻‍♂️":"1f9dd-1f3fb-200d-2642-fe0f","🧝🏼‍♂️":"1f9dd-1f3fc-200d-2642-fe0f","🧝🏽‍♂️":"1f9dd-1f3fd-200d-2642-fe0f","🧝🏾‍♂️":"1f9dd-1f3fe-200d-2642-fe0f","🧝🏿‍♂️":"1f9dd-1f3ff-200d-2642-fe0f","🧝🏻‍♀️":"1f9dd-1f3fb-200d-2640-fe0f","🧝🏼‍♀️":"1f9dd-1f3fc-200d-2640-fe0f","🧝🏽‍♀️":"1f9dd-1f3fd-200d-2640-fe0f","🧝🏾‍♀️":"1f9dd-1f3fe-200d-2640-fe0f","🧝🏿‍♀️":"1f9dd-1f3ff-200d-2640-fe0f","💆🏻‍♂️":"1f486-1f3fb-200d-2642-fe0f","💆🏼‍♂️":"1f486-1f3fc-200d-2642-fe0f","💆🏽‍♂️":"1f486-1f3fd-200d-2642-fe0f","💆🏾‍♂️":"1f486-1f3fe-200d-2642-fe0f","💆🏿‍♂️":"1f486-1f3ff-200d-2642-fe0f","💆🏻‍♀️":"1f486-1f3fb-200d-2640-fe0f","💆🏼‍♀️":"1f486-1f3fc-200d-2640-fe0f","💆🏽‍♀️":"1f486-1f3fd-200d-2640-fe0f","💆🏾‍♀️":"1f486-1f3fe-200d-2640-fe0f","💆🏿‍♀️":"1f486-1f3ff-200d-2640-fe0f","💇🏻‍♂️":"1f487-1f3fb-200d-2642-fe0f","💇🏼‍♂️":"1f487-1f3fc-200d-2642-fe0f","💇🏽‍♂️":"1f487-1f3fd-200d-2642-fe0f","💇🏾‍♂️":"1f487-1f3fe-200d-2642-fe0f","💇🏿‍♂️":"1f487-1f3ff-200d-2642-fe0f","💇🏻‍♀️":"1f487-1f3fb-200d-2640-fe0f","💇🏼‍♀️":"1f487-1f3fc-200d-2640-fe0f","💇🏽‍♀️":"1f487-1f3fd-200d-2640-fe0f","💇🏾‍♀️":"1f487-1f3fe-200d-2640-fe0f","💇🏿‍♀️":"1f487-1f3ff-200d-2640-fe0f","🚶🏻‍♂️":"1f6b6-1f3fb-200d-2642-fe0f","🚶🏼‍♂️":"1f6b6-1f3fc-200d-2642-fe0f","🚶🏽‍♂️":"1f6b6-1f3fd-200d-2642-fe0f","🚶🏾‍♂️":"1f6b6-1f3fe-200d-2642-fe0f","🚶🏿‍♂️":"1f6b6-1f3ff-200d-2642-fe0f","🚶🏻‍♀️":"1f6b6-1f3fb-200d-2640-fe0f","🚶🏼‍♀️":"1f6b6-1f3fc-200d-2640-fe0f","🚶🏽‍♀️":"1f6b6-1f3fd-200d-2640-fe0f","🚶🏾‍♀️":"1f6b6-1f3fe-200d-2640-fe0f","🚶🏿‍♀️":"1f6b6-1f3ff-200d-2640-fe0f","🧍🏻‍♂️":"1f9cd-1f3fb-200d-2642-fe0f","🧍🏼‍♂️":"1f9cd-1f3fc-200d-2642-fe0f","🧍🏽‍♂️":"1f9cd-1f3fd-200d-2642-fe0f","🧍🏾‍♂️":"1f9cd-1f3fe-200d-2642-fe0f","🧍🏿‍♂️":"1f9cd-1f3ff-200d-2642-fe0f","🧍🏻‍♀️":"1f9cd-1f3fb-200d-2640-fe0f","🧍🏼‍♀️":"1f9cd-1f3fc-200d-2640-fe0f","🧍🏽‍♀️":"1f9cd-1f3fd-200d-2640-fe0f","🧍🏾‍♀️":"1f9cd-1f3fe-200d-2640-fe0f","🧍🏿‍♀️":"1f9cd-1f3ff-200d-2640-fe0f","🧎🏻‍♂️":"1f9ce-1f3fb-200d-2642-fe0f","🧎🏼‍♂️":"1f9ce-1f3fc-200d-2642-fe0f","🧎🏽‍♂️":"1f9ce-1f3fd-200d-2642-fe0f","🧎🏾‍♂️":"1f9ce-1f3fe-200d-2642-fe0f","🧎🏿‍♂️":"1f9ce-1f3ff-200d-2642-fe0f","🧎🏻‍♀️":"1f9ce-1f3fb-200d-2640-fe0f","🧎🏼‍♀️":"1f9ce-1f3fc-200d-2640-fe0f","🧎🏽‍♀️":"1f9ce-1f3fd-200d-2640-fe0f","🧎🏾‍♀️":"1f9ce-1f3fe-200d-2640-fe0f","🧎🏿‍♀️":"1f9ce-1f3ff-200d-2640-fe0f","🏃🏻‍♂️":"1f3c3-1f3fb-200d-2642-fe0f","🏃🏼‍♂️":"1f3c3-1f3fc-200d-2642-fe0f","🏃🏽‍♂️":"1f3c3-1f3fd-200d-2642-fe0f","🏃🏾‍♂️":"1f3c3-1f3fe-200d-2642-fe0f","🏃🏿‍♂️":"1f3c3-1f3ff-200d-2642-fe0f","🏃🏻‍♀️":"1f3c3-1f3fb-200d-2640-fe0f","🏃🏼‍♀️":"1f3c3-1f3fc-200d-2640-fe0f","🏃🏽‍♀️":"1f3c3-1f3fd-200d-2640-fe0f","🏃🏾‍♀️":"1f3c3-1f3fe-200d-2640-fe0f","🏃🏿‍♀️":"1f3c3-1f3ff-200d-2640-fe0f","🧖🏻‍♂️":"1f9d6-1f3fb-200d-2642-fe0f","🧖🏼‍♂️":"1f9d6-1f3fc-200d-2642-fe0f","🧖🏽‍♂️":"1f9d6-1f3fd-200d-2642-fe0f","🧖🏾‍♂️":"1f9d6-1f3fe-200d-2642-fe0f","🧖🏿‍♂️":"1f9d6-1f3ff-200d-2642-fe0f","🧖🏻‍♀️":"1f9d6-1f3fb-200d-2640-fe0f","🧖🏼‍♀️":"1f9d6-1f3fc-200d-2640-fe0f","🧖🏽‍♀️":"1f9d6-1f3fd-200d-2640-fe0f","🧖🏾‍♀️":"1f9d6-1f3fe-200d-2640-fe0f","🧖🏿‍♀️":"1f9d6-1f3ff-200d-2640-fe0f","🧗🏻‍♂️":"1f9d7-1f3fb-200d-2642-fe0f","🧗🏼‍♂️":"1f9d7-1f3fc-200d-2642-fe0f","🧗🏽‍♂️":"1f9d7-1f3fd-200d-2642-fe0f","🧗🏾‍♂️":"1f9d7-1f3fe-200d-2642-fe0f","🧗🏿‍♂️":"1f9d7-1f3ff-200d-2642-fe0f","🧗🏻‍♀️":"1f9d7-1f3fb-200d-2640-fe0f","🧗🏼‍♀️":"1f9d7-1f3fc-200d-2640-fe0f","🧗🏽‍♀️":"1f9d7-1f3fd-200d-2640-fe0f","🧗🏾‍♀️":"1f9d7-1f3fe-200d-2640-fe0f","🧗🏿‍♀️":"1f9d7-1f3ff-200d-2640-fe0f","🏌️‍♂️":"1f3cc-fe0f-200d-2642-fe0f","🏌🏻‍♂️":"1f3cc-1f3fb-200d-2642-fe0f","🏌🏼‍♂️":"1f3cc-1f3fc-200d-2642-fe0f","🏌🏽‍♂️":"1f3cc-1f3fd-200d-2642-fe0f","🏌🏾‍♂️":"1f3cc-1f3fe-200d-2642-fe0f","🏌🏿‍♂️":"1f3cc-1f3ff-200d-2642-fe0f","🏌️‍♀️":"1f3cc-fe0f-200d-2640-fe0f","🏌🏻‍♀️":"1f3cc-1f3fb-200d-2640-fe0f","🏌🏼‍♀️":"1f3cc-1f3fc-200d-2640-fe0f","🏌🏽‍♀️":"1f3cc-1f3fd-200d-2640-fe0f","🏌🏾‍♀️":"1f3cc-1f3fe-200d-2640-fe0f","🏌🏿‍♀️":"1f3cc-1f3ff-200d-2640-fe0f","🏄🏻‍♂️":"1f3c4-1f3fb-200d-2642-fe0f","🏄🏼‍♂️":"1f3c4-1f3fc-200d-2642-fe0f","🏄🏽‍♂️":"1f3c4-1f3fd-200d-2642-fe0f","🏄🏾‍♂️":"1f3c4-1f3fe-200d-2642-fe0f","🏄🏿‍♂️":"1f3c4-1f3ff-200d-2642-fe0f","🏄🏻‍♀️":"1f3c4-1f3fb-200d-2640-fe0f","🏄🏼‍♀️":"1f3c4-1f3fc-200d-2640-fe0f","🏄🏽‍♀️":"1f3c4-1f3fd-200d-2640-fe0f","🏄🏾‍♀️":"1f3c4-1f3fe-200d-2640-fe0f","🏄🏿‍♀️":"1f3c4-1f3ff-200d-2640-fe0f","🚣🏻‍♂️":"1f6a3-1f3fb-200d-2642-fe0f","🚣🏼‍♂️":"1f6a3-1f3fc-200d-2642-fe0f","🚣🏽‍♂️":"1f6a3-1f3fd-200d-2642-fe0f","🚣🏾‍♂️":"1f6a3-1f3fe-200d-2642-fe0f","🚣🏿‍♂️":"1f6a3-1f3ff-200d-2642-fe0f","🚣🏻‍♀️":"1f6a3-1f3fb-200d-2640-fe0f","🚣🏼‍♀️":"1f6a3-1f3fc-200d-2640-fe0f","🚣🏽‍♀️":"1f6a3-1f3fd-200d-2640-fe0f","🚣🏾‍♀️":"1f6a3-1f3fe-200d-2640-fe0f","🚣🏿‍♀️":"1f6a3-1f3ff-200d-2640-fe0f","🏊🏻‍♂️":"1f3ca-1f3fb-200d-2642-fe0f","🏊🏼‍♂️":"1f3ca-1f3fc-200d-2642-fe0f","🏊🏽‍♂️":"1f3ca-1f3fd-200d-2642-fe0f","🏊🏾‍♂️":"1f3ca-1f3fe-200d-2642-fe0f","🏊🏿‍♂️":"1f3ca-1f3ff-200d-2642-fe0f","🏊🏻‍♀️":"1f3ca-1f3fb-200d-2640-fe0f","🏊🏼‍♀️":"1f3ca-1f3fc-200d-2640-fe0f","🏊🏽‍♀️":"1f3ca-1f3fd-200d-2640-fe0f","🏊🏾‍♀️":"1f3ca-1f3fe-200d-2640-fe0f","🏊🏿‍♀️":"1f3ca-1f3ff-200d-2640-fe0f","⛹️‍♂️":"26f9-fe0f-200d-2642-fe0f","⛹🏻‍♂️":"26f9-1f3fb-200d-2642-fe0f","⛹🏼‍♂️":"26f9-1f3fc-200d-2642-fe0f","⛹🏽‍♂️":"26f9-1f3fd-200d-2642-fe0f","⛹🏾‍♂️":"26f9-1f3fe-200d-2642-fe0f","⛹🏿‍♂️":"26f9-1f3ff-200d-2642-fe0f","⛹️‍♀️":"26f9-fe0f-200d-2640-fe0f","⛹🏻‍♀️":"26f9-1f3fb-200d-2640-fe0f","⛹🏼‍♀️":"26f9-1f3fc-200d-2640-fe0f","⛹🏽‍♀️":"26f9-1f3fd-200d-2640-fe0f","⛹🏾‍♀️":"26f9-1f3fe-200d-2640-fe0f","⛹🏿‍♀️":"26f9-1f3ff-200d-2640-fe0f","🏋️‍♂️":"1f3cb-fe0f-200d-2642-fe0f","🏋🏻‍♂️":"1f3cb-1f3fb-200d-2642-fe0f","🏋🏼‍♂️":"1f3cb-1f3fc-200d-2642-fe0f","🏋🏽‍♂️":"1f3cb-1f3fd-200d-2642-fe0f","🏋🏾‍♂️":"1f3cb-1f3fe-200d-2642-fe0f","🏋🏿‍♂️":"1f3cb-1f3ff-200d-2642-fe0f","🏋️‍♀️":"1f3cb-fe0f-200d-2640-fe0f","🏋🏻‍♀️":"1f3cb-1f3fb-200d-2640-fe0f","🏋🏼‍♀️":"1f3cb-1f3fc-200d-2640-fe0f","🏋🏽‍♀️":"1f3cb-1f3fd-200d-2640-fe0f","🏋🏾‍♀️":"1f3cb-1f3fe-200d-2640-fe0f","🏋🏿‍♀️":"1f3cb-1f3ff-200d-2640-fe0f","🚴🏻‍♂️":"1f6b4-1f3fb-200d-2642-fe0f","🚴🏼‍♂️":"1f6b4-1f3fc-200d-2642-fe0f","🚴🏽‍♂️":"1f6b4-1f3fd-200d-2642-fe0f","🚴🏾‍♂️":"1f6b4-1f3fe-200d-2642-fe0f","🚴🏿‍♂️":"1f6b4-1f3ff-200d-2642-fe0f","🚴🏻‍♀️":"1f6b4-1f3fb-200d-2640-fe0f","🚴🏼‍♀️":"1f6b4-1f3fc-200d-2640-fe0f","🚴🏽‍♀️":"1f6b4-1f3fd-200d-2640-fe0f","🚴🏾‍♀️":"1f6b4-1f3fe-200d-2640-fe0f","🚴🏿‍♀️":"1f6b4-1f3ff-200d-2640-fe0f","🚵🏻‍♂️":"1f6b5-1f3fb-200d-2642-fe0f","🚵🏼‍♂️":"1f6b5-1f3fc-200d-2642-fe0f","🚵🏽‍♂️":"1f6b5-1f3fd-200d-2642-fe0f","🚵🏾‍♂️":"1f6b5-1f3fe-200d-2642-fe0f","🚵🏿‍♂️":"1f6b5-1f3ff-200d-2642-fe0f","🚵🏻‍♀️":"1f6b5-1f3fb-200d-2640-fe0f","🚵🏼‍♀️":"1f6b5-1f3fc-200d-2640-fe0f","🚵🏽‍♀️":"1f6b5-1f3fd-200d-2640-fe0f","🚵🏾‍♀️":"1f6b5-1f3fe-200d-2640-fe0f","🚵🏿‍♀️":"1f6b5-1f3ff-200d-2640-fe0f","🤸🏻‍♂️":"1f938-1f3fb-200d-2642-fe0f","🤸🏼‍♂️":"1f938-1f3fc-200d-2642-fe0f","🤸🏽‍♂️":"1f938-1f3fd-200d-2642-fe0f","🤸🏾‍♂️":"1f938-1f3fe-200d-2642-fe0f","🤸🏿‍♂️":"1f938-1f3ff-200d-2642-fe0f","🤸🏻‍♀️":"1f938-1f3fb-200d-2640-fe0f","🤸🏼‍♀️":"1f938-1f3fc-200d-2640-fe0f","🤸🏽‍♀️":"1f938-1f3fd-200d-2640-fe0f","🤸🏾‍♀️":"1f938-1f3fe-200d-2640-fe0f","🤸🏿‍♀️":"1f938-1f3ff-200d-2640-fe0f","🤽🏻‍♂️":"1f93d-1f3fb-200d-2642-fe0f","🤽🏼‍♂️":"1f93d-1f3fc-200d-2642-fe0f","🤽🏽‍♂️":"1f93d-1f3fd-200d-2642-fe0f","🤽🏾‍♂️":"1f93d-1f3fe-200d-2642-fe0f","🤽🏿‍♂️":"1f93d-1f3ff-200d-2642-fe0f","🤽🏻‍♀️":"1f93d-1f3fb-200d-2640-fe0f","🤽🏼‍♀️":"1f93d-1f3fc-200d-2640-fe0f","🤽🏽‍♀️":"1f93d-1f3fd-200d-2640-fe0f","🤽🏾‍♀️":"1f93d-1f3fe-200d-2640-fe0f","🤽🏿‍♀️":"1f93d-1f3ff-200d-2640-fe0f","🤾🏻‍♂️":"1f93e-1f3fb-200d-2642-fe0f","🤾🏼‍♂️":"1f93e-1f3fc-200d-2642-fe0f","🤾🏽‍♂️":"1f93e-1f3fd-200d-2642-fe0f","🤾🏾‍♂️":"1f93e-1f3fe-200d-2642-fe0f","🤾🏿‍♂️":"1f93e-1f3ff-200d-2642-fe0f","🤾🏻‍♀️":"1f93e-1f3fb-200d-2640-fe0f","🤾🏼‍♀️":"1f93e-1f3fc-200d-2640-fe0f","🤾🏽‍♀️":"1f93e-1f3fd-200d-2640-fe0f","🤾🏾‍♀️":"1f93e-1f3fe-200d-2640-fe0f","🤾🏿‍♀️":"1f93e-1f3ff-200d-2640-fe0f","🤹🏻‍♂️":"1f939-1f3fb-200d-2642-fe0f","🤹🏼‍♂️":"1f939-1f3fc-200d-2642-fe0f","🤹🏽‍♂️":"1f939-1f3fd-200d-2642-fe0f","🤹🏾‍♂️":"1f939-1f3fe-200d-2642-fe0f","🤹🏿‍♂️":"1f939-1f3ff-200d-2642-fe0f","🤹🏻‍♀️":"1f939-1f3fb-200d-2640-fe0f","🤹🏼‍♀️":"1f939-1f3fc-200d-2640-fe0f","🤹🏽‍♀️":"1f939-1f3fd-200d-2640-fe0f","🤹🏾‍♀️":"1f939-1f3fe-200d-2640-fe0f","🤹🏿‍♀️":"1f939-1f3ff-200d-2640-fe0f","🧘🏻‍♂️":"1f9d8-1f3fb-200d-2642-fe0f","🧘🏼‍♂️":"1f9d8-1f3fc-200d-2642-fe0f","🧘🏽‍♂️":"1f9d8-1f3fd-200d-2642-fe0f","🧘🏾‍♂️":"1f9d8-1f3fe-200d-2642-fe0f","🧘🏿‍♂️":"1f9d8-1f3ff-200d-2642-fe0f","🧘🏻‍♀️":"1f9d8-1f3fb-200d-2640-fe0f","🧘🏼‍♀️":"1f9d8-1f3fc-200d-2640-fe0f","🧘🏽‍♀️":"1f9d8-1f3fd-200d-2640-fe0f","🧘🏾‍♀️":"1f9d8-1f3fe-200d-2640-fe0f","🧘🏿‍♀️":"1f9d8-1f3ff-200d-2640-fe0f","🧑‍🤝‍🧑":"1f9d1-200d-1f91d-200d-1f9d1","👩‍❤‍👨":"1f469-200d-2764-fe0f-200d-1f468","👨‍❤‍👨":"1f468-200d-2764-fe0f-200d-1f468","👩‍❤‍👩":"1f469-200d-2764-fe0f-200d-1f469","👨‍👩‍👦":"1f468-200d-1f469-200d-1f466","👨‍👩‍👧":"1f468-200d-1f469-200d-1f467","👨‍👨‍👦":"1f468-200d-1f468-200d-1f466","👨‍👨‍👧":"1f468-200d-1f468-200d-1f467","👩‍👩‍👦":"1f469-200d-1f469-200d-1f466","👩‍👩‍👧":"1f469-200d-1f469-200d-1f467","👨‍👦‍👦":"1f468-200d-1f466-200d-1f466","👨‍👧‍👦":"1f468-200d-1f467-200d-1f466","👨‍👧‍👧":"1f468-200d-1f467-200d-1f467","👩‍👦‍👦":"1f469-200d-1f466-200d-1f466","👩‍👧‍👦":"1f469-200d-1f467-200d-1f466","👩‍👧‍👧":"1f469-200d-1f467-200d-1f467","🏳️‍⚧️":"1f3f3-fe0f-200d-26a7-fe0f","👩‍❤️‍👨":"1f469-200d-2764-fe0f-200d-1f468","👨‍❤️‍👨":"1f468-200d-2764-fe0f-200d-1f468","👩‍❤️‍👩":"1f469-200d-2764-fe0f-200d-1f469","🧑🏻‍🤝‍🧑🏻":"1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3fb","🧑🏻‍🤝‍🧑🏼":"1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3fc","🧑🏻‍🤝‍🧑🏽":"1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3fd","🧑🏻‍🤝‍🧑🏾":"1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3fe","🧑🏻‍🤝‍🧑🏿":"1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3ff","🧑🏼‍🤝‍🧑🏻":"1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3fb","🧑🏼‍🤝‍🧑🏼":"1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3fc","🧑🏼‍🤝‍🧑🏽":"1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3fd","🧑🏼‍🤝‍🧑🏾":"1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3fe","🧑🏼‍🤝‍🧑🏿":"1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3ff","🧑🏽‍🤝‍🧑🏻":"1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3fb","🧑🏽‍🤝‍🧑🏼":"1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3fc","🧑🏽‍🤝‍🧑🏽":"1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3fd","🧑🏽‍🤝‍🧑🏾":"1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3fe","🧑🏽‍🤝‍🧑🏿":"1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3ff","🧑🏾‍🤝‍🧑🏻":"1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3fb","🧑🏾‍🤝‍🧑🏼":"1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3fc","🧑🏾‍🤝‍🧑🏽":"1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3fd","🧑🏾‍🤝‍🧑🏾":"1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3fe","🧑🏾‍🤝‍🧑🏿":"1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3ff","🧑🏿‍🤝‍🧑🏻":"1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3fb","🧑🏿‍🤝‍🧑🏼":"1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3fc","🧑🏿‍🤝‍🧑🏽":"1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3fd","🧑🏿‍🤝‍🧑🏾":"1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3fe","🧑🏿‍🤝‍🧑🏿":"1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3ff","👩🏻‍🤝‍👩🏼":"1f469-1f3fb-200d-1f91d-200d-1f469-1f3fc","👩🏻‍🤝‍👩🏽":"1f469-1f3fb-200d-1f91d-200d-1f469-1f3fd","👩🏻‍🤝‍👩🏾":"1f469-1f3fb-200d-1f91d-200d-1f469-1f3fe","👩🏻‍🤝‍👩🏿":"1f469-1f3fb-200d-1f91d-200d-1f469-1f3ff","👩🏼‍🤝‍👩🏻":"1f469-1f3fc-200d-1f91d-200d-1f469-1f3fb","👩🏼‍🤝‍👩🏽":"1f469-1f3fc-200d-1f91d-200d-1f469-1f3fd","👩🏼‍🤝‍👩🏾":"1f469-1f3fc-200d-1f91d-200d-1f469-1f3fe","👩🏼‍🤝‍👩🏿":"1f469-1f3fc-200d-1f91d-200d-1f469-1f3ff","👩🏽‍🤝‍👩🏻":"1f469-1f3fd-200d-1f91d-200d-1f469-1f3fb","👩🏽‍🤝‍👩🏼":"1f469-1f3fd-200d-1f91d-200d-1f469-1f3fc","👩🏽‍🤝‍👩🏾":"1f469-1f3fd-200d-1f91d-200d-1f469-1f3fe","👩🏽‍🤝‍👩🏿":"1f469-1f3fd-200d-1f91d-200d-1f469-1f3ff","👩🏾‍🤝‍👩🏻":"1f469-1f3fe-200d-1f91d-200d-1f469-1f3fb","👩🏾‍🤝‍👩🏼":"1f469-1f3fe-200d-1f91d-200d-1f469-1f3fc","👩🏾‍🤝‍👩🏽":"1f469-1f3fe-200d-1f91d-200d-1f469-1f3fd","👩🏾‍🤝‍👩🏿":"1f469-1f3fe-200d-1f91d-200d-1f469-1f3ff","👩🏿‍🤝‍👩🏻":"1f469-1f3ff-200d-1f91d-200d-1f469-1f3fb","👩🏿‍🤝‍👩🏼":"1f469-1f3ff-200d-1f91d-200d-1f469-1f3fc","👩🏿‍🤝‍👩🏽":"1f469-1f3ff-200d-1f91d-200d-1f469-1f3fd","👩🏿‍🤝‍👩🏾":"1f469-1f3ff-200d-1f91d-200d-1f469-1f3fe","👩🏻‍🤝‍👨🏼":"1f469-1f3fb-200d-1f91d-200d-1f468-1f3fc","👩🏻‍🤝‍👨🏽":"1f469-1f3fb-200d-1f91d-200d-1f468-1f3fd","👩🏻‍🤝‍👨🏾":"1f469-1f3fb-200d-1f91d-200d-1f468-1f3fe","👩🏻‍🤝‍👨🏿":"1f469-1f3fb-200d-1f91d-200d-1f468-1f3ff","👩🏼‍🤝‍👨🏻":"1f469-1f3fc-200d-1f91d-200d-1f468-1f3fb","👩🏼‍🤝‍👨🏽":"1f469-1f3fc-200d-1f91d-200d-1f468-1f3fd","👩🏼‍🤝‍👨🏾":"1f469-1f3fc-200d-1f91d-200d-1f468-1f3fe","👩🏼‍🤝‍👨🏿":"1f469-1f3fc-200d-1f91d-200d-1f468-1f3ff","👩🏽‍🤝‍👨🏻":"1f469-1f3fd-200d-1f91d-200d-1f468-1f3fb","👩🏽‍🤝‍👨🏼":"1f469-1f3fd-200d-1f91d-200d-1f468-1f3fc","👩🏽‍🤝‍👨🏾":"1f469-1f3fd-200d-1f91d-200d-1f468-1f3fe","👩🏽‍🤝‍👨🏿":"1f469-1f3fd-200d-1f91d-200d-1f468-1f3ff","👩🏾‍🤝‍👨🏻":"1f469-1f3fe-200d-1f91d-200d-1f468-1f3fb","👩🏾‍🤝‍👨🏼":"1f469-1f3fe-200d-1f91d-200d-1f468-1f3fc","👩🏾‍🤝‍👨🏽":"1f469-1f3fe-200d-1f91d-200d-1f468-1f3fd","👩🏾‍🤝‍👨🏿":"1f469-1f3fe-200d-1f91d-200d-1f468-1f3ff","👩🏿‍🤝‍👨🏻":"1f469-1f3ff-200d-1f91d-200d-1f468-1f3fb","👩🏿‍🤝‍👨🏼":"1f469-1f3ff-200d-1f91d-200d-1f468-1f3fc","👩🏿‍🤝‍👨🏽":"1f469-1f3ff-200d-1f91d-200d-1f468-1f3fd","👩🏿‍🤝‍👨🏾":"1f469-1f3ff-200d-1f91d-200d-1f468-1f3fe","👨🏻‍🤝‍👨🏼":"1f468-1f3fb-200d-1f91d-200d-1f468-1f3fc","👨🏻‍🤝‍👨🏽":"1f468-1f3fb-200d-1f91d-200d-1f468-1f3fd","👨🏻‍🤝‍👨🏾":"1f468-1f3fb-200d-1f91d-200d-1f468-1f3fe","👨🏻‍🤝‍👨🏿":"1f468-1f3fb-200d-1f91d-200d-1f468-1f3ff","👨🏼‍🤝‍👨🏻":"1f468-1f3fc-200d-1f91d-200d-1f468-1f3fb","👨🏼‍🤝‍👨🏽":"1f468-1f3fc-200d-1f91d-200d-1f468-1f3fd","👨🏼‍🤝‍👨🏾":"1f468-1f3fc-200d-1f91d-200d-1f468-1f3fe","👨🏼‍🤝‍👨🏿":"1f468-1f3fc-200d-1f91d-200d-1f468-1f3ff","👨🏽‍🤝‍👨🏻":"1f468-1f3fd-200d-1f91d-200d-1f468-1f3fb","👨🏽‍🤝‍👨🏼":"1f468-1f3fd-200d-1f91d-200d-1f468-1f3fc","👨🏽‍🤝‍👨🏾":"1f468-1f3fd-200d-1f91d-200d-1f468-1f3fe","👨🏽‍🤝‍👨🏿":"1f468-1f3fd-200d-1f91d-200d-1f468-1f3ff","👨🏾‍🤝‍👨🏻":"1f468-1f3fe-200d-1f91d-200d-1f468-1f3fb","👨🏾‍🤝‍👨🏼":"1f468-1f3fe-200d-1f91d-200d-1f468-1f3fc","👨🏾‍🤝‍👨🏽":"1f468-1f3fe-200d-1f91d-200d-1f468-1f3fd","👨🏾‍🤝‍👨🏿":"1f468-1f3fe-200d-1f91d-200d-1f468-1f3ff","👨🏿‍🤝‍👨🏻":"1f468-1f3ff-200d-1f91d-200d-1f468-1f3fb","👨🏿‍🤝‍👨🏼":"1f468-1f3ff-200d-1f91d-200d-1f468-1f3fc","👨🏿‍🤝‍👨🏽":"1f468-1f3ff-200d-1f91d-200d-1f468-1f3fd","👨🏿‍🤝‍👨🏾":"1f468-1f3ff-200d-1f91d-200d-1f468-1f3fe","👩‍❤‍💋‍👨":"1f469-200d-2764-fe0f-200d-1f48b-200d-1f468","👨‍❤‍💋‍👨":"1f468-200d-2764-fe0f-200d-1f48b-200d-1f468","👩‍❤‍💋‍👩":"1f469-200d-2764-fe0f-200d-1f48b-200d-1f469","🧑🏻‍❤‍🧑🏼":"1f9d1-1f3fb-200d-2764-fe0f-200d-1f9d1-1f3fc","🧑🏻‍❤‍🧑🏽":"1f9d1-1f3fb-200d-2764-fe0f-200d-1f9d1-1f3fd","🧑🏻‍❤‍🧑🏾":"1f9d1-1f3fb-200d-2764-fe0f-200d-1f9d1-1f3fe","🧑🏻‍❤‍🧑🏿":"1f9d1-1f3fb-200d-2764-fe0f-200d-1f9d1-1f3ff","🧑🏼‍❤‍🧑🏻":"1f9d1-1f3fc-200d-2764-fe0f-200d-1f9d1-1f3fb","🧑🏼‍❤‍🧑🏽":"1f9d1-1f3fc-200d-2764-fe0f-200d-1f9d1-1f3fd","🧑🏼‍❤‍🧑🏾":"1f9d1-1f3fc-200d-2764-fe0f-200d-1f9d1-1f3fe","🧑🏼‍❤‍🧑🏿":"1f9d1-1f3fc-200d-2764-fe0f-200d-1f9d1-1f3ff","🧑🏽‍❤‍🧑🏻":"1f9d1-1f3fd-200d-2764-fe0f-200d-1f9d1-1f3fb","🧑🏽‍❤‍🧑🏼":"1f9d1-1f3fd-200d-2764-fe0f-200d-1f9d1-1f3fc","🧑🏽‍❤‍🧑🏾":"1f9d1-1f3fd-200d-2764-fe0f-200d-1f9d1-1f3fe","🧑🏽‍❤‍🧑🏿":"1f9d1-1f3fd-200d-2764-fe0f-200d-1f9d1-1f3ff","🧑🏾‍❤‍🧑🏻":"1f9d1-1f3fe-200d-2764-fe0f-200d-1f9d1-1f3fb","🧑🏾‍❤‍🧑🏼":"1f9d1-1f3fe-200d-2764-fe0f-200d-1f9d1-1f3fc","🧑🏾‍❤‍🧑🏽":"1f9d1-1f3fe-200d-2764-fe0f-200d-1f9d1-1f3fd","🧑🏾‍❤‍🧑🏿":"1f9d1-1f3fe-200d-2764-fe0f-200d-1f9d1-1f3ff","🧑🏿‍❤‍🧑🏻":"1f9d1-1f3ff-200d-2764-fe0f-200d-1f9d1-1f3fb","🧑🏿‍❤‍🧑🏼":"1f9d1-1f3ff-200d-2764-fe0f-200d-1f9d1-1f3fc","🧑🏿‍❤‍🧑🏽":"1f9d1-1f3ff-200d-2764-fe0f-200d-1f9d1-1f3fd","🧑🏿‍❤‍🧑🏾":"1f9d1-1f3ff-200d-2764-fe0f-200d-1f9d1-1f3fe","👩🏻‍❤‍👨🏻":"1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3fb","👩🏻‍❤‍👨🏼":"1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3fc","👩🏻‍❤‍👨🏽":"1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3fd","👩🏻‍❤‍👨🏾":"1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3fe","👩🏻‍❤‍👨🏿":"1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3ff","👩🏼‍❤‍👨🏻":"1f469-1f3fc-200d-2764-fe0f-200d-1f468-1f3fb","👩🏼‍❤‍👨🏼":"1f469-1f3fc-200d-2764-fe0f-200d-1f468-1f3fc","👩🏼‍❤‍👨🏽":"1f469-1f3fc-200d-2764-fe0f-200d-1f468-1f3fd","👩🏼‍❤‍👨🏾":"1f469-1f3fc-200d-2764-fe0f-200d-1f468-1f3fe","👩🏼‍❤‍👨🏿":"1f469-1f3fc-200d-2764-fe0f-200d-1f468-1f3ff","👩🏽‍❤‍👨🏻":"1f469-1f3fd-200d-2764-fe0f-200d-1f468-1f3fb","👩🏽‍❤‍👨🏼":"1f469-1f3fd-200d-2764-fe0f-200d-1f468-1f3fc","👩🏽‍❤‍👨🏽":"1f469-1f3fd-200d-2764-fe0f-200d-1f468-1f3fd","👩🏽‍❤‍👨🏾":"1f469-1f3fd-200d-2764-fe0f-200d-1f468-1f3fe","👩🏽‍❤‍👨🏿":"1f469-1f3fd-200d-2764-fe0f-200d-1f468-1f3ff","👩🏾‍❤‍👨🏻":"1f469-1f3fe-200d-2764-fe0f-200d-1f468-1f3fb","👩🏾‍❤‍👨🏼":"1f469-1f3fe-200d-2764-fe0f-200d-1f468-1f3fc","👩🏾‍❤‍👨🏽":"1f469-1f3fe-200d-2764-fe0f-200d-1f468-1f3fd","👩🏾‍❤‍👨🏾":"1f469-1f3fe-200d-2764-fe0f-200d-1f468-1f3fe","👩🏾‍❤‍👨🏿":"1f469-1f3fe-200d-2764-fe0f-200d-1f468-1f3ff","👩🏿‍❤‍👨🏻":"1f469-1f3ff-200d-2764-fe0f-200d-1f468-1f3fb","👩🏿‍❤‍👨🏼":"1f469-1f3ff-200d-2764-fe0f-200d-1f468-1f3fc","👩🏿‍❤‍👨🏽":"1f469-1f3ff-200d-2764-fe0f-200d-1f468-1f3fd","👩🏿‍❤‍👨🏾":"1f469-1f3ff-200d-2764-fe0f-200d-1f468-1f3fe","👩🏿‍❤‍👨🏿":"1f469-1f3ff-200d-2764-fe0f-200d-1f468-1f3ff","👨🏻‍❤‍👨🏻":"1f468-1f3fb-200d-2764-fe0f-200d-1f468-1f3fb","👨🏻‍❤‍👨🏼":"1f468-1f3fb-200d-2764-fe0f-200d-1f468-1f3fc","👨🏻‍❤‍👨🏽":"1f468-1f3fb-200d-2764-fe0f-200d-1f468-1f3fd","👨🏻‍❤‍👨🏾":"1f468-1f3fb-200d-2764-fe0f-200d-1f468-1f3fe","👨🏻‍❤‍👨🏿":"1f468-1f3fb-200d-2764-fe0f-200d-1f468-1f3ff","👨🏼‍❤‍👨🏻":"1f468-1f3fc-200d-2764-fe0f-200d-1f468-1f3fb","👨🏼‍❤‍👨🏼":"1f468-1f3fc-200d-2764-fe0f-200d-1f468-1f3fc","👨🏼‍❤‍👨🏽":"1f468-1f3fc-200d-2764-fe0f-200d-1f468-1f3fd","👨🏼‍❤‍👨🏾":"1f468-1f3fc-200d-2764-fe0f-200d-1f468-1f3fe","👨🏼‍❤‍👨🏿":"1f468-1f3fc-200d-2764-fe0f-200d-1f468-1f3ff","👨🏽‍❤‍👨🏻":"1f468-1f3fd-200d-2764-fe0f-200d-1f468-1f3fb","👨🏽‍❤‍👨🏼":"1f468-1f3fd-200d-2764-fe0f-200d-1f468-1f3fc","👨🏽‍❤‍👨🏽":"1f468-1f3fd-200d-2764-fe0f-200d-1f468-1f3fd","👨🏽‍❤‍👨🏾":"1f468-1f3fd-200d-2764-fe0f-200d-1f468-1f3fe","👨🏽‍❤‍👨🏿":"1f468-1f3fd-200d-2764-fe0f-200d-1f468-1f3ff","👨🏾‍❤‍👨🏻":"1f468-1f3fe-200d-2764-fe0f-200d-1f468-1f3fb","👨🏾‍❤‍👨🏼":"1f468-1f3fe-200d-2764-fe0f-200d-1f468-1f3fc","👨🏾‍❤‍👨🏽":"1f468-1f3fe-200d-2764-fe0f-200d-1f468-1f3fd","👨🏾‍❤‍👨🏾":"1f468-1f3fe-200d-2764-fe0f-200d-1f468-1f3fe","👨🏾‍❤‍👨🏿":"1f468-1f3fe-200d-2764-fe0f-200d-1f468-1f3ff","👨🏿‍❤‍👨🏻":"1f468-1f3ff-200d-2764-fe0f-200d-1f468-1f3fb","👨🏿‍❤‍👨🏼":"1f468-1f3ff-200d-2764-fe0f-200d-1f468-1f3fc","👨🏿‍❤‍👨🏽":"1f468-1f3ff-200d-2764-fe0f-200d-1f468-1f3fd","👨🏿‍❤‍👨🏾":"1f468-1f3ff-200d-2764-fe0f-200d-1f468-1f3fe","👨🏿‍❤‍👨🏿":"1f468-1f3ff-200d-2764-fe0f-200d-1f468-1f3ff","👩🏻‍❤‍👩🏻":"1f469-1f3fb-200d-2764-fe0f-200d-1f469-1f3fb","👩🏻‍❤‍👩🏼":"1f469-1f3fb-200d-2764-fe0f-200d-1f469-1f3fc","👩🏻‍❤‍👩🏽":"1f469-1f3fb-200d-2764-fe0f-200d-1f469-1f3fd","👩🏻‍❤‍👩🏾":"1f469-1f3fb-200d-2764-fe0f-200d-1f469-1f3fe","👩🏻‍❤‍👩🏿":"1f469-1f3fb-200d-2764-fe0f-200d-1f469-1f3ff","👩🏼‍❤‍👩🏻":"1f469-1f3fc-200d-2764-fe0f-200d-1f469-1f3fb","👩🏼‍❤‍👩🏼":"1f469-1f3fc-200d-2764-fe0f-200d-1f469-1f3fc","👩🏼‍❤‍👩🏽":"1f469-1f3fc-200d-2764-fe0f-200d-1f469-1f3fd","👩🏼‍❤‍👩🏾":"1f469-1f3fc-200d-2764-fe0f-200d-1f469-1f3fe","👩🏼‍❤‍👩🏿":"1f469-1f3fc-200d-2764-fe0f-200d-1f469-1f3ff","👩🏽‍❤‍👩🏻":"1f469-1f3fd-200d-2764-fe0f-200d-1f469-1f3fb","👩🏽‍❤‍👩🏼":"1f469-1f3fd-200d-2764-fe0f-200d-1f469-1f3fc","👩🏽‍❤‍👩🏽":"1f469-1f3fd-200d-2764-fe0f-200d-1f469-1f3fd","👩🏽‍❤‍👩🏾":"1f469-1f3fd-200d-2764-fe0f-200d-1f469-1f3fe","👩🏽‍❤‍👩🏿":"1f469-1f3fd-200d-2764-fe0f-200d-1f469-1f3ff","👩🏾‍❤‍👩🏻":"1f469-1f3fe-200d-2764-fe0f-200d-1f469-1f3fb","👩🏾‍❤‍👩🏼":"1f469-1f3fe-200d-2764-fe0f-200d-1f469-1f3fc","👩🏾‍❤‍👩🏽":"1f469-1f3fe-200d-2764-fe0f-200d-1f469-1f3fd","👩🏾‍❤‍👩🏾":"1f469-1f3fe-200d-2764-fe0f-200d-1f469-1f3fe","👩🏾‍❤‍👩🏿":"1f469-1f3fe-200d-2764-fe0f-200d-1f469-1f3ff","👩🏿‍❤‍👩🏻":"1f469-1f3ff-200d-2764-fe0f-200d-1f469-1f3fb","👩🏿‍❤‍👩🏼":"1f469-1f3ff-200d-2764-fe0f-200d-1f469-1f3fc","👩🏿‍❤‍👩🏽":"1f469-1f3ff-200d-2764-fe0f-200d-1f469-1f3fd","👩🏿‍❤‍👩🏾":"1f469-1f3ff-200d-2764-fe0f-200d-1f469-1f3fe","👩🏿‍❤‍👩🏿":"1f469-1f3ff-200d-2764-fe0f-200d-1f469-1f3ff","👨‍👩‍👧‍👦":"1f468-200d-1f469-200d-1f467-200d-1f466","👨‍👩‍👦‍👦":"1f468-200d-1f469-200d-1f466-200d-1f466","👨‍👩‍👧‍👧":"1f468-200d-1f469-200d-1f467-200d-1f467","👨‍👨‍👧‍👦":"1f468-200d-1f468-200d-1f467-200d-1f466","👨‍👨‍👦‍👦":"1f468-200d-1f468-200d-1f466-200d-1f466","👨‍👨‍👧‍👧":"1f468-200d-1f468-200d-1f467-200d-1f467","👩‍👩‍👧‍👦":"1f469-200d-1f469-200d-1f467-200d-1f466","👩‍👩‍👦‍👦":"1f469-200d-1f469-200d-1f466-200d-1f466","👩‍👩‍👧‍👧":"1f469-200d-1f469-200d-1f467-200d-1f467","🏴󠁧󠁢󠁥󠁮󠁧󠁿":"1f3f4-e0067-e0062-e0065-e006e-e0067-e007f","🏴󠁧󠁢󠁳󠁣󠁴󠁿":"1f3f4-e0067-e0062-e0073-e0063-e0074-e007f","🏴󠁧󠁢󠁷󠁬󠁳󠁿":"1f3f4-e0067-e0062-e0077-e006c-e0073-e007f","👩‍❤️‍💋‍👨":"1f469-200d-2764-fe0f-200d-1f48b-200d-1f468","👨‍❤️‍💋‍👨":"1f468-200d-2764-fe0f-200d-1f48b-200d-1f468","👩‍❤️‍💋‍👩":"1f469-200d-2764-fe0f-200d-1f48b-200d-1f469","🧑🏻‍❤️‍🧑🏼":"1f9d1-1f3fb-200d-2764-fe0f-200d-1f9d1-1f3fc","🧑🏻‍❤️‍🧑🏽":"1f9d1-1f3fb-200d-2764-fe0f-200d-1f9d1-1f3fd","🧑🏻‍❤️‍🧑🏾":"1f9d1-1f3fb-200d-2764-fe0f-200d-1f9d1-1f3fe","🧑🏻‍❤️‍🧑🏿":"1f9d1-1f3fb-200d-2764-fe0f-200d-1f9d1-1f3ff","🧑🏼‍❤️‍🧑🏻":"1f9d1-1f3fc-200d-2764-fe0f-200d-1f9d1-1f3fb","🧑🏼‍❤️‍🧑🏽":"1f9d1-1f3fc-200d-2764-fe0f-200d-1f9d1-1f3fd","🧑🏼‍❤️‍🧑🏾":"1f9d1-1f3fc-200d-2764-fe0f-200d-1f9d1-1f3fe","🧑🏼‍❤️‍🧑🏿":"1f9d1-1f3fc-200d-2764-fe0f-200d-1f9d1-1f3ff","🧑🏽‍❤️‍🧑🏻":"1f9d1-1f3fd-200d-2764-fe0f-200d-1f9d1-1f3fb","🧑🏽‍❤️‍🧑🏼":"1f9d1-1f3fd-200d-2764-fe0f-200d-1f9d1-1f3fc","🧑🏽‍❤️‍🧑🏾":"1f9d1-1f3fd-200d-2764-fe0f-200d-1f9d1-1f3fe","🧑🏽‍❤️‍🧑🏿":"1f9d1-1f3fd-200d-2764-fe0f-200d-1f9d1-1f3ff","🧑🏾‍❤️‍🧑🏻":"1f9d1-1f3fe-200d-2764-fe0f-200d-1f9d1-1f3fb","🧑🏾‍❤️‍🧑🏼":"1f9d1-1f3fe-200d-2764-fe0f-200d-1f9d1-1f3fc","🧑🏾‍❤️‍🧑🏽":"1f9d1-1f3fe-200d-2764-fe0f-200d-1f9d1-1f3fd","🧑🏾‍❤️‍🧑🏿":"1f9d1-1f3fe-200d-2764-fe0f-200d-1f9d1-1f3ff","🧑🏿‍❤️‍🧑🏻":"1f9d1-1f3ff-200d-2764-fe0f-200d-1f9d1-1f3fb","🧑🏿‍❤️‍🧑🏼":"1f9d1-1f3ff-200d-2764-fe0f-200d-1f9d1-1f3fc","🧑🏿‍❤️‍🧑🏽":"1f9d1-1f3ff-200d-2764-fe0f-200d-1f9d1-1f3fd","🧑🏿‍❤️‍🧑🏾":"1f9d1-1f3ff-200d-2764-fe0f-200d-1f9d1-1f3fe","👩🏻‍❤️‍👨🏻":"1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3fb","👩🏻‍❤️‍👨🏼":"1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3fc","👩🏻‍❤️‍👨🏽":"1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3fd","👩🏻‍❤️‍👨🏾":"1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3fe","👩🏻‍❤️‍👨🏿":"1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3ff","👩🏼‍❤️‍👨🏻":"1f469-1f3fc-200d-2764-fe0f-200d-1f468-1f3fb","👩🏼‍❤️‍👨🏼":"1f469-1f3fc-200d-2764-fe0f-200d-1f468-1f3fc","👩🏼‍❤️‍👨🏽":"1f469-1f3fc-200d-2764-fe0f-200d-1f468-1f3fd","👩🏼‍❤️‍👨🏾":"1f469-1f3fc-200d-2764-fe0f-200d-1f468-1f3fe","👩🏼‍❤️‍👨🏿":"1f469-1f3fc-200d-2764-fe0f-200d-1f468-1f3ff","👩🏽‍❤️‍👨🏻":"1f469-1f3fd-200d-2764-fe0f-200d-1f468-1f3fb","👩🏽‍❤️‍👨🏼":"1f469-1f3fd-200d-2764-fe0f-200d-1f468-1f3fc","👩🏽‍❤️‍👨🏽":"1f469-1f3fd-200d-2764-fe0f-200d-1f468-1f3fd","👩🏽‍❤️‍👨🏾":"1f469-1f3fd-200d-2764-fe0f-200d-1f468-1f3fe","👩🏽‍❤️‍👨🏿":"1f469-1f3fd-200d-2764-fe0f-200d-1f468-1f3ff","👩🏾‍❤️‍👨🏻":"1f469-1f3fe-200d-2764-fe0f-200d-1f468-1f3fb","👩🏾‍❤️‍👨🏼":"1f469-1f3fe-200d-2764-fe0f-200d-1f468-1f3fc","👩🏾‍❤️‍👨🏽":"1f469-1f3fe-200d-2764-fe0f-200d-1f468-1f3fd","👩🏾‍❤️‍👨🏾":"1f469-1f3fe-200d-2764-fe0f-200d-1f468-1f3fe","👩🏾‍❤️‍👨🏿":"1f469-1f3fe-200d-2764-fe0f-200d-1f468-1f3ff","👩🏿‍❤️‍👨🏻":"1f469-1f3ff-200d-2764-fe0f-200d-1f468-1f3fb","👩🏿‍❤️‍👨🏼":"1f469-1f3ff-200d-2764-fe0f-200d-1f468-1f3fc","👩🏿‍❤️‍👨🏽":"1f469-1f3ff-200d-2764-fe0f-200d-1f468-1f3fd","👩🏿‍❤️‍👨🏾":"1f469-1f3ff-200d-2764-fe0f-200d-1f468-1f3fe","👩🏿‍❤️‍👨🏿":"1f469-1f3ff-200d-2764-fe0f-200d-1f468-1f3ff","👨🏻‍❤️‍👨🏻":"1f468-1f3fb-200d-2764-fe0f-200d-1f468-1f3fb","👨🏻‍❤️‍👨🏼":"1f468-1f3fb-200d-2764-fe0f-200d-1f468-1f3fc","👨🏻‍❤️‍👨🏽":"1f468-1f3fb-200d-2764-fe0f-200d-1f468-1f3fd","👨🏻‍❤️‍👨🏾":"1f468-1f3fb-200d-2764-fe0f-200d-1f468-1f3fe","👨🏻‍❤️‍👨🏿":"1f468-1f3fb-200d-2764-fe0f-200d-1f468-1f3ff","👨🏼‍❤️‍👨🏻":"1f468-1f3fc-200d-2764-fe0f-200d-1f468-1f3fb","👨🏼‍❤️‍👨🏼":"1f468-1f3fc-200d-2764-fe0f-200d-1f468-1f3fc","👨🏼‍❤️‍👨🏽":"1f468-1f3fc-200d-2764-fe0f-200d-1f468-1f3fd","👨🏼‍❤️‍👨🏾":"1f468-1f3fc-200d-2764-fe0f-200d-1f468-1f3fe","👨🏼‍❤️‍👨🏿":"1f468-1f3fc-200d-2764-fe0f-200d-1f468-1f3ff","👨🏽‍❤️‍👨🏻":"1f468-1f3fd-200d-2764-fe0f-200d-1f468-1f3fb","👨🏽‍❤️‍👨🏼":"1f468-1f3fd-200d-2764-fe0f-200d-1f468-1f3fc","👨🏽‍❤️‍👨🏽":"1f468-1f3fd-200d-2764-fe0f-200d-1f468-1f3fd","👨🏽‍❤️‍👨🏾":"1f468-1f3fd-200d-2764-fe0f-200d-1f468-1f3fe","👨🏽‍❤️‍👨🏿":"1f468-1f3fd-200d-2764-fe0f-200d-1f468-1f3ff","👨🏾‍❤️‍👨🏻":"1f468-1f3fe-200d-2764-fe0f-200d-1f468-1f3fb","👨🏾‍❤️‍👨🏼":"1f468-1f3fe-200d-2764-fe0f-200d-1f468-1f3fc","👨🏾‍❤️‍👨🏽":"1f468-1f3fe-200d-2764-fe0f-200d-1f468-1f3fd","👨🏾‍❤️‍👨🏾":"1f468-1f3fe-200d-2764-fe0f-200d-1f468-1f3fe","👨🏾‍❤️‍👨🏿":"1f468-1f3fe-200d-2764-fe0f-200d-1f468-1f3ff","👨🏿‍❤️‍👨🏻":"1f468-1f3ff-200d-2764-fe0f-200d-1f468-1f3fb","👨🏿‍❤️‍👨🏼":"1f468-1f3ff-200d-2764-fe0f-200d-1f468-1f3fc","👨🏿‍❤️‍👨🏽":"1f468-1f3ff-200d-2764-fe0f-200d-1f468-1f3fd","👨🏿‍❤️‍👨🏾":"1f468-1f3ff-200d-2764-fe0f-200d-1f468-1f3fe","👨🏿‍❤️‍👨🏿":"1f468-1f3ff-200d-2764-fe0f-200d-1f468-1f3ff","👩🏻‍❤️‍👩🏻":"1f469-1f3fb-200d-2764-fe0f-200d-1f469-1f3fb","👩🏻‍❤️‍👩🏼":"1f469-1f3fb-200d-2764-fe0f-200d-1f469-1f3fc","👩🏻‍❤️‍👩🏽":"1f469-1f3fb-200d-2764-fe0f-200d-1f469-1f3fd","👩🏻‍❤️‍👩🏾":"1f469-1f3fb-200d-2764-fe0f-200d-1f469-1f3fe","👩🏻‍❤️‍👩🏿":"1f469-1f3fb-200d-2764-fe0f-200d-1f469-1f3ff","👩🏼‍❤️‍👩🏻":"1f469-1f3fc-200d-2764-fe0f-200d-1f469-1f3fb","👩🏼‍❤️‍👩🏼":"1f469-1f3fc-200d-2764-fe0f-200d-1f469-1f3fc","👩🏼‍❤️‍👩🏽":"1f469-1f3fc-200d-2764-fe0f-200d-1f469-1f3fd","👩🏼‍❤️‍👩🏾":"1f469-1f3fc-200d-2764-fe0f-200d-1f469-1f3fe","👩🏼‍❤️‍👩🏿":"1f469-1f3fc-200d-2764-fe0f-200d-1f469-1f3ff","👩🏽‍❤️‍👩🏻":"1f469-1f3fd-200d-2764-fe0f-200d-1f469-1f3fb","👩🏽‍❤️‍👩🏼":"1f469-1f3fd-200d-2764-fe0f-200d-1f469-1f3fc","👩🏽‍❤️‍👩🏽":"1f469-1f3fd-200d-2764-fe0f-200d-1f469-1f3fd","👩🏽‍❤️‍👩🏾":"1f469-1f3fd-200d-2764-fe0f-200d-1f469-1f3fe","👩🏽‍❤️‍👩🏿":"1f469-1f3fd-200d-2764-fe0f-200d-1f469-1f3ff","👩🏾‍❤️‍👩🏻":"1f469-1f3fe-200d-2764-fe0f-200d-1f469-1f3fb","👩🏾‍❤️‍👩🏼":"1f469-1f3fe-200d-2764-fe0f-200d-1f469-1f3fc","👩🏾‍❤️‍👩🏽":"1f469-1f3fe-200d-2764-fe0f-200d-1f469-1f3fd","👩🏾‍❤️‍👩🏾":"1f469-1f3fe-200d-2764-fe0f-200d-1f469-1f3fe","👩🏾‍❤️‍👩🏿":"1f469-1f3fe-200d-2764-fe0f-200d-1f469-1f3ff","👩🏿‍❤️‍👩🏻":"1f469-1f3ff-200d-2764-fe0f-200d-1f469-1f3fb","👩🏿‍❤️‍👩🏼":"1f469-1f3ff-200d-2764-fe0f-200d-1f469-1f3fc","👩🏿‍❤️‍👩🏽":"1f469-1f3ff-200d-2764-fe0f-200d-1f469-1f3fd","👩🏿‍❤️‍👩🏾":"1f469-1f3ff-200d-2764-fe0f-200d-1f469-1f3fe","👩🏿‍❤️‍👩🏿":"1f469-1f3ff-200d-2764-fe0f-200d-1f469-1f3ff","🧑🏻‍❤‍💋‍🧑🏼":"1f9d1-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fc","🧑🏻‍❤‍💋‍🧑🏽":"1f9d1-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fd","🧑🏻‍❤‍💋‍🧑🏾":"1f9d1-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fe","🧑🏻‍❤‍💋‍🧑🏿":"1f9d1-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3ff","🧑🏼‍❤‍💋‍🧑🏻":"1f9d1-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fb","🧑🏼‍❤‍💋‍🧑🏽":"1f9d1-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fd","🧑🏼‍❤‍💋‍🧑🏾":"1f9d1-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fe","🧑🏼‍❤‍💋‍🧑🏿":"1f9d1-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3ff","🧑🏽‍❤‍💋‍🧑🏻":"1f9d1-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fb","🧑🏽‍❤‍💋‍🧑🏼":"1f9d1-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fc","🧑🏽‍❤‍💋‍🧑🏾":"1f9d1-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fe","🧑🏽‍❤‍💋‍🧑🏿":"1f9d1-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3ff","🧑🏾‍❤‍💋‍🧑🏻":"1f9d1-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fb","🧑🏾‍❤‍💋‍🧑🏼":"1f9d1-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fc","🧑🏾‍❤‍💋‍🧑🏽":"1f9d1-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fd","🧑🏾‍❤‍💋‍🧑🏿":"1f9d1-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3ff","🧑🏿‍❤‍💋‍🧑🏻":"1f9d1-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fb","🧑🏿‍❤‍💋‍🧑🏼":"1f9d1-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fc","🧑🏿‍❤‍💋‍🧑🏽":"1f9d1-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fd","🧑🏿‍❤‍💋‍🧑🏾":"1f9d1-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fe","👩🏻‍❤‍💋‍👨🏻":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb","👩🏻‍❤‍💋‍👨🏼":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc","👩🏻‍❤‍💋‍👨🏽":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd","👩🏻‍❤‍💋‍👨🏾":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe","👩🏻‍❤‍💋‍👨🏿":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff","👩🏼‍❤‍💋‍👨🏻":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb","👩🏼‍❤‍💋‍👨🏼":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc","👩🏼‍❤‍💋‍👨🏽":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd","👩🏼‍❤‍💋‍👨🏾":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe","👩🏼‍❤‍💋‍👨🏿":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff","👩🏽‍❤‍💋‍👨🏻":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb","👩🏽‍❤‍💋‍👨🏼":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc","👩🏽‍❤‍💋‍👨🏽":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd","👩🏽‍❤‍💋‍👨🏾":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe","👩🏽‍❤‍💋‍👨🏿":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff","👩🏾‍❤‍💋‍👨🏻":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb","👩🏾‍❤‍💋‍👨🏼":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc","👩🏾‍❤‍💋‍👨🏽":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd","👩🏾‍❤‍💋‍👨🏾":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe","👩🏾‍❤‍💋‍👨🏿":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff","👩🏿‍❤‍💋‍👨🏻":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb","👩🏿‍❤‍💋‍👨🏼":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc","👩🏿‍❤‍💋‍👨🏽":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd","👩🏿‍❤‍💋‍👨🏾":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe","👩🏿‍❤‍💋‍👨🏿":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff","👨🏻‍❤‍💋‍👨🏻":"1f468-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb","👨🏻‍❤‍💋‍👨🏼":"1f468-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc","👨🏻‍❤‍💋‍👨🏽":"1f468-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd","👨🏻‍❤‍💋‍👨🏾":"1f468-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe","👨🏻‍❤‍💋‍👨🏿":"1f468-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff","👨🏼‍❤‍💋‍👨🏻":"1f468-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb","👨🏼‍❤‍💋‍👨🏼":"1f468-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc","👨🏼‍❤‍💋‍👨🏽":"1f468-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd","👨🏼‍❤‍💋‍👨🏾":"1f468-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe","👨🏼‍❤‍💋‍👨🏿":"1f468-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff","👨🏽‍❤‍💋‍👨🏻":"1f468-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb","👨🏽‍❤‍💋‍👨🏼":"1f468-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc","👨🏽‍❤‍💋‍👨🏽":"1f468-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd","👨🏽‍❤‍💋‍👨🏾":"1f468-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe","👨🏽‍❤‍💋‍👨🏿":"1f468-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff","👨🏾‍❤‍💋‍👨🏻":"1f468-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb","👨🏾‍❤‍💋‍👨🏼":"1f468-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc","👨🏾‍❤‍💋‍👨🏽":"1f468-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd","👨🏾‍❤‍💋‍👨🏾":"1f468-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe","👨🏾‍❤‍💋‍👨🏿":"1f468-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff","👨🏿‍❤‍💋‍👨🏻":"1f468-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb","👨🏿‍❤‍💋‍👨🏼":"1f468-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc","👨🏿‍❤‍💋‍👨🏽":"1f468-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd","👨🏿‍❤‍💋‍👨🏾":"1f468-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe","👨🏿‍❤‍💋‍👨🏿":"1f468-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff","👩🏻‍❤‍💋‍👩🏻":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fb","👩🏻‍❤‍💋‍👩🏼":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fc","👩🏻‍❤‍💋‍👩🏽":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd","👩🏻‍❤‍💋‍👩🏾":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fe","👩🏻‍❤‍💋‍👩🏿":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3ff","👩🏼‍❤‍💋‍👩🏻":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fb","👩🏼‍❤‍💋‍👩🏼":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fc","👩🏼‍❤‍💋‍👩🏽":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd","👩🏼‍❤‍💋‍👩🏾":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fe","👩🏼‍❤‍💋‍👩🏿":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3ff","👩🏽‍❤‍💋‍👩🏻":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fb","👩🏽‍❤‍💋‍👩🏼":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fc","👩🏽‍❤‍💋‍👩🏽":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd","👩🏽‍❤‍💋‍👩🏾":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fe","👩🏽‍❤‍💋‍👩🏿":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3ff","👩🏾‍❤‍💋‍👩🏻":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fb","👩🏾‍❤‍💋‍👩🏼":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fc","👩🏾‍❤‍💋‍👩🏽":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd","👩🏾‍❤‍💋‍👩🏾":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fe","👩🏾‍❤‍💋‍👩🏿":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3ff","👩🏿‍❤‍💋‍👩🏻":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fb","👩🏿‍❤‍💋‍👩🏼":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fc","👩🏿‍❤‍💋‍👩🏽":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd","👩🏿‍❤‍💋‍👩🏾":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fe","👩🏿‍❤‍💋‍👩🏿":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3ff","🧑🏻‍❤️‍💋‍🧑🏼":"1f9d1-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fc","🧑🏻‍❤️‍💋‍🧑🏽":"1f9d1-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fd","🧑🏻‍❤️‍💋‍🧑🏾":"1f9d1-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fe","🧑🏻‍❤️‍💋‍🧑🏿":"1f9d1-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3ff","🧑🏼‍❤️‍💋‍🧑🏻":"1f9d1-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fb","🧑🏼‍❤️‍💋‍🧑🏽":"1f9d1-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fd","🧑🏼‍❤️‍💋‍🧑🏾":"1f9d1-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fe","🧑🏼‍❤️‍💋‍🧑🏿":"1f9d1-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3ff","🧑🏽‍❤️‍💋‍🧑🏻":"1f9d1-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fb","🧑🏽‍❤️‍💋‍🧑🏼":"1f9d1-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fc","🧑🏽‍❤️‍💋‍🧑🏾":"1f9d1-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fe","🧑🏽‍❤️‍💋‍🧑🏿":"1f9d1-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3ff","🧑🏾‍❤️‍💋‍🧑🏻":"1f9d1-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fb","🧑🏾‍❤️‍💋‍🧑🏼":"1f9d1-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fc","🧑🏾‍❤️‍💋‍🧑🏽":"1f9d1-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fd","🧑🏾‍❤️‍💋‍🧑🏿":"1f9d1-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3ff","🧑🏿‍❤️‍💋‍🧑🏻":"1f9d1-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fb","🧑🏿‍❤️‍💋‍🧑🏼":"1f9d1-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fc","🧑🏿‍❤️‍💋‍🧑🏽":"1f9d1-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fd","🧑🏿‍❤️‍💋‍🧑🏾":"1f9d1-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fe","👩🏻‍❤️‍💋‍👨🏻":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb","👩🏻‍❤️‍💋‍👨🏼":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc","👩🏻‍❤️‍💋‍👨🏽":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd","👩🏻‍❤️‍💋‍👨🏾":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe","👩🏻‍❤️‍💋‍👨🏿":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff","👩🏼‍❤️‍💋‍👨🏻":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb","👩🏼‍❤️‍💋‍👨🏼":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc","👩🏼‍❤️‍💋‍👨🏽":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd","👩🏼‍❤️‍💋‍👨🏾":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe","👩🏼‍❤️‍💋‍👨🏿":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff","👩🏽‍❤️‍💋‍👨🏻":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb","👩🏽‍❤️‍💋‍👨🏼":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc","👩🏽‍❤️‍💋‍👨🏽":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd","👩🏽‍❤️‍💋‍👨🏾":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe","👩🏽‍❤️‍💋‍👨🏿":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff","👩🏾‍❤️‍💋‍👨🏻":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb","👩🏾‍❤️‍💋‍👨🏼":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc","👩🏾‍❤️‍💋‍👨🏽":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd","👩🏾‍❤️‍💋‍👨🏾":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe","👩🏾‍❤️‍💋‍👨🏿":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff","👩🏿‍❤️‍💋‍👨🏻":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb","👩🏿‍❤️‍💋‍👨🏼":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc","👩🏿‍❤️‍💋‍👨🏽":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd","👩🏿‍❤️‍💋‍👨🏾":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe","👩🏿‍❤️‍💋‍👨🏿":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff","👨🏻‍❤️‍💋‍👨🏻":"1f468-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb","👨🏻‍❤️‍💋‍👨🏼":"1f468-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc","👨🏻‍❤️‍💋‍👨🏽":"1f468-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd","👨🏻‍❤️‍💋‍👨🏾":"1f468-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe","👨🏻‍❤️‍💋‍👨🏿":"1f468-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff","👨🏼‍❤️‍💋‍👨🏻":"1f468-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb","👨🏼‍❤️‍💋‍👨🏼":"1f468-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc","👨🏼‍❤️‍💋‍👨🏽":"1f468-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd","👨🏼‍❤️‍💋‍👨🏾":"1f468-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe","👨🏼‍❤️‍💋‍👨🏿":"1f468-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff","👨🏽‍❤️‍💋‍👨🏻":"1f468-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb","👨🏽‍❤️‍💋‍👨🏼":"1f468-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc","👨🏽‍❤️‍💋‍👨🏽":"1f468-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd","👨🏽‍❤️‍💋‍👨🏾":"1f468-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe","👨🏽‍❤️‍💋‍👨🏿":"1f468-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff","👨🏾‍❤️‍💋‍👨🏻":"1f468-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb","👨🏾‍❤️‍💋‍👨🏼":"1f468-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc","👨🏾‍❤️‍💋‍👨🏽":"1f468-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd","👨🏾‍❤️‍💋‍👨🏾":"1f468-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe","👨🏾‍❤️‍💋‍👨🏿":"1f468-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff","👨🏿‍❤️‍💋‍👨🏻":"1f468-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb","👨🏿‍❤️‍💋‍👨🏼":"1f468-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc","👨🏿‍❤️‍💋‍👨🏽":"1f468-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd","👨🏿‍❤️‍💋‍👨🏾":"1f468-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe","👨🏿‍❤️‍💋‍👨🏿":"1f468-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff","👩🏻‍❤️‍💋‍👩🏻":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fb","👩🏻‍❤️‍💋‍👩🏼":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fc","👩🏻‍❤️‍💋‍👩🏽":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd","👩🏻‍❤️‍💋‍👩🏾":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fe","👩🏻‍❤️‍💋‍👩🏿":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3ff","👩🏼‍❤️‍💋‍👩🏻":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fb","👩🏼‍❤️‍💋‍👩🏼":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fc","👩🏼‍❤️‍💋‍👩🏽":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd","👩🏼‍❤️‍💋‍👩🏾":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fe","👩🏼‍❤️‍💋‍👩🏿":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3ff","👩🏽‍❤️‍💋‍👩🏻":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fb","👩🏽‍❤️‍💋‍👩🏼":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fc","👩🏽‍❤️‍💋‍👩🏽":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd","👩🏽‍❤️‍💋‍👩🏾":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fe","👩🏽‍❤️‍💋‍👩🏿":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3ff","👩🏾‍❤️‍💋‍👩🏻":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fb","👩🏾‍❤️‍💋‍👩🏼":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fc","👩🏾‍❤️‍💋‍👩🏽":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd","👩🏾‍❤️‍💋‍👩🏾":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fe","👩🏾‍❤️‍💋‍👩🏿":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3ff","👩🏿‍❤️‍💋‍👩🏻":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fb","👩🏿‍❤️‍💋‍👩🏼":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fc","👩🏿‍❤️‍💋‍👩🏽":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd","👩🏿‍❤️‍💋‍👩🏾":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fe","👩🏿‍❤️‍💋‍👩🏿":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3ff"}
\ No newline at end of file
diff --git a/app/javascript/flavours/blobfox/features/emoji/emoji_mart_data_light.ts b/app/javascript/flavours/blobfox/features/emoji/emoji_mart_data_light.ts
new file mode 100644
index 00000000000000..ffca1f8b0630a3
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/emoji/emoji_mart_data_light.ts
@@ -0,0 +1,43 @@
+// The output of this module is designed to mimic emoji-mart's
+// "data" object, such that we can use it for a light version of emoji-mart's
+// emojiIndex.search functionality.
+import type { BaseEmoji } from 'emoji-mart';
+import type { Emoji } from 'emoji-mart/dist-es/utils/data';
+
+import type { Search, ShortCodesToEmojiData } from './emoji_compressed';
+import emojiCompressed from './emoji_compressed';
+import { unicodeToUnifiedName } from './unicode_to_unified_name';
+
+type Emojis = {
+  [key in NonNullable<keyof ShortCodesToEmojiData>]: {
+    native: BaseEmoji['native'];
+    search: Search;
+    short_names: Emoji['short_names'];
+    unified: Emoji['unified'];
+  };
+};
+
+const [
+  shortCodesToEmojiData,
+  skins,
+  categories,
+  short_names,
+  _emojisWithoutShortCodes,
+] = emojiCompressed;
+
+const emojis: Emojis = {};
+
+// decompress
+Object.keys(shortCodesToEmojiData).forEach((shortCode) => {
+  const [_filenameData, searchData] = shortCodesToEmojiData[shortCode];
+  const [native, short_names, search, unified] = searchData;
+
+  emojis[shortCode] = {
+    native,
+    search,
+    short_names: short_names ? [shortCode].concat(short_names) : undefined,
+    unified: unified ?? unicodeToUnifiedName(native),
+  };
+});
+
+export { emojis, skins, categories, short_names };
diff --git a/app/javascript/flavours/blobfox/features/emoji/emoji_mart_search_light.js b/app/javascript/flavours/blobfox/features/emoji/emoji_mart_search_light.js
new file mode 100644
index 00000000000000..83e154b0b287e5
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/emoji/emoji_mart_search_light.js
@@ -0,0 +1,185 @@
+// This code is largely borrowed from:
+// https://github.com/missive/emoji-mart/blob/5f2ffcc/src/utils/emoji-index.js
+
+import * as data from './emoji_mart_data_light';
+import { getData, getSanitizedData, uniq, intersect } from './emoji_utils';
+
+let originalPool = {};
+let index = {};
+let emojisList = {};
+let emoticonsList = {};
+let customEmojisList = [];
+
+for (let emoji in data.emojis) {
+  let emojiData = data.emojis[emoji];
+  let { short_names, emoticons } = emojiData;
+  let id = short_names[0];
+
+  if (emoticons) {
+    emoticons.forEach(emoticon => {
+      if (emoticonsList[emoticon]) {
+        return;
+      }
+
+      emoticonsList[emoticon] = id;
+    });
+  }
+
+  emojisList[id] = getSanitizedData(id);
+  originalPool[id] = emojiData;
+}
+
+function clearCustomEmojis(pool) {
+  customEmojisList.forEach((emoji) => {
+    let emojiId = emoji.id || emoji.short_names[0];
+
+    delete pool[emojiId];
+    delete emojisList[emojiId];
+  });
+}
+
+function addCustomToPool(custom, pool) {
+  if (customEmojisList.length) clearCustomEmojis(pool);
+
+  custom.forEach((emoji) => {
+    let emojiId = emoji.id || emoji.short_names[0];
+
+    if (emojiId && !pool[emojiId]) {
+      pool[emojiId] = getData(emoji);
+      emojisList[emojiId] = getSanitizedData(emoji);
+    }
+  });
+
+  customEmojisList = custom;
+  index = {};
+}
+
+function search(value, { emojisToShowFilter, maxResults, include, exclude, custom } = {}) {
+  if (custom !== undefined) {
+    if (customEmojisList !== custom)
+      addCustomToPool(custom, originalPool);
+  } else {
+    custom = [];
+  }
+
+  maxResults = maxResults || 75;
+  include = include || [];
+  exclude = exclude || [];
+
+  let results = null,
+    pool = originalPool;
+
+  if (value.length) {
+    if (value === '-' || value === '-1') {
+      return [emojisList['-1']];
+    }
+
+    let values = value.toLowerCase().split(/[\s|,\-_]+/),
+      allResults = [];
+
+    if (values.length > 2) {
+      values = [values[0], values[1]];
+    }
+
+    if (include.length || exclude.length) {
+      pool = {};
+
+      data.categories.forEach(category => {
+        let isIncluded = include && include.length ? include.indexOf(category.name.toLowerCase()) > -1 : true;
+        let isExcluded = exclude && exclude.length ? exclude.indexOf(category.name.toLowerCase()) > -1 : false;
+        if (!isIncluded || isExcluded) {
+          return;
+        }
+
+        category.emojis.forEach(emojiId => pool[emojiId] = data.emojis[emojiId]);
+      });
+
+      if (custom.length) {
+        let customIsIncluded = include && include.length ? include.indexOf('custom') > -1 : true;
+        let customIsExcluded = exclude && exclude.length ? exclude.indexOf('custom') > -1 : false;
+        if (customIsIncluded && !customIsExcluded) {
+          addCustomToPool(custom, pool);
+        }
+      }
+    }
+
+    const searchValue = (value) => {
+      let aPool = pool,
+        aIndex = index,
+        length = 0;
+
+      for (let charIndex = 0; charIndex < value.length; charIndex++) {
+        const char = value[charIndex];
+        length++;
+
+        aIndex[char] = aIndex[char] || {};
+        aIndex = aIndex[char];
+
+        if (!aIndex.results) {
+          let scores = {};
+
+          aIndex.results = [];
+          aIndex.pool = {};
+
+          for (let id in aPool) {
+            let emoji = aPool[id],
+              { search } = emoji,
+              sub = value.slice(0, length),
+              subIndex = search.indexOf(sub);
+
+            if (subIndex !== -1) {
+              let score = subIndex + 1;
+              if (sub === id) score = 0;
+
+              aIndex.results.push(emojisList[id]);
+              aIndex.pool[id] = emoji;
+
+              scores[id] = score;
+            }
+          }
+
+          aIndex.results.sort((a, b) => {
+            let aScore = scores[a.id],
+              bScore = scores[b.id];
+
+            return aScore - bScore;
+          });
+        }
+
+        aPool = aIndex.pool;
+      }
+
+      return aIndex.results;
+    };
+
+    if (values.length > 1) {
+      results = searchValue(value);
+    } else {
+      results = [];
+    }
+
+    allResults = values.map(searchValue).filter(a => a);
+
+    if (allResults.length > 1) {
+      allResults = intersect.apply(null, allResults);
+    } else if (allResults.length) {
+      allResults = allResults[0];
+    }
+
+    results = uniq(results.concat(allResults));
+  }
+
+  if (results) {
+    if (emojisToShowFilter) {
+      results = results.filter((result) => emojisToShowFilter(data.emojis[result.id]));
+    }
+
+    if (results && results.length > maxResults) {
+      results = results.slice(0, maxResults);
+    }
+  }
+
+  return results;
+}
+
+export { search };
diff --git a/app/javascript/flavours/blobfox/features/emoji/emoji_picker.js b/app/javascript/flavours/blobfox/features/emoji/emoji_picker.js
new file mode 100644
index 00000000000000..8725d39ecd78a3
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/emoji/emoji_picker.js
@@ -0,0 +1,7 @@
+import Emoji from 'emoji-mart/dist-es/components/emoji/emoji';
+import Picker from 'emoji-mart/dist-es/components/picker/picker';
+
+export {
+  Picker,
+  Emoji,
+};
diff --git a/app/javascript/flavours/blobfox/features/emoji/emoji_unicode_mapping_light.ts b/app/javascript/flavours/blobfox/features/emoji/emoji_unicode_mapping_light.ts
new file mode 100644
index 00000000000000..191419496fb2a9
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/emoji/emoji_unicode_mapping_light.ts
@@ -0,0 +1,60 @@
+// A mapping of unicode strings to an object containing the filename
+// (i.e. the svg filename) and a shortCode intended to be shown
+// as a "title" attribute in an HTML element (aka tooltip).
+
+import type {
+  FilenameData,
+  ShortCodesToEmojiDataKey,
+} from './emoji_compressed';
+import emojiCompressed from './emoji_compressed';
+import { unicodeToFilename } from './unicode_to_filename';
+
+type UnicodeMapping = {
+  [key in FilenameData[number][0]]: {
+    shortCode: ShortCodesToEmojiDataKey;
+    filename: FilenameData[number][number];
+  };
+};
+
+const [
+  shortCodesToEmojiData,
+  _skins,
+  _categories,
+  _short_names,
+  emojisWithoutShortCodes,
+] = emojiCompressed;
+
+// decompress
+const unicodeMapping: UnicodeMapping = {};
+
+function processEmojiMapData(
+  emojiMapData: FilenameData[number],
+  shortCode?: ShortCodesToEmojiDataKey,
+) {
+  const [native, _filename] = emojiMapData;
+  let filename = emojiMapData[1];
+  if (!filename) {
+    // filename name can be derived from unicodeToFilename
+    filename = unicodeToFilename(native);
+  }
+  unicodeMapping[native] = {
+    shortCode,
+    filename,
+  };
+}
+
+Object.keys(shortCodesToEmojiData).forEach(
+  (shortCode: ShortCodesToEmojiDataKey) => {
+    if (shortCode === undefined) return;
+    const [filenameData, _searchData] = shortCodesToEmojiData[shortCode];
+    filenameData.forEach((emojiMapData) => {
+      processEmojiMapData(emojiMapData, shortCode);
+    });
+  },
+);
+
+emojisWithoutShortCodes.forEach((emojiMapData) => {
+  processEmojiMapData(emojiMapData);
+});
+
+export { unicodeMapping };
diff --git a/app/javascript/flavours/blobfox/features/emoji/emoji_utils.js b/app/javascript/flavours/blobfox/features/emoji/emoji_utils.js
new file mode 100644
index 00000000000000..83bcc9d82f9f14
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/emoji/emoji_utils.js
@@ -0,0 +1,258 @@
+// This code is largely borrowed from:
+// https://github.com/missive/emoji-mart/blob/5f2ffcc/src/utils/index.js
+
+import * as data from './emoji_mart_data_light';
+
+const buildSearch = (data) => {
+  const search = [];
+
+  let addToSearch = (strings, split) => {
+    if (!strings) {
+      return;
+    }
+
+    (Array.isArray(strings) ? strings : [strings]).forEach((string) => {
+      (split ? string.split(/[-|_|\s]+/) : [string]).forEach((s) => {
+        s = s.toLowerCase();
+
+        if (search.indexOf(s) === -1) {
+          search.push(s);
+        }
+      });
+    });
+  };
+
+  addToSearch(data.short_names, true);
+  addToSearch(data.name, true);
+  addToSearch(data.keywords, false);
+  addToSearch(data.emoticons, false);
+
+  return search.join(',');
+};
+
+const _String = String;
+
+const stringFromCodePoint = _String.fromCodePoint || function () {
+  let MAX_SIZE = 0x4000;
+  let codeUnits = [];
+  let highSurrogate;
+  let lowSurrogate;
+  let index = -1;
+  let length = arguments.length;
+  if (!length) {
+    return '';
+  }
+  let result = '';
+  while (++index < length) {
+    let codePoint = Number(arguments[index]);
+    if (
+      !isFinite(codePoint) ||       // `NaN`, `+Infinity`, or `-Infinity`
+      codePoint < 0 ||              // not a valid Unicode code point
+      codePoint > 0x10FFFF ||       // not a valid Unicode code point
+      Math.floor(codePoint) !== codePoint // not an integer
+    ) {
+      throw RangeError('Invalid code point: ' + codePoint);
+    }
+    if (codePoint <= 0xFFFF) { // BMP code point
+      codeUnits.push(codePoint);
+    } else { // Astral code point; split in surrogate halves
+      // http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
+      codePoint -= 0x10000;
+      highSurrogate = (codePoint >> 10) + 0xD800;
+      lowSurrogate = (codePoint % 0x400) + 0xDC00;
+      codeUnits.push(highSurrogate, lowSurrogate);
+    }
+    if (index + 1 === length || codeUnits.length > MAX_SIZE) {
+      result += String.fromCharCode.apply(null, codeUnits);
+      codeUnits.length = 0;
+    }
+  }
+  return result;
+};
+
+
+const _JSON = JSON;
+
+const COLONS_REGEX = /^(?::([^:]+):)(?::skin-tone-(\d):)?$/;
+const SKINS = [
+  '1F3FA', '1F3FB', '1F3FC',
+  '1F3FD', '1F3FE', '1F3FF',
+];
+
+function unifiedToNative(unified) {
+  let unicodes = unified.split('-'),
+    codePoints = unicodes.map((u) => `0x${u}`);
+
+  return stringFromCodePoint.apply(null, codePoints);
+}
+
+function sanitize(emoji) {
+  let { name, short_names, skin_tone, skin_variations, emoticons, unified, custom, imageUrl } = emoji,
+    id = emoji.id || short_names[0],
+    colons = `:${id}:`;
+
+  if (custom) {
+    return {
+      id,
+      name,
+      colons,
+      emoticons,
+      custom,
+      imageUrl,
+    };
+  }
+
+  if (skin_tone) {
+    colons += `:skin-tone-${skin_tone}:`;
+  }
+
+  return {
+    id,
+    name,
+    colons,
+    emoticons,
+    unified: unified.toLowerCase(),
+    skin: skin_tone || (skin_variations ? 1 : null),
+    native: unifiedToNative(unified),
+  };
+}
+
+function getSanitizedData() {
+  return sanitize(getData(...arguments));
+}
+
+function getData(emoji, skin, set) {
+  let emojiData = {};
+
+  if (typeof emoji === 'string') {
+    let matches = emoji.match(COLONS_REGEX);
+
+    if (matches) {
+      emoji = matches[1];
+
+      if (matches[2]) {
+        skin = parseInt(matches[2]);
+      }
+    }
+
+    if (Object.prototype.hasOwnProperty.call(data.short_names, emoji)) {
+      emoji = data.short_names[emoji];
+    }
+
+    if (Object.prototype.hasOwnProperty.call(data.emojis, emoji)) {
+      emojiData = data.emojis[emoji];
+    }
+  } else if (emoji.id) {
+    if (Object.prototype.hasOwnProperty.call(data.short_names, emoji.id)) {
+      emoji.id = data.short_names[emoji.id];
+    }
+
+    if (Object.prototype.hasOwnProperty.call(data.emojis, emoji.id)) {
+      emojiData = data.emojis[emoji.id];
+      skin = skin || emoji.skin;
+    }
+  }
+
+  if (!Object.keys(emojiData).length) {
+    emojiData = emoji;
+    emojiData.custom = true;
+
+    if (!emojiData.search) {
+      emojiData.search = buildSearch(emoji);
+    }
+  }
+
+  emojiData.emoticons = emojiData.emoticons || [];
+  emojiData.variations = emojiData.variations || [];
+
+  if (emojiData.skin_variations && skin > 1 && set) {
+    emojiData = JSON.parse(_JSON.stringify(emojiData));
+
+    let skinKey = SKINS[skin - 1],
+      variationData = emojiData.skin_variations[skinKey];
+
+    if (!variationData.variations && emojiData.variations) {
+      delete emojiData.variations;
+    }
+
+    if (variationData[`has_img_${set}`]) {
+      emojiData.skin_tone = skin;
+
+      for (let k in variationData) {
+        let v = variationData[k];
+        emojiData[k] = v;
+      }
+    }
+  }
+
+  if (emojiData.variations && emojiData.variations.length) {
+    emojiData = JSON.parse(_JSON.stringify(emojiData));
+    emojiData.unified = emojiData.variations.shift();
+  }
+
+  return emojiData;
+}
+
+function uniq(arr) {
+  return arr.reduce((acc, item) => {
+    if (acc.indexOf(item) === -1) {
+      acc.push(item);
+    }
+    return acc;
+  }, []);
+}
+
+function intersect(a, b) {
+  const uniqA = uniq(a);
+  const uniqB = uniq(b);
+
+  return uniqA.filter(item => uniqB.indexOf(item) >= 0);
+}
+
+function deepMerge(a, b) {
+  let o = {};
+
+  for (let key in a) {
+    let originalValue = a[key],
+      value = originalValue;
+
+    if (Object.prototype.hasOwnProperty.call(b, key)) {
+      value = b[key];
+    }
+
+    if (typeof value === 'object') {
+      value = deepMerge(originalValue, value);
+    }
+
+    o[key] = value;
+  }
+
+  return o;
+}
+
+// https://github.com/sonicdoe/measure-scrollbar
+function measureScrollbar() {
+  const div = document.createElement('div');
+
+  div.style.width = '100px';
+  div.style.height = '100px';
+  div.style.overflow = 'scroll';
+  div.style.position = 'absolute';
+  div.style.top = '-9999px';
+
+  document.body.appendChild(div);
+  const scrollbarWidth = div.offsetWidth - div.clientWidth;
+  document.body.removeChild(div);
+
+  return scrollbarWidth;
+}
+
+export {
+  getData,
+  getSanitizedData,
+  uniq,
+  intersect,
+  deepMerge,
+  unifiedToNative,
+  measureScrollbar,
+};
diff --git a/app/javascript/flavours/blobfox/features/emoji/unicode_to_filename.js b/app/javascript/flavours/blobfox/features/emoji/unicode_to_filename.js
new file mode 100644
index 00000000000000..3395c77174cc9d
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/emoji/unicode_to_filename.js
@@ -0,0 +1,29 @@
+/* eslint-disable import/no-commonjs --
+   We need to use CommonJS here as its imported into a preval file (`emoji_compressed.js`) */
+
+// taken from:
+// https://github.com/twitter/twemoji/blob/47732c7/twemoji-generator.js#L848-L866
+exports.unicodeToFilename = (str) => {
+  let result = '';
+  let charCode = 0;
+  let p = 0;
+  let i = 0;
+  while (i < str.length) {
+    charCode = str.charCodeAt(i++);
+    if (p) {
+      if (result.length > 0) {
+        result += '-';
+      }
+      result += (0x10000 + ((p - 0xD800) << 10) + (charCode - 0xDC00)).toString(16);
+      p = 0;
+    } else if (0xD800 <= charCode && charCode <= 0xDBFF) {
+      p = charCode;
+    } else {
+      if (result.length > 0) {
+        result += '-';
+      }
+      result += charCode.toString(16);
+    }
+  }
+  return result;
+};
diff --git a/app/javascript/flavours/blobfox/features/emoji/unicode_to_unified_name.js b/app/javascript/flavours/blobfox/features/emoji/unicode_to_unified_name.js
new file mode 100644
index 00000000000000..108b911222adc0
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/emoji/unicode_to_unified_name.js
@@ -0,0 +1,24 @@
+/* eslint-disable import/no-commonjs --
+   We need to use CommonJS here as its imported into a preval file (`emoji_compressed.js`) */
+
+function padLeft(str, num) {
+  while (str.length < num) {
+    str = '0' + str;
+  }
+
+  return str;
+}
+
+exports.unicodeToUnifiedName = (str) => {
+  let output = '';
+
+  for (let i = 0; i < str.length; i += 2) {
+    if (i > 0) {
+      output += '-';
+    }
+
+    output += padLeft(str.codePointAt(i).toString(16).toUpperCase(), 4);
+  }
+
+  return output;
+};
diff --git a/app/javascript/flavours/blobfox/features/explore/components/search_section.jsx b/app/javascript/flavours/blobfox/features/explore/components/search_section.jsx
new file mode 100644
index 00000000000000..c84e3f7cef6f84
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/explore/components/search_section.jsx
@@ -0,0 +1,20 @@
+import PropTypes from 'prop-types';
+
+import { FormattedMessage } from 'react-intl';
+
+export const SearchSection = ({ title, onClickMore, children }) => (
+  <div className='search-results__section'>
+    <div className='search-results__section__header'>
+      <h3>{title}</h3>
+      {onClickMore && <button onClick={onClickMore}><FormattedMessage id='search_results.see_all' defaultMessage='See all' /></button>}
+    </div>
+
+    {children}
+  </div>
+);
+
+SearchSection.propTypes = {
+  title: PropTypes.node.isRequired,
+  onClickMore: PropTypes.func,
+  children: PropTypes.children,
+};
\ No newline at end of file
diff --git a/app/javascript/flavours/blobfox/features/explore/components/story.jsx b/app/javascript/flavours/blobfox/features/explore/components/story.jsx
new file mode 100644
index 00000000000000..da1a170580d667
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/explore/components/story.jsx
@@ -0,0 +1,61 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import classNames from 'classnames';
+
+import { Blurhash } from 'flavours/blobfox/components/blurhash';
+import { accountsCountRenderer } from 'flavours/blobfox/components/hashtag';
+import { RelativeTimestamp } from 'flavours/blobfox/components/relative_timestamp';
+import { ShortNumber } from 'flavours/blobfox/components/short_number';
+import { Skeleton } from 'flavours/blobfox/components/skeleton';
+
+export default class Story extends PureComponent {
+
+  static propTypes = {
+    url: PropTypes.string,
+    title: PropTypes.string,
+    lang: PropTypes.string,
+    publisher: PropTypes.string,
+    publishedAt: PropTypes.string,
+    author: PropTypes.string,
+    sharedTimes: PropTypes.number,
+    thumbnail: PropTypes.string,
+    thumbnailDescription: PropTypes.string,
+    blurhash: PropTypes.string,
+    expanded: PropTypes.bool,
+  };
+
+  state = {
+    thumbnailLoaded: false,
+  };
+
+  handleImageLoad = () => this.setState({ thumbnailLoaded: true });
+
+  render () {
+    const { expanded, url, title, lang, publisher, author, publishedAt, sharedTimes, thumbnail, thumbnailDescription, blurhash } = this.props;
+
+    const { thumbnailLoaded } = this.state;
+
+    return (
+      <a className={classNames('story', { expanded })} href={url} target='blank' rel='noopener'>
+        <div className='story__details'>
+          <div className='story__details__publisher'>{publisher ? <span lang={lang}>{publisher}</span> : <Skeleton width={50} />}{publishedAt && <> · <RelativeTimestamp timestamp={publishedAt} /></>}</div>
+          <div className='story__details__title' lang={lang}>{title ? title : <Skeleton />}</div>
+          <div className='story__details__shared'>{author && <><FormattedMessage id='link_preview.author' defaultMessage='By {name}' values={{ name: <strong>{author}</strong> }} /> · </>}{typeof sharedTimes === 'number' ? <ShortNumber value={sharedTimes} renderer={accountsCountRenderer} /> : <Skeleton width={100} />}</div>
+        </div>
+
+        <div className='story__thumbnail'>
+          {thumbnail ? (
+            <>
+              <div className={classNames('story__thumbnail__preview', { 'story__thumbnail__preview--hidden': thumbnailLoaded })}><Blurhash hash={blurhash} /></div>
+              <img src={thumbnail} onLoad={this.handleImageLoad} alt={thumbnailDescription} title={thumbnailDescription} lang={lang} />
+            </>
+          ) : <Skeleton />}
+        </div>
+      </a>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/explore/index.jsx b/app/javascript/flavours/blobfox/features/explore/index.jsx
new file mode 100644
index 00000000000000..a63368f0f5eee2
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/explore/index.jsx
@@ -0,0 +1,114 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+import { NavLink, Switch, Route } from 'react-router-dom';
+
+import { connect } from 'react-redux';
+
+import Column from 'flavours/blobfox/components/column';
+import ColumnHeader from 'flavours/blobfox/components/column_header';
+import Search from 'flavours/blobfox/features/compose/containers/search_container';
+import { trendsEnabled } from 'flavours/blobfox/initial_state';
+
+import Links from './links';
+import SearchResults from './results';
+import Statuses from './statuses';
+import Suggestions from './suggestions';
+import Tags from './tags';
+
+const messages = defineMessages({
+  title: { id: 'explore.title', defaultMessage: 'Explore' },
+  searchResults: { id: 'explore.search_results', defaultMessage: 'Search results' },
+});
+
+const mapStateToProps = state => ({
+  layout: state.getIn(['meta', 'layout']),
+  isSearching: state.getIn(['search', 'submitted']) || !trendsEnabled,
+});
+
+class Explore extends PureComponent {
+
+  static contextTypes = {
+    identity: PropTypes.object,
+  };
+
+  static propTypes = {
+    intl: PropTypes.object.isRequired,
+    multiColumn: PropTypes.bool,
+    isSearching: PropTypes.bool,
+  };
+
+  handleHeaderClick = () => {
+    this.column.scrollTop();
+  };
+
+  setRef = c => {
+    this.column = c;
+  };
+
+  render() {
+    const { intl, multiColumn, isSearching } = this.props;
+    const { signedIn } = this.context.identity;
+
+    return (
+      <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
+        <ColumnHeader
+          icon={isSearching ? 'search' : 'hashtag'}
+          title={intl.formatMessage(isSearching ? messages.searchResults : messages.title)}
+          onClick={this.handleHeaderClick}
+          multiColumn={multiColumn}
+        />
+
+        <div className='explore__search-header'>
+          <Search />
+        </div>
+
+        {isSearching ? (
+          <SearchResults />
+        ) : (
+          <>
+            <div className='account__section-headline'>
+              <NavLink exact to='/explore'>
+                <FormattedMessage tagName='div' id='explore.trending_statuses' defaultMessage='Posts' />
+              </NavLink>
+
+              <NavLink exact to='/explore/tags'>
+                <FormattedMessage tagName='div' id='explore.trending_tags' defaultMessage='Hashtags' />
+              </NavLink>
+
+              {signedIn && (
+                <NavLink exact to='/explore/suggestions'>
+                  <FormattedMessage tagName='div' id='explore.suggested_follows' defaultMessage='People' />
+                </NavLink>
+              )}
+
+              <NavLink exact to='/explore/links'>
+                <FormattedMessage tagName='div' id='explore.trending_links' defaultMessage='News' />
+              </NavLink>
+            </div>
+
+            <Switch>
+              <Route path='/explore/tags' component={Tags} />
+              <Route path='/explore/links' component={Links} />
+              <Route path='/explore/suggestions' component={Suggestions} />
+              <Route exact path={['/explore', '/explore/posts', '/search']}>
+                <Statuses multiColumn={multiColumn} />
+              </Route>
+            </Switch>
+
+            <Helmet>
+              <title>{intl.formatMessage(messages.title)}</title>
+              <meta name='robots' content={isSearching ? 'noindex' : 'all'} />
+            </Helmet>
+          </>
+        )}
+      </Column>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(injectIntl(Explore));
diff --git a/app/javascript/flavours/blobfox/features/explore/links.jsx b/app/javascript/flavours/blobfox/features/explore/links.jsx
new file mode 100644
index 00000000000000..83836304f616de
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/explore/links.jsx
@@ -0,0 +1,90 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import { withRouter } from 'react-router-dom';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
+
+import { fetchTrendingLinks } from 'flavours/blobfox/actions/trends';
+import { DismissableBanner } from 'flavours/blobfox/components/dismissable_banner';
+import { LoadingIndicator } from 'flavours/blobfox/components/loading_indicator';
+import { WithRouterPropTypes } from 'flavours/blobfox/utils/react_router';
+
+import Story from './components/story';
+
+const mapStateToProps = state => ({
+  links: state.getIn(['trends', 'links', 'items']),
+  isLoading: state.getIn(['trends', 'links', 'isLoading']),
+});
+
+class Links extends PureComponent {
+
+  static propTypes = {
+    links: ImmutablePropTypes.list,
+    isLoading: PropTypes.bool,
+    dispatch: PropTypes.func.isRequired,
+    ...WithRouterPropTypes,
+  };
+
+  componentDidMount () {
+    const { dispatch, links, history } = this.props;
+
+    // If we're navigating back to the screen, do not trigger a reload
+    if (history.action === 'POP' && links.size > 0) {
+      return;
+    }
+
+    dispatch(fetchTrendingLinks());
+  }
+
+  render () {
+    const { isLoading, links } = this.props;
+
+    const banner = (
+      <DismissableBanner id='explore/links'>
+        <FormattedMessage id='dismissable_banner.explore_links' defaultMessage='These are news stories being shared the most on the social web today. Newer news stories posted by more different people are ranked higher.' />
+      </DismissableBanner>
+    );
+
+    if (!isLoading && links.isEmpty()) {
+      return (
+        <div className='explore__links scrollable scrollable--flex'>
+          {banner}
+
+          <div className='empty-column-indicator'>
+            <FormattedMessage id='empty_column.explore_statuses' defaultMessage='Nothing is trending right now. Check back later!' />
+          </div>
+        </div>
+      );
+    }
+
+    return (
+      <div className='explore__links scrollable' data-nosnippet>
+        {banner}
+
+        {isLoading ? (<LoadingIndicator />) : links.map((link, i) => (
+          <Story
+            key={link.get('id')}
+            expanded={i === 0}
+            lang={link.get('language')}
+            url={link.get('url')}
+            title={link.get('title')}
+            publisher={link.get('provider_name')}
+            publishedAt={link.get('published_at')}
+            author={link.get('author_name')}
+            sharedTimes={link.getIn(['history', 0, 'accounts']) * 1 + link.getIn(['history', 1, 'accounts']) * 1}
+            thumbnail={link.get('image')}
+            thumbnailDescription={link.get('image_description')}
+            blurhash={link.get('blurhash')}
+          />
+        ))}
+      </div>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(withRouter(Links));
diff --git a/app/javascript/flavours/blobfox/features/explore/results.jsx b/app/javascript/flavours/blobfox/features/explore/results.jsx
new file mode 100644
index 00000000000000..63d2a41af67bba
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/explore/results.jsx
@@ -0,0 +1,229 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+
+import { List as ImmutableList } from 'immutable';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
+
+import { submitSearch, expandSearch } from 'flavours/blobfox/actions/search';
+import { ImmutableHashtag as Hashtag } from 'flavours/blobfox/components/hashtag';
+import { Icon } from 'flavours/blobfox/components/icon';
+import ScrollableList from 'flavours/blobfox/components/scrollable_list';
+import Account from 'flavours/blobfox/containers/account_container';
+import Status from 'flavours/blobfox/containers/status_container';
+
+import { SearchSection } from './components/search_section';
+
+const messages = defineMessages({
+  title: { id: 'search_results.title', defaultMessage: 'Search for {q}' },
+});
+
+const mapStateToProps = state => ({
+  isLoading: state.getIn(['search', 'isLoading']),
+  results: state.getIn(['search', 'results']),
+  q: state.getIn(['search', 'searchTerm']),
+  submittedType: state.getIn(['search', 'type']),
+});
+
+const INITIAL_PAGE_LIMIT = 10;
+const INITIAL_DISPLAY = 4;
+
+const hidePeek = list => {
+  if (list.size > INITIAL_PAGE_LIMIT && list.size % INITIAL_PAGE_LIMIT === 1) {
+    return list.skipLast(1);
+  } else {
+    return list;
+  }
+};
+
+const renderAccounts = accounts => hidePeek(accounts).map(id => (
+  <Account key={id} id={id} />
+));
+
+const renderHashtags = hashtags => hidePeek(hashtags).map(hashtag => (
+  <Hashtag key={hashtag.get('name')} hashtag={hashtag} />
+));
+
+const renderStatuses = statuses => hidePeek(statuses).map(id => (
+  <Status key={id} id={id} />
+));
+
+class Results extends PureComponent {
+
+  static propTypes = {
+    results: ImmutablePropTypes.contains({
+      accounts: ImmutablePropTypes.orderedSet,
+      statuses: ImmutablePropTypes.orderedSet,
+      hashtags: ImmutablePropTypes.orderedSet,
+    }),
+    isLoading: PropTypes.bool,
+    multiColumn: PropTypes.bool,
+    dispatch: PropTypes.func.isRequired,
+    q: PropTypes.string,
+    intl: PropTypes.object,
+    submittedType: PropTypes.oneOf(['accounts', 'statuses', 'hashtags']),
+  };
+
+  state = {
+    type: this.props.submittedType || 'all',
+  };
+
+  static getDerivedStateFromProps(props, state) {
+    if (props.submittedType !== state.type) {
+      return {
+        type: props.submittedType || 'all',
+      };
+    }
+
+    return null;
+  }
+
+  handleSelectAll = () => {
+    const { submittedType, dispatch } = this.props;
+
+    // If we originally searched for a specific type, we need to resubmit
+    // the query to get all types of results
+    if (submittedType) {
+      dispatch(submitSearch());
+    }
+
+    this.setState({ type: 'all' });
+  };
+
+  handleSelectAccounts = () => {
+    const { submittedType, dispatch } = this.props;
+
+    // If we originally searched for something else (but not everything),
+    // we need to resubmit the query for this specific type
+    if (submittedType !== 'accounts') {
+      dispatch(submitSearch('accounts'));
+    }
+
+    this.setState({ type: 'accounts' });
+  };
+
+  handleSelectHashtags = () => {
+    const { submittedType, dispatch } = this.props;
+
+    // If we originally searched for something else (but not everything),
+    // we need to resubmit the query for this specific type
+    if (submittedType !== 'hashtags') {
+      dispatch(submitSearch('hashtags'));
+    }
+
+    this.setState({ type: 'hashtags' });
+  };
+
+  handleSelectStatuses = () => {
+    const { submittedType, dispatch } = this.props;
+
+    // If we originally searched for something else (but not everything),
+    // we need to resubmit the query for this specific type
+    if (submittedType !== 'statuses') {
+      dispatch(submitSearch('statuses'));
+    }
+
+    this.setState({ type: 'statuses' });
+  };
+
+  handleLoadMoreAccounts = () => this._loadMore('accounts');
+  handleLoadMoreStatuses = () => this._loadMore('statuses');
+  handleLoadMoreHashtags = () => this._loadMore('hashtags');
+
+  _loadMore (type) {
+    const { dispatch } = this.props;
+    dispatch(expandSearch(type));
+  }
+
+  handleLoadMore = () => {
+    const { type } = this.state;
+
+    if (type !== 'all') {
+      this._loadMore(type);
+    }
+  };
+
+  render () {
+    const { intl, isLoading, q, results } = this.props;
+    const { type } = this.state;
+
+    // We request 1 more result than we display so we can tell if there'd be a next page
+    const hasMore = type !== 'all' ? results.get(type, ImmutableList()).size > INITIAL_PAGE_LIMIT && results.get(type).size % INITIAL_PAGE_LIMIT === 1 : false;
+
+    let filteredResults;
+
+    const accounts = results.get('accounts', ImmutableList());
+    const hashtags = results.get('hashtags', ImmutableList());
+    const statuses = results.get('statuses', ImmutableList());
+
+    switch(type) {
+    case 'all':
+      filteredResults = (accounts.size + hashtags.size + statuses.size) > 0 ? (
+        <>
+          {accounts.size > 0 && (
+            <SearchSection key='accounts' title={<><Icon id='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></>} onClickMore={this.handleLoadMoreAccounts}>
+              {accounts.take(INITIAL_DISPLAY).map(id => <Account key={id} id={id} />)}
+            </SearchSection>
+          )}
+
+          {hashtags.size > 0 && (
+            <SearchSection key='hashtags' title={<><Icon id='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></>} onClickMore={this.handleLoadMoreHashtags}>
+              {hashtags.take(INITIAL_DISPLAY).map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
+            </SearchSection>
+          )}
+
+          {statuses.size > 0 && (
+            <SearchSection key='statuses' title={<><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></>} onClickMore={this.handleLoadMoreStatuses}>
+              {statuses.take(INITIAL_DISPLAY).map(id => <Status key={id} id={id} />)}
+            </SearchSection>
+          )}
+        </>
+      ) : [];
+      break;
+    case 'accounts':
+      filteredResults = renderAccounts(accounts);
+      break;
+    case 'hashtags':
+      filteredResults = renderHashtags(hashtags);
+      break;
+    case 'statuses':
+      filteredResults = renderStatuses(statuses);
+      break;
+    }
+
+    return (
+      <>
+        <div className='account__section-headline'>
+          <button onClick={this.handleSelectAll} className={type === 'all' ? 'active' : undefined}><FormattedMessage id='search_results.all' defaultMessage='All' /></button>
+          <button onClick={this.handleSelectAccounts} className={type === 'accounts' ? 'active' : undefined}><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></button>
+          <button onClick={this.handleSelectHashtags} className={type === 'hashtags' ? 'active' : undefined}><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></button>
+          <button onClick={this.handleSelectStatuses} className={type === 'statuses' ? 'active' : undefined}><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></button>
+        </div>
+
+        <div className='explore__search-results' data-nosnippet>
+          <ScrollableList
+            scrollKey='search-results'
+            isLoading={isLoading}
+            onLoadMore={this.handleLoadMore}
+            hasMore={hasMore}
+            emptyMessage={<FormattedMessage id='search_results.nothing_found' defaultMessage='Could not find anything for these search terms' />}
+            bindToDocument
+          >
+            {filteredResults}
+          </ScrollableList>
+        </div>
+
+        <Helmet>
+          <title>{intl.formatMessage(messages.title, { q })}</title>
+        </Helmet>
+      </>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(injectIntl(Results));
diff --git a/app/javascript/flavours/blobfox/features/explore/statuses.jsx b/app/javascript/flavours/blobfox/features/explore/statuses.jsx
new file mode 100644
index 00000000000000..3039709fe78fd2
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/explore/statuses.jsx
@@ -0,0 +1,78 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import { withRouter } from 'react-router-dom';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
+
+import { debounce } from 'lodash';
+
+
+import { fetchTrendingStatuses, expandTrendingStatuses } from 'flavours/blobfox/actions/trends';
+import { DismissableBanner } from 'flavours/blobfox/components/dismissable_banner';
+import StatusList from 'flavours/blobfox/components/status_list';
+import { getStatusList } from 'flavours/blobfox/selectors';
+import { WithRouterPropTypes } from 'flavours/blobfox/utils/react_router';
+
+const mapStateToProps = state => ({
+  statusIds: getStatusList(state, 'trending'),
+  isLoading: state.getIn(['status_lists', 'trending', 'isLoading'], true),
+  hasMore: !!state.getIn(['status_lists', 'trending', 'next']),
+});
+
+class Statuses extends PureComponent {
+
+  static propTypes = {
+    statusIds: ImmutablePropTypes.list,
+    isLoading: PropTypes.bool,
+    hasMore: PropTypes.bool,
+    multiColumn: PropTypes.bool,
+    dispatch: PropTypes.func.isRequired,
+    ...WithRouterPropTypes,
+  };
+
+  componentDidMount () {
+    const { dispatch, statusIds, history } = this.props;
+
+    // If we're navigating back to the screen, do not trigger a reload
+    if (history.action === 'POP' && statusIds.size > 0) {
+      return;
+    }
+
+    dispatch(fetchTrendingStatuses());
+  }
+
+  handleLoadMore = debounce(() => {
+    const { dispatch } = this.props;
+    dispatch(expandTrendingStatuses());
+  }, 300, { leading: true });
+
+  render () {
+    const { isLoading, hasMore, statusIds, multiColumn } = this.props;
+
+    const emptyMessage = <FormattedMessage id='empty_column.explore_statuses' defaultMessage='Nothing is trending right now. Check back later!' />;
+
+    return (
+      <StatusList
+        trackScroll
+        prepend={<DismissableBanner id='explore/statuses'><FormattedMessage id='dismissable_banner.explore_statuses' defaultMessage='These are posts from across the social web that are gaining traction today. Newer posts with more boosts and favorites are ranked higher.' /></DismissableBanner>}
+        alwaysPrepend
+        timelineId='explore'
+        statusIds={statusIds}
+        scrollKey='explore-statuses'
+        hasMore={hasMore}
+        isLoading={isLoading}
+        onLoadMore={this.handleLoadMore}
+        emptyMessage={emptyMessage}
+        bindToDocument={!multiColumn}
+        withCounters
+      />
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(withRouter(Statuses));
diff --git a/app/javascript/flavours/blobfox/features/explore/suggestions.jsx b/app/javascript/flavours/blobfox/features/explore/suggestions.jsx
new file mode 100644
index 00000000000000..405b05c2f5eb06
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/explore/suggestions.jsx
@@ -0,0 +1,70 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import { withRouter } from 'react-router-dom';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
+
+import { fetchSuggestions, dismissSuggestion } from 'flavours/blobfox/actions/suggestions';
+import { LoadingIndicator } from 'flavours/blobfox/components/loading_indicator';
+import AccountCard from 'flavours/blobfox/features/directory/components/account_card';
+import { WithRouterPropTypes } from 'flavours/blobfox/utils/react_router';
+
+const mapStateToProps = state => ({
+  suggestions: state.getIn(['suggestions', 'items']),
+  isLoading: state.getIn(['suggestions', 'isLoading']),
+});
+
+class Suggestions extends PureComponent {
+
+  static propTypes = {
+    isLoading: PropTypes.bool,
+    suggestions: ImmutablePropTypes.list,
+    dispatch: PropTypes.func.isRequired,
+    ...WithRouterPropTypes,
+  };
+
+  componentDidMount () {
+    const { dispatch, suggestions, history } = this.props;
+
+    // If we're navigating back to the screen, do not trigger a reload
+    if (history.action === 'POP' && suggestions.size > 0) {
+      return;
+    }
+
+    dispatch(fetchSuggestions(true));
+  }
+
+  handleDismiss = (accountId) => {
+    const { dispatch } = this.props;
+    dispatch(dismissSuggestion(accountId));
+  };
+
+  render () {
+    const { isLoading, suggestions } = this.props;
+
+    if (!isLoading && suggestions.isEmpty()) {
+      return (
+        <div className='explore__suggestions scrollable scrollable--flex'>
+          <div className='empty-column-indicator'>
+            <FormattedMessage id='empty_column.explore_statuses' defaultMessage='Nothing is trending right now. Check back later!' />
+          </div>
+        </div>
+      );
+    }
+
+    return (
+      <div className='explore__suggestions scrollable' data-nosnippet>
+        {isLoading ? <LoadingIndicator /> : suggestions.map(suggestion => (
+          <AccountCard key={suggestion.get('account')} id={suggestion.get('account')} onDismiss={suggestion.get('source') === 'past_interactions' ? this.handleDismiss : null} />
+        ))}
+      </div>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(withRouter(Suggestions));
diff --git a/app/javascript/flavours/blobfox/features/explore/tags.jsx b/app/javascript/flavours/blobfox/features/explore/tags.jsx
new file mode 100644
index 00000000000000..c08acac515ccce
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/explore/tags.jsx
@@ -0,0 +1,76 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import { withRouter } from 'react-router-dom';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
+
+import { fetchTrendingHashtags } from 'flavours/blobfox/actions/trends';
+import { DismissableBanner } from 'flavours/blobfox/components/dismissable_banner';
+import { ImmutableHashtag as Hashtag } from 'flavours/blobfox/components/hashtag';
+import { LoadingIndicator } from 'flavours/blobfox/components/loading_indicator';
+import { WithRouterPropTypes } from 'flavours/blobfox/utils/react_router';
+
+const mapStateToProps = state => ({
+  hashtags: state.getIn(['trends', 'tags', 'items']),
+  isLoadingHashtags: state.getIn(['trends', 'tags', 'isLoading']),
+});
+
+class Tags extends PureComponent {
+
+  static propTypes = {
+    hashtags: ImmutablePropTypes.list,
+    isLoading: PropTypes.bool,
+    dispatch: PropTypes.func.isRequired,
+    ...WithRouterPropTypes,
+  };
+
+  componentDidMount () {
+    const { dispatch, history, hashtags } = this.props;
+
+    // If we're navigating back to the screen, do not trigger a reload
+    if (history.action === 'POP' && hashtags.size > 0) {
+      return;
+    }
+
+    dispatch(fetchTrendingHashtags());
+  }
+
+  render () {
+    const { isLoading, hashtags } = this.props;
+
+    const banner = (
+      <DismissableBanner id='explore/tags'>
+        <FormattedMessage id='dismissable_banner.explore_tags' defaultMessage='These are hashtags that are gaining traction on the social web today. Hashtags that are used by more different people are ranked higher.' />
+      </DismissableBanner>
+    );
+
+    if (!isLoading && hashtags.isEmpty()) {
+      return (
+        <div className='explore__links scrollable scrollable--flex'>
+          {banner}
+
+          <div className='empty-column-indicator'>
+            <FormattedMessage id='empty_column.explore_statuses' defaultMessage='Nothing is trending right now. Check back later!' />
+          </div>
+        </div>
+      );
+    }
+
+    return (
+      <div className='scrollable explore__links' data-nosnippet>
+        {banner}
+
+        {isLoading ? (<LoadingIndicator />) : hashtags.map(hashtag => (
+          <Hashtag key={hashtag.get('name')} hashtag={hashtag} />
+        ))}
+      </div>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(withRouter(Tags));
diff --git a/app/javascript/flavours/blobfox/features/favourited_statuses/index.jsx b/app/javascript/flavours/blobfox/features/favourited_statuses/index.jsx
new file mode 100644
index 00000000000000..2be1489dc325c7
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/favourited_statuses/index.jsx
@@ -0,0 +1,113 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
+import { debounce } from 'lodash';
+
+import { addColumn, removeColumn, moveColumn } from 'flavours/blobfox/actions/columns';
+import { fetchFavouritedStatuses, expandFavouritedStatuses } from 'flavours/blobfox/actions/favourites';
+import ColumnHeader from 'flavours/blobfox/components/column_header';
+import StatusList from 'flavours/blobfox/components/status_list';
+import Column from 'flavours/blobfox/features/ui/components/column';
+import { getStatusList } from 'flavours/blobfox/selectors';
+
+const messages = defineMessages({
+  heading: { id: 'column.favourites', defaultMessage: 'Favorites' },
+});
+
+const mapStateToProps = state => ({
+  statusIds: getStatusList(state, 'favourites'),
+  isLoading: state.getIn(['status_lists', 'favourites', 'isLoading'], true),
+  hasMore: !!state.getIn(['status_lists', 'favourites', 'next']),
+});
+
+class Favourites extends ImmutablePureComponent {
+
+  static propTypes = {
+    dispatch: PropTypes.func.isRequired,
+    statusIds: ImmutablePropTypes.list.isRequired,
+    intl: PropTypes.object.isRequired,
+    columnId: PropTypes.string,
+    multiColumn: PropTypes.bool,
+    hasMore: PropTypes.bool,
+    isLoading: PropTypes.bool,
+  };
+
+  UNSAFE_componentWillMount () {
+    this.props.dispatch(fetchFavouritedStatuses());
+  }
+
+  handlePin = () => {
+    const { columnId, dispatch } = this.props;
+
+    if (columnId) {
+      dispatch(removeColumn(columnId));
+    } else {
+      dispatch(addColumn('FAVOURITES', {}));
+    }
+  };
+
+  handleMove = (dir) => {
+    const { columnId, dispatch } = this.props;
+    dispatch(moveColumn(columnId, dir));
+  };
+
+  handleHeaderClick = () => {
+    this.column.scrollTop();
+  };
+
+  setRef = c => {
+    this.column = c;
+  };
+
+  handleLoadMore = debounce(() => {
+    this.props.dispatch(expandFavouritedStatuses());
+  }, 300, { leading: true });
+
+  render () {
+    const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
+    const pinned = !!columnId;
+
+    const emptyMessage = <FormattedMessage id='empty_column.favourited_statuses' defaultMessage="You don't have any favorite posts yet. When you favorite one, it will show up here." />;
+
+    return (
+      <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.heading)}>
+        <ColumnHeader
+          icon='star'
+          title={intl.formatMessage(messages.heading)}
+          onPin={this.handlePin}
+          onMove={this.handleMove}
+          onClick={this.handleHeaderClick}
+          pinned={pinned}
+          multiColumn={multiColumn}
+          showBackButton
+        />
+
+        <StatusList
+          trackScroll={!pinned}
+          statusIds={statusIds}
+          scrollKey={`favourited_statuses-${columnId}`}
+          hasMore={hasMore}
+          isLoading={isLoading}
+          onLoadMore={this.handleLoadMore}
+          emptyMessage={emptyMessage}
+          bindToDocument={!multiColumn}
+        />
+
+        <Helmet>
+          <title>{intl.formatMessage(messages.heading)}</title>
+          <meta name='robots' content='noindex' />
+        </Helmet>
+      </Column>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(injectIntl(Favourites));
diff --git a/app/javascript/flavours/blobfox/features/favourites/index.jsx b/app/javascript/flavours/blobfox/features/favourites/index.jsx
new file mode 100644
index 00000000000000..4451d047c2a433
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/favourites/index.jsx
@@ -0,0 +1,114 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
+import { debounce } from 'lodash';
+
+import { fetchFavourites, expandFavourites } from 'flavours/blobfox/actions/interactions';
+import ColumnHeader from 'flavours/blobfox/components/column_header';
+import { Icon }  from 'flavours/blobfox/components/icon';
+import { LoadingIndicator } from 'flavours/blobfox/components/loading_indicator';
+import ScrollableList from 'flavours/blobfox/components/scrollable_list';
+import AccountContainer from 'flavours/blobfox/containers/account_container';
+import Column from 'flavours/blobfox/features/ui/components/column';
+
+const messages = defineMessages({
+  heading: { id: 'column.favourited_by', defaultMessage: 'Favourited by' },
+  refresh: { id: 'refresh', defaultMessage: 'Refresh' },
+});
+
+const mapStateToProps = (state, props) => ({
+  accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId, 'items']),
+  hasMore: !!state.getIn(['user_lists', 'favourited_by', props.params.statusId, 'next']),
+  isLoading: state.getIn(['user_lists', 'favourited_by', props.params.statusId, 'isLoading'], true),
+});
+
+class Favourites extends ImmutablePureComponent {
+
+  static propTypes = {
+    params: PropTypes.object.isRequired,
+    dispatch: PropTypes.func.isRequired,
+    accountIds: ImmutablePropTypes.list,
+    hasMore: PropTypes.bool,
+    isLoading: PropTypes.bool,
+    multiColumn: PropTypes.bool,
+    intl: PropTypes.object.isRequired,
+  };
+
+  UNSAFE_componentWillMount () {
+    if (!this.props.accountIds) {
+      this.props.dispatch(fetchFavourites(this.props.params.statusId));
+    }
+  }
+
+  handleHeaderClick = () => {
+    this.column.scrollTop();
+  };
+
+  setRef = c => {
+    this.column = c;
+  };
+
+  handleRefresh = () => {
+    this.props.dispatch(fetchFavourites(this.props.params.statusId));
+  };
+
+  handleLoadMore = debounce(() => {
+    this.props.dispatch(expandFavourites(this.props.params.statusId));
+  }, 300, { leading: true });
+
+  render () {
+    const { intl, accountIds, hasMore, isLoading, multiColumn } = this.props;
+
+    if (!accountIds) {
+      return (
+        <Column>
+          <LoadingIndicator />
+        </Column>
+      );
+    }
+
+    const emptyMessage = <FormattedMessage id='empty_column.favourites' defaultMessage='No one has favorited this post yet. When someone does, they will show up here.' />;
+
+    return (
+      <Column ref={this.setRef}>
+        <ColumnHeader
+          icon='star'
+          title={intl.formatMessage(messages.heading)}
+          onClick={this.handleHeaderClick}
+          showBackButton
+          multiColumn={multiColumn}
+          extraButton={(
+            <button type='button' className='column-header__button' title={intl.formatMessage(messages.refresh)} aria-label={intl.formatMessage(messages.refresh)} onClick={this.handleRefresh}><Icon id='refresh' /></button>
+          )}
+        />
+
+        <ScrollableList
+          scrollKey='favourites'
+          onLoadMore={this.handleLoadMore}
+          hasMore={hasMore}
+          isLoading={isLoading}
+          emptyMessage={emptyMessage}
+          bindToDocument={!multiColumn}
+        >
+          {accountIds.map(id =>
+            <AccountContainer key={id} id={id} withNote={false} />,
+          )}
+        </ScrollableList>
+
+        <Helmet>
+          <meta name='robots' content='noindex' />
+        </Helmet>
+      </Column>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(injectIntl(Favourites));
diff --git a/app/javascript/flavours/blobfox/features/filters/added_to_filter.jsx b/app/javascript/flavours/blobfox/features/filters/added_to_filter.jsx
new file mode 100644
index 00000000000000..e4264b90ceaccb
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/filters/added_to_filter.jsx
@@ -0,0 +1,106 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
+
+import { Button } from 'flavours/blobfox/components/button';
+import { toServerSideType } from 'flavours/blobfox/utils/filters';
+
+const mapStateToProps = (state, { filterId }) => ({
+  filter: state.getIn(['filters', filterId]),
+});
+
+class AddedToFilter extends PureComponent {
+
+  static propTypes = {
+    onClose: PropTypes.func.isRequired,
+    contextType: PropTypes.string,
+    filter: ImmutablePropTypes.map.isRequired,
+    dispatch: PropTypes.func.isRequired,
+  };
+
+  handleCloseClick = () => {
+    const { onClose } = this.props;
+    onClose();
+  };
+
+  render () {
+    const { filter, contextType } = this.props;
+
+    let expiredMessage = null;
+    if (filter.get('expires_at') && filter.get('expires_at') < new Date()) {
+      expiredMessage = (
+        <>
+          <h4 className='report-dialog-modal__subtitle'><FormattedMessage id='filter_modal.added.expired_title' defaultMessage='Expired filter!' /></h4>
+          <p className='report-dialog-modal__lead'>
+            <FormattedMessage
+              id='filter_modal.added.expired_explanation'
+              defaultMessage='This filter category has expired, you will need to change the expiration date for it to apply.'
+            />
+          </p>
+        </>
+      );
+    }
+
+    let contextMismatchMessage = null;
+    if (contextType && !filter.get('context').includes(toServerSideType(contextType))) {
+      contextMismatchMessage = (
+        <>
+          <h4 className='report-dialog-modal__subtitle'><FormattedMessage id='filter_modal.added.context_mismatch_title' defaultMessage='Context mismatch!' /></h4>
+          <p className='report-dialog-modal__lead'>
+            <FormattedMessage
+              id='filter_modal.added.context_mismatch_explanation'
+              defaultMessage='This filter category does not apply to the context in which you have accessed this post. If you want the post to be filtered in this context too, you will have to edit the filter.'
+            />
+          </p>
+        </>
+      );
+    }
+
+    const settings_link = (
+      <a href={`/filters/${filter.get('id')}/edit`}>
+        <FormattedMessage
+          id='filter_modal.added.settings_link'
+          defaultMessage='settings page'
+        />
+      </a>
+    );
+
+    return (
+      <>
+        <h3 className='report-dialog-modal__title'><FormattedMessage id='filter_modal.added.title' defaultMessage='Filter added!' /></h3>
+        <p className='report-dialog-modal__lead'>
+          <FormattedMessage
+            id='filter_modal.added.short_explanation'
+            defaultMessage='This post has been added to the following filter category: {title}.'
+            values={{ title: filter.get('title') }}
+          />
+        </p>
+
+        {expiredMessage}
+        {contextMismatchMessage}
+
+        <h4 className='report-dialog-modal__subtitle'><FormattedMessage id='filter_modal.added.review_and_configure_title' defaultMessage='Filter settings' /></h4>
+        <p className='report-dialog-modal__lead'>
+          <FormattedMessage
+            id='filter_modal.added.review_and_configure'
+            defaultMessage='To review and further configure this filter category, go to the {settings_link}.'
+            values={{ settings_link }}
+          />
+        </p>
+
+        <div className='flex-spacer' />
+
+        <div className='report-dialog-modal__actions'>
+          <Button onClick={this.handleCloseClick}><FormattedMessage id='report.close' defaultMessage='Done' /></Button>
+        </div>
+      </>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(AddedToFilter);
diff --git a/app/javascript/flavours/blobfox/features/filters/select_filter.jsx b/app/javascript/flavours/blobfox/features/filters/select_filter.jsx
new file mode 100644
index 00000000000000..23ce2cc73b8a39
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/filters/select_filter.jsx
@@ -0,0 +1,196 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import { connect } from 'react-redux';
+
+import fuzzysort from 'fuzzysort';
+
+import { Icon }  from 'flavours/blobfox/components/icon';
+import { toServerSideType } from 'flavours/blobfox/utils/filters';
+import { loupeIcon, deleteIcon } from 'flavours/blobfox/utils/icons';
+
+const messages = defineMessages({
+  search: { id: 'filter_modal.select_filter.search', defaultMessage: 'Search or create' },
+  clear: { id: 'emoji_button.clear', defaultMessage: 'Clear' },
+});
+
+const mapStateToProps = (state, { contextType }) => ({
+  filters: Array.from(state.get('filters').values()).map((filter) => [
+    filter.get('id'),
+    filter.get('title'),
+    filter.get('keywords')?.map((keyword) => keyword.get('keyword')).join('\n'),
+    filter.get('expires_at') && filter.get('expires_at') < new Date(),
+    contextType && !filter.get('context').includes(toServerSideType(contextType)),
+  ]),
+});
+
+class SelectFilter extends PureComponent {
+
+  static propTypes = {
+    onSelectFilter: PropTypes.func.isRequired,
+    onNewFilter: PropTypes.func.isRequired,
+    filters: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.object)),
+    intl: PropTypes.object.isRequired,
+  };
+
+  state = {
+    searchValue: '',
+  };
+
+  search () {
+    const { filters } = this.props;
+    const { searchValue } = this.state;
+
+    if (searchValue === '') {
+      return filters;
+    }
+
+    return fuzzysort.go(searchValue, filters, {
+      keys: ['1', '2'],
+      limit: 5,
+      threshold: -10000,
+    }).map(result => result.obj);
+  }
+
+  renderItem = filter => {
+    let warning = null;
+    if (filter[3] || filter[4]) {
+      warning = (
+        <span className='language-dropdown__dropdown__results__item__common-name'>
+          (
+          {filter[3] && <FormattedMessage id='filter_modal.select_filter.expired' defaultMessage='expired' />}
+          {filter[3] && filter[4] && ', '}
+          {filter[4] && <FormattedMessage id='filter_modal.select_filter.context_mismatch' defaultMessage='does not apply to this context' />}
+          )
+        </span>
+      );
+    }
+
+    return (
+      <div key={filter[0]} role='button' tabIndex={0} data-index={filter[0]} className='language-dropdown__dropdown__results__item' onClick={this.handleItemClick} onKeyDown={this.handleKeyDown}>
+        <span className='language-dropdown__dropdown__results__item__native-name'>{filter[1]}</span> {warning}
+      </div>
+    );
+  };
+
+  renderCreateNew (name) {
+    return (
+      <div key='add-new-filter' role='button' tabIndex={0} className='language-dropdown__dropdown__results__item' onClick={this.handleNewFilterClick} onKeyDown={this.handleKeyDown}>
+        <Icon id='plus' fixedWidth /> <FormattedMessage id='filter_modal.select_filter.prompt_new' defaultMessage='New category: {name}' values={{ name }} />
+      </div>
+    );
+  }
+
+  handleSearchChange = ({ target }) => {
+    this.setState({ searchValue: target.value });
+  };
+
+  setListRef = c => {
+    this.listNode = c;
+  };
+
+  handleKeyDown = e => {
+    const index = Array.from(this.listNode.childNodes).findIndex(node => node === e.currentTarget);
+
+    let element = null;
+
+    switch(e.key) {
+    case ' ':
+    case 'Enter':
+      e.currentTarget.click();
+      break;
+    case 'ArrowDown':
+      element = this.listNode.childNodes[index + 1] || this.listNode.firstChild;
+      break;
+    case 'ArrowUp':
+      element = this.listNode.childNodes[index - 1] || this.listNode.lastChild;
+      break;
+    case 'Tab':
+      if (e.shiftKey) {
+        element = this.listNode.childNodes[index - 1] || this.listNode.lastChild;
+      } else {
+        element = this.listNode.childNodes[index + 1] || this.listNode.firstChild;
+      }
+      break;
+    case 'Home':
+      element = this.listNode.firstChild;
+      break;
+    case 'End':
+      element = this.listNode.lastChild;
+      break;
+    }
+
+    if (element) {
+      element.focus();
+      e.preventDefault();
+      e.stopPropagation();
+    }
+  };
+
+  handleSearchKeyDown = e => {
+    let element = null;
+
+    switch(e.key) {
+    case 'Tab':
+    case 'ArrowDown':
+      element = this.listNode.firstChild;
+
+      if (element) {
+        element.focus();
+        e.preventDefault();
+        e.stopPropagation();
+      }
+
+      break;
+    }
+  };
+
+  handleClear = () => {
+    this.setState({ searchValue: '' });
+  };
+
+  handleItemClick = e => {
+    const value = e.currentTarget.getAttribute('data-index');
+
+    e.preventDefault();
+
+    this.props.onSelectFilter(value);
+  };
+
+  handleNewFilterClick = e => {
+    e.preventDefault();
+
+    this.props.onNewFilter(this.state.searchValue);
+  };
+
+  render () {
+    const { intl } = this.props;
+
+    const { searchValue } = this.state;
+    const isSearching = searchValue !== '';
+    const results = this.search();
+
+    return (
+      <>
+        <h3 className='report-dialog-modal__title'><FormattedMessage id='filter_modal.select_filter.title' defaultMessage='Filter this post' /></h3>
+        <p className='report-dialog-modal__lead'><FormattedMessage id='filter_modal.select_filter.subtitle' defaultMessage='Use an existing category or create a new one' /></p>
+
+        <div className='emoji-mart-search'>
+          <input type='search' value={searchValue} onChange={this.handleSearchChange} onKeyDown={this.handleSearchKeyDown} placeholder={intl.formatMessage(messages.search)} autoFocus />
+          <button className='emoji-mart-search-icon' disabled={!isSearching} aria-label={intl.formatMessage(messages.clear)} onClick={this.handleClear}>{!isSearching ? loupeIcon : deleteIcon}</button>
+        </div>
+
+        <div className='language-dropdown__dropdown__results emoji-mart-scroll' role='listbox' ref={this.setListRef}>
+          {results.map(this.renderItem)}
+          {isSearching && this.renderCreateNew(searchValue) }
+        </div>
+
+      </>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(injectIntl(SelectFilter));
diff --git a/app/javascript/flavours/blobfox/features/firehose/index.jsx b/app/javascript/flavours/blobfox/features/firehose/index.jsx
new file mode 100644
index 00000000000000..86a3c894f1a593
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/firehose/index.jsx
@@ -0,0 +1,229 @@
+import PropTypes from 'prop-types';
+import { useRef, useCallback, useEffect } from 'react';
+
+import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+import { NavLink } from 'react-router-dom';
+
+import { addColumn } from 'flavours/blobfox/actions/columns';
+import { changeSetting } from 'flavours/blobfox/actions/settings';
+import { connectPublicStream, connectCommunityStream } from 'flavours/blobfox/actions/streaming';
+import { expandPublicTimeline, expandCommunityTimeline } from 'flavours/blobfox/actions/timelines';
+import { DismissableBanner } from 'flavours/blobfox/components/dismissable_banner';
+import SettingText from 'flavours/blobfox/components/setting_text';
+import initialState, { domain } from 'flavours/blobfox/initial_state';
+import { useAppDispatch, useAppSelector } from 'flavours/blobfox/store';
+
+import Column from '../../components/column';
+import ColumnHeader from '../../components/column_header';
+import SettingToggle from '../notifications/components/setting_toggle';
+import StatusListContainer from '../ui/containers/status_list_container';
+
+const messages = defineMessages({
+  title: { id: 'column.firehose', defaultMessage: 'Live feeds' },
+  filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' },
+});
+
+// TODO: use a proper React context later on
+const useIdentity = () => ({
+  signedIn: !!initialState.meta.me,
+  accountId: initialState.meta.me,
+  disabledAccountId: initialState.meta.disabled_account_id,
+  accessToken: initialState.meta.access_token,
+  permissions: initialState.role ? initialState.role.permissions : 0,
+});
+
+const ColumnSettings = () => {
+  const intl = useIntl();
+  const dispatch = useAppDispatch();
+  const settings = useAppSelector((state) => state.getIn(['settings', 'firehose']));
+  const onChange = useCallback(
+    (key, checked) => dispatch(changeSetting(['firehose', ...key], checked)),
+    [dispatch],
+  );
+
+  return (
+    <div>
+      <div className='column-settings__row'>
+        <SettingToggle
+          settings={settings}
+          settingPath={['onlyMedia']}
+          onChange={onChange}
+          label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />}
+        />
+        <SettingToggle
+          settings={settings}
+          settingPath={['allowLocalOnly']}
+          onChange={onChange}
+          label={<FormattedMessage id='firehose.column_settings.allow_local_only' defaultMessage='Show local-only posts in "All"' />}
+        />
+        <span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
+        <SettingText
+          settings={settings}
+          settingPath={['regex', 'body']}
+          onChange={onChange}
+          label={intl.formatMessage(messages.filter_regex)}
+        />
+      </div>
+    </div>
+  );
+};
+
+const Firehose = ({ feedType, multiColumn }) => {
+  const dispatch = useAppDispatch();
+  const intl = useIntl();
+  const { signedIn } = useIdentity();
+  const columnRef = useRef(null);
+
+  const allowLocalOnly = useAppSelector((state) => state.getIn(['settings', 'firehose', 'allowLocalOnly']));
+  const regex = useAppSelector((state) => state.getIn(['settings', 'firehose', 'regex', 'body']));
+
+  const onlyMedia = useAppSelector((state) => state.getIn(['settings', 'firehose', 'onlyMedia'], false));
+  const hasUnread = useAppSelector((state) => state.getIn(['timelines', `${feedType}${feedType === 'public' && allowLocalOnly ? ':allow_local_only' : ''}${onlyMedia ? ':media' : ''}`, 'unread'], 0) > 0);
+
+  const handlePin = useCallback(
+    () => {
+      switch(feedType) {
+      case 'community':
+        dispatch(addColumn('COMMUNITY', { other: { onlyMedia }, regex: { body: regex } }));
+        break;
+      case 'public':
+        dispatch(addColumn('PUBLIC', { other: { onlyMedia, allowLocalOnly }, regex: { body: regex }  }));
+        break;
+      case 'public:remote':
+        dispatch(addColumn('REMOTE', { other: { onlyMedia, onlyRemote: true }, regex: { body: regex }  }));
+        break;
+      }
+    },
+    [dispatch, onlyMedia, feedType, allowLocalOnly, regex],
+  );
+
+  const handleLoadMore = useCallback(
+    (maxId) => {
+      switch(feedType) {
+      case 'community':
+        dispatch(expandCommunityTimeline({ maxId, onlyMedia }));
+        break;
+      case 'public':
+        dispatch(expandPublicTimeline({ maxId, onlyMedia, allowLocalOnly }));
+        break;
+      case 'public:remote':
+        dispatch(expandPublicTimeline({ maxId, onlyMedia, onlyRemote: true }));
+        break;
+      }
+    },
+    [dispatch, onlyMedia, allowLocalOnly, feedType],
+  );
+
+  const handleHeaderClick = useCallback(() => columnRef.current?.scrollTop(), []);
+
+  useEffect(() => {
+    let disconnect;
+
+    switch(feedType) {
+    case 'community':
+      dispatch(expandCommunityTimeline({ onlyMedia }));
+      if (signedIn) {
+        disconnect = dispatch(connectCommunityStream({ onlyMedia }));
+      }
+      break;
+    case 'public':
+      dispatch(expandPublicTimeline({ onlyMedia, allowLocalOnly }));
+      if (signedIn) {
+        disconnect = dispatch(connectPublicStream({ onlyMedia, allowLocalOnly }));
+      }
+      break;
+    case 'public:remote':
+      dispatch(expandPublicTimeline({ onlyMedia, onlyRemote: true }));
+      if (signedIn) {
+        disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote: true }));
+      }
+      break;
+    }
+
+    return () => disconnect?.();
+  }, [dispatch, signedIn, feedType, onlyMedia, allowLocalOnly]);
+
+  const prependBanner = feedType === 'community' ? (
+    <DismissableBanner id='community_timeline'>
+      <FormattedMessage
+        id='dismissable_banner.community_timeline'
+        defaultMessage='These are the most recent public posts from people whose accounts are hosted by {domain}.'
+        values={{ domain }}
+      />
+    </DismissableBanner>
+  ) : (
+    <DismissableBanner id='public_timeline'>
+      <FormattedMessage
+        id='dismissable_banner.public_timeline'
+        defaultMessage='These are the most recent public posts from people on the social web that people on {domain} follow.'
+        values={{ domain }}
+      />
+    </DismissableBanner>
+  );
+
+  const emptyMessage = feedType === 'community' ? (
+    <FormattedMessage
+      id='empty_column.community'
+      defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!'
+    />
+  ) : (
+    <FormattedMessage
+      id='empty_column.public'
+      defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up'
+    />
+  );
+
+  return (
+    <Column bindToDocument={!multiColumn} ref={columnRef} label={intl.formatMessage(messages.title)}>
+      <ColumnHeader
+        icon='globe'
+        active={hasUnread}
+        title={intl.formatMessage(messages.title)}
+        onPin={handlePin}
+        onClick={handleHeaderClick}
+        multiColumn={multiColumn}
+      >
+        <ColumnSettings />
+      </ColumnHeader>
+
+      <div className='account__section-headline'>
+        <NavLink exact to='/public/local'>
+          <FormattedMessage tagName='div' id='firehose.local' defaultMessage='This server' />
+        </NavLink>
+
+        <NavLink exact to='/public/remote'>
+          <FormattedMessage tagName='div' id='firehose.remote' defaultMessage='Other servers' />
+        </NavLink>
+
+        <NavLink exact to='/public'>
+          <FormattedMessage tagName='div' id='firehose.all' defaultMessage='All' />
+        </NavLink>
+      </div>
+
+      <StatusListContainer
+        prepend={prependBanner}
+        timelineId={`${feedType}${feedType === 'public' && allowLocalOnly ? ':allow_local_only' : ''}${onlyMedia ? ':media' : ''}`}
+        onLoadMore={handleLoadMore}
+        trackScroll
+        scrollKey='firehose'
+        emptyMessage={emptyMessage}
+        bindToDocument={!multiColumn}
+        regex={regex}
+      />
+
+      <Helmet>
+        <title>{intl.formatMessage(messages.title)}</title>
+        <meta name='robots' content='noindex' />
+      </Helmet>
+    </Column>
+  );
+};
+
+Firehose.propTypes = {
+  multiColumn: PropTypes.bool,
+  feedType: PropTypes.string,
+};
+
+export default Firehose;
diff --git a/app/javascript/flavours/blobfox/features/follow_requests/components/account_authorize.jsx b/app/javascript/flavours/blobfox/features/follow_requests/components/account_authorize.jsx
new file mode 100644
index 00000000000000..5746af1b392a5f
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/follow_requests/components/account_authorize.jsx
@@ -0,0 +1,52 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import { Avatar } from '../../../components/avatar';
+import { DisplayName } from '../../../components/display_name';
+import { IconButton } from '../../../components/icon_button';
+import Permalink from '../../../components/permalink';
+
+const messages = defineMessages({
+  authorize: { id: 'follow_request.authorize', defaultMessage: 'Authorize' },
+  reject: { id: 'follow_request.reject', defaultMessage: 'Reject' },
+});
+
+class AccountAuthorize extends ImmutablePureComponent {
+
+  static propTypes = {
+    account: ImmutablePropTypes.record.isRequired,
+    onAuthorize: PropTypes.func.isRequired,
+    onReject: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  render () {
+    const { intl, account, onAuthorize, onReject } = this.props;
+    const content = { __html: account.get('note_emojified') };
+
+    return (
+      <div className='account-authorize__wrapper'>
+        <div className='account-authorize'>
+          <Permalink href={account.get('url')} to={`/@${account.get('acct')}`} className='detailed-status__display-name'>
+            <div className='account-authorize__avatar'><Avatar account={account} size={48} /></div>
+            <DisplayName account={account} />
+          </Permalink>
+
+          <div className='account__header__content translate' dangerouslySetInnerHTML={content} />
+        </div>
+
+        <div className='account--panel'>
+          <div className='account--panel__button'><IconButton title={intl.formatMessage(messages.authorize)} icon='check' onClick={onAuthorize} /></div>
+          <div className='account--panel__button'><IconButton title={intl.formatMessage(messages.reject)} icon='times' onClick={onReject} /></div>
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(AccountAuthorize);
diff --git a/app/javascript/flavours/blobfox/features/follow_requests/containers/account_authorize_container.js b/app/javascript/flavours/blobfox/features/follow_requests/containers/account_authorize_container.js
new file mode 100644
index 00000000000000..c9c8dd7d874d13
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/follow_requests/containers/account_authorize_container.js
@@ -0,0 +1,27 @@
+import { connect } from 'react-redux';
+
+import { authorizeFollowRequest, rejectFollowRequest } from '../../../actions/accounts';
+import { makeGetAccount } from '../../../selectors';
+import AccountAuthorize from '../components/account_authorize';
+
+const makeMapStateToProps = () => {
+  const getAccount = makeGetAccount();
+
+  const mapStateToProps = (state, props) => ({
+    account: getAccount(state, props.id),
+  });
+
+  return mapStateToProps;
+};
+
+const mapDispatchToProps = (dispatch, { id }) => ({
+  onAuthorize () {
+    dispatch(authorizeFollowRequest(id));
+  },
+
+  onReject () {
+    dispatch(rejectFollowRequest(id));
+  },
+});
+
+export default connect(makeMapStateToProps, mapDispatchToProps)(AccountAuthorize);
diff --git a/app/javascript/flavours/blobfox/features/follow_requests/index.jsx b/app/javascript/flavours/blobfox/features/follow_requests/index.jsx
new file mode 100644
index 00000000000000..796254e0ec9928
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/follow_requests/index.jsx
@@ -0,0 +1,96 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
+import { debounce } from 'lodash';
+
+import { fetchFollowRequests, expandFollowRequests } from '../../actions/accounts';
+import ColumnBackButtonSlim from '../../components/column_back_button_slim';
+import ScrollableList from '../../components/scrollable_list';
+import { me } from '../../initial_state';
+import Column from '../ui/components/column';
+
+import AccountAuthorizeContainer from './containers/account_authorize_container';
+
+const messages = defineMessages({
+  heading: { id: 'column.follow_requests', defaultMessage: 'Follow requests' },
+});
+
+const mapStateToProps = state => ({
+  accountIds: state.getIn(['user_lists', 'follow_requests', 'items']),
+  isLoading: state.getIn(['user_lists', 'follow_requests', 'isLoading'], true),
+  hasMore: !!state.getIn(['user_lists', 'follow_requests', 'next']),
+  locked: !!state.getIn(['accounts', me, 'locked']),
+  domain: state.getIn(['meta', 'domain']),
+});
+
+class FollowRequests extends ImmutablePureComponent {
+
+  static propTypes = {
+    params: PropTypes.object.isRequired,
+    dispatch: PropTypes.func.isRequired,
+    hasMore: PropTypes.bool,
+    isLoading: PropTypes.bool,
+    accountIds: ImmutablePropTypes.list,
+    locked: PropTypes.bool,
+    domain: PropTypes.string,
+    intl: PropTypes.object.isRequired,
+    multiColumn: PropTypes.bool,
+  };
+
+  UNSAFE_componentWillMount () {
+    this.props.dispatch(fetchFollowRequests());
+  }
+
+  handleLoadMore = debounce(() => {
+    this.props.dispatch(expandFollowRequests());
+  }, 300, { leading: true });
+
+  render () {
+    const { intl, accountIds, hasMore, multiColumn, locked, domain, isLoading } = this.props;
+
+    const emptyMessage = <FormattedMessage id='empty_column.follow_requests' defaultMessage="You don't have any follow requests yet. When you receive one, it will show up here." />;
+    const unlockedPrependMessage = !locked && accountIds.size > 0 && (
+      <div className='follow_requests-unlocked_explanation'>
+        <FormattedMessage
+          id='follow_requests.unlocked_explanation'
+          defaultMessage='Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.'
+          values={{ domain: domain }}
+        />
+      </div>
+    );
+
+    return (
+      <Column bindToDocument={!multiColumn} icon='user-plus' heading={intl.formatMessage(messages.heading)}>
+        <ColumnBackButtonSlim />
+        <ScrollableList
+          scrollKey='follow_requests'
+          onLoadMore={this.handleLoadMore}
+          hasMore={hasMore}
+          isLoading={isLoading}
+          showLoading={isLoading && accountIds.size === 0}
+          emptyMessage={emptyMessage}
+          bindToDocument={!multiColumn}
+          prepend={unlockedPrependMessage}
+        >
+          {accountIds.map(id =>
+            <AccountAuthorizeContainer key={id} id={id} />,
+          )}
+        </ScrollableList>
+
+        <Helmet>
+          <meta name='robots' content='noindex' />
+        </Helmet>
+      </Column>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(injectIntl(FollowRequests));
diff --git a/app/javascript/flavours/blobfox/features/followed_tags/index.jsx b/app/javascript/flavours/blobfox/features/followed_tags/index.jsx
new file mode 100644
index 00000000000000..7d877402b668dd
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/followed_tags/index.jsx
@@ -0,0 +1,93 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
+import { debounce } from 'lodash';
+
+import { expandFollowedHashtags, fetchFollowedHashtags } from 'flavours/blobfox/actions/tags';
+import ColumnHeader from 'flavours/blobfox/components/column_header';
+import Hashtag from 'flavours/blobfox/components/hashtag';
+import ScrollableList from 'flavours/blobfox/components/scrollable_list';
+import Column from 'flavours/blobfox/features/ui/components/column';
+
+const messages = defineMessages({
+  heading: { id: 'followed_tags', defaultMessage: 'Followed hashtags' },
+});
+
+const mapStateToProps = state => ({
+  hashtags: state.getIn(['followed_tags', 'items']),
+  isLoading: state.getIn(['followed_tags', 'isLoading'], true),
+  hasMore: !!state.getIn(['followed_tags', 'next']),
+});
+
+class FollowedTags extends ImmutablePureComponent {
+
+  static propTypes = {
+    params: PropTypes.object.isRequired,
+    dispatch: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+    hashtags: ImmutablePropTypes.list,
+    isLoading: PropTypes.bool,
+    hasMore: PropTypes.bool,
+    multiColumn: PropTypes.bool,
+  };
+
+  componentDidMount() {
+    this.props.dispatch(fetchFollowedHashtags());
+  }
+
+  handleLoadMore = debounce(() => {
+    this.props.dispatch(expandFollowedHashtags());
+  }, 300, { leading: true });
+
+  render () {
+    const { intl, hashtags, isLoading, hasMore, multiColumn } = this.props;
+
+    const emptyMessage = <FormattedMessage id='empty_column.followed_tags' defaultMessage='You have not followed any hashtags yet. When you do, they will show up here.' />;
+
+    return (
+      <Column bindToDocument={!multiColumn}>
+        <ColumnHeader
+          icon='hashtag'
+          title={intl.formatMessage(messages.heading)}
+          showBackButton
+          multiColumn={multiColumn}
+        />
+
+        <ScrollableList
+          scrollKey='followed_tags'
+          emptyMessage={emptyMessage}
+          hasMore={hasMore}
+          isLoading={isLoading}
+          onLoadMore={this.handleLoadMore}
+          bindToDocument={!multiColumn}
+        >
+          {hashtags.map((hashtag) => (
+            <Hashtag
+              key={hashtag.get('name')}
+              name={hashtag.get('name')}
+              to={`/tags/${hashtag.get('name')}`}
+              withGraph={false}
+              // Taken from ImmutableHashtag. Should maybe refactor ImmutableHashtag to accept more options?
+              people={hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1}
+              history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()}
+            />
+          ))}
+        </ScrollableList>
+
+        <Helmet>
+          <meta name='robots' content='noindex' />
+        </Helmet>
+      </Column>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(injectIntl(FollowedTags));
diff --git a/app/javascript/flavours/blobfox/features/followers/index.jsx b/app/javascript/flavours/blobfox/features/followers/index.jsx
new file mode 100644
index 00000000000000..85cc79b362122d
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/followers/index.jsx
@@ -0,0 +1,177 @@
+import PropTypes from 'prop-types';
+
+import { 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 { TimelineHint } from 'flavours/blobfox/components/timeline_hint';
+import BundleColumnError from 'flavours/blobfox/features/ui/components/bundle_column_error';
+import { normalizeForLookup } from 'flavours/blobfox/reducers/accounts_map';
+import { getAccountHidden } from 'flavours/blobfox/selectors';
+
+import {
+  lookupAccount,
+  fetchAccount,
+  fetchFollowers,
+  expandFollowers,
+} from '../../actions/accounts';
+import { LoadingIndicator } from '../../components/loading_indicator';
+import ScrollableList from '../../components/scrollable_list';
+import AccountContainer from '../../containers/account_container';
+import ProfileColumnHeader from '../account/components/profile_column_header';
+import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint';
+import HeaderContainer from '../account_timeline/containers/header_container';
+import Column from '../ui/components/column';
+
+const mapStateToProps = (state, { params: { acct, id } }) => {
+  const accountId = id || state.getIn(['accounts_map', normalizeForLookup(acct)]);
+
+  if (!accountId) {
+    return {
+      isLoading: true,
+    };
+  }
+
+  return {
+    accountId,
+    remote: !!(state.getIn(['accounts', accountId, 'acct']) !== state.getIn(['accounts', accountId, 'username'])),
+    remoteUrl: state.getIn(['accounts', accountId, 'url']),
+    isAccount: !!state.getIn(['accounts', accountId]),
+    accountIds: state.getIn(['user_lists', 'followers', accountId, 'items']),
+    hasMore: !!state.getIn(['user_lists', 'followers', accountId, 'next']),
+    isLoading: state.getIn(['user_lists', 'followers', accountId, 'isLoading'], true),
+    suspended: state.getIn(['accounts', accountId, 'suspended'], false),
+    hidden: getAccountHidden(state, accountId),
+  };
+};
+
+const RemoteHint = ({ url }) => (
+  <TimelineHint url={url} resource={<FormattedMessage id='timeline_hint.resources.followers' defaultMessage='Followers' />} />
+);
+
+RemoteHint.propTypes = {
+  url: PropTypes.string.isRequired,
+};
+
+class Followers extends ImmutablePureComponent {
+
+  static propTypes = {
+    params: PropTypes.shape({
+      acct: PropTypes.string,
+      id: PropTypes.string,
+    }).isRequired,
+    accountId: PropTypes.string,
+    dispatch: PropTypes.func.isRequired,
+    accountIds: ImmutablePropTypes.list,
+    hasMore: PropTypes.bool,
+    isLoading: PropTypes.bool,
+    isAccount: PropTypes.bool,
+    suspended: PropTypes.bool,
+    hidden: PropTypes.bool,
+    remote: PropTypes.bool,
+    remoteUrl: PropTypes.string,
+    multiColumn: PropTypes.bool,
+  };
+
+  _load () {
+    const { accountId, isAccount, dispatch } = this.props;
+
+    if (!isAccount) dispatch(fetchAccount(accountId));
+    dispatch(fetchFollowers(accountId));
+  }
+
+  componentDidMount () {
+    const { params: { acct }, accountId, dispatch } = this.props;
+
+    if (accountId) {
+      this._load();
+    } else {
+      dispatch(lookupAccount(acct));
+    }
+  }
+
+  componentDidUpdate (prevProps) {
+    const { params: { acct }, accountId, dispatch } = this.props;
+
+    if (prevProps.accountId !== accountId && accountId) {
+      this._load();
+    } else if (prevProps.params.acct !== acct) {
+      dispatch(lookupAccount(acct));
+    }
+  }
+
+  handleLoadMore = debounce(() => {
+    this.props.dispatch(expandFollowers(this.props.accountId));
+  }, 300, { leading: true });
+
+  setRef = c => {
+    this.column = c;
+  };
+
+  handleHeaderClick = () => {
+    this.column.scrollTop();
+  };
+
+  render () {
+    const { accountId, accountIds, hasMore, isAccount, multiColumn, isLoading, suspended, hidden, remote, remoteUrl } = this.props;
+
+    if (!isAccount) {
+      return (
+        <BundleColumnError multiColumn={multiColumn} errorType='routing' />
+      );
+    }
+
+    if (!accountIds) {
+      return (
+        <Column>
+          <LoadingIndicator />
+        </Column>
+      );
+    }
+
+    let emptyMessage;
+
+    const forceEmptyState = suspended || hidden;
+
+    if (suspended) {
+      emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
+    } else if (hidden) {
+      emptyMessage = <LimitedAccountHint accountId={accountId} />;
+    } else if (remote && accountIds.isEmpty()) {
+      emptyMessage = <RemoteHint url={remoteUrl} />;
+    } else {
+      emptyMessage = <FormattedMessage id='account.followers.empty' defaultMessage='No one follows this user yet.' />;
+    }
+
+    const remoteMessage = remote ? <RemoteHint url={remoteUrl} /> : null;
+
+    return (
+      <Column ref={this.setRef}>
+        <ProfileColumnHeader onClick={this.handleHeaderClick} multiColumn={multiColumn} />
+
+        <ScrollableList
+          scrollKey='followers'
+          hasMore={!forceEmptyState && hasMore}
+          isLoading={isLoading}
+          onLoadMore={this.handleLoadMore}
+          prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />}
+          alwaysPrepend
+          append={remoteMessage}
+          emptyMessage={emptyMessage}
+          bindToDocument={!multiColumn}
+        >
+          {accountIds.map(id =>
+            <AccountContainer key={id} id={id} withNote={false} />,
+          )}
+        </ScrollableList>
+      </Column>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(Followers);
diff --git a/app/javascript/flavours/blobfox/features/following/index.jsx b/app/javascript/flavours/blobfox/features/following/index.jsx
new file mode 100644
index 00000000000000..42336864d7551b
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/following/index.jsx
@@ -0,0 +1,177 @@
+import PropTypes from 'prop-types';
+
+import { 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 { TimelineHint } from 'flavours/blobfox/components/timeline_hint';
+import BundleColumnError from 'flavours/blobfox/features/ui/components/bundle_column_error';
+import { normalizeForLookup } from 'flavours/blobfox/reducers/accounts_map';
+import { getAccountHidden } from 'flavours/blobfox/selectors';
+
+import {
+  lookupAccount,
+  fetchAccount,
+  fetchFollowing,
+  expandFollowing,
+} from '../../actions/accounts';
+import { LoadingIndicator } from '../../components/loading_indicator';
+import ScrollableList from '../../components/scrollable_list';
+import AccountContainer from '../../containers/account_container';
+import ProfileColumnHeader from '../account/components/profile_column_header';
+import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint';
+import HeaderContainer from '../account_timeline/containers/header_container';
+import Column from '../ui/components/column';
+
+const mapStateToProps = (state, { params: { acct, id } }) => {
+  const accountId = id || state.getIn(['accounts_map', normalizeForLookup(acct)]);
+
+  if (!accountId) {
+    return {
+      isLoading: true,
+    };
+  }
+
+  return {
+    accountId,
+    remote: !!(state.getIn(['accounts', accountId, 'acct']) !== state.getIn(['accounts', accountId, 'username'])),
+    remoteUrl: state.getIn(['accounts', accountId, 'url']),
+    isAccount: !!state.getIn(['accounts', accountId]),
+    accountIds: state.getIn(['user_lists', 'following', accountId, 'items']),
+    hasMore: !!state.getIn(['user_lists', 'following', accountId, 'next']),
+    isLoading: state.getIn(['user_lists', 'following', accountId, 'isLoading'], true),
+    suspended: state.getIn(['accounts', accountId, 'suspended'], false),
+    hidden: getAccountHidden(state, accountId),
+  };
+};
+
+const RemoteHint = ({ url }) => (
+  <TimelineHint url={url} resource={<FormattedMessage id='timeline_hint.resources.follows' defaultMessage='Follows' />} />
+);
+
+RemoteHint.propTypes = {
+  url: PropTypes.string.isRequired,
+};
+
+class Following extends ImmutablePureComponent {
+
+  static propTypes = {
+    params: PropTypes.shape({
+      acct: PropTypes.string,
+      id: PropTypes.string,
+    }).isRequired,
+    accountId: PropTypes.string,
+    dispatch: PropTypes.func.isRequired,
+    accountIds: ImmutablePropTypes.list,
+    hasMore: PropTypes.bool,
+    isLoading: PropTypes.bool,
+    isAccount: PropTypes.bool,
+    suspended: PropTypes.bool,
+    hidden: PropTypes.bool,
+    remote: PropTypes.bool,
+    remoteUrl: PropTypes.string,
+    multiColumn: PropTypes.bool,
+  };
+
+  _load () {
+    const { accountId, isAccount, dispatch } = this.props;
+
+    if (!isAccount) dispatch(fetchAccount(accountId));
+    dispatch(fetchFollowing(accountId));
+  }
+
+  componentDidMount () {
+    const { params: { acct }, accountId, dispatch } = this.props;
+
+    if (accountId) {
+      this._load();
+    } else {
+      dispatch(lookupAccount(acct));
+    }
+  }
+
+  componentDidUpdate (prevProps) {
+    const { params: { acct }, accountId, dispatch } = this.props;
+
+    if (prevProps.accountId !== accountId && accountId) {
+      this._load();
+    } else if (prevProps.params.acct !== acct) {
+      dispatch(lookupAccount(acct));
+    }
+  }
+
+  handleLoadMore = debounce(() => {
+    this.props.dispatch(expandFollowing(this.props.accountId));
+  }, 300, { leading: true });
+
+  setRef = c => {
+    this.column = c;
+  };
+
+  handleHeaderClick = () => {
+    this.column.scrollTop();
+  };
+
+  render () {
+    const { accountId, accountIds, hasMore, isAccount, multiColumn, isLoading, suspended, hidden, remote, remoteUrl } = this.props;
+
+    if (!isAccount) {
+      return (
+        <BundleColumnError multiColumn={multiColumn} errorType='routing' />
+      );
+    }
+
+    if (!accountIds) {
+      return (
+        <Column>
+          <LoadingIndicator />
+        </Column>
+      );
+    }
+
+    let emptyMessage;
+
+    const forceEmptyState = suspended || hidden;
+
+    if (suspended) {
+      emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
+    } else if (hidden) {
+      emptyMessage = <LimitedAccountHint accountId={accountId} />;
+    } else if (remote && accountIds.isEmpty()) {
+      emptyMessage = <RemoteHint url={remoteUrl} />;
+    } else {
+      emptyMessage = <FormattedMessage id='account.follows.empty' defaultMessage="This user doesn't follow anyone yet." />;
+    }
+
+    const remoteMessage = remote ? <RemoteHint url={remoteUrl} /> : null;
+
+    return (
+      <Column ref={this.setRef}>
+        <ProfileColumnHeader onClick={this.handleHeaderClick} multiColumn={multiColumn} />
+
+        <ScrollableList
+          scrollKey='following'
+          hasMore={!forceEmptyState && hasMore}
+          isLoading={isLoading}
+          onLoadMore={this.handleLoadMore}
+          prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />}
+          alwaysPrepend
+          append={remoteMessage}
+          emptyMessage={emptyMessage}
+          bindToDocument={!multiColumn}
+        >
+          {accountIds.map(id =>
+            <AccountContainer key={id} id={id} withNote={false} />,
+          )}
+        </ScrollableList>
+      </Column>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(Following);
diff --git a/app/javascript/flavours/blobfox/features/getting_started/components/announcements.jsx b/app/javascript/flavours/blobfox/features/getting_started/components/announcements.jsx
new file mode 100644
index 00000000000000..982053a3953b51
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/getting_started/components/announcements.jsx
@@ -0,0 +1,455 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, injectIntl, FormattedMessage, FormattedDate } from 'react-intl';
+
+import classNames from 'classnames';
+import { withRouter } from 'react-router-dom';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import TransitionMotion from 'react-motion/lib/TransitionMotion';
+import spring from 'react-motion/lib/spring';
+import ReactSwipeableViews from 'react-swipeable-views';
+
+import { AnimatedNumber } from 'flavours/blobfox/components/animated_number';
+import { Icon }  from 'flavours/blobfox/components/icon';
+import { IconButton } from 'flavours/blobfox/components/icon_button';
+import EmojiPickerDropdown from 'flavours/blobfox/features/compose/containers/emoji_picker_dropdown_container';
+import { unicodeMapping } from 'flavours/blobfox/features/emoji/emoji_unicode_mapping_light';
+import { autoPlayGif, reduceMotion, disableSwiping, mascot } from 'flavours/blobfox/initial_state';
+import { assetHost } from 'flavours/blobfox/utils/config';
+import { WithRouterPropTypes } from 'flavours/blobfox/utils/react_router';
+import elephantUIPlane from 'mastodon/../images/elephant_ui_plane.svg';
+
+const messages = defineMessages({
+  close: { id: 'lightbox.close', defaultMessage: 'Close' },
+  previous: { id: 'lightbox.previous', defaultMessage: 'Previous' },
+  next: { id: 'lightbox.next', defaultMessage: 'Next' },
+});
+
+class ContentWithRouter extends ImmutablePureComponent {
+  static propTypes = {
+    announcement: ImmutablePropTypes.map.isRequired,
+    ...WithRouterPropTypes,
+  };
+
+  setRef = c => {
+    this.node = c;
+  };
+
+  componentDidMount () {
+    this._updateLinks();
+  }
+
+  componentDidUpdate () {
+    this._updateLinks();
+  }
+
+  _updateLinks () {
+    const node = this.node;
+
+    if (!node) {
+      return;
+    }
+
+    const links = node.querySelectorAll('a');
+
+    for (var i = 0; i < links.length; ++i) {
+      let link = links[i];
+
+      if (link.classList.contains('status-link')) {
+        continue;
+      }
+
+      link.classList.add('status-link');
+
+      let mention = this.props.announcement.get('mentions').find(item => link.href === item.get('url'));
+
+      if (mention) {
+        link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
+        link.setAttribute('title', mention.get('acct'));
+      } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
+        link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
+      } else {
+        let status = this.props.announcement.get('statuses').find(item => link.href === item.get('url'));
+        if (status) {
+          link.addEventListener('click', this.onStatusClick.bind(this, status), false);
+        }
+        link.setAttribute('title', link.href);
+        link.classList.add('unhandled-link');
+      }
+
+      link.setAttribute('target', '_blank');
+      link.setAttribute('rel', 'noopener noreferrer');
+    }
+  }
+
+  onMentionClick = (mention, e) => {
+    if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
+      e.preventDefault();
+      this.props.history.push(`/@${mention.get('acct')}`);
+    }
+  };
+
+  onHashtagClick = (hashtag, e) => {
+    hashtag = hashtag.replace(/^#/, '');
+
+    if (this.props.history&& e.button === 0 && !(e.ctrlKey || e.metaKey)) {
+      e.preventDefault();
+      this.props.history.push(`/tags/${hashtag}`);
+    }
+  };
+
+  onStatusClick = (status, e) => {
+    if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
+      e.preventDefault();
+      this.props.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
+    }
+  };
+
+  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 { announcement } = this.props;
+
+    return (
+      <div
+        className='announcements__item__content translate'
+        ref={this.setRef}
+        dangerouslySetInnerHTML={{ __html: announcement.get('contentHtml') }}
+        onMouseEnter={this.handleMouseEnter}
+        onMouseLeave={this.handleMouseLeave}
+      />
+    );
+  }
+
+}
+
+const Content = withRouter(ContentWithRouter);
+
+class Emoji extends PureComponent {
+
+  static propTypes = {
+    emoji: PropTypes.string.isRequired,
+    emojiMap: ImmutablePropTypes.map.isRequired,
+    hovered: PropTypes.bool.isRequired,
+  };
+
+  render () {
+    const { emoji, emojiMap, hovered } = this.props;
+
+    if (unicodeMapping[emoji]) {
+      const { filename, shortCode } = unicodeMapping[this.props.emoji];
+      const title = shortCode ? `:${shortCode}:` : '';
+
+      return (
+        <img
+          draggable='false'
+          className='emojione'
+          alt={emoji}
+          title={title}
+          src={`${assetHost}/emoji/${filename}.svg`}
+        />
+      );
+    } else if (emojiMap.get(emoji)) {
+      const filename  = (autoPlayGif || hovered) ? emojiMap.getIn([emoji, 'url']) : emojiMap.getIn([emoji, 'static_url']);
+      const shortCode = `:${emoji}:`;
+
+      return (
+        <img
+          draggable='false'
+          className='emojione custom-emoji'
+          alt={shortCode}
+          title={shortCode}
+          src={filename}
+        />
+      );
+    } else {
+      return null;
+    }
+  }
+
+}
+
+class Reaction extends ImmutablePureComponent {
+
+  static propTypes = {
+    announcementId: PropTypes.string.isRequired,
+    reaction: ImmutablePropTypes.map.isRequired,
+    addReaction: PropTypes.func.isRequired,
+    removeReaction: PropTypes.func.isRequired,
+    emojiMap: ImmutablePropTypes.map.isRequired,
+    style: PropTypes.object,
+  };
+
+  state = {
+    hovered: false,
+  };
+
+  handleClick = () => {
+    const { reaction, announcementId, addReaction, removeReaction } = this.props;
+
+    if (reaction.get('me')) {
+      removeReaction(announcementId, reaction.get('name'));
+    } else {
+      addReaction(announcementId, reaction.get('name'));
+    }
+  };
+
+  handleMouseEnter = () => this.setState({ hovered: true });
+
+  handleMouseLeave = () => this.setState({ hovered: false });
+
+  render () {
+    const { reaction } = this.props;
+
+    let shortCode = reaction.get('name');
+
+    if (unicodeMapping[shortCode]) {
+      shortCode = unicodeMapping[shortCode].shortCode;
+    }
+
+    return (
+      <button className={classNames('reactions-bar__item', { active: reaction.get('me') })} onClick={this.handleClick} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} title={`:${shortCode}:`} style={this.props.style}>
+        <span className='reactions-bar__item__emoji'><Emoji hovered={this.state.hovered} emoji={reaction.get('name')} emojiMap={this.props.emojiMap} /></span>
+        <span className='reactions-bar__item__count'><AnimatedNumber value={reaction.get('count')} /></span>
+      </button>
+    );
+  }
+
+}
+
+class ReactionsBar extends ImmutablePureComponent {
+
+  static propTypes = {
+    announcementId: PropTypes.string.isRequired,
+    reactions: ImmutablePropTypes.list.isRequired,
+    addReaction: PropTypes.func.isRequired,
+    removeReaction: PropTypes.func.isRequired,
+    emojiMap: ImmutablePropTypes.map.isRequired,
+  };
+
+  handleEmojiPick = data => {
+    const { addReaction, announcementId } = this.props;
+    addReaction(announcementId, data.native.replace(/:/g, ''));
+  };
+
+  willEnter () {
+    return { scale: reduceMotion ? 1 : 0 };
+  }
+
+  willLeave () {
+    return { scale: reduceMotion ? 0 : spring(0, { stiffness: 170, damping: 26 }) };
+  }
+
+  render () {
+    const { reactions } = this.props;
+    const visibleReactions = reactions.filter(x => x.get('count') > 0);
+
+    const styles = visibleReactions.map(reaction => ({
+      key: reaction.get('name'),
+      data: reaction,
+      style: { scale: reduceMotion ? 1 : spring(1, { stiffness: 150, damping: 13 }) },
+    })).toArray();
+
+    return (
+      <TransitionMotion styles={styles} willEnter={this.willEnter} willLeave={this.willLeave}>
+        {items => (
+          <div className={classNames('reactions-bar', { 'reactions-bar--empty': visibleReactions.isEmpty() })}>
+            {items.map(({ key, data, style }) => (
+              <Reaction
+                key={key}
+                reaction={data}
+                style={{ transform: `scale(${style.scale})`, position: style.scale < 0.5 ? 'absolute' : 'static' }}
+                announcementId={this.props.announcementId}
+                addReaction={this.props.addReaction}
+                removeReaction={this.props.removeReaction}
+                emojiMap={this.props.emojiMap}
+              />
+            ))}
+
+            {visibleReactions.size < 8 && <EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} button={<Icon id='plus' />} />}
+          </div>
+        )}
+      </TransitionMotion>
+    );
+  }
+
+}
+
+class Announcement extends ImmutablePureComponent {
+
+  static propTypes = {
+    announcement: ImmutablePropTypes.map.isRequired,
+    emojiMap: ImmutablePropTypes.map.isRequired,
+    addReaction: PropTypes.func.isRequired,
+    removeReaction: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+    selected: PropTypes.bool,
+  };
+
+  state = {
+    unread: !this.props.announcement.get('read'),
+  };
+
+  componentDidUpdate () {
+    const { selected, announcement } = this.props;
+    if (!selected && this.state.unread !== !announcement.get('read')) {
+      this.setState({ unread: !announcement.get('read') });
+    }
+  }
+
+  render () {
+    const { announcement } = this.props;
+    const { unread } = this.state;
+    const startsAt = announcement.get('starts_at') && new Date(announcement.get('starts_at'));
+    const endsAt = announcement.get('ends_at') && new Date(announcement.get('ends_at'));
+    const now = new Date();
+    const hasTimeRange = startsAt && endsAt;
+    const skipYear = hasTimeRange && startsAt.getFullYear() === endsAt.getFullYear() && endsAt.getFullYear() === now.getFullYear();
+    const skipEndDate = hasTimeRange && startsAt.getDate() === endsAt.getDate() && startsAt.getMonth() === endsAt.getMonth() && startsAt.getFullYear() === endsAt.getFullYear();
+    const skipTime = announcement.get('all_day');
+
+    return (
+      <div className='announcements__item'>
+        <strong className='announcements__item__range'>
+          <FormattedMessage id='announcement.announcement' defaultMessage='Announcement' />
+          {hasTimeRange && <span> · <FormattedDate value={startsAt} hour12={false} year={(skipYear || startsAt.getFullYear() === now.getFullYear()) ? undefined : 'numeric'} month='short' day='2-digit' hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'} /> - <FormattedDate value={endsAt} hour12={false} year={(skipYear || endsAt.getFullYear() === now.getFullYear()) ? undefined : 'numeric'} month={skipEndDate ? undefined : 'short'} day={skipEndDate ? undefined : '2-digit'} hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'} /></span>}
+        </strong>
+
+        <Content announcement={announcement} />
+
+        <ReactionsBar
+          reactions={announcement.get('reactions')}
+          announcementId={announcement.get('id')}
+          addReaction={this.props.addReaction}
+          removeReaction={this.props.removeReaction}
+          emojiMap={this.props.emojiMap}
+        />
+
+        {unread && <span className='announcements__item__unread' />}
+      </div>
+    );
+  }
+
+}
+
+class Announcements extends ImmutablePureComponent {
+
+  static propTypes = {
+    announcements: ImmutablePropTypes.list,
+    emojiMap: ImmutablePropTypes.map.isRequired,
+    dismissAnnouncement: PropTypes.func.isRequired,
+    addReaction: PropTypes.func.isRequired,
+    removeReaction: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  state = {
+    index: 0,
+  };
+
+  static getDerivedStateFromProps(props, state) {
+    if (props.announcements.size > 0 && state.index >= props.announcements.size) {
+      return { index: props.announcements.size - 1 };
+    } else {
+      return null;
+    }
+  }
+
+  componentDidMount () {
+    this._markAnnouncementAsRead();
+  }
+
+  componentDidUpdate () {
+    this._markAnnouncementAsRead();
+  }
+
+  _markAnnouncementAsRead () {
+    const { dismissAnnouncement, announcements } = this.props;
+    const { index } = this.state;
+    const announcement = announcements.get(announcements.size - 1 - index);
+    if (!announcement.get('read')) dismissAnnouncement(announcement.get('id'));
+  }
+
+  handleChangeIndex = index => {
+    this.setState({ index: index % this.props.announcements.size });
+  };
+
+  handleNextClick = () => {
+    this.setState({ index: (this.state.index + 1) % this.props.announcements.size });
+  };
+
+  handlePrevClick = () => {
+    this.setState({ index: (this.props.announcements.size + this.state.index - 1) % this.props.announcements.size });
+  };
+
+  render () {
+    const { announcements, intl } = this.props;
+    const { index } = this.state;
+
+    if (announcements.isEmpty()) {
+      return null;
+    }
+
+    return (
+      <div className='announcements'>
+        <img className='announcements__mastodon' alt='' draggable='false' src={mascot || elephantUIPlane} />
+
+        <div className='announcements__container'>
+          <ReactSwipeableViews animateHeight animateTransitions={!reduceMotion} index={index} onChangeIndex={this.handleChangeIndex}>
+            {announcements.map((announcement, idx) => (
+              <Announcement
+                key={announcement.get('id')}
+                announcement={announcement}
+                emojiMap={this.props.emojiMap}
+                addReaction={this.props.addReaction}
+                removeReaction={this.props.removeReaction}
+                intl={intl}
+                selected={index === idx}
+                disabled={disableSwiping}
+              />
+            )).reverse()}
+          </ReactSwipeableViews>
+
+          {announcements.size > 1 && (
+            <div className='announcements__pagination'>
+              <IconButton disabled={announcements.size === 1} title={intl.formatMessage(messages.previous)} icon='chevron-left' onClick={this.handlePrevClick} size={13} />
+              <span>{index + 1} / {announcements.size}</span>
+              <IconButton disabled={announcements.size === 1} title={intl.formatMessage(messages.next)} icon='chevron-right' onClick={this.handleNextClick} size={13} />
+            </div>
+          )}
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(Announcements);
diff --git a/app/javascript/flavours/blobfox/features/getting_started/components/trends.jsx b/app/javascript/flavours/blobfox/features/getting_started/components/trends.jsx
new file mode 100644
index 00000000000000..0971cce1352270
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/getting_started/components/trends.jsx
@@ -0,0 +1,54 @@
+import PropTypes from 'prop-types';
+
+import { FormattedMessage } from 'react-intl';
+
+import { Link } from 'react-router-dom';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import { ImmutableHashtag as Hashtag } from 'flavours/blobfox/components/hashtag';
+
+export default class Trends extends ImmutablePureComponent {
+
+  static defaultProps = {
+    loading: false,
+  };
+
+  static propTypes = {
+    trends: ImmutablePropTypes.list,
+    fetchTrends: PropTypes.func.isRequired,
+  };
+
+  componentDidMount () {
+    this.props.fetchTrends();
+    this.refreshInterval = setInterval(() => this.props.fetchTrends(), 900 * 1000);
+  }
+
+  componentWillUnmount () {
+    if (this.refreshInterval) {
+      clearInterval(this.refreshInterval);
+    }
+  }
+
+  render () {
+    const { trends } = this.props;
+
+    if (!trends || trends.isEmpty()) {
+      return null;
+    }
+
+    return (
+      <div className='getting-started__trends'>
+        <h4>
+          <Link to={'/explore/tags'}>
+            <FormattedMessage id='trends.trending_now' defaultMessage='Trending now' />
+          </Link>
+        </h4>
+
+        {trends.take(3).map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/getting_started/containers/announcements_container.js b/app/javascript/flavours/blobfox/features/getting_started/containers/announcements_container.js
new file mode 100644
index 00000000000000..3af2628a403965
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/getting_started/containers/announcements_container.js
@@ -0,0 +1,22 @@
+import { Map as ImmutableMap } from 'immutable';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+
+import { addReaction, removeReaction, dismissAnnouncement } from 'flavours/blobfox/actions/announcements';
+
+import Announcements from '../components/announcements';
+
+const customEmojiMap = createSelector([state => state.get('custom_emojis')], items => items.reduce((map, emoji) => map.set(emoji.get('shortcode'), emoji), ImmutableMap()));
+
+const mapStateToProps = state => ({
+  announcements: state.getIn(['announcements', 'items']),
+  emojiMap: customEmojiMap(state),
+});
+
+const mapDispatchToProps = dispatch => ({
+  dismissAnnouncement: id => dispatch(dismissAnnouncement(id)),
+  addReaction: (id, name) => dispatch(addReaction(id, name)),
+  removeReaction: (id, name) => dispatch(removeReaction(id, name)),
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(Announcements);
diff --git a/app/javascript/flavours/blobfox/features/getting_started/containers/trends_container.js b/app/javascript/flavours/blobfox/features/getting_started/containers/trends_container.js
new file mode 100644
index 00000000000000..e35c0c46f54336
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/getting_started/containers/trends_container.js
@@ -0,0 +1,15 @@
+import { connect } from 'react-redux';
+
+import { fetchTrendingHashtags } from 'flavours/blobfox/actions/trends';
+
+import Trends from '../components/trends';
+
+const mapStateToProps = state => ({
+  trends: state.getIn(['trends', 'tags', 'items']),
+});
+
+const mapDispatchToProps = dispatch => ({
+  fetchTrends: () => dispatch(fetchTrendingHashtags()),
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(Trends);
diff --git a/app/javascript/flavours/blobfox/features/getting_started/index.jsx b/app/javascript/flavours/blobfox/features/getting_started/index.jsx
new file mode 100644
index 00000000000000..e1bc9cb9fbfe2b
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/getting_started/index.jsx
@@ -0,0 +1,208 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+
+import { List as ImmutableList } from 'immutable';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+
+import { fetchFollowRequests } from 'flavours/blobfox/actions/accounts';
+import { fetchLists } from 'flavours/blobfox/actions/lists';
+import { openModal } from 'flavours/blobfox/actions/modal';
+import Column from 'flavours/blobfox/features/ui/components/column';
+import LinkFooter from 'flavours/blobfox/features/ui/components/link_footer';
+import { preferencesLink } from 'flavours/blobfox/utils/backend_links';
+
+import { me, showTrends } from '../../initial_state';
+import NavigationBar from '../compose/components/navigation_bar';
+import ColumnLink from '../ui/components/column_link';
+import ColumnSubheading from '../ui/components/column_subheading';
+
+import TrendsContainer from './containers/trends_container';
+
+const messages = defineMessages({
+  heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
+  home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
+  notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
+  public_timeline: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' },
+  navigation_subheading: { id: 'column_subheading.navigation', defaultMessage: 'Navigation' },
+  settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings' },
+  community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
+  explore: { id: 'navigation_bar.explore', defaultMessage: 'Explore' },
+  direct: { id: 'navigation_bar.direct', defaultMessage: 'Private mentions' },
+  bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
+  preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
+  settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' },
+  follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
+  lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
+  keyboard_shortcuts: { id: 'navigation_bar.keyboard_shortcuts', defaultMessage: 'Keyboard shortcuts' },
+  lists_subheading: { id: 'column_subheading.lists', defaultMessage: 'Lists' },
+  misc: { id: 'navigation_bar.misc', defaultMessage: 'Misc' },
+  menu: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
+});
+
+const makeMapStateToProps = () => {
+  const getOrderedLists = createSelector([state => state.get('lists')], lists => {
+    if (!lists) {
+      return lists;
+    }
+
+    return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
+  });
+
+  const mapStateToProps = state => ({
+    lists: getOrderedLists(state),
+    myAccount: state.getIn(['accounts', me]),
+    columns: state.getIn(['settings', 'columns']),
+    unreadFollowRequests: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
+    unreadNotifications: state.getIn(['notifications', 'unread']),
+  });
+
+  return mapStateToProps;
+};
+
+const mapDispatchToProps = dispatch => ({
+  fetchFollowRequests: () => dispatch(fetchFollowRequests()),
+  fetchLists: () => dispatch(fetchLists()),
+  openSettings: () => dispatch(openModal({
+    modalType: 'SETTINGS',
+    modalProps: {},
+  })),
+});
+
+const badgeDisplay = (number, limit) => {
+  if (number === 0) {
+    return undefined;
+  } else if (limit && number >= limit) {
+    return `${limit}+`;
+  } else {
+    return number;
+  }
+};
+
+class GettingStarted extends ImmutablePureComponent {
+
+  static contextTypes = {
+    identity: PropTypes.object,
+  };
+
+  static propTypes = {
+    intl: PropTypes.object.isRequired,
+    myAccount: ImmutablePropTypes.map,
+    columns: ImmutablePropTypes.list,
+    multiColumn: PropTypes.bool,
+    fetchFollowRequests: PropTypes.func.isRequired,
+    unreadFollowRequests: PropTypes.number,
+    unreadNotifications: PropTypes.number,
+    lists: ImmutablePropTypes.list,
+    fetchLists: PropTypes.func.isRequired,
+    openSettings: PropTypes.func.isRequired,
+  };
+
+  UNSAFE_componentWillMount () {
+    this.props.fetchLists();
+  }
+
+  componentDidMount () {
+    const { fetchFollowRequests } = this.props;
+    const { signedIn } = this.context.identity;
+
+    if (!signedIn) {
+      return;
+    }
+
+    fetchFollowRequests();
+  }
+
+  render () {
+    const { intl, myAccount, columns, multiColumn, unreadFollowRequests, unreadNotifications, lists, openSettings } = this.props;
+    const { signedIn } = this.context.identity;
+
+    const navItems = [];
+    let listItems = [];
+
+    if (multiColumn) {
+      if (signedIn && !columns.find(item => item.get('id') === 'HOME')) {
+        navItems.push(<ColumnLink key='home' icon='home' text={intl.formatMessage(messages.home_timeline)} to='/home' />);
+      }
+
+      if (!columns.find(item => item.get('id') === 'NOTIFICATIONS')) {
+        navItems.push(<ColumnLink key='notifications' icon='bell' text={intl.formatMessage(messages.notifications)} badge={badgeDisplay(unreadNotifications)} to='/notifications' />);
+      }
+
+      if (!columns.find(item => item.get('id') === 'COMMUNITY')) {
+        navItems.push(<ColumnLink key='community_timeline' icon='users' text={intl.formatMessage(messages.community_timeline)} to='/public/local' />);
+      }
+
+      if (!columns.find(item => item.get('id') === 'PUBLIC')) {
+        navItems.push(<ColumnLink key='public_timeline' icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/public' />);
+      }
+    }
+
+    if (showTrends) {
+      navItems.push(<ColumnLink key='explore' icon='hashtag' text={intl.formatMessage(messages.explore)} to='/explore' />);
+    }
+
+    if (signedIn) {
+      if (!multiColumn || !columns.find(item => item.get('id') === 'DIRECT')) {
+        navItems.push(<ColumnLink key='conversations' icon='envelope' text={intl.formatMessage(messages.direct)} to='/conversations' />);
+      }
+
+      if (!multiColumn || !columns.find(item => item.get('id') === 'BOOKMARKS')) {
+        navItems.push(<ColumnLink key='bookmarks' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} to='/bookmarks' />);
+      }
+
+      if (myAccount.get('locked') || unreadFollowRequests > 0) {
+        navItems.push(<ColumnLink key='follow_requests' icon='user-plus' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);
+      }
+
+      navItems.push(<ColumnLink key='getting_started' icon='ellipsis-h' text={intl.formatMessage(messages.misc)} to='/getting-started-misc' />);
+
+      listItems = listItems.concat([
+        <div key='9'>
+          <ColumnLink key='lists' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />
+          {lists.filter(list => !columns.find(item => item.get('id') === 'LIST' && item.getIn(['params', 'id']) === list.get('id'))).map(list =>
+            <ColumnLink key={`list-${list.get('id')}`} to={`/lists/${list.get('id')}`} icon='list-ul' text={list.get('title')} />,
+          )}
+        </div>,
+      ]);
+    }
+
+    return (
+      <Column bindToDocument={!multiColumn} icon='asterisk' heading={intl.formatMessage(messages.heading)} label={intl.formatMessage(messages.menu)} hideHeadingOnMobile>
+        <div className='scrollable optionally-scrollable'>
+          <div className='getting-started__wrapper'>
+            {!multiColumn && signedIn && <NavigationBar account={myAccount} />}
+            {multiColumn && <ColumnSubheading text={intl.formatMessage(messages.navigation_subheading)} />}
+            {navItems}
+            {signedIn && (
+              <>
+                <ColumnSubheading text={intl.formatMessage(messages.lists_subheading)} />
+                {listItems}
+                <ColumnSubheading text={intl.formatMessage(messages.settings_subheading)} />
+                { preferencesLink !== undefined && <ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href={preferencesLink} /> }
+                <ColumnLink icon='cogs' text={intl.formatMessage(messages.settings)} onClick={openSettings} />
+              </>
+            )}
+          </div>
+
+          <LinkFooter multiColumn />
+        </div>
+
+        {(multiColumn && showTrends) && <TrendsContainer />}
+
+        <Helmet>
+          <title>{intl.formatMessage(messages.menu)}</title>
+          <meta name='robots' content='noindex' />
+        </Helmet>
+      </Column>
+    );
+  }
+
+}
+
+export default connect(makeMapStateToProps, mapDispatchToProps)(injectIntl(GettingStarted));
diff --git a/app/javascript/flavours/blobfox/features/getting_started_misc/index.jsx b/app/javascript/flavours/blobfox/features/getting_started_misc/index.jsx
new file mode 100644
index 00000000000000..b1d94e608b597b
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/getting_started_misc/index.jsx
@@ -0,0 +1,68 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl } from 'react-intl';
+
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
+import { openModal } from 'flavours/blobfox/actions/modal';
+import ColumnBackButtonSlim from 'flavours/blobfox/components/column_back_button_slim';
+import Column from 'flavours/blobfox/features/ui/components/column';
+import ColumnLink from 'flavours/blobfox/features/ui/components/column_link';
+import ColumnSubheading from 'flavours/blobfox/features/ui/components/column_subheading';
+
+
+const messages = defineMessages({
+  heading: { id: 'column.heading', defaultMessage: 'Misc' },
+  subheading: { id: 'column.subheading', defaultMessage: 'Miscellaneous options' },
+  favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' },
+  blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
+  domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Blocked domains' },
+  mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
+  pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' },
+  keyboard_shortcuts: { id: 'navigation_bar.keyboard_shortcuts', defaultMessage: 'Keyboard shortcuts' },
+  featured_users: { id: 'navigation_bar.featured_users', defaultMessage: 'Featured users' },
+});
+
+class GettingStartedMisc extends ImmutablePureComponent {
+
+  static contextTypes = {
+    identity: PropTypes.object,
+  };
+
+  static propTypes = {
+    intl: PropTypes.object.isRequired,
+    dispatch: PropTypes.func.isRequired,
+  };
+
+  openFeaturedAccountsModal = () => {
+    this.props.dispatch(openModal({
+      modalType: 'PINNED_ACCOUNTS_EDITOR',
+    }));
+  };
+
+  render () {
+    const { intl } = this.props;
+    const { signedIn } = this.context.identity;
+
+    return (
+      <Column icon='ellipsis-h' heading={intl.formatMessage(messages.heading)}>
+        <ColumnBackButtonSlim />
+
+        <div className='scrollable'>
+          <ColumnSubheading text={intl.formatMessage(messages.subheading)} />
+          {signedIn && (<ColumnLink key='favourites' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />)}
+          {signedIn && (<ColumnLink key='pinned' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />)}
+          {signedIn && (<ColumnLink key='featured_users' icon='users' text={intl.formatMessage(messages.featured_users)} onClick={this.openFeaturedAccountsModal} />)}
+          {signedIn && (<ColumnLink key='mutes' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />)}
+          {signedIn && (<ColumnLink key='blocks' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />)}
+          {signedIn && (<ColumnLink key='domain_blocks' icon='minus-circle' text={intl.formatMessage(messages.domain_blocks)} to='/domain_blocks' />)}
+          <ColumnLink key='shortcuts' icon='question' text={intl.formatMessage(messages.keyboard_shortcuts)} to='/keyboard-shortcuts' />
+        </div>
+      </Column>
+    );
+  }
+
+}
+
+export default connect()(injectIntl(GettingStartedMisc));
diff --git a/app/javascript/flavours/blobfox/features/hashtag_timeline/components/column_settings.jsx b/app/javascript/flavours/blobfox/features/hashtag_timeline/components/column_settings.jsx
new file mode 100644
index 00000000000000..c60de4c5189469
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/hashtag_timeline/components/column_settings.jsx
@@ -0,0 +1,138 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
+import { NonceProvider } from 'react-select';
+import AsyncSelect from 'react-select/async';
+import Toggle from 'react-toggle';
+
+import SettingToggle from '../../notifications/components/setting_toggle';
+
+const messages = defineMessages({
+  placeholder: { id: 'hashtag.column_settings.select.placeholder', defaultMessage: 'Enter hashtags…' },
+  noOptions: { id: 'hashtag.column_settings.select.no_options_message', defaultMessage: 'No suggestions found' },
+});
+
+class ColumnSettings extends PureComponent {
+
+  static propTypes = {
+    settings: ImmutablePropTypes.map.isRequired,
+    onChange: PropTypes.func.isRequired,
+    onLoad: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  state = {
+    open: this.hasTags(),
+  };
+
+  hasTags () {
+    return ['all', 'any', 'none'].map(mode => this.tags(mode).length > 0).includes(true);
+  }
+
+  tags (mode) {
+    let tags = this.props.settings.getIn(['tags', mode]) || [];
+
+    if (tags.toJS) {
+      return tags.toJS();
+    } else {
+      return tags;
+    }
+  }
+
+  onSelect = mode => value => {
+    const oldValue = this.tags(mode);
+
+    // Prevent changes that add more than 4 tags, but allow removing
+    // tags that were already added before
+    if ((value.length > 4) && !(value < oldValue)) {
+      return;
+    }
+
+    this.props.onChange(['tags', mode], value);
+  };
+
+  onToggle = () => {
+    if (this.state.open && this.hasTags()) {
+      this.props.onChange('tags', {});
+    }
+
+    this.setState({ open: !this.state.open });
+  };
+
+  noOptionsMessage = () => this.props.intl.formatMessage(messages.noOptions);
+
+  modeSelect (mode) {
+    return (
+      <div className='column-settings__row'>
+        <span className='column-settings__section'>
+          {this.modeLabel(mode)}
+        </span>
+
+        <NonceProvider nonce={document.querySelector('meta[name=style-nonce]').content} cacheKey='tags'>
+          <AsyncSelect
+            isMulti
+            autoFocus
+            value={this.tags(mode)}
+            onChange={this.onSelect(mode)}
+            loadOptions={this.props.onLoad}
+            className='column-select__container'
+            classNamePrefix='column-select'
+            name='tags'
+            placeholder={this.props.intl.formatMessage(messages.placeholder)}
+            noOptionsMessage={this.noOptionsMessage}
+          />
+        </NonceProvider>
+      </div>
+    );
+  }
+
+  modeLabel (mode) {
+    switch(mode) {
+    case 'any':
+      return <FormattedMessage id='hashtag.column_settings.tag_mode.any' defaultMessage='Any of these' />;
+    case 'all':
+      return <FormattedMessage id='hashtag.column_settings.tag_mode.all' defaultMessage='All of these' />;
+    case 'none':
+      return <FormattedMessage id='hashtag.column_settings.tag_mode.none' defaultMessage='None of these' />;
+    default:
+      return '';
+    }
+  }
+
+  render () {
+    const { settings, onChange } = this.props;
+
+    return (
+      <div>
+        <div className='column-settings__row'>
+          <div className='setting-toggle'>
+            <Toggle id='hashtag.column_settings.tag_toggle' onChange={this.onToggle} checked={this.state.open} />
+
+            <span className='setting-toggle__label'>
+              <FormattedMessage id='hashtag.column_settings.tag_toggle' defaultMessage='Include additional tags in this column' />
+            </span>
+          </div>
+        </div>
+
+        {this.state.open && (
+          <div className='column-settings__hashtags'>
+            {this.modeSelect('any')}
+            {this.modeSelect('all')}
+            {this.modeSelect('none')}
+          </div>
+        )}
+
+        <div className='column-settings__row'>
+          <SettingToggle settings={settings} settingPath={['local']} onChange={onChange} label={<FormattedMessage id='community.column_settings.local_only' defaultMessage='Local only' />} />
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(ColumnSettings);
diff --git a/app/javascript/flavours/blobfox/features/hashtag_timeline/components/hashtag_header.jsx b/app/javascript/flavours/blobfox/features/hashtag_timeline/components/hashtag_header.jsx
new file mode 100644
index 00000000000000..cf1eb76b8a7dbe
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/hashtag_timeline/components/hashtag_header.jsx
@@ -0,0 +1,79 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
+import { Button } from 'flavours/blobfox/components/button';
+import { ShortNumber } from 'flavours/blobfox/components/short_number';
+
+const messages = defineMessages({
+  followHashtag: { id: 'hashtag.follow', defaultMessage: 'Follow hashtag' },
+  unfollowHashtag: { id: 'hashtag.unfollow', defaultMessage: 'Unfollow hashtag' },
+});
+
+const usesRenderer = (displayNumber, pluralReady) => (
+  <FormattedMessage
+    id='hashtag.counter_by_uses'
+    defaultMessage='{count, plural, one {{counter} post} other {{counter} posts}}'
+    values={{
+      count: pluralReady,
+      counter: <strong>{displayNumber}</strong>,
+    }}
+  />
+);
+
+const peopleRenderer = (displayNumber, pluralReady) => (
+  <FormattedMessage
+    id='hashtag.counter_by_accounts'
+    defaultMessage='{count, plural, one {{counter} participant} other {{counter} participants}}'
+    values={{
+      count: pluralReady,
+      counter: <strong>{displayNumber}</strong>,
+    }}
+  />
+);
+
+const usesTodayRenderer = (displayNumber, pluralReady) => (
+  <FormattedMessage
+    id='hashtag.counter_by_uses_today'
+    defaultMessage='{count, plural, one {{counter} post} other {{counter} posts}} today'
+    values={{
+      count: pluralReady,
+      counter: <strong>{displayNumber}</strong>,
+    }}
+  />
+);
+
+export const HashtagHeader = injectIntl(({ tag, intl, disabled, onClick }) => {
+  if (!tag) {
+    return null;
+  }
+
+  const [uses, people] = tag.get('history').reduce((arr, day) => [arr[0] + day.get('uses') * 1, arr[1] + day.get('accounts') * 1], [0, 0]);
+  const dividingCircle = <span aria-hidden>{' · '}</span>;
+
+  return (
+    <div className='hashtag-header'>
+      <div className='hashtag-header__header'>
+        <h1>#{tag.get('name')}</h1>
+        <Button onClick={onClick} text={intl.formatMessage(tag.get('following') ? messages.unfollowHashtag : messages.followHashtag)} disabled={disabled} />
+      </div>
+
+      <div>
+        <ShortNumber value={uses} renderer={usesRenderer} />
+        {dividingCircle}
+        <ShortNumber value={people} renderer={peopleRenderer} />
+        {dividingCircle}
+        <ShortNumber value={tag.getIn(['history', 0, 'uses']) * 1} renderer={usesTodayRenderer} />
+      </div>
+    </div>
+  );
+});
+
+HashtagHeader.propTypes = {
+  tag: ImmutablePropTypes.map,
+  disabled: PropTypes.bool,
+  onClick: PropTypes.func,
+  intl: PropTypes.object,
+};
\ No newline at end of file
diff --git a/app/javascript/flavours/blobfox/features/hashtag_timeline/containers/column_settings_container.js b/app/javascript/flavours/blobfox/features/hashtag_timeline/containers/column_settings_container.js
new file mode 100644
index 00000000000000..be95004cc7bfaf
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/hashtag_timeline/containers/column_settings_container.js
@@ -0,0 +1,33 @@
+import { connect } from 'react-redux';
+
+import { changeColumnParams } from '../../../actions/columns';
+import api from '../../../api';
+import ColumnSettings from '../components/column_settings';
+
+const mapStateToProps = (state, { columnId }) => {
+  const columns = state.getIn(['settings', 'columns']);
+  const index   = columns.findIndex(c => c.get('uuid') === columnId);
+
+  if (!(columnId && index >= 0)) {
+    return {};
+  }
+
+  return {
+    settings: columns.get(index).get('params'),
+    onLoad (value) {
+      return api(() => state).get('/api/v2/search', { params: { q: value, type: 'hashtags' } }).then(response => {
+        return (response.data.hashtags || []).map((tag) => {
+          return { value: tag.name, label: `#${tag.name}` };
+        });
+      });
+    },
+  };
+};
+
+const mapDispatchToProps = (dispatch, { columnId }) => ({
+  onChange (key, value) {
+    dispatch(changeColumnParams(columnId, key, value));
+  },
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);
diff --git a/app/javascript/flavours/blobfox/features/hashtag_timeline/index.jsx b/app/javascript/flavours/blobfox/features/hashtag_timeline/index.jsx
new file mode 100644
index 00000000000000..bcc2e99aee450f
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/hashtag_timeline/index.jsx
@@ -0,0 +1,226 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
+
+import { isEqual } from 'lodash';
+
+import { addColumn, removeColumn, moveColumn } from 'flavours/blobfox/actions/columns';
+import { connectHashtagStream } from 'flavours/blobfox/actions/streaming';
+import { fetchHashtag, followHashtag, unfollowHashtag } from 'flavours/blobfox/actions/tags';
+import { expandHashtagTimeline, clearTimeline } from 'flavours/blobfox/actions/timelines';
+import Column from 'flavours/blobfox/components/column';
+import ColumnHeader from 'flavours/blobfox/components/column_header';
+
+import StatusListContainer from '../ui/containers/status_list_container';
+
+import { HashtagHeader } from './components/hashtag_header';
+import ColumnSettingsContainer from './containers/column_settings_container';
+
+const mapStateToProps = (state, props) => ({
+  hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}${props.params.local ? ':local' : ''}`, 'unread']) > 0,
+  tag: state.getIn(['tags', props.params.id]),
+});
+
+class HashtagTimeline extends PureComponent {
+
+  disconnects = [];
+
+  static contextTypes = {
+    identity: PropTypes.object,
+  };
+
+  static propTypes = {
+    params: PropTypes.object.isRequired,
+    columnId: PropTypes.string,
+    dispatch: PropTypes.func.isRequired,
+    hasUnread: PropTypes.bool,
+    tag: ImmutablePropTypes.map,
+    multiColumn: PropTypes.bool,
+  };
+
+  handlePin = () => {
+    const { columnId, dispatch } = this.props;
+
+    if (columnId) {
+      dispatch(removeColumn(columnId));
+    } else {
+      dispatch(addColumn('HASHTAG', { id: this.props.params.id }));
+    }
+  };
+
+  title = () => {
+    const { id } = this.props.params;
+    const title  = [id];
+
+    if (this.additionalFor('any')) {
+      title.push(' ', <FormattedMessage key='any' id='hashtag.column_header.tag_mode.any'  values={{ additional: this.additionalFor('any') }} defaultMessage='or {additional}' />);
+    }
+
+    if (this.additionalFor('all')) {
+      title.push(' ', <FormattedMessage key='all' id='hashtag.column_header.tag_mode.all'  values={{ additional: this.additionalFor('all') }} defaultMessage='and {additional}' />);
+    }
+
+    if (this.additionalFor('none')) {
+      title.push(' ', <FormattedMessage key='none' id='hashtag.column_header.tag_mode.none' values={{ additional: this.additionalFor('none') }} defaultMessage='without {additional}' />);
+    }
+
+    return title;
+  };
+
+  additionalFor = (mode) => {
+    const { tags } = this.props.params;
+
+    if (tags && (tags[mode] || []).length > 0) {
+      return tags[mode].map(tag => tag.value).join('/');
+    } else {
+      return '';
+    }
+  };
+
+  handleMove = (dir) => {
+    const { columnId, dispatch } = this.props;
+    dispatch(moveColumn(columnId, dir));
+  };
+
+  handleHeaderClick = () => {
+    this.column.scrollTop();
+  };
+
+  _subscribe (dispatch, id, tags = {}, local) {
+    const { signedIn } = this.context.identity;
+
+    if (!signedIn) {
+      return;
+    }
+
+    let any  = (tags.any || []).map(tag => tag.value);
+    let all  = (tags.all || []).map(tag => tag.value);
+    let none = (tags.none || []).map(tag => tag.value);
+
+    [id, ...any].map(tag => {
+      this.disconnects.push(dispatch(connectHashtagStream(id, tag, local, status => {
+        let tags = status.tags.map(tag => tag.name);
+
+        return all.filter(tag => tags.includes(tag)).length === all.length &&
+               none.filter(tag => tags.includes(tag)).length === 0;
+      })));
+    });
+  }
+
+  _unsubscribe () {
+    this.disconnects.map(disconnect => disconnect());
+    this.disconnects = [];
+  }
+
+  _unload () {
+    const { dispatch } = this.props;
+    const { id, local } = this.props.params;
+
+    this._unsubscribe();
+    dispatch(clearTimeline(`hashtag:${id}${local ? ':local' : ''}`));
+  }
+
+  _load() {
+    const { dispatch } = this.props;
+    const { id, tags, local } = this.props.params;
+
+    this._subscribe(dispatch, id, tags, local);
+    dispatch(expandHashtagTimeline(id, { tags, local }));
+    dispatch(fetchHashtag(id));
+  }
+
+  componentDidMount () {
+    this._load();
+  }
+
+  componentDidUpdate (prevProps) {
+    const { params } = this.props;
+    const { id, tags, local } = prevProps.params;
+
+    if (id !== params.id || !isEqual(tags, params.tags) || !isEqual(local, params.local)) {
+      this._unload();
+      this._load();
+    }
+  }
+
+  componentWillUnmount () {
+    this._unsubscribe();
+  }
+
+  setRef = c => {
+    this.column = c;
+  };
+
+  handleLoadMore = maxId => {
+    const { dispatch, params } = this.props;
+    const { id, tags, local }  = params;
+
+    dispatch(expandHashtagTimeline(id, { maxId, tags, local }));
+  };
+
+  handleFollow = () => {
+    const { dispatch, params, tag } = this.props;
+    const { id } = params;
+    const { signedIn } = this.context.identity;
+
+    if (!signedIn) {
+      return;
+    }
+
+    if (tag.get('following')) {
+      dispatch(unfollowHashtag(id));
+    } else {
+      dispatch(followHashtag(id));
+    }
+  };
+
+  render () {
+    const { hasUnread, columnId, multiColumn, tag } = this.props;
+    const { id, local } = this.props.params;
+    const pinned = !!columnId;
+    const { signedIn } = this.context.identity;
+
+    return (
+      <Column bindToDocument={!multiColumn} ref={this.setRef} label={`#${id}`}>
+        <ColumnHeader
+          icon='hashtag'
+          active={hasUnread}
+          title={this.title()}
+          onPin={this.handlePin}
+          onMove={this.handleMove}
+          onClick={this.handleHeaderClick}
+          pinned={pinned}
+          multiColumn={multiColumn}
+          showBackButton
+        >
+          {columnId && <ColumnSettingsContainer columnId={columnId} />}
+        </ColumnHeader>
+
+        <StatusListContainer
+          prepend={pinned ? null : <HashtagHeader tag={tag} disabled={!signedIn} onClick={this.handleFollow} />}
+          alwaysPrepend
+          trackScroll={!pinned}
+          scrollKey={`hashtag_timeline-${columnId}`}
+          timelineId={`hashtag:${id}${local ? ':local' : ''}`}
+          onLoadMore={this.handleLoadMore}
+          emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />}
+          bindToDocument={!multiColumn}
+        />
+
+        <Helmet>
+          <title>#{id}</title>
+          <meta name='robots' content='noindex' />
+        </Helmet>
+      </Column>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(HashtagTimeline);
diff --git a/app/javascript/flavours/blobfox/features/home_timeline/components/column_settings.tsx b/app/javascript/flavours/blobfox/features/home_timeline/components/column_settings.tsx
new file mode 100644
index 00000000000000..70fc35cbb18d1a
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/home_timeline/components/column_settings.tsx
@@ -0,0 +1,109 @@
+/* eslint-disable @typescript-eslint/no-unsafe-call,
+                  @typescript-eslint/no-unsafe-return,
+                  @typescript-eslint/no-unsafe-assignment,
+                  @typescript-eslint/no-unsafe-member-access
+                  -- the settings store is not yet typed */
+import { useCallback } from 'react';
+
+import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
+
+import SettingText from 'flavours/blobfox/components/setting_text';
+import { useAppSelector, useAppDispatch } from 'flavours/blobfox/store';
+
+import { changeSetting } from '../../../actions/settings';
+import SettingToggle from '../../notifications/components/setting_toggle';
+
+const messages = defineMessages({
+  filter_regex: {
+    id: 'home.column_settings.filter_regex',
+    defaultMessage: 'Filter out by regular expressions',
+  },
+  settings: { id: 'home.settings', defaultMessage: 'Column settings' },
+});
+
+export const ColumnSettings: React.FC = () => {
+  const settings = useAppSelector((state) => state.settings.get('home'));
+
+  const intl = useIntl();
+
+  const dispatch = useAppDispatch();
+  const onChange = useCallback(
+    (key: string, checked: boolean) => {
+      dispatch(changeSetting(['home', ...key], checked));
+    },
+    [dispatch],
+  );
+
+  return (
+    <div>
+      <span className='column-settings__section'>
+        <FormattedMessage
+          id='home.column_settings.basic'
+          defaultMessage='Basic'
+        />
+      </span>
+
+      <div className='column-settings__row'>
+        <SettingToggle
+          prefix='home_timeline'
+          settings={settings}
+          settingPath={['shows', 'reblog']}
+          onChange={onChange}
+          label={
+            <FormattedMessage
+              id='home.column_settings.show_reblogs'
+              defaultMessage='Show boosts'
+            />
+          }
+        />
+      </div>
+
+      <div className='column-settings__row'>
+        <SettingToggle
+          prefix='home_timeline'
+          settings={settings}
+          settingPath={['shows', 'reply']}
+          onChange={onChange}
+          label={
+            <FormattedMessage
+              id='home.column_settings.show_replies'
+              defaultMessage='Show replies'
+            />
+          }
+        />
+      </div>
+
+      <div className='column-settings__row'>
+        <SettingToggle
+          prefix='home_timeline'
+          settings={settings}
+          settingPath={['shows', 'direct']}
+          onChange={onChange}
+          label={
+            <FormattedMessage
+              id='home.column_settings.show_direct'
+              defaultMessage='Show private mentions'
+            />
+          }
+        />
+      </div>
+
+      <span className='column-settings__section'>
+        <FormattedMessage
+          id='home.column_settings.advanced'
+          defaultMessage='Advanced'
+        />
+      </span>
+
+      <div className='column-settings__row'>
+        <SettingText
+          prefix='home_timeline'
+          settings={settings}
+          settingPath={['regex', 'body']}
+          onChange={onChange}
+          label={intl.formatMessage(messages.filter_regex)}
+        />
+      </div>
+    </div>
+  );
+};
diff --git a/app/javascript/flavours/blobfox/features/home_timeline/components/critical_update_banner.tsx b/app/javascript/flavours/blobfox/features/home_timeline/components/critical_update_banner.tsx
new file mode 100644
index 00000000000000..d0dd2b6acda66f
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/home_timeline/components/critical_update_banner.tsx
@@ -0,0 +1,26 @@
+import { FormattedMessage } from 'react-intl';
+
+export const CriticalUpdateBanner = () => (
+  <div className='warning-banner'>
+    <div className='warning-banner__message'>
+      <h1>
+        <FormattedMessage
+          id='home.pending_critical_update.title'
+          defaultMessage='Critical security update available!'
+        />
+      </h1>
+      <p>
+        <FormattedMessage
+          id='home.pending_critical_update.body'
+          defaultMessage='Please update your Mastodon server as soon as possible!'
+        />{' '}
+        <a href='/admin/software_updates'>
+          <FormattedMessage
+            id='home.pending_critical_update.link'
+            defaultMessage='See updates'
+          />
+        </a>
+      </p>
+    </div>
+  </div>
+);
diff --git a/app/javascript/flavours/blobfox/features/home_timeline/components/explore_prompt.tsx b/app/javascript/flavours/blobfox/features/home_timeline/components/explore_prompt.tsx
new file mode 100644
index 00000000000000..8d39d6372d793f
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/home_timeline/components/explore_prompt.tsx
@@ -0,0 +1,46 @@
+import { FormattedMessage } from 'react-intl';
+
+import { Link } from 'react-router-dom';
+
+import { DismissableBanner } from 'flavours/blobfox/components/dismissable_banner';
+import background from 'mastodon/../images/friends-cropped.png';
+
+export const ExplorePrompt = () => (
+  <DismissableBanner id='home.explore_prompt'>
+    <img
+      src={background}
+      alt=''
+      className='dismissable-banner__background-image'
+    />
+
+    <h1>
+      <FormattedMessage
+        id='home.explore_prompt.title'
+        defaultMessage='This is your home base within Mastodon.'
+      />
+    </h1>
+    <p>
+      <FormattedMessage
+        id='home.explore_prompt.body'
+        defaultMessage="Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. If that feels too quiet, you may want to:"
+      />
+    </p>
+
+    <div className='dismissable-banner__message__wrapper'>
+      <div className='dismissable-banner__message__actions'>
+        <Link to='/explore' className='button'>
+          <FormattedMessage
+            id='home.actions.go_to_explore'
+            defaultMessage="See what's trending"
+          />
+        </Link>
+        <Link to='/explore/suggestions' className='button button-tertiary'>
+          <FormattedMessage
+            id='home.actions.go_to_suggestions'
+            defaultMessage='Find people to follow'
+          />
+        </Link>
+      </div>
+    </div>
+  </DismissableBanner>
+);
diff --git a/app/javascript/flavours/blobfox/features/home_timeline/index.jsx b/app/javascript/flavours/blobfox/features/home_timeline/index.jsx
new file mode 100644
index 00000000000000..0164ed006a050b
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/home_timeline/index.jsx
@@ -0,0 +1,239 @@
+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 { List as ImmutableList } from 'immutable';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+
+import { fetchAnnouncements, toggleShowAnnouncements } from 'flavours/blobfox/actions/announcements';
+import { IconWithBadge } from 'flavours/blobfox/components/icon_with_badge';
+import { NotSignedInIndicator } from 'flavours/blobfox/components/not_signed_in_indicator';
+import AnnouncementsContainer from 'flavours/blobfox/features/getting_started/containers/announcements_container';
+import { me, criticalUpdatesPending } from 'flavours/blobfox/initial_state';
+
+import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
+import { expandHomeTimeline } from '../../actions/timelines';
+import Column from '../../components/column';
+import ColumnHeader from '../../components/column_header';
+import StatusListContainer from '../ui/containers/status_list_container';
+
+import { ColumnSettings } from './components/column_settings';
+import { CriticalUpdateBanner } from './components/critical_update_banner';
+import { ExplorePrompt } from './components/explore_prompt';
+
+const messages = defineMessages({
+  title: { id: 'column.home', defaultMessage: 'Home' },
+  show_announcements: { id: 'home.show_announcements', defaultMessage: 'Show announcements' },
+  hide_announcements: { id: 'home.hide_announcements', defaultMessage: 'Hide announcements' },
+});
+
+const getHomeFeedSpeed = createSelector([
+  state => state.getIn(['timelines', 'home', 'items'], ImmutableList()),
+  state => state.getIn(['timelines', 'home', 'pendingItems'], ImmutableList()),
+  state => state.get('statuses'),
+], (statusIds, pendingStatusIds, statusMap) => {
+  const recentStatusIds = pendingStatusIds.concat(statusIds);
+  const statuses = recentStatusIds.filter(id => id !== null).map(id => statusMap.get(id)).filter(status => status?.get('account') !== me).take(20);
+
+  if (statuses.isEmpty()) {
+    return {
+      gap: 0,
+      newest: new Date(0),
+    };
+  }
+
+  const datetimes = statuses.map(status => status.get('created_at', 0));
+  const oldest = new Date(datetimes.min());
+  const newest = new Date(datetimes.max());
+  const averageGap = (newest - oldest) / (1000 * (statuses.size + 1)); // Average gap between posts on first page in seconds
+
+  return {
+    gap: averageGap,
+    newest,
+  };
+});
+
+const homeTooSlow = createSelector([
+  state => state.getIn(['timelines', 'home', 'isLoading']),
+  state => state.getIn(['timelines', 'home', 'isPartial']),
+  getHomeFeedSpeed,
+], (isLoading, isPartial, speed) =>
+  !isLoading && !isPartial // Only if the home feed has finished loading
+  && (
+    (speed.gap > (30 * 60) // If the average gap between posts is more than 30 minutes
+    || (Date.now() - speed.newest) > (1000 * 3600)) // If the most recent post is from over an hour ago
+  )
+);
+
+const mapStateToProps = state => ({
+  hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0,
+  isPartial: state.getIn(['timelines', 'home', 'isPartial']),
+  hasAnnouncements: !state.getIn(['announcements', 'items']).isEmpty(),
+  unreadAnnouncements: state.getIn(['announcements', 'items']).count(item => !item.get('read')),
+  showAnnouncements: state.getIn(['announcements', 'show']),
+  tooSlow: homeTooSlow(state),
+  regex: state.getIn(['settings', 'home', 'regex', 'body']),
+});
+
+class HomeTimeline extends PureComponent {
+
+  static contextTypes = {
+    identity: PropTypes.object,
+  };
+
+  static propTypes = {
+    dispatch: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+    hasUnread: PropTypes.bool,
+    isPartial: PropTypes.bool,
+    columnId: PropTypes.string,
+    multiColumn: PropTypes.bool,
+    hasAnnouncements: PropTypes.bool,
+    unreadAnnouncements: PropTypes.number,
+    showAnnouncements: PropTypes.bool,
+    tooSlow: PropTypes.bool,
+    regex: PropTypes.string,
+  };
+
+  handlePin = () => {
+    const { columnId, dispatch } = this.props;
+
+    if (columnId) {
+      dispatch(removeColumn(columnId));
+    } else {
+      dispatch(addColumn('HOME', {}));
+    }
+  };
+
+  handleMove = (dir) => {
+    const { columnId, dispatch } = this.props;
+    dispatch(moveColumn(columnId, dir));
+  };
+
+  handleHeaderClick = () => {
+    this.column.scrollTop();
+  };
+
+  setRef = c => {
+    this.column = c;
+  };
+
+  handleLoadMore = maxId => {
+    this.props.dispatch(expandHomeTimeline({ maxId }));
+  };
+
+  componentDidMount () {
+    setTimeout(() => this.props.dispatch(fetchAnnouncements()), 700);
+    this._checkIfReloadNeeded(false, this.props.isPartial);
+  }
+
+  componentDidUpdate (prevProps) {
+    this._checkIfReloadNeeded(prevProps.isPartial, this.props.isPartial);
+  }
+
+  componentWillUnmount () {
+    this._stopPolling();
+  }
+
+  _checkIfReloadNeeded (wasPartial, isPartial) {
+    const { dispatch } = this.props;
+
+    if (wasPartial === isPartial) {
+      return;
+    } else if (!wasPartial && isPartial) {
+      this.polling = setInterval(() => {
+        dispatch(expandHomeTimeline());
+      }, 3000);
+    } else if (wasPartial && !isPartial) {
+      this._stopPolling();
+    }
+  }
+
+  _stopPolling () {
+    if (this.polling) {
+      clearInterval(this.polling);
+      this.polling = null;
+    }
+  }
+
+  handleToggleAnnouncementsClick = (e) => {
+    e.stopPropagation();
+    this.props.dispatch(toggleShowAnnouncements());
+  };
+
+  render () {
+    const { intl, hasUnread, columnId, multiColumn, tooSlow, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props;
+    const pinned = !!columnId;
+    const { signedIn } = this.context.identity;
+    const banners = [];
+
+    let announcementsButton;
+
+    if (hasAnnouncements) {
+      announcementsButton = (
+        <button
+          className={classNames('column-header__button', { 'active': showAnnouncements })}
+          title={intl.formatMessage(showAnnouncements ? messages.hide_announcements : messages.show_announcements)}
+          aria-label={intl.formatMessage(showAnnouncements ? messages.hide_announcements : messages.show_announcements)}
+          onClick={this.handleToggleAnnouncementsClick}
+        >
+          <IconWithBadge id='bullhorn' count={unreadAnnouncements} />
+        </button>
+      );
+    }
+
+    if (criticalUpdatesPending) {
+      banners.push(<CriticalUpdateBanner key='critical-update-banner' />);
+    }
+
+    if (tooSlow) {
+      banners.push(<ExplorePrompt key='explore-prompt' />);
+    }
+
+    return (
+      <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
+        <ColumnHeader
+          icon='home'
+          active={hasUnread}
+          title={intl.formatMessage(messages.title)}
+          onPin={this.handlePin}
+          onMove={this.handleMove}
+          onClick={this.handleHeaderClick}
+          pinned={pinned}
+          multiColumn={multiColumn}
+          extraButton={announcementsButton}
+          appendContent={hasAnnouncements && showAnnouncements && <AnnouncementsContainer />}
+        >
+          <ColumnSettings />
+        </ColumnHeader>
+
+        {signedIn ? (
+          <StatusListContainer
+            prepend={banners}
+            alwaysPrepend
+            trackScroll={!pinned}
+            scrollKey={`home_timeline-${columnId}`}
+            onLoadMore={this.handleLoadMore}
+            timelineId='home'
+            emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty! Follow more people to fill it up.' />}
+            bindToDocument={!multiColumn}
+            regex={this.props.regex}
+          />
+        ) : <NotSignedInIndicator />}
+
+        <Helmet>
+          <title>{intl.formatMessage(messages.title)}</title>
+          <meta name='robots' content='noindex' />
+        </Helmet>
+      </Column>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(injectIntl(HomeTimeline));
diff --git a/app/javascript/flavours/blobfox/features/interaction_modal/index.jsx b/app/javascript/flavours/blobfox/features/interaction_modal/index.jsx
new file mode 100644
index 00000000000000..f6ac45d07dbc42
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/interaction_modal/index.jsx
@@ -0,0 +1,417 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
+
+import classNames from 'classnames';
+
+import { connect } from 'react-redux';
+
+import { throttle, escapeRegExp } from 'lodash';
+
+import { openModal, closeModal } from 'flavours/blobfox/actions/modal';
+import api from 'flavours/blobfox/api';
+import { Button } from 'flavours/blobfox/components/button';
+import { Icon }  from 'flavours/blobfox/components/icon';
+import { registrationsOpen, sso_redirect } from 'flavours/blobfox/initial_state';
+
+const messages = defineMessages({
+  loginPrompt: { id: 'interaction_modal.login.prompt', defaultMessage: 'Domain of your home server, e.g. mastodon.social' },
+});
+
+const mapStateToProps = (state, { accountId }) => ({
+  displayNameHtml: state.getIn(['accounts', accountId, 'display_name_html']),
+  signupUrl: state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up',
+});
+
+const mapDispatchToProps = (dispatch) => ({
+  onSignupClick() {
+    dispatch(closeModal({
+      modalType: undefined,
+      ignoreFocus: false,
+    }));
+    dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS' }));
+  },
+});
+
+const PERSISTENCE_KEY = 'mastodon_home';
+
+const isValidDomain = value => {
+  const url = new URL('https:///path');
+  url.hostname = value;
+  return url.hostname === value;
+};
+
+const valueToDomain = value => {
+  // If the user starts typing an URL
+  if (/^https?:\/\//.test(value)) {
+    try {
+      const url = new URL(value);
+
+      // Consider that if there is a path, the URL is more meaningful than a bare domain
+      if (url.pathname.length > 1) {
+        return '';
+      }
+
+      return url.host;
+    } catch {
+      return undefined;
+    }
+  // If the user writes their full handle including username
+  } else if (value.includes('@')) {
+    if (value.replace(/^@/, '').split('@').length > 2) {
+      return undefined;
+    }
+    return '';
+  }
+
+  return value;
+};
+
+const addInputToOptions = (value, options) => {
+  value = value.trim();
+
+  if (value.includes('.') && isValidDomain(value)) {
+    return [value].concat(options.filter((x) => x !== value));
+  }
+
+  return options;
+};
+
+class LoginForm extends React.PureComponent {
+
+  static propTypes = {
+    resourceUrl: PropTypes.string,
+    intl: PropTypes.object.isRequired,
+  };
+
+  state = {
+    value: localStorage ? (localStorage.getItem(PERSISTENCE_KEY) || '') : '',
+    expanded: false,
+    selectedOption: -1,
+    isLoading: false,
+    isSubmitting: false,
+    error: false,
+    options: [],
+    networkOptions: [],
+  };
+
+  setRef = c => {
+    this.input = c;
+  };
+
+  isValueValid = (value) => {
+    let likelyAcct = false;
+    let url = null;
+
+    if (value.startsWith('/')) {
+      return false;
+    }
+
+    if (value.startsWith('@')) {
+      value = value.slice(1);
+      likelyAcct = true;
+    }
+
+    // The user is in the middle of typing something, do not error out
+    if (value === '') {
+      return true;
+    }
+
+    if (/^https?:\/\//.test(value) && !likelyAcct) {
+      url = value;
+    } else {
+      url = `https://${value}`;
+    }
+
+    try {
+      new URL(url);
+      return true;
+    } catch(_) {
+      return false;
+    }
+  };
+
+  handleChange = ({ target }) => {
+    const error = !this.isValueValid(target.value);
+    this.setState(state => ({ error, value: target.value, isLoading: true, options: addInputToOptions(target.value, state.networkOptions) }), () => this._loadOptions());
+  };
+
+  handleMessage = (event) => {
+    const { resourceUrl } = this.props;
+
+    if (event.origin !== window.origin || event.source !== this.iframeRef.contentWindow) {
+      return;
+    }
+
+    if (event.data?.type === 'fetchInteractionURL-failure') {
+      this.setState({ isSubmitting: false, error: true });
+    } else if (event.data?.type === 'fetchInteractionURL-success') {
+      if (/^https?:\/\//.test(event.data.template)) {
+        try {
+          const url = new URL(event.data.template.replace('{uri}', encodeURIComponent(resourceUrl)));
+
+          if (localStorage) {
+            localStorage.setItem(PERSISTENCE_KEY, event.data.uri_or_domain);
+          }
+
+          window.location.href = url;
+        } catch (e) {
+          console.error(e);
+          this.setState({ isSubmitting: false, error: true });
+        }
+      } else {
+        this.setState({ isSubmitting: false, error: true });
+      }
+    }
+  };
+
+  componentDidMount () {
+    window.addEventListener('message', this.handleMessage);
+  }
+
+  componentWillUnmount () {
+    window.removeEventListener('message', this.handleMessage);
+  }
+
+  handleSubmit = () => {
+    const { value } = this.state;
+
+    this.setState({ isSubmitting: true });
+
+    this.iframeRef.contentWindow.postMessage({
+      type: 'fetchInteractionURL',
+      uri_or_domain: value.trim(),
+    }, window.origin);
+  };
+
+  setIFrameRef = (iframe) => {
+    this.iframeRef = iframe;
+  };
+
+  handleFocus = () => {
+    this.setState({ expanded: true });
+  };
+
+  handleBlur = () => {
+    this.setState({ expanded: false });
+  };
+
+  handleKeyDown = (e) => {
+    const { options, selectedOption } = this.state;
+
+    switch(e.key) {
+    case 'ArrowDown':
+      e.preventDefault();
+
+      if (options.length > 0) {
+        this.setState({ selectedOption: Math.min(selectedOption + 1, options.length - 1) });
+      }
+
+      break;
+    case 'ArrowUp':
+      e.preventDefault();
+
+      if (options.length > 0) {
+        this.setState({ selectedOption: Math.max(selectedOption - 1, -1) });
+      }
+
+      break;
+    case 'Enter':
+      e.preventDefault();
+
+      if (selectedOption === -1) {
+        this.handleSubmit();
+      } else if (options.length > 0) {
+        this.setState({ value: options[selectedOption], error: false }, () => this.handleSubmit());
+      }
+
+      break;
+    }
+  };
+
+  handleOptionClick = e => {
+    const index  = Number(e.currentTarget.getAttribute('data-index'));
+    const option = this.state.options[index];
+
+    e.preventDefault();
+    this.setState({ selectedOption: index, value: option, error: false }, () => this.handleSubmit());
+  };
+
+  _loadOptions = throttle(() => {
+    const { value } = this.state;
+
+    const domain = valueToDomain(value.trim());
+
+    if (typeof domain === 'undefined') {
+      this.setState({ options: [], networkOptions: [], isLoading: false, error: true });
+      return;
+    }
+
+    if (domain.length === 0) {
+      this.setState({ options: [], networkOptions: [], isLoading: false });
+      return;
+    }
+
+    api().get('/api/v1/peers/search', { params: { q: domain } }).then(({ data }) => {
+      if (!data) {
+        data = [];
+      }
+
+      this.setState((state) => ({ networkOptions: data, options: addInputToOptions(state.value, data), isLoading: false }));
+    }).catch(() => {
+      this.setState({ isLoading: false });
+    });
+  }, 200, { leading: true, trailing: true });
+
+  render () {
+    const { intl } = this.props;
+    const { value, expanded, options, selectedOption, error, isSubmitting } = this.state;
+    const domain = (valueToDomain(value) || '').trim();
+    const domainRegExp = new RegExp(`(${escapeRegExp(domain)})`, 'gi');
+    const hasPopOut = domain.length > 0 && options.length > 0;
+
+    return (
+      <div className={classNames('interaction-modal__login', { focused: expanded, expanded: hasPopOut, invalid: error })}>
+
+        <iframe
+          ref={this.setIFrameRef}
+          style={{display: 'none'}}
+          src='/remote_interaction_helper'
+          sandbox='allow-scripts allow-same-origin'
+          title='remote interaction helper'
+        />
+
+        <div className='interaction-modal__login__input'>
+          <input
+            ref={this.setRef}
+            type='text'
+            value={value}
+            placeholder={intl.formatMessage(messages.loginPrompt)}
+            aria-label={intl.formatMessage(messages.loginPrompt)}
+            autoFocus
+            onChange={this.handleChange}
+            onFocus={this.handleFocus}
+            onBlur={this.handleBlur}
+            onKeyDown={this.handleKeyDown}
+            autoComplete='off'
+            autoCapitalize='off'
+            spellCheck='false'
+          />
+
+          <Button onClick={this.handleSubmit} disabled={isSubmitting || error}><FormattedMessage id='interaction_modal.login.action' defaultMessage='Take me home' /></Button>
+        </div>
+
+        {hasPopOut && (
+          <div className='search__popout'>
+            <div className='search__popout__menu'>
+              {options.map((option, i) => (
+                <button key={option} onMouseDown={this.handleOptionClick} data-index={i} className={classNames('search__popout__menu__item', { selected: selectedOption === i })}>
+                  {option.split(domainRegExp).map((part, i) => (
+                    part.toLowerCase() === domain.toLowerCase() ? (
+                      <mark key={i}>
+                        {part}
+                      </mark>
+                    ) : (
+                      <span key={i}>
+                        {part}
+                      </span>
+                    )
+                  ))}
+                </button>
+              ))}
+            </div>
+          </div>
+        )}
+      </div>
+    );
+  }
+
+}
+
+const IntlLoginForm = injectIntl(LoginForm);
+
+class InteractionModal extends React.PureComponent {
+
+  static propTypes = {
+    displayNameHtml: PropTypes.string,
+    url: PropTypes.string,
+    type: PropTypes.oneOf(['reply', 'reblog', 'favourite', 'follow']),
+    onSignupClick: PropTypes.func.isRequired,
+    signupUrl: PropTypes.string.isRequired,
+  };
+
+  handleSignupClick = () => {
+    this.props.onSignupClick();
+  };
+
+  render () {
+    const { url, type, displayNameHtml, signupUrl } = this.props;
+
+    const name = <bdi dangerouslySetInnerHTML={{ __html: displayNameHtml }} />;
+
+    let title, actionDescription, icon;
+
+    switch(type) {
+    case 'reply':
+      icon = <Icon id='reply' />;
+      title = <FormattedMessage id='interaction_modal.title.reply' defaultMessage="Reply to {name}'s post" values={{ name }} />;
+      actionDescription = <FormattedMessage id='interaction_modal.description.reply' defaultMessage='With an account on Mastodon, you can respond to this post.' />;
+      break;
+    case 'reblog':
+      icon = <Icon id='retweet' />;
+      title = <FormattedMessage id='interaction_modal.title.reblog' defaultMessage="Boost {name}'s post" values={{ name }} />;
+      actionDescription = <FormattedMessage id='interaction_modal.description.reblog' defaultMessage='With an account on Mastodon, you can boost this post to share it with your own followers.' />;
+      break;
+    case 'favourite':
+      icon = <Icon id='star' />;
+      title = <FormattedMessage id='interaction_modal.title.favourite' defaultMessage="Favorite {name}'s post" values={{ name }} />;
+      actionDescription = <FormattedMessage id='interaction_modal.description.favourite' defaultMessage='With an account on Mastodon, you can favorite this post to let the author know you appreciate it and save it for later.' />;
+      break;
+    case 'follow':
+      icon = <Icon id='user-plus' />;
+      title = <FormattedMessage id='interaction_modal.title.follow' defaultMessage='Follow {name}' values={{ name }} />;
+      actionDescription = <FormattedMessage id='interaction_modal.description.follow' defaultMessage='With an account on Mastodon, you can follow {name} to receive their posts in your home feed.' values={{ name }} />;
+      break;
+    }
+
+    let signupButton;
+
+    if (sso_redirect) {
+      signupButton = (
+        <a href={sso_redirect} data-method='post' className='link-button'>
+          <FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
+        </a>
+      );
+    } else if (registrationsOpen) {
+      signupButton = (
+        <a href={signupUrl} className='link-button'>
+          <FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
+        </a>
+      );
+    } else {
+      signupButton = (
+        <button className='link-button' onClick={this.handleSignupClick}>
+          <FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
+        </button>
+      );
+    }
+
+    return (
+      <div className='modal-root__modal interaction-modal'>
+        <div className='interaction-modal__lead'>
+          <h3><span className='interaction-modal__icon'>{icon}</span> {title}</h3>
+          <p>{actionDescription} <strong><FormattedMessage id='interaction_modal.sign_in' defaultMessage='You are not logged in to this server. Where is your account hosted?' /></strong></p>
+        </div>
+
+        <IntlLoginForm resourceUrl={url} />
+
+        <p className='hint'><FormattedMessage id='interaction_modal.sign_in_hint' defaultMessage="Tip: That's the website where you signed up. If you don't remember, look for the welcome e-mail in your inbox. You can also enter your full username! (e.g. @Mastodon@mastodon.social)" /></p>
+        <p><FormattedMessage id='interaction_modal.no_account_yet' defaultMessage='Not on Mastodon?' /> {signupButton}</p>
+      </div>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(InteractionModal);
diff --git a/app/javascript/flavours/blobfox/features/keyboard_shortcuts/index.jsx b/app/javascript/flavours/blobfox/features/keyboard_shortcuts/index.jsx
new file mode 100644
index 00000000000000..220cc37ccc6b75
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/keyboard_shortcuts/index.jsx
@@ -0,0 +1,152 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
+import Column from 'flavours/blobfox/components/column';
+import ColumnHeader from 'flavours/blobfox/components/column_header';
+
+const messages = defineMessages({
+  heading: { id: 'keyboard_shortcuts.heading', defaultMessage: 'Keyboard Shortcuts' },
+});
+
+const mapStateToProps = state => ({
+  collapseEnabled: state.getIn(['local_settings', 'collapsed', 'enabled']),
+});
+
+class KeyboardShortcuts extends ImmutablePureComponent {
+
+  static propTypes = {
+    intl: PropTypes.object.isRequired,
+    multiColumn: PropTypes.bool,
+    collapseEnabled: PropTypes.bool,
+  };
+
+  render () {
+    const { intl, collapseEnabled, multiColumn } = this.props;
+
+    return (
+      <Column>
+        <ColumnHeader
+          title={intl.formatMessage(messages.heading)}
+          icon='question'
+          multiColumn={multiColumn}
+        />
+
+        <div className='keyboard-shortcuts scrollable optionally-scrollable'>
+          <table>
+            <thead>
+              <tr>
+                <th><FormattedMessage id='keyboard_shortcuts.hotkey' defaultMessage='Hotkey' /></th>
+                <th><FormattedMessage id='keyboard_shortcuts.description' defaultMessage='Description' /></th>
+              </tr>
+            </thead>
+            <tbody>
+              <tr>
+                <td><kbd>r</kbd></td>
+                <td><FormattedMessage id='keyboard_shortcuts.reply' defaultMessage='to reply' /></td>
+              </tr>
+              <tr>
+                <td><kbd>m</kbd></td>
+                <td><FormattedMessage id='keyboard_shortcuts.mention' defaultMessage='to mention author' /></td>
+              </tr>
+              <tr>
+                <td><kbd>p</kbd></td>
+                <td><FormattedMessage id='keyboard_shortcuts.profile' defaultMessage="to open author's profile" /></td>
+              </tr>
+              <tr>
+                <td><kbd>f</kbd></td>
+                <td><FormattedMessage id='keyboard_shortcuts.favourite' defaultMessage='to favorite' /></td>
+              </tr>
+              <tr>
+                <td><kbd>b</kbd></td>
+                <td><FormattedMessage id='keyboard_shortcuts.boost' defaultMessage='to boost' /></td>
+              </tr>
+              <tr>
+                <td><kbd>d</kbd></td>
+                <td><FormattedMessage id='keyboard_shortcuts.bookmark' defaultMessage='to bookmark' /></td>
+              </tr>
+              <tr>
+                <td><kbd>enter</kbd>, <kbd>o</kbd></td>
+                <td><FormattedMessage id='keyboard_shortcuts.enter' defaultMessage='to open status' /></td>
+              </tr>
+              <tr>
+                <td><kbd>e</kbd></td>
+                <td><FormattedMessage id='keyboard_shortcuts.open_media' defaultMessage='to open media' /></td>
+              </tr>
+              <tr>
+                <td><kbd>x</kbd></td>
+                <td><FormattedMessage id='keyboard_shortcuts.toggle_hidden' defaultMessage='to show/hide text behind CW' /></td>
+              </tr>
+              <tr>
+                <td><kbd>h</kbd></td>
+                <td><FormattedMessage id='keyboard_shortcuts.toggle_sensitivity' defaultMessage='to show/hide media' /></td>
+              </tr>
+              {collapseEnabled && (
+                <tr>
+                  <td><kbd>shift</kbd>+<kbd>x</kbd></td>
+                  <td><FormattedMessage id='keyboard_shortcuts.toggle_collapse' defaultMessage='to collapse/uncollapse toots' /></td>
+                </tr>
+              )}
+              <tr>
+                <td><kbd>up</kbd>, <kbd>k</kbd></td>
+                <td><FormattedMessage id='keyboard_shortcuts.up' defaultMessage='to move up in the list' /></td>
+              </tr>
+              <tr>
+                <td><kbd>down</kbd>, <kbd>j</kbd></td>
+                <td><FormattedMessage id='keyboard_shortcuts.down' defaultMessage='to move down in the list' /></td>
+              </tr>
+              <tr>
+                <td><kbd>1</kbd>-<kbd>9</kbd></td>
+                <td><FormattedMessage id='keyboard_shortcuts.column' defaultMessage='to focus a status in one of the columns' /></td>
+              </tr>
+              <tr>
+                <td><kbd>n</kbd></td>
+                <td><FormattedMessage id='keyboard_shortcuts.compose' defaultMessage='to focus the compose textarea' /></td>
+              </tr>
+              <tr>
+                <td><kbd>alt</kbd>+<kbd>n</kbd></td>
+                <td><FormattedMessage id='keyboard_shortcuts.toot' defaultMessage='to start a brand new post' /></td>
+              </tr>
+              <tr>
+                <td><kbd>alt</kbd>+<kbd>x</kbd></td>
+                <td><FormattedMessage id='keyboard_shortcuts.spoilers' defaultMessage='to show/hide CW field' /></td>
+              </tr>
+              <tr>
+                <td><kbd>backspace</kbd></td>
+                <td><FormattedMessage id='keyboard_shortcuts.back' defaultMessage='to navigate back' /></td>
+              </tr>
+              <tr>
+                <td><kbd>s</kbd></td>
+                <td><FormattedMessage id='keyboard_shortcuts.search' defaultMessage='to focus search' /></td>
+              </tr>
+              <tr>
+                <td><kbd>alt</kbd>+<kbd>enter</kbd></td>
+                <td><FormattedMessage id='keyboard_shortcuts.secondary_toot' defaultMessage='to send toot using secondary privacy setting' /></td>
+              </tr>
+              <tr>
+                <td><kbd>esc</kbd></td>
+                <td><FormattedMessage id='keyboard_shortcuts.unfocus' defaultMessage='to un-focus compose textarea/search' /></td>
+              </tr>
+              <tr>
+                <td><kbd>?</kbd></td>
+                <td><FormattedMessage id='keyboard_shortcuts.legend' defaultMessage='to display this legend' /></td>
+              </tr>
+            </tbody>
+          </table>
+        </div>
+
+        <Helmet>
+          <meta name='robots' content='noindex' />
+        </Helmet>
+      </Column>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(injectIntl(KeyboardShortcuts));
diff --git a/app/javascript/flavours/blobfox/features/list_adder/components/account.jsx b/app/javascript/flavours/blobfox/features/list_adder/components/account.jsx
new file mode 100644
index 00000000000000..94a90726e33342
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/list_adder/components/account.jsx
@@ -0,0 +1,43 @@
+import { injectIntl } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
+import { Avatar } from '../../../components/avatar';
+import { DisplayName } from '../../../components/display_name';
+import { makeGetAccount } from '../../../selectors';
+
+const makeMapStateToProps = () => {
+  const getAccount = makeGetAccount();
+
+  const mapStateToProps = (state, { accountId }) => ({
+    account: getAccount(state, accountId),
+  });
+
+  return mapStateToProps;
+};
+
+class Account extends ImmutablePureComponent {
+
+  static propTypes = {
+    account: ImmutablePropTypes.record.isRequired,
+  };
+
+  render () {
+    const { account } = this.props;
+    return (
+      <div className='account'>
+        <div className='account__wrapper'>
+          <div className='account__display-name'>
+            <div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
+            <DisplayName account={account} />
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default connect(makeMapStateToProps)(injectIntl(Account));
diff --git a/app/javascript/flavours/blobfox/features/list_adder/components/list.jsx b/app/javascript/flavours/blobfox/features/list_adder/components/list.jsx
new file mode 100644
index 00000000000000..385bd9f404a44f
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/list_adder/components/list.jsx
@@ -0,0 +1,72 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
+import { Icon }  from 'flavours/blobfox/components/icon';
+
+import { removeFromListAdder, addToListAdder } from '../../../actions/lists';
+import { IconButton }  from '../../../components/icon_button';
+
+const messages = defineMessages({
+  remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' },
+  add: { id: 'lists.account.add', defaultMessage: 'Add to list' },
+});
+
+const MapStateToProps = (state, { listId, added }) => ({
+  list: state.get('lists').get(listId),
+  added: typeof added === 'undefined' ? state.getIn(['listAdder', 'lists', 'items']).includes(listId) : added,
+});
+
+const mapDispatchToProps = (dispatch, { listId }) => ({
+  onRemove: () => dispatch(removeFromListAdder(listId)),
+  onAdd: () => dispatch(addToListAdder(listId)),
+});
+
+class List extends ImmutablePureComponent {
+
+  static propTypes = {
+    list: ImmutablePropTypes.map.isRequired,
+    intl: PropTypes.object.isRequired,
+    onRemove: PropTypes.func.isRequired,
+    onAdd: PropTypes.func.isRequired,
+    added: PropTypes.bool,
+  };
+
+  static defaultProps = {
+    added: false,
+  };
+
+  render () {
+    const { list, intl, onRemove, onAdd, added } = this.props;
+
+    let button;
+
+    if (added) {
+      button = <IconButton icon='times' title={intl.formatMessage(messages.remove)} onClick={onRemove} />;
+    } else {
+      button = <IconButton icon='plus' title={intl.formatMessage(messages.add)} onClick={onAdd} />;
+    }
+
+    return (
+      <div className='list'>
+        <div className='list__wrapper'>
+          <div className='list__display-name'>
+            <Icon id='list-ul' className='column-link__icon' fixedWidth />
+            {list.get('title')}
+          </div>
+
+          <div className='account__relationship'>
+            {button}
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default connect(MapStateToProps, mapDispatchToProps)(injectIntl(List));
diff --git a/app/javascript/flavours/blobfox/features/list_adder/index.jsx b/app/javascript/flavours/blobfox/features/list_adder/index.jsx
new file mode 100644
index 00000000000000..1ba9972e00d346
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/list_adder/index.jsx
@@ -0,0 +1,76 @@
+import PropTypes from 'prop-types';
+
+import { injectIntl } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+
+import { setupListAdder, resetListAdder } from '../../actions/lists';
+import NewListForm from '../lists/components/new_list_form';
+
+import Account from './components/account';
+import List from './components/list';
+// hack
+
+const getOrderedLists = createSelector([state => state.get('lists')], lists => {
+  if (!lists) {
+    return lists;
+  }
+
+  return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
+});
+
+const mapStateToProps = state => ({
+  listIds: getOrderedLists(state).map(list=>list.get('id')),
+});
+
+const mapDispatchToProps = dispatch => ({
+  onInitialize: accountId => dispatch(setupListAdder(accountId)),
+  onReset: () => dispatch(resetListAdder()),
+});
+
+class ListAdder extends ImmutablePureComponent {
+
+  static propTypes = {
+    accountId: PropTypes.string.isRequired,
+    onClose: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+    onInitialize: PropTypes.func.isRequired,
+    onReset: PropTypes.func.isRequired,
+    listIds: ImmutablePropTypes.list.isRequired,
+  };
+
+  componentDidMount () {
+    const { onInitialize, accountId } = this.props;
+    onInitialize(accountId);
+  }
+
+  componentWillUnmount () {
+    const { onReset } = this.props;
+    onReset();
+  }
+
+  render () {
+    const { accountId, listIds } = this.props;
+
+    return (
+      <div className='modal-root__modal list-adder'>
+        <div className='list-adder__account'>
+          <Account accountId={accountId} />
+        </div>
+
+        <NewListForm />
+
+
+        <div className='list-adder__lists'>
+          {listIds.map(ListId => <List key={ListId} listId={ListId} />)}
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(ListAdder));
diff --git a/app/javascript/flavours/blobfox/features/list_editor/components/account.jsx b/app/javascript/flavours/blobfox/features/list_editor/components/account.jsx
new file mode 100644
index 00000000000000..96b5e96df87e55
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/list_editor/components/account.jsx
@@ -0,0 +1,79 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
+import { removeFromListEditor, addToListEditor } from '../../../actions/lists';
+import { Avatar } from '../../../components/avatar';
+import { DisplayName } from '../../../components/display_name';
+import { IconButton } from '../../../components/icon_button';
+import { makeGetAccount } from '../../../selectors';
+
+const messages = defineMessages({
+  remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' },
+  add: { id: 'lists.account.add', defaultMessage: 'Add to list' },
+});
+
+const makeMapStateToProps = () => {
+  const getAccount = makeGetAccount();
+
+  const mapStateToProps = (state, { accountId, added }) => ({
+    account: getAccount(state, accountId),
+    added: typeof added === 'undefined' ? state.getIn(['listEditor', 'accounts', 'items']).includes(accountId) : added,
+  });
+
+  return mapStateToProps;
+};
+
+const mapDispatchToProps = (dispatch, { accountId }) => ({
+  onRemove: () => dispatch(removeFromListEditor(accountId)),
+  onAdd: () => dispatch(addToListEditor(accountId)),
+});
+
+class Account extends ImmutablePureComponent {
+
+  static propTypes = {
+    account: ImmutablePropTypes.record.isRequired,
+    intl: PropTypes.object.isRequired,
+    onRemove: PropTypes.func.isRequired,
+    onAdd: PropTypes.func.isRequired,
+    added: PropTypes.bool,
+  };
+
+  static defaultProps = {
+    added: false,
+  };
+
+  render () {
+    const { account, intl, onRemove, onAdd, added } = this.props;
+
+    let button;
+
+    if (added) {
+      button = <IconButton icon='times' title={intl.formatMessage(messages.remove)} onClick={onRemove} />;
+    } else {
+      button = <IconButton icon='plus' title={intl.formatMessage(messages.add)} onClick={onAdd} />;
+    }
+
+    return (
+      <div className='account'>
+        <div className='account__wrapper'>
+          <div className='account__display-name'>
+            <div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
+            <DisplayName account={account} />
+          </div>
+
+          <div className='account__relationship'>
+            {button}
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default connect(makeMapStateToProps, mapDispatchToProps)(injectIntl(Account));
diff --git a/app/javascript/flavours/blobfox/features/list_editor/components/edit_list_form.jsx b/app/javascript/flavours/blobfox/features/list_editor/components/edit_list_form.jsx
new file mode 100644
index 00000000000000..9e087a97d714d9
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/list_editor/components/edit_list_form.jsx
@@ -0,0 +1,73 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, injectIntl } from 'react-intl';
+
+import { connect } from 'react-redux';
+
+import { changeListEditorTitle, submitListEditor } from '../../../actions/lists';
+import { IconButton } from '../../../components/icon_button';
+
+const messages = defineMessages({
+  title: { id: 'lists.edit.submit', defaultMessage: 'Change title' },
+});
+
+const mapStateToProps = state => ({
+  value: state.getIn(['listEditor', 'title']),
+  disabled: !state.getIn(['listEditor', 'isChanged']) || !state.getIn(['listEditor', 'title']),
+});
+
+const mapDispatchToProps = dispatch => ({
+  onChange: value => dispatch(changeListEditorTitle(value)),
+  onSubmit: () => dispatch(submitListEditor(false)),
+});
+
+class ListForm extends PureComponent {
+
+  static propTypes = {
+    value: PropTypes.string.isRequired,
+    disabled: PropTypes.bool,
+    intl: PropTypes.object.isRequired,
+    onChange: PropTypes.func.isRequired,
+    onSubmit: PropTypes.func.isRequired,
+  };
+
+  handleChange = e => {
+    this.props.onChange(e.target.value);
+  };
+
+  handleSubmit = e => {
+    e.preventDefault();
+    this.props.onSubmit();
+  };
+
+  handleClick = () => {
+    this.props.onSubmit();
+  };
+
+  render () {
+    const { value, disabled, intl } = this.props;
+
+    const title = intl.formatMessage(messages.title);
+
+    return (
+      <form className='column-inline-form' onSubmit={this.handleSubmit}>
+        <input
+          className='setting-text'
+          value={value}
+          onChange={this.handleChange}
+        />
+
+        <IconButton
+          disabled={disabled}
+          icon='check'
+          title={title}
+          onClick={this.handleClick}
+        />
+      </form>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(ListForm));
diff --git a/app/javascript/flavours/blobfox/features/list_editor/components/search.jsx b/app/javascript/flavours/blobfox/features/list_editor/components/search.jsx
new file mode 100644
index 00000000000000..04899ac8a719a1
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/list_editor/components/search.jsx
@@ -0,0 +1,81 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, injectIntl } from 'react-intl';
+
+import classNames from 'classnames';
+
+import { connect } from 'react-redux';
+
+import { Icon }  from 'flavours/blobfox/components/icon';
+
+import { fetchListSuggestions, clearListSuggestions, changeListSuggestions } from '../../../actions/lists';
+
+const messages = defineMessages({
+  search: { id: 'lists.search', defaultMessage: 'Search among people you follow' },
+});
+
+const mapStateToProps = state => ({
+  value: state.getIn(['listEditor', 'suggestions', 'value']),
+});
+
+const mapDispatchToProps = dispatch => ({
+  onSubmit: value => dispatch(fetchListSuggestions(value)),
+  onClear: () => dispatch(clearListSuggestions()),
+  onChange: value => dispatch(changeListSuggestions(value)),
+});
+
+class Search extends PureComponent {
+
+  static propTypes = {
+    intl: PropTypes.object.isRequired,
+    value: PropTypes.string.isRequired,
+    onChange: PropTypes.func.isRequired,
+    onSubmit: PropTypes.func.isRequired,
+    onClear: PropTypes.func.isRequired,
+  };
+
+  handleChange = e => {
+    this.props.onChange(e.target.value);
+  };
+
+  handleKeyUp = e => {
+    if (e.keyCode === 13) {
+      this.props.onSubmit(this.props.value);
+    }
+  };
+
+  handleClear = () => {
+    this.props.onClear();
+  };
+
+  render () {
+    const { value, intl } = this.props;
+    const hasValue = value.length > 0;
+
+    return (
+      <div className='list-editor__search search'>
+        <label>
+          <span style={{ display: 'none' }}>{intl.formatMessage(messages.search)}</span>
+
+          <input
+            className='search__input'
+            type='text'
+            value={value}
+            onChange={this.handleChange}
+            onKeyUp={this.handleKeyUp}
+            placeholder={intl.formatMessage(messages.search)}
+          />
+        </label>
+
+        <div role='button' tabIndex={0} className='search__icon' onClick={this.handleClear}>
+          <Icon id='search' className={classNames({ active: !hasValue })} />
+          <Icon id='times-circle' aria-label={intl.formatMessage(messages.search)} className={classNames({ active: hasValue })} />
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(Search));
diff --git a/app/javascript/flavours/blobfox/features/list_editor/index.jsx b/app/javascript/flavours/blobfox/features/list_editor/index.jsx
new file mode 100644
index 00000000000000..85e90169e80b2c
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/list_editor/index.jsx
@@ -0,0 +1,83 @@
+import PropTypes from 'prop-types';
+
+import { injectIntl } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
+import spring from 'react-motion/lib/spring';
+
+import { setupListEditor, clearListSuggestions, resetListEditor } from '../../actions/lists';
+import Motion from '../ui/util/optional_motion';
+
+import Account from './components/account';
+import EditListForm from './components/edit_list_form';
+import Search from './components/search';
+
+const mapStateToProps = state => ({
+  accountIds: state.getIn(['listEditor', 'accounts', 'items']),
+  searchAccountIds: state.getIn(['listEditor', 'suggestions', 'items']),
+});
+
+const mapDispatchToProps = dispatch => ({
+  onInitialize: listId => dispatch(setupListEditor(listId)),
+  onClear: () => dispatch(clearListSuggestions()),
+  onReset: () => dispatch(resetListEditor()),
+});
+
+class ListEditor extends ImmutablePureComponent {
+
+  static propTypes = {
+    listId: PropTypes.string.isRequired,
+    onClose: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+    onInitialize: PropTypes.func.isRequired,
+    onClear: PropTypes.func.isRequired,
+    onReset: PropTypes.func.isRequired,
+    accountIds: ImmutablePropTypes.list.isRequired,
+    searchAccountIds: ImmutablePropTypes.list.isRequired,
+  };
+
+  componentDidMount () {
+    const { onInitialize, listId } = this.props;
+    onInitialize(listId);
+  }
+
+  componentWillUnmount () {
+    const { onReset } = this.props;
+    onReset();
+  }
+
+  render () {
+    const { accountIds, searchAccountIds, onClear } = this.props;
+    const showSearch = searchAccountIds.size > 0;
+
+    return (
+      <div className='modal-root__modal list-editor'>
+        <EditListForm />
+
+        <Search />
+
+        <div className='drawer__pager'>
+          <div className='drawer__inner list-editor__accounts'>
+            {accountIds.map(accountId => <Account key={accountId} accountId={accountId} added />)}
+          </div>
+
+          {showSearch && <div role='button' tabIndex={-1} className='drawer__backdrop' onClick={onClear} />}
+
+          <Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
+            {({ x }) => (
+              <div className='drawer__inner backdrop' style={{ transform: x === 0 ? null : `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}>
+                {searchAccountIds.map(accountId => <Account key={accountId} accountId={accountId} />)}
+              </div>
+            )}
+          </Motion>
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(ListEditor));
diff --git a/app/javascript/flavours/blobfox/features/list_timeline/index.jsx b/app/javascript/flavours/blobfox/features/list_timeline/index.jsx
new file mode 100644
index 00000000000000..e3ddab06abcfff
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/list_timeline/index.jsx
@@ -0,0 +1,244 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+import { withRouter } from 'react-router-dom';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
+
+import Toggle from 'react-toggle';
+
+import { addColumn, removeColumn, moveColumn } from 'flavours/blobfox/actions/columns';
+import { fetchList, deleteList, updateList } from 'flavours/blobfox/actions/lists';
+import { openModal } from 'flavours/blobfox/actions/modal';
+import { connectListStream } from 'flavours/blobfox/actions/streaming';
+import { expandListTimeline } from 'flavours/blobfox/actions/timelines';
+import Column from 'flavours/blobfox/components/column';
+import ColumnHeader from 'flavours/blobfox/components/column_header';
+import { Icon }  from 'flavours/blobfox/components/icon';
+import { LoadingIndicator } from 'flavours/blobfox/components/loading_indicator';
+import { RadioButton } from 'flavours/blobfox/components/radio_button';
+import BundleColumnError from 'flavours/blobfox/features/ui/components/bundle_column_error';
+import StatusListContainer from 'flavours/blobfox/features/ui/containers/status_list_container';
+import { WithRouterPropTypes } from 'flavours/blobfox/utils/react_router';
+
+const messages = defineMessages({
+  deleteMessage: { id: 'confirmations.delete_list.message', defaultMessage: 'Are you sure you want to permanently delete this list?' },
+  deleteConfirm: { id: 'confirmations.delete_list.confirm', defaultMessage: 'Delete' },
+  followed:   { id: 'lists.replies_policy.followed', defaultMessage: 'Any followed user' },
+  none:    { id: 'lists.replies_policy.none', defaultMessage: 'No one' },
+  list:  { id: 'lists.replies_policy.list', defaultMessage: 'Members of the list' },
+});
+
+const mapStateToProps = (state, props) => ({
+  list: state.getIn(['lists', props.params.id]),
+  hasUnread: state.getIn(['timelines', `list:${props.params.id}`, 'unread']) > 0,
+});
+
+class ListTimeline extends PureComponent {
+
+  static propTypes = {
+    params: PropTypes.object.isRequired,
+    dispatch: PropTypes.func.isRequired,
+    columnId: PropTypes.string,
+    hasUnread: PropTypes.bool,
+    multiColumn: PropTypes.bool,
+    list: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
+    intl: PropTypes.object.isRequired,
+    ...WithRouterPropTypes,
+  };
+
+  handlePin = () => {
+    const { columnId, dispatch } = this.props;
+
+    if (columnId) {
+      dispatch(removeColumn(columnId));
+    } else {
+      dispatch(addColumn('LIST', { id: this.props.params.id }));
+      this.props.history.push('/');
+    }
+  };
+
+  handleMove = (dir) => {
+    const { columnId, dispatch } = this.props;
+    dispatch(moveColumn(columnId, dir));
+  };
+
+  handleHeaderClick = () => {
+    this.column.scrollTop();
+  };
+
+  componentDidMount () {
+    const { dispatch } = this.props;
+    const { id } = this.props.params;
+
+    dispatch(fetchList(id));
+    dispatch(expandListTimeline(id));
+
+    this.disconnect = dispatch(connectListStream(id));
+  }
+
+  UNSAFE_componentWillReceiveProps (nextProps) {
+    const { dispatch } = this.props;
+    const { id } = nextProps.params;
+
+    if (id !== this.props.params.id) {
+      if (this.disconnect) {
+        this.disconnect();
+        this.disconnect = null;
+      }
+
+      dispatch(fetchList(id));
+      dispatch(expandListTimeline(id));
+
+      this.disconnect = dispatch(connectListStream(id));
+    }
+  }
+
+  componentWillUnmount () {
+    if (this.disconnect) {
+      this.disconnect();
+      this.disconnect = null;
+    }
+  }
+
+  setRef = c => {
+    this.column = c;
+  };
+
+  handleLoadMore = maxId => {
+    const { id } = this.props.params;
+    this.props.dispatch(expandListTimeline(id, { maxId }));
+  };
+
+  handleEditClick = () => {
+    this.props.dispatch(openModal({
+      modalType: 'LIST_EDITOR',
+      modalProps: { listId: this.props.params.id },
+    }));
+  };
+
+  handleDeleteClick = () => {
+    const { dispatch, columnId, intl } = this.props;
+    const { id } = this.props.params;
+
+    dispatch(openModal({
+      modalType: 'CONFIRM',
+      modalProps: {
+        message: intl.formatMessage(messages.deleteMessage),
+        confirm: intl.formatMessage(messages.deleteConfirm),
+        onConfirm: () => {
+          dispatch(deleteList(id));
+
+          if (columnId) {
+            dispatch(removeColumn(columnId));
+          } else {
+            this.props.history.push('/lists');
+          }
+        },
+      },
+    }));
+  };
+
+  handleRepliesPolicyChange = ({ target }) => {
+    const { dispatch } = this.props;
+    const { id } = this.props.params;
+    dispatch(updateList(id, undefined, false, undefined, target.value));
+  };
+
+  onExclusiveToggle = ({ target }) => {
+    const { dispatch } = this.props;
+    const { id } = this.props.params;
+    dispatch(updateList(id, undefined, false, target.checked, undefined));
+  };
+
+  render () {
+    const { hasUnread, columnId, multiColumn, list, intl } = this.props;
+    const { id } = this.props.params;
+    const pinned = !!columnId;
+    const title  = list ? list.get('title') : id;
+    const replies_policy = list ? list.get('replies_policy') : undefined;
+    const isExclusive = list ? list.get('exclusive') : undefined;
+
+    if (typeof list === 'undefined') {
+      return (
+        <Column>
+          <div className='scrollable'>
+            <LoadingIndicator />
+          </div>
+        </Column>
+      );
+    } else if (list === false) {
+      return (
+        <BundleColumnError multiColumn={multiColumn} errorType='routing' />
+      );
+    }
+
+    return (
+      <Column bindToDocument={!multiColumn} ref={this.setRef} label={title}>
+        <ColumnHeader
+          icon='list-ul'
+          active={hasUnread}
+          title={title}
+          onPin={this.handlePin}
+          onMove={this.handleMove}
+          onClick={this.handleHeaderClick}
+          pinned={pinned}
+          multiColumn={multiColumn}
+        >
+          <div className='column-settings__row column-header__links'>
+            <button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleEditClick}>
+              <Icon id='pencil' /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' />
+            </button>
+
+            <button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleDeleteClick}>
+              <Icon id='trash' /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' />
+            </button>
+          </div>
+
+          <div className='setting-toggle'>
+            <Toggle id={`list-${id}-exclusive`} checked={isExclusive} onChange={this.onExclusiveToggle} />
+            <label htmlFor={`list-${id}-exclusive`} className='setting-toggle__label'>
+              <FormattedMessage id='lists.exclusive' defaultMessage='Hide these posts from home' />
+            </label>
+          </div>
+
+          { replies_policy !== undefined && (
+            <div role='group' aria-labelledby={`list-${id}-replies-policy`}>
+              <span id={`list-${id}-replies-policy`} className='column-settings__section'>
+                <FormattedMessage id='lists.replies_policy.title' defaultMessage='Show replies to:' />
+              </span>
+              <div className='column-settings__row'>
+                { ['none', 'list', 'followed'].map(policy => (
+                  <RadioButton name='order' key={policy} value={policy} label={intl.formatMessage(messages[policy])} checked={replies_policy === policy} onChange={this.handleRepliesPolicyChange} />
+                ))}
+              </div>
+            </div>
+          )}
+
+          <hr />
+        </ColumnHeader>
+
+        <StatusListContainer
+          trackScroll={!pinned}
+          scrollKey={`list_timeline-${columnId}`}
+          timelineId={`list:${id}`}
+          onLoadMore={this.handleLoadMore}
+          emptyMessage={<FormattedMessage id='empty_column.list' defaultMessage='There is nothing in this list yet. When members of this list post new statuses, they will appear here.' />}
+          bindToDocument={!multiColumn}
+        />
+
+        <Helmet>
+          <title>{title}</title>
+          <meta name='robots' content='noindex' />
+        </Helmet>
+      </Column>
+    );
+  }
+
+}
+
+export default withRouter(connect(mapStateToProps)(injectIntl(ListTimeline)));
diff --git a/app/javascript/flavours/blobfox/features/lists/components/new_list_form.jsx b/app/javascript/flavours/blobfox/features/lists/components/new_list_form.jsx
new file mode 100644
index 00000000000000..f5581a765a7a7c
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/lists/components/new_list_form.jsx
@@ -0,0 +1,81 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, injectIntl } from 'react-intl';
+
+import { connect } from 'react-redux';
+
+import { changeListEditorTitle, submitListEditor } from 'flavours/blobfox/actions/lists';
+import { IconButton } from 'flavours/blobfox/components/icon_button';
+
+const messages = defineMessages({
+  label: { id: 'lists.new.title_placeholder', defaultMessage: 'New list title' },
+  title: { id: 'lists.new.create', defaultMessage: 'Add list' },
+});
+
+const mapStateToProps = state => ({
+  value: state.getIn(['listEditor', 'title']),
+  disabled: state.getIn(['listEditor', 'isSubmitting']),
+});
+
+const mapDispatchToProps = dispatch => ({
+  onChange: value => dispatch(changeListEditorTitle(value)),
+  onSubmit: () => dispatch(submitListEditor(true)),
+});
+
+class NewListForm extends PureComponent {
+
+  static propTypes = {
+    value: PropTypes.string.isRequired,
+    disabled: PropTypes.bool,
+    intl: PropTypes.object.isRequired,
+    onChange: PropTypes.func.isRequired,
+    onSubmit: PropTypes.func.isRequired,
+  };
+
+  handleChange = e => {
+    this.props.onChange(e.target.value);
+  };
+
+  handleSubmit = e => {
+    e.preventDefault();
+    this.props.onSubmit();
+  };
+
+  handleClick = () => {
+    this.props.onSubmit();
+  };
+
+  render () {
+    const { value, disabled, intl } = this.props;
+
+    const label = intl.formatMessage(messages.label);
+    const title = intl.formatMessage(messages.title);
+
+    return (
+      <form className='column-inline-form' onSubmit={this.handleSubmit}>
+        <label>
+          <span style={{ display: 'none' }}>{label}</span>
+
+          <input
+            className='setting-text'
+            value={value}
+            disabled={disabled}
+            onChange={this.handleChange}
+            placeholder={label}
+          />
+        </label>
+
+        <IconButton
+          disabled={disabled || !value}
+          icon='plus'
+          title={title}
+          onClick={this.handleClick}
+        />
+      </form>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(NewListForm));
diff --git a/app/javascript/flavours/blobfox/features/lists/index.jsx b/app/javascript/flavours/blobfox/features/lists/index.jsx
new file mode 100644
index 00000000000000..9384df3cf210e1
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/lists/index.jsx
@@ -0,0 +1,93 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+
+import { fetchLists } from 'flavours/blobfox/actions/lists';
+import ColumnBackButtonSlim from 'flavours/blobfox/components/column_back_button_slim';
+import { LoadingIndicator } from 'flavours/blobfox/components/loading_indicator';
+import ScrollableList from 'flavours/blobfox/components/scrollable_list';
+import Column from 'flavours/blobfox/features/ui/components/column';
+import ColumnLink from 'flavours/blobfox/features/ui/components/column_link';
+import ColumnSubheading from 'flavours/blobfox/features/ui/components/column_subheading';
+
+import NewListForm from './components/new_list_form';
+
+const messages = defineMessages({
+  heading: { id: 'column.lists', defaultMessage: 'Lists' },
+  subheading: { id: 'lists.subheading', defaultMessage: 'Your lists' },
+});
+
+const getOrderedLists = createSelector([state => state.get('lists')], lists => {
+  if (!lists) {
+    return lists;
+  }
+
+  return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
+});
+
+const mapStateToProps = state => ({
+  lists: getOrderedLists(state),
+});
+
+class Lists extends ImmutablePureComponent {
+
+  static propTypes = {
+    params: PropTypes.object.isRequired,
+    dispatch: PropTypes.func.isRequired,
+    lists: ImmutablePropTypes.list,
+    intl: PropTypes.object.isRequired,
+    multiColumn: PropTypes.bool,
+  };
+
+  UNSAFE_componentWillMount () {
+    this.props.dispatch(fetchLists());
+  }
+
+  render () {
+    const { intl, lists, multiColumn } = this.props;
+
+    if (!lists) {
+      return (
+        <Column>
+          <LoadingIndicator />
+        </Column>
+      );
+    }
+
+    const emptyMessage = <FormattedMessage id='empty_column.lists' defaultMessage="You don't have any lists yet. When you create one, it will show up here." />;
+
+    return (
+      <Column bindToDocument={!multiColumn} icon='bars' heading={intl.formatMessage(messages.heading)}>
+        <ColumnBackButtonSlim />
+
+        <NewListForm />
+
+        <ColumnSubheading text={intl.formatMessage(messages.subheading)} />
+        <ScrollableList
+          scrollKey='lists'
+          emptyMessage={emptyMessage}
+          bindToDocument={!multiColumn}
+        >
+          {lists.map(list =>
+            <ColumnLink key={list.get('id')} to={`/lists/${list.get('id')}`} icon='list-ul' text={list.get('title')} />,
+          )}
+        </ScrollableList>
+
+        <Helmet>
+          <title>{intl.formatMessage(messages.heading)}</title>
+          <meta name='robots' content='noindex' />
+        </Helmet>
+      </Column>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(injectIntl(Lists));
diff --git a/app/javascript/flavours/blobfox/features/local_settings/index.jsx b/app/javascript/flavours/blobfox/features/local_settings/index.jsx
new file mode 100644
index 00000000000000..c6062c2bd06c7c
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/local_settings/index.jsx
@@ -0,0 +1,70 @@
+//  Package imports.
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
+
+//  Our imports
+import { changeLocalSetting } from 'flavours/blobfox/actions/local_settings';
+import { closeModal } from 'flavours/blobfox/actions/modal';
+
+import LocalSettingsNavigation from './navigation';
+import LocalSettingsPage from './page';
+
+const mapStateToProps = state => ({
+  settings: state.get('local_settings'),
+});
+
+const mapDispatchToProps = dispatch => ({
+  onChange (setting, value) {
+    dispatch(changeLocalSetting(setting, value));
+  },
+  onClose () {
+    dispatch(closeModal({
+      modalType: undefined,
+      ignoreFocus: false,
+    }));
+  },
+});
+
+class LocalSettings extends PureComponent {
+
+  static propTypes = {
+    onChange: PropTypes.func.isRequired,
+    onClose: PropTypes.func.isRequired,
+    settings: ImmutablePropTypes.map.isRequired,
+  };
+
+  state = {
+    currentIndex: 0,
+  };
+
+  navigateTo = (index) =>
+    this.setState({ currentIndex: +index });
+
+  render () {
+
+    const { navigateTo } = this;
+    const { onChange, onClose, settings } = this.props;
+    const { currentIndex } = this.state;
+
+    return (
+      <div className='blobfox modal-root__modal local-settings'>
+        <LocalSettingsNavigation
+          index={currentIndex}
+          onClose={onClose}
+          onNavigate={navigateTo}
+        />
+        <LocalSettingsPage
+          index={currentIndex}
+          onChange={onChange}
+          settings={settings}
+        />
+      </div>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(LocalSettings);
diff --git a/app/javascript/flavours/blobfox/features/local_settings/navigation/index.jsx b/app/javascript/flavours/blobfox/features/local_settings/navigation/index.jsx
new file mode 100644
index 00000000000000..1f9f3a3edb84cc
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/local_settings/navigation/index.jsx
@@ -0,0 +1,95 @@
+//  Package imports
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { injectIntl, defineMessages } from 'react-intl';
+
+//  Our imports
+import { preferencesLink } from 'flavours/blobfox/utils/backend_links';
+
+import LocalSettingsNavigationItem from './item';
+
+//  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+
+const messages = defineMessages({
+  general: {  id: 'settings.general', defaultMessage: 'General' },
+  compose: {  id: 'settings.compose_box_opts', defaultMessage: 'Compose box' },
+  content_warnings: { id: 'settings.content_warnings', defaultMessage: 'Content Warnings' },
+  collapsed: { id: 'settings.collapsed_statuses', defaultMessage: 'Collapsed toots' },
+  media: { id: 'settings.media', defaultMessage: 'Media' },
+  preferences: { id: 'settings.preferences', defaultMessage: 'Preferences' },
+  close: { id: 'settings.close', defaultMessage: 'Close' },
+});
+
+class LocalSettingsNavigation extends PureComponent {
+
+  static propTypes = {
+    index      : PropTypes.number,
+    intl       : PropTypes.object.isRequired,
+    onClose    : PropTypes.func.isRequired,
+    onNavigate : PropTypes.func.isRequired,
+  };
+
+  render () {
+
+    const { index, intl, onClose, onNavigate } = this.props;
+
+    return (
+      <nav className='blobfox local-settings__navigation'>
+        <LocalSettingsNavigationItem
+          active={index === 0}
+          index={0}
+          onNavigate={onNavigate}
+          icon='cogs'
+          title={intl.formatMessage(messages.general)}
+        />
+        <LocalSettingsNavigationItem
+          active={index === 1}
+          index={1}
+          onNavigate={onNavigate}
+          icon='pencil'
+          title={intl.formatMessage(messages.compose)}
+        />
+        <LocalSettingsNavigationItem
+          active={index === 2}
+          index={2}
+          onNavigate={onNavigate}
+          textIcon='CW'
+          title={intl.formatMessage(messages.content_warnings)}
+        />
+        <LocalSettingsNavigationItem
+          active={index === 3}
+          index={3}
+          onNavigate={onNavigate}
+          icon='angle-double-up'
+          title={intl.formatMessage(messages.collapsed)}
+        />
+        <LocalSettingsNavigationItem
+          active={index === 4}
+          index={4}
+          onNavigate={onNavigate}
+          icon='image'
+          title={intl.formatMessage(messages.media)}
+        />
+        <LocalSettingsNavigationItem
+          active={index === 5}
+          href={preferencesLink}
+          index={5}
+          icon='cog'
+          title={intl.formatMessage(messages.preferences)}
+        />
+        <LocalSettingsNavigationItem
+          active={index === 6}
+          className='close'
+          index={6}
+          onNavigate={onClose}
+          icon='times'
+          title={intl.formatMessage(messages.close)}
+        />
+      </nav>
+    );
+  }
+
+}
+
+export default injectIntl(LocalSettingsNavigation);
diff --git a/app/javascript/flavours/blobfox/features/local_settings/navigation/item/index.jsx b/app/javascript/flavours/blobfox/features/local_settings/navigation/item/index.jsx
new file mode 100644
index 00000000000000..30e692a92f0844
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/local_settings/navigation/item/index.jsx
@@ -0,0 +1,73 @@
+//  Package imports
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import classNames from 'classnames';
+
+import { Icon } from 'flavours/blobfox/components/icon';
+
+//  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+
+export default class LocalSettingsPage extends PureComponent {
+
+  static propTypes = {
+    active: PropTypes.bool,
+    className: PropTypes.string,
+    href: PropTypes.string,
+    icon: PropTypes.string,
+    textIcon: PropTypes.string,
+    index: PropTypes.number.isRequired,
+    onNavigate: PropTypes.func,
+    title: PropTypes.string,
+  };
+
+  handleClick = (e) => {
+    const { index, onNavigate } = this.props;
+    if (onNavigate) {
+      onNavigate(index);
+      e.preventDefault();
+    }
+  };
+
+  render () {
+    const { handleClick } = this;
+    const {
+      active,
+      className,
+      href,
+      icon,
+      textIcon,
+      onNavigate,
+      title,
+    } = this.props;
+
+    const finalClassName = classNames('blobfox', 'local-settings__navigation__item', {
+      active,
+    }, className);
+
+    const iconElem = icon ? <Icon fixedWidth id={icon} /> : (textIcon ? <span className='text-icon-button'>{textIcon}</span> : null);
+
+    if (href) return (
+      <a
+        href={href}
+        className={finalClassName}
+        title={title}
+        aria-label={title}
+      >
+        {iconElem} <span>{title}</span>
+      </a>
+    );
+    else if (onNavigate) return (
+      <button
+        onClick={handleClick}
+        className={finalClassName}
+        title={title}
+        aria-label={title}
+      >
+        {iconElem} <span>{title}</span>
+      </button>
+    );
+    else return null;
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/local_settings/page/deprecated_item/index.jsx b/app/javascript/flavours/blobfox/features/local_settings/page/deprecated_item/index.jsx
new file mode 100644
index 00000000000000..0f53952cbe45cf
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/local_settings/page/deprecated_item/index.jsx
@@ -0,0 +1,83 @@
+//  Package imports
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+//  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+
+export default class LocalSettingsPageItem extends PureComponent {
+
+  static propTypes = {
+    children: PropTypes.node.isRequired,
+    id: PropTypes.string.isRequired,
+    options: PropTypes.arrayOf(PropTypes.shape({
+      value: PropTypes.string.isRequired,
+      message: PropTypes.string.isRequired,
+      hint: PropTypes.string,
+    })),
+    value: PropTypes.any,
+    placeholder: PropTypes.string,
+  };
+
+  render () {
+    const { id, options, children, placeholder, value } = this.props;
+
+    if (options && options.length > 0) {
+      const currentValue = value;
+      const optionElems = options && options.length > 0 && options.map((opt) => {
+        let optionId = `${id}--${opt.value}`;
+        return (
+          <label key={id} htmlFor={optionId}>
+            <input
+              type='radio'
+              name={id}
+              id={optionId}
+              value={opt.value}
+              checked={currentValue === opt.value}
+              disabled
+            />
+            {opt.message}
+            {opt.hint && <span className='hint'>{opt.hint}</span>}
+          </label>
+        );
+      });
+      return (
+        <div className='blobfox local-settings__page__item radio_buttons'>
+          <fieldset>
+            <legend>{children}</legend>
+            {optionElems}
+          </fieldset>
+        </div>
+      );
+    } else if (placeholder) {
+      return (
+        <div className='blobfox local-settings__page__item string'>
+          <label htmlFor={id}>
+            <p>{children}</p>
+            <p>
+              <input
+                id={id}
+                type='text'
+                value={value}
+                placeholder={placeholder}
+                disabled
+              />
+            </p>
+          </label>
+        </div>
+      );
+    } else return (
+      <div className='blobfox local-settings__page__item boolean'>
+        <label htmlFor={id}>
+          <input
+            id={id}
+            type='checkbox'
+            checked={value}
+            disabled
+          />
+          {children}
+        </label>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/local_settings/page/index.jsx b/app/javascript/flavours/blobfox/features/local_settings/page/index.jsx
new file mode 100644
index 00000000000000..917839d5c10ac5
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/local_settings/page/index.jsx
@@ -0,0 +1,505 @@
+//  Package imports
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
+
+//  Our imports
+import { expandSpoilers } from 'flavours/blobfox/initial_state';
+import { preferenceLink } from 'flavours/blobfox/utils/backend_links';
+
+import DeprecatedLocalSettingsPageItem from './deprecated_item';
+import LocalSettingsPageItem from './item';
+
+//  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+
+const messages = defineMessages({
+  side_arm_none: { id: 'settings.side_arm.none', defaultMessage: 'None' },
+  side_arm_keep: { id: 'settings.side_arm_reply_mode.keep', defaultMessage: 'Keep its set privacy' },
+  side_arm_copy: { id: 'settings.side_arm_reply_mode.copy', defaultMessage: 'Copy privacy setting of the toot being replied to' },
+  side_arm_restrict: { id: 'settings.side_arm_reply_mode.restrict', defaultMessage: 'Restrict privacy setting to that of the toot being replied to' },
+  regexp: { id: 'settings.content_warnings.regexp', defaultMessage: 'Regular expression' },
+  rewrite_mentions_no: { id: 'settings.rewrite_mentions_no', defaultMessage: 'Do not rewrite mentions' },
+  rewrite_mentions_acct: { id: 'settings.rewrite_mentions_acct', defaultMessage: 'Rewrite with username and domain (when the account is remote)' },
+  rewrite_mentions_username: { id: 'settings.rewrite_mentions_username', defaultMessage:  'Rewrite with username' },
+  pop_in_left: { id: 'settings.pop_in_left', defaultMessage: 'Left' },
+  pop_in_right: { id: 'settings.pop_in_right', defaultMessage:  'Right' },
+  public: { id: 'privacy.public.short', defaultMessage: 'Public' },
+  unlisted: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
+  private: { id: 'privacy.private.short', defaultMessage: 'Followers only' },
+  direct: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
+});
+
+class LocalSettingsPage extends PureComponent {
+
+  static propTypes = {
+    index    : PropTypes.number,
+    intl     : PropTypes.object.isRequired,
+    onChange : PropTypes.func.isRequired,
+    settings : ImmutablePropTypes.map.isRequired,
+  };
+
+  pages = [
+    ({ intl, onChange, settings }) => (
+      <div className='blobfox local-settings__page general'>
+        <h1><FormattedMessage id='settings.general' defaultMessage='General' /></h1>
+        <LocalSettingsPageItem
+          settings={settings}
+          item={['show_reply_count']}
+          id='mastodon-settings--reply-count'
+          onChange={onChange}
+        >
+          <FormattedMessage id='settings.show_reply_counter' defaultMessage='Display an estimate of the reply count' />
+        </LocalSettingsPageItem>
+        <LocalSettingsPageItem
+          settings={settings}
+          item={['hicolor_privacy_icons']}
+          id='mastodon-settings--hicolor_privacy_icons'
+          onChange={onChange}
+        >
+          <FormattedMessage id='settings.hicolor_privacy_icons' defaultMessage='High color privacy icons' />
+          <span className='hint'><FormattedMessage id='settings.hicolor_privacy_icons.hint' defaultMessage='Display privacy icons in bright and easily distinguishable colors' /></span>
+        </LocalSettingsPageItem>
+        <LocalSettingsPageItem
+          settings={settings}
+          item={['confirm_boost_missing_media_description']}
+          id='mastodon-settings--confirm_boost_missing_media_description'
+          onChange={onChange}
+        >
+          <FormattedMessage id='settings.confirm_boost_missing_media_description' defaultMessage='Show confirmation dialog before boosting toots lacking media descriptions' />
+        </LocalSettingsPageItem>
+        <LocalSettingsPageItem
+          settings={settings}
+          item={['tag_misleading_links']}
+          id='mastodon-settings--tag_misleading_links'
+          onChange={onChange}
+        >
+          <FormattedMessage id='settings.tag_misleading_links' defaultMessage='Tag misleading links' />
+          <span className='hint'><FormattedMessage id='settings.tag_misleading_links.hint' defaultMessage='Add a visual indication with the link target host to every link not mentioning it explicitly' /></span>
+        </LocalSettingsPageItem>
+        <LocalSettingsPageItem
+          settings={settings}
+          item={['rewrite_mentions']}
+          id='mastodon-settings--rewrite_mentions'
+          options={[
+            { value: 'no', message: intl.formatMessage(messages.rewrite_mentions_no) },
+            { value: 'acct', message: intl.formatMessage(messages.rewrite_mentions_acct) },
+            { value: 'username', message: intl.formatMessage(messages.rewrite_mentions_username) },
+          ]}
+          onChange={onChange}
+        >
+          <FormattedMessage id='settings.rewrite_mentions' defaultMessage='Rewrite mentions in displayed statuses' />
+        </LocalSettingsPageItem>
+        <section>
+          <h2><FormattedMessage id='settings.notifications_opts' defaultMessage='Notifications options' /></h2>
+          <LocalSettingsPageItem
+            settings={settings}
+            item={['notifications', 'tab_badge']}
+            id='mastodon-settings--notifications-tab_badge'
+            onChange={onChange}
+          >
+            <FormattedMessage id='settings.notifications.tab_badge' defaultMessage='Unread notifications badge' />
+            <span className='hint'><FormattedMessage id='settings.notifications.tab_badge.hint' defaultMessage="Display a badge for unread notifications in the column icons when the notifications column isn't open" /></span>
+          </LocalSettingsPageItem>
+          <LocalSettingsPageItem
+            settings={settings}
+            item={['notifications', 'favicon_badge']}
+            id='mastodon-settings--notifications-favicon_badge'
+            onChange={onChange}
+          >
+            <FormattedMessage id='settings.notifications.favicon_badge' defaultMessage='Unread notifications favicon badge' />
+            <span className='hint'><FormattedMessage id='settings.notifications.favicon_badge.hint' defaultMessage='Add a badge for unread notifications to the favicon' /></span>
+          </LocalSettingsPageItem>
+        </section>
+
+        <section>
+          <h2><FormattedMessage id='settings.status_icons' defaultMessage='Toot icons' /></h2>
+          <LocalSettingsPageItem
+            settings={settings}
+            item={['status_icons', 'language']}
+            id='mastodon-settings--status-icons-language'
+            onChange={onChange}
+          >
+            <FormattedMessage id='settings.status_icons_language' defaultMessage='Language indicator' />
+          </LocalSettingsPageItem>
+          <LocalSettingsPageItem
+            settings={settings}
+            item={['status_icons', 'reply']}
+            id='mastodon-settings--status-icons-reply'
+            onChange={onChange}
+          >
+            <FormattedMessage id='settings.status_icons_reply' defaultMessage='Reply indicator' />
+          </LocalSettingsPageItem>
+          <LocalSettingsPageItem
+            settings={settings}
+            item={['status_icons', 'local_only']}
+            id='mastodon-settings--status-icons-local_only'
+            onChange={onChange}
+          >
+            <FormattedMessage id='settings.status_icons_local_only' defaultMessage='Local-only indicator' />
+          </LocalSettingsPageItem>
+          <LocalSettingsPageItem
+            settings={settings}
+            item={['status_icons', 'media']}
+            id='mastodon-settings--status-icons-media'
+            onChange={onChange}
+          >
+            <FormattedMessage id='settings.status_icons_media' defaultMessage='Media and poll indicators' />
+          </LocalSettingsPageItem>
+          <LocalSettingsPageItem
+            settings={settings}
+            item={['status_icons', 'visibility']}
+            id='mastodon-settings--status-icons-visibility'
+            onChange={onChange}
+          >
+            <FormattedMessage id='settings.status_icons_visibility' defaultMessage='Toot privacy indicator' />
+          </LocalSettingsPageItem>
+        </section>
+        <section>
+          <h2><FormattedMessage id='settings.layout_opts' defaultMessage='Layout options' /></h2>
+          <LocalSettingsPageItem
+            settings={settings}
+            item={['stretch']}
+            id='mastodon-settings--stretch'
+            onChange={onChange}
+          >
+            <FormattedMessage id='settings.wide_view' defaultMessage='Wide view (Desktop mode only)' />
+            <span className='hint'><FormattedMessage id='settings.wide_view_hint' defaultMessage='Stretches columns to better fill the available space.' /></span>
+          </LocalSettingsPageItem>
+        </section>
+      </div>
+    ),
+    ({ intl, onChange, settings }) => (
+      <div className='blobfox local-settings__page compose_box_opts'>
+        <h1><FormattedMessage id='settings.compose_box_opts' defaultMessage='Compose box' /></h1>
+        <LocalSettingsPageItem
+          settings={settings}
+          item={['always_show_spoilers_field']}
+          id='mastodon-settings--always_show_spoilers_field'
+          onChange={onChange}
+        >
+          <FormattedMessage id='settings.always_show_spoilers_field' defaultMessage='Always enable the Content Warning field' />
+        </LocalSettingsPageItem>
+        <LocalSettingsPageItem
+          settings={settings}
+          item={['prepend_cw_re']}
+          id='mastodon-settings--prepend_cw_re'
+          onChange={onChange}
+        >
+          <FormattedMessage id='settings.prepend_cw_re' defaultMessage='Prepend “re: ” to content warnings when replying' />
+        </LocalSettingsPageItem>
+        <LocalSettingsPageItem
+          settings={settings}
+          item={['preselect_on_reply']}
+          id='mastodon-settings--preselect_on_reply'
+          onChange={onChange}
+        >
+          <FormattedMessage id='settings.preselect_on_reply' defaultMessage='Pre-select usernames on reply' />
+          <span className='hint'><FormattedMessage id='settings.preselect_on_reply_hint' defaultMessage='When replying to a conversation with multiple participants, pre-select usernames past the first' /></span>
+        </LocalSettingsPageItem>
+        <LocalSettingsPageItem
+          settings={settings}
+          item={['confirm_missing_media_description']}
+          id='mastodon-settings--confirm_missing_media_description'
+          onChange={onChange}
+        >
+          <FormattedMessage id='settings.confirm_missing_media_description' defaultMessage='Show confirmation dialog before sending toots lacking media descriptions' />
+        </LocalSettingsPageItem>
+        <LocalSettingsPageItem
+          settings={settings}
+          item={['confirm_before_clearing_draft']}
+          id='mastodon-settings--confirm_before_clearing_draft'
+          onChange={onChange}
+        >
+          <FormattedMessage id='settings.confirm_before_clearing_draft' defaultMessage='Show confirmation dialog before overwriting the message being composed' />
+        </LocalSettingsPageItem>
+        <LocalSettingsPageItem
+          settings={settings}
+          item={['show_content_type_choice']}
+          id='mastodon-settings--show_content_type_choice'
+          onChange={onChange}
+        >
+          <FormattedMessage id='settings.show_content_type_choice' defaultMessage='Show content-type choice when authoring toots' />
+        </LocalSettingsPageItem>
+        <LocalSettingsPageItem
+          settings={settings}
+          item={['side_arm']}
+          id='mastodon-settings--side_arm'
+          options={[
+            { value: 'none', message: intl.formatMessage(messages.side_arm_none) },
+            { value: 'direct', message: intl.formatMessage(messages.direct) },
+            { value: 'private', message: intl.formatMessage(messages.private) },
+            { value: 'unlisted', message: intl.formatMessage(messages.unlisted) },
+            { value: 'public', message: intl.formatMessage(messages.public) },
+          ]}
+          onChange={onChange}
+        >
+          <FormattedMessage id='settings.side_arm' defaultMessage='Secondary toot button:' />
+        </LocalSettingsPageItem>
+        <LocalSettingsPageItem
+          settings={settings}
+          item={['side_arm_reply_mode']}
+          id='mastodon-settings--side_arm_reply_mode'
+          options={[
+            { value: 'keep', message: intl.formatMessage(messages.side_arm_keep) },
+            { value: 'copy', message: intl.formatMessage(messages.side_arm_copy) },
+            { value: 'restrict', message: intl.formatMessage(messages.side_arm_restrict) },
+          ]}
+          onChange={onChange}
+        >
+          <FormattedMessage id='settings.side_arm_reply_mode' defaultMessage='When replying to a toot, the secondary toot button should:' />
+        </LocalSettingsPageItem>
+      </div>
+    ),
+    ({ intl, onChange, settings }) => (
+      <div className='blobfox local-settings__page content_warnings'>
+        <h1><FormattedMessage id='settings.content_warnings' defaultMessage='Content Warnings' /></h1>
+        <LocalSettingsPageItem
+          settings={settings}
+          item={['content_warnings', 'shared_state']}
+          id='mastodon-settings--content_warnings-shared_state'
+          onChange={onChange}
+        >
+          <FormattedMessage id='settings.content_warnings_shared_state' defaultMessage='Show/hide content of all copies at once' />
+          <span className='hint'><FormattedMessage id='settings.content_warnings_shared_state_hint' defaultMessage='Reproduce upstream Mastodon behavior by having the Content Warning button affect all copies of a post at once. This will prevent automatic collapsing of any copy of a toot with unfolded CW' /></span>
+        </LocalSettingsPageItem>
+        <LocalSettingsPageItem
+          settings={settings}
+          item={['content_warnings', 'media_outside']}
+          id='mastodon-settings--content_warnings-media_outside'
+          onChange={onChange}
+        >
+          <FormattedMessage id='settings.content_warnings_media_outside' defaultMessage='Display media attachments outside content warnings' />
+          <span className='hint'><FormattedMessage id='settings.content_warnings_media_outside_hint' defaultMessage='Reproduce upstream Mastodon behavior by having the Content Warning toggle not affect media attachments' /></span>
+        </LocalSettingsPageItem>
+        <section>
+          <h2><FormattedMessage id='settings.content_warnings_unfold_opts' defaultMessage='Auto-unfolding options' /></h2>
+          <DeprecatedLocalSettingsPageItem
+            id='mastodon-settings--content_warnings-auto_unfold'
+            value={expandSpoilers}
+          >
+            <FormattedMessage id='settings.enable_content_warnings_auto_unfold' defaultMessage='Automatically unfold content-warnings' />
+            <span className='hint'>
+              <FormattedMessage
+                id='settings.deprecated_setting'
+                defaultMessage="This setting is now controlled from Mastodon's {settings_page_link}"
+                values={{
+                  settings_page_link: (
+                    <a href={preferenceLink('user_setting_expand_spoilers')}>
+                      <FormattedMessage
+                        id='settings.shared_settings_link'
+                        defaultMessage='user preferences'
+                      />
+                    </a>
+                  ),
+                }}
+              />
+            </span>
+          </DeprecatedLocalSettingsPageItem>
+          <LocalSettingsPageItem
+            settings={settings}
+            item={['content_warnings', 'filter']}
+            id='mastodon-settings--content_warnings-auto_unfold'
+            onChange={onChange}
+            placeholder={intl.formatMessage(messages.regexp)}
+            disabled={!expandSpoilers}
+          >
+            <FormattedMessage id='settings.content_warnings_filter' defaultMessage='Content warnings to not automatically unfold:' />
+          </LocalSettingsPageItem>
+        </section>
+      </div>
+    ),
+    ({ onChange, settings }) => (
+      <div className='blobfox local-settings__page collapsed'>
+        <h1><FormattedMessage id='settings.collapsed_statuses' defaultMessage='Collapsed toots' /></h1>
+        <LocalSettingsPageItem
+          settings={settings}
+          item={['collapsed', 'enabled']}
+          id='mastodon-settings--collapsed-enabled'
+          onChange={onChange}
+        >
+          <FormattedMessage id='settings.enable_collapsed' defaultMessage='Enable collapsed toots' />
+          <span className='hint'><FormattedMessage id='settings.enable_collapsed_hint' defaultMessage='Collapsed posts have parts of their contents hidden to take up less screen space. This is distinct from the Content Warning feature' /></span>
+        </LocalSettingsPageItem>
+        <LocalSettingsPageItem
+          settings={settings}
+          item={['collapsed', 'show_action_bar']}
+          id='mastodon-settings--collapsed-show-action-bar'
+          onChange={onChange}
+          dependsOn={[['collapsed', 'enabled']]}
+        >
+          <FormattedMessage id='settings.show_action_bar' defaultMessage='Show action buttons in collapsed toots' />
+        </LocalSettingsPageItem>
+        <section>
+          <h2><FormattedMessage id='settings.auto_collapse' defaultMessage='Automatic collapsing' /></h2>
+          <LocalSettingsPageItem
+            settings={settings}
+            item={['collapsed', 'auto', 'all']}
+            id='mastodon-settings--collapsed-auto-all'
+            onChange={onChange}
+            dependsOn={[['collapsed', 'enabled']]}
+          >
+            <FormattedMessage id='settings.auto_collapse_all' defaultMessage='Everything' />
+          </LocalSettingsPageItem>
+          <LocalSettingsPageItem
+            settings={settings}
+            item={['collapsed', 'auto', 'notifications']}
+            id='mastodon-settings--collapsed-auto-notifications'
+            onChange={onChange}
+            dependsOn={[['collapsed', 'enabled']]}
+            dependsOnNot={[['collapsed', 'auto', 'all']]}
+          >
+            <FormattedMessage id='settings.auto_collapse_notifications' defaultMessage='Notifications' />
+          </LocalSettingsPageItem>
+          <LocalSettingsPageItem
+            settings={settings}
+            item={['collapsed', 'auto', 'lengthy']}
+            id='mastodon-settings--collapsed-auto-lengthy'
+            onChange={onChange}
+            dependsOn={[['collapsed', 'enabled']]}
+            dependsOnNot={[['collapsed', 'auto', 'all']]}
+          >
+            <FormattedMessage id='settings.auto_collapse_lengthy' defaultMessage='Lengthy toots' />
+          </LocalSettingsPageItem>
+          <LocalSettingsPageItem
+            settings={settings}
+            item={['collapsed', 'auto', 'reblogs']}
+            id='mastodon-settings--collapsed-auto-reblogs'
+            onChange={onChange}
+            dependsOn={[['collapsed', 'enabled']]}
+            dependsOnNot={[['collapsed', 'auto', 'all']]}
+          >
+            <FormattedMessage id='settings.auto_collapse_reblogs' defaultMessage='Boosts' />
+          </LocalSettingsPageItem>
+          <LocalSettingsPageItem
+            settings={settings}
+            item={['collapsed', 'auto', 'replies']}
+            id='mastodon-settings--collapsed-auto-replies'
+            onChange={onChange}
+            dependsOn={[['collapsed', 'enabled']]}
+            dependsOnNot={[['collapsed', 'auto', 'all']]}
+          >
+            <FormattedMessage id='settings.auto_collapse_replies' defaultMessage='Replies' />
+          </LocalSettingsPageItem>
+          <LocalSettingsPageItem
+            settings={settings}
+            item={['collapsed', 'auto', 'media']}
+            id='mastodon-settings--collapsed-auto-media'
+            onChange={onChange}
+            dependsOn={[['collapsed', 'enabled']]}
+            dependsOnNot={[['collapsed', 'auto', 'all']]}
+          >
+            <FormattedMessage id='settings.auto_collapse_media' defaultMessage='Toots with media' />
+          </LocalSettingsPageItem>
+          <LocalSettingsPageItem
+            settings={settings}
+            item={['collapsed', 'auto', 'height']}
+            id='mastodon-settings--collapsed-auto-height'
+            placeholder='400'
+            onChange={onChange}
+            dependsOn={[['collapsed', 'enabled']]}
+            dependsOnNot={[['collapsed', 'auto', 'all']]}
+            inputProps={{ type: 'number', min: '200', max: '999' }}
+          >
+            <FormattedMessage id='settings.auto_collapse_height' defaultMessage='Height (in pixels) for a toot to be considered lengthy' />
+          </LocalSettingsPageItem>
+        </section>
+        <section>
+          <h2><FormattedMessage id='settings.image_backgrounds' defaultMessage='Image backgrounds' /></h2>
+          <LocalSettingsPageItem
+            settings={settings}
+            item={['collapsed', 'backgrounds', 'user_backgrounds']}
+            id='mastodon-settings--collapsed-user-backgrouns'
+            onChange={onChange}
+            dependsOn={[['collapsed', 'enabled']]}
+          >
+            <FormattedMessage id='settings.image_backgrounds_users' defaultMessage='Give collapsed toots an image background' />
+          </LocalSettingsPageItem>
+          <LocalSettingsPageItem
+            settings={settings}
+            item={['collapsed', 'backgrounds', 'preview_images']}
+            id='mastodon-settings--collapsed-preview-images'
+            onChange={onChange}
+            dependsOn={[['collapsed', 'enabled']]}
+          >
+            <FormattedMessage id='settings.image_backgrounds_media' defaultMessage='Preview collapsed toot media' />
+            <span className='hint'><FormattedMessage id='settings.image_backgrounds_media_hint' defaultMessage='If the post has any media attachment, use the first one as a background' /></span>
+          </LocalSettingsPageItem>
+        </section>
+      </div>
+    ),
+    ({ intl, onChange, settings }) => (
+      <div className='blobfox local-settings__page media'>
+        <h1><FormattedMessage id='settings.media' defaultMessage='Media' /></h1>
+        <LocalSettingsPageItem
+          settings={settings}
+          item={['media', 'letterbox']}
+          id='mastodon-settings--media-letterbox'
+          onChange={onChange}
+        >
+          <FormattedMessage id='settings.media_letterbox' defaultMessage='Letterbox media' />
+          <span className='hint'><FormattedMessage id='settings.media_letterbox_hint' defaultMessage='Scale down and letterbox media to fill the image containers instead of stretching and cropping them' /></span>
+        </LocalSettingsPageItem>
+        <LocalSettingsPageItem
+          settings={settings}
+          item={['media', 'fullwidth']}
+          id='mastodon-settings--media-fullwidth'
+          onChange={onChange}
+        >
+          <FormattedMessage id='settings.media_fullwidth' defaultMessage='Full-width media previews' />
+        </LocalSettingsPageItem>
+        <LocalSettingsPageItem
+          settings={settings}
+          item={['inline_preview_cards']}
+          id='mastodon-settings--inline-preview-cards'
+          onChange={onChange}
+        >
+          <FormattedMessage id='settings.inline_preview_cards' defaultMessage='Inline preview cards for external links' />
+        </LocalSettingsPageItem>
+        <LocalSettingsPageItem
+          settings={settings}
+          item={['media', 'reveal_behind_cw']}
+          id='mastodon-settings--reveal-behind-cw'
+          onChange={onChange}
+        >
+          <FormattedMessage id='settings.media_reveal_behind_cw' defaultMessage='Reveal sensitive media behind a CW by default' />
+        </LocalSettingsPageItem>
+        <LocalSettingsPageItem
+          settings={settings}
+          item={['media', 'pop_in_player']}
+          id='mastodon-settings--pop-in-player'
+          onChange={onChange}
+        >
+          <FormattedMessage id='settings.pop_in_player' defaultMessage='Enable pop-in player' />
+        </LocalSettingsPageItem>
+        <LocalSettingsPageItem
+          settings={settings}
+          item={['media', 'pop_in_position']}
+          id='mastodon-settings--pop-in-position'
+          options={[
+            { value: 'left', message: intl.formatMessage(messages.pop_in_left) },
+            { value: 'right', message: intl.formatMessage(messages.pop_in_right) },
+          ]}
+          onChange={onChange}
+          dependsOn={[['media', 'pop_in_player']]}
+        >
+          <FormattedMessage id='settings.pop_in_position' defaultMessage='Pop-in player position:' />
+        </LocalSettingsPageItem>
+      </div>
+    ),
+  ];
+
+  render () {
+    const { pages } = this;
+    const { index, intl, onChange, settings } = this.props;
+    const CurrentPage = pages[index] || pages[0];
+
+    return <CurrentPage intl={intl} onChange={onChange} settings={settings} />;
+  }
+
+}
+
+export default injectIntl(LocalSettingsPage);
diff --git a/app/javascript/flavours/blobfox/features/local_settings/page/item/index.jsx b/app/javascript/flavours/blobfox/features/local_settings/page/item/index.jsx
new file mode 100644
index 00000000000000..82f1c19088f8dc
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/local_settings/page/item/index.jsx
@@ -0,0 +1,118 @@
+//  Package imports
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+//  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+
+export default class LocalSettingsPageItem extends PureComponent {
+
+  static propTypes = {
+    children: PropTypes.node.isRequired,
+    dependsOn: PropTypes.array,
+    dependsOnNot: PropTypes.array,
+    id: PropTypes.string.isRequired,
+    item: PropTypes.array.isRequired,
+    onChange: PropTypes.func.isRequired,
+    inputProps: PropTypes.object,
+    options: PropTypes.arrayOf(PropTypes.shape({
+      value: PropTypes.string.isRequired,
+      message: PropTypes.string.isRequired,
+      hint: PropTypes.string,
+    })),
+    settings: ImmutablePropTypes.map.isRequired,
+    placeholder: PropTypes.string,
+    disabled: PropTypes.bool,
+  };
+
+  handleChange = e => {
+    const { target } = e;
+    const { item, onChange, options, placeholder } = this.props;
+    if (options && options.length > 0) onChange(item, target.value);
+    else if (placeholder) onChange(item, target.value);
+    else onChange(item, target.checked);
+  };
+
+  render () {
+    const { handleChange } = this;
+    const { settings, item, id, inputProps, options, children, dependsOn, dependsOnNot, placeholder, disabled } = this.props;
+    let enabled = !disabled;
+
+    if (dependsOn) {
+      for (let i = 0; i < dependsOn.length; i++) {
+        enabled = enabled && settings.getIn(dependsOn[i]);
+      }
+    }
+    if (dependsOnNot) {
+      for (let i = 0; i < dependsOnNot.length; i++) {
+        enabled = enabled && !settings.getIn(dependsOnNot[i]);
+      }
+    }
+
+    if (options && options.length > 0) {
+      const currentValue = settings.getIn(item);
+      const optionElems = options && options.length > 0 && options.map((opt) => {
+        let optionId = `${id}--${opt.value}`;
+        return (
+          <label key={optionId} htmlFor={optionId}>
+            <input
+              type='radio'
+              name={id}
+              id={optionId}
+              value={opt.value}
+              onBlur={handleChange}
+              onChange={handleChange}
+              checked={currentValue === opt.value}
+              disabled={!enabled}
+              {...inputProps}
+            />
+            {opt.message}
+            {opt.hint && <span className='hint'>{opt.hint}</span>}
+          </label>
+        );
+      });
+      return (
+        <div className='blobfox local-settings__page__item radio_buttons'>
+          <fieldset>
+            <legend>{children}</legend>
+            {optionElems}
+          </fieldset>
+        </div>
+      );
+    } else if (placeholder) {
+      return (
+        <div className='blobfox local-settings__page__item string'>
+          <label htmlFor={id}>
+            <p>{children}</p>
+            <p>
+              <input
+                id={id}
+                type='text'
+                value={settings.getIn(item)}
+                placeholder={placeholder}
+                onChange={handleChange}
+                disabled={!enabled}
+                {...inputProps}
+              />
+            </p>
+          </label>
+        </div>
+      );
+    } else return (
+      <div className='blobfox local-settings__page__item boolean'>
+        <label htmlFor={id}>
+          <input
+            id={id}
+            type='checkbox'
+            checked={settings.getIn(item)}
+            onChange={handleChange}
+            disabled={!enabled}
+            {...inputProps}
+          />
+          {children}
+        </label>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/mutes/index.jsx b/app/javascript/flavours/blobfox/features/mutes/index.jsx
new file mode 100644
index 00000000000000..947fe4c9b2712d
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/mutes/index.jsx
@@ -0,0 +1,88 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
+import { debounce } from 'lodash';
+
+import { fetchMutes, expandMutes } from '../../actions/mutes';
+import ColumnBackButtonSlim from '../../components/column_back_button_slim';
+import { LoadingIndicator } from '../../components/loading_indicator';
+import ScrollableList from '../../components/scrollable_list';
+import AccountContainer from '../../containers/account_container';
+import Column from '../ui/components/column';
+
+const messages = defineMessages({
+  heading: { id: 'column.mutes', defaultMessage: 'Muted users' },
+});
+
+const mapStateToProps = state => ({
+  accountIds: state.getIn(['user_lists', 'mutes', 'items']),
+  hasMore: !!state.getIn(['user_lists', 'mutes', 'next']),
+  isLoading: state.getIn(['user_lists', 'mutes', 'isLoading'], true),
+});
+
+class Mutes extends ImmutablePureComponent {
+
+  static propTypes = {
+    params: PropTypes.object.isRequired,
+    dispatch: PropTypes.func.isRequired,
+    hasMore: PropTypes.bool,
+    isLoading: PropTypes.bool,
+    accountIds: ImmutablePropTypes.list,
+    intl: PropTypes.object.isRequired,
+    multiColumn: PropTypes.bool,
+  };
+
+  UNSAFE_componentWillMount () {
+    this.props.dispatch(fetchMutes());
+  }
+
+  handleLoadMore = debounce(() => {
+    this.props.dispatch(expandMutes());
+  }, 300, { leading: true });
+
+  render () {
+    const { intl, hasMore, accountIds, multiColumn, isLoading } = this.props;
+
+    if (!accountIds) {
+      return (
+        <Column>
+          <LoadingIndicator />
+        </Column>
+      );
+    }
+
+    const emptyMessage = <FormattedMessage id='empty_column.mutes' defaultMessage="You haven't muted any users yet." />;
+
+    return (
+      <Column bindToDocument={!multiColumn} icon='volume-off' heading={intl.formatMessage(messages.heading)}>
+        <ColumnBackButtonSlim />
+        <ScrollableList
+          scrollKey='mutes'
+          onLoadMore={this.handleLoadMore}
+          hasMore={hasMore}
+          isLoading={isLoading}
+          emptyMessage={emptyMessage}
+          bindToDocument={!multiColumn}
+        >
+          {accountIds.map(id =>
+            <AccountContainer key={id} id={id} defaultAction='mute' />,
+          )}
+        </ScrollableList>
+
+        <Helmet>
+          <meta name='robots' content='noindex' />
+        </Helmet>
+      </Column>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(injectIntl(Mutes));
diff --git a/app/javascript/flavours/blobfox/features/notifications/components/admin_report.jsx b/app/javascript/flavours/blobfox/features/notifications/components/admin_report.jsx
new file mode 100644
index 00000000000000..b19b8d7453f01d
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/notifications/components/admin_report.jsx
@@ -0,0 +1,115 @@
+import PropTypes from 'prop-types';
+
+import { FormattedMessage } from 'react-intl';
+
+import classNames from 'classnames';
+import { withRouter } from 'react-router-dom';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import { HotKeys } from 'react-hotkeys';
+
+import { Icon } from 'flavours/blobfox/components/icon';
+import Permalink from 'flavours/blobfox/components/permalink';
+import { WithRouterPropTypes } from 'flavours/blobfox/utils/react_router';
+
+import NotificationOverlayContainer from '../containers/overlay_container';
+
+import Report from './report';
+
+class AdminReport extends ImmutablePureComponent {
+
+  static propTypes = {
+    hidden: PropTypes.bool,
+    id: PropTypes.string.isRequired,
+    account: ImmutablePropTypes.map.isRequired,
+    notification: ImmutablePropTypes.map.isRequired,
+    unread: PropTypes.bool,
+    report: ImmutablePropTypes.map.isRequired,
+    ...WithRouterPropTypes,
+  };
+
+  handleMoveUp = () => {
+    const { notification, onMoveUp } = this.props;
+    onMoveUp(notification.get('id'));
+  };
+
+  handleMoveDown = () => {
+    const { notification, onMoveDown } = this.props;
+    onMoveDown(notification.get('id'));
+  };
+
+  handleOpen = () => {
+    this.handleOpenProfile();
+  };
+
+  handleOpenProfile = () => {
+    const { history, notification } = this.props;
+    history.push(`/@${notification.getIn(['account', 'acct'])}`);
+  };
+
+  handleMention = e => {
+    e.preventDefault();
+
+    const { history, notification, onMention } = this.props;
+    onMention(notification.get('account'), history);
+  };
+
+  getHandlers () {
+    return {
+      moveUp: this.handleMoveUp,
+      moveDown: this.handleMoveDown,
+      open: this.handleOpen,
+      openProfile: this.handleOpenProfile,
+      mention: this.handleMention,
+      reply: this.handleMention,
+    };
+  }
+
+  render () {
+    const { account, notification, unread, report } = this.props;
+
+    if (!report) {
+      return null;
+    }
+
+    //  Links to the display name.
+    const displayName = account.get('display_name_html') || account.get('username');
+    const link = (
+      <bdi><Permalink
+        className='notification__display-name'
+        href={account.get('url')}
+        title={account.get('acct')}
+        to={`/@${account.get('acct')}`}
+        dangerouslySetInnerHTML={{ __html: displayName }}
+      /></bdi>
+    );
+
+    const targetAccount = report.get('target_account');
+    const targetDisplayNameHtml = { __html: targetAccount.get('display_name_html') };
+    const targetLink = <bdi><Permalink className='notification__display-name' href={targetAccount.get('url')} title={targetAccount.get('acct')} to={`/@${targetAccount.get('acct')}`} dangerouslySetInnerHTML={targetDisplayNameHtml} /></bdi>;
+
+    return (
+      <HotKeys handlers={this.getHandlers()}>
+        <div className={classNames('notification notification-admin-report focusable', { unread })} tabIndex={0}>
+          <div className='notification__message'>
+            <div className='notification__favourite-icon-wrapper'>
+              <Icon id='flag' fixedWidth />
+            </div>
+
+            <span title={notification.get('created_at')}>
+              <FormattedMessage id='notification.admin.report' defaultMessage='{name} reported {target}' values={{ name: link, target: targetLink }} />
+            </span>
+          </div>
+
+          <Report account={account} report={notification.get('report')} hidden={this.props.hidden} />
+          <NotificationOverlayContainer notification={notification} />
+        </div>
+      </HotKeys>
+    );
+  }
+
+}
+
+export default withRouter(AdminReport);
diff --git a/app/javascript/flavours/blobfox/features/notifications/components/admin_signup.jsx b/app/javascript/flavours/blobfox/features/notifications/components/admin_signup.jsx
new file mode 100644
index 00000000000000..39fe15a8aa2eb4
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/notifications/components/admin_signup.jsx
@@ -0,0 +1,108 @@
+import PropTypes from 'prop-types';
+
+import { FormattedMessage } from 'react-intl';
+
+import classNames from 'classnames';
+import { withRouter } from 'react-router-dom';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import { HotKeys } from 'react-hotkeys';
+
+import { Icon } from 'flavours/blobfox/components/icon';
+import Permalink from 'flavours/blobfox/components/permalink';
+import AccountContainer from 'flavours/blobfox/containers/account_container';
+import { WithRouterPropTypes } from 'flavours/blobfox/utils/react_router';
+
+import NotificationOverlayContainer from '../containers/overlay_container';
+
+class NotificationAdminSignup extends ImmutablePureComponent {
+
+  static propTypes = {
+    hidden: PropTypes.bool,
+    id: PropTypes.string.isRequired,
+    account: ImmutablePropTypes.map.isRequired,
+    notification: ImmutablePropTypes.map.isRequired,
+    unread: PropTypes.bool,
+    ...WithRouterPropTypes,
+  };
+
+  handleMoveUp = () => {
+    const { notification, onMoveUp } = this.props;
+    onMoveUp(notification.get('id'));
+  };
+
+  handleMoveDown = () => {
+    const { notification, onMoveDown } = this.props;
+    onMoveDown(notification.get('id'));
+  };
+
+  handleOpen = () => {
+    this.handleOpenProfile();
+  };
+
+  handleOpenProfile = () => {
+    const { history, notification } = this.props;
+    history.push(`/@${notification.getIn(['account', 'acct'])}`);
+  };
+
+  handleMention = e => {
+    e.preventDefault();
+
+    const { history, notification, onMention } = this.props;
+    onMention(notification.get('account'), history);
+  };
+
+  getHandlers () {
+    return {
+      moveUp: this.handleMoveUp,
+      moveDown: this.handleMoveDown,
+      open: this.handleOpen,
+      openProfile: this.handleOpenProfile,
+      mention: this.handleMention,
+      reply: this.handleMention,
+    };
+  }
+
+  render () {
+    const { account, notification, hidden, unread } = this.props;
+
+    //  Links to the display name.
+    const displayName = account.get('display_name_html') || account.get('username');
+    const link = (
+      <bdi><Permalink
+        className='notification__display-name'
+        href={account.get('url')}
+        title={account.get('acct')}
+        to={`/@${account.get('acct')}`}
+        dangerouslySetInnerHTML={{ __html: displayName }}
+      /></bdi>
+    );
+
+    //  Renders.
+    return (
+      <HotKeys handlers={this.getHandlers()}>
+        <div className={classNames('notification notification-admin-sign-up focusable', { unread })} tabIndex={0}>
+          <div className='notification__message'>
+            <div className='notification__favourite-icon-wrapper'>
+              <Icon fixedWidth id='user-plus' />
+            </div>
+
+            <FormattedMessage
+              id='notification.admin.sign_up'
+              defaultMessage='{name} signed up'
+              values={{ name: link }}
+            />
+          </div>
+
+          <AccountContainer hidden={hidden} id={account.get('id')} withNote={false} />
+          <NotificationOverlayContainer notification={notification} />
+        </div>
+      </HotKeys>
+    );
+  }
+
+}
+
+export default withRouter(NotificationAdminSignup);
diff --git a/app/javascript/flavours/blobfox/features/notifications/components/clear_column_button.jsx b/app/javascript/flavours/blobfox/features/notifications/components/clear_column_button.jsx
new file mode 100644
index 00000000000000..63121999227fe3
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/notifications/components/clear_column_button.jsx
@@ -0,0 +1,20 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import { Icon }  from 'flavours/blobfox/components/icon';
+
+export default class ClearColumnButton extends PureComponent {
+
+  static propTypes = {
+    onClick: PropTypes.func.isRequired,
+  };
+
+  render () {
+    return (
+      <button className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.props.onClick}><Icon id='eraser' /> <FormattedMessage id='notifications.clear' defaultMessage='Clear notifications' /></button>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/notifications/components/column_settings.jsx b/app/javascript/flavours/blobfox/features/notifications/components/column_settings.jsx
new file mode 100644
index 00000000000000..9b6ed0ff123542
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/notifications/components/column_settings.jsx
@@ -0,0 +1,218 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
+import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_REPORTS } from 'flavours/blobfox/permissions';
+
+import ClearColumnButton from './clear_column_button';
+import GrantPermissionButton from './grant_permission_button';
+import PillBarButton from './pill_bar_button';
+import SettingToggle from './setting_toggle';
+
+export default class ColumnSettings extends PureComponent {
+
+  static contextTypes = {
+    identity: PropTypes.object,
+  };
+
+  static propTypes = {
+    settings: ImmutablePropTypes.map.isRequired,
+    pushSettings: ImmutablePropTypes.map.isRequired,
+    onChange: PropTypes.func.isRequired,
+    onClear: PropTypes.func.isRequired,
+    onRequestNotificationPermission: PropTypes.func,
+    alertsEnabled: PropTypes.bool,
+    browserSupport: PropTypes.bool,
+    browserPermission: PropTypes.string,
+  };
+
+  onPushChange = (path, checked) => {
+    this.props.onChange(['push', ...path], checked);
+  };
+
+  render () {
+    const { settings, pushSettings, onChange, onClear, alertsEnabled, browserSupport, browserPermission, onRequestNotificationPermission } = this.props;
+
+    const unreadMarkersShowStr = <FormattedMessage id='notifications.column_settings.unread_notifications.highlight' defaultMessage='Highlight unread notifications' />;
+    const filterBarShowStr = <FormattedMessage id='notifications.column_settings.filter_bar.show_bar' defaultMessage='Show filter bar' />;
+    const filterAdvancedStr = <FormattedMessage id='notifications.column_settings.filter_bar.advanced' defaultMessage='Display all categories' />;
+    const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
+    const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />;
+    const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />;
+
+    const showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed');
+    const pushStr = showPushSettings && <FormattedMessage id='notifications.column_settings.push' defaultMessage='Push notifications' />;
+
+    return (
+      <div>
+        {alertsEnabled && browserSupport && browserPermission === 'denied' && (
+          <div className='column-settings__row column-settings__row--with-margin'>
+            <span className='warning-hint'><FormattedMessage id='notifications.permission_denied' defaultMessage='Desktop notifications are unavailable due to previously denied browser permissions request' /></span>
+          </div>
+        )}
+
+        {alertsEnabled && browserSupport && browserPermission === 'default' && (
+          <div className='column-settings__row column-settings__row--with-margin'>
+            <span className='warning-hint'>
+              <FormattedMessage id='notifications.permission_required' defaultMessage='Desktop notifications are unavailable because the required permission has not been granted.' /> <GrantPermissionButton onClick={onRequestNotificationPermission} />
+            </span>
+          </div>
+        )}
+
+        <div className='column-settings__row'>
+          <ClearColumnButton onClick={onClear} />
+        </div>
+
+        <div role='group' aria-labelledby='notifications-unread-markers'>
+          <span id='notifications-unread-markers' className='column-settings__section'>
+            <FormattedMessage id='notifications.column_settings.unread_notifications.category' defaultMessage='Unread notifications' />
+          </span>
+
+          <div className='column-settings__row'>
+            <SettingToggle id='unread-notification-markers' prefix='notifications' settings={settings} settingPath={['showUnread']} onChange={onChange} label={unreadMarkersShowStr} />
+          </div>
+        </div>
+
+        <div role='group' aria-labelledby='notifications-filter-bar'>
+          <span id='notifications-filter-bar' className='column-settings__section'>
+            <FormattedMessage id='notifications.column_settings.filter_bar.category' defaultMessage='Quick filter bar' />
+          </span>
+
+          <div className='column-settings__row'>
+            <SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'show']} onChange={onChange} label={filterBarShowStr} />
+            <SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'advanced']} onChange={onChange} label={filterAdvancedStr} />
+          </div>
+        </div>
+
+        <div role='group' aria-labelledby='notifications-follow'>
+          <span id='notifications-follow' className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span>
+
+          <div className='column-settings__pillbar'>
+            <PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'follow']} onChange={onChange} label={alertStr} />
+            {showPushSettings && <PillBarButton prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'follow']} onChange={this.onPushChange} label={pushStr} />}
+            <PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'follow']} onChange={onChange} label={showStr} />
+            <PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'follow']} onChange={onChange} label={soundStr} />
+          </div>
+        </div>
+
+        <div role='group' aria-labelledby='notifications-follow-request'>
+          <span id='notifications-follow-request' className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow_request' defaultMessage='New follow requests:' /></span>
+
+          <div className='column-settings__pillbar'>
+            <PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'follow_request']} onChange={onChange} label={alertStr} />
+            {showPushSettings && <PillBarButton prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'follow_request']} onChange={this.onPushChange} label={pushStr} />}
+            <PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'follow_request']} onChange={onChange} label={showStr} />
+            <PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'follow_request']} onChange={onChange} label={soundStr} />
+          </div>
+        </div>
+
+        <div role='group' aria-labelledby='notifications-favourite'>
+          <span id='notifications-favourite' className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favorites:' /></span>
+
+          <div className='column-settings__pillbar'>
+            <PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'favourite']} onChange={onChange} label={alertStr} />
+            {showPushSettings && <PillBarButton prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'favourite']} onChange={this.onPushChange} label={pushStr} />}
+            <PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'favourite']} onChange={onChange} label={showStr} />
+            <PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'favourite']} onChange={onChange} label={soundStr} />
+          </div>
+        </div>
+
+        <div role='group' aria-labelledby='notifications-reaction'>
+          <span id='notifications-reaction' className='column-settings__section'><FormattedMessage id='notifications.column_settings.reaction' defaultMessage='Reactions:' /></span>
+
+          <div className='column-settings__pillbar'>
+            <PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'reaction']} onChange={onChange} label={alertStr} />
+            {showPushSettings && <PillBarButton prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'reaction']} onChange={this.onPushChange} label={pushStr} />}
+            <PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'reaction']} onChange={onChange} label={showStr} />
+            <PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'reaction']} onChange={onChange} label={soundStr} />
+          </div>
+        </div>
+
+        <div role='group' aria-labelledby='notifications-mention'>
+          <span id='notifications-mention' className='column-settings__section'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span>
+
+          <div className='column-settings__pillbar'>
+            <PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'mention']} onChange={onChange} label={alertStr} />
+            {showPushSettings && <PillBarButton prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'mention']} onChange={this.onPushChange} label={pushStr} />}
+            <PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'mention']} onChange={onChange} label={showStr} />
+            <PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'mention']} onChange={onChange} label={soundStr} />
+          </div>
+        </div>
+
+        <div role='group' aria-labelledby='notifications-reblog'>
+          <span id='notifications-reblog' className='column-settings__section'><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span>
+
+          <div className='column-settings__pillbar'>
+            <PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'reblog']} onChange={onChange} label={alertStr} />
+            {showPushSettings && <PillBarButton prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'reblog']} onChange={this.onPushChange} label={pushStr} />}
+            <PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'reblog']} onChange={onChange} label={showStr} />
+            <PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'reblog']} onChange={onChange} label={soundStr} />
+          </div>
+        </div>
+
+        <div role='group' aria-labelledby='notifications-poll'>
+          <span id='notifications-poll' className='column-settings__section'><FormattedMessage id='notifications.column_settings.poll' defaultMessage='Poll results:' /></span>
+
+          <div className='column-settings__pillbar'>
+            <PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'poll']} onChange={onChange} label={alertStr} />
+            {showPushSettings && <PillBarButton prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'poll']} onChange={this.onPushChange} label={pushStr} />}
+            <PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'poll']} onChange={onChange} label={showStr} />
+            <PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'poll']} onChange={onChange} label={soundStr} />
+          </div>
+        </div>
+
+        <div role='group' aria-labelledby='notifications-status'>
+          <span id='notifications-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.status' defaultMessage='New posts:' /></span>
+
+          <div className='column-settings__pillbar'>
+            <PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'status']} onChange={onChange} label={alertStr} />
+            {showPushSettings && <PillBarButton prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'status']} onChange={this.onPushChange} label={pushStr} />}
+            <PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'status']} onChange={onChange} label={showStr} />
+            <PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'status']} onChange={onChange} label={soundStr} />
+          </div>
+        </div>
+
+        <div role='group' aria-labelledby='notifications-update'>
+          <span id='notifications-update' className='column-settings__section'><FormattedMessage id='notifications.column_settings.update' defaultMessage='Edits:' /></span>
+
+          <div className='column-settings__pillbar'>
+            <PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'update']} onChange={onChange} label={alertStr} />
+            {showPushSettings && <PillBarButton prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'update']} onChange={this.onPushChange} label={pushStr} />}
+            <PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'update']} onChange={onChange} label={showStr} />
+            <PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'update']} onChange={onChange} label={soundStr} />
+          </div>
+        </div>
+
+        {((this.context.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) && (
+          <div role='group' aria-labelledby='notifications-admin-sign-up'>
+            <span id='notifications-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.admin.sign_up' defaultMessage='New sign-ups:' /></span>
+
+            <div className='column-settings__pillbar'>
+              <PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'admin.sign_up']} onChange={onChange} label={alertStr} />
+              {showPushSettings && <PillBarButton prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'admin.sign_up']} onChange={this.onPushChange} label={pushStr} />}
+              <PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'admin.sign_up']} onChange={onChange} label={showStr} />
+              <PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'admin.sign_up']} onChange={onChange} label={soundStr} />
+            </div>
+          </div>
+        )}
+
+        {((this.context.identity.permissions & PERMISSION_MANAGE_REPORTS) === PERMISSION_MANAGE_REPORTS) && (
+          <div role='group' aria-labelledby='notifications-admin-report'>
+            <span id='notifications-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.admin.report' defaultMessage='New reports:' /></span>
+
+            <div className='column-settings__pillbar'>
+              <PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'admin.report']} onChange={onChange} label={alertStr} />
+              {showPushSettings && <PillBarButton prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'admin.report']} onChange={this.onPushChange} label={pushStr} />}
+              <PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'admin.report']} onChange={onChange} label={showStr} />
+              <PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'admin.report']} onChange={onChange} label={soundStr} />
+            </div>
+          </div>
+        )}
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/notifications/components/filter_bar.jsx b/app/javascript/flavours/blobfox/features/notifications/components/filter_bar.jsx
new file mode 100644
index 00000000000000..0bfbf8afa3c9d0
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/notifications/components/filter_bar.jsx
@@ -0,0 +1,121 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import { Icon }  from 'flavours/blobfox/components/icon';
+
+const tooltips = defineMessages({
+  mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' },
+  favourites: { id: 'notifications.filter.favourites', defaultMessage: 'Favorites' },
+  reactions: { id: 'notifications.filter.reactions', defaultMessage: 'Reactions' },
+  boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' },
+  polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' },
+  follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' },
+  statuses: { id: 'notifications.filter.statuses', defaultMessage: 'Updates from people you follow' },
+});
+
+class FilterBar extends PureComponent {
+
+  static propTypes = {
+    selectFilter: PropTypes.func.isRequired,
+    selectedFilter: PropTypes.string.isRequired,
+    advancedMode: PropTypes.bool.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  onClick (notificationType) {
+    return () => this.props.selectFilter(notificationType);
+  }
+
+  render () {
+    const { selectedFilter, advancedMode, intl } = this.props;
+    const renderedElement = !advancedMode ? (
+      <div className='notification__filter-bar'>
+        <button
+          className={selectedFilter === 'all' ? 'active' : ''}
+          onClick={this.onClick('all')}
+        >
+          <FormattedMessage
+            id='notifications.filter.all'
+            defaultMessage='All'
+          />
+        </button>
+        <button
+          className={selectedFilter === 'mention' ? 'active' : ''}
+          onClick={this.onClick('mention')}
+        >
+          <FormattedMessage
+            id='notifications.filter.mentions'
+            defaultMessage='Mentions'
+          />
+        </button>
+      </div>
+    ) : (
+      <div className='notification__filter-bar'>
+        <button
+          className={selectedFilter === 'all' ? 'active' : ''}
+          onClick={this.onClick('all')}
+        >
+          <FormattedMessage
+            id='notifications.filter.all'
+            defaultMessage='All'
+          />
+        </button>
+        <button
+          className={selectedFilter === 'mention' ? 'active' : ''}
+          onClick={this.onClick('mention')}
+          title={intl.formatMessage(tooltips.mentions)}
+        >
+          <Icon id='reply-all' fixedWidth />
+        </button>
+        <button
+          className={selectedFilter === 'favourite' ? 'active' : ''}
+          onClick={this.onClick('favourite')}
+          title={intl.formatMessage(tooltips.favourites)}
+        >
+          <Icon id='star' fixedWidth />
+        </button>
+        <button
+          className={selectedFilter === 'reaction' ? 'active' : ''}
+          onClick={this.onClick('reaction')}
+          title={intl.formatMessage(tooltips.reactions)}
+        >
+          <Icon id='plus' fixedWidth />
+        </button>
+        <button
+          className={selectedFilter === 'reblog' ? 'active' : ''}
+          onClick={this.onClick('reblog')}
+          title={intl.formatMessage(tooltips.boosts)}
+        >
+          <Icon id='retweet' fixedWidth />
+        </button>
+        <button
+          className={selectedFilter === 'poll' ? 'active' : ''}
+          onClick={this.onClick('poll')}
+          title={intl.formatMessage(tooltips.polls)}
+        >
+          <Icon id='tasks' fixedWidth />
+        </button>
+        <button
+          className={selectedFilter === 'status' ? 'active' : ''}
+          onClick={this.onClick('status')}
+          title={intl.formatMessage(tooltips.statuses)}
+        >
+          <Icon id='home' fixedWidth />
+        </button>
+        <button
+          className={selectedFilter === 'follow' ? 'active' : ''}
+          onClick={this.onClick('follow')}
+          title={intl.formatMessage(tooltips.follows)}
+        >
+          <Icon id='user-plus' fixedWidth />
+        </button>
+      </div>
+    );
+    return renderedElement;
+  }
+
+}
+
+export default injectIntl(FilterBar);
diff --git a/app/javascript/flavours/blobfox/features/notifications/components/follow.jsx b/app/javascript/flavours/blobfox/features/notifications/components/follow.jsx
new file mode 100644
index 00000000000000..2d04ab9d16ce10
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/notifications/components/follow.jsx
@@ -0,0 +1,108 @@
+import PropTypes from 'prop-types';
+
+import { FormattedMessage } from 'react-intl';
+
+import classNames from 'classnames';
+import { withRouter } from 'react-router-dom';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import { HotKeys } from 'react-hotkeys';
+
+import { Icon } from 'flavours/blobfox/components/icon';
+import Permalink from 'flavours/blobfox/components/permalink';
+import AccountContainer from 'flavours/blobfox/containers/account_container';
+import { WithRouterPropTypes } from 'flavours/blobfox/utils/react_router';
+
+import NotificationOverlayContainer from '../containers/overlay_container';
+
+class NotificationFollow extends ImmutablePureComponent {
+
+  static propTypes = {
+    hidden: PropTypes.bool,
+    id: PropTypes.string.isRequired,
+    account: ImmutablePropTypes.map.isRequired,
+    notification: ImmutablePropTypes.map.isRequired,
+    unread: PropTypes.bool,
+    ...WithRouterPropTypes,
+  };
+
+  handleMoveUp = () => {
+    const { notification, onMoveUp } = this.props;
+    onMoveUp(notification.get('id'));
+  };
+
+  handleMoveDown = () => {
+    const { notification, onMoveDown } = this.props;
+    onMoveDown(notification.get('id'));
+  };
+
+  handleOpen = () => {
+    this.handleOpenProfile();
+  };
+
+  handleOpenProfile = () => {
+    const { history, notification } = this.props;
+    history.push(`/@${notification.getIn(['account', 'acct'])}`);
+  };
+
+  handleMention = e => {
+    e.preventDefault();
+
+    const { history, notification, onMention } = this.props;
+    onMention(notification.get('account'), history);
+  };
+
+  getHandlers () {
+    return {
+      moveUp: this.handleMoveUp,
+      moveDown: this.handleMoveDown,
+      open: this.handleOpen,
+      openProfile: this.handleOpenProfile,
+      mention: this.handleMention,
+      reply: this.handleMention,
+    };
+  }
+
+  render () {
+    const { account, notification, hidden, unread } = this.props;
+
+    //  Links to the display name.
+    const displayName = account.get('display_name_html') || account.get('username');
+    const link = (
+      <bdi><Permalink
+        className='notification__display-name'
+        href={account.get('url')}
+        title={account.get('acct')}
+        to={`/@${account.get('acct')}`}
+        dangerouslySetInnerHTML={{ __html: displayName }}
+      /></bdi>
+    );
+
+    //  Renders.
+    return (
+      <HotKeys handlers={this.getHandlers()}>
+        <div className={classNames('notification notification-follow focusable', { unread })} tabIndex={0}>
+          <div className='notification__message'>
+            <div className='notification__favourite-icon-wrapper'>
+              <Icon fixedWidth id='user-plus' />
+            </div>
+
+            <FormattedMessage
+              id='notification.follow'
+              defaultMessage='{name} followed you'
+              values={{ name: link }}
+            />
+          </div>
+
+          <AccountContainer hidden={hidden} id={account.get('id')} withNote={false} />
+          <NotificationOverlayContainer notification={notification} />
+        </div>
+      </HotKeys>
+    );
+  }
+
+}
+
+export default withRouter(NotificationFollow);
diff --git a/app/javascript/flavours/blobfox/features/notifications/components/follow_request.jsx b/app/javascript/flavours/blobfox/features/notifications/components/follow_request.jsx
new file mode 100644
index 00000000000000..749169358b4110
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/notifications/components/follow_request.jsx
@@ -0,0 +1,141 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import classNames from 'classnames';
+import { withRouter } from 'react-router-dom';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import { HotKeys } from 'react-hotkeys';
+
+import { Avatar } from 'flavours/blobfox/components/avatar';
+import { DisplayName } from 'flavours/blobfox/components/display_name';
+import { Icon } from 'flavours/blobfox/components/icon';
+import { IconButton } from 'flavours/blobfox/components/icon_button';
+import Permalink from 'flavours/blobfox/components/permalink';
+import { WithRouterPropTypes } from 'flavours/blobfox/utils/react_router';
+
+import NotificationOverlayContainer from '../containers/overlay_container';
+
+const messages = defineMessages({
+  authorize: { id: 'follow_request.authorize', defaultMessage: 'Authorize' },
+  reject: { id: 'follow_request.reject', defaultMessage: 'Reject' },
+});
+
+class FollowRequest extends ImmutablePureComponent {
+
+  static propTypes = {
+    account: ImmutablePropTypes.record.isRequired,
+    onAuthorize: PropTypes.func.isRequired,
+    onReject: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+    notification: ImmutablePropTypes.map.isRequired,
+    unread: PropTypes.bool,
+    ...WithRouterPropTypes,
+  };
+
+  handleMoveUp = () => {
+    const { notification, onMoveUp } = this.props;
+    onMoveUp(notification.get('id'));
+  };
+
+  handleMoveDown = () => {
+    const { notification, onMoveDown } = this.props;
+    onMoveDown(notification.get('id'));
+  };
+
+  handleOpen = () => {
+    this.handleOpenProfile();
+  };
+
+  handleOpenProfile = () => {
+    const { history, notification } = this.props;
+    history.push(`/@${notification.getIn(['account', 'acct'])}`);
+  };
+
+  handleMention = e => {
+    e.preventDefault();
+
+    const { history, notification, onMention } = this.props;
+    onMention(notification.get('account'), history);
+  };
+
+  getHandlers () {
+    return {
+      moveUp: this.handleMoveUp,
+      moveDown: this.handleMoveDown,
+      open: this.handleOpen,
+      openProfile: this.handleOpenProfile,
+      mention: this.handleMention,
+      reply: this.handleMention,
+    };
+  }
+
+  render () {
+    const { intl, hidden, account, onAuthorize, onReject, notification, unread } = this.props;
+
+    if (!account) {
+      return <div />;
+    }
+
+    if (hidden) {
+      return (
+        <>
+          {account.get('display_name')}
+          {account.get('username')}
+        </>
+      );
+    }
+
+    //  Links to the display name.
+    const displayName = account.get('display_name_html') || account.get('username');
+    const link = (
+      <bdi><Permalink
+        className='notification__display-name'
+        href={account.get('url')}
+        title={account.get('acct')}
+        to={`/@${account.get('acct')}`}
+        dangerouslySetInnerHTML={{ __html: displayName }}
+      /></bdi>
+    );
+
+    return (
+      <HotKeys handlers={this.getHandlers()}>
+        <div className={classNames('notification notification-follow-request focusable', { unread })} tabIndex={0}>
+          <div className='notification__message'>
+            <div className='notification__favourite-icon-wrapper'>
+              <Icon id='user' fixedWidth />
+            </div>
+
+            <FormattedMessage
+              id='notification.follow_request'
+              defaultMessage='{name} has requested to follow you'
+              values={{ name: link }}
+            />
+          </div>
+
+          <div className='account'>
+            <div className='account__wrapper'>
+              <Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`}>
+                <div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
+                <DisplayName account={account} />
+              </Permalink>
+
+              <div className='account__relationship'>
+                <IconButton title={intl.formatMessage(messages.authorize)} icon='check' onClick={onAuthorize} />
+                <IconButton title={intl.formatMessage(messages.reject)} icon='times' onClick={onReject} />
+              </div>
+            </div>
+          </div>
+
+          <NotificationOverlayContainer notification={notification} />
+        </div>
+      </HotKeys>
+    );
+  }
+
+}
+
+export default withRouter(injectIntl(FollowRequest));
diff --git a/app/javascript/flavours/blobfox/features/notifications/components/grant_permission_button.jsx b/app/javascript/flavours/blobfox/features/notifications/components/grant_permission_button.jsx
new file mode 100644
index 00000000000000..cd46d878bbc2e0
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/notifications/components/grant_permission_button.jsx
@@ -0,0 +1,20 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+export default class GrantPermissionButton extends PureComponent {
+
+  static propTypes = {
+    onClick: PropTypes.func.isRequired,
+  };
+
+  render () {
+    return (
+      <button className='text-btn column-header__permission-btn' tabIndex={0} onClick={this.props.onClick}>
+        <FormattedMessage id='notifications.grant_permission' defaultMessage='Grant permission.' />
+      </button>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/notifications/components/notification.jsx b/app/javascript/flavours/blobfox/features/notifications/components/notification.jsx
new file mode 100644
index 00000000000000..146562563be009
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/notifications/components/notification.jsx
@@ -0,0 +1,258 @@
+//  Package imports.
+import PropTypes from 'prop-types';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+//  Our imports,
+import StatusContainer from 'flavours/blobfox/containers/status_container';
+
+import NotificationAdminReportContainer from '../containers/admin_report_container';
+import NotificationFollowRequestContainer from '../containers/follow_request_container';
+
+import NotificationAdminSignup from './admin_signup';
+import NotificationFollow from './follow';
+
+export default class Notification extends ImmutablePureComponent {
+
+  static propTypes = {
+    notification: ImmutablePropTypes.map.isRequired,
+    hidden: PropTypes.bool,
+    onMoveUp: PropTypes.func.isRequired,
+    onMoveDown: PropTypes.func.isRequired,
+    onMention: PropTypes.func.isRequired,
+    getScrollPosition: PropTypes.func,
+    updateScrollBottom: PropTypes.func,
+    cacheMediaWidth: PropTypes.func,
+    cachedMediaWidth: PropTypes.number,
+    onUnmount: PropTypes.func,
+    unread: PropTypes.bool,
+  };
+
+  render () {
+    const {
+      hidden,
+      notification,
+      onMoveDown,
+      onMoveUp,
+      onMention,
+      getScrollPosition,
+      updateScrollBottom,
+    } = this.props;
+
+    switch(notification.get('type')) {
+    case 'follow':
+      return (
+        <NotificationFollow
+          hidden={hidden}
+          id={notification.get('id')}
+          account={notification.get('account')}
+          notification={notification}
+          onMoveDown={onMoveDown}
+          onMoveUp={onMoveUp}
+          onMention={onMention}
+          unread={this.props.unread}
+        />
+      );
+    case 'follow_request':
+      return (
+        <NotificationFollowRequestContainer
+          hidden={hidden}
+          id={notification.get('id')}
+          account={notification.get('account')}
+          notification={notification}
+          onMoveDown={onMoveDown}
+          onMoveUp={onMoveUp}
+          onMention={onMention}
+          unread={this.props.unread}
+        />
+      );
+    case 'admin.sign_up':
+      return (
+        <NotificationAdminSignup
+          hidden={hidden}
+          id={notification.get('id')}
+          account={notification.get('account')}
+          notification={notification}
+          onMoveDown={onMoveDown}
+          onMoveUp={onMoveUp}
+          onMention={onMention}
+          unread={this.props.unread}
+        />
+      );
+    case 'admin.report':
+      return (
+        <NotificationAdminReportContainer
+          hidden={hidden}
+          id={notification.get('id')}
+          account={notification.get('account')}
+          notification={notification}
+          onMoveDown={onMoveDown}
+          onMoveUp={onMoveUp}
+          onMention={onMention}
+          unread={this.props.unread}
+        />
+      );
+    case 'mention':
+      return (
+        <StatusContainer
+          containerId={notification.get('id')}
+          hidden={hidden}
+          id={notification.get('status')}
+          notification={notification}
+          onMoveDown={onMoveDown}
+          onMoveUp={onMoveUp}
+          onMention={onMention}
+          contextType='notifications'
+          getScrollPosition={getScrollPosition}
+          updateScrollBottom={updateScrollBottom}
+          cachedMediaWidth={this.props.cachedMediaWidth}
+          cacheMediaWidth={this.props.cacheMediaWidth}
+          onUnmount={this.props.onUnmount}
+          withDismiss
+          unread={this.props.unread}
+        />
+      );
+    case 'status':
+      return (
+        <StatusContainer
+          containerId={notification.get('id')}
+          hidden={hidden}
+          id={notification.get('status')}
+          account={notification.get('account')}
+          prepend='status'
+          muted
+          notification={notification}
+          onMoveDown={onMoveDown}
+          onMoveUp={onMoveUp}
+          onMention={onMention}
+          contextType='notifications'
+          getScrollPosition={getScrollPosition}
+          updateScrollBottom={updateScrollBottom}
+          cachedMediaWidth={this.props.cachedMediaWidth}
+          cacheMediaWidth={this.props.cacheMediaWidth}
+          onUnmount={this.props.onUnmount}
+          withDismiss
+          unread={this.props.unread}
+        />
+      );
+    case 'favourite':
+      return (
+        <StatusContainer
+          containerId={notification.get('id')}
+          hidden={hidden}
+          id={notification.get('status')}
+          account={notification.get('account')}
+          prepend='favourite'
+          muted
+          notification={notification}
+          onMoveDown={onMoveDown}
+          onMoveUp={onMoveUp}
+          onMention={onMention}
+          contextType='notifications'
+          getScrollPosition={getScrollPosition}
+          updateScrollBottom={updateScrollBottom}
+          cachedMediaWidth={this.props.cachedMediaWidth}
+          cacheMediaWidth={this.props.cacheMediaWidth}
+          onUnmount={this.props.onUnmount}
+          withDismiss
+          unread={this.props.unread}
+        />
+      );
+    case 'reaction':
+      return (
+        <StatusContainer
+          containerId={notification.get('id')}
+          hidden={hidden}
+          id={notification.get('status')}
+          account={notification.get('account')}
+          prepend='reaction'
+          muted
+          notification={notification}
+          onMoveDown={onMoveDown}
+          onMoveUp={onMoveUp}
+          onMention={onMention}
+          getScrollPosition={getScrollPosition}
+          updateScrollBottom={updateScrollBottom}
+          cachedMediaWidth={this.props.cachedMediaWidth}
+          cacheMediaWidth={this.props.cacheMediaWidth}
+          onUnmount={this.props.onUnmount}
+          withDismiss
+          unread={this.props.unread}
+        />
+      );
+    case 'reblog':
+      return (
+        <StatusContainer
+          containerId={notification.get('id')}
+          hidden={hidden}
+          id={notification.get('status')}
+          account={notification.get('account')}
+          prepend='reblog'
+          muted
+          notification={notification}
+          onMoveDown={onMoveDown}
+          onMoveUp={onMoveUp}
+          onMention={onMention}
+          contextType='notifications'
+          getScrollPosition={getScrollPosition}
+          updateScrollBottom={updateScrollBottom}
+          cachedMediaWidth={this.props.cachedMediaWidth}
+          cacheMediaWidth={this.props.cacheMediaWidth}
+          onUnmount={this.props.onUnmount}
+          withDismiss
+          unread={this.props.unread}
+        />
+      );
+    case 'poll':
+      return (
+        <StatusContainer
+          containerId={notification.get('id')}
+          hidden={hidden}
+          id={notification.get('status')}
+          account={notification.get('account')}
+          prepend='poll'
+          muted
+          notification={notification}
+          onMoveDown={onMoveDown}
+          onMoveUp={onMoveUp}
+          onMention={onMention}
+          contextType='notifications'
+          getScrollPosition={getScrollPosition}
+          updateScrollBottom={updateScrollBottom}
+          cachedMediaWidth={this.props.cachedMediaWidth}
+          cacheMediaWidth={this.props.cacheMediaWidth}
+          onUnmount={this.props.onUnmount}
+          withDismiss
+          unread={this.props.unread}
+        />
+      );
+    case 'update':
+      return (
+        <StatusContainer
+          containerId={notification.get('id')}
+          hidden={hidden}
+          id={notification.get('status')}
+          account={notification.get('account')}
+          prepend='update'
+          muted
+          notification={notification}
+          onMoveDown={onMoveDown}
+          onMoveUp={onMoveUp}
+          onMention={onMention}
+          contextType='notifications'
+          getScrollPosition={getScrollPosition}
+          updateScrollBottom={updateScrollBottom}
+          cachedMediaWidth={this.props.cachedMediaWidth}
+          cacheMediaWidth={this.props.cacheMediaWidth}
+          onUnmount={this.props.onUnmount}
+          withDismiss
+          unread={this.props.unread}
+        />
+      );
+    default:
+      return null;
+    }
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/notifications/components/notifications_permission_banner.jsx b/app/javascript/flavours/blobfox/features/notifications/components/notifications_permission_banner.jsx
new file mode 100644
index 00000000000000..ea45de786f8d6c
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/notifications/components/notifications_permission_banner.jsx
@@ -0,0 +1,51 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import { connect } from 'react-redux';
+
+import { requestBrowserPermission } from 'flavours/blobfox/actions/notifications';
+import { changeSetting } from 'flavours/blobfox/actions/settings';
+import { Button } from 'flavours/blobfox/components/button';
+import { Icon }  from 'flavours/blobfox/components/icon';
+import { IconButton } from 'flavours/blobfox/components/icon_button';
+
+const messages = defineMessages({
+  close: { id: 'lightbox.close', defaultMessage: 'Close' },
+});
+
+class NotificationsPermissionBanner extends PureComponent {
+
+  static propTypes = {
+    dispatch: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  handleClick = () => {
+    this.props.dispatch(requestBrowserPermission());
+  };
+
+  handleClose = () => {
+    this.props.dispatch(changeSetting(['notifications', 'dismissPermissionBanner'], true));
+  };
+
+  render () {
+    const { intl } = this.props;
+
+    return (
+      <div className='notifications-permission-banner'>
+        <div className='notifications-permission-banner__close'>
+          <IconButton icon='times' onClick={this.handleClose} title={intl.formatMessage(messages.close)} />
+        </div>
+
+        <h2><FormattedMessage id='notifications_permission_banner.title' defaultMessage='Never miss a thing' /></h2>
+        <p><FormattedMessage id='notifications_permission_banner.how_to_control' defaultMessage="To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled." values={{ icon: <Icon id='sliders' /> }} /></p>
+        <Button onClick={this.handleClick}><FormattedMessage id='notifications_permission_banner.enable' defaultMessage='Enable desktop notifications' /></Button>
+      </div>
+    );
+  }
+
+}
+
+export default connect()(injectIntl(NotificationsPermissionBanner));
diff --git a/app/javascript/flavours/blobfox/features/notifications/components/overlay.jsx b/app/javascript/flavours/blobfox/features/notifications/components/overlay.jsx
new file mode 100644
index 00000000000000..066f0647f31f06
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/notifications/components/overlay.jsx
@@ -0,0 +1,61 @@
+/**
+ * Notification overlay
+ */
+
+
+//  Package imports.
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import { Icon } from 'flavours/blobfox/components/icon';
+
+const messages = defineMessages({
+  markForDeletion: { id: 'notification.markForDeletion', defaultMessage: 'Mark for deletion' },
+});
+
+class NotificationOverlay extends ImmutablePureComponent {
+
+  static propTypes = {
+    notification    : ImmutablePropTypes.map.isRequired,
+    onMarkForDelete : PropTypes.func.isRequired,
+    show            : PropTypes.bool.isRequired,
+    intl            : PropTypes.object.isRequired,
+  };
+
+  onToggleMark = () => {
+    const mark = !this.props.notification.get('markedForDelete');
+    const id = this.props.notification.get('id');
+    this.props.onMarkForDelete(id, mark);
+  };
+
+  render () {
+    const { notification, show, intl } = this.props;
+
+    const active = notification.get('markedForDelete');
+    const label = intl.formatMessage(messages.markForDeletion);
+
+    return show ? (
+      <div
+        aria-label={label}
+        role='checkbox'
+        aria-checked={active}
+        tabIndex={0}
+        className={`notification__dismiss-overlay ${active ? 'active' : ''}`}
+        onClick={this.onToggleMark}
+      >
+        <div className='wrappy'>
+          <div className='ckbox' aria-hidden='true' title={label}>
+            {active ? (<Icon id='check' />) : ''}
+          </div>
+        </div>
+      </div>
+    ) : null;
+  }
+
+}
+
+export default injectIntl(NotificationOverlay);
diff --git a/app/javascript/flavours/blobfox/features/notifications/components/pill_bar_button.jsx b/app/javascript/flavours/blobfox/features/notifications/components/pill_bar_button.jsx
new file mode 100644
index 00000000000000..633401d6e3102b
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/notifications/components/pill_bar_button.jsx
@@ -0,0 +1,43 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import classNames from 'classnames';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
+export default class PillBarButton extends PureComponent {
+
+  static propTypes = {
+    prefix: PropTypes.string,
+    settings: ImmutablePropTypes.map.isRequired,
+    settingPath: PropTypes.array.isRequired,
+    label: PropTypes.node.isRequired,
+    onChange: PropTypes.func.isRequired,
+    disabled: PropTypes.bool,
+  };
+
+  onChange = () => {
+    const { settings, settingPath } = this.props;
+    this.props.onChange(settingPath, !settings.getIn(settingPath));
+  };
+
+  render () {
+    const { prefix, settings, settingPath, label, disabled } = this.props;
+    const id = ['setting-pillbar-button', prefix, ...settingPath].filter(Boolean).join('-');
+    const active = settings.getIn(settingPath);
+
+    return (
+      <button
+        key={id}
+        id={id}
+        className={classNames('pillbar-button', { active })}
+        disabled={disabled}
+        onClick={this.onChange}
+        aria-pressed={active}
+      >
+        {label}
+      </button>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/notifications/components/report.jsx b/app/javascript/flavours/blobfox/features/notifications/components/report.jsx
new file mode 100644
index 00000000000000..63b4f18a98f028
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/notifications/components/report.jsx
@@ -0,0 +1,67 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import { AvatarOverlay } from 'flavours/blobfox/components/avatar_overlay';
+import { RelativeTimestamp } from 'flavours/blobfox/components/relative_timestamp';
+
+// This needs to be kept in sync with app/models/report.rb
+const messages = defineMessages({
+  openReport: { id: 'report_notification.open', defaultMessage: 'Open report' },
+  other: { id: 'report_notification.categories.other', defaultMessage: 'Other' },
+  spam: { id: 'report_notification.categories.spam', defaultMessage: 'Spam' },
+  legal: { id: 'report_notification.categories.legal', defaultMessage: 'Legal' },
+  violation: { id: 'report_notification.categories.violation', defaultMessage: 'Rule violation' },
+});
+
+class Report extends ImmutablePureComponent {
+
+  static propTypes = {
+    account: ImmutablePropTypes.record.isRequired,
+    report: ImmutablePropTypes.map.isRequired,
+    hidden: PropTypes.bool,
+    intl: PropTypes.object.isRequired,
+  };
+
+  render () {
+    const { intl, hidden, report, account } = this.props;
+
+    if (!report) {
+      return null;
+    }
+
+    if (hidden) {
+      return (
+        <>
+          {report.get('id')}
+        </>
+      );
+    }
+
+    return (
+      <div className='notification__report'>
+        <div className='notification__report__avatar'>
+          <AvatarOverlay account={report.get('target_account')} friend={account} />
+        </div>
+
+        <div className='notification__report__details'>
+          <div>
+            <RelativeTimestamp timestamp={report.get('created_at')} short={false} /> · <FormattedMessage id='report_notification.attached_statuses' defaultMessage='{count, plural, one {# post} other {# posts}} attached' values={{ count: report.get('status_ids').size }} />
+            <br />
+            <strong>{intl.formatMessage(messages[report.get('category')])}</strong>
+          </div>
+
+          <div className='notification__report__actions'>
+            <a href={`/admin/reports/${report.get('id')}`} className='button' target='_blank' rel='noopener noreferrer'>{intl.formatMessage(messages.openReport)}</a>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(Report);
diff --git a/app/javascript/flavours/blobfox/features/notifications/components/setting_toggle.jsx b/app/javascript/flavours/blobfox/features/notifications/components/setting_toggle.jsx
new file mode 100644
index 00000000000000..2f849c54841031
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/notifications/components/setting_toggle.jsx
@@ -0,0 +1,38 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
+import Toggle from 'react-toggle';
+
+export default class SettingToggle extends PureComponent {
+
+  static propTypes = {
+    prefix: PropTypes.string,
+    settings: ImmutablePropTypes.map.isRequired,
+    settingPath: PropTypes.array.isRequired,
+    label: PropTypes.node.isRequired,
+    meta: PropTypes.node,
+    onChange: PropTypes.func.isRequired,
+    defaultValue: PropTypes.bool,
+    disabled: PropTypes.bool,
+  };
+
+  onChange = ({ target }) => {
+    this.props.onChange(this.props.settingPath, target.checked);
+  };
+
+  render () {
+    const { prefix, settings, settingPath, label, meta, defaultValue, disabled } = this.props;
+    const id = ['setting-toggle', prefix, ...settingPath].filter(Boolean).join('-');
+
+    return (
+      <div className='setting-toggle'>
+        <Toggle disabled={disabled} id={id} checked={settings.getIn(settingPath, defaultValue)} onChange={this.onChange} onKeyDown={this.onKeyDown} />
+        <label htmlFor={id} className='setting-toggle__label'>{label}</label>
+        {meta && <span className='setting-meta__label'>{meta}</span>}
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/notifications/containers/admin_report_container.js b/app/javascript/flavours/blobfox/features/notifications/containers/admin_report_container.js
new file mode 100644
index 00000000000000..27a8450a750a24
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/notifications/containers/admin_report_container.js
@@ -0,0 +1,15 @@
+import { connect } from 'react-redux';
+
+import { makeGetReport } from 'flavours/blobfox/selectors';
+
+import AdminReport from '../components/admin_report';
+
+const mapStateToProps = (state, { notification }) => {
+  const getReport = makeGetReport();
+
+  return {
+    report: notification.get('report') ? getReport(state, notification.get('report'), notification.getIn(['report', 'target_account', 'id'])) : null,
+  };
+};
+
+export default connect(mapStateToProps)(AdminReport);
diff --git a/app/javascript/flavours/blobfox/features/notifications/containers/column_settings_container.js b/app/javascript/flavours/blobfox/features/notifications/containers/column_settings_container.js
new file mode 100644
index 00000000000000..1e62ed9a5a45b6
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/notifications/containers/column_settings_container.js
@@ -0,0 +1,78 @@
+import { defineMessages, injectIntl } from 'react-intl';
+
+import { connect } from 'react-redux';
+
+import { showAlert } from '../../../actions/alerts';
+import { openModal } from '../../../actions/modal';
+import { setFilter, clearNotifications, requestBrowserPermission } from '../../../actions/notifications';
+import { changeAlerts as changePushNotifications } from '../../../actions/push_notifications';
+import { changeSetting } from '../../../actions/settings';
+import ColumnSettings from '../components/column_settings';
+
+const messages = defineMessages({
+  clearMessage: { id: 'notifications.clear_confirmation', defaultMessage: 'Are you sure you want to permanently clear all your notifications?' },
+  clearConfirm: { id: 'notifications.clear', defaultMessage: 'Clear notifications' },
+  permissionDenied: { id: 'notifications.permission_denied_alert', defaultMessage: 'Desktop notifications can\'t be enabled, as browser permission has been denied before' },
+});
+
+const mapStateToProps = state => ({
+  settings: state.getIn(['settings', 'notifications']),
+  pushSettings: state.get('push_notifications'),
+  alertsEnabled: state.getIn(['settings', 'notifications', 'alerts']).includes(true),
+  browserSupport: state.getIn(['notifications', 'browserSupport']),
+  browserPermission: state.getIn(['notifications', 'browserPermission']),
+});
+
+const mapDispatchToProps = (dispatch, { intl }) => ({
+
+  onChange (path, checked) {
+    if (path[0] === 'push') {
+      if (checked && typeof window.Notification !== 'undefined' && Notification.permission !== 'granted') {
+        dispatch(requestBrowserPermission((permission) => {
+          if (permission === 'granted') {
+            dispatch(changePushNotifications(path.slice(1), checked));
+          } else {
+            dispatch(showAlert({ message: messages.permissionDenied }));
+          }
+        }));
+      } else {
+        dispatch(changePushNotifications(path.slice(1), checked));
+      }
+    } else if (path[0] === 'quickFilter') {
+      dispatch(changeSetting(['notifications', ...path], checked));
+      dispatch(setFilter('all'));
+    } else if (path[0] === 'alerts' && checked && typeof window.Notification !== 'undefined' && Notification.permission !== 'granted') {
+      if (checked && typeof window.Notification !== 'undefined' && Notification.permission !== 'granted') {
+        dispatch(requestBrowserPermission((permission) => {
+          if (permission === 'granted') {
+            dispatch(changeSetting(['notifications', ...path], checked));
+          } else {
+            dispatch(showAlert({ message: messages.permissionDenied }));
+          }
+        }));
+      } else {
+        dispatch(changeSetting(['notifications', ...path], checked));
+      }
+    } else {
+      dispatch(changeSetting(['notifications', ...path], checked));
+    }
+  },
+
+  onClear () {
+    dispatch(openModal({
+      modalType: 'CONFIRM',
+      modalProps: {
+        message: intl.formatMessage(messages.clearMessage),
+        confirm: intl.formatMessage(messages.clearConfirm),
+        onConfirm: () => dispatch(clearNotifications()),
+      },
+    }));
+  },
+
+  onRequestNotificationPermission () {
+    dispatch(requestBrowserPermission());
+  },
+
+});
+
+export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ColumnSettings));
diff --git a/app/javascript/flavours/blobfox/features/notifications/containers/filter_bar_container.js b/app/javascript/flavours/blobfox/features/notifications/containers/filter_bar_container.js
new file mode 100644
index 00000000000000..4e0184cef30534
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/notifications/containers/filter_bar_container.js
@@ -0,0 +1,17 @@
+import { connect } from 'react-redux';
+
+import { setFilter } from '../../../actions/notifications';
+import FilterBar from '../components/filter_bar';
+
+const makeMapStateToProps = state => ({
+  selectedFilter: state.getIn(['settings', 'notifications', 'quickFilter', 'active']),
+  advancedMode: state.getIn(['settings', 'notifications', 'quickFilter', 'advanced']),
+});
+
+const mapDispatchToProps = (dispatch) => ({
+  selectFilter (newActiveFilter) {
+    dispatch(setFilter(newActiveFilter));
+  },
+});
+
+export default connect(makeMapStateToProps, mapDispatchToProps)(FilterBar);
diff --git a/app/javascript/flavours/blobfox/features/notifications/containers/follow_request_container.js b/app/javascript/flavours/blobfox/features/notifications/containers/follow_request_container.js
new file mode 100644
index 00000000000000..ebe41844dc2697
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/notifications/containers/follow_request_container.js
@@ -0,0 +1,17 @@
+import { connect } from 'react-redux';
+
+import { authorizeFollowRequest, rejectFollowRequest } from 'flavours/blobfox/actions/accounts';
+
+import FollowRequest from '../components/follow_request';
+
+const mapDispatchToProps = (dispatch, { account }) => ({
+  onAuthorize () {
+    dispatch(authorizeFollowRequest(account.get('id')));
+  },
+
+  onReject () {
+    dispatch(rejectFollowRequest(account.get('id')));
+  },
+});
+
+export default connect(null, mapDispatchToProps)(FollowRequest);
diff --git a/app/javascript/flavours/blobfox/features/notifications/containers/notification_container.js b/app/javascript/flavours/blobfox/features/notifications/containers/notification_container.js
new file mode 100644
index 00000000000000..3a507128983b81
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/notifications/containers/notification_container.js
@@ -0,0 +1,25 @@
+import { connect } from 'react-redux';
+
+import { mentionCompose } from 'flavours/blobfox/actions/compose';
+import { makeGetNotification } from 'flavours/blobfox/selectors';
+
+import Notification from '../components/notification';
+
+const makeMapStateToProps = () => {
+  const getNotification = makeGetNotification();
+
+  const mapStateToProps = (state, props) => ({
+    notification: getNotification(state, props.notification, props.accountId),
+    notifCleaning: state.getIn(['notifications', 'cleaningMode']),
+  });
+
+  return mapStateToProps;
+};
+
+const mapDispatchToProps = dispatch => ({
+  onMention: (account, history) => {
+    dispatch(mentionCompose(account, history));
+  },
+});
+
+export default connect(makeMapStateToProps, mapDispatchToProps)(Notification);
diff --git a/app/javascript/flavours/blobfox/features/notifications/containers/overlay_container.js b/app/javascript/flavours/blobfox/features/notifications/containers/overlay_container.js
new file mode 100644
index 00000000000000..145efb3663f6d4
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/notifications/containers/overlay_container.js
@@ -0,0 +1,19 @@
+//  Package imports.
+import { connect } from 'react-redux';
+
+//  Our imports.
+import { markNotificationForDelete } from 'flavours/blobfox/actions/notifications';
+
+import NotificationOverlay from '../components/overlay';
+
+const mapDispatchToProps = dispatch => ({
+  onMarkForDelete(id, yes) {
+    dispatch(markNotificationForDelete(id, yes));
+  },
+});
+
+const mapStateToProps = state => ({
+  show: state.getIn(['notifications', 'cleaningMode']),
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(NotificationOverlay);
diff --git a/app/javascript/flavours/blobfox/features/notifications/index.jsx b/app/javascript/flavours/blobfox/features/notifications/index.jsx
new file mode 100644
index 00000000000000..c0cca9debcd56c
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/notifications/index.jsx
@@ -0,0 +1,368 @@
+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 { List as ImmutableList } from 'immutable';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+
+import { debounce } from 'lodash';
+
+import { compareId } from 'flavours/blobfox/compare_id';
+import { Icon }  from 'flavours/blobfox/components/icon';
+import { NotSignedInIndicator } from 'flavours/blobfox/components/not_signed_in_indicator';
+
+import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
+import { submitMarkers } from '../../actions/markers';
+import {
+  enterNotificationClearingMode,
+  expandNotifications,
+  scrollTopNotifications,
+  loadPending,
+  mountNotifications,
+  unmountNotifications,
+  markNotificationsAsRead,
+} from '../../actions/notifications';
+import Column from '../../components/column';
+import ColumnHeader from '../../components/column_header';
+import { LoadGap } from '../../components/load_gap';
+import ScrollableList from '../../components/scrollable_list';
+import NotificationPurgeButtonsContainer from '../../containers/notification_purge_buttons_container';
+
+import NotificationsPermissionBanner from './components/notifications_permission_banner';
+import ColumnSettingsContainer from './containers/column_settings_container';
+import FilterBarContainer from './containers/filter_bar_container';
+import NotificationContainer from './containers/notification_container';
+
+const messages = defineMessages({
+  title: { id: 'column.notifications', defaultMessage: 'Notifications' },
+  enterNotifCleaning : { id: 'notification_purge.start', defaultMessage: 'Enter notification cleaning mode' },
+  markAsRead : { id: 'notifications.mark_as_read', defaultMessage: 'Mark every notification as read' },
+});
+
+const getExcludedTypes = createSelector([
+  state => state.getIn(['settings', 'notifications', 'shows']),
+], (shows) => {
+  return ImmutableList(shows.filter(item => !item).keys());
+});
+
+const getNotifications = createSelector([
+  state => state.getIn(['settings', 'notifications', 'quickFilter', 'show']),
+  state => state.getIn(['settings', 'notifications', 'quickFilter', 'active']),
+  getExcludedTypes,
+  state => state.getIn(['notifications', 'items']),
+], (showFilterBar, allowedType, excludedTypes, notifications) => {
+  if (!showFilterBar || allowedType === 'all') {
+    // used if user changed the notification settings after loading the notifications from the server
+    // otherwise a list of notifications will come pre-filtered from the backend
+    // we need to turn it off for FilterBar in order not to block ourselves from seeing a specific category
+    return notifications.filterNot(item => item !== null && excludedTypes.includes(item.get('type')));
+  }
+  return notifications.filter(item => item === null || allowedType === item.get('type'));
+});
+
+const mapStateToProps = state => ({
+  showFilterBar: state.getIn(['settings', 'notifications', 'quickFilter', 'show']),
+  notifications: getNotifications(state),
+  localSettings:  state.get('local_settings'),
+  isLoading: state.getIn(['notifications', 'isLoading'], 0) > 0,
+  isUnread: state.getIn(['notifications', 'unread']) > 0 || state.getIn(['notifications', 'pendingItems']).size > 0,
+  hasMore: state.getIn(['notifications', 'hasMore']),
+  numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size,
+  notifCleaningActive: state.getIn(['notifications', 'cleaningMode']),
+  lastReadId: state.getIn(['settings', 'notifications', 'showUnread']) ? state.getIn(['notifications', 'readMarkerId']) : '0',
+  canMarkAsRead: state.getIn(['settings', 'notifications', 'showUnread']) && state.getIn(['notifications', 'readMarkerId']) !== '0' && getNotifications(state).some(item => item !== null && compareId(item.get('id'), state.getIn(['notifications', 'readMarkerId'])) > 0),
+  needsNotificationPermission: state.getIn(['settings', 'notifications', 'alerts']).includes(true) && state.getIn(['notifications', 'browserSupport']) && state.getIn(['notifications', 'browserPermission']) === 'default' && !state.getIn(['settings', 'notifications', 'dismissPermissionBanner']),
+});
+
+/* blobfox */
+const mapDispatchToProps = dispatch => ({
+  onEnterCleaningMode(yes) {
+    dispatch(enterNotificationClearingMode(yes));
+  },
+  dispatch,
+});
+
+class Notifications extends PureComponent {
+
+  static contextTypes = {
+    identity: PropTypes.object,
+  };
+
+  static propTypes = {
+    columnId: PropTypes.string,
+    notifications: ImmutablePropTypes.list.isRequired,
+    showFilterBar: PropTypes.bool.isRequired,
+    dispatch: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+    isLoading: PropTypes.bool,
+    isUnread: PropTypes.bool,
+    multiColumn: PropTypes.bool,
+    hasMore: PropTypes.bool,
+    numPending: PropTypes.number,
+    localSettings: ImmutablePropTypes.map,
+    notifCleaningActive: PropTypes.bool,
+    onEnterCleaningMode: PropTypes.func,
+    lastReadId: PropTypes.string,
+    canMarkAsRead: PropTypes.bool,
+    needsNotificationPermission: PropTypes.bool,
+  };
+
+  static defaultProps = {
+    trackScroll: true,
+  };
+
+  state = {
+    animatingNCD: false,
+  };
+
+  componentDidMount() {
+    this.props.dispatch(mountNotifications());
+  }
+
+  componentWillUnmount () {
+    this.handleLoadOlder.cancel();
+    this.handleScrollToTop.cancel();
+    this.handleScroll.cancel();
+    // this.props.dispatch(scrollTopNotifications(false));
+    this.props.dispatch(unmountNotifications());
+  }
+
+  handleLoadGap = (maxId) => {
+    this.props.dispatch(expandNotifications({ maxId }));
+  };
+
+  handleLoadOlder = debounce(() => {
+    const last = this.props.notifications.last();
+    this.props.dispatch(expandNotifications({ maxId: last && last.get('id') }));
+  }, 300, { leading: true });
+
+  handleLoadPending = () => {
+    this.props.dispatch(loadPending());
+  };
+
+  handleScrollToTop = debounce(() => {
+    this.props.dispatch(scrollTopNotifications(true));
+  }, 100);
+
+  handleScroll = debounce(() => {
+    this.props.dispatch(scrollTopNotifications(false));
+  }, 100);
+
+  handlePin = () => {
+    const { columnId, dispatch } = this.props;
+
+    if (columnId) {
+      dispatch(removeColumn(columnId));
+    } else {
+      dispatch(addColumn('NOTIFICATIONS', {}));
+    }
+  };
+
+  handleMove = (dir) => {
+    const { columnId, dispatch } = this.props;
+    dispatch(moveColumn(columnId, dir));
+  };
+
+  handleHeaderClick = () => {
+    this.column.scrollTop();
+  };
+
+  setColumnRef = c => {
+    this.column = c;
+  };
+
+  handleMoveUp = id => {
+    const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) - 1;
+    this._selectChild(elementIndex, true);
+  };
+
+  handleMoveDown = id => {
+    const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) + 1;
+    this._selectChild(elementIndex, false);
+  };
+
+  _selectChild (index, align_top) {
+    const container = this.column.node;
+    const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
+
+    if (element) {
+      if (align_top && container.scrollTop > element.offsetTop) {
+        element.scrollIntoView(true);
+      } else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
+        element.scrollIntoView(false);
+      }
+      element.focus();
+    }
+  }
+
+  handleTransitionEndNCD = () => {
+    this.setState({ animatingNCD: false });
+  };
+
+  onEnterCleaningMode = () => {
+    this.setState({ animatingNCD: true });
+    this.props.onEnterCleaningMode(!this.props.notifCleaningActive);
+  };
+
+  handleMarkAsRead = () => {
+    this.props.dispatch(markNotificationsAsRead());
+    this.props.dispatch(submitMarkers({ immediate: true }));
+  };
+
+  render () {
+    const { intl, notifications, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar, lastReadId, canMarkAsRead, needsNotificationPermission } = this.props;
+    const { notifCleaningActive } = this.props;
+    const { animatingNCD } = this.state;
+    const pinned = !!columnId;
+    const emptyMessage = <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. When other people interact with you, you will see it here." />;
+    const { signedIn } = this.context.identity;
+
+    let scrollableContent = null;
+
+    const filterBarContainer = (signedIn && showFilterBar)
+      ? (<FilterBarContainer />)
+      : null;
+
+    if (isLoading && this.scrollableContent) {
+      scrollableContent = this.scrollableContent;
+    } else if (notifications.size > 0 || hasMore) {
+      scrollableContent = notifications.map((item, index) => item === null ? (
+        <LoadGap
+          key={'gap:' + notifications.getIn([index + 1, 'id'])}
+          disabled={isLoading}
+          maxId={index > 0 ? notifications.getIn([index - 1, 'id']) : null}
+          onClick={this.handleLoadGap}
+        />
+      ) : (
+        <NotificationContainer
+          key={item.get('id')}
+          notification={item}
+          accountId={item.get('account')}
+          onMoveUp={this.handleMoveUp}
+          onMoveDown={this.handleMoveDown}
+          unread={lastReadId !== '0' && compareId(item.get('id'), lastReadId) > 0}
+        />
+      ));
+    } else {
+      scrollableContent = null;
+    }
+
+    this.scrollableContent = scrollableContent;
+
+    let scrollContainer;
+
+    if (signedIn) {
+      scrollContainer = (
+        <ScrollableList
+          scrollKey={`notifications-${columnId}`}
+          trackScroll={!pinned}
+          isLoading={isLoading}
+          showLoading={isLoading && notifications.size === 0}
+          hasMore={hasMore}
+          numPending={numPending}
+          prepend={needsNotificationPermission && <NotificationsPermissionBanner />}
+          alwaysPrepend
+          emptyMessage={emptyMessage}
+          onLoadMore={this.handleLoadOlder}
+          onLoadPending={this.handleLoadPending}
+          onScrollToTop={this.handleScrollToTop}
+          onScroll={this.handleScroll}
+          bindToDocument={!multiColumn}
+        >
+          {scrollableContent}
+        </ScrollableList>
+      );
+    } else {
+      scrollContainer = <NotSignedInIndicator />;
+    }
+
+    const extraButtons = [];
+
+    if (canMarkAsRead) {
+      extraButtons.push(
+        <button
+          key='mark-as-read'
+          aria-label={intl.formatMessage(messages.markAsRead)}
+          title={intl.formatMessage(messages.markAsRead)}
+          onClick={this.handleMarkAsRead}
+          className='column-header__button'
+        >
+          <Icon id='check' />
+        </button>,
+      );
+    }
+
+    const notifCleaningButtonClassName = classNames('column-header__button', {
+      'active': notifCleaningActive,
+    });
+
+    const notifCleaningDrawerClassName = classNames('ncd column-header__collapsible', {
+      'collapsed': !notifCleaningActive,
+      'animating': animatingNCD,
+    });
+
+    const msgEnterNotifCleaning = intl.formatMessage(messages.enterNotifCleaning);
+
+    extraButtons.push(
+      <button
+        key='notif-cleaning'
+        aria-label={msgEnterNotifCleaning}
+        title={msgEnterNotifCleaning}
+        onClick={this.onEnterCleaningMode}
+        className={notifCleaningButtonClassName}
+      >
+        <Icon id='eraser' />
+      </button>,
+    );
+
+    const notifCleaningDrawer = (
+      <div className={notifCleaningDrawerClassName} onTransitionEnd={this.handleTransitionEndNCD}>
+        <div className='column-header__collapsible-inner nopad-drawer'>
+          {(notifCleaningActive || animatingNCD) ? (<NotificationPurgeButtonsContainer />) : null }
+        </div>
+      </div>
+    );
+
+    return (
+      <Column
+        bindToDocument={!multiColumn}
+        ref={this.setColumnRef}
+        extraClasses={this.props.notifCleaningActive ? 'notif-cleaning' : null}
+        label={intl.formatMessage(messages.title)}
+      >
+        <ColumnHeader
+          icon='bell'
+          active={isUnread}
+          title={intl.formatMessage(messages.title)}
+          onPin={this.handlePin}
+          onMove={this.handleMove}
+          onClick={this.handleHeaderClick}
+          pinned={pinned}
+          multiColumn={multiColumn}
+          localSettings={this.props.localSettings}
+          extraButton={extraButtons}
+          appendContent={notifCleaningDrawer}
+        >
+          <ColumnSettingsContainer />
+        </ColumnHeader>
+
+        {filterBarContainer}
+        {scrollContainer}
+
+        <Helmet>
+          <title>{intl.formatMessage(messages.title)}</title>
+          <meta name='robots' content='noindex' />
+        </Helmet>
+      </Column>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(Notifications));
diff --git a/app/javascript/flavours/blobfox/features/onboarding/components/arrow_small_right.jsx b/app/javascript/flavours/blobfox/features/onboarding/components/arrow_small_right.jsx
new file mode 100644
index 00000000000000..79b9db383f563d
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/onboarding/components/arrow_small_right.jsx
@@ -0,0 +1,7 @@
+const ArrowSmallRight = () => (
+  <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='currentColor'>
+    <path fillRule='evenodd' d='M5 10a.75.75 0 01.75-.75h6.638L10.23 7.29a.75.75 0 111.04-1.08l3.5 3.25a.75.75 0 010 1.08l-3.5 3.25a.75.75 0 11-1.04-1.08l2.158-1.96H5.75A.75.75 0 015 10z' clipRule='evenodd' />
+  </svg>
+);
+
+export default ArrowSmallRight;
\ No newline at end of file
diff --git a/app/javascript/flavours/blobfox/features/onboarding/components/progress_indicator.jsx b/app/javascript/flavours/blobfox/features/onboarding/components/progress_indicator.jsx
new file mode 100644
index 00000000000000..5a2623616ecb3f
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/onboarding/components/progress_indicator.jsx
@@ -0,0 +1,28 @@
+import PropTypes from 'prop-types';
+import { Fragment } from 'react';
+
+import classNames from 'classnames';
+
+import { Check } from 'flavours/blobfox/components/check';
+
+
+const ProgressIndicator = ({ steps, completed }) => (
+  <div className='onboarding__progress-indicator'>
+    {(new Array(steps)).fill().map((_, i) => (
+      <Fragment key={i}>
+        {i > 0 && <div className={classNames('onboarding__progress-indicator__line', { active: completed > i })} />}
+
+        <div className={classNames('onboarding__progress-indicator__step', { active: completed > i })}>
+          {completed > i && <Check />}
+        </div>
+      </Fragment>
+    ))}
+  </div>
+);
+
+ProgressIndicator.propTypes = {
+  steps: PropTypes.number.isRequired,
+  completed: PropTypes.number,
+};
+
+export default ProgressIndicator;
diff --git a/app/javascript/flavours/blobfox/features/onboarding/components/step.jsx b/app/javascript/flavours/blobfox/features/onboarding/components/step.jsx
new file mode 100644
index 00000000000000..b904c47ab308f4
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/onboarding/components/step.jsx
@@ -0,0 +1,50 @@
+import PropTypes from 'prop-types';
+
+import { Check } from 'flavours/blobfox/components/check';
+import { Icon }  from 'flavours/blobfox/components/icon';
+
+import ArrowSmallRight from './arrow_small_right';
+
+const Step = ({ label, description, icon, completed, onClick, href }) => {
+  const content = (
+    <>
+      <div className='onboarding__steps__item__icon'>
+        <Icon id={icon} />
+      </div>
+
+      <div className='onboarding__steps__item__description'>
+        <h6>{label}</h6>
+        <p>{description}</p>
+      </div>
+
+      <div className={completed ? 'onboarding__steps__item__progress' : 'onboarding__steps__item__go'}>
+        {completed ? <Check /> : <ArrowSmallRight />}
+      </div>
+    </>
+  );
+
+  if (href) {
+    return (
+      <a href={href} onClick={onClick} target='_blank' rel='noopener' className='onboarding__steps__item'>
+        {content}
+      </a>
+    );
+  }
+
+  return (
+    <button onClick={onClick} className='onboarding__steps__item'>
+      {content}
+    </button>
+  );
+};
+
+Step.propTypes = {
+  label: PropTypes.node,
+  description: PropTypes.node,
+  icon: PropTypes.string,
+  completed: PropTypes.bool,
+  href: PropTypes.string,
+  onClick: PropTypes.func,
+};
+
+export default Step;
diff --git a/app/javascript/flavours/blobfox/features/onboarding/follows.jsx b/app/javascript/flavours/blobfox/features/onboarding/follows.jsx
new file mode 100644
index 00000000000000..de753b0e6dbaf8
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/onboarding/follows.jsx
@@ -0,0 +1,80 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
+
+import { fetchSuggestions } from 'flavours/blobfox/actions/suggestions';
+import { markAsPartial } from 'flavours/blobfox/actions/timelines';
+import Column from 'flavours/blobfox/components/column';
+import ColumnBackButton from 'flavours/blobfox/components/column_back_button';
+import { EmptyAccount } from 'flavours/blobfox/components/empty_account';
+import Account from 'flavours/blobfox/containers/account_container';
+
+const mapStateToProps = state => ({
+  suggestions: state.getIn(['suggestions', 'items']),
+  isLoading: state.getIn(['suggestions', 'isLoading']),
+});
+
+class Follows extends PureComponent {
+
+  static propTypes = {
+    onBack: PropTypes.func,
+    dispatch: PropTypes.func.isRequired,
+    suggestions: ImmutablePropTypes.list,
+    isLoading: PropTypes.bool,
+    multiColumn: PropTypes.bool,
+  };
+
+  componentDidMount () {
+    const { dispatch } = this.props;
+    dispatch(fetchSuggestions(true));
+  }
+
+  componentWillUnmount () {
+    const { dispatch } = this.props;
+    dispatch(markAsPartial('home'));
+  }
+
+  render () {
+    const { onBack, isLoading, suggestions, multiColumn } = this.props;
+
+    let loadedContent;
+
+    if (isLoading) {
+      loadedContent = (new Array(8)).fill().map((_, i) => <EmptyAccount key={i} />);
+    } else if (suggestions.isEmpty()) {
+      loadedContent = <div className='follow-recommendations__empty'><FormattedMessage id='onboarding.follows.empty' defaultMessage='Unfortunately, no results can be shown right now. You can try using search or browsing the explore page to find people to follow, or try again later.' /></div>;
+    } else {
+      loadedContent = suggestions.map(suggestion => <Account id={suggestion.get('account')} key={suggestion.get('account')} withBio />);
+    }
+
+    return (
+      <Column>
+        <ColumnBackButton multiColumn={multiColumn} onClick={onBack} />
+
+        <div className='scrollable privacy-policy'>
+          <div className='column-title'>
+            <h3><FormattedMessage id='onboarding.follows.title' defaultMessage='Popular on Mastodon' /></h3>
+            <p><FormattedMessage id='onboarding.follows.lead' defaultMessage='You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!' /></p>
+          </div>
+
+          <div className='follow-recommendations'>
+            {loadedContent}
+          </div>
+
+          <p className='onboarding__lead'><FormattedMessage id='onboarding.tips.accounts_from_other_servers' defaultMessage='<strong>Did you know?</strong> Since Mastodon is decentralized, some profiles you come across will be hosted on servers other than yours. And yet you can interact with them seamlessly! Their server is in the second half of their username!' values={{ strong: chunks => <strong>{chunks}</strong> }} /></p>
+
+          <div className='onboarding__footer'>
+            <button className='link-button' onClick={onBack}><FormattedMessage id='onboarding.actions.back' defaultMessage='Take me back' /></button>
+          </div>
+        </div>
+      </Column>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(Follows);
diff --git a/app/javascript/flavours/blobfox/features/onboarding/index.jsx b/app/javascript/flavours/blobfox/features/onboarding/index.jsx
new file mode 100644
index 00000000000000..abb1dd7d075ec0
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/onboarding/index.jsx
@@ -0,0 +1,149 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+import { Link, withRouter } from 'react-router-dom';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
+import { debounce } from 'lodash';
+
+import { fetchAccount } from 'flavours/blobfox/actions/accounts';
+import { focusCompose } from 'flavours/blobfox/actions/compose';
+import { closeOnboarding } from 'flavours/blobfox/actions/onboarding';
+import Column from 'flavours/blobfox/features/ui/components/column';
+import { me } from 'flavours/blobfox/initial_state';
+import { makeGetAccount } from 'flavours/blobfox/selectors';
+import { assetHost } from 'flavours/blobfox/utils/config';
+import { WithRouterPropTypes } from 'flavours/blobfox/utils/react_router';
+import illustration from 'mastodon/../images/elephant_ui_conversation.svg';
+
+import ArrowSmallRight from './components/arrow_small_right';
+import Step from './components/step';
+import Follows from './follows';
+import Share from './share';
+
+const messages = defineMessages({
+  template: { id: 'onboarding.compose.template', defaultMessage: 'Hello #Mastodon!' },
+});
+
+const mapStateToProps = () => {
+  const getAccount = makeGetAccount();
+
+  return state => ({
+    account: getAccount(state, me),
+  });
+};
+
+class Onboarding extends ImmutablePureComponent {
+  static propTypes = {
+    dispatch: PropTypes.func.isRequired,
+    account: ImmutablePropTypes.record,
+    multiColumn: PropTypes.bool,
+    ...WithRouterPropTypes,
+  };
+
+  state = {
+    step: null,
+    profileClicked: false,
+    shareClicked: false,
+  };
+
+  handleClose = () => {
+    const { dispatch, history } = this.props;
+
+    dispatch(closeOnboarding());
+    history.push('/home');
+  };
+
+  handleProfileClick = () => {
+    this.setState({ profileClicked: true });
+  };
+
+  handleFollowClick = () => {
+    this.setState({ step: 'follows' });
+  };
+
+  handleComposeClick = () => {
+    const { dispatch, intl, history } = this.props;
+
+    dispatch(focusCompose(history, intl.formatMessage(messages.template)));
+  };
+
+  handleShareClick = () => {
+    this.setState({ step: 'share', shareClicked: true });
+  };
+
+  handleBackClick = () => {
+    this.setState({ step: null });
+  };
+
+  handleWindowFocus = debounce(() => {
+    const { dispatch, account } = this.props;
+    dispatch(fetchAccount(account.get('id')));
+  }, 1000, { trailing: true });
+
+  componentDidMount () {
+    window.addEventListener('focus', this.handleWindowFocus, false);
+  }
+
+  componentWillUnmount () {
+    window.removeEventListener('focus', this.handleWindowFocus);
+  }
+
+  render () {
+    const { account, multiColumn } = this.props;
+    const { step, shareClicked } = this.state;
+
+    switch(step) {
+    case 'follows':
+      return <Follows onBack={this.handleBackClick} multiColumn={multiColumn} />;
+    case 'share':
+      return <Share onBack={this.handleBackClick} multiColumn={multiColumn} />;
+    }
+
+    return (
+      <Column>
+        <div className='scrollable privacy-policy'>
+          <div className='column-title'>
+            <img src={illustration} alt='' className='onboarding__illustration' />
+            <h3><FormattedMessage id='onboarding.start.title' defaultMessage="You've made it!" /></h3>
+            <p><FormattedMessage id='onboarding.start.lead' defaultMessage="Your new Mastodon account is ready to go. Here's how you can make the most of it:" /></p>
+          </div>
+
+          <div className='onboarding__steps'>
+            <Step onClick={this.handleProfileClick} href='/settings/profile' completed={(!account.get('avatar').endsWith('missing.png')) || (account.get('display_name').length > 0 && account.get('note').length > 0)} icon='address-book-o' label={<FormattedMessage id='onboarding.steps.setup_profile.title' defaultMessage='Customize your profile' />} description={<FormattedMessage id='onboarding.steps.setup_profile.body' defaultMessage='Others are more likely to interact with you with a filled out profile.' />} />
+            <Step onClick={this.handleFollowClick} completed={(account.get('following_count') * 1) >= 7} icon='user-plus' label={<FormattedMessage id='onboarding.steps.follow_people.title' defaultMessage='Find at least {count, plural, one {one person} other {# people}} to follow' values={{ count: 7 }} />} description={<FormattedMessage id='onboarding.steps.follow_people.body' defaultMessage="You curate your own home feed. Let's fill it with interesting people." />} />
+            <Step onClick={this.handleComposeClick} completed={(account.get('statuses_count') * 1) >= 1} icon='pencil-square-o' label={<FormattedMessage id='onboarding.steps.publish_status.title' defaultMessage='Make your first post' />} description={<FormattedMessage id='onboarding.steps.publish_status.body' defaultMessage='Say hello to the world.' values={{ emoji: <img className='emojione' alt='🐘' src={`${assetHost}/emoji/1f418.svg`} /> }} />} />
+            <Step onClick={this.handleShareClick} completed={shareClicked} icon='copy' label={<FormattedMessage id='onboarding.steps.share_profile.title' defaultMessage='Share your profile' />} description={<FormattedMessage id='onboarding.steps.share_profile.body' defaultMessage='Let your friends know how to find you on Mastodon!' />} />
+          </div>
+
+          <p className='onboarding__lead'><FormattedMessage id='onboarding.start.skip' defaultMessage="Don't need help getting started?" /></p>
+
+          <div className='onboarding__links'>
+            <Link to='/explore' className='onboarding__link'>
+              <FormattedMessage id='onboarding.actions.go_to_explore' defaultMessage='Take me to trending' />
+              <ArrowSmallRight />
+            </Link>
+
+            <Link to='/home' className='onboarding__link'>
+              <FormattedMessage id='onboarding.actions.go_to_home' defaultMessage='Take me to my home feed' />
+              <ArrowSmallRight />
+            </Link>
+          </div>
+        </div>
+
+        <Helmet>
+          <meta name='robots' content='noindex' />
+        </Helmet>
+      </Column>
+    );
+  }
+
+}
+
+export default withRouter(connect(mapStateToProps)(injectIntl(Onboarding)));
diff --git a/app/javascript/flavours/blobfox/features/onboarding/share.jsx b/app/javascript/flavours/blobfox/features/onboarding/share.jsx
new file mode 100644
index 00000000000000..b6d4d0eb02c798
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/onboarding/share.jsx
@@ -0,0 +1,200 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import classNames from 'classnames';
+import { Link } from 'react-router-dom';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
+
+import SwipeableViews from 'react-swipeable-views';
+
+import Column from 'flavours/blobfox/components/column';
+import ColumnBackButton from 'flavours/blobfox/components/column_back_button';
+import { Icon }  from 'flavours/blobfox/components/icon';
+import { me, domain } from 'flavours/blobfox/initial_state';
+
+import ArrowSmallRight from './components/arrow_small_right';
+
+const messages = defineMessages({
+  shareableMessage: { id: 'onboarding.share.message', defaultMessage: 'I\'m {username} on #Mastodon! Come follow me at {url}' },
+});
+
+const mapStateToProps = state => ({
+  account: state.getIn(['accounts', me]),
+});
+
+class CopyPasteText extends PureComponent {
+
+  static propTypes = {
+    value: PropTypes.string,
+  };
+
+  state = {
+    copied: false,
+    focused: false,
+  };
+
+  setRef = c => {
+    this.input = c;
+  };
+
+  handleInputClick = () => {
+    this.setState({ copied: false });
+    this.input.focus();
+    this.input.select();
+    this.input.setSelectionRange(0, this.props.value.length);
+  };
+
+  handleButtonClick = e => {
+    e.stopPropagation();
+
+    const { value } = this.props;
+    navigator.clipboard.writeText(value);
+    this.input.blur();
+    this.setState({ copied: true });
+    this.timeout = setTimeout(() => this.setState({ copied: false }), 700);
+  };
+
+  handleFocus = () => {
+    this.setState({ focused: true });
+  };
+
+  handleBlur = () => {
+    this.setState({ focused: false });
+  };
+
+  componentWillUnmount () {
+    if (this.timeout) clearTimeout(this.timeout);
+  }
+
+  render () {
+    const { value } = this.props;
+    const { copied, focused } = this.state;
+
+    return (
+      <div className={classNames('copy-paste-text', { copied, focused })} tabIndex='0' role='button' onClick={this.handleInputClick}>
+        <textarea readOnly value={value} ref={this.setRef} onClick={this.handleInputClick} onFocus={this.handleFocus} onBlur={this.handleBlur} />
+
+        <button className='button' onClick={this.handleButtonClick}>
+          <Icon id='copy' /> {copied ? <FormattedMessage id='copypaste.copied' defaultMessage='Copied' /> : <FormattedMessage id='copypaste.copy_to_clipboard' defaultMessage='Copy to clipboard' />}
+        </button>
+      </div>
+    );
+  }
+
+}
+
+class TipCarousel extends PureComponent {
+
+  static propTypes = {
+    children: PropTypes.node,
+  };
+
+  state = {
+    index: 0,
+  };
+
+  handleSwipe = index => {
+    this.setState({ index });
+  };
+
+  handleChangeIndex = e => {
+    this.setState({ index: Number(e.currentTarget.getAttribute('data-index')) });
+  };
+
+  handleKeyDown = e => {
+    switch(e.key) {
+    case 'ArrowLeft':
+      e.preventDefault();
+      this.setState(({ index }, { children }) => ({ index: Math.abs(index - 1) % children.length }));
+      break;
+    case 'ArrowRight':
+      e.preventDefault();
+      this.setState(({ index }, { children }) => ({ index: (index + 1) % children.length }));
+      break;
+    }
+  };
+
+  render () {
+    const { children } = this.props;
+    const { index } = this.state;
+
+    return (
+      <div className='tip-carousel' tabIndex='0' onKeyDown={this.handleKeyDown}>
+        <SwipeableViews onChangeIndex={this.handleSwipe} index={index} enableMouseEvents tabIndex='-1'>
+          {children}
+        </SwipeableViews>
+
+        <div className='media-modal__pagination'>
+          {children.map((_, i) => (
+            <button key={i} className={classNames('media-modal__page-dot', { active: i === index })} data-index={i} onClick={this.handleChangeIndex}>
+              {i + 1}
+            </button>
+          ))}
+        </div>
+      </div>
+    );
+  }
+
+}
+
+class Share extends PureComponent {
+
+  static propTypes = {
+    onBack: PropTypes.func,
+    account: ImmutablePropTypes.record,
+    multiColumn: PropTypes.bool,
+    intl: PropTypes.object,
+  };
+
+  render () {
+    const { onBack, account, multiColumn, intl } = this.props;
+
+    const url = (new URL(`/@${account.get('username')}`, document.baseURI)).href;
+
+    return (
+      <Column>
+        <ColumnBackButton multiColumn={multiColumn} onClick={onBack} />
+
+        <div className='scrollable privacy-policy'>
+          <div className='column-title'>
+            <h3><FormattedMessage id='onboarding.share.title' defaultMessage='Share your profile' /></h3>
+            <p><FormattedMessage id='onboarding.share.lead' defaultMessage='Let people know how they can find you on Mastodon!' /></p>
+          </div>
+
+          <CopyPasteText value={intl.formatMessage(messages.shareableMessage, { username: `@${account.get('username')}@${domain}`, url })} />
+
+          <TipCarousel>
+            <div><p className='onboarding__lead'><FormattedMessage id='onboarding.tips.verification' defaultMessage='<strong>Did you know?</strong> You can verify your account by putting a link to your Mastodon profile on your own website and adding the website to your profile. No fees or documents necessary!'  values={{ strong: chunks => <strong>{chunks}</strong> }}  /></p></div>
+            <div><p className='onboarding__lead'><FormattedMessage id='onboarding.tips.migration' defaultMessage='<strong>Did you know?</strong> If you feel like {domain} is not a great server choice for you in the future, you can move to another Mastodon server without losing your followers. You can even host your own server!' values={{ domain, strong: chunks => <strong>{chunks}</strong> }} /></p></div>
+            <div><p className='onboarding__lead'><FormattedMessage id='onboarding.tips.2fa' defaultMessage='<strong>Did you know?</strong> You can secure your account by setting up two-factor authentication in your account settings. It works with any TOTP app of your choice, no phone number necessary!'  values={{ strong: chunks => <strong>{chunks}</strong> }}  /></p></div>
+          </TipCarousel>
+
+          <p className='onboarding__lead'><FormattedMessage id='onboarding.share.next_steps' defaultMessage='Possible next steps:' /></p>
+
+          <div className='onboarding__links'>
+            <Link to='/home' className='onboarding__link'>
+              <FormattedMessage id='onboarding.actions.go_to_home' defaultMessage='Take me to my home feed' />
+              <ArrowSmallRight />
+            </Link>
+
+            <Link to='/explore' className='onboarding__link'>
+              <FormattedMessage id='onboarding.actions.go_to_explore' defaultMessage='Take me to trending' />
+              <ArrowSmallRight />
+            </Link>
+          </div>
+
+          <div className='onboarding__footer'>
+            <button className='link-button' onClick={onBack}><FormattedMessage id='onboarding.action.back' defaultMessage='Take me back' /></button>
+          </div>
+        </div>
+      </Column>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(injectIntl(Share));
diff --git a/app/javascript/flavours/blobfox/features/picture_in_picture/components/footer.jsx b/app/javascript/flavours/blobfox/features/picture_in_picture/components/footer.jsx
new file mode 100644
index 00000000000000..e91282d9529027
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/picture_in_picture/components/footer.jsx
@@ -0,0 +1,231 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl } from 'react-intl';
+
+import classNames from 'classnames';
+import { withRouter } from 'react-router-dom';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
+import { initBoostModal } from 'flavours/blobfox/actions/boosts';
+import { replyCompose } from 'flavours/blobfox/actions/compose';
+import { reblog, favourite, unreblog, unfavourite } from 'flavours/blobfox/actions/interactions';
+import { openModal } from 'flavours/blobfox/actions/modal';
+import { IconButton } from 'flavours/blobfox/components/icon_button';
+import { me, boostModal } from 'flavours/blobfox/initial_state';
+import { makeGetStatus } from 'flavours/blobfox/selectors';
+import { WithRouterPropTypes } from 'flavours/blobfox/utils/react_router';
+
+const messages = defineMessages({
+  reply: { id: 'status.reply', defaultMessage: 'Reply' },
+  replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
+  reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
+  reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
+  cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
+  cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
+  favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
+  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?' },
+  open: { id: 'status.open', defaultMessage: 'Expand this status' },
+});
+
+const makeMapStateToProps = () => {
+  const getStatus = makeGetStatus();
+
+  const mapStateToProps = (state, { statusId }) => ({
+    status: getStatus(state, { id: statusId }),
+    askReplyConfirmation: state.getIn(['compose', 'text']).trim().length !== 0,
+    showReplyCount: state.getIn(['local_settings', 'show_reply_count']),
+  });
+
+  return mapStateToProps;
+};
+
+class Footer extends ImmutablePureComponent {
+
+  static contextTypes = {
+    identity: PropTypes.object,
+  };
+
+  static propTypes = {
+    statusId: PropTypes.string.isRequired,
+    status: ImmutablePropTypes.map.isRequired,
+    intl: PropTypes.object.isRequired,
+    dispatch: PropTypes.func.isRequired,
+    askReplyConfirmation: PropTypes.bool,
+    showReplyCount: PropTypes.bool,
+    withOpenButton: PropTypes.bool,
+    onClose: PropTypes.func,
+    ...WithRouterPropTypes,
+  };
+
+  _performReply = () => {
+    const { dispatch, status, onClose, history } = this.props;
+
+    if (onClose) {
+      onClose(true);
+    }
+
+    dispatch(replyCompose(status, history));
+  };
+
+  handleReplyClick = () => {
+    const { dispatch, askReplyConfirmation, status, intl } = this.props;
+    const { signedIn } = this.context.identity;
+
+    if (signedIn) {
+      if (askReplyConfirmation) {
+        dispatch(openModal({
+          modalType: 'CONFIRM',
+          modalProps: {
+            message: intl.formatMessage(messages.replyMessage),
+            confirm: intl.formatMessage(messages.replyConfirm),
+            onConfirm: this._performReply,
+          },
+        }));
+      } else {
+        this._performReply();
+      }
+    } else {
+      dispatch(openModal({
+        modalType: 'INTERACTION',
+        modalProps: {
+          type: 'reply',
+          accountId: status.getIn(['account', 'id']),
+          url: status.get('uri'),
+        },
+      }));
+    }
+  };
+
+  handleFavouriteClick = () => {
+    const { dispatch, status } = this.props;
+    const { signedIn } = this.context.identity;
+
+    if (signedIn) {
+      if (status.get('favourited')) {
+        dispatch(unfavourite(status));
+      } else {
+        dispatch(favourite(status));
+      }
+    } else {
+      dispatch(openModal({
+        modalType: 'INTERACTION',
+        modalProps: {
+          type: 'favourite',
+          accountId: status.getIn(['account', 'id']),
+          url: status.get('uri'),
+        },
+      }));
+    }
+  };
+
+  _performReblog = (status, privacy) => {
+    const { dispatch } = this.props;
+    dispatch(reblog(status, privacy));
+  };
+
+  handleReblogClick = e => {
+    const { dispatch, status } = this.props;
+    const { signedIn } = this.context.identity;
+
+    if (signedIn) {
+      if (status.get('reblogged')) {
+        dispatch(unreblog(status));
+      } else if ((e && e.shiftKey) || !boostModal) {
+        this._performReblog(status);
+      } else {
+        dispatch(initBoostModal({ status, onReblog: this._performReblog }));
+      }
+    } else {
+      dispatch(openModal({
+        modalType: 'INTERACTION',
+        modalProps: {
+          type: 'reblog',
+          accountId: status.getIn(['account', 'id']),
+          url: status.get('uri'),
+        },
+      }));
+    }
+  };
+
+  handleOpenClick = e => {
+    if (e.button !== 0 || !history) {
+      return;
+    }
+
+    const { status, onClose } = this.props;
+
+    if (onClose) {
+      onClose();
+    }
+
+    this.props.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
+  };
+
+  render () {
+    const { status, intl, showReplyCount, withOpenButton } = this.props;
+
+    const publicStatus  = ['public', 'unlisted'].includes(status.get('visibility'));
+    const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
+
+    let replyIcon, replyTitle;
+
+    if (status.get('in_reply_to_id', null) === null) {
+      replyIcon = 'reply';
+      replyTitle = intl.formatMessage(messages.reply);
+    } else {
+      replyIcon = 'reply-all';
+      replyTitle = intl.formatMessage(messages.replyAll);
+    }
+
+    let reblogTitle = '';
+
+    if (status.get('reblogged')) {
+      reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
+    } else if (publicStatus) {
+      reblogTitle = intl.formatMessage(messages.reblog);
+    } else if (reblogPrivate) {
+      reblogTitle = intl.formatMessage(messages.reblog_private);
+    } else {
+      reblogTitle = intl.formatMessage(messages.cannot_reblog);
+    }
+
+    let replyButton = null;
+    if (showReplyCount) {
+      replyButton = (
+        <IconButton
+          className='status__action-bar-button'
+          title={replyTitle}
+          icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon}
+          onClick={this.handleReplyClick}
+          counter={status.get('replies_count')}
+          obfuscateCount
+        />
+      );
+    } else {
+      replyButton = (
+        <IconButton
+          className='status__action-bar-button'
+          title={replyTitle}
+          icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon}
+          onClick={this.handleReplyClick}
+        />
+      );
+    }
+
+    return (
+      <div className='picture-in-picture__footer'>
+        {replyButton}
+        <IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate}  active={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} counter={status.get('reblogs_count')} />
+        <IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={status.get('favourites_count')} />
+        {withOpenButton && <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.open)} icon='external-link' onClick={this.handleOpenClick} href={status.get('url')} />}
+      </div>
+    );
+  }
+
+}
+
+export default  withRouter(connect(makeMapStateToProps)(injectIntl(Footer)));
diff --git a/app/javascript/flavours/blobfox/features/picture_in_picture/components/header.jsx b/app/javascript/flavours/blobfox/features/picture_in_picture/components/header.jsx
new file mode 100644
index 00000000000000..887924f33767e8
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/picture_in_picture/components/header.jsx
@@ -0,0 +1,50 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl } from 'react-intl';
+
+import { Link } from 'react-router-dom';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
+import { Avatar } from 'flavours/blobfox/components/avatar';
+import { DisplayName } from 'flavours/blobfox/components/display_name';
+import { IconButton } from 'flavours/blobfox/components/icon_button';
+
+const messages = defineMessages({
+  close: { id: 'lightbox.close', defaultMessage: 'Close' },
+});
+
+const mapStateToProps = (state, { accountId }) => ({
+  account: state.getIn(['accounts', accountId]),
+});
+
+class Header extends ImmutablePureComponent {
+
+  static propTypes = {
+    accountId: PropTypes.string.isRequired,
+    statusId: PropTypes.string.isRequired,
+    account: ImmutablePropTypes.record.isRequired,
+    onClose: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  render () {
+    const { account, statusId, onClose, intl } = this.props;
+
+    return (
+      <div className='picture-in-picture__header'>
+        <Link to={`/@${account.get('acct')}/${statusId}`} className='picture-in-picture__header__account'>
+          <Avatar account={account} size={36} />
+          <DisplayName account={account} />
+        </Link>
+
+        <IconButton icon='times' onClick={onClose} title={intl.formatMessage(messages.close)} />
+      </div>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(injectIntl(Header));
diff --git a/app/javascript/flavours/blobfox/features/picture_in_picture/index.jsx b/app/javascript/flavours/blobfox/features/picture_in_picture/index.jsx
new file mode 100644
index 00000000000000..9c5d0bb422bd01
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/picture_in_picture/index.jsx
@@ -0,0 +1,93 @@
+import PropTypes from 'prop-types';
+import { Component } from 'react';
+
+import classNames from 'classnames';
+
+import { connect } from 'react-redux';
+
+import { removePictureInPicture } from 'flavours/blobfox/actions/picture_in_picture';
+import Audio from 'flavours/blobfox/features/audio';
+import Video from 'flavours/blobfox/features/video';
+
+import Footer from './components/footer';
+import Header from './components/header';
+
+const mapStateToProps = state => ({
+  ...state.get('picture_in_picture'),
+  left: state.getIn(['local_settings', 'media', 'pop_in_position']) === 'left',
+});
+
+class PictureInPicture extends Component {
+
+  static propTypes = {
+    statusId: PropTypes.string,
+    accountId: PropTypes.string,
+    type: PropTypes.string,
+    src: PropTypes.string,
+    muted: PropTypes.bool,
+    volume: PropTypes.number,
+    currentTime: PropTypes.number,
+    poster: PropTypes.string,
+    backgroundColor: PropTypes.string,
+    foregroundColor: PropTypes.string,
+    accentColor: PropTypes.string,
+    dispatch: PropTypes.func.isRequired,
+    left: PropTypes.bool,
+  };
+
+  handleClose = () => {
+    const { dispatch } = this.props;
+    dispatch(removePictureInPicture());
+  };
+
+  render () {
+    const { type, src, currentTime, accountId, statusId, left } = this.props;
+
+    if (!currentTime) {
+      return null;
+    }
+
+    let player;
+
+    if (type === 'video') {
+      player = (
+        <Video
+          src={src}
+          currentTime={this.props.currentTime}
+          volume={this.props.volume}
+          muted={this.props.muted}
+          autoPlay
+          inline
+          alwaysVisible
+        />
+      );
+    } else if (type === 'audio') {
+      player = (
+        <Audio
+          src={src}
+          currentTime={this.props.currentTime}
+          volume={this.props.volume}
+          muted={this.props.muted}
+          poster={this.props.poster}
+          backgroundColor={this.props.backgroundColor}
+          foregroundColor={this.props.foregroundColor}
+          accentColor={this.props.accentColor}
+          autoPlay
+        />
+      );
+    }
+
+    return (
+      <div className={classNames('picture-in-picture', { left })}>
+        <Header accountId={accountId} statusId={statusId} onClose={this.handleClose} />
+
+        {player}
+
+        <Footer statusId={statusId} />
+      </div>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(PictureInPicture);
diff --git a/app/javascript/flavours/blobfox/features/pinned_accounts_editor/containers/account_container.js b/app/javascript/flavours/blobfox/features/pinned_accounts_editor/containers/account_container.js
new file mode 100644
index 00000000000000..841d5baeb4d85c
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/pinned_accounts_editor/containers/account_container.js
@@ -0,0 +1,25 @@
+import { injectIntl } from 'react-intl';
+
+import { connect } from 'react-redux';
+
+import { pinAccount, unpinAccount } from 'flavours/blobfox/actions/accounts';
+import Account from 'flavours/blobfox/features/list_editor/components/account';
+import { makeGetAccount } from 'flavours/blobfox/selectors';
+
+const makeMapStateToProps = () => {
+  const getAccount = makeGetAccount();
+
+  const mapStateToProps = (state, { accountId, added }) => ({
+    account: getAccount(state, accountId),
+    added: typeof added === 'undefined' ? state.getIn(['pinnedAccountsEditor', 'accounts', 'items']).includes(accountId) : added,
+  });
+
+  return mapStateToProps;
+};
+
+const mapDispatchToProps = (dispatch, { accountId }) => ({
+  onRemove: () => dispatch(unpinAccount(accountId)),
+  onAdd: () => dispatch(pinAccount(accountId)),
+});
+
+export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Account));
diff --git a/app/javascript/flavours/blobfox/features/pinned_accounts_editor/containers/search_container.js b/app/javascript/flavours/blobfox/features/pinned_accounts_editor/containers/search_container.js
new file mode 100644
index 00000000000000..095417884afcc7
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/pinned_accounts_editor/containers/search_container.js
@@ -0,0 +1,24 @@
+import { injectIntl } from 'react-intl';
+
+import { connect } from 'react-redux';
+
+import Search from 'flavours/blobfox/features/list_editor/components/search';
+
+import {
+  fetchPinnedAccountsSuggestions,
+  clearPinnedAccountsSuggestions,
+  changePinnedAccountsSuggestions,
+} from '../../../actions/accounts';
+
+
+const mapStateToProps = state => ({
+  value: state.getIn(['pinnedAccountsEditor', 'suggestions', 'value']),
+});
+
+const mapDispatchToProps = dispatch => ({
+  onSubmit: value => dispatch(fetchPinnedAccountsSuggestions(value)),
+  onClear: () => dispatch(clearPinnedAccountsSuggestions()),
+  onChange: value => dispatch(changePinnedAccountsSuggestions(value)),
+});
+
+export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(Search));
diff --git a/app/javascript/flavours/blobfox/features/pinned_accounts_editor/index.jsx b/app/javascript/flavours/blobfox/features/pinned_accounts_editor/index.jsx
new file mode 100644
index 00000000000000..d550984f597ab9
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/pinned_accounts_editor/index.jsx
@@ -0,0 +1,82 @@
+import PropTypes from 'prop-types';
+
+import { injectIntl, FormattedMessage } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
+import spring from 'react-motion/lib/spring';
+
+import { fetchPinnedAccounts, clearPinnedAccountsSuggestions, resetPinnedAccountsEditor } from 'flavours/blobfox/actions/accounts';
+import Motion from 'flavours/blobfox/features/ui/util/optional_motion';
+
+import AccountContainer from './containers/account_container';
+import SearchContainer from './containers/search_container';
+
+const mapStateToProps = state => ({
+  accountIds: state.getIn(['pinnedAccountsEditor', 'accounts', 'items']),
+  searchAccountIds: state.getIn(['pinnedAccountsEditor', 'suggestions', 'items']),
+});
+
+const mapDispatchToProps = dispatch => ({
+  onInitialize: () => dispatch(fetchPinnedAccounts()),
+  onClear: () => dispatch(clearPinnedAccountsSuggestions()),
+  onReset: () => dispatch(resetPinnedAccountsEditor()),
+});
+
+class PinnedAccountsEditor extends ImmutablePureComponent {
+
+  static propTypes = {
+    onClose: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+    onInitialize: PropTypes.func.isRequired,
+    onClear: PropTypes.func.isRequired,
+    onReset: PropTypes.func.isRequired,
+    title: PropTypes.string.isRequired,
+    accountIds: ImmutablePropTypes.list.isRequired,
+    searchAccountIds: ImmutablePropTypes.list.isRequired,
+  };
+
+  componentDidMount () {
+    const { onInitialize } = this.props;
+    onInitialize();
+  }
+
+  componentWillUnmount () {
+    const { onReset } = this.props;
+    onReset();
+  }
+
+  render () {
+    const { accountIds, searchAccountIds, onClear } = this.props;
+    const showSearch = searchAccountIds.size > 0;
+
+    return (
+      <div className='modal-root__modal list-editor'>
+        <h4><FormattedMessage id='endorsed_accounts_editor.endorsed_accounts' defaultMessage='Featured accounts' /></h4>
+
+        <SearchContainer />
+
+        <div className='drawer__pager'>
+          <div className='drawer__inner list-editor__accounts'>
+            {accountIds.map(accountId => <AccountContainer key={accountId} accountId={accountId} added />)}
+          </div>
+
+          {showSearch && <div role='button' tabIndex={-1} className='drawer__backdrop' onClick={onClear} />}
+
+          <Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
+            {({ x }) =>
+              (<div className='drawer__inner backdrop' style={{ transform: x === 0 ? null : `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}>
+                {searchAccountIds.map(accountId => <AccountContainer key={accountId} accountId={accountId} />)}
+              </div>)
+            }
+          </Motion>
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(PinnedAccountsEditor));
diff --git a/app/javascript/flavours/blobfox/features/pinned_statuses/index.jsx b/app/javascript/flavours/blobfox/features/pinned_statuses/index.jsx
new file mode 100644
index 00000000000000..287b5be16235c8
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/pinned_statuses/index.jsx
@@ -0,0 +1,70 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
+import { getStatusList } from 'flavours/blobfox/selectors';
+
+import { fetchPinnedStatuses } from '../../actions/pin_statuses';
+import ColumnBackButtonSlim from '../../components/column_back_button_slim';
+import StatusList from '../../components/status_list';
+import Column from '../ui/components/column';
+
+const messages = defineMessages({
+  heading: { id: 'column.pins', defaultMessage: 'Pinned post' },
+});
+
+const mapStateToProps = state => ({
+  statusIds: getStatusList(state, 'pins'),
+  hasMore: !!state.getIn(['status_lists', 'pins', 'next']),
+});
+
+class PinnedStatuses extends ImmutablePureComponent {
+
+  static propTypes = {
+    dispatch: PropTypes.func.isRequired,
+    statusIds: ImmutablePropTypes.list.isRequired,
+    intl: PropTypes.object.isRequired,
+    hasMore: PropTypes.bool.isRequired,
+    multiColumn: PropTypes.bool,
+  };
+
+  UNSAFE_componentWillMount () {
+    this.props.dispatch(fetchPinnedStatuses());
+  }
+
+  handleHeaderClick = () => {
+    this.column.scrollTop();
+  };
+
+  setRef = c => {
+    this.column = c;
+  };
+
+  render () {
+    const { intl, statusIds, hasMore, multiColumn } = this.props;
+
+    return (
+      <Column bindToDocument={!multiColumn} icon='thumb-tack' heading={intl.formatMessage(messages.heading)} ref={this.setRef}>
+        <ColumnBackButtonSlim />
+        <StatusList
+          statusIds={statusIds}
+          scrollKey='pinned_statuses'
+          hasMore={hasMore}
+          bindToDocument={!multiColumn}
+        />
+        <Helmet>
+          <meta name='robots' content='noindex' />
+        </Helmet>
+      </Column>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(injectIntl(PinnedStatuses));
diff --git a/app/javascript/flavours/blobfox/features/privacy_policy/index.jsx b/app/javascript/flavours/blobfox/features/privacy_policy/index.jsx
new file mode 100644
index 00000000000000..888037e46ef2a7
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/privacy_policy/index.jsx
@@ -0,0 +1,65 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { FormattedMessage, FormattedDate, injectIntl, defineMessages } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+
+import api from 'flavours/blobfox/api';
+import Column from 'flavours/blobfox/components/column';
+import { Skeleton } from 'flavours/blobfox/components/skeleton';
+
+const messages = defineMessages({
+  title: { id: 'privacy_policy.title', defaultMessage: 'Privacy Policy' },
+});
+
+class PrivacyPolicy extends PureComponent {
+
+  static propTypes = {
+    intl: PropTypes.object,
+    multiColumn: PropTypes.bool,
+  };
+
+  state = {
+    content: null,
+    lastUpdated: null,
+    isLoading: true,
+  };
+
+  componentDidMount () {
+    api().get('/api/v1/instance/privacy_policy').then(({ data }) => {
+      this.setState({ content: data.content, lastUpdated: data.updated_at, isLoading: false });
+    }).catch(() => {
+      this.setState({ isLoading: false });
+    });
+  }
+
+  render () {
+    const { intl, multiColumn } = this.props;
+    const { isLoading, content, lastUpdated } = this.state;
+
+    return (
+      <Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.title)}>
+        <div className='scrollable privacy-policy'>
+          <div className='column-title'>
+            <h3><FormattedMessage id='privacy_policy.title' defaultMessage='Privacy Policy' /></h3>
+            <p><FormattedMessage id='privacy_policy.last_updated' defaultMessage='Last updated {date}' values={{ date: isLoading ? <Skeleton width='10ch' /> : <FormattedDate value={lastUpdated} year='numeric' month='short' day='2-digit' /> }} /></p>
+          </div>
+
+          <div
+            className='privacy-policy__body prose'
+            dangerouslySetInnerHTML={{ __html: content }}
+          />
+        </div>
+
+        <Helmet>
+          <title>{intl.formatMessage(messages.title)}</title>
+          <meta name='robots' content='all' />
+        </Helmet>
+      </Column>
+    );
+  }
+
+}
+
+export default injectIntl(PrivacyPolicy);
diff --git a/app/javascript/flavours/blobfox/features/public_timeline/components/column_settings.jsx b/app/javascript/flavours/blobfox/features/public_timeline/components/column_settings.jsx
new file mode 100644
index 00000000000000..82684c83685439
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/public_timeline/components/column_settings.jsx
@@ -0,0 +1,46 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
+import SettingText from '../../../components/setting_text';
+import SettingToggle from '../../notifications/components/setting_toggle';
+
+const messages = defineMessages({
+  filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' },
+});
+
+class ColumnSettings extends PureComponent {
+
+  static propTypes = {
+    settings: ImmutablePropTypes.map.isRequired,
+    onChange: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+    columnId: PropTypes.string,
+  };
+
+  render () {
+    const { settings, onChange, intl } = this.props;
+
+    return (
+      <div>
+        <div className='column-settings__row'>
+          <SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
+          <SettingToggle settings={settings} settingPath={['other', 'onlyRemote']} onChange={onChange} label={<FormattedMessage id='community.column_settings.remote_only' defaultMessage='Remote only' />} />
+          {!settings.getIn(['other', 'onlyRemote']) && <SettingToggle settings={settings} settingPath={['other', 'allowLocalOnly']} onChange={onChange} label={<FormattedMessage id='community.column_settings.allow_local_only' defaultMessage='Show local-only toots' />} />}
+        </div>
+
+        <span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
+
+        <div className='column-settings__row'>
+          <SettingText settings={settings} settingPath={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} />
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(ColumnSettings);
diff --git a/app/javascript/flavours/blobfox/features/public_timeline/containers/column_settings_container.js b/app/javascript/flavours/blobfox/features/public_timeline/containers/column_settings_container.js
new file mode 100644
index 00000000000000..6476d51ffbf25e
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/public_timeline/containers/column_settings_container.js
@@ -0,0 +1,29 @@
+import { connect } from 'react-redux';
+
+import { changeColumnParams } from '../../../actions/columns';
+import { changeSetting } from '../../../actions/settings';
+import ColumnSettings from '../components/column_settings';
+
+const mapStateToProps = (state, { columnId }) => {
+  const uuid = columnId;
+  const columns = state.getIn(['settings', 'columns']);
+  const index = columns.findIndex(c => c.get('uuid') === uuid);
+
+  return {
+    settings: (uuid && index >= 0) ? columns.get(index).get('params') : state.getIn(['settings', 'public']),
+  };
+};
+
+const mapDispatchToProps = (dispatch, { columnId }) => {
+  return {
+    onChange (key, checked) {
+      if (columnId) {
+        dispatch(changeColumnParams(columnId, key, checked));
+      } else {
+        dispatch(changeSetting(['public', ...key], checked));
+      }
+    },
+  };
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);
diff --git a/app/javascript/flavours/blobfox/features/public_timeline/index.jsx b/app/javascript/flavours/blobfox/features/public_timeline/index.jsx
new file mode 100644
index 00000000000000..b5e750851c73b5
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/public_timeline/index.jsx
@@ -0,0 +1,171 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+
+import { connect } from 'react-redux';
+
+import { DismissableBanner } from 'flavours/blobfox/components/dismissable_banner';
+import { domain } from 'flavours/blobfox/initial_state';
+
+import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
+import { connectPublicStream } from '../../actions/streaming';
+import { expandPublicTimeline } from '../../actions/timelines';
+import Column from '../../components/column';
+import ColumnHeader from '../../components/column_header';
+import StatusListContainer from '../ui/containers/status_list_container';
+
+import ColumnSettingsContainer from './containers/column_settings_container';
+
+const messages = defineMessages({
+  title: { id: 'column.public', defaultMessage: 'Federated timeline' },
+});
+
+const mapStateToProps = (state, { columnId }) => {
+  const uuid = columnId;
+  const columns = state.getIn(['settings', 'columns']);
+  const index = columns.findIndex(c => c.get('uuid') === uuid);
+  const onlyMedia = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'public', 'other', 'onlyMedia']);
+  const onlyRemote = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyRemote']) : state.getIn(['settings', 'public', 'other', 'onlyRemote']);
+  const allowLocalOnly = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'allowLocalOnly']) : state.getIn(['settings', 'public', 'other', 'allowLocalOnly']);
+  const regex = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'regex', 'body']) : state.getIn(['settings', 'public', 'regex', 'body']);
+  const timelineState = state.getIn(['timelines', `public${onlyRemote ? ':remote' : allowLocalOnly ? ':allow_local_only' : ''}${onlyMedia ? ':media' : ''}`]);
+
+  return {
+    hasUnread: !!timelineState && timelineState.get('unread') > 0,
+    onlyMedia,
+    onlyRemote,
+    allowLocalOnly,
+    regex,
+  };
+};
+
+class PublicTimeline extends PureComponent {
+
+  static contextTypes = {
+    identity: PropTypes.object,
+  };
+
+  static defaultProps = {
+    onlyMedia: false,
+  };
+
+  static propTypes = {
+    dispatch: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+    columnId: PropTypes.string,
+    multiColumn: PropTypes.bool,
+    hasUnread: PropTypes.bool,
+    onlyMedia: PropTypes.bool,
+    onlyRemote: PropTypes.bool,
+    allowLocalOnly: PropTypes.bool,
+    regex: PropTypes.string,
+  };
+
+  handlePin = () => {
+    const { columnId, dispatch, onlyMedia, onlyRemote, allowLocalOnly } = this.props;
+
+    if (columnId) {
+      dispatch(removeColumn(columnId));
+    } else {
+      dispatch(addColumn(onlyRemote ? 'REMOTE' : 'PUBLIC', { other: { onlyMedia, onlyRemote, allowLocalOnly } }));
+    }
+  };
+
+  handleMove = (dir) => {
+    const { columnId, dispatch } = this.props;
+    dispatch(moveColumn(columnId, dir));
+  };
+
+  handleHeaderClick = () => {
+    this.column.scrollTop();
+  };
+
+  componentDidMount () {
+    const { dispatch, onlyMedia, onlyRemote, allowLocalOnly } = this.props;
+    const { signedIn } = this.context.identity;
+
+    dispatch(expandPublicTimeline({ onlyMedia, onlyRemote, allowLocalOnly }));
+    if (signedIn) {
+      this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote, allowLocalOnly }));
+    }
+  }
+
+  componentDidUpdate (prevProps) {
+    const { signedIn } = this.context.identity;
+
+    if (prevProps.onlyMedia !== this.props.onlyMedia || prevProps.onlyRemote !== this.props.onlyRemote || prevProps.allowLocalOnly !== this.props.allowLocalOnly) {
+      const { dispatch, onlyMedia, onlyRemote, allowLocalOnly } = this.props;
+
+      if (this.disconnect) {
+        this.disconnect();
+      }
+
+      dispatch(expandPublicTimeline({ onlyMedia, onlyRemote, allowLocalOnly }));
+
+      if (signedIn) {
+        this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote, allowLocalOnly }));
+      }
+    }
+  }
+
+  componentWillUnmount () {
+    if (this.disconnect) {
+      this.disconnect();
+      this.disconnect = null;
+    }
+  }
+
+  setRef = c => {
+    this.column = c;
+  };
+
+  handleLoadMore = maxId => {
+    const { dispatch, onlyMedia, onlyRemote, allowLocalOnly } = this.props;
+
+    dispatch(expandPublicTimeline({ maxId, onlyMedia, onlyRemote, allowLocalOnly }));
+  };
+
+  render () {
+    const { intl, columnId, hasUnread, multiColumn, onlyMedia, onlyRemote, allowLocalOnly } = this.props;
+    const pinned = !!columnId;
+
+    return (
+      <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
+        <ColumnHeader
+          icon='globe'
+          active={hasUnread}
+          title={intl.formatMessage(messages.title)}
+          onPin={this.handlePin}
+          onMove={this.handleMove}
+          onClick={this.handleHeaderClick}
+          pinned={pinned}
+          multiColumn={multiColumn}
+        >
+          <ColumnSettingsContainer columnId={columnId} />
+        </ColumnHeader>
+
+        <StatusListContainer
+          prepend={<DismissableBanner id='public_timeline'><FormattedMessage id='dismissable_banner.public_timeline' defaultMessage='These are the most recent public posts from people on the social web that people on {domain} follow.' values={{ domain }} /></DismissableBanner>}
+          timelineId={`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`}
+          onLoadMore={this.handleLoadMore}
+          trackScroll={!pinned}
+          scrollKey={`public_timeline-${columnId}`}
+          emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up' />}
+          bindToDocument={!multiColumn}
+          regex={this.props.regex}
+        />
+
+        <Helmet>
+          <title>{intl.formatMessage(messages.title)}</title>
+          <meta name='robots' content='noindex' />
+        </Helmet>
+      </Column>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(injectIntl(PublicTimeline));
diff --git a/app/javascript/flavours/blobfox/features/reblogs/index.jsx b/app/javascript/flavours/blobfox/features/reblogs/index.jsx
new file mode 100644
index 00000000000000..d617b4de829715
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/reblogs/index.jsx
@@ -0,0 +1,115 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
+import { debounce } from 'lodash';
+
+import { Icon }  from 'flavours/blobfox/components/icon';
+
+import { fetchReblogs, expandReblogs } from '../../actions/interactions';
+import ColumnHeader from '../../components/column_header';
+import { LoadingIndicator } from '../../components/loading_indicator';
+import ScrollableList from '../../components/scrollable_list';
+import AccountContainer from '../../containers/account_container';
+import Column from '../ui/components/column';
+
+const messages = defineMessages({
+  heading: { id: 'column.reblogged_by', defaultMessage: 'Boosted by' },
+  refresh: { id: 'refresh', defaultMessage: 'Refresh' },
+});
+
+const mapStateToProps = (state, props) => ({
+  accountIds: state.getIn(['user_lists', 'reblogged_by', props.params.statusId, 'items']),
+  hasMore: !!state.getIn(['user_lists', 'reblogged_by', props.params.statusId, 'next']),
+  isLoading: state.getIn(['user_lists', 'reblogged_by', props.params.statusId, 'isLoading'], true),
+});
+
+class Reblogs extends ImmutablePureComponent {
+
+  static propTypes = {
+    params: PropTypes.object.isRequired,
+    dispatch: PropTypes.func.isRequired,
+    accountIds: ImmutablePropTypes.list,
+    hasMore: PropTypes.bool,
+    isLoading: PropTypes.bool,
+    multiColumn: PropTypes.bool,
+    intl: PropTypes.object.isRequired,
+  };
+
+  UNSAFE_componentWillMount () {
+    if (!this.props.accountIds) {
+      this.props.dispatch(fetchReblogs(this.props.params.statusId));
+    }
+  }
+
+  handleHeaderClick = () => {
+    this.column.scrollTop();
+  };
+
+  setRef = c => {
+    this.column = c;
+  };
+
+  handleRefresh = () => {
+    this.props.dispatch(fetchReblogs(this.props.params.statusId));
+  };
+
+  handleLoadMore = debounce(() => {
+    this.props.dispatch(expandReblogs(this.props.params.statusId));
+  }, 300, { leading: true });
+
+  render () {
+    const { intl, accountIds, hasMore, isLoading, multiColumn } = this.props;
+
+    if (!accountIds) {
+      return (
+        <Column>
+          <LoadingIndicator />
+        </Column>
+      );
+    }
+
+    const emptyMessage = <FormattedMessage id='status.reblogs.empty' defaultMessage='No one has boosted this post yet. When someone does, they will show up here.' />;
+
+    return (
+      <Column ref={this.setRef}>
+        <ColumnHeader
+          icon='retweet'
+          title={intl.formatMessage(messages.heading)}
+          onClick={this.handleHeaderClick}
+          showBackButton
+          multiColumn={multiColumn}
+          extraButton={(
+            <button className='column-header__button' title={intl.formatMessage(messages.refresh)} aria-label={intl.formatMessage(messages.refresh)} onClick={this.handleRefresh}><Icon id='refresh' /></button>
+          )}
+        />
+
+        <ScrollableList
+          scrollKey='reblogs'
+          onLoadMore={this.handleLoadMore}
+          hasMore={hasMore}
+          isLoading={isLoading}
+          emptyMessage={emptyMessage}
+          bindToDocument={!multiColumn}
+        >
+          {accountIds.map(id =>
+            <AccountContainer key={id} id={id} withNote={false} />,
+          )}
+        </ScrollableList>
+
+        <Helmet>
+          <meta name='robots' content='noindex' />
+        </Helmet>
+      </Column>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(injectIntl(Reblogs));
diff --git a/app/javascript/flavours/blobfox/features/report/category.jsx b/app/javascript/flavours/blobfox/features/report/category.jsx
new file mode 100644
index 00000000000000..ee9382667fd96c
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/report/category.jsx
@@ -0,0 +1,108 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import { List as ImmutableList } from 'immutable';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
+
+import { Button } from 'flavours/blobfox/components/button';
+
+import Option from './components/option';
+
+const messages = defineMessages({
+  dislike: { id: 'report.reasons.dislike', defaultMessage: 'I don\'t like it' },
+  dislike_description: { id: 'report.reasons.dislike_description', defaultMessage: 'It is not something you want to see' },
+  spam: { id: 'report.reasons.spam', defaultMessage: 'It\'s spam' },
+  spam_description: { id: 'report.reasons.spam_description', defaultMessage: 'Malicious links, fake engagement, or repetitive replies' },
+  violation: { id: 'report.reasons.violation', defaultMessage: 'It violates server rules' },
+  violation_description: { id: 'report.reasons.violation_description', defaultMessage: 'You are aware that it breaks specific rules' },
+  other: { id: 'report.reasons.other', defaultMessage: 'It\'s something else' },
+  other_description: { id: 'report.reasons.other_description', defaultMessage: 'The issue does not fit into other categories' },
+  status: { id: 'report.category.title_status', defaultMessage: 'post' },
+  account: { id: 'report.category.title_account', defaultMessage: 'profile' },
+});
+
+const mapStateToProps = state => ({
+  rules: state.getIn(['server', 'server', 'rules'], ImmutableList()),
+});
+
+class Category extends PureComponent {
+
+  static propTypes = {
+    onNextStep: PropTypes.func.isRequired,
+    rules: ImmutablePropTypes.list,
+    category: PropTypes.string,
+    onChangeCategory: PropTypes.func.isRequired,
+    startedFrom: PropTypes.oneOf(['status', 'account']),
+    intl: PropTypes.object.isRequired,
+  };
+
+  handleNextClick = () => {
+    const { onNextStep, category } = this.props;
+
+    switch(category) {
+    case 'dislike':
+      onNextStep('thanks');
+      break;
+    case 'violation':
+      onNextStep('rules');
+      break;
+    default:
+      onNextStep('statuses');
+      break;
+    }
+  };
+
+  handleCategoryToggle = (value, checked) => {
+    const { onChangeCategory } = this.props;
+
+    if (checked) {
+      onChangeCategory(value);
+    }
+  };
+
+  render () {
+    const { category, startedFrom, rules, intl } = this.props;
+
+    const options = rules.size > 0 ? [
+      'spam',
+      'violation',
+      'other',
+    ] : [
+      'spam',
+      'other',
+    ];
+
+    return (
+      <>
+        <h3 className='report-dialog-modal__title'><FormattedMessage id='report.category.title' defaultMessage="Tell us what's going on with this {type}" values={{ type: intl.formatMessage(messages[startedFrom]) }} /></h3>
+        <p className='report-dialog-modal__lead'><FormattedMessage id='report.category.subtitle' defaultMessage='Choose the best match' /></p>
+
+        <div>
+          {options.map(item => (
+            <Option
+              key={item}
+              name='category'
+              value={item}
+              checked={category === item}
+              onToggle={this.handleCategoryToggle}
+              label={intl.formatMessage(messages[item])}
+              description={intl.formatMessage(messages[`${item}_description`])}
+            />
+          ))}
+        </div>
+
+        <div className='flex-spacer' />
+
+        <div className='report-dialog-modal__actions'>
+          <Button onClick={this.handleNextClick} disabled={category === null}><FormattedMessage id='report.next' defaultMessage='Next' /></Button>
+        </div>
+      </>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(injectIntl(Category));
diff --git a/app/javascript/flavours/blobfox/features/report/comment.jsx b/app/javascript/flavours/blobfox/features/report/comment.jsx
new file mode 100644
index 00000000000000..5c7c835704e623
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/report/comment.jsx
@@ -0,0 +1,121 @@
+import PropTypes from 'prop-types';
+import { useCallback, useEffect, useRef } from 'react';
+
+import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
+
+import { OrderedSet, List as ImmutableList } from 'immutable';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { shallowEqual } from 'react-redux';
+import { createSelector } from 'reselect';
+
+import Toggle from 'react-toggle';
+
+import { fetchAccount } from 'flavours/blobfox/actions/accounts';
+import { Button } from 'flavours/blobfox/components/button';
+import { useAppDispatch, useAppSelector } from 'flavours/blobfox/store';
+
+const messages = defineMessages({
+  placeholder: { id: 'report.placeholder', defaultMessage: 'Type or paste additional comments' },
+});
+
+const selectRepliedToAccountIds = createSelector(
+  [
+    (state) => state.get('statuses'),
+    (_, statusIds) => statusIds,
+  ],
+  (statusesMap, statusIds) => statusIds.map((statusId) => statusesMap.getIn([statusId, 'in_reply_to_account_id'])),
+  {
+    resultEqualityCheck: shallowEqual,
+  }
+);
+
+const Comment = ({ comment, domain, statusIds, isRemote, isSubmitting, selectedDomains, onSubmit, onChangeComment, onToggleDomain }) => {
+  const intl = useIntl();
+
+  const dispatch = useAppDispatch();
+  const loadedRef = useRef(false);
+
+  const handleClick = useCallback(() => onSubmit(), [onSubmit]);
+  const handleChange = useCallback((e) => onChangeComment(e.target.value), [onChangeComment]);
+  const handleToggleDomain = useCallback(e => onToggleDomain(e.target.value, e.target.checked), [onToggleDomain]);
+
+  const handleKeyDown = useCallback((e) => {
+    if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
+      handleClick();
+    }
+  }, [handleClick]);
+
+  // Memoize accountIds since we don't want it to trigger `useEffect` on each render
+  const accountIds = useAppSelector((state) => domain ? selectRepliedToAccountIds(state, statusIds) : ImmutableList());
+
+  // While we could memoize `availableDomains`, it is pretty inexpensive to recompute
+  const accountsMap = useAppSelector((state) => state.get('accounts'));
+  const availableDomains = domain ? OrderedSet([domain]).union(accountIds.map((accountId) => accountsMap.getIn([accountId, 'acct'], '').split('@')[1]).filter(domain => !!domain)) : OrderedSet();
+
+  useEffect(() => {
+    if (loadedRef.current) {
+      return;
+    }
+
+    loadedRef.current = true;
+
+    // First, pre-select known domains
+    availableDomains.forEach((domain) => {
+      onToggleDomain(domain, true);
+    });
+
+    // Then, fetch missing replied-to accounts
+    const unknownAccounts = OrderedSet(accountIds.filter(accountId => accountId && !accountsMap.has(accountId)));
+    unknownAccounts.forEach((accountId) => {
+      dispatch(fetchAccount(accountId));
+    });
+  });
+
+  return (
+    <>
+      <h3 className='report-dialog-modal__title'><FormattedMessage id='report.comment.title' defaultMessage='Is there anything else you think we should know?' /></h3>
+
+      <textarea
+        className='report-dialog-modal__textarea'
+        placeholder={intl.formatMessage(messages.placeholder)}
+        value={comment}
+        onChange={handleChange}
+        onKeyDown={handleKeyDown}
+        disabled={isSubmitting}
+      />
+
+      {isRemote && (
+        <>
+          <p className='report-dialog-modal__lead'><FormattedMessage id='report.forward_hint' defaultMessage='The account is from another server. Send an anonymized copy of the report there as well?' /></p>
+
+          { availableDomains.map((domain) => (
+            <label className='report-dialog-modal__toggle' key={`toggle-${domain}`}>
+              <Toggle checked={selectedDomains.includes(domain)} disabled={isSubmitting} onChange={handleToggleDomain} value={domain} />
+              <FormattedMessage id='report.forward' defaultMessage='Forward to {target}' values={{ target: domain }} />
+            </label>
+          ))}
+        </>
+      )}
+
+      <div className='flex-spacer' />
+
+      <div className='report-dialog-modal__actions'>
+        <Button onClick={handleClick} disabled={isSubmitting}><FormattedMessage id='report.submit' defaultMessage='Submit report' /></Button>
+      </div>
+    </>
+  );
+};
+
+Comment.propTypes = {
+  comment: PropTypes.string.isRequired,
+  domain: PropTypes.string,
+  statusIds: ImmutablePropTypes.list.isRequired,
+  isRemote: PropTypes.bool,
+  isSubmitting: PropTypes.bool,
+  selectedDomains: ImmutablePropTypes.set.isRequired,
+  onSubmit: PropTypes.func.isRequired,
+  onChangeComment: PropTypes.func.isRequired,
+  onToggleDomain: PropTypes.func.isRequired,
+};
+
+export default Comment;
diff --git a/app/javascript/flavours/blobfox/features/report/components/option.jsx b/app/javascript/flavours/blobfox/features/report/components/option.jsx
new file mode 100644
index 00000000000000..873fb0d8274892
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/report/components/option.jsx
@@ -0,0 +1,62 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import classNames from 'classnames';
+
+import { Check } from 'flavours/blobfox/components/check';
+
+export default class Option extends PureComponent {
+
+  static propTypes = {
+    name: PropTypes.string.isRequired,
+    value: PropTypes.string.isRequired,
+    checked: PropTypes.bool,
+    label: PropTypes.node,
+    description: PropTypes.node,
+    onToggle: PropTypes.func,
+    multiple: PropTypes.bool,
+    labelComponent: PropTypes.node,
+  };
+
+  handleKeyPress = e => {
+    const { value, checked, onToggle } = this.props;
+
+    if (e.key === 'Enter' || e.key === ' ') {
+      e.stopPropagation();
+      e.preventDefault();
+      onToggle(value, !checked);
+    }
+  };
+
+  handleChange = e => {
+    const { value, onToggle } = this.props;
+    onToggle(value, e.target.checked);
+  };
+
+  render () {
+    const { name, value, checked, label, labelComponent, description, multiple } = this.props;
+
+    return (
+      <label className='dialog-option poll__option selectable'>
+        <input type={multiple ? 'checkbox' : 'radio'} name={name} value={value} checked={checked} onChange={this.handleChange} />
+
+        <span
+          className={classNames('poll__input', { active: checked, checkbox: multiple })}
+          tabIndex={0}
+          role='radio'
+          onKeyPress={this.handleKeyPress}
+          aria-checked={checked}
+          aria-label={label}
+        >{checked && <Check />}</span>
+
+        {labelComponent ? labelComponent : (
+          <span className='poll__option__text'>
+            <strong>{label}</strong>
+            {description}
+          </span>
+        )}
+      </label>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/report/components/status_check_box.jsx b/app/javascript/flavours/blobfox/features/report/components/status_check_box.jsx
new file mode 100644
index 00000000000000..d7c5cd2c1e3299
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/report/components/status_check_box.jsx
@@ -0,0 +1,67 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
+import { Avatar } from 'flavours/blobfox/components/avatar';
+import { DisplayName } from 'flavours/blobfox/components/display_name';
+import MediaAttachments from 'flavours/blobfox/components/media_attachments';
+import { RelativeTimestamp } from 'flavours/blobfox/components/relative_timestamp';
+import StatusContent from 'flavours/blobfox/components/status_content';
+import VisibilityIcon from 'flavours/blobfox/components/status_visibility_icon';
+
+import Option from './option';
+
+class StatusCheckBox extends PureComponent {
+
+  static propTypes = {
+    id: PropTypes.string.isRequired,
+    status: ImmutablePropTypes.map.isRequired,
+    checked: PropTypes.bool,
+    onToggle: PropTypes.func.isRequired,
+  };
+
+  handleStatusesToggle = (value, checked) => {
+    const { onToggle } = this.props;
+    onToggle(value, checked);
+  };
+
+  render () {
+    const { status, checked } = this.props;
+
+    if (status.get('reblog')) {
+      return null;
+    }
+
+    const labelComponent = (
+      <div className='status-check-box__status poll__option__text'>
+        <div className='detailed-status__display-name'>
+          <div className='detailed-status__display-avatar'>
+            <Avatar account={status.get('account')} size={46} />
+          </div>
+
+          <div>
+            <DisplayName account={status.get('account')} /> · <VisibilityIcon visibility={status.get('visibility')} /><RelativeTimestamp timestamp={status.get('created_at')} />
+          </div>
+        </div>
+
+        <StatusContent status={status} media={<MediaAttachments status={status} visible={false} />} />
+      </div>
+    );
+
+    return (
+      <Option
+        name='status_ids'
+        value={status.get('id')}
+        checked={checked}
+        onToggle={this.handleStatusesToggle}
+        label={status.get('search_index')}
+        labelComponent={labelComponent}
+        multiple
+      />
+    );
+  }
+
+}
+
+export default StatusCheckBox;
diff --git a/app/javascript/flavours/blobfox/features/report/containers/status_check_box_container.js b/app/javascript/flavours/blobfox/features/report/containers/status_check_box_container.js
new file mode 100644
index 00000000000000..123b6f127a6ea4
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/report/containers/status_check_box_container.js
@@ -0,0 +1,17 @@
+import { connect } from 'react-redux';
+
+import { makeGetStatus } from 'flavours/blobfox/selectors';
+
+import StatusCheckBox from '../components/status_check_box';
+
+const makeMapStateToProps = () => {
+  const getStatus = makeGetStatus();
+
+  const mapStateToProps = (state, { id }) => ({
+    status: getStatus(state, { id }),
+  });
+
+  return mapStateToProps;
+};
+
+export default connect(makeMapStateToProps)(StatusCheckBox);
diff --git a/app/javascript/flavours/blobfox/features/report/rules.jsx b/app/javascript/flavours/blobfox/features/report/rules.jsx
new file mode 100644
index 00000000000000..ffb0e29e7feb83
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/report/rules.jsx
@@ -0,0 +1,69 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
+
+import { Button } from 'flavours/blobfox/components/button';
+
+import Option from './components/option';
+
+const mapStateToProps = state => ({
+  rules: state.getIn(['server', 'server', 'rules']),
+});
+
+class Rules extends PureComponent {
+
+  static propTypes = {
+    onNextStep: PropTypes.func.isRequired,
+    rules: ImmutablePropTypes.list,
+    selectedRuleIds: ImmutablePropTypes.set.isRequired,
+    onToggle: PropTypes.func.isRequired,
+  };
+
+  handleNextClick = () => {
+    const { onNextStep } = this.props;
+    onNextStep('statuses');
+  };
+
+  handleRulesToggle = (value, checked) => {
+    const { onToggle } = this.props;
+    onToggle(value, checked);
+  };
+
+  render () {
+    const { rules, selectedRuleIds } = this.props;
+
+    return (
+      <>
+        <h3 className='report-dialog-modal__title'><FormattedMessage id='report.rules.title' defaultMessage='Which rules are being violated?' /></h3>
+        <p className='report-dialog-modal__lead'><FormattedMessage id='report.rules.subtitle' defaultMessage='Select all that apply' /></p>
+
+        <div>
+          {rules.map(item => (
+            <Option
+              key={item.get('id')}
+              name='rule_ids'
+              value={item.get('id')}
+              checked={selectedRuleIds.includes(item.get('id'))}
+              onToggle={this.handleRulesToggle}
+              label={item.get('text')}
+              multiple
+            />
+          ))}
+        </div>
+
+        <div className='flex-spacer' />
+
+        <div className='report-dialog-modal__actions'>
+          <Button onClick={this.handleNextClick} disabled={selectedRuleIds.size < 1}><FormattedMessage id='report.next' defaultMessage='Next' /></Button>
+        </div>
+      </>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(Rules);
diff --git a/app/javascript/flavours/blobfox/features/report/statuses.jsx b/app/javascript/flavours/blobfox/features/report/statuses.jsx
new file mode 100644
index 00000000000000..5bcf1f795c373a
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/report/statuses.jsx
@@ -0,0 +1,65 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import { OrderedSet } from 'immutable';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
+
+import { Button } from 'flavours/blobfox/components/button';
+import { LoadingIndicator } from 'flavours/blobfox/components/loading_indicator';
+import StatusCheckBox from 'flavours/blobfox/features/report/containers/status_check_box_container';
+
+const mapStateToProps = (state, { accountId }) => ({
+  availableStatusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}:with_replies`, 'items'])),
+  isLoading: state.getIn(['timelines', `account:${accountId}:with_replies`, 'isLoading']),
+});
+
+class Statuses extends PureComponent {
+
+  static propTypes = {
+    onNextStep: PropTypes.func.isRequired,
+    accountId: PropTypes.string.isRequired,
+    availableStatusIds: ImmutablePropTypes.set.isRequired,
+    selectedStatusIds: ImmutablePropTypes.set.isRequired,
+    isLoading: PropTypes.bool,
+    onToggle: PropTypes.func.isRequired,
+  };
+
+  handleNextClick = () => {
+    const { onNextStep } = this.props;
+    onNextStep('comment');
+  };
+
+  render () {
+    const { availableStatusIds, selectedStatusIds, onToggle, isLoading } = this.props;
+
+    return (
+      <>
+        <h3 className='report-dialog-modal__title'><FormattedMessage id='report.statuses.title' defaultMessage='Are there any posts that back up this report?' /></h3>
+        <p className='report-dialog-modal__lead'><FormattedMessage id='report.statuses.subtitle' defaultMessage='Select all that apply' /></p>
+
+        <div className='report-dialog-modal__statuses'>
+          {isLoading ? <LoadingIndicator /> : availableStatusIds.union(selectedStatusIds).map(statusId => (
+            <StatusCheckBox
+              id={statusId}
+              key={statusId}
+              checked={selectedStatusIds.includes(statusId)}
+              onToggle={onToggle}
+            />
+          ))}
+        </div>
+
+        <div className='flex-spacer' />
+
+        <div className='report-dialog-modal__actions'>
+          <Button onClick={this.handleNextClick}><FormattedMessage id='report.next' defaultMessage='Next' /></Button>
+        </div>
+      </>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(Statuses);
diff --git a/app/javascript/flavours/blobfox/features/report/thanks.jsx b/app/javascript/flavours/blobfox/features/report/thanks.jsx
new file mode 100644
index 00000000000000..aee3e9980134fa
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/report/thanks.jsx
@@ -0,0 +1,88 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
+
+import {
+  unfollowAccount,
+  muteAccount,
+  blockAccount,
+} from 'flavours/blobfox/actions/accounts';
+import { Button } from 'flavours/blobfox/components/button';
+
+const mapStateToProps = () => ({});
+
+class Thanks extends PureComponent {
+
+  static propTypes = {
+    submitted: PropTypes.bool,
+    onClose: PropTypes.func.isRequired,
+    account: ImmutablePropTypes.record.isRequired,
+    dispatch: PropTypes.func.isRequired,
+  };
+
+  handleCloseClick = () => {
+    const { onClose } = this.props;
+    onClose();
+  };
+
+  handleUnfollowClick = () => {
+    const { dispatch, account, onClose } = this.props;
+    dispatch(unfollowAccount(account.get('id')));
+    onClose();
+  };
+
+  handleMuteClick = () => {
+    const { dispatch, account, onClose } = this.props;
+    dispatch(muteAccount(account.get('id')));
+    onClose();
+  };
+
+  handleBlockClick = () => {
+    const { dispatch, account, onClose } = this.props;
+    dispatch(blockAccount(account.get('id')));
+    onClose();
+  };
+
+  render () {
+    const { account, submitted } = this.props;
+
+    return (
+      <>
+        <h3 className='report-dialog-modal__title'>{submitted ? <FormattedMessage id='report.thanks.title_actionable' defaultMessage="Thanks for reporting, we'll look into this." /> : <FormattedMessage id='report.thanks.title' defaultMessage="Don't want to see this?" />}</h3>
+        <p className='report-dialog-modal__lead'>{submitted ? <FormattedMessage id='report.thanks.take_action_actionable' defaultMessage='While we review this, you can take action against @{name}:' values={{ name: account.get('username') }} /> : <FormattedMessage id='report.thanks.take_action' defaultMessage='Here are your options for controlling what you see on Mastodon:' />}</p>
+
+        {account.getIn(['relationship', 'following']) && (
+          <>
+            <h4 className='report-dialog-modal__subtitle'><FormattedMessage id='report.unfollow' defaultMessage='Unfollow @{name}' values={{ name: account.get('username') }} /></h4>
+            <p className='report-dialog-modal__lead'><FormattedMessage id='report.unfollow_explanation' defaultMessage='You are following this account. To not see their posts in your home feed anymore, unfollow them.' /></p>
+            <Button secondary onClick={this.handleUnfollowClick}><FormattedMessage id='account.unfollow' defaultMessage='Unfollow' /></Button>
+            <hr />
+          </>
+        )}
+
+        <h4 className='report-dialog-modal__subtitle'><FormattedMessage id='account.mute' defaultMessage='Mute @{name}' values={{ name: account.get('username') }} /></h4>
+        <p className='report-dialog-modal__lead'><FormattedMessage id='report.mute_explanation' defaultMessage='You will not see their posts. They can still follow you and see your posts and will not know that they are muted.' /></p>
+        <Button secondary onClick={this.handleMuteClick}>{!account.getIn(['relationship', 'muting']) ? <FormattedMessage id='report.mute' defaultMessage='Mute' /> : <FormattedMessage id='account.muted' defaultMessage='Muted' />}</Button>
+
+        <hr />
+
+        <h4 className='report-dialog-modal__subtitle'><FormattedMessage id='account.block' defaultMessage='Block @{name}' values={{ name: account.get('username') }} /></h4>
+        <p className='report-dialog-modal__lead'><FormattedMessage id='report.block_explanation' defaultMessage='You will not see their posts. They will not be able to see your posts or follow you. They will be able to tell that they are blocked.' /></p>
+        <Button secondary onClick={this.handleBlockClick}>{!account.getIn(['relationship', 'blocking']) ? <FormattedMessage id='report.block' defaultMessage='Block' /> : <FormattedMessage id='account.blocked' defaultMessage='Blocked' />}</Button>
+
+        <div className='flex-spacer' />
+
+        <div className='report-dialog-modal__actions'>
+          <Button onClick={this.handleCloseClick}><FormattedMessage id='report.close' defaultMessage='Done' /></Button>
+        </div>
+      </>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(Thanks);
diff --git a/app/javascript/flavours/blobfox/features/standalone/compose/index.jsx b/app/javascript/flavours/blobfox/features/standalone/compose/index.jsx
new file mode 100644
index 00000000000000..c36e843f5ada65
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/standalone/compose/index.jsx
@@ -0,0 +1,21 @@
+import { PureComponent } from 'react';
+
+import ComposeFormContainer from '../../compose/containers/compose_form_container';
+import LoadingBarContainer from '../../ui/containers/loading_bar_container';
+import ModalContainer from '../../ui/containers/modal_container';
+import NotificationsContainer from '../../ui/containers/notifications_container';
+
+export default class Compose extends PureComponent {
+
+  render () {
+    return (
+      <div>
+        <ComposeFormContainer autoFocus />
+        <NotificationsContainer />
+        <ModalContainer />
+        <LoadingBarContainer className='loading-bar' />
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/status/components/action_bar.jsx b/app/javascript/flavours/blobfox/features/status/components/action_bar.jsx
new file mode 100644
index 00000000000000..a71eea35e318ef
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/status/components/action_bar.jsx
@@ -0,0 +1,267 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, injectIntl } from 'react-intl';
+
+import classNames from 'classnames';
+import { withRouter } from 'react-router-dom';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
+import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/blobfox/permissions';
+import { accountAdminLink, statusAdminLink } from 'flavours/blobfox/utils/backend_links';
+import { WithRouterPropTypes } from 'flavours/blobfox/utils/react_router';
+
+import { IconButton } from '../../../components/icon_button';
+import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
+import { me, maxReactions } from '../../../initial_state';
+import EmojiPickerDropdown from '../../compose/containers/emoji_picker_dropdown_container';
+
+const messages = defineMessages({
+  delete: { id: 'status.delete', defaultMessage: 'Delete' },
+  redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
+  edit: { id: 'status.edit', defaultMessage: 'Edit' },
+  direct: { id: 'status.direct', defaultMessage: 'Privately mention @{name}' },
+  mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
+  reply: { id: 'status.reply', defaultMessage: 'Reply' },
+  reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
+  reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
+  cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
+  cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
+  favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
+  react: { id: 'status.react', defaultMessage: 'React' },
+  bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
+  more: { id: 'status.more', defaultMessage: 'More' },
+  mute: { id: 'status.mute', defaultMessage: 'Mute @{name}' },
+  muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
+  unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
+  block: { id: 'status.block', defaultMessage: 'Block @{name}' },
+  report: { id: 'status.report', defaultMessage: 'Report @{name}' },
+  share: { id: 'status.share', defaultMessage: 'Share' },
+  pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
+  unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
+  embed: { id: 'status.embed', defaultMessage: 'Embed' },
+  admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
+  admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' },
+  admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },
+  copy: { id: 'status.copy', defaultMessage: 'Copy link to post' },
+  openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' },
+});
+
+class ActionBar extends PureComponent {
+
+  static contextTypes = {
+    identity: PropTypes.object,
+  };
+
+  static propTypes = {
+    status: ImmutablePropTypes.map.isRequired,
+    onReply: PropTypes.func.isRequired,
+    onReblog: PropTypes.func.isRequired,
+    onFavourite: PropTypes.func.isRequired,
+    onReactionAdd: PropTypes.func.isRequired,
+    onBookmark: PropTypes.func.isRequired,
+    onDelete: PropTypes.func.isRequired,
+    onEdit: PropTypes.func.isRequired,
+    onDirect: PropTypes.func.isRequired,
+    onMention: PropTypes.func.isRequired,
+    onMute: PropTypes.func,
+    onBlock: PropTypes.func,
+    onMuteConversation: PropTypes.func,
+    onReport: PropTypes.func,
+    onPin: PropTypes.func,
+    onEmbed: PropTypes.func,
+    intl: PropTypes.object.isRequired,
+    ...WithRouterPropTypes,
+  };
+
+  handleReplyClick = () => {
+    this.props.onReply(this.props.status);
+  };
+
+  handleReblogClick = (e) => {
+    this.props.onReblog(this.props.status, e);
+  };
+
+  handleFavouriteClick = (e) => {
+    this.props.onFavourite(this.props.status, e);
+  };
+
+  handleEmojiPick = data => {
+    this.props.onReactionAdd(this.props.status.get('id'), data.native.replace(/:/g, ''), data.imageUrl);
+  };
+
+  handleBookmarkClick = (e) => {
+    this.props.onBookmark(this.props.status, e);
+  };
+
+  handleDeleteClick = () => {
+    this.props.onDelete(this.props.status, this.props.history);
+  };
+
+  handleRedraftClick = () => {
+    this.props.onDelete(this.props.status, this.props.history, true);
+  };
+
+  handleEditClick = () => {
+    this.props.onEdit(this.props.status, this.props.history);
+  };
+
+  handleDirectClick = () => {
+    this.props.onDirect(this.props.status.get('account'), this.props.history);
+  };
+
+  handleMentionClick = () => {
+    this.props.onMention(this.props.status.get('account'), this.props.history);
+  };
+
+  handleMuteClick = () => {
+    this.props.onMute(this.props.status.get('account'));
+  };
+
+  handleBlockClick = () => {
+    this.props.onBlock(this.props.status);
+  };
+
+  handleConversationMuteClick = () => {
+    this.props.onMuteConversation(this.props.status);
+  };
+
+  handleReport = () => {
+    this.props.onReport(this.props.status);
+  };
+
+  handlePinClick = () => {
+    this.props.onPin(this.props.status);
+  };
+
+  handleShare = () => {
+    navigator.share({
+      url: this.props.status.get('url'),
+    });
+  };
+
+  handleEmbed = () => {
+    this.props.onEmbed(this.props.status);
+  };
+
+  handleCopy = () => {
+    const url = this.props.status.get('url');
+    navigator.clipboard.writeText(url);
+  };
+
+  handleNoOp = () => {}; // hack for reaction add button
+
+  render () {
+    const { status, intl } = this.props;
+    const { signedIn, permissions } = this.context.identity;
+
+    const publicStatus       = ['public', 'unlisted'].includes(status.get('visibility'));
+    const pinnableStatus     = ['public', 'unlisted', 'private'].includes(status.get('visibility'));
+    const mutingConversation = status.get('muted');
+    const writtenByMe        = status.getIn(['account', 'id']) === me;
+    const isRemote           = status.getIn(['account', 'username']) !== status.getIn(['account', 'acct']);
+
+    let menu = [];
+
+    if (publicStatus && isRemote) {
+      menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: status.get('url') });
+    }
+
+    menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy });
+
+    if (publicStatus && 'share' in navigator) {
+      menu.push({ text: intl.formatMessage(messages.share), action: this.handleShare });
+    }
+
+    if (publicStatus && (signedIn || !isRemote)) {
+      menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
+    }
+
+    if (signedIn) {
+      menu.push(null);
+
+      if (writtenByMe) {
+        if (pinnableStatus) {
+          menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
+          menu.push(null);
+        }
+
+        menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
+        menu.push(null);
+        menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
+        menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick, dangerous: true });
+        menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick, dangerous: true });
+      } else {
+        menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
+        menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick });
+        menu.push(null);
+        menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick, dangerous: true });
+        menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick, dangerous: true });
+        menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport, dangerous: true });
+        if (((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && (accountAdminLink || statusAdminLink)) || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) {
+          menu.push(null);
+          if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) {
+            if (accountAdminLink !== undefined) {
+              menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: accountAdminLink(status.getIn(['account', 'id'])) });
+            }
+            if (statusAdminLink !== undefined) {
+              menu.push({ text: intl.formatMessage(messages.admin_status), href: statusAdminLink(status.getIn(['account', 'id']), status.get('id')) });
+            }
+          }
+          if (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) {
+            const domain = status.getIn(['account', 'acct']).split('@')[1];
+            menu.push({ text: intl.formatMessage(messages.admin_domain, { domain: domain }), href: `/admin/instances/${domain}` });
+          }
+        }
+      }
+    }
+
+    const canReact = signedIn && status.get('reactions').filter(r => r.get('count') > 0 && r.get('me')).size < maxReactions;
+    const reactButton = (
+      <IconButton
+        className='plus-icon'
+        onClick={this.handleNoOp} // EmojiPickerDropdown handles that
+        title={intl.formatMessage(messages.react)}
+        disabled={!canReact}
+        icon='plus'
+      />
+    );
+
+    const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
+
+    let reblogTitle;
+    if (status.get('reblogged')) {
+      reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
+    } else if (publicStatus) {
+      reblogTitle = intl.formatMessage(messages.reblog);
+    } else if (reblogPrivate) {
+      reblogTitle = intl.formatMessage(messages.reblog_private);
+    } else {
+      reblogTitle = intl.formatMessage(messages.cannot_reblog);
+    }
+
+    return (
+      <div className='detailed-status__action-bar'>
+        <div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_id', null) === null ? 'reply' : 'reply-all'} onClick={this.handleReplyClick} /></div>
+        <div className='detailed-status__button'><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} /></div>
+        <div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div>
+        <div className='detailed-status__button'>
+          {
+            signedIn
+              ? <EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} button={reactButton} disabled={!canReact} />
+              : reactButton
+          }
+        </div>
+        <div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div>
+
+        <div className='detailed-status__action-bar-dropdown'>
+          <DropdownMenuContainer size={18} icon='ellipsis-h' items={menu} direction='left' title={intl.formatMessage(messages.more)} />
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default withRouter(injectIntl(ActionBar));
diff --git a/app/javascript/flavours/blobfox/features/status/components/card.jsx b/app/javascript/flavours/blobfox/features/status/components/card.jsx
new file mode 100644
index 00000000000000..51e548a1584b24
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/status/components/card.jsx
@@ -0,0 +1,256 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import classNames from 'classnames';
+
+import Immutable from 'immutable';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
+import { Blurhash } from 'flavours/blobfox/components/blurhash';
+import { Icon }  from 'flavours/blobfox/components/icon';
+import { useBlurhash } from 'flavours/blobfox/initial_state';
+import { decode as decodeIDNA } from 'flavours/blobfox/utils/idna';
+
+const getHostname = url => {
+  const parser = document.createElement('a');
+  parser.href = url;
+  return parser.hostname;
+};
+
+const domParser = new DOMParser();
+
+const addAutoPlay = html => {
+  const document = domParser.parseFromString(html, 'text/html').documentElement;
+  const iframe = document.querySelector('iframe');
+
+  if (iframe) {
+    if (iframe.src.indexOf('?') !== -1) {
+      iframe.src += '&';
+    } else {
+      iframe.src += '?';
+    }
+
+    iframe.src += 'autoplay=1&auto_play=1';
+
+    // DOM parser creates html/body elements around original HTML fragment,
+    // so we need to get innerHTML out of the body and not the entire document
+    return document.querySelector('body').innerHTML;
+  }
+
+  return html;
+};
+
+export default class Card extends PureComponent {
+
+  static propTypes = {
+    card: ImmutablePropTypes.map,
+    onOpenMedia: PropTypes.func.isRequired,
+    compact: PropTypes.bool,
+    sensitive: PropTypes.bool,
+  };
+
+  static defaultProps = {
+    compact: false,
+  };
+
+  state = {
+    previewLoaded: false,
+    embedded: false,
+    revealed: !this.props.sensitive,
+  };
+
+  UNSAFE_componentWillReceiveProps (nextProps) {
+    if (!Immutable.is(this.props.card, nextProps.card)) {
+      this.setState({ embedded: false, previewLoaded: false });
+    }
+    if (this.props.sensitive !== nextProps.sensitive) {
+      this.setState({ revealed: !nextProps.sensitive });
+    }
+  }
+
+  componentDidMount () {
+    window.addEventListener('resize', this.handleResize, { passive: true });
+  }
+
+  componentWillUnmount () {
+    window.removeEventListener('resize', this.handleResize);
+  }
+
+  handlePhotoClick = () => {
+    const { card, onOpenMedia } = this.props;
+
+    onOpenMedia(
+      Immutable.fromJS([
+        {
+          type: 'image',
+          url: card.get('embed_url'),
+          description: card.get('title'),
+          meta: {
+            original: {
+              width: card.get('width'),
+              height: card.get('height'),
+            },
+          },
+        },
+      ]),
+      0,
+    );
+  };
+
+  handleEmbedClick = () => {
+    const { card } = this.props;
+
+    if (card.get('type') === 'photo') {
+      this.handlePhotoClick();
+    } else {
+      this.setState({ embedded: true });
+    }
+  };
+
+  setRef = c => {
+    this.node = c;
+  };
+
+  handleImageLoad = () => {
+    this.setState({ previewLoaded: true });
+  };
+
+  handleReveal = e => {
+    e.preventDefault();
+    e.stopPropagation();
+    this.setState({ revealed: true });
+  };
+
+  renderVideo () {
+    const { card }  = this.props;
+    const content   = { __html: addAutoPlay(card.get('html')) };
+
+    return (
+      <div
+        ref={this.setRef}
+        className='status-card__image status-card-video'
+        dangerouslySetInnerHTML={content}
+        style={{ aspectRatio: `${card.get('width')} / ${card.get('height')}` }}
+      />
+    );
+  }
+
+  render () {
+    const { card, compact } = this.props;
+    const { embedded, revealed } = this.state;
+
+    if (card === null) {
+      return null;
+    }
+
+    const provider    = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name');
+    const horizontal  = (!compact && card.get('width') > card.get('height')) || card.get('type') !== 'link' || embedded;
+    const interactive = card.get('type') !== 'link';
+    const className   = classNames('status-card', { horizontal, compact, interactive });
+    const title       = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener noreferrer' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>;
+    const language    = card.get('language') || '';
+
+    const description = (
+      <div className='status-card__content' lang={language}>
+        {title}
+        {!(horizontal || compact) && <p className='status-card__description' title={card.get('description')}>{card.get('description')}</p>}
+        <span className='status-card__host'>{provider}</span>
+      </div>
+    );
+
+    const thumbnailStyle = {
+      visibility: revealed? null : 'hidden',
+    };
+
+    if (horizontal) {
+      thumbnailStyle.aspectRatio = (compact && !embedded) ? '16 / 9' : `${card.get('width')} / ${card.get('height')}`;
+    }
+
+    let embed     = '';
+    let canvas = (
+      <Blurhash
+        className={classNames('status-card__image-preview', {
+          'status-card__image-preview--hidden': revealed && this.state.previewLoaded,
+        })}
+        hash={card.get('blurhash')}
+        dummy={!useBlurhash}
+      />
+    );
+
+    const thumbnailDescription = card.get('image_description');
+    const thumbnail = <img src={card.get('image')} alt={thumbnailDescription} title={thumbnailDescription} lang={language} style={thumbnailStyle} onLoad={this.handleImageLoad} className='status-card__image-image' />;
+
+    let spoilerButton = (
+      <button type='button' onClick={this.handleReveal} className='spoiler-button__overlay'>
+        <span className='spoiler-button__overlay__label'>
+          <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />
+          <span className='spoiler-button__overlay__action'><FormattedMessage id='status.media.show' defaultMessage='Click to show' /></span>
+        </span>
+      </button>
+    );
+
+    spoilerButton = (
+      <div className={classNames('spoiler-button', { 'spoiler-button--minified': revealed })}>
+        {spoilerButton}
+      </div>
+    );
+
+    if (interactive) {
+      if (embedded) {
+        embed = this.renderVideo();
+      } else {
+        let iconVariant = 'play';
+
+        if (card.get('type') === 'photo') {
+          iconVariant = 'search-plus';
+        }
+
+        embed = (
+          <div className='status-card__image'>
+            {canvas}
+            {thumbnail}
+
+            {revealed ? (
+              <div className='status-card__actions'>
+                <div>
+                  <button type='button' onClick={this.handleEmbedClick}><Icon id={iconVariant} /></button>
+                  {horizontal && <a href={card.get('url')} target='_blank' rel='noopener noreferrer'><Icon id='external-link' /></a>}
+                </div>
+              </div>
+            ) : spoilerButton}
+          </div>
+        );
+      }
+
+      return (
+        <div className={className} ref={this.setRef} onClick={revealed ? null : this.handleReveal} role={revealed ? 'button' : null}>
+          {embed}
+          {!compact && description}
+        </div>
+      );
+    } else if (card.get('image')) {
+      embed = (
+        <div className='status-card__image'>
+          {canvas}
+          {thumbnail}
+        </div>
+      );
+    } else {
+      embed = (
+        <div className='status-card__image'>
+          <Icon id='file-text' />
+        </div>
+      );
+    }
+
+    return (
+      <a href={card.get('url')} className={className} target='_blank' rel='noopener noreferrer' ref={this.setRef}>
+        {embed}
+        {description}
+      </a>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/status/components/detailed_status.jsx b/app/javascript/flavours/blobfox/features/status/components/detailed_status.jsx
new file mode 100644
index 00000000000000..2b4c96bc83ce97
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/status/components/detailed_status.jsx
@@ -0,0 +1,362 @@
+import PropTypes from 'prop-types';
+
+import { injectIntl, FormattedDate } from 'react-intl';
+
+import classNames from 'classnames';
+import { Link, withRouter } from 'react-router-dom';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import { AnimatedNumber } from 'flavours/blobfox/components/animated_number';
+import AttachmentList from 'flavours/blobfox/components/attachment_list';
+import EditedTimestamp from 'flavours/blobfox/components/edited_timestamp';
+import { getHashtagBarForStatus } from 'flavours/blobfox/components/hashtag_bar';
+import { Icon }  from 'flavours/blobfox/components/icon';
+import PictureInPicturePlaceholder from 'flavours/blobfox/components/picture_in_picture_placeholder';
+import VisibilityIcon from 'flavours/blobfox/components/status_visibility_icon';
+import PollContainer from 'flavours/blobfox/containers/poll_container';
+import { WithRouterPropTypes } from 'flavours/blobfox/utils/react_router';
+
+import { Avatar } from '../../../components/avatar';
+import { DisplayName } from '../../../components/display_name';
+import MediaGallery from '../../../components/media_gallery';
+import StatusContent from '../../../components/status_content';
+import StatusReactions from '../../../components/status_reactions';
+import Audio from '../../audio';
+import scheduleIdleTask from '../../ui/util/schedule_idle_task';
+import Video from '../../video';
+
+import Card from './card';
+
+class DetailedStatus extends ImmutablePureComponent {
+
+  static contextTypes = {
+    identity: PropTypes.object,
+  };
+
+  static propTypes = {
+    status: ImmutablePropTypes.map,
+    settings: ImmutablePropTypes.map.isRequired,
+    onOpenMedia: PropTypes.func.isRequired,
+    onOpenVideo: PropTypes.func.isRequired,
+    onToggleHidden: PropTypes.func,
+    onTranslate: PropTypes.func.isRequired,
+    expanded: PropTypes.bool,
+    measureHeight: PropTypes.bool,
+    onHeightChange: PropTypes.func,
+    domain: PropTypes.string.isRequired,
+    compact: PropTypes.bool,
+    showMedia: PropTypes.bool,
+    pictureInPicture: ImmutablePropTypes.contains({
+      inUse: PropTypes.bool,
+      available: PropTypes.bool,
+    }),
+    onToggleMediaVisibility: PropTypes.func,
+    onReactionAdd: PropTypes.func.isRequired,
+    onReactionRemove: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+    ...WithRouterPropTypes,
+  };
+
+  state = {
+    height: null,
+  };
+
+  handleAccountClick = (e) => {
+    if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey) && this.props.history) {
+      e.preventDefault();
+      this.props.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
+    }
+
+    e.stopPropagation();
+  };
+
+  parseClick = (e, destination) => {
+    if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey) && this.props.history) {
+      e.preventDefault();
+      this.props.history.push(destination);
+    }
+
+    e.stopPropagation();
+  };
+
+  handleOpenVideo = (options) => {
+    this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), options);
+  };
+
+  _measureHeight (heightJustChanged) {
+    if (this.props.measureHeight && this.node) {
+      scheduleIdleTask(() => this.node && this.setState({ height: Math.ceil(this.node.scrollHeight) + 1 }));
+
+      if (this.props.onHeightChange && heightJustChanged) {
+        this.props.onHeightChange();
+      }
+    }
+  }
+
+  setRef = c => {
+    this.node = c;
+    this._measureHeight();
+  };
+
+  componentDidUpdate (prevProps, prevState) {
+    this._measureHeight(prevState.height !== this.state.height);
+  }
+
+  handleChildUpdate = () => {
+    this._measureHeight();
+  };
+
+  handleModalLink = e => {
+    e.preventDefault();
+
+    let href;
+
+    if (e.target.nodeName !== 'A') {
+      href = e.target.parentNode.href;
+    } else {
+      href = e.target.href;
+    }
+
+    window.open(href, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
+  };
+
+  handleTranslate = () => {
+    const { onTranslate, status } = this.props;
+    onTranslate(status);
+  };
+
+  render () {
+    const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
+    const outerStyle = { boxSizing: 'border-box' };
+    const { compact, pictureInPicture, expanded, onToggleHidden, settings } = this.props;
+
+    if (!status) {
+      return null;
+    }
+
+    let applicationLink = '';
+    let reblogLink = '';
+    let reblogIcon = 'retweet';
+    let favouriteLink = '';
+    let edited = '';
+
+    //  Depending on user settings, some media are considered as parts of the
+    //  contents (affected by CW) while other will be displayed outside of the
+    //  CW.
+    let contentMedia = [];
+    let contentMediaIcons = [];
+    let extraMedia = [];
+    let extraMediaIcons = [];
+    let media = contentMedia;
+    let mediaIcons = contentMediaIcons;
+
+    if (settings.getIn(['content_warnings', 'media_outside'])) {
+      media = extraMedia;
+      mediaIcons = extraMediaIcons;
+    }
+
+    if (this.props.measureHeight) {
+      outerStyle.height = `${this.state.height}px`;
+    }
+
+    const language = status.getIn(['translation', 'language']) || status.get('language');
+
+    if (pictureInPicture.get('inUse')) {
+      media.push(<PictureInPicturePlaceholder />);
+      mediaIcons.push('video-camera');
+    } else if (status.get('media_attachments').size > 0) {
+      if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
+        media.push(<AttachmentList media={status.get('media_attachments')} />);
+      } else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
+        const attachment = status.getIn(['media_attachments', 0]);
+        const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
+
+        media.push(
+          <Audio
+            src={attachment.get('url')}
+            alt={description}
+            lang={language}
+            duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
+            poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
+            backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
+            foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
+            accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
+            sensitive={status.get('sensitive')}
+            visible={this.props.showMedia}
+            blurhash={attachment.get('blurhash')}
+            height={150}
+            onToggleVisibility={this.props.onToggleMediaVisibility}
+          />,
+        );
+        mediaIcons.push('music');
+      } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
+        const attachment = status.getIn(['media_attachments', 0]);
+        const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
+
+        media.push(
+          <Video
+            preview={attachment.get('preview_url')}
+            frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
+            blurhash={attachment.get('blurhash')}
+            src={attachment.get('url')}
+            alt={description}
+            lang={language}
+            inline
+            sensitive={status.get('sensitive')}
+            letterbox={settings.getIn(['media', 'letterbox'])}
+            fullwidth={settings.getIn(['media', 'fullwidth'])}
+            preventPlayback={!expanded}
+            onOpenVideo={this.handleOpenVideo}
+            autoplay
+            visible={this.props.showMedia}
+            onToggleVisibility={this.props.onToggleMediaVisibility}
+          />,
+        );
+        mediaIcons.push('video-camera');
+      } else {
+        media.push(
+          <MediaGallery
+            standalone
+            sensitive={status.get('sensitive')}
+            media={status.get('media_attachments')}
+            lang={language}
+            letterbox={settings.getIn(['media', 'letterbox'])}
+            fullwidth={settings.getIn(['media', 'fullwidth'])}
+            hidden={!expanded}
+            onOpenMedia={this.props.onOpenMedia}
+            visible={this.props.showMedia}
+            onToggleVisibility={this.props.onToggleMediaVisibility}
+          />,
+        );
+        mediaIcons.push('picture-o');
+      }
+    } else if (status.get('card')) {
+      media.push(<Card sensitive={status.get('sensitive')} onOpenMedia={this.props.onOpenMedia} card={status.get('card')} />);
+      mediaIcons.push('link');
+    }
+
+    if (status.get('poll')) {
+      contentMedia.push(<PollContainer pollId={status.get('poll')} lang={status.get('language')} />);
+      contentMediaIcons.push('tasks');
+    }
+
+    if (status.get('application')) {
+      applicationLink = <> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener noreferrer'>{status.getIn(['application', 'name'])}</a></>;
+    }
+
+    const visibilityLink = <> · <VisibilityIcon visibility={status.get('visibility')} /></>;
+
+    if (status.get('visibility') === 'direct') {
+      reblogIcon = 'envelope';
+    } else if (status.get('visibility') === 'private') {
+      reblogIcon = 'lock';
+    }
+
+    if (!['unlisted', 'public'].includes(status.get('visibility'))) {
+      reblogLink = null;
+    } else if (this.props.history) {
+      reblogLink = (
+        <>
+          {' · '}
+          <Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reblogs`} className='detailed-status__link'>
+            <Icon id={reblogIcon} />
+            <span className='detailed-status__reblogs'>
+              <AnimatedNumber value={status.get('reblogs_count')} />
+            </span>
+          </Link>
+        </>
+      );
+    } else {
+      reblogLink = (
+        <>
+          {' · '}
+          <a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}>
+            <Icon id={reblogIcon} />
+            <span className='detailed-status__reblogs'>
+              <AnimatedNumber value={status.get('reblogs_count')} />
+            </span>
+          </a>
+        </>
+      );
+    }
+
+    if (this.props.history) {
+      favouriteLink = (
+        <Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/favourites`} className='detailed-status__link'>
+          <Icon id='star' />
+          <span className='detailed-status__favorites'>
+            <AnimatedNumber value={status.get('favourites_count')} />
+          </span>
+        </Link>
+      );
+    } else {
+      favouriteLink = (
+        <a href={`/interact/${status.get('id')}?type=favourite`} className='detailed-status__link' onClick={this.handleModalLink}>
+          <Icon id='star' />
+          <span className='detailed-status__favorites'>
+            <AnimatedNumber value={status.get('favourites_count')} />
+          </span>
+        </a>
+      );
+    }
+
+    if (status.get('edited_at')) {
+      edited = (
+        <>
+          {' · '}
+          <EditedTimestamp statusId={status.get('id')} timestamp={status.get('edited_at')} />
+        </>
+      );
+    }
+
+    const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
+    contentMedia.push(hashtagBar);
+
+    return (
+      <div style={outerStyle}>
+        <div ref={this.setRef} className={classNames('detailed-status', `detailed-status-${status.get('visibility')}`, { compact })} data-status-by={status.getIn(['account', 'acct'])}>
+          <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name'>
+            <div className='detailed-status__display-avatar'><Avatar account={status.get('account')} size={48} /></div>
+            <DisplayName account={status.get('account')} localDomain={this.props.domain} />
+          </a>
+
+          <StatusContent
+            status={status}
+            media={contentMedia}
+            extraMedia={extraMedia}
+            mediaIcons={contentMediaIcons}
+            expanded={expanded}
+            collapsed={false}
+            onExpandedToggle={onToggleHidden}
+            onTranslate={this.handleTranslate}
+            parseClick={this.parseClick}
+            onUpdate={this.handleChildUpdate}
+            tagLinks={settings.get('tag_misleading_links')}
+            rewriteMentions={settings.get('rewrite_mentions')}
+            disabled
+            {...statusContentProps}
+          />
+
+          <StatusReactions
+            statusId={status.get('id')}
+            reactions={status.get('reactions')}
+            addReaction={this.props.onReactionAdd}
+            removeReaction={this.props.onReactionRemove}
+            canReact={this.context.identity.signedIn}
+          />
+
+          <div className='detailed-status__meta'>
+            <a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'>
+              <FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
+            </a>{edited}{visibilityLink}{applicationLink}{reblogLink} · {favouriteLink}
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default withRouter(injectIntl(DetailedStatus));
diff --git a/app/javascript/flavours/blobfox/features/status/containers/detailed_status_container.js b/app/javascript/flavours/blobfox/features/status/containers/detailed_status_container.js
new file mode 100644
index 00000000000000..40803441d201d2
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/status/containers/detailed_status_container.js
@@ -0,0 +1,176 @@
+import { defineMessages, injectIntl } from 'react-intl';
+
+import { connect } from 'react-redux';
+
+import { showAlertForError } from '../../../actions/alerts';
+import { initBlockModal } from '../../../actions/blocks';
+import { initBoostModal } from '../../../actions/boosts';
+import {
+  replyCompose,
+  mentionCompose,
+  directCompose,
+} from '../../../actions/compose';
+import {
+  reblog,
+  favourite,
+  unreblog,
+  unfavourite,
+  pin,
+  unpin,
+} from '../../../actions/interactions';
+import { openModal } from '../../../actions/modal';
+import { initMuteModal } from '../../../actions/mutes';
+import { initReport } from '../../../actions/reports';
+import {
+  muteStatus,
+  unmuteStatus,
+  deleteStatus,
+} from '../../../actions/statuses';
+import { boostModal, deleteModal } from '../../../initial_state';
+import { makeGetStatus } from '../../../selectors';
+import DetailedStatus from '../components/detailed_status';
+
+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? Favorites and boosts will be lost, and replies to the original post will be orphaned.' },
+  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?' },
+});
+
+const makeMapStateToProps = () => {
+  const getStatus = makeGetStatus();
+
+  const mapStateToProps = (state, props) => ({
+    status: getStatus(state, props),
+    domain: state.getIn(['meta', 'domain']),
+    settings: state.get('local_settings'),
+  });
+
+  return mapStateToProps;
+};
+
+const mapDispatchToProps = (dispatch, { intl }) => ({
+
+  onReply (status, router) {
+    dispatch((_, getState) => {
+      let state = getState();
+      if (state.getIn(['compose', 'text']).trim().length !== 0) {
+        dispatch(openModal({
+          modalType: 'CONFIRM',
+          modalProps: {
+            message: intl.formatMessage(messages.replyMessage),
+            confirm: intl.formatMessage(messages.replyConfirm),
+            onConfirm: () => dispatch(replyCompose(status, router)),
+          },
+        }));
+      } else {
+        dispatch(replyCompose(status, router));
+      }
+    });
+  },
+
+  onModalReblog (status, privacy) {
+    dispatch(reblog(status, privacy));
+  },
+
+  onReblog (status, e) {
+    if (status.get('reblogged')) {
+      dispatch(unreblog(status));
+    } else {
+      if (e.shiftKey || !boostModal) {
+        this.onModalReblog(status);
+      } else {
+        dispatch(initBoostModal({ status, onReblog: this.onModalReblog }));
+      }
+    }
+  },
+
+  onFavourite (status) {
+    if (status.get('favourited')) {
+      dispatch(unfavourite(status));
+    } else {
+      dispatch(favourite(status));
+    }
+  },
+
+  onPin (status) {
+    if (status.get('pinned')) {
+      dispatch(unpin(status));
+    } else {
+      dispatch(pin(status));
+    }
+  },
+
+  onEmbed (status) {
+    dispatch(openModal({
+      modalType: 'EMBED',
+      modalProps: {
+        id: status.get('id'),
+        onError: error => dispatch(showAlertForError(error)),
+      },
+    }));
+  },
+
+  onDelete (status, history, withRedraft = false) {
+    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)),
+        },
+      }));
+    }
+  },
+
+  onDirect (account, router) {
+    dispatch(directCompose(account, router));
+  },
+
+  onMention (account, router) {
+    dispatch(mentionCompose(account, router));
+  },
+
+  onOpenMedia (media, index, lang) {
+    dispatch(openModal({
+      modalType: 'MEDIA',
+      modalProps: { media, index, lang },
+    }));
+  },
+
+  onOpenVideo (media, lang, options) {
+    dispatch(openModal({
+      modalType: 'VIDEO',
+      modalProps: { media, lang, options },
+    }));
+  },
+
+  onBlock (status) {
+    const account = status.get('account');
+    dispatch(initBlockModal(account));
+  },
+
+  onReport (status) {
+    dispatch(initReport(status.get('account'), status));
+  },
+
+  onMute (account) {
+    dispatch(initMuteModal(account));
+  },
+
+  onMuteConversation (status) {
+    if (status.get('muted')) {
+      dispatch(unmuteStatus(status.get('id')));
+    } else {
+      dispatch(muteStatus(status.get('id')));
+    }
+  },
+
+});
+
+export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(DetailedStatus));
diff --git a/app/javascript/flavours/blobfox/features/status/index.jsx b/app/javascript/flavours/blobfox/features/status/index.jsx
new file mode 100644
index 00000000000000..0e387e6f237179
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/status/index.jsx
@@ -0,0 +1,807 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl } from 'react-intl';
+
+import classNames from 'classnames';
+import { Helmet } from 'react-helmet';
+import { withRouter } from 'react-router-dom';
+
+import Immutable from 'immutable';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+
+import { HotKeys } from 'react-hotkeys';
+
+import { Icon }  from 'flavours/blobfox/components/icon';
+import { LoadingIndicator } from 'flavours/blobfox/components/loading_indicator';
+import ScrollContainer from 'flavours/blobfox/containers/scroll_container';
+import BundleColumnError from 'flavours/blobfox/features/ui/components/bundle_column_error';
+import { autoUnfoldCW } from 'flavours/blobfox/utils/content_warning';
+import { WithRouterPropTypes } from 'flavours/blobfox/utils/react_router';
+
+import { initBlockModal } from '../../actions/blocks';
+import { initBoostModal } from '../../actions/boosts';
+import {
+  replyCompose,
+  mentionCompose,
+  directCompose,
+} from '../../actions/compose';
+import {
+  favourite,
+  unfavourite,
+  bookmark,
+  unbookmark,
+  reblog,
+  unreblog,
+  pin,
+  unpin,
+  addReaction,
+  removeReaction,
+} from '../../actions/interactions';
+import { changeLocalSetting } from '../../actions/local_settings';
+import { openModal } from '../../actions/modal';
+import { initMuteModal } from '../../actions/mutes';
+import { initReport } from '../../actions/reports';
+import {
+  fetchStatus,
+  muteStatus,
+  unmuteStatus,
+  deleteStatus,
+  editStatus,
+  hideStatus,
+  revealStatus,
+  translateStatus,
+  undoStatusTranslation,
+} from '../../actions/statuses';
+import ColumnHeader from '../../components/column_header';
+import { textForScreenReader, defaultMediaVisibility } from '../../components/status';
+import StatusContainer from '../../containers/status_container';
+import { boostModal, favouriteModal, deleteModal } from '../../initial_state';
+import { makeGetStatus, makeGetPictureInPicture } from '../../selectors';
+import Column from '../ui/components/column';
+import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
+
+import ActionBar from './components/action_bar';
+import DetailedStatus from './components/detailed_status';
+
+
+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? Favorites and boosts will be lost, and replies to the original post will be orphaned.' },
+  revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' },
+  hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' },
+  statusTitleWithAttachments: { id: 'status.title.with_attachments', defaultMessage: '{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}' },
+  detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' },
+  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?' },
+  tootHeading: { id: 'account.posts_with_replies', defaultMessage: 'Posts and replies' },
+});
+
+const makeMapStateToProps = () => {
+  const getStatus = makeGetStatus();
+  const getPictureInPicture = makeGetPictureInPicture();
+
+  const getAncestorsIds = createSelector([
+    (_, { id }) => id,
+    state => state.getIn(['contexts', 'inReplyTos']),
+  ], (statusId, inReplyTos) => {
+    let ancestorsIds = Immutable.List();
+    ancestorsIds = ancestorsIds.withMutations(mutable => {
+      let id = statusId;
+
+      while (id && !mutable.includes(id)) {
+        mutable.unshift(id);
+        id = inReplyTos.get(id);
+      }
+    });
+
+    return ancestorsIds;
+  });
+
+  const getDescendantsIds = createSelector([
+    (_, { id }) => id,
+    state => state.getIn(['contexts', 'replies']),
+    state => state.get('statuses'),
+  ], (statusId, contextReplies, statuses) => {
+    let descendantsIds = [];
+    const ids = [statusId];
+
+    while (ids.length > 0) {
+      let id        = ids.pop();
+      const replies = contextReplies.get(id);
+
+      if (statusId !== id) {
+        descendantsIds.push(id);
+      }
+
+      if (replies) {
+        replies.reverse().forEach(reply => {
+          if (!ids.includes(reply) && !descendantsIds.includes(reply) && statusId !== reply) ids.push(reply);
+        });
+      }
+    }
+
+    let insertAt = descendantsIds.findIndex((id) => statuses.get(id).get('in_reply_to_account_id') !== statuses.get(id).get('account'));
+    if (insertAt !== -1) {
+      descendantsIds.forEach((id, idx) => {
+        if (idx > insertAt && statuses.get(id).get('in_reply_to_account_id') === statuses.get(id).get('account')) {
+          descendantsIds.splice(idx, 1);
+          descendantsIds.splice(insertAt, 0, id);
+          insertAt += 1;
+        }
+      });
+    }
+
+    return Immutable.List(descendantsIds);
+  });
+
+  const mapStateToProps = (state, props) => {
+    const status = getStatus(state, { id: props.params.statusId });
+
+    let ancestorsIds   = Immutable.List();
+    let descendantsIds = Immutable.List();
+
+    if (status) {
+      ancestorsIds   = getAncestorsIds(state, { id: status.get('in_reply_to_id') });
+      descendantsIds = getDescendantsIds(state, { id: status.get('id') });
+    }
+
+    return {
+      isLoading: state.getIn(['statuses', props.params.statusId, 'isLoading']),
+      status,
+      ancestorsIds,
+      descendantsIds,
+      settings: state.get('local_settings'),
+      askReplyConfirmation: state.getIn(['local_settings', 'confirm_before_clearing_draft']) && state.getIn(['compose', 'text']).trim().length !== 0,
+      domain: state.getIn(['meta', 'domain']),
+      pictureInPicture: getPictureInPicture(state, { id: props.params.statusId }),
+    };
+  };
+
+  return mapStateToProps;
+};
+
+const truncate = (str, num) => {
+  const arr = Array.from(str);
+  if (arr.length > num) {
+    return arr.slice(0, num).join('') + '…';
+  } else {
+    return str;
+  }
+};
+
+const titleFromStatus = (intl, status) => {
+  const displayName = status.getIn(['account', 'display_name']);
+  const username = status.getIn(['account', 'username']);
+  const user = displayName.trim().length === 0 ? username : displayName;
+  const text = status.get('search_index');
+  const attachmentCount = status.get('media_attachments').size;
+
+  return text ? `${user}: "${truncate(text, 30)}"` : intl.formatMessage(messages.statusTitleWithAttachments, { user, attachmentCount });
+};
+
+class Status extends ImmutablePureComponent {
+
+  static contextTypes = {
+    identity: PropTypes.object,
+  };
+
+  static propTypes = {
+    params: PropTypes.object.isRequired,
+    dispatch: PropTypes.func.isRequired,
+    status: ImmutablePropTypes.map,
+    isLoading: PropTypes.bool,
+    settings: ImmutablePropTypes.map.isRequired,
+    ancestorsIds: ImmutablePropTypes.list.isRequired,
+    descendantsIds: ImmutablePropTypes.list.isRequired,
+    intl: PropTypes.object.isRequired,
+    askReplyConfirmation: PropTypes.bool,
+    multiColumn: PropTypes.bool,
+    domain: PropTypes.string.isRequired,
+    pictureInPicture: ImmutablePropTypes.contains({
+      inUse: PropTypes.bool,
+      available: PropTypes.bool,
+    }),
+    ...WithRouterPropTypes
+  };
+
+  state = {
+    fullscreen: false,
+    isExpanded: undefined,
+    threadExpanded: undefined,
+    statusId: undefined,
+    loadedStatusId: undefined,
+    showMedia: undefined,
+    revealBehindCW: undefined,
+  };
+
+  componentDidMount () {
+    attachFullscreenListener(this.onFullScreenChange);
+    this.props.dispatch(fetchStatus(this.props.params.statusId));
+    this._scrollStatusIntoView();
+  }
+
+  static getDerivedStateFromProps(props, state) {
+    let update = {};
+    let updated = false;
+
+    if (props.params.statusId && state.statusId !== props.params.statusId) {
+      props.dispatch(fetchStatus(props.params.statusId));
+      update.threadExpanded = undefined;
+      update.statusId = props.params.statusId;
+      updated = true;
+    }
+
+    const revealBehindCW = props.settings.getIn(['media', 'reveal_behind_cw']);
+    if (revealBehindCW !== state.revealBehindCW) {
+      update.revealBehindCW = revealBehindCW;
+      if (revealBehindCW) update.showMedia = defaultMediaVisibility(props.status, props.settings);
+      updated = true;
+    }
+
+    if (props.status && state.loadedStatusId !== props.status.get('id')) {
+      update.showMedia = defaultMediaVisibility(props.status, props.settings);
+      update.loadedStatusId = props.status.get('id');
+      update.isExpanded = autoUnfoldCW(props.settings, props.status);
+      updated = true;
+    }
+
+    return updated ? update : null;
+  }
+
+  handleToggleHidden = () => {
+    const { status } = this.props;
+
+    if (this.props.settings.getIn(['content_warnings', 'shared_state'])) {
+      if (status.get('hidden')) {
+        this.props.dispatch(revealStatus(status.get('id')));
+      } else {
+        this.props.dispatch(hideStatus(status.get('id')));
+      }
+    } else if (this.props.status.get('spoiler_text')) {
+      this.setExpansion(!this.state.isExpanded);
+    }
+  };
+
+  handleToggleMediaVisibility = () => {
+    this.setState({ showMedia: !this.state.showMedia });
+  };
+
+  handleModalFavourite = (status) => {
+    this.props.dispatch(favourite(status));
+  };
+
+  handleFavouriteClick = (status, e) => {
+    const { dispatch } = this.props;
+    const { signedIn } = this.context.identity;
+
+    if (signedIn) {
+      if (status.get('favourited')) {
+        dispatch(unfavourite(status));
+      } else {
+        if ((e && e.shiftKey) || !favouriteModal) {
+          this.handleModalFavourite(status);
+        } else {
+          dispatch(openModal({
+            modalType: 'FAVOURITE',
+            modalProps: {
+              status,
+              onFavourite: this.handleModalFavourite,
+            },
+          }));
+        }
+      }
+    } else {
+      dispatch(openModal({
+        modalType: 'INTERACTION',
+        modalProps: {
+          type: 'favourite',
+          accountId: status.getIn(['account', 'id']),
+          url: status.get('uri'),
+        },
+      }));
+    }
+  };
+
+  handleReactionAdd = (statusId, name, url) => {
+    const { dispatch } = this.props;
+    const { signedIn } = this.context.identity;
+
+    if (signedIn) {
+      dispatch(addReaction(statusId, name, url));
+    }
+  };
+
+  handleReactionRemove = (statusId, name) => {
+    this.props.dispatch(removeReaction(statusId, name));
+  };
+
+  handlePin = (status) => {
+    if (status.get('pinned')) {
+      this.props.dispatch(unpin(status));
+    } else {
+      this.props.dispatch(pin(status));
+    }
+  };
+
+  handleReplyClick = (status) => {
+    const { askReplyConfirmation, dispatch, intl } = this.props;
+    const { signedIn } = this.context.identity;
+
+    if (signedIn) {
+      if (askReplyConfirmation) {
+        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, this.props.history)),
+          },
+        }));
+      } else {
+        dispatch(replyCompose(status, this.props.history));
+      }
+    } else {
+      dispatch(openModal({
+        modalType: 'INTERACTION',
+        modalProps: {
+          type: 'reply',
+          accountId: status.getIn(['account', 'id']),
+          url: status.get('uri'),
+        },
+      }));
+    }
+  };
+
+  handleModalReblog = (status, privacy) => {
+    const { dispatch } = this.props;
+
+    if (status.get('reblogged')) {
+      dispatch(unreblog(status));
+    } else {
+      dispatch(reblog(status, privacy));
+    }
+  };
+
+  handleReblogClick = (status, e) => {
+    const { settings, dispatch } = this.props;
+    const { signedIn } = this.context.identity;
+
+    if (signedIn) {
+      if (settings.get('confirm_boost_missing_media_description') && status.get('media_attachments').some(item => !item.get('description')) && !status.get('reblogged')) {
+        dispatch(initBoostModal({ status, onReblog: this.handleModalReblog, missingMediaDescription: true }));
+      } else if ((e && e.shiftKey) || !boostModal) {
+        this.handleModalReblog(status);
+      } else {
+        dispatch(initBoostModal({ status, onReblog: this.handleModalReblog }));
+      }
+    } else {
+      dispatch(openModal({
+        modalType: 'INTERACTION',
+        modalProps: {
+          type: 'reblog',
+          accountId: status.getIn(['account', 'id']),
+          url: status.get('uri'),
+        },
+      }));
+    }
+  };
+
+  handleBookmarkClick = (status) => {
+    if (status.get('bookmarked')) {
+      this.props.dispatch(unbookmark(status));
+    } else {
+      this.props.dispatch(bookmark(status));
+    }
+  };
+
+  handleDeleteClick = (status, history, withRedraft = false) => {
+    const { dispatch, intl } = this.props;
+
+    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)),
+        },
+      }));
+    }
+  };
+
+  handleEditClick = (status, history) => {
+    this.props.dispatch(editStatus(status.get('id'), history));
+  };
+
+  handleDirectClick = (account, history) => {
+    this.props.dispatch(directCompose(account, history));
+  };
+
+  handleMentionClick = (account, history) => {
+    this.props.dispatch(mentionCompose(account, history));
+  };
+
+  handleOpenMedia = (media, index, lang) => {
+    this.props.dispatch(openModal({
+      modalType: 'MEDIA',
+      modalProps: { statusId: this.props.status.get('id'), media, index, lang },
+    }));
+  };
+
+  handleOpenVideo = (media, lang, options) => {
+    this.props.dispatch(openModal({
+      modalType: 'VIDEO',
+      modalProps: { statusId: this.props.status.get('id'), media, lang, options },
+    }));
+  };
+
+  handleHotkeyOpenMedia = e => {
+    const { status } = this.props;
+
+    e.preventDefault();
+
+    if (status.get('media_attachments').size > 0) {
+      if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
+        this.handleOpenVideo(status.getIn(['media_attachments', 0]), { startTime: 0 });
+      } else {
+        this.handleOpenMedia(status.get('media_attachments'), 0);
+      }
+    }
+  };
+
+  handleMuteClick = (account) => {
+    this.props.dispatch(initMuteModal(account));
+  };
+
+  handleConversationMuteClick = (status) => {
+    if (status.get('muted')) {
+      this.props.dispatch(unmuteStatus(status.get('id')));
+    } else {
+      this.props.dispatch(muteStatus(status.get('id')));
+    }
+  };
+
+  handleToggleAll = () => {
+    const { status, ancestorsIds, descendantsIds, settings } = this.props;
+    const statusIds = [status.get('id')].concat(ancestorsIds.toJS(), descendantsIds.toJS());
+    let { isExpanded } = this.state;
+
+    if (settings.getIn(['content_warnings', 'shared_state']))
+      isExpanded = !status.get('hidden');
+
+    if (!isExpanded) {
+      this.props.dispatch(revealStatus(statusIds));
+    } else {
+      this.props.dispatch(hideStatus(statusIds));
+    }
+
+    this.setState({ isExpanded: !isExpanded, threadExpanded: !isExpanded });
+  };
+
+  handleTranslate = status => {
+    const { dispatch } = this.props;
+
+    if (status.get('translation')) {
+      dispatch(undoStatusTranslation(status.get('id'), status.get('poll')));
+    } else {
+      dispatch(translateStatus(status.get('id')));
+    }
+  };
+
+  handleBlockClick = (status) => {
+    const { dispatch } = this.props;
+    const account = status.get('account');
+    dispatch(initBlockModal(account));
+  };
+
+  handleReport = (status) => {
+    this.props.dispatch(initReport(status.get('account'), status));
+  };
+
+  handleEmbed = (status) => {
+    this.props.dispatch(openModal({
+      modalType: 'EMBED',
+      modalProps: { id: status.get('id') },
+    }));
+  };
+
+  handleHotkeyToggleSensitive = () => {
+    this.handleToggleMediaVisibility();
+  };
+
+  handleHotkeyMoveUp = () => {
+    this.handleMoveUp(this.props.status.get('id'));
+  };
+
+  handleHotkeyMoveDown = () => {
+    this.handleMoveDown(this.props.status.get('id'));
+  };
+
+  handleHotkeyReply = e => {
+    e.preventDefault();
+    this.handleReplyClick(this.props.status);
+  };
+
+  handleHotkeyFavourite = () => {
+    this.handleFavouriteClick(this.props.status);
+  };
+
+  handleHotkeyBoost = () => {
+    this.handleReblogClick(this.props.status);
+  };
+
+  handleHotkeyBookmark = () => {
+    this.handleBookmarkClick(this.props.status);
+  };
+
+  handleHotkeyMention = e => {
+    e.preventDefault();
+    this.handleMentionClick(this.props.status);
+  };
+
+  handleHotkeyOpenProfile = () => {
+    this.props.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
+  };
+
+  handleMoveUp = id => {
+    const { status, ancestorsIds, descendantsIds } = this.props;
+
+    if (id === status.get('id')) {
+      this._selectChild(ancestorsIds.size - 1, true);
+    } else {
+      let index = ancestorsIds.indexOf(id);
+
+      if (index === -1) {
+        index = descendantsIds.indexOf(id);
+        this._selectChild(ancestorsIds.size + index, true);
+      } else {
+        this._selectChild(index - 1, true);
+      }
+    }
+  };
+
+  handleMoveDown = id => {
+    const { status, ancestorsIds, descendantsIds } = this.props;
+
+    if (id === status.get('id')) {
+      this._selectChild(ancestorsIds.size + 1, false);
+    } else {
+      let index = ancestorsIds.indexOf(id);
+
+      if (index === -1) {
+        index = descendantsIds.indexOf(id);
+        this._selectChild(ancestorsIds.size + index + 2, false);
+      } else {
+        this._selectChild(index + 1, false);
+      }
+    }
+  };
+
+  _selectChild (index, align_top) {
+    const container = this.node;
+    const element = container.querySelectorAll('.focusable')[index];
+
+    if (element) {
+      if (align_top && container.scrollTop > element.offsetTop) {
+        element.scrollIntoView(true);
+      } else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
+        element.scrollIntoView(false);
+      }
+      element.focus();
+    }
+  }
+
+  handleHeaderClick = () => {
+    this.column.scrollTop();
+  };
+
+  renderChildren (list, ancestors) {
+    const { params: { statusId } } = this.props;
+
+    return list.map((id, i) => (
+      <StatusContainer
+        key={id}
+        id={id}
+        expanded={this.state.threadExpanded}
+        onMoveUp={this.handleMoveUp}
+        onMoveDown={this.handleMoveDown}
+        contextType='thread'
+        previousId={i > 0 ? list.get(i - 1) : undefined}
+        nextId={list.get(i + 1) || (ancestors && statusId)}
+        rootId={statusId}
+      />
+    ));
+  }
+
+  setExpansion = value => {
+    this.setState({ isExpanded: value });
+  };
+
+  setRef = c => {
+    this.node = c;
+  };
+
+  setColumnRef = c => {
+    this.column = c;
+  };
+
+  _scrollStatusIntoView () {
+    const { status, multiColumn } = this.props;
+
+    if (status) {
+      window.requestAnimationFrame(() => {
+        this.node?.querySelector('.detailed-status__wrapper')?.scrollIntoView(true);
+
+        // In the single-column interface, `scrollIntoView` will put the post behind the header,
+        // so compensate for that.
+        if (!multiColumn) {
+          const offset = document.querySelector('.column-header__wrapper')?.getBoundingClientRect()?.bottom;
+          if (offset) {
+            const scrollingElement = document.scrollingElement || document.body;
+            scrollingElement.scrollBy(0, -offset);
+          }
+        }
+      });
+    }
+  }
+
+  componentDidUpdate (prevProps) {
+    const { status, ancestorsIds } = this.props;
+
+    if (status && (ancestorsIds.size > prevProps.ancestorsIds.size || prevProps.status?.get('id') !== status.get('id'))) {
+      this._scrollStatusIntoView();
+    }
+  }
+
+  componentWillUnmount () {
+    detachFullscreenListener(this.onFullScreenChange);
+  }
+
+  onFullScreenChange = () => {
+    this.setState({ fullscreen: isFullscreen() });
+  };
+
+  shouldUpdateScroll = (prevRouterProps, { location }) => {
+    // Do not change scroll when opening a modal
+    if (location.state?.mastodonModalKey !== prevRouterProps?.location?.state?.mastodonModalKey) {
+      return false;
+    }
+
+    // Scroll to focused post if it is loaded
+    const child = this.node?.querySelector('.detailed-status__wrapper');
+    if (child) {
+      return [0, child.offsetTop];
+    }
+
+    // Do not scroll otherwise, `componentDidUpdate` will take care of that
+    return false;
+  };
+
+  render () {
+    let ancestors, descendants;
+    const { isLoading, status, settings, ancestorsIds, descendantsIds, intl, domain, multiColumn, pictureInPicture } = this.props;
+    const { fullscreen } = this.state;
+
+    if (isLoading) {
+      return (
+        <Column>
+          <LoadingIndicator />
+        </Column>
+      );
+    }
+
+    if (status === null) {
+      return (
+        <BundleColumnError multiColumn={multiColumn} errorType='routing' />
+      );
+    }
+
+    const isExpanded = settings.getIn(['content_warnings', 'shared_state']) ? !status.get('hidden') : this.state.isExpanded;
+
+    if (ancestorsIds && ancestorsIds.size > 0) {
+      ancestors = <>{this.renderChildren(ancestorsIds, true)}</>;
+    }
+
+    if (descendantsIds && descendantsIds.size > 0) {
+      descendants = <>{this.renderChildren(descendantsIds)}</>;
+    }
+
+    const isLocal = status.getIn(['account', 'acct'], '').indexOf('@') === -1;
+    const isIndexable = !status.getIn(['account', 'noindex']);
+
+    const handlers = {
+      moveUp: this.handleHotkeyMoveUp,
+      moveDown: this.handleHotkeyMoveDown,
+      reply: this.handleHotkeyReply,
+      favourite: this.handleHotkeyFavourite,
+      boost: this.handleHotkeyBoost,
+      bookmark: this.handleHotkeyBookmark,
+      mention: this.handleHotkeyMention,
+      openProfile: this.handleHotkeyOpenProfile,
+      toggleHidden: this.handleToggleHidden,
+      toggleSensitive: this.handleHotkeyToggleSensitive,
+      openMedia: this.handleHotkeyOpenMedia,
+    };
+
+    return (
+      <Column bindToDocument={!multiColumn} ref={this.setColumnRef} label={intl.formatMessage(messages.detailedStatus)}>
+        <ColumnHeader
+          icon='comment'
+          title={intl.formatMessage(messages.tootHeading)}
+          onClick={this.handleHeaderClick}
+          showBackButton
+          multiColumn={multiColumn}
+          extraButton={(
+            <button type='button' className='column-header__button' title={intl.formatMessage(!isExpanded ? messages.revealAll : messages.hideAll)} aria-label={intl.formatMessage(!isExpanded ? messages.revealAll : messages.hideAll)} onClick={this.handleToggleAll}><Icon id={!isExpanded ? 'eye-slash' : 'eye'} /></button>
+          )}
+        />
+
+        <ScrollContainer scrollKey='thread' shouldUpdateScroll={this.shouldUpdateScroll}>
+          <div className={classNames('scrollable', { fullscreen })} ref={this.setRef}>
+            {ancestors}
+
+            <HotKeys handlers={handlers}>
+              <div className={classNames('focusable', 'detailed-status__wrapper', `detailed-status__wrapper-${status.get('visibility')}`)} tabIndex={0} aria-label={textForScreenReader(intl, status, false, isExpanded)}>
+                <DetailedStatus
+                  key={`details-${status.get('id')}`}
+                  status={status}
+                  settings={settings}
+                  onOpenVideo={this.handleOpenVideo}
+                  onOpenMedia={this.handleOpenMedia}
+                  onReactionAdd={this.handleReactionAdd}
+                  onReactionRemove={this.handleReactionRemove}
+                  expanded={isExpanded}
+                  onToggleHidden={this.handleToggleHidden}
+                  onTranslate={this.handleTranslate}
+                  domain={domain}
+                  showMedia={this.state.showMedia}
+                  onToggleMediaVisibility={this.handleToggleMediaVisibility}
+                  pictureInPicture={pictureInPicture}
+                />
+
+                <ActionBar
+                  key={`action-bar-${status.get('id')}`}
+                  status={status}
+                  onReply={this.handleReplyClick}
+                  onFavourite={this.handleFavouriteClick}
+                  onReactionAdd={this.handleReactionAdd}
+                  onReblog={this.handleReblogClick}
+                  onBookmark={this.handleBookmarkClick}
+                  onDelete={this.handleDeleteClick}
+                  onEdit={this.handleEditClick}
+                  onDirect={this.handleDirectClick}
+                  onMention={this.handleMentionClick}
+                  onMute={this.handleMuteClick}
+                  onMuteConversation={this.handleConversationMuteClick}
+                  onBlock={this.handleBlockClick}
+                  onReport={this.handleReport}
+                  onPin={this.handlePin}
+                  onEmbed={this.handleEmbed}
+                />
+              </div>
+            </HotKeys>
+
+            {descendants}
+          </div>
+        </ScrollContainer>
+
+        <Helmet>
+          <title>{titleFromStatus(intl, status)}</title>
+          <meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} />
+          <link rel='canonical' href={status.get('url')} />
+        </Helmet>
+      </Column>
+    );
+  }
+
+}
+
+export default withRouter(injectIntl(connect(makeMapStateToProps)(Status)));
diff --git a/app/javascript/flavours/blobfox/features/subscribed_languages_modal/index.jsx b/app/javascript/flavours/blobfox/features/subscribed_languages_modal/index.jsx
new file mode 100644
index 00000000000000..31308c6ce822fa
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/subscribed_languages_modal/index.jsx
@@ -0,0 +1,127 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
+
+import { is, List as ImmutableList, Set as ImmutableSet } from 'immutable';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+
+import { followAccount } from 'flavours/blobfox/actions/accounts';
+import { Button } from 'flavours/blobfox/components/button';
+import { IconButton } from 'flavours/blobfox/components/icon_button';
+import Option from 'flavours/blobfox/features/report/components/option';
+import { languages as preloadedLanguages } from 'flavours/blobfox/initial_state';
+
+const messages = defineMessages({
+  close: { id: 'lightbox.close', defaultMessage: 'Close' },
+});
+
+const getAccountLanguages = createSelector([
+  (state, accountId) => state.getIn(['timelines', `account:${accountId}`, 'items'], ImmutableList()),
+  state => state.get('statuses'),
+], (statusIds, statuses) =>
+  new ImmutableSet(statusIds.map(statusId => statuses.get(statusId)).filter(status => !status.get('reblog')).map(status => status.get('language'))));
+
+const mapStateToProps = (state, { accountId }) => ({
+  acct: state.getIn(['accounts', accountId, 'acct']),
+  availableLanguages: getAccountLanguages(state, accountId),
+  selectedLanguages: ImmutableSet(state.getIn(['relationships', accountId, 'languages']) || ImmutableList()),
+});
+
+const mapDispatchToProps = (dispatch, { accountId }) => ({
+
+  onSubmit (languages) {
+    dispatch(followAccount(accountId, { languages }));
+  },
+
+});
+
+class SubscribedLanguagesModal extends ImmutablePureComponent {
+
+  static propTypes = {
+    accountId: PropTypes.string.isRequired,
+    acct: PropTypes.string.isRequired,
+    availableLanguages: ImmutablePropTypes.setOf(PropTypes.string),
+    selectedLanguages: ImmutablePropTypes.setOf(PropTypes.string),
+    onClose: PropTypes.func.isRequired,
+    languages: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)),
+    intl: PropTypes.object.isRequired,
+    submit: PropTypes.func.isRequired,
+  };
+
+  static defaultProps = {
+    languages: preloadedLanguages,
+  };
+
+  state = {
+    selectedLanguages: this.props.selectedLanguages,
+  };
+
+  handleLanguageToggle = (value, checked) => {
+    const { selectedLanguages } = this.state;
+
+    if (checked) {
+      this.setState({ selectedLanguages: selectedLanguages.add(value) });
+    } else {
+      this.setState({ selectedLanguages: selectedLanguages.delete(value) });
+    }
+  };
+
+  handleSubmit = () => {
+    this.props.onSubmit(this.state.selectedLanguages.toArray());
+    this.props.onClose();
+  };
+
+  renderItem (value) {
+    const language = this.props.languages.find(language => language[0] === value);
+    const checked = this.state.selectedLanguages.includes(value);
+
+    if (!language) {
+      return null;
+    }
+
+    return (
+      <Option
+        key={value}
+        name='languages'
+        value={value}
+        label={language[1]}
+        checked={checked}
+        onToggle={this.handleLanguageToggle}
+        multiple
+      />
+    );
+  }
+
+  render () {
+    const { acct, availableLanguages, selectedLanguages, intl, onClose } = this.props;
+
+    return (
+      <div className='modal-root__modal report-dialog-modal'>
+        <div className='report-modal__target'>
+          <IconButton className='report-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={20} />
+          <FormattedMessage id='subscribed_languages.target' defaultMessage='Change subscribed languages for {target}' values={{ target: <strong>{acct}</strong> }} />
+        </div>
+
+        <div className='report-dialog-modal__container'>
+          <p className='report-dialog-modal__lead'><FormattedMessage id='subscribed_languages.lead' defaultMessage='Only posts in selected languages will appear on your home and list timelines after the change. Select none to receive posts in all languages.' /></p>
+
+          <div>
+            {availableLanguages.union(selectedLanguages).delete(null).map(value => this.renderItem(value))}
+          </div>
+
+          <div className='flex-spacer' />
+
+          <div className='report-dialog-modal__actions'>
+            <Button disabled={is(this.state.selectedLanguages, this.props.selectedLanguages)} onClick={this.handleSubmit}><FormattedMessage id='subscribed_languages.save' defaultMessage='Save changes' /></Button>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(SubscribedLanguagesModal));
diff --git a/app/javascript/flavours/blobfox/features/ui/components/actions_modal.jsx b/app/javascript/flavours/blobfox/features/ui/components/actions_modal.jsx
new file mode 100644
index 00000000000000..6b58f9423b2c3a
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/actions_modal.jsx
@@ -0,0 +1,94 @@
+import PropTypes from 'prop-types';
+
+import classNames from 'classnames';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import { Avatar } from 'flavours/blobfox/components/avatar';
+import { DisplayName } from 'flavours/blobfox/components/display_name';
+import { RelativeTimestamp } from 'flavours/blobfox/components/relative_timestamp';
+import StatusContent from 'flavours/blobfox/components/status_content';
+
+import { IconButton } from '../../../components/icon_button';
+
+export default class ActionsModal extends ImmutablePureComponent {
+
+  static propTypes = {
+    status: ImmutablePropTypes.map,
+    onClick: PropTypes.func,
+    actions: PropTypes.arrayOf(PropTypes.shape({
+      active: PropTypes.bool,
+      href: PropTypes.string,
+      icon: PropTypes.string,
+      meta: PropTypes.string,
+      name: PropTypes.string,
+      text: PropTypes.string,
+    })),
+    renderItemContents: PropTypes.func,
+  };
+
+  renderAction = (action, i) => {
+    if (action === null) {
+      return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
+    }
+
+    const { icon = null, text, meta = null, active = false, href = '#' } = action;
+    let contents = this.props.renderItemContents && this.props.renderItemContents(action, i);
+
+    if (!contents) {
+      contents = (
+        <>
+          {icon && <IconButton title={text} icon={icon} role='presentation' tabIndex={-1} inverted />}
+          <div>
+            <div className={classNames({ 'actions-modal__item-label': !!meta })}>{text}</div>
+            <div>{meta}</div>
+          </div>
+        </>
+      );
+    }
+
+    return (
+      <li key={`${text}-${i}`}>
+        <a href={href} target='_blank' rel='noopener noreferrer' onClick={this.props.onClick} data-index={i} className={classNames('link', { active })}>
+          {contents}
+        </a>
+      </li>
+    );
+  };
+
+  render () {
+    const status = this.props.status && (
+      <div className='status light'>
+        <div className='boost-modal__status-header'>
+          <div className='boost-modal__status-time'>
+            <a href={this.props.status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
+              <RelativeTimestamp timestamp={this.props.status.get('created_at')} />
+            </a>
+          </div>
+
+          <a href={this.props.status.getIn(['account', 'url'])} className='status__display-name' rel='noopener noreferrer'>
+            <div className='status__avatar'>
+              <Avatar account={this.props.status.get('account')} size={48} />
+            </div>
+
+            <DisplayName account={this.props.status.get('account')} />
+          </a>
+        </div>
+
+        <StatusContent status={this.props.status} />
+      </div>
+    );
+
+    return (
+      <div className='modal-root__modal actions-modal'>
+        {status}
+
+        <ul className={classNames({ 'with-status': !!status })}>
+          {this.props.actions.map(this.renderAction)}
+        </ul>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/ui/components/audio_modal.jsx b/app/javascript/flavours/blobfox/features/ui/components/audio_modal.jsx
new file mode 100644
index 00000000000000..afdbade68ef987
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/audio_modal.jsx
@@ -0,0 +1,61 @@
+import PropTypes from 'prop-types';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
+import Audio from 'flavours/blobfox/features/audio';
+import Footer from 'flavours/blobfox/features/picture_in_picture/components/footer';
+
+const mapStateToProps = (state, { statusId }) => ({
+  status: state.getIn(['statuses', statusId]),
+  accountStaticAvatar: state.getIn(['accounts', state.getIn(['statuses', statusId, 'account']), 'avatar_static']),
+});
+
+class AudioModal extends ImmutablePureComponent {
+
+  static propTypes = {
+    media: ImmutablePropTypes.map.isRequired,
+    statusId: PropTypes.string.isRequired,
+    status: ImmutablePropTypes.map.isRequired,
+    accountStaticAvatar: PropTypes.string.isRequired,
+    options: PropTypes.shape({
+      autoPlay: PropTypes.bool,
+    }),
+    onClose: PropTypes.func.isRequired,
+    onChangeBackgroundColor: PropTypes.func.isRequired,
+  };
+
+  render () {
+    const { media, status, accountStaticAvatar, onClose } = this.props;
+    const options = this.props.options || {};
+    const language = status.getIn(['translation', 'language']) || status.get('language');
+    const description = media.getIn(['translation', 'description']) || media.get('description');
+
+    return (
+      <div className='modal-root__modal audio-modal'>
+        <div className='audio-modal__container'>
+          <Audio
+            src={media.get('url')}
+            alt={description}
+            lang={language}
+            duration={media.getIn(['meta', 'original', 'duration'], 0)}
+            height={150}
+            poster={media.get('preview_url') || accountStaticAvatar}
+            backgroundColor={media.getIn(['meta', 'colors', 'background'])}
+            foregroundColor={media.getIn(['meta', 'colors', 'foreground'])}
+            accentColor={media.getIn(['meta', 'colors', 'accent'])}
+            autoPlay={options.autoPlay}
+          />
+        </div>
+
+        <div className='media-modal__overlay'>
+          {status && <Footer statusId={status.get('id')} withOpenButton onClose={onClose} />}
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps, null, null, { forwardRef: true })(AudioModal);
diff --git a/app/javascript/flavours/blobfox/features/ui/components/block_modal.jsx b/app/javascript/flavours/blobfox/features/ui/components/block_modal.jsx
new file mode 100644
index 00000000000000..cfac692324aa23
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/block_modal.jsx
@@ -0,0 +1,100 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { injectIntl, FormattedMessage } from 'react-intl';
+
+import { connect } from 'react-redux';
+
+import { blockAccount } from '../../../actions/accounts';
+import { closeModal } from '../../../actions/modal';
+import { initReport } from '../../../actions/reports';
+import { Button } from '../../../components/button';
+import { makeGetAccount } from '../../../selectors';
+
+const makeMapStateToProps = () => {
+  const getAccount = makeGetAccount();
+
+  const mapStateToProps = state => ({
+    account: getAccount(state, state.getIn(['blocks', 'new', 'account_id'])),
+  });
+
+  return mapStateToProps;
+};
+
+const mapDispatchToProps = dispatch => {
+  return {
+    onConfirm(account) {
+      dispatch(blockAccount(account.get('id')));
+    },
+
+    onBlockAndReport(account) {
+      dispatch(blockAccount(account.get('id')));
+      dispatch(initReport(account));
+    },
+
+    onClose() {
+      dispatch(closeModal({
+        modalType: undefined,
+        ignoreFocus: false,
+      }));
+    },
+  };
+};
+
+class BlockModal extends PureComponent {
+
+  static propTypes = {
+    account: PropTypes.object.isRequired,
+    onClose: PropTypes.func.isRequired,
+    onBlockAndReport: PropTypes.func.isRequired,
+    onConfirm: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  handleClick = () => {
+    this.props.onClose();
+    this.props.onConfirm(this.props.account);
+  };
+
+  handleSecondary = () => {
+    this.props.onClose();
+    this.props.onBlockAndReport(this.props.account);
+  };
+
+  handleCancel = () => {
+    this.props.onClose();
+  };
+
+  render () {
+    const { account } = this.props;
+
+    return (
+      <div className='modal-root__modal block-modal'>
+        <div className='block-modal__container'>
+          <p>
+            <FormattedMessage
+              id='confirmations.block.message'
+              defaultMessage='Are you sure you want to block {name}?'
+              values={{ name: <strong>@{account.get('acct')}</strong> }}
+            />
+          </p>
+        </div>
+
+        <div className='block-modal__action-bar'>
+          <Button onClick={this.handleCancel} className='block-modal__cancel-button'>
+            <FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
+          </Button>
+          <Button onClick={this.handleSecondary} className='confirmation-modal__secondary-button'>
+            <FormattedMessage id='confirmations.block.block_and_report' defaultMessage='Block & Report' />
+          </Button>
+          <Button onClick={this.handleClick} autoFocus>
+            <FormattedMessage id='confirmations.block.confirm' defaultMessage='Block' />
+          </Button>
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default connect(makeMapStateToProps, mapDispatchToProps)(injectIntl(BlockModal));
diff --git a/app/javascript/flavours/blobfox/features/ui/components/boost_modal.jsx b/app/javascript/flavours/blobfox/features/ui/components/boost_modal.jsx
new file mode 100644
index 00000000000000..8a1ebe77b4c187
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/boost_modal.jsx
@@ -0,0 +1,131 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import classNames from 'classnames';
+import { withRouter } from 'react-router-dom';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
+import { changeBoostPrivacy } from 'flavours/blobfox/actions/boosts';
+import AttachmentList from 'flavours/blobfox/components/attachment_list';
+import { Icon }  from 'flavours/blobfox/components/icon';
+import VisibilityIcon from 'flavours/blobfox/components/status_visibility_icon';
+import PrivacyDropdown from 'flavours/blobfox/features/compose/components/privacy_dropdown';
+import { WithRouterPropTypes } from 'flavours/blobfox/utils/react_router';
+
+import { Avatar } from '../../../components/avatar';
+import { Button } from '../../../components/button';
+import { DisplayName } from '../../../components/display_name';
+import { RelativeTimestamp } from '../../../components/relative_timestamp';
+import StatusContent from '../../../components/status_content';
+
+const messages = defineMessages({
+  cancel_reblog: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
+  reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
+});
+
+const mapStateToProps = state => {
+  return {
+    privacy: state.getIn(['boosts', 'new', 'privacy']),
+  };
+};
+
+const mapDispatchToProps = dispatch => {
+  return {
+    onChangeBoostPrivacy(value) {
+      dispatch(changeBoostPrivacy(value));
+    },
+  };
+};
+
+class BoostModal extends ImmutablePureComponent {
+  static propTypes = {
+    status: ImmutablePropTypes.map.isRequired,
+    onReblog: PropTypes.func.isRequired,
+    onClose: PropTypes.func.isRequired,
+    missingMediaDescription: PropTypes.bool,
+    intl: PropTypes.object.isRequired,
+    ...WithRouterPropTypes,
+  };
+
+  handleReblog = () => {
+    this.props.onReblog(this.props.status, this.props.privacy);
+    this.props.onClose();
+  };
+
+  handleAccountClick = (e) => {
+    if (e.button === 0) {
+      e.preventDefault();
+      this.props.onClose();
+      this.props.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
+    }
+  };
+
+  _findContainer = () => {
+    return document.getElementsByClassName('modal-root__container')[0];
+  };
+
+  render () {
+    const { status, missingMediaDescription, privacy, intl } = this.props;
+    const buttonText = status.get('reblogged') ? messages.cancel_reblog : messages.reblog;
+
+    return (
+      <div className='modal-root__modal boost-modal'>
+        <div className='boost-modal__container'>
+          <div className={classNames('status', `status-${status.get('visibility')}`, 'light')}>
+            <div className='boost-modal__status-header'>
+              <div className='boost-modal__status-time'>
+                <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
+                  <VisibilityIcon visibility={status.get('visibility')} />
+                  <RelativeTimestamp timestamp={status.get('created_at')} /></a>
+              </div>
+
+              <a onClick={this.handleAccountClick} href={status.getIn(['account', 'url'])} className='status__display-name'>
+                <div className='status__avatar'>
+                  <Avatar account={status.get('account')} size={48} />
+                </div>
+
+                <DisplayName account={status.get('account')} />
+              </a>
+            </div>
+
+            <StatusContent status={status} />
+
+            {status.get('media_attachments').size > 0 && (
+              <AttachmentList
+                compact
+                media={status.get('media_attachments')}
+              />
+            )}
+          </div>
+        </div>
+
+        <div className='boost-modal__action-bar'>
+          <div>
+            { missingMediaDescription ?
+              <FormattedMessage id='boost_modal.missing_description' defaultMessage='This toot contains some media without description' />
+              :
+              <FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <Icon id='retweet' /></span> }} />
+            }
+          </div>
+
+          {status.get('visibility') !== 'private' && !status.get('reblogged') && (
+            <PrivacyDropdown
+              noDirect
+              value={privacy}
+              container={this._findContainer}
+              onChange={this.props.onChangeBoostPrivacy}
+            />
+          )}
+          <Button text={intl.formatMessage(buttonText)} onClick={this.handleReblog} autoFocus />
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default withRouter(connect(mapStateToProps, mapDispatchToProps)(injectIntl(BoostModal)));
diff --git a/app/javascript/flavours/blobfox/features/ui/components/bundle.jsx b/app/javascript/flavours/blobfox/features/ui/components/bundle.jsx
new file mode 100644
index 00000000000000..15c4220b3483ad
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/bundle.jsx
@@ -0,0 +1,106 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+const emptyComponent = () => null;
+const noop = () => { };
+
+class Bundle extends PureComponent {
+
+  static propTypes = {
+    fetchComponent: PropTypes.func.isRequired,
+    loading: PropTypes.func,
+    error: PropTypes.func,
+    children: PropTypes.func.isRequired,
+    renderDelay: PropTypes.number,
+    onFetch: PropTypes.func,
+    onFetchSuccess: PropTypes.func,
+    onFetchFail: PropTypes.func,
+  };
+
+  static defaultProps = {
+    loading: emptyComponent,
+    error: emptyComponent,
+    renderDelay: 0,
+    onFetch: noop,
+    onFetchSuccess: noop,
+    onFetchFail: noop,
+  };
+
+  static cache = new Map;
+
+  state = {
+    mod: undefined,
+    forceRender: false,
+  };
+
+  UNSAFE_componentWillMount() {
+    this.load(this.props);
+  }
+
+  UNSAFE_componentWillReceiveProps(nextProps) {
+    if (nextProps.fetchComponent !== this.props.fetchComponent) {
+      this.load(nextProps);
+    }
+  }
+
+  componentWillUnmount () {
+    if (this.timeout) {
+      clearTimeout(this.timeout);
+    }
+  }
+
+  load = (props) => {
+    const { fetchComponent, onFetch, onFetchSuccess, onFetchFail, renderDelay } = props || this.props;
+    const cachedMod = Bundle.cache.get(fetchComponent);
+
+    if (fetchComponent === undefined) {
+      this.setState({ mod: null });
+      return Promise.resolve();
+    }
+
+    onFetch();
+
+    if (cachedMod) {
+      this.setState({ mod: cachedMod.default });
+      onFetchSuccess();
+      return Promise.resolve();
+    }
+
+    this.setState({ mod: undefined });
+
+    if (renderDelay !== 0) {
+      this.timestamp = new Date();
+      this.timeout = setTimeout(() => this.setState({ forceRender: true }), renderDelay);
+    }
+
+    return fetchComponent()
+      .then((mod) => {
+        Bundle.cache.set(fetchComponent, mod);
+        this.setState({ mod: mod.default });
+        onFetchSuccess();
+      })
+      .catch((error) => {
+        this.setState({ mod: null });
+        onFetchFail(error);
+      });
+  };
+
+  render() {
+    const { loading: Loading, error: Error, children, renderDelay } = this.props;
+    const { mod, forceRender } = this.state;
+    const elapsed = this.timestamp ? (new Date() - this.timestamp) : renderDelay;
+
+    if (mod === undefined) {
+      return (elapsed >= renderDelay || forceRender) ? <Loading /> : null;
+    }
+
+    if (mod === null) {
+      return <Error onRetry={this.load} />;
+    }
+
+    return children(mod);
+  }
+
+}
+
+export default Bundle;
diff --git a/app/javascript/flavours/blobfox/features/ui/components/bundle_column_error.jsx b/app/javascript/flavours/blobfox/features/ui/components/bundle_column_error.jsx
new file mode 100644
index 00000000000000..59e8ffda043917
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/bundle_column_error.jsx
@@ -0,0 +1,166 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { injectIntl, FormattedMessage } from 'react-intl';
+
+import classNames from 'classnames';
+import { Helmet } from 'react-helmet';
+import { Link } from 'react-router-dom';
+
+import { Button } from 'flavours/blobfox/components/button';
+import Column from 'flavours/blobfox/components/column';
+import { autoPlayGif } from 'flavours/blobfox/initial_state';
+
+class GIF extends PureComponent {
+
+  static propTypes = {
+    src: PropTypes.string.isRequired,
+    staticSrc: PropTypes.string.isRequired,
+    className: PropTypes.string,
+    animate: PropTypes.bool,
+  };
+
+  static defaultProps = {
+    animate: autoPlayGif,
+  };
+
+  state = {
+    hovering: false,
+  };
+
+  handleMouseEnter = () => {
+    const { animate } = this.props;
+
+    if (!animate) {
+      this.setState({ hovering: true });
+    }
+  };
+
+  handleMouseLeave = () => {
+    const { animate } = this.props;
+
+    if (!animate) {
+      this.setState({ hovering: false });
+    }
+  };
+
+  render () {
+    const { src, staticSrc, className, animate } = this.props;
+    const { hovering } = this.state;
+
+    return (
+      <img
+        className={className}
+        src={(hovering || animate) ? src : staticSrc}
+        alt=''
+        role='presentation'
+        onMouseEnter={this.handleMouseEnter}
+        onMouseLeave={this.handleMouseLeave}
+      />
+    );
+  }
+
+}
+
+class CopyButton extends PureComponent {
+
+  static propTypes = {
+    children: PropTypes.node.isRequired,
+    value: PropTypes.string.isRequired,
+  };
+
+  state = {
+    copied: false,
+  };
+
+  handleClick = () => {
+    const { value } = this.props;
+    navigator.clipboard.writeText(value);
+    this.setState({ copied: true });
+    this.timeout = setTimeout(() => this.setState({ copied: false }), 700);
+  };
+
+  componentWillUnmount () {
+    if (this.timeout) clearTimeout(this.timeout);
+  }
+
+  render () {
+    const { children } = this.props;
+    const { copied } = this.state;
+
+    return (
+      <Button onClick={this.handleClick} className={copied ? 'copied' : 'copyable'}>{copied ? <FormattedMessage id='copypaste.copied' defaultMessage='Copied' /> : children}</Button>
+    );
+  }
+
+}
+
+class BundleColumnError extends PureComponent {
+
+  static propTypes = {
+    errorType: PropTypes.oneOf(['routing', 'network', 'error']),
+    onRetry: PropTypes.func,
+    intl: PropTypes.object.isRequired,
+    multiColumn: PropTypes.bool,
+    stacktrace: PropTypes.string,
+  };
+
+  static defaultProps = {
+    errorType: 'routing',
+  };
+
+  handleRetry = () => {
+    const { onRetry } = this.props;
+
+    if (onRetry) {
+      onRetry();
+    }
+  };
+
+  render () {
+    const { errorType, multiColumn, stacktrace } = this.props;
+
+    let title, body;
+
+    switch(errorType) {
+    case 'routing':
+      title = <FormattedMessage id='bundle_column_error.routing.title' defaultMessage='404' />;
+      body = <FormattedMessage id='bundle_column_error.routing.body' defaultMessage='The requested page could not be found. Are you sure the URL in the address bar is correct?' />;
+      break;
+    case 'network':
+      title = <FormattedMessage id='bundle_column_error.network.title' defaultMessage='Network error' />;
+      body = <FormattedMessage id='bundle_column_error.network.body' defaultMessage='There was an error when trying to load this page. This could be due to a temporary problem with your internet connection or this server.' />;
+      break;
+    case 'error':
+      title = <FormattedMessage id='bundle_column_error.error.title' defaultMessage='Oh, no!' />;
+      body = <FormattedMessage id='bundle_column_error.error.body' defaultMessage='The requested page could not be rendered. It could be due to a bug in our code, or a browser compatibility issue.' />;
+      break;
+    }
+
+    return (
+      <Column bindToDocument={!multiColumn}>
+        <div className='error-column'>
+          <GIF src='/oops.gif' staticSrc='/oops.png' className='error-column__image' />
+
+          <div className='error-column__message'>
+            <h1>{title}</h1>
+            <p>{body}</p>
+
+            <div className='error-column__message__actions'>
+              {errorType === 'network' && <Button onClick={this.handleRetry}><FormattedMessage id='bundle_column_error.retry' defaultMessage='Try again' /></Button>}
+              {errorType === 'error' && <CopyButton value={stacktrace}><FormattedMessage id='bundle_column_error.copy_stacktrace' defaultMessage='Copy error report' /></CopyButton>}
+              <Link to='/' className={classNames('button', { 'button-tertiary': errorType !== 'routing' })}><FormattedMessage id='bundle_column_error.return' defaultMessage='Go back home' /></Link>
+            </div>
+          </div>
+        </div>
+
+        <Helmet>
+          <meta name='robots' content='noindex' />
+        </Helmet>
+      </Column>
+    );
+  }
+
+}
+
+export default injectIntl(BundleColumnError);
diff --git a/app/javascript/flavours/blobfox/features/ui/components/bundle_modal_error.jsx b/app/javascript/flavours/blobfox/features/ui/components/bundle_modal_error.jsx
new file mode 100644
index 00000000000000..67dba3ce0cefcc
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/bundle_modal_error.jsx
@@ -0,0 +1,54 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, injectIntl } from 'react-intl';
+
+import { IconButton } from '../../../components/icon_button';
+
+const messages = defineMessages({
+  error: { id: 'bundle_modal_error.message', defaultMessage: 'Something went wrong while loading this component.' },
+  retry: { id: 'bundle_modal_error.retry', defaultMessage: 'Try again' },
+  close: { id: 'bundle_modal_error.close', defaultMessage: 'Close' },
+});
+
+class BundleModalError extends PureComponent {
+
+  static propTypes = {
+    onRetry: PropTypes.func.isRequired,
+    onClose: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  handleRetry = () => {
+    this.props.onRetry();
+  };
+
+  render () {
+    const { onClose, intl: { formatMessage } } = this.props;
+
+    // Keep the markup in sync with <ModalLoading />
+    // (make sure they have the same dimensions)
+    return (
+      <div className='modal-root__modal error-modal'>
+        <div className='error-modal__body'>
+          <IconButton title={formatMessage(messages.retry)} icon='refresh' onClick={this.handleRetry} size={64} />
+          {formatMessage(messages.error)}
+        </div>
+
+        <div className='error-modal__footer'>
+          <div>
+            <button
+              onClick={onClose}
+              className='error-modal__nav onboarding-modal__skip'
+            >
+              {formatMessage(messages.close)}
+            </button>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(BundleModalError);
diff --git a/app/javascript/flavours/blobfox/features/ui/components/column.jsx b/app/javascript/flavours/blobfox/features/ui/components/column.jsx
new file mode 100644
index 00000000000000..6e8ff93e190f3b
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/column.jsx
@@ -0,0 +1,78 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { debounce } from 'lodash';
+
+import { isMobile } from '../../../is_mobile';
+import { scrollTop } from '../../../scroll';
+
+import ColumnHeader from './column_header';
+
+export default class Column extends PureComponent {
+
+  static propTypes = {
+    heading: PropTypes.string,
+    icon: PropTypes.string,
+    children: PropTypes.node,
+    active: PropTypes.bool,
+    hideHeadingOnMobile: PropTypes.bool,
+    name: PropTypes.string,
+    bindToDocument: PropTypes.bool,
+  };
+
+  handleHeaderClick = () => {
+    const scrollable = this.props.bindToDocument ? document.scrollingElement : this.node.querySelector('.scrollable');
+
+    if (!scrollable) {
+      return;
+    }
+
+    this._interruptScrollAnimation = scrollTop(scrollable);
+  };
+
+  scrollTop () {
+    const scrollable = this.props.bindToDocument ? document.scrollingElement : this.node.querySelector('.scrollable');
+
+    if (!scrollable) {
+      return;
+    }
+
+    this._interruptScrollAnimation = scrollTop(scrollable);
+  }
+
+
+  handleScroll = debounce(() => {
+    if (typeof this._interruptScrollAnimation !== 'undefined') {
+      this._interruptScrollAnimation();
+    }
+  }, 200);
+
+  setRef = (c) => {
+    this.node = c;
+  };
+
+  render () {
+    const { heading, icon, children, active, hideHeadingOnMobile, name } = this.props;
+
+    const showHeading = heading && (!hideHeadingOnMobile || (hideHeadingOnMobile && !isMobile(window.innerWidth)));
+
+    const columnHeaderId = showHeading && heading.replace(/ /g, '-');
+    const header = showHeading && (
+      <ColumnHeader icon={icon} active={active} type={heading} onClick={this.handleHeaderClick} columnHeaderId={columnHeaderId} />
+    );
+    return (
+      <div
+        ref={this.setRef}
+        role='region'
+        data-column={name}
+        aria-labelledby={columnHeaderId}
+        className='column'
+        onScroll={this.handleScroll}
+      >
+        {header}
+        {children}
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/ui/components/column_header.jsx b/app/javascript/flavours/blobfox/features/ui/components/column_header.jsx
new file mode 100644
index 00000000000000..5d535f2e4fb0fe
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/column_header.jsx
@@ -0,0 +1,40 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import classNames from 'classnames';
+
+import { Icon }  from 'flavours/blobfox/components/icon';
+
+export default class ColumnHeader extends PureComponent {
+
+  static propTypes = {
+    icon: PropTypes.string,
+    type: PropTypes.string,
+    active: PropTypes.bool,
+    onClick: PropTypes.func,
+    columnHeaderId: PropTypes.string,
+  };
+
+  handleClick = () => {
+    this.props.onClick();
+  };
+
+  render () {
+    const { icon, type, active, columnHeaderId } = this.props;
+    let iconElement = '';
+
+    if (icon) {
+      iconElement = <Icon id={icon} fixedWidth className='column-header__icon' />;
+    }
+
+    return (
+      <h1 className={classNames('column-header', { active })} id={columnHeaderId || null}>
+        <button onClick={this.handleClick}>
+          {iconElement}
+          {type}
+        </button>
+      </h1>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/ui/components/column_link.jsx b/app/javascript/flavours/blobfox/features/ui/components/column_link.jsx
new file mode 100644
index 00000000000000..04c09632a576ca
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/column_link.jsx
@@ -0,0 +1,57 @@
+import PropTypes from 'prop-types';
+
+import classNames from 'classnames';
+import { NavLink } from 'react-router-dom';
+
+import { Icon } from 'flavours/blobfox/components/icon';
+
+const ColumnLink = ({ icon, text, to, onClick, href, method, badge, transparent, ...other }) => {
+  const className = classNames('column-link', { 'column-link--transparent': transparent });
+  const badgeElement = typeof badge !== 'undefined' ? <span className='column-link__badge'>{badge}</span> : null;
+  const iconElement = typeof icon === 'string' ? <Icon id={icon} fixedWidth className='column-link__icon' /> : icon;
+
+  if (href) {
+    return (
+      <a href={href} className={className} data-method={method} title={text} {...other}>
+        {iconElement}
+        <span>{text}</span>
+        {badgeElement}
+      </a>
+    );
+  } else if (to) {
+    return (
+      <NavLink to={to} className={className} title={text} {...other}>
+        {iconElement}
+        <span>{text}</span>
+        {badgeElement}
+      </NavLink>
+    );
+  } else {
+    const handleOnClick = (e) => {
+      e.preventDefault();
+      e.stopPropagation();
+      return onClick(e);
+    };
+    return (
+      // eslint-disable-next-line jsx-a11y/anchor-is-valid -- intentional to have the same look and feel as other menu items
+      <a href='#' onClick={onClick && handleOnClick} className={className} title={text} {...other} tabIndex={0}>
+        {iconElement}
+        <span>{text}</span>
+        {badgeElement}
+      </a>
+    );
+  }
+};
+
+ColumnLink.propTypes = {
+  icon: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
+  text: PropTypes.string.isRequired,
+  to: PropTypes.string,
+  onClick: PropTypes.func,
+  href: PropTypes.string,
+  method: PropTypes.string,
+  badge: PropTypes.node,
+  transparent: PropTypes.bool,
+};
+
+export default ColumnLink;
diff --git a/app/javascript/flavours/blobfox/features/ui/components/column_loading.jsx b/app/javascript/flavours/blobfox/features/ui/components/column_loading.jsx
new file mode 100644
index 00000000000000..d00da6a66f9bc0
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/column_loading.jsx
@@ -0,0 +1,32 @@
+import PropTypes from 'prop-types';
+
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import Column from 'flavours/blobfox/components/column';
+import ColumnHeader from 'flavours/blobfox/components/column_header';
+
+export default class ColumnLoading extends ImmutablePureComponent {
+
+  static propTypes = {
+    title: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
+    icon: PropTypes.string,
+    multiColumn: PropTypes.bool,
+  };
+
+  static defaultProps = {
+    title: '',
+    icon: '',
+  };
+
+  render() {
+    let { title, icon, multiColumn } = this.props;
+
+    return (
+      <Column>
+        <ColumnHeader icon={icon} title={title} multiColumn={multiColumn} focusable={false} placeholder />
+        <div className='scrollable' />
+      </Column>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/ui/components/column_subheading.jsx b/app/javascript/flavours/blobfox/features/ui/components/column_subheading.jsx
new file mode 100644
index 00000000000000..e970a0bfdd0e1e
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/column_subheading.jsx
@@ -0,0 +1,15 @@
+import PropTypes from 'prop-types';
+
+const ColumnSubheading = ({ text }) => {
+  return (
+    <div className='column-subheading'>
+      {text}
+    </div>
+  );
+};
+
+ColumnSubheading.propTypes = {
+  text: PropTypes.string.isRequired,
+};
+
+export default ColumnSubheading;
diff --git a/app/javascript/flavours/blobfox/features/ui/components/columns_area.jsx b/app/javascript/flavours/blobfox/features/ui/components/columns_area.jsx
new file mode 100644
index 00000000000000..05a02ae6ceea9c
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/columns_area.jsx
@@ -0,0 +1,180 @@
+import PropTypes from 'prop-types';
+import { Children, cloneElement } from 'react';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import { supportsPassiveEvents } from 'detect-passive-events';
+
+import { scrollRight } from '../../../scroll';
+import BundleContainer from '../containers/bundle_container';
+import {
+  Compose,
+  Notifications,
+  HomeTimeline,
+  CommunityTimeline,
+  PublicTimeline,
+  HashtagTimeline,
+  DirectTimeline,
+  FavouritedStatuses,
+  BookmarkedStatuses,
+  ListTimeline,
+  Directory,
+} from '../util/async-components';
+
+import BundleColumnError from './bundle_column_error';
+import ColumnLoading from './column_loading';
+import ComposePanel from './compose_panel';
+import DrawerLoading from './drawer_loading';
+import NavigationPanel from './navigation_panel';
+
+const componentMap = {
+  'COMPOSE': Compose,
+  'HOME': HomeTimeline,
+  'NOTIFICATIONS': Notifications,
+  'PUBLIC': PublicTimeline,
+  'REMOTE': PublicTimeline,
+  'COMMUNITY': CommunityTimeline,
+  'HASHTAG': HashtagTimeline,
+  'DIRECT': DirectTimeline,
+  'FAVOURITES': FavouritedStatuses,
+  'BOOKMARKS': BookmarkedStatuses,
+  'LIST': ListTimeline,
+  'DIRECTORY': Directory,
+};
+
+export default class ColumnsArea extends ImmutablePureComponent {
+  static propTypes = {
+    columns: ImmutablePropTypes.list.isRequired,
+    singleColumn: PropTypes.bool,
+    children: PropTypes.node,
+    openSettings: PropTypes.func,
+  };
+
+  // Corresponds to (max-width: $no-gap-breakpoint + 285px - 1px) in SCSS
+  mediaQuery = 'matchMedia' in window && window.matchMedia('(max-width: 1174px)');
+
+  state = {
+    renderComposePanel: !(this.mediaQuery && this.mediaQuery.matches),
+  };
+
+  componentDidMount() {
+    if (!this.props.singleColumn) {
+      this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false);
+    }
+
+    if (this.mediaQuery) {
+      if (this.mediaQuery.addEventListener) {
+        this.mediaQuery.addEventListener('change', this.handleLayoutChange);
+      } else {
+        this.mediaQuery.addListener(this.handleLayoutChange);
+      }
+      this.setState({ renderComposePanel: !this.mediaQuery.matches });
+    }
+
+    this.isRtlLayout = document.getElementsByTagName('body')[0].classList.contains('rtl');
+  }
+
+  UNSAFE_componentWillUpdate(nextProps) {
+    if (this.props.singleColumn !== nextProps.singleColumn && nextProps.singleColumn) {
+      this.node.removeEventListener('wheel', this.handleWheel);
+    }
+  }
+
+  componentDidUpdate(prevProps) {
+    if (this.props.singleColumn !== prevProps.singleColumn && !this.props.singleColumn) {
+      this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false);
+    }
+  }
+
+  componentWillUnmount () {
+    if (!this.props.singleColumn) {
+      this.node.removeEventListener('wheel', this.handleWheel);
+    }
+
+    if (this.mediaQuery) {
+      if (this.mediaQuery.removeEventListener) {
+        this.mediaQuery.removeEventListener('change', this.handleLayoutChange);
+      } else {
+        this.mediaQuery.removeListener(this.handleLayoutChange);
+      }
+    }
+  }
+
+  handleChildrenContentChange() {
+    if (!this.props.singleColumn) {
+      const modifier = this.isRtlLayout ? -1 : 1;
+      this._interruptScrollAnimation = scrollRight(this.node, (this.node.scrollWidth - window.innerWidth) * modifier);
+    }
+  }
+
+  handleLayoutChange = (e) => {
+    this.setState({ renderComposePanel: !e.matches });
+  };
+
+  handleWheel = () => {
+    if (typeof this._interruptScrollAnimation !== 'function') {
+      return;
+    }
+
+    this._interruptScrollAnimation();
+  };
+
+  setRef = (node) => {
+    this.node = node;
+  };
+
+  renderLoading = columnId => () => {
+    return columnId === 'COMPOSE' ? <DrawerLoading /> : <ColumnLoading multiColumn />;
+  };
+
+  renderError = (props) => {
+    return <BundleColumnError multiColumn errorType='network' {...props} />;
+  };
+
+  render () {
+    const { columns, children, singleColumn, openSettings } = this.props;
+    const { renderComposePanel } = this.state;
+
+    if (singleColumn) {
+      return (
+        <div className='columns-area__panels'>
+          <div className='columns-area__panels__pane columns-area__panels__pane--compositional'>
+            <div className='columns-area__panels__pane__inner'>
+              {renderComposePanel && <ComposePanel />}
+            </div>
+          </div>
+
+          <div className='columns-area__panels__main'>
+            <div className='tabs-bar__wrapper'><div id='tabs-bar__portal' /></div>
+            <div className='columns-area columns-area--mobile'>{children}</div>
+          </div>
+
+          <div className='columns-area__panels__pane columns-area__panels__pane--start columns-area__panels__pane--navigational'>
+            <div className='columns-area__panels__pane__inner'>
+              <NavigationPanel onOpenSettings={openSettings} />
+            </div>
+          </div>
+        </div>
+      );
+    }
+
+    return (
+      <div className='columns-area' ref={this.setRef}>
+        {columns.map(column => {
+          const params = column.get('params', null) === null ? null : column.get('params').toJS();
+          const other  = params && params.other ? params.other : {};
+
+          return (
+            <BundleContainer key={column.get('uuid')} fetchComponent={componentMap[column.get('id')]} loading={this.renderLoading(column.get('id'))} error={this.renderError}>
+              {SpecificComponent => <SpecificComponent columnId={column.get('uuid')} params={params} multiColumn {...other} />}
+            </BundleContainer>
+          );
+        })}
+
+        {Children.map(children, child => cloneElement(child, { multiColumn: true }))}
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/ui/components/compare_history_modal.jsx b/app/javascript/flavours/blobfox/features/ui/components/compare_history_modal.jsx
new file mode 100644
index 00000000000000..3cc75328076965
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/compare_history_modal.jsx
@@ -0,0 +1,110 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
+
+import escapeTextContentForBrowser from 'escape-html';
+
+import { closeModal } from 'flavours/blobfox/actions/modal';
+import { IconButton } from 'flavours/blobfox/components/icon_button';
+import InlineAccount from 'flavours/blobfox/components/inline_account';
+import MediaAttachments from 'flavours/blobfox/components/media_attachments';
+import { RelativeTimestamp } from 'flavours/blobfox/components/relative_timestamp';
+import emojify from 'flavours/blobfox/features/emoji/emoji';
+
+const mapStateToProps = (state, { statusId }) => ({
+  language: state.getIn(['statuses', statusId, 'language']),
+  versions: state.getIn(['history', statusId, 'items']),
+});
+
+const mapDispatchToProps = dispatch => ({
+
+  onClose() {
+    dispatch(closeModal({
+      modalType: undefined,
+      ignoreFocus: false,
+    }));
+  },
+
+});
+
+class CompareHistoryModal extends PureComponent {
+
+  static propTypes = {
+    onClose: PropTypes.func.isRequired,
+    index: PropTypes.number.isRequired,
+    statusId: PropTypes.string.isRequired,
+    language: PropTypes.string.isRequired,
+    versions: ImmutablePropTypes.list.isRequired,
+  };
+
+  render () {
+    const { index, versions, language, onClose } = this.props;
+    const currentVersion = versions.get(index);
+
+    const emojiMap = currentVersion.get('emojis').reduce((obj, emoji) => {
+      obj[`:${emoji.get('shortcode')}:`] = emoji.toJS();
+      return obj;
+    }, {});
+
+    const content = { __html: emojify(currentVersion.get('content'), emojiMap) };
+    const spoilerContent = { __html: emojify(escapeTextContentForBrowser(currentVersion.get('spoiler_text')), emojiMap) };
+
+    const formattedDate = <RelativeTimestamp timestamp={currentVersion.get('created_at')} short={false} />;
+    const formattedName = <InlineAccount accountId={currentVersion.get('account')} />;
+
+    const label = currentVersion.get('original') ? (
+      <FormattedMessage id='status.history.created' defaultMessage='{name} created {date}' values={{ name: formattedName, date: formattedDate }} />
+    ) : (
+      <FormattedMessage id='status.history.edited' defaultMessage='{name} edited {date}' values={{ name: formattedName, date: formattedDate }} />
+    );
+
+    return (
+      <div className='modal-root__modal compare-history-modal'>
+        <div className='report-modal__target'>
+          <IconButton className='report-modal__close' icon='times' onClick={onClose} size={20} />
+          {label}
+        </div>
+
+        <div className='compare-history-modal__container'>
+          <div className='status__content'>
+            {currentVersion.get('spoiler_text').length > 0 && (
+              <>
+                <div className='translate' dangerouslySetInnerHTML={spoilerContent} lang={language} />
+                <hr />
+              </>
+            )}
+
+            <div className='status__content__text status__content__text--visible translate' dangerouslySetInnerHTML={content} lang={language} />
+
+            {!!currentVersion.get('poll') && (
+              <div className='poll'>
+                <ul>
+                  {currentVersion.getIn(['poll', 'options']).map(option => (
+                    <li key={option.get('title')}>
+                      <span className='poll__input disabled' />
+
+                      <span
+                        className='poll__option__text translate'
+                        dangerouslySetInnerHTML={{ __html: emojify(escapeTextContentForBrowser(option.get('title')), emojiMap) }}
+                        lang={language}
+                      />
+                    </li>
+                  ))}
+                </ul>
+              </div>
+            )}
+
+            <MediaAttachments status={currentVersion} lang={language} />
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(CompareHistoryModal);
diff --git a/app/javascript/flavours/blobfox/features/ui/components/compose_panel.jsx b/app/javascript/flavours/blobfox/features/ui/components/compose_panel.jsx
new file mode 100644
index 00000000000000..c6cda04da767f5
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/compose_panel.jsx
@@ -0,0 +1,62 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { connect } from 'react-redux';
+
+import { mountCompose, unmountCompose } from 'flavours/blobfox/actions/compose';
+import ServerBanner from 'flavours/blobfox/components/server_banner';
+import ComposeFormContainer from 'flavours/blobfox/features/compose/containers/compose_form_container';
+import NavigationContainer from 'flavours/blobfox/features/compose/containers/navigation_container';
+import SearchContainer from 'flavours/blobfox/features/compose/containers/search_container';
+
+import LinkFooter from './link_footer';
+
+class ComposePanel extends PureComponent {
+
+  static contextTypes = {
+    identity: PropTypes.object.isRequired,
+  };
+
+  static propTypes = {
+    dispatch: PropTypes.func.isRequired,
+  };
+
+  componentDidMount () {
+    const { dispatch } = this.props;
+    dispatch(mountCompose());
+  }
+
+  componentWillUnmount () {
+    const { dispatch } = this.props;
+    dispatch(unmountCompose());
+  }
+
+  render() {
+    const { signedIn } = this.context.identity;
+
+    return (
+      <div className='compose-panel'>
+        <SearchContainer openInRoute />
+
+        {!signedIn && (
+          <>
+            <ServerBanner />
+            <div className='flex-spacer' />
+          </>
+        )}
+
+        {signedIn && (
+          <>
+            <NavigationContainer />
+            <ComposeFormContainer singleColumn />
+          </>
+        )}
+
+        <LinkFooter />
+      </div>
+    );
+  }
+
+}
+
+export default connect()(ComposePanel);
diff --git a/app/javascript/flavours/blobfox/features/ui/components/confirmation_modal.jsx b/app/javascript/flavours/blobfox/features/ui/components/confirmation_modal.jsx
new file mode 100644
index 00000000000000..51a501595bb6e0
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/confirmation_modal.jsx
@@ -0,0 +1,83 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { injectIntl, FormattedMessage } from 'react-intl';
+
+import { Button } from '../../../components/button';
+
+class ConfirmationModal extends PureComponent {
+
+  static propTypes = {
+    message: PropTypes.node.isRequired,
+    confirm: PropTypes.string.isRequired,
+    onClose: PropTypes.func.isRequired,
+    onConfirm: PropTypes.func.isRequired,
+    secondary: PropTypes.string,
+    onSecondary: PropTypes.func,
+    closeWhenConfirm: PropTypes.bool,
+    onDoNotAsk: PropTypes.func,
+    intl: PropTypes.object.isRequired,
+  };
+
+  static defaultProps = {
+    closeWhenConfirm: true,
+  };
+
+  handleClick = () => {
+    if (this.props.closeWhenConfirm) {
+      this.props.onClose();
+    }
+    this.props.onConfirm();
+    if (this.props.onDoNotAsk && this.doNotAskCheckbox.checked) {
+      this.props.onDoNotAsk();
+    }
+  };
+
+  handleSecondary = () => {
+    this.props.onClose();
+    this.props.onSecondary();
+  };
+
+  handleCancel = () => {
+    this.props.onClose();
+  };
+
+  setDoNotAskRef = (c) => {
+    this.doNotAskCheckbox = c;
+  };
+
+  render () {
+    const { message, confirm, secondary, onDoNotAsk } = this.props;
+
+    return (
+      <div className='modal-root__modal confirmation-modal'>
+        <div className='confirmation-modal__container'>
+          {message}
+        </div>
+
+        <div>
+          { onDoNotAsk && (
+            <div className='confirmation-modal__do_not_ask_again'>
+              <input type='checkbox' id='confirmation-modal__do_not_ask_again-checkbox' ref={this.setDoNotAskRef} />
+              <label htmlFor='confirmation-modal__do_not_ask_again-checkbox'>
+                <FormattedMessage id='confirmation_modal.do_not_ask_again' defaultMessage='Do not ask for confirmation again' />
+              </label>
+            </div>
+          )}
+          <div className='confirmation-modal__action-bar'>
+            <Button onClick={this.handleCancel} className='confirmation-modal__cancel-button'>
+              <FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
+            </Button>
+            {secondary !== undefined && (
+              <Button text={secondary} onClick={this.handleSecondary} className='confirmation-modal__secondary-button' />
+            )}
+            <Button text={confirm} onClick={this.handleClick} autoFocus />
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(ConfirmationModal);
diff --git a/app/javascript/flavours/blobfox/features/ui/components/deprecated_settings_modal.jsx b/app/javascript/flavours/blobfox/features/ui/components/deprecated_settings_modal.jsx
new file mode 100644
index 00000000000000..90b0084b175dc0
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/deprecated_settings_modal.jsx
@@ -0,0 +1,82 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
+import { Button } from 'flavours/blobfox/components/button';
+import { Icon } from 'flavours/blobfox/components/icon';
+import illustration from 'flavours/blobfox/images/logo_warn_blobfox.svg';
+import { preferenceLink } from 'flavours/blobfox/utils/backend_links';
+
+const messages = defineMessages({
+  discardChanges: { id: 'confirmations.deprecated_settings.confirm', defaultMessage: 'Use Mastodon preferences' },
+  user_setting_expand_spoilers: { id: 'settings.enable_content_warnings_auto_unfold', defaultMessage: 'Automatically unfold content-warnings' },
+  user_setting_disable_swiping: { id: 'settings.swipe_to_change_columns', defaultMessage: 'Allow swiping to change columns (Mobile only)' },
+});
+
+class DeprecatedSettingsModal extends PureComponent {
+
+  static propTypes = {
+    settings: ImmutablePropTypes.list.isRequired,
+    onClose: PropTypes.func.isRequired,
+    onConfirm: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  handleClick = () => {
+    this.props.onConfirm();
+    this.props.onClose();
+  };
+
+  render () {
+    const { settings, intl } = this.props;
+
+    return (
+      <div className='modal-root__modal confirmation-modal'>
+        <div className='confirmation-modal__container'>
+
+          <img src={illustration} className='modal-warning' alt='' />
+
+          <FormattedMessage
+            id='confirmations.deprecated_settings.message'
+            defaultMessage='Some of the blobfox-soc device-specific {app_settings} you are using have been replaced by Mastodon {preferences} and will be overriden:'
+            values={{
+              app_settings: (
+                <strong className='deprecated-settings-label'>
+                  <Icon id='cogs' /> <FormattedMessage id='navigation_bar.app_settings' defaultMessage='App settings' />
+                </strong>
+              ),
+              preferences: (
+                <strong className='deprecated-settings-label'>
+                  <Icon id='cog' /> <FormattedMessage id='navigation_bar.preferences' defaultMessage='Preferences' />
+                </strong>
+              ),
+            }}
+          />
+
+          <div className='deprecated-settings-info'>
+            <ul>
+              { settings.map((setting_name) => (
+                <li key={setting_name}>
+                  <a href={preferenceLink(setting_name)}><FormattedMessage {...messages[setting_name]} /></a>
+                </li>
+              )) }
+            </ul>
+          </div>
+        </div>
+
+        <div>
+          <div className='confirmation-modal__action-bar'>
+            <div />
+            <Button text={intl.formatMessage(messages.discardChanges)} onClick={this.handleClick} autoFocus />
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(DeprecatedSettingsModal);
diff --git a/app/javascript/flavours/blobfox/features/ui/components/disabled_account_banner.jsx b/app/javascript/flavours/blobfox/features/ui/components/disabled_account_banner.jsx
new file mode 100644
index 00000000000000..94fbce30dc8d00
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/disabled_account_banner.jsx
@@ -0,0 +1,99 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
+
+import { Link } from 'react-router-dom';
+
+import { connect } from 'react-redux';
+
+import { openModal } from 'flavours/blobfox/actions/modal';
+import { disabledAccountId, movedToAccountId, domain } from 'flavours/blobfox/initial_state';
+import { logOut } from 'flavours/blobfox/utils/log_out';
+
+const messages = defineMessages({
+  logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
+  logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
+});
+
+const mapStateToProps = (state) => ({
+  disabledAcct: state.getIn(['accounts', disabledAccountId, 'acct']),
+  movedToAcct: movedToAccountId ? state.getIn(['accounts', movedToAccountId, 'acct']) : undefined,
+});
+
+const mapDispatchToProps = (dispatch, { intl }) => ({
+  onLogout () {
+    dispatch(openModal({
+      modalType: 'CONFIRM',
+      modalProps: {
+        message: intl.formatMessage(messages.logoutMessage),
+        confirm: intl.formatMessage(messages.logoutConfirm),
+        closeWhenConfirm: false,
+        onConfirm: () => logOut(),
+      },
+    }));
+  },
+});
+
+class DisabledAccountBanner extends PureComponent {
+
+  static propTypes = {
+    disabledAcct: PropTypes.string.isRequired,
+    movedToAcct: PropTypes.string,
+    onLogout: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  handleLogOutClick = e => {
+    e.preventDefault();
+    e.stopPropagation();
+
+    this.props.onLogout();
+
+    return false;
+  };
+
+  render () {
+    const { disabledAcct, movedToAcct } = this.props;
+
+    const disabledAccountLink = (
+      <Link to={`/@${disabledAcct}`}>
+        {disabledAcct}@{domain}
+      </Link>
+    );
+
+    return (
+      <div className='sign-in-banner'>
+        <p>
+          {movedToAcct ? (
+            <FormattedMessage
+              id='moved_to_account_banner.text'
+              defaultMessage='Your account {disabledAccount} is currently disabled because you moved to {movedToAccount}.'
+              values={{
+                disabledAccount: disabledAccountLink,
+                movedToAccount: <Link to={`/@${movedToAcct}`}>{movedToAcct.includes('@') ? movedToAcct : `${movedToAcct}@${domain}`}</Link>,
+              }}
+            />
+          ) : (
+            <FormattedMessage
+              id='disabled_account_banner.text'
+              defaultMessage='Your account {disabledAccount} is currently disabled.'
+              values={{
+                disabledAccount: disabledAccountLink,
+              }}
+            />
+          )}
+        </p>
+        <a href='/auth/edit' className='button button--block'>
+          <FormattedMessage id='disabled_account_banner.account_settings' defaultMessage='Account settings' />
+        </a>
+        <button type='button' className='button button--block button-tertiary' onClick={this.handleLogOutClick}>
+          <FormattedMessage id='confirmations.logout.confirm' defaultMessage='Log out' />
+        </button>
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(DisabledAccountBanner));
diff --git a/app/javascript/flavours/blobfox/features/ui/components/doodle_modal.jsx b/app/javascript/flavours/blobfox/features/ui/components/doodle_modal.jsx
new file mode 100644
index 00000000000000..b4c3c905dc0cf6
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/doodle_modal.jsx
@@ -0,0 +1,619 @@
+import PropTypes from 'prop-types';
+
+import classNames from 'classnames';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
+import Atrament from 'atrament'; // the doodling library
+import { debounce, mapValues } from 'lodash';
+
+import { doodleSet, uploadCompose } from 'flavours/blobfox/actions/compose';
+import { Button } from 'flavours/blobfox/components/button';
+import { IconButton } from 'flavours/blobfox/components/icon_button';
+// palette nicked from MyPaint, CC0
+const palette = [
+  ['rgb(  0,    0,    0)', 'Black'],
+  ['rgb( 38,   38,   38)', 'Gray 15'],
+  ['rgb( 77,   77,   77)', 'Grey 30'],
+  ['rgb(128,  128,  128)', 'Grey 50'],
+  ['rgb(171,  171,  171)', 'Grey 67'],
+  ['rgb(217,  217,  217)', 'Grey 85'],
+  ['rgb(255,  255,  255)', 'White'],
+  ['rgb(128,    0,    0)', 'Maroon'],
+  ['rgb(209,    0,    0)', 'English-red'],
+  ['rgb(255,   54,   34)', 'Tomato'],
+  ['rgb(252,   60,    3)', 'Orange-red'],
+  ['rgb(255,  140,  105)', 'Salmon'],
+  ['rgb(252,  232,   32)', 'Cadium-yellow'],
+  ['rgb(243,  253,   37)', 'Lemon yellow'],
+  ['rgb(121,    5,   35)', 'Dark crimson'],
+  ['rgb(169,   32,   62)', 'Deep carmine'],
+  ['rgb(255,  140,    0)', 'Orange'],
+  ['rgb(255,  168,   18)', 'Dark tangerine'],
+  ['rgb(217,  144,   88)', 'Persian orange'],
+  ['rgb(194,  178,  128)', 'Sand'],
+  ['rgb(255,  229,  180)', 'Peach'],
+  ['rgb(100,   54,   46)', 'Bole'],
+  ['rgb(108,   41,   52)', 'Dark cordovan'],
+  ['rgb(163,   65,   44)', 'Chestnut'],
+  ['rgb(228,  136,  100)', 'Dark salmon'],
+  ['rgb(255,  195,  143)', 'Apricot'],
+  ['rgb(255,  219,  188)', 'Unbleached silk'],
+  ['rgb(242,  227,  198)', 'Straw'],
+  ['rgb( 53,   19,   13)', 'Bistre'],
+  ['rgb( 84,   42,   14)', 'Dark chocolate'],
+  ['rgb(102,   51,   43)', 'Burnt sienna'],
+  ['rgb(184,   66,    0)', 'Sienna'],
+  ['rgb(216,  153,   12)', 'Yellow ochre'],
+  ['rgb(210,  180,  140)', 'Tan'],
+  ['rgb(232,  204,  144)', 'Dark wheat'],
+  ['rgb(  0,   49,   83)', 'Prussian blue'],
+  ['rgb( 48,   69,  119)', 'Dark grey blue'],
+  ['rgb(  0,   71,  171)', 'Cobalt blue'],
+  ['rgb( 31,  117,  254)', 'Blue'],
+  ['rgb(120,  180,  255)', 'Bright french blue'],
+  ['rgb(171,  200,  255)', 'Bright steel blue'],
+  ['rgb(208,  231,  255)', 'Ice blue'],
+  ['rgb( 30,   51,   58)', 'Medium jungle green'],
+  ['rgb( 47,   79,   79)', 'Dark slate grey'],
+  ['rgb( 74,  104,   93)', 'Dark grullo green'],
+  ['rgb(  0,  128,  128)', 'Teal'],
+  ['rgb( 67,  170,  176)', 'Turquoise'],
+  ['rgb(109,  174,  199)', 'Cerulean frost'],
+  ['rgb(173,  217,  186)', 'Tiffany green'],
+  ['rgb( 22,   34,   29)', 'Gray-asparagus'],
+  ['rgb( 36,   48,   45)', 'Medium dark teal'],
+  ['rgb( 74,  104,   93)', 'Xanadu'],
+  ['rgb(119,  198,  121)', 'Mint'],
+  ['rgb(175,  205,  182)', 'Timberwolf'],
+  ['rgb(185,  245,  246)', 'Celeste'],
+  ['rgb(193,  255,  234)', 'Aquamarine'],
+  ['rgb( 29,   52,   35)', 'Cal Poly Pomona'],
+  ['rgb(  1,   68,   33)', 'Forest green'],
+  ['rgb( 42,  128,    0)', 'Napier green'],
+  ['rgb(128,  128,    0)', 'Olive'],
+  ['rgb( 65,  156,  105)', 'Sea green'],
+  ['rgb(189,  246,   29)', 'Green-yellow'],
+  ['rgb(231,  244,  134)', 'Bright chartreuse'],
+  ['rgb(138,   23,  137)', 'Purple'],
+  ['rgb( 78,   39,  138)', 'Violet'],
+  ['rgb(193,   75,  110)', 'Dark thulian pink'],
+  ['rgb(222,   49,   99)', 'Cerise'],
+  ['rgb(255,   20,  147)', 'Deep pink'],
+  ['rgb(255,  102,  204)', 'Rose pink'],
+  ['rgb(255,  203,  219)', 'Pink'],
+  ['rgb(255,  255,  255)', 'White'],
+  ['rgb(229,   17,    1)', 'RGB Red'],
+  ['rgb(  0,  255,    0)', 'RGB Green'],
+  ['rgb(  0,    0,  255)', 'RGB Blue'],
+  ['rgb(  0,  255,  255)', 'CMYK Cyan'],
+  ['rgb(255,    0,  255)', 'CMYK Magenta'],
+  ['rgb(255,  255,    0)', 'CMYK Yellow'],
+];
+
+// re-arrange to the right order for display
+let palReordered = [];
+for (let row = 0; row < 7; row++) {
+  for (let col = 0; col < 11; col++) {
+    palReordered.push(palette[col * 7 + row]);
+  }
+  palReordered.push(null); // null indicates a <br />
+}
+
+// Utility for converting base64 image to binary for upload
+// https://stackoverflow.com/questions/35940290/how-to-convert-base64-string-to-javascript-file-object-like-as-from-file-input-f
+function dataURLtoFile(dataurl, filename) {
+  let arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1],
+    bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
+  while(n--){
+    u8arr[n] = bstr.charCodeAt(n);
+  }
+  return new File([u8arr], filename, { type: mime });
+}
+/** Doodle canvas size options */
+const DOODLE_SIZES = {
+  normal: [500, 500, 'Square 500'],
+  tootbanner: [702, 330, 'Tootbanner'],
+  s640x480: [640, 480, '640×480 - 480p'],
+  s800x600: [800, 600, '800×600 - SVGA'],
+  s720x480: [720, 405, '720x405 - 16:9'],
+};
+
+
+const mapStateToProps = state => ({
+  options: state.getIn(['compose', 'doodle']),
+});
+
+const mapDispatchToProps = dispatch => ({
+  /**
+   * Set options in the redux store
+   * @param {Object} opts
+   */
+  setOpt: (opts) => dispatch(doodleSet(opts)),
+  /**
+   * Submit doodle for upload
+   * @param {File} file
+   */
+  submit: (file) => dispatch(uploadCompose([file])),
+});
+
+/**
+ * Doodling dialog with drawing canvas
+ *
+ * Keyboard shortcuts:
+ * - Delete: Clear screen, fill with background color
+ * - Backspace, Ctrl+Z: Undo one step
+ * - Ctrl held while drawing: Use background color
+ * - Shift held while clicking screen: Use fill tool
+ *
+ * Palette:
+ * - Left mouse button: pick foreground
+ * - Ctrl + left mouse button: pick background
+ * - Right mouse button: pick background
+ */
+class DoodleModal extends ImmutablePureComponent {
+
+  static propTypes = {
+    options: ImmutablePropTypes.map,
+    onClose: PropTypes.func.isRequired,
+    setOpt: PropTypes.func.isRequired,
+    submit: PropTypes.func.isRequired,
+  };
+
+  //region Option getters/setters
+
+  /** Foreground color */
+  get fg () {
+    return this.props.options.get('fg');
+  }
+  set fg (value) {
+    this.props.setOpt({ fg: value });
+  }
+
+  /** Background color */
+  get bg () {
+    return this.props.options.get('bg');
+  }
+  set bg (value) {
+    this.props.setOpt({ bg: value });
+  }
+
+  /** Swap Fg and Bg for drawing */
+  get swapped () {
+    return this.props.options.get('swapped');
+  }
+  set swapped (value) {
+    this.props.setOpt({ swapped: value });
+  }
+
+  /** Mode - 'draw' or 'fill' */
+  get mode () {
+    return this.props.options.get('mode');
+  }
+  set mode (value) {
+    this.props.setOpt({ mode: value });
+  }
+
+  /** Base line weight */
+  get weight () {
+    return this.props.options.get('weight');
+  }
+  set weight (value) {
+    this.props.setOpt({ weight: value });
+  }
+
+  /** Drawing opacity */
+  get opacity () {
+    return this.props.options.get('opacity');
+  }
+  set opacity (value) {
+    this.props.setOpt({ opacity: value });
+  }
+
+  /** Adaptive stroke - change width with speed */
+  get adaptiveStroke () {
+    return this.props.options.get('adaptiveStroke');
+  }
+  set adaptiveStroke (value) {
+    this.props.setOpt({ adaptiveStroke: value });
+  }
+
+  /** Smoothing (for mouse drawing) */
+  get smoothing () {
+    return this.props.options.get('smoothing');
+  }
+  set smoothing (value) {
+    this.props.setOpt({ smoothing: value });
+  }
+
+  /** Size preset */
+  get size () {
+    return this.props.options.get('size');
+  }
+  set size (value) {
+    this.props.setOpt({ size: value });
+  }
+
+  //endregion
+
+  /**
+   * Key up handler
+   * @param {KeyboardEvent} e
+   */
+  handleKeyUp = (e) => {
+    if (e.target.nodeName === 'INPUT') return;
+
+    if (e.key === 'Delete') {
+      e.preventDefault();
+      this.handleClearBtn();
+      return;
+    }
+
+    if (e.key === 'Backspace' || (e.key === 'z' && (e.ctrlKey || e.metaKey))) {
+      e.preventDefault();
+      this.undo();
+    }
+
+    if (e.key === 'Control' || e.key === 'Meta') {
+      this.controlHeld = false;
+      this.swapped = false;
+    }
+
+    if (e.key === 'Shift') {
+      this.shiftHeld = false;
+      this.mode = 'draw';
+    }
+  };
+
+  /**
+   * Key down handler
+   * @param {KeyboardEvent} e
+   */
+  handleKeyDown = (e) => {
+    if (e.key === 'Control' || e.key === 'Meta') {
+      this.controlHeld = true;
+      this.swapped = true;
+    }
+
+    if (e.key === 'Shift') {
+      this.shiftHeld = true;
+      this.mode = 'fill';
+    }
+  };
+
+  /**
+   * Component installed in the DOM, do some initial set-up
+   */
+  componentDidMount () {
+    this.controlHeld = false;
+    this.shiftHeld = false;
+    this.swapped = false;
+    window.addEventListener('keyup', this.handleKeyUp, false);
+    window.addEventListener('keydown', this.handleKeyDown, false);
+  }
+
+  /**
+   * Tear component down
+   */
+  componentWillUnmount () {
+    window.removeEventListener('keyup', this.handleKeyUp, false);
+    window.removeEventListener('keydown', this.handleKeyDown, false);
+    if (this.sketcher) this.sketcher.destroy();
+  }
+
+  /**
+   * Set reference to the canvas element.
+   * This is called during component init
+   * @param {HTMLCanvasElement} elem - canvas element
+   */
+  setCanvasRef = (elem) => {
+    this.canvas = elem;
+    if (elem) {
+      elem.addEventListener('dirty', () => {
+        this.saveUndo();
+        this.sketcher._dirty = false;
+      });
+
+      elem.addEventListener('click', () => {
+        // sketcher bug - does not fire dirty on fill
+        if (this.mode === 'fill') {
+          this.saveUndo();
+        }
+      });
+
+      // prevent context menu
+      elem.addEventListener('contextmenu', (e) => {
+        e.preventDefault();
+      });
+
+      elem.addEventListener('mousedown', (e) => {
+        if (e.button === 2) {
+          this.swapped = true;
+        }
+      });
+
+      elem.addEventListener('mouseup', (e) => {
+        if (e.button === 2) {
+          this.swapped = this.controlHeld;
+        }
+      });
+
+      this.initSketcher(elem);
+      this.mode = 'draw'; // Reset mode - it's confusing if left at 'fill'
+    }
+  };
+
+  /**
+   * Set up the sketcher instance
+   * @param {HTMLCanvasElement | null} canvas - canvas element. Null if we're just resizing
+   */
+  initSketcher (canvas = null) {
+    const sizepreset = DOODLE_SIZES[this.size];
+
+    if (this.sketcher) this.sketcher.destroy();
+    this.sketcher = new Atrament(canvas || this.canvas, sizepreset[0], sizepreset[1]);
+
+    if (canvas) {
+      this.ctx = this.sketcher.context;
+      this.updateSketcherSettings();
+    }
+
+    this.clearScreen();
+  }
+
+  /**
+   * Done button handler
+   */
+  onDoneButton = () => {
+    const dataUrl = this.sketcher.toImage();
+    const file = dataURLtoFile(dataUrl, 'doodle.png');
+    this.props.submit(file);
+    this.props.onClose(); // close dialog
+  };
+
+  /**
+   * Cancel button handler
+   */
+  onCancelButton = () => {
+    if (this.undos.length > 1 && !confirm('Discard doodle? All changes will be lost!')) {
+      return;
+    }
+
+    this.props.onClose(); // close dialog
+  };
+
+  /**
+   * Update sketcher options based on state
+   */
+  updateSketcherSettings () {
+    if (!this.sketcher) return;
+
+    if (this.oldSize !== this.size) this.initSketcher();
+
+    this.sketcher.color = (this.swapped ? this.bg : this.fg);
+    this.sketcher.opacity = this.opacity;
+    this.sketcher.weight = this.weight;
+    this.sketcher.mode = this.mode;
+    this.sketcher.smoothing = this.smoothing;
+    this.sketcher.adaptiveStroke = this.adaptiveStroke;
+
+    this.oldSize = this.size;
+  }
+
+  /**
+   * Fill screen with background color
+   */
+  clearScreen = () => {
+    this.ctx.fillStyle = this.bg;
+    this.ctx.fillRect(-1, -1, this.canvas.width+2, this.canvas.height+2);
+    this.undos = [];
+
+    this.doSaveUndo();
+  };
+
+  /**
+   * Undo one step
+   */
+  undo = () => {
+    if (this.undos.length > 1) {
+      this.undos.pop();
+      const buf = this.undos.pop();
+
+      this.sketcher.clear();
+      this.ctx.putImageData(buf, 0, 0);
+      this.doSaveUndo();
+    }
+  };
+
+  /**
+   * Save canvas content into the undo buffer immediately
+   */
+  doSaveUndo = () => {
+    this.undos.push(this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height));
+  };
+
+  /**
+   * Called on each canvas change.
+   * Saves canvas content to the undo buffer after some period of inactivity.
+   */
+  saveUndo = debounce(() => {
+    this.doSaveUndo();
+  }, 100);
+
+  /**
+   * Palette left click.
+   * Selects Fg color (or Bg, if Control/Meta is held)
+   * @param {MouseEvent<HTMLButtonElement>} e - event
+   */
+  onPaletteClick = (e) => {
+    const c = e.target.dataset.color;
+
+    if (this.controlHeld) {
+      this.bg = c;
+    } else {
+      this.fg = c;
+    }
+
+    e.target.blur();
+    e.preventDefault();
+  };
+
+  /**
+   * Palette right click.
+   * Selects Bg color
+   * @param {MouseEvent<HTMLButtonElement>} e - event
+   */
+  onPaletteRClick = (e) => {
+    this.bg = e.target.dataset.color;
+    e.target.blur();
+    e.preventDefault();
+  };
+
+  /**
+   * Handle click on the Draw mode button
+   * @param {MouseEvent<HTMLButtonElement>} e - event
+   */
+  setModeDraw = (e) => {
+    this.mode = 'draw';
+    e.target.blur();
+  };
+
+  /**
+   * Handle click on the Fill mode button
+   * @param {MouseEvent<HTMLButtonElement>} e - event
+   */
+  setModeFill = (e) => {
+    this.mode = 'fill';
+    e.target.blur();
+  };
+
+  /**
+   * Handle click on Smooth checkbox
+   * @param {ChangeEvent<HTMLInputElement>} e - event
+   */
+  tglSmooth = (e) => {
+    this.smoothing = !this.smoothing;
+    e.target.blur();
+  };
+
+  /**
+   * Handle click on Adaptive checkbox
+   * @param {ChangeEvent<HTMLInputElement>} e - event
+   */
+  tglAdaptive = (e) => {
+    this.adaptiveStroke = !this.adaptiveStroke;
+    e.target.blur();
+  };
+
+  /**
+   * Handle change of the Weight input field
+   * @param {ChangeEvent<HTMLInputElement>} e - event
+   */
+  setWeight = (e) => {
+    this.weight = +e.target.value || 1;
+  };
+
+  /**
+   * Set size - clalback from the select box
+   * @param {ChangeEvent<HTMLSelectElement>} e - event
+   */
+  changeSize = (e) => {
+    let newSize = e.target.value;
+    if (newSize === this.oldSize) return;
+
+    if (this.undos.length > 1 && !confirm('Change canvas size? This will erase your current drawing!')) {
+      return;
+    }
+
+    this.size = newSize;
+  };
+
+  handleClearBtn = () => {
+    if (this.undos.length > 1 && !confirm('Clear canvas? This will erase your current drawing!')) {
+      return;
+    }
+
+    this.clearScreen();
+  };
+
+  /**
+   * Render the component
+   */
+  render () {
+    this.updateSketcherSettings();
+
+    return (
+      <div className='modal-root__modal doodle-modal'>
+        <div className='doodle-modal__container'>
+          <canvas ref={this.setCanvasRef} />
+        </div>
+
+        <div className='doodle-modal__action-bar'>
+          <div className='doodle-toolbar'>
+            <Button text='Done' onClick={this.onDoneButton} />
+            <Button text='Cancel' onClick={this.onCancelButton} />
+          </div>
+          <div className='filler' />
+          <div className='doodle-toolbar with-inputs'>
+            <div>
+              <label htmlFor='dd_smoothing'>Smoothing</label>
+              <span className='val'>
+                <input type='checkbox' id='dd_smoothing' onChange={this.tglSmooth} checked={this.smoothing} />
+              </span>
+            </div>
+            <div>
+              <label htmlFor='dd_adaptive'>Adaptive</label>
+              <span className='val'>
+                <input type='checkbox' id='dd_adaptive' onChange={this.tglAdaptive} checked={this.adaptiveStroke} />
+              </span>
+            </div>
+            <div>
+              <label htmlFor='dd_weight'>Weight</label>
+              <span className='val'>
+                <input type='number' min={1} id='dd_weight' value={this.weight} onChange={this.setWeight} />
+              </span>
+            </div>
+            <div>
+              <select aria-label='Canvas size' onInput={this.changeSize} defaultValue={this.size}>
+                { Object.values(mapValues(DOODLE_SIZES, (val, k) =>
+                  <option key={k} value={k}>{val[2]}</option>,
+                )) }
+              </select>
+            </div>
+          </div>
+          <div className='doodle-toolbar'>
+            <IconButton icon='pencil' title='Draw' label='Draw' onClick={this.setModeDraw} size={18} active={this.mode === 'draw'} inverted />
+            <IconButton icon='bath' title='Fill' label='Fill' onClick={this.setModeFill} size={18} active={this.mode === 'fill'} inverted />
+            <IconButton icon='undo' title='Undo' label='Undo' onClick={this.undo} size={18} inverted />
+            <IconButton icon='trash' title='Clear' label='Clear' onClick={this.handleClearBtn} size={18} inverted />
+          </div>
+          <div className='doodle-palette'>
+            {
+              palReordered.map((c, i) =>
+                c === null ?
+                  <br key={i} /> :
+                  <button
+                    key={i}
+                    style={{ backgroundColor: c[0] }}
+                    onClick={this.onPaletteClick}
+                    onContextMenu={this.onPaletteRClick}
+                    data-color={c[0]}
+                    title={c[1]}
+                    className={classNames({
+                      'foreground': this.fg === c[0],
+                      'background': this.bg === c[0],
+                    })}
+                  />,
+              )
+            }
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(DoodleModal);
diff --git a/app/javascript/flavours/blobfox/features/ui/components/drawer_loading.jsx b/app/javascript/flavours/blobfox/features/ui/components/drawer_loading.jsx
new file mode 100644
index 00000000000000..11072ad98cc076
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/drawer_loading.jsx
@@ -0,0 +1,9 @@
+const DrawerLoading = () => (
+  <div className='drawer'>
+    <div className='drawer__pager'>
+      <div className='drawer__inner' />
+    </div>
+  </div>
+);
+
+export default DrawerLoading;
diff --git a/app/javascript/flavours/blobfox/features/ui/components/embed_modal.jsx b/app/javascript/flavours/blobfox/features/ui/components/embed_modal.jsx
new file mode 100644
index 00000000000000..f357059d76d53b
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/embed_modal.jsx
@@ -0,0 +1,100 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
+
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import api from 'flavours/blobfox/api';
+import { IconButton } from 'flavours/blobfox/components/icon_button';
+
+const messages = defineMessages({
+  close: { id: 'lightbox.close', defaultMessage: 'Close' },
+});
+
+class EmbedModal extends ImmutablePureComponent {
+
+  static propTypes = {
+    id: PropTypes.string.isRequired,
+    onClose: PropTypes.func.isRequired,
+    onError: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  state = {
+    loading: false,
+    oembed: null,
+  };
+
+  componentDidMount () {
+    const { id } = this.props;
+
+    this.setState({ loading: true });
+
+    api().get(`/api/web/embeds/${id}`).then(res => {
+      this.setState({ loading: false, oembed: res.data });
+
+      const iframeDocument = this.iframe.contentWindow.document;
+
+      iframeDocument.open();
+      iframeDocument.write(res.data.html);
+      iframeDocument.close();
+
+      iframeDocument.body.style.margin = 0;
+      this.iframe.width  = iframeDocument.body.scrollWidth;
+      this.iframe.height = iframeDocument.body.scrollHeight;
+    }).catch(error => {
+      this.props.onError(error);
+    });
+  }
+
+  setIframeRef = c =>  {
+    this.iframe = c;
+  };
+
+  handleTextareaClick = (e) => {
+    e.target.select();
+  };
+
+  render () {
+    const { intl, onClose } = this.props;
+    const { oembed } = this.state;
+
+    return (
+      <div className='modal-root__modal report-modal embed-modal'>
+        <div className='report-modal__target'>
+          <IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={16} />
+          <FormattedMessage id='status.embed' defaultMessage='Embed' />
+        </div>
+
+        <div className='report-modal__container embed-modal__container' style={{ display: 'block' }}>
+          <p className='hint'>
+            <FormattedMessage id='embed.instructions' defaultMessage='Embed this status on your website by copying the code below.' />
+          </p>
+
+          <input
+            type='text'
+            className='embed-modal__html'
+            readOnly
+            value={oembed && oembed.html || ''}
+            onClick={this.handleTextareaClick}
+          />
+
+          <p className='hint'>
+            <FormattedMessage id='embed.preview' defaultMessage='Here is what it will look like:' />
+          </p>
+
+          <iframe
+            className='embed-modal__iframe'
+            frameBorder='0'
+            ref={this.setIframeRef}
+            sandbox='allow-scripts allow-same-origin'
+            title='preview'
+          />
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(EmbedModal);
diff --git a/app/javascript/flavours/blobfox/features/ui/components/favourite_modal.jsx b/app/javascript/flavours/blobfox/features/ui/components/favourite_modal.jsx
new file mode 100644
index 00000000000000..47bcd6a612c744
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/favourite_modal.jsx
@@ -0,0 +1,94 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import classNames from 'classnames';
+import { withRouter } from 'react-router-dom';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import AttachmentList from 'flavours/blobfox/components/attachment_list';
+import { Avatar } from 'flavours/blobfox/components/avatar';
+import { Button } from 'flavours/blobfox/components/button';
+import { DisplayName } from 'flavours/blobfox/components/display_name';
+import { Icon } from 'flavours/blobfox/components/icon';
+import { RelativeTimestamp } from 'flavours/blobfox/components/relative_timestamp';
+import StatusContent from 'flavours/blobfox/components/status_content';
+import VisibilityIcon from 'flavours/blobfox/components/status_visibility_icon';
+import { WithRouterPropTypes } from 'flavours/blobfox/utils/react_router';
+
+const messages = defineMessages({
+  favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
+});
+
+class FavouriteModal extends ImmutablePureComponent {
+
+  static propTypes = {
+    status: ImmutablePropTypes.map.isRequired,
+    onFavourite: PropTypes.func.isRequired,
+    onClose: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+    ...WithRouterPropTypes,
+  };
+
+  handleFavourite = () => {
+    this.props.onFavourite(this.props.status);
+    this.props.onClose();
+  };
+
+  handleAccountClick = (e) => {
+    if (e.button === 0) {
+      e.preventDefault();
+      this.props.onClose();
+      this.props.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
+    }
+  };
+
+  render () {
+    const { status, intl } = this.props;
+
+    return (
+      <div className='modal-root__modal boost-modal'>
+        <div className='boost-modal__container'>
+          <div className={classNames('status', `status-${status.get('visibility')}`, 'light')}>
+            <div className='boost-modal__status-header'>
+              <div className='boost-modal__status-time'>
+                <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
+                  <VisibilityIcon visibility={status.get('visibility')} />
+                  <RelativeTimestamp timestamp={status.get('created_at')} />
+                </a>
+              </div>
+
+              <a onClick={this.handleAccountClick} href={status.getIn(['account', 'url'])} className='status__display-name'>
+                <div className='status__avatar'>
+                  <Avatar account={status.get('account')} size={48} />
+                </div>
+
+                <DisplayName account={status.get('account')} />
+
+              </a>
+            </div>
+
+            <StatusContent status={status} />
+
+            {status.get('media_attachments').size > 0 && (
+              <AttachmentList
+                compact
+                media={status.get('media_attachments')}
+              />
+            )}
+          </div>
+        </div>
+
+        <div className='boost-modal__action-bar'>
+          <div><FormattedMessage id='favourite_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <Icon id='star' /></span> }} /></div>
+          <Button text={intl.formatMessage(messages.favourite)} onClick={this.handleFavourite} autoFocus />
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default withRouter(injectIntl(FavouriteModal));
diff --git a/app/javascript/flavours/blobfox/features/ui/components/filter_modal.jsx b/app/javascript/flavours/blobfox/features/ui/components/filter_modal.jsx
new file mode 100644
index 00000000000000..7d66e150e49d04
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/filter_modal.jsx
@@ -0,0 +1,136 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
+
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
+import { fetchFilters, createFilter, createFilterStatus } from 'flavours/blobfox/actions/filters';
+import { fetchStatus } from 'flavours/blobfox/actions/statuses';
+import { IconButton } from 'flavours/blobfox/components/icon_button';
+import AddedToFilter from 'flavours/blobfox/features/filters/added_to_filter';
+import SelectFilter from 'flavours/blobfox/features/filters/select_filter';
+
+const messages = defineMessages({
+  close: { id: 'lightbox.close', defaultMessage: 'Close' },
+});
+
+class FilterModal extends ImmutablePureComponent {
+
+  static propTypes = {
+    statusId: PropTypes.string.isRequired,
+    contextType: PropTypes.string,
+    dispatch: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  state = {
+    step: 'select',
+    filterId: null,
+    isSubmitting: false,
+    isSubmitted: false,
+  };
+
+  handleNewFilterSuccess = (result) => {
+    this.handleSelectFilter(result.id);
+  };
+
+  handleSuccess = () => {
+    const { dispatch, statusId } = this.props;
+    dispatch(fetchStatus(statusId, true));
+    this.setState({ isSubmitting: false, isSubmitted: true, step: 'submitted' });
+  };
+
+  handleFail = () => {
+    this.setState({ isSubmitting: false });
+  };
+
+  handleNextStep = step => {
+    this.setState({ step });
+  };
+
+  handleSelectFilter = (filterId) => {
+    const { dispatch, statusId } = this.props;
+
+    this.setState({ isSubmitting: true, filterId });
+
+    dispatch(createFilterStatus({
+      filter_id: filterId,
+      status_id: statusId,
+    }, this.handleSuccess, this.handleFail));
+  };
+
+  handleNewFilter = (title) => {
+    const { dispatch } = this.props;
+
+    this.setState({ isSubmitting: true });
+
+    dispatch(createFilter({
+      title,
+      context: ['home', 'notifications', 'public', 'thread', 'account'],
+      action: 'warn',
+    }, this.handleNewFilterSuccess, this.handleFail));
+  };
+
+  componentDidMount () {
+    const { dispatch } = this.props;
+
+    dispatch(fetchFilters());
+  }
+
+  render () {
+    const {
+      intl,
+      statusId,
+      contextType,
+      onClose,
+    } = this.props;
+
+    const {
+      step,
+      filterId,
+    } = this.state;
+
+    let stepComponent;
+
+    switch(step) {
+    case 'select':
+      stepComponent = (
+        <SelectFilter
+          contextType={contextType}
+          onSelectFilter={this.handleSelectFilter}
+          onNewFilter={this.handleNewFilter}
+        />
+      );
+      break;
+    case 'create':
+      stepComponent = null;
+      break;
+    case 'submitted':
+      stepComponent = (
+        <AddedToFilter
+          contextType={contextType}
+          filterId={filterId}
+          statusId={statusId}
+          onClose={onClose}
+        />
+      );
+    }
+
+    return (
+      <div className='modal-root__modal report-dialog-modal'>
+        <div className='report-modal__target'>
+          <IconButton className='report-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={20} />
+          <FormattedMessage id='filter_modal.title.status' defaultMessage='Filter a post' />
+        </div>
+
+        <div className='report-dialog-modal__container'>
+          {stepComponent}
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default connect()(injectIntl(FilterModal));
diff --git a/app/javascript/flavours/blobfox/features/ui/components/focal_point_modal.jsx b/app/javascript/flavours/blobfox/features/ui/components/focal_point_modal.jsx
new file mode 100644
index 00000000000000..6d75f2acab94f8
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/focal_point_modal.jsx
@@ -0,0 +1,426 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
+
+import classNames from 'classnames';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
+import Textarea from 'react-textarea-autosize';
+import { length } from 'stringz';
+// eslint-disable-next-line import/extensions
+import tesseractWorkerPath from 'tesseract.js/dist/worker.min.js';
+// eslint-disable-next-line import/no-extraneous-dependencies
+import tesseractCorePath from 'tesseract.js-core/tesseract-core.wasm.js';
+
+import { Button } from 'flavours/blobfox/components/button';
+import { GIFV } from 'flavours/blobfox/components/gifv';
+import { IconButton } from 'flavours/blobfox/components/icon_button';
+import Audio from 'flavours/blobfox/features/audio';
+import CharacterCounter from 'flavours/blobfox/features/compose/components/character_counter';
+import UploadProgress from 'flavours/blobfox/features/compose/components/upload_progress';
+import { Tesseract as fetchTesseract } from 'flavours/blobfox/features/ui/util/async-components';
+import { me } from 'flavours/blobfox/initial_state';
+import { assetHost } from 'flavours/blobfox/utils/config';
+
+import { changeUploadCompose, uploadThumbnail, onChangeMediaDescription, onChangeMediaFocus } from '../../../actions/compose';
+import Video, { getPointerPosition } from '../../video';
+
+const messages = defineMessages({
+  close: { id: 'lightbox.close', defaultMessage: 'Close' },
+  apply: { id: 'upload_modal.apply', defaultMessage: 'Apply' },
+  applying: { id: 'upload_modal.applying', defaultMessage: 'Applying…' },
+  placeholder: { id: 'upload_modal.description_placeholder', defaultMessage: 'A quick brown fox jumps over the lazy dog' },
+  chooseImage: { id: 'upload_modal.choose_image', defaultMessage: 'Choose image' },
+  discardMessage: { id: 'confirmations.discard_edit_media.message', defaultMessage: 'You have unsaved changes to the media description or preview, discard them anyway?' },
+  discardConfirm: { id: 'confirmations.discard_edit_media.confirm', defaultMessage: 'Discard' },
+});
+
+const mapStateToProps = (state, { id }) => ({
+  media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
+  account: state.getIn(['accounts', me]),
+  isUploadingThumbnail: state.getIn(['compose', 'isUploadingThumbnail']),
+  description: state.getIn(['compose', 'media_modal', 'description']),
+  lang: state.getIn(['compose', 'language']),
+  focusX: state.getIn(['compose', 'media_modal', 'focusX']),
+  focusY: state.getIn(['compose', 'media_modal', 'focusY']),
+  dirty: state.getIn(['compose', 'media_modal', 'dirty']),
+  is_changing_upload: state.getIn(['compose', 'is_changing_upload']),
+});
+
+const mapDispatchToProps = (dispatch, { id }) => ({
+
+  onSave: (description, x, y) => {
+    dispatch(changeUploadCompose(id, { description, focus: `${x.toFixed(2)},${y.toFixed(2)}` }));
+  },
+
+  onChangeDescription: (description) => {
+    dispatch(onChangeMediaDescription(description));
+  },
+
+  onChangeFocus: (focusX, focusY) => {
+    dispatch(onChangeMediaFocus(focusX, focusY));
+  },
+
+  onSelectThumbnail: files => {
+    dispatch(uploadThumbnail(id, files[0]));
+  },
+
+});
+
+const removeExtraLineBreaks = str => str.replace(/\n\n/g, '******')
+  .replace(/\n/g, ' ')
+  .replace(/\*\*\*\*\*\*/g, '\n\n');
+
+class ImageLoader extends PureComponent {
+
+  static propTypes = {
+    src: PropTypes.string.isRequired,
+    width: PropTypes.number,
+    height: PropTypes.number,
+  };
+
+  state = {
+    loading: true,
+  };
+
+  componentDidMount() {
+    const image = new Image();
+    image.addEventListener('load', () => this.setState({ loading: false }));
+    image.src = this.props.src;
+  }
+
+  render () {
+    const { loading } = this.state;
+
+    if (loading) {
+      return <canvas width={this.props.width} height={this.props.height} />;
+    } else {
+      return <img {...this.props} alt='' />;
+    }
+  }
+
+}
+
+class FocalPointModal extends ImmutablePureComponent {
+
+  static propTypes = {
+    media: ImmutablePropTypes.map.isRequired,
+    account: ImmutablePropTypes.record.isRequired,
+    isUploadingThumbnail: PropTypes.bool,
+    onSave: PropTypes.func.isRequired,
+    onChangeDescription: PropTypes.func.isRequired,
+    onChangeFocus: PropTypes.func.isRequired,
+    onSelectThumbnail: PropTypes.func.isRequired,
+    onClose: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  state = {
+    dragging: false,
+    dirty: false,
+    progress: 0,
+    loading: true,
+    ocrStatus: '',
+  };
+
+  componentWillUnmount () {
+    document.removeEventListener('mousemove', this.handleMouseMove);
+    document.removeEventListener('mouseup', this.handleMouseUp);
+  }
+
+  handleMouseDown = e => {
+    document.addEventListener('mousemove', this.handleMouseMove);
+    document.addEventListener('mouseup', this.handleMouseUp);
+
+    this.updatePosition(e);
+    this.setState({ dragging: true });
+  };
+
+  handleTouchStart = e => {
+    document.addEventListener('touchmove', this.handleMouseMove);
+    document.addEventListener('touchend', this.handleTouchEnd);
+
+    this.updatePosition(e);
+    this.setState({ dragging: true });
+  };
+
+  handleMouseMove = e => {
+    this.updatePosition(e);
+  };
+
+  handleMouseUp = () => {
+    document.removeEventListener('mousemove', this.handleMouseMove);
+    document.removeEventListener('mouseup', this.handleMouseUp);
+
+    this.setState({ dragging: false });
+  };
+
+  handleTouchEnd = () => {
+    document.removeEventListener('touchmove', this.handleMouseMove);
+    document.removeEventListener('touchend', this.handleTouchEnd);
+
+    this.setState({ dragging: false });
+  };
+
+  updatePosition = e => {
+    const { x, y } = getPointerPosition(this.node, e);
+    const focusX   = (x - .5) *  2;
+    const focusY   = (y - .5) * -2;
+
+    this.props.onChangeFocus(focusX, focusY);
+  };
+
+  handleChange = e => {
+    this.props.onChangeDescription(e.target.value);
+  };
+
+  handleKeyDown = (e) => {
+    if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
+      e.preventDefault();
+      e.stopPropagation();
+      this.props.onChangeDescription(e.target.value);
+      this.handleSubmit();
+    }
+  };
+
+  handleSubmit = () => {
+    this.props.onSave(this.props.description, this.props.focusX, this.props.focusY);
+  };
+
+  getCloseConfirmationMessage = () => {
+    const { intl, dirty } = this.props;
+
+    if (dirty) {
+      return {
+        message: intl.formatMessage(messages.discardMessage),
+        confirm: intl.formatMessage(messages.discardConfirm),
+      };
+    } else {
+      return null;
+    }
+  };
+
+  setRef = c => {
+    this.node = c;
+  };
+
+  handleTextDetection = () => {
+    this._detectText();
+  };
+
+  _detectText = (refreshCache = false) => {
+    const { media } = this.props;
+
+    this.setState({ detecting: true });
+
+    fetchTesseract().then(({ createWorker }) => {
+      const worker = createWorker({
+        workerPath: tesseractWorkerPath,
+        corePath: tesseractCorePath,
+        langPath: `${assetHost}/ocr/lang-data`,
+        logger: ({ status, progress }) => {
+          if (status === 'recognizing text') {
+            this.setState({ ocrStatus: 'detecting', progress });
+          } else {
+            this.setState({ ocrStatus: 'preparing', progress });
+          }
+        },
+        cacheMethod: refreshCache ? 'refresh' : 'write',
+      });
+
+      let media_url = media.get('url');
+
+      if (window.URL && URL.createObjectURL) {
+        try {
+          media_url = URL.createObjectURL(media.get('file'));
+        } catch (error) {
+          console.error(error);
+        }
+      }
+
+      return (async () => {
+        await worker.load();
+        await worker.loadLanguage('eng');
+        await worker.initialize('eng');
+        const { data: { text } } = await worker.recognize(media_url);
+        this.setState({ detecting: false });
+        this.props.onChangeDescription(removeExtraLineBreaks(text));
+        await worker.terminate();
+      })().catch((e) => {
+        if (refreshCache) {
+          throw e;
+        } else {
+          this._detectText(true);
+        }
+      });
+    }).catch((e) => {
+      console.error(e);
+      this.setState({ detecting: false });
+    });
+  };
+
+  handleThumbnailChange = e => {
+    if (e.target.files.length > 0) {
+      this.props.onSelectThumbnail(e.target.files);
+    }
+  };
+
+  setFileInputRef = c => {
+    this.fileInput = c;
+  };
+
+  handleFileInputClick = () => {
+    this.fileInput.click();
+  };
+
+  render () {
+    const { media, intl, account, onClose, isUploadingThumbnail, description, lang, focusX, focusY, dirty, is_changing_upload } = this.props;
+    const { dragging, detecting, progress, ocrStatus } = this.state;
+    const x = (focusX /  2) + .5;
+    const y = (focusY / -2) + .5;
+
+    const width  = media.getIn(['meta', 'original', 'width']) || null;
+    const height = media.getIn(['meta', 'original', 'height']) || null;
+    const focals = ['image', 'gifv'].includes(media.get('type'));
+    const thumbnailable = ['audio', 'video'].includes(media.get('type'));
+
+    const previewRatio  = 16/9;
+    const previewWidth  = 200;
+    const previewHeight = previewWidth / previewRatio;
+
+    let descriptionLabel = null;
+
+    if (media.get('type') === 'audio') {
+      descriptionLabel = <FormattedMessage id='upload_form.audio_description' defaultMessage='Describe for people who are hard of hearing' />;
+    } else if (media.get('type') === 'video') {
+      descriptionLabel = <FormattedMessage id='upload_form.video_description' defaultMessage='Describe for people who are deaf, hard of hearing, blind or have low vision' />;
+    } else {
+      descriptionLabel = <FormattedMessage id='upload_form.description' defaultMessage='Describe for people who are blind or have low vision' />;
+    }
+
+    let ocrMessage = '';
+    if (ocrStatus === 'detecting') {
+      ocrMessage = <FormattedMessage id='upload_modal.analyzing_picture' defaultMessage='Analyzing picture…' />;
+    } else {
+      ocrMessage = <FormattedMessage id='upload_modal.preparing_ocr' defaultMessage='Preparing OCR…' />;
+    }
+
+    return (
+      <div className='modal-root__modal report-modal' style={{ maxWidth: 960 }}>
+        <div className='report-modal__target'>
+          <IconButton className='report-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={20} />
+          <FormattedMessage id='upload_modal.edit_media' defaultMessage='Edit media' />
+        </div>
+
+        <div className='report-modal__container'>
+          <div className='report-modal__comment'>
+            {focals && <p><FormattedMessage id='upload_modal.hint' defaultMessage='Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.' /></p>}
+
+            {thumbnailable && (
+              <>
+                <label className='setting-text-label' htmlFor='upload-modal__thumbnail'><FormattedMessage id='upload_form.thumbnail' defaultMessage='Change thumbnail' /></label>
+
+                <Button disabled={isUploadingThumbnail || !media.get('unattached')} text={intl.formatMessage(messages.chooseImage)} onClick={this.handleFileInputClick} />
+
+                <label>
+                  <span style={{ display: 'none' }}>{intl.formatMessage(messages.chooseImage)}</span>
+
+                  <input
+                    id='upload-modal__thumbnail'
+                    ref={this.setFileInputRef}
+                    type='file'
+                    accept='image/png,image/jpeg'
+                    onChange={this.handleThumbnailChange}
+                    style={{ display: 'none' }}
+                    disabled={isUploadingThumbnail || is_changing_upload}
+                  />
+                </label>
+
+                <hr className='setting-divider' />
+              </>
+            )}
+
+            <label className='setting-text-label' htmlFor='upload-modal__description'>
+              {descriptionLabel}
+            </label>
+
+            <div className='setting-text__wrapper'>
+              <Textarea
+                id='upload-modal__description'
+                className='setting-text light'
+                value={detecting ? '…' : description}
+                lang={lang}
+                onChange={this.handleChange}
+                onKeyDown={this.handleKeyDown}
+                disabled={detecting || is_changing_upload}
+                autoFocus
+              />
+
+              <div className='setting-text__modifiers'>
+                <UploadProgress progress={progress * 100} active={detecting} icon='file-text-o' message={ocrMessage} />
+              </div>
+            </div>
+
+            <div className='setting-text__toolbar'>
+              <button disabled={detecting || media.get('type') !== 'image' || is_changing_upload} className='link-button' onClick={this.handleTextDetection}><FormattedMessage id='upload_modal.detect_text' defaultMessage='Detect text from picture' /></button>
+              <CharacterCounter max={10000} text={detecting ? '' : description} />
+            </div>
+
+            <Button disabled={!dirty || detecting || isUploadingThumbnail || length(description) > 10000 || is_changing_upload} text={intl.formatMessage(is_changing_upload ? messages.applying : messages.apply)} onClick={this.handleSubmit} />
+          </div>
+
+          <div className='focal-point-modal__content'>
+            {focals && (
+              <div className={classNames('focal-point', { dragging })} ref={this.setRef} onMouseDown={this.handleMouseDown} onTouchStart={this.handleTouchStart}>
+                {media.get('type') === 'image' && <ImageLoader src={media.get('url')} width={width} height={height} alt='' />}
+                {media.get('type') === 'gifv' && <GIFV src={media.get('url')} key={media.get('url')} width={width} height={height} />}
+
+                <div className='focal-point__preview'>
+                  <strong><FormattedMessage id='upload_modal.preview_label' defaultMessage='Preview ({ratio})' values={{ ratio: '16:9' }} /></strong>
+                  <div style={{ width: previewWidth, height: previewHeight, backgroundImage: `url(${media.get('preview_url')})`, backgroundSize: 'cover', backgroundPosition: `${x * 100}% ${y * 100}%` }} />
+                </div>
+
+                <div className='focal-point__reticle' style={{ top: `${y * 100}%`, left: `${x * 100}%` }} />
+                <div className='focal-point__overlay' />
+              </div>
+            )}
+
+            {media.get('type') === 'video' && (
+              <Video
+                preview={media.get('preview_url')}
+                frameRate={media.getIn(['meta', 'original', 'frame_rate'])}
+                blurhash={media.get('blurhash')}
+                src={media.get('url')}
+                detailed
+                inline
+                editable
+              />
+            )}
+
+            {media.get('type') === 'audio' && (
+              <Audio
+                src={media.get('url')}
+                duration={media.getIn(['meta', 'original', 'duration'], 0)}
+                height={150}
+                poster={media.get('preview_url') || account.get('avatar_static')}
+                backgroundColor={media.getIn(['meta', 'colors', 'background'])}
+                foregroundColor={media.getIn(['meta', 'colors', 'foreground'])}
+                accentColor={media.getIn(['meta', 'colors', 'accent'])}
+                editable
+              />
+            )}
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps, mapDispatchToProps, null, {
+  forwardRef: true,
+})(injectIntl(FocalPointModal, { forwardRef: true }));
diff --git a/app/javascript/flavours/blobfox/features/ui/components/follow_requests_column_link.jsx b/app/javascript/flavours/blobfox/features/ui/components/follow_requests_column_link.jsx
new file mode 100644
index 00000000000000..d2481f0d4d49d0
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/follow_requests_column_link.jsx
@@ -0,0 +1,54 @@
+import PropTypes from 'prop-types';
+import { Component } from 'react';
+
+import { injectIntl, defineMessages } from 'react-intl';
+
+import { List as ImmutableList } from 'immutable';
+import { connect } from 'react-redux';
+
+import { fetchFollowRequests } from 'flavours/blobfox/actions/accounts';
+import { IconWithBadge } from 'flavours/blobfox/components/icon_with_badge';
+import ColumnLink from 'flavours/blobfox/features/ui/components/column_link';
+
+const messages = defineMessages({
+  text: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
+});
+
+const mapStateToProps = state => ({
+  count: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
+});
+
+class FollowRequestsColumnLink extends Component {
+
+  static propTypes = {
+    dispatch: PropTypes.func.isRequired,
+    count: PropTypes.number.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  componentDidMount () {
+    const { dispatch } = this.props;
+
+    dispatch(fetchFollowRequests());
+  }
+
+  render () {
+    const { count, intl } = this.props;
+
+    if (count === 0) {
+      return null;
+    }
+
+    return (
+      <ColumnLink
+        transparent
+        to='/follow_requests'
+        icon={<IconWithBadge className='column-link__icon' id='user-plus' count={count} />}
+        text={intl.formatMessage(messages.text)}
+      />
+    );
+  }
+
+}
+
+export default injectIntl(connect(mapStateToProps)(FollowRequestsColumnLink));
diff --git a/app/javascript/flavours/blobfox/features/ui/components/header.jsx b/app/javascript/flavours/blobfox/features/ui/components/header.jsx
new file mode 100644
index 00000000000000..6ef773b4d30418
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/header.jsx
@@ -0,0 +1,124 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
+
+import { Link, withRouter } from 'react-router-dom';
+
+import { connect } from 'react-redux';
+
+import { openModal } from 'flavours/blobfox/actions/modal';
+import { fetchServer } from 'flavours/blobfox/actions/server';
+import { Avatar } from 'flavours/blobfox/components/avatar';
+import { Icon } from 'flavours/blobfox/components/icon';
+import { WordmarkLogo, SymbolLogo } from 'flavours/blobfox/components/logo';
+import Permalink from 'flavours/blobfox/components/permalink';
+import { registrationsOpen, me, sso_redirect } from 'flavours/blobfox/initial_state';
+
+const Account = connect(state => ({
+  account: state.getIn(['accounts', me]),
+}))(({ account }) => (
+  <Permalink href={account.get('url')} to={`/@${account.get('acct')}`} title={account.get('acct')}>
+    <Avatar account={account} size={35} />
+  </Permalink>
+));
+
+const messages = defineMessages({
+  search: { id: 'navigation_bar.search', defaultMessage: 'Search' },
+});
+
+const mapStateToProps = (state) => ({
+  signupUrl: state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up',
+});
+
+const mapDispatchToProps = (dispatch) => ({
+  openClosedRegistrationsModal() {
+    dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS' }));
+  },
+  dispatchServer() {
+    dispatch(fetchServer());
+  }
+});
+
+class Header extends PureComponent {
+
+  static contextTypes = {
+    identity: PropTypes.object,
+  };
+
+  static propTypes = {
+    openClosedRegistrationsModal: PropTypes.func,
+    location: PropTypes.object,
+    signupUrl: PropTypes.string.isRequired,
+    dispatchServer: PropTypes.func,
+    intl: PropTypes.object.isRequired,
+  };
+
+  componentDidMount () {
+    const { dispatchServer } = this.props;
+    dispatchServer();
+  }
+
+  render () {
+    const { signedIn } = this.context.identity;
+    const { location, openClosedRegistrationsModal, signupUrl, intl } = this.props;
+
+    let content;
+
+    if (signedIn) {
+      content = (
+        <>
+          {location.pathname !== '/search' && <Link to='/search' className='button button-secondary' aria-label={intl.formatMessage(messages.search)}><Icon id='search' /></Link>}
+          {location.pathname !== '/publish' && <Link to='/publish' className='button button-secondary'><FormattedMessage id='compose_form.publish_form' defaultMessage='New post' /></Link>}
+          <Account />
+        </>
+      );
+    } else {
+
+      if (sso_redirect) {
+        content = (
+          <a href={sso_redirect} data-method='post' className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.sso_redirect' defaultMessage='Login or Register' /></a>
+        );
+      } else {
+        let signupButton;
+
+        if (registrationsOpen) {
+          signupButton = (
+            <a href={signupUrl} className='button'>
+              <FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
+            </a>
+          );
+        } else {
+          signupButton = (
+            <button className='button' onClick={openClosedRegistrationsModal}>
+              <FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
+            </button>
+          );
+        }
+
+        content = (
+          <>
+            {signupButton}
+            <a href='/auth/sign_in' className='button button-tertiary'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' /></a>
+          </>
+        );
+      }
+    }
+
+    return (
+      <div className='ui__header'>
+        <Link to='/' className='ui__header__logo'>
+          <WordmarkLogo />
+          <SymbolLogo />
+        </Link>
+
+        <div className='ui__header__links'>
+          {content}
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(withRouter(connect(mapStateToProps, mapDispatchToProps)(Header)));
diff --git a/app/javascript/flavours/blobfox/features/ui/components/image_loader.jsx b/app/javascript/flavours/blobfox/features/ui/components/image_loader.jsx
new file mode 100644
index 00000000000000..9dabc621b427e4
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/image_loader.jsx
@@ -0,0 +1,174 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import classNames from 'classnames';
+
+import { LoadingBar } from 'react-redux-loading-bar';
+
+import ZoomableImage from './zoomable_image';
+
+export default class ImageLoader extends PureComponent {
+
+  static propTypes = {
+    alt: PropTypes.string,
+    lang: PropTypes.string,
+    src: PropTypes.string.isRequired,
+    previewSrc: PropTypes.string,
+    width: PropTypes.number,
+    height: PropTypes.number,
+    onClick: PropTypes.func,
+    zoomButtonHidden: PropTypes.bool,
+  };
+
+  static defaultProps = {
+    alt: '',
+    lang: '',
+    width: null,
+    height: null,
+  };
+
+  state = {
+    loading: true,
+    error: false,
+    width: null,
+  };
+
+  removers = [];
+  canvas = null;
+
+  get canvasContext() {
+    if (!this.canvas) {
+      return null;
+    }
+    this._canvasContext = this._canvasContext || this.canvas.getContext('2d');
+    return this._canvasContext;
+  }
+
+  componentDidMount () {
+    this.loadImage(this.props);
+  }
+
+  UNSAFE_componentWillReceiveProps (nextProps) {
+    if (this.props.src !== nextProps.src) {
+      this.loadImage(nextProps);
+    }
+  }
+
+  componentWillUnmount () {
+    this.removeEventListeners();
+  }
+
+  loadImage (props) {
+    this.removeEventListeners();
+    this.setState({ loading: true, error: false });
+    Promise.all([
+      props.previewSrc && this.loadPreviewCanvas(props),
+      this.hasSize() && this.loadOriginalImage(props),
+    ].filter(Boolean))
+      .then(() => {
+        this.setState({ loading: false, error: false });
+        this.clearPreviewCanvas();
+      })
+      .catch(() => this.setState({ loading: false, error: true }));
+  }
+
+  loadPreviewCanvas = ({ previewSrc, width, height }) => new Promise((resolve, reject) => {
+    const image = new Image();
+    const removeEventListeners = () => {
+      image.removeEventListener('error', handleError);
+      image.removeEventListener('load', handleLoad);
+    };
+    const handleError = () => {
+      removeEventListeners();
+      reject();
+    };
+    const handleLoad = () => {
+      removeEventListeners();
+      this.canvasContext.drawImage(image, 0, 0, width, height);
+      resolve();
+    };
+    image.addEventListener('error', handleError);
+    image.addEventListener('load', handleLoad);
+    image.src = previewSrc;
+    this.removers.push(removeEventListeners);
+  });
+
+  clearPreviewCanvas () {
+    const { width, height } = this.canvas;
+    this.canvasContext.clearRect(0, 0, width, height);
+  }
+
+  loadOriginalImage = ({ src }) => new Promise((resolve, reject) => {
+    const image = new Image();
+    const removeEventListeners = () => {
+      image.removeEventListener('error', handleError);
+      image.removeEventListener('load', handleLoad);
+    };
+    const handleError = () => {
+      removeEventListeners();
+      reject();
+    };
+    const handleLoad = () => {
+      removeEventListeners();
+      resolve();
+    };
+    image.addEventListener('error', handleError);
+    image.addEventListener('load', handleLoad);
+    image.src = src;
+    this.removers.push(removeEventListeners);
+  });
+
+  removeEventListeners () {
+    this.removers.forEach(listeners => listeners());
+    this.removers = [];
+  }
+
+  hasSize () {
+    const { width, height } = this.props;
+    return typeof width === 'number' && typeof height === 'number';
+  }
+
+  setCanvasRef = c => {
+    this.canvas = c;
+    if (c) this.setState({ width: c.offsetWidth });
+  };
+
+  render () {
+    const { alt, lang, src, width, height, onClick } = this.props;
+    const { loading } = this.state;
+
+    const className = classNames('image-loader', {
+      'image-loader--loading': loading,
+      'image-loader--amorphous': !this.hasSize(),
+    });
+
+    return (
+      <div className={className}>
+        {loading ? (
+          <>
+            <div className='loading-bar__container' style={{ width: this.state.width || width }}>
+              <LoadingBar className='loading-bar' loading={1} />
+            </div>
+            <canvas
+              className='image-loader__preview-canvas'
+              ref={this.setCanvasRef}
+              width={width}
+              height={height}
+            />
+          </>
+        ) : (
+          <ZoomableImage
+            alt={alt}
+            lang={lang}
+            src={src}
+            onClick={onClick}
+            width={width}
+            height={height}
+            zoomButtonHidden={this.props.zoomButtonHidden}
+          />
+        )}
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/ui/components/image_modal.jsx b/app/javascript/flavours/blobfox/features/ui/components/image_modal.jsx
new file mode 100644
index 00000000000000..a3106d0ac18c3e
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/image_modal.jsx
@@ -0,0 +1,64 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, injectIntl } from 'react-intl';
+
+import classNames from 'classnames';
+
+import { IconButton } from 'flavours/blobfox/components/icon_button';
+
+import ImageLoader from './image_loader';
+
+const messages = defineMessages({
+  close: { id: 'lightbox.close', defaultMessage: 'Close' },
+});
+
+class ImageModal extends PureComponent {
+
+  static propTypes = {
+    src: PropTypes.string.isRequired,
+    alt: PropTypes.string.isRequired,
+    onClose: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  state = {
+    navigationHidden: false,
+  };
+
+  toggleNavigation = () => {
+    this.setState(prevState => ({
+      navigationHidden: !prevState.navigationHidden,
+    }));
+  };
+
+  render () {
+    const { intl, src, alt, onClose } = this.props;
+    const { navigationHidden } = this.state;
+
+    const navigationClassName = classNames('media-modal__navigation', {
+      'media-modal__navigation--hidden': navigationHidden,
+    });
+
+    return (
+      <div className='modal-root__modal media-modal'>
+        <div className='media-modal__closer' role='presentation' onClick={onClose} >
+          <ImageLoader
+            src={src}
+            width={400}
+            height={400}
+            alt={alt}
+            onClick={this.toggleNavigation}
+          />
+        </div>
+
+        <div className={navigationClassName}>
+          <IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={40} />
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(ImageModal);
diff --git a/app/javascript/flavours/blobfox/features/ui/components/link_footer.jsx b/app/javascript/flavours/blobfox/features/ui/components/link_footer.jsx
new file mode 100644
index 00000000000000..d9e80497f371c4
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/link_footer.jsx
@@ -0,0 +1,111 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
+
+import { Link } from 'react-router-dom';
+
+import { connect } from 'react-redux';
+
+import { openModal } from 'flavours/blobfox/actions/modal';
+import { domain, version, source_url, statusPageUrl, profile_directory as profileDirectory } from 'flavours/blobfox/initial_state';
+import { PERMISSION_INVITE_USERS } from 'flavours/blobfox/permissions';
+import { logOut } from 'flavours/blobfox/utils/log_out';
+
+const messages = defineMessages({
+  logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
+  logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
+});
+
+const mapDispatchToProps = (dispatch, { intl }) => ({
+  onLogout () {
+    dispatch(openModal({
+      modalType: 'CONFIRM',
+      modalProps: {
+        message: intl.formatMessage(messages.logoutMessage),
+        confirm: intl.formatMessage(messages.logoutConfirm),
+        closeWhenConfirm: false,
+        onConfirm: () => logOut(),
+      },
+    }));
+  },
+});
+
+class LinkFooter extends PureComponent {
+
+  static contextTypes = {
+    identity: PropTypes.object,
+  };
+
+  static propTypes = {
+    multiColumn: PropTypes.bool,
+    onLogout: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  handleLogoutClick = e => {
+    e.preventDefault();
+    e.stopPropagation();
+
+    this.props.onLogout();
+
+    return false;
+  };
+
+  render () {
+    const { signedIn, permissions } = this.context.identity;
+    const { multiColumn } = this.props;
+
+    const canInvite = signedIn && ((permissions & PERMISSION_INVITE_USERS) === PERMISSION_INVITE_USERS);
+    const canProfileDirectory = profileDirectory;
+
+    const DividingCircle = <span aria-hidden>{' · '}</span>;
+
+    return (
+      <div className='link-footer'>
+        <p>
+          <strong>{domain}</strong>:
+          {' '}
+          <Link to='/about' target={multiColumn ? '_blank' : undefined}><FormattedMessage id='footer.about' defaultMessage='About' /></Link>
+          {statusPageUrl && (
+            <>
+              {DividingCircle}
+              <a href={statusPageUrl} target='_blank' rel='noopener'><FormattedMessage id='footer.status' defaultMessage='Status' /></a>
+            </>
+          )}
+          {canInvite && (
+            <>
+              {DividingCircle}
+              <a href='/invites' target='_blank'><FormattedMessage id='footer.invite' defaultMessage='Invite people' /></a>
+            </>
+          )}
+          {canProfileDirectory && (
+            <>
+              {DividingCircle}
+              <Link to='/directory'><FormattedMessage id='footer.directory' defaultMessage='Profiles directory' /></Link>
+            </>
+          )}
+          {DividingCircle}
+          <Link to='/privacy-policy' target={multiColumn ? '_blank' : undefined}><FormattedMessage id='footer.privacy_policy' defaultMessage='Privacy policy' /></Link>
+        </p>
+
+        <p>
+          <strong>Mastodon</strong>:
+          {' '}
+          <a href='https://joinmastodon.org' target='_blank'><FormattedMessage id='footer.about' defaultMessage='About' /></a>
+          {DividingCircle}
+          <a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='footer.get_app' defaultMessage='Get the app' /></a>
+          {DividingCircle}
+          <Link to='/keyboard-shortcuts'><FormattedMessage id='footer.keyboard_shortcuts' defaultMessage='Keyboard shortcuts' /></Link>
+          {DividingCircle}
+          <a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='footer.source_code' defaultMessage='View source code' /></a>
+          {DividingCircle}
+          <span className='version'>v{version}</span>
+        </p>
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(connect(null, mapDispatchToProps)(LinkFooter));
diff --git a/app/javascript/flavours/blobfox/features/ui/components/list_panel.jsx b/app/javascript/flavours/blobfox/features/ui/components/list_panel.jsx
new file mode 100644
index 00000000000000..aeae19018172d2
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/list_panel.jsx
@@ -0,0 +1,56 @@
+import PropTypes from 'prop-types';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+
+import { fetchLists } from 'flavours/blobfox/actions/lists';
+
+import ColumnLink from './column_link';
+
+const getOrderedLists = createSelector([state => state.get('lists')], lists => {
+  if (!lists) {
+    return lists;
+  }
+
+  return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title'))).take(4);
+});
+
+const mapStateToProps = state => ({
+  lists: getOrderedLists(state),
+});
+
+class ListPanel extends ImmutablePureComponent {
+
+  static propTypes = {
+    dispatch: PropTypes.func.isRequired,
+    lists: ImmutablePropTypes.list,
+  };
+
+  componentDidMount () {
+    const { dispatch } = this.props;
+    dispatch(fetchLists());
+  }
+
+  render () {
+    const { lists } = this.props;
+
+    if (!lists || lists.isEmpty()) {
+      return null;
+    }
+
+    return (
+      <div className='list-panel'>
+        <hr />
+
+        {lists.map(list => (
+          <ColumnLink icon='list-ul' key={list.get('id')} strict text={list.get('title')} to={`/lists/${list.get('id')}`} transparent />
+        ))}
+      </div>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(ListPanel);
diff --git a/app/javascript/flavours/blobfox/features/ui/components/media_modal.jsx b/app/javascript/flavours/blobfox/features/ui/components/media_modal.jsx
new file mode 100644
index 00000000000000..77d4b7ae792f9b
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/media_modal.jsx
@@ -0,0 +1,261 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl } from 'react-intl';
+
+import classNames from 'classnames';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import ReactSwipeableViews from 'react-swipeable-views';
+
+import { getAverageFromBlurhash } from 'flavours/blobfox/blurhash';
+import { GIFV } from 'flavours/blobfox/components/gifv';
+import { Icon }  from 'flavours/blobfox/components/icon';
+import { IconButton } from 'flavours/blobfox/components/icon_button';
+import Footer from 'flavours/blobfox/features/picture_in_picture/components/footer';
+import Video from 'flavours/blobfox/features/video';
+import { disableSwiping } from 'flavours/blobfox/initial_state';
+
+import ImageLoader from './image_loader';
+
+const messages = defineMessages({
+  close: { id: 'lightbox.close', defaultMessage: 'Close' },
+  previous: { id: 'lightbox.previous', defaultMessage: 'Previous' },
+  next: { id: 'lightbox.next', defaultMessage: 'Next' },
+});
+
+class MediaModal extends ImmutablePureComponent {
+
+  static propTypes = {
+    media: ImmutablePropTypes.list.isRequired,
+    statusId: PropTypes.string,
+    lang: PropTypes.string,
+    index: PropTypes.number.isRequired,
+    onClose: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+    onChangeBackgroundColor: PropTypes.func.isRequired,
+    currentTime: PropTypes.number,
+    autoPlay: PropTypes.bool,
+    volume: PropTypes.number,
+  };
+
+  state = {
+    index: null,
+    navigationHidden: false,
+    zoomButtonHidden: false,
+  };
+
+  handleSwipe = (index) => {
+    this.setState({ index: index % this.props.media.size });
+  };
+
+  handleTransitionEnd = () => {
+    this.setState({
+      zoomButtonHidden: false,
+    });
+  };
+
+  handleNextClick = () => {
+    this.setState({
+      index: (this.getIndex() + 1) % this.props.media.size,
+      zoomButtonHidden: true,
+    });
+  };
+
+  handlePrevClick = () => {
+    this.setState({
+      index: (this.props.media.size + this.getIndex() - 1) % this.props.media.size,
+      zoomButtonHidden: true,
+    });
+  };
+
+  handleChangeIndex = (e) => {
+    const index = Number(e.currentTarget.getAttribute('data-index'));
+
+    this.setState({
+      index: index % this.props.media.size,
+      zoomButtonHidden: true,
+    });
+  };
+
+  handleKeyDown = (e) => {
+    switch(e.key) {
+    case 'ArrowLeft':
+      this.handlePrevClick();
+      e.preventDefault();
+      e.stopPropagation();
+      break;
+    case 'ArrowRight':
+      this.handleNextClick();
+      e.preventDefault();
+      e.stopPropagation();
+      break;
+    }
+  };
+
+  componentDidMount () {
+    window.addEventListener('keydown', this.handleKeyDown, false);
+
+    this._sendBackgroundColor();
+  }
+
+  componentDidUpdate (prevProps, prevState) {
+    if (prevState.index !== this.state.index) {
+      this._sendBackgroundColor();
+    }
+  }
+
+  _sendBackgroundColor () {
+    const { media, onChangeBackgroundColor } = this.props;
+    const index = this.getIndex();
+    const blurhash = media.getIn([index, 'blurhash']);
+
+    if (blurhash) {
+      const backgroundColor = getAverageFromBlurhash(blurhash);
+      onChangeBackgroundColor(backgroundColor);
+    }
+  }
+
+  componentWillUnmount () {
+    window.removeEventListener('keydown', this.handleKeyDown);
+
+    this.props.onChangeBackgroundColor(null);
+  }
+
+  getIndex () {
+    return this.state.index !== null ? this.state.index : this.props.index;
+  }
+
+  toggleNavigation = () => {
+    this.setState(prevState => ({
+      navigationHidden: !prevState.navigationHidden,
+    }));
+  };
+
+  render () {
+    const { media, statusId, lang, intl, onClose } = this.props;
+    const { navigationHidden } = this.state;
+
+    const index = this.getIndex();
+
+    const leftNav  = media.size > 1 && <button tabIndex={0} className='media-modal__nav media-modal__nav--left' onClick={this.handlePrevClick} aria-label={intl.formatMessage(messages.previous)}><Icon id='chevron-left' fixedWidth /></button>;
+    const rightNav = media.size > 1 && <button tabIndex={0} className='media-modal__nav  media-modal__nav--right' onClick={this.handleNextClick} aria-label={intl.formatMessage(messages.next)}><Icon id='chevron-right' fixedWidth /></button>;
+
+    const content = media.map((image) => {
+      const width  = image.getIn(['meta', 'original', 'width']) || null;
+      const height = image.getIn(['meta', 'original', 'height']) || null;
+      const description = image.getIn(['translation', 'description']) || image.get('description');
+
+      if (image.get('type') === 'image') {
+        return (
+          <ImageLoader
+            previewSrc={image.get('preview_url')}
+            src={image.get('url')}
+            width={width}
+            height={height}
+            alt={description}
+            lang={lang}
+            key={image.get('url')}
+            onClick={this.toggleNavigation}
+            zoomButtonHidden={this.state.zoomButtonHidden}
+          />
+        );
+      } else if (image.get('type') === 'video') {
+        const { currentTime, autoPlay, volume } = this.props;
+
+        return (
+          <Video
+            preview={image.get('preview_url')}
+            blurhash={image.get('blurhash')}
+            src={image.get('url')}
+            width={image.get('width')}
+            height={image.get('height')}
+            frameRate={image.getIn(['meta', 'original', 'frame_rate'])}
+            currentTime={currentTime || 0}
+            autoPlay={autoPlay || false}
+            volume={volume || 1}
+            onCloseVideo={onClose}
+            detailed
+            alt={description}
+            lang={lang}
+            key={image.get('url')}
+          />
+        );
+      } else if (image.get('type') === 'gifv') {
+        return (
+          <GIFV
+            src={image.get('url')}
+            width={width}
+            height={height}
+            key={image.get('url')}
+            alt={description}
+            lang={lang}
+            onClick={this.toggleNavigation}
+          />
+        );
+      }
+
+      return null;
+    }).toArray();
+
+    // you can't use 100vh, because the viewport height is taller
+    // than the visible part of the document in some mobile
+    // browsers when it's address bar is visible.
+    // https://developers.google.com/web/updates/2016/12/url-bar-resizing
+    const swipeableViewsStyle = {
+      width: '100%',
+      height: '100%',
+    };
+
+    const containerStyle = {
+      alignItems: 'center', // center vertically
+    };
+
+    const navigationClassName = classNames('media-modal__navigation', {
+      'media-modal__navigation--hidden': navigationHidden,
+    });
+
+    let pagination;
+
+    if (media.size > 1) {
+      pagination = media.map((item, i) => (
+        <button key={i} className={classNames('media-modal__page-dot', { active: i === index })} data-index={i} onClick={this.handleChangeIndex}>
+          {i + 1}
+        </button>
+      ));
+    }
+
+    return (
+      <div className='modal-root__modal media-modal'>
+        <div className='media-modal__closer' role='presentation' onClick={onClose} >
+          <ReactSwipeableViews
+            style={swipeableViewsStyle}
+            containerStyle={containerStyle}
+            onChangeIndex={this.handleSwipe}
+            onTransitionEnd={this.handleTransitionEnd}
+            index={index}
+            disabled={disableSwiping}
+          >
+            {content}
+          </ReactSwipeableViews>
+        </div>
+
+        <div className={navigationClassName}>
+          <IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={40} />
+
+          {leftNav}
+          {rightNav}
+
+          <div className='media-modal__overlay'>
+            {pagination && <ul className='media-modal__pagination'>{pagination}</ul>}
+            {statusId && <Footer statusId={statusId} withOpenButton onClose={onClose} />}
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(MediaModal);
diff --git a/app/javascript/flavours/blobfox/features/ui/components/modal_loading.jsx b/app/javascript/flavours/blobfox/features/ui/components/modal_loading.jsx
new file mode 100644
index 00000000000000..7d19e73513336d
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/modal_loading.jsx
@@ -0,0 +1,18 @@
+import { LoadingIndicator } from '../../../components/loading_indicator';
+
+// Keep the markup in sync with <BundleModalError />
+// (make sure they have the same dimensions)
+const ModalLoading = () => (
+  <div className='modal-root__modal error-modal'>
+    <div className='error-modal__body'>
+      <LoadingIndicator />
+    </div>
+    <div className='error-modal__footer'>
+      <div>
+        <button className='error-modal__nav onboarding-modal__skip' />
+      </div>
+    </div>
+  </div>
+);
+
+export default ModalLoading;
diff --git a/app/javascript/flavours/blobfox/features/ui/components/modal_root.jsx b/app/javascript/flavours/blobfox/features/ui/components/modal_root.jsx
new file mode 100644
index 00000000000000..81c63cab9fc401
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/modal_root.jsx
@@ -0,0 +1,142 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { Helmet } from 'react-helmet';
+
+import Base from 'flavours/blobfox/components/modal_root';
+import {
+  MuteModal,
+  BlockModal,
+  ReportModal,
+  SettingsModal,
+  EmbedModal,
+  ListEditor,
+  ListAdder,
+  PinnedAccountsEditor,
+  CompareHistoryModal,
+  FilterModal,
+  InteractionModal,
+  SubscribedLanguagesModal,
+  ClosedRegistrationsModal,
+} from 'flavours/blobfox/features/ui/util/async-components';
+import { getScrollbarWidth } from 'flavours/blobfox/utils/scrollbar';
+
+import BundleContainer from '../containers/bundle_container';
+
+import ActionsModal from './actions_modal';
+import AudioModal from './audio_modal';
+import BoostModal from './boost_modal';
+import BundleModalError from './bundle_modal_error';
+import ConfirmationModal from './confirmation_modal';
+import DeprecatedSettingsModal from './deprecated_settings_modal';
+import DoodleModal from './doodle_modal';
+import FavouriteModal from './favourite_modal';
+import FocalPointModal from './focal_point_modal';
+import ImageModal from './image_modal';
+import MediaModal from './media_modal';
+import ModalLoading from './modal_loading';
+import VideoModal from './video_modal';
+
+export const MODAL_COMPONENTS = {
+  'MEDIA': () => Promise.resolve({ default: MediaModal }),
+  'VIDEO': () => Promise.resolve({ default: VideoModal }),
+  'AUDIO': () => Promise.resolve({ default: AudioModal }),
+  'IMAGE': () => Promise.resolve({ default: ImageModal }),
+  'BOOST': () => Promise.resolve({ default: BoostModal }),
+  'FAVOURITE': () => Promise.resolve({ default: FavouriteModal }),
+  'DOODLE': () => Promise.resolve({ default: DoodleModal }),
+  'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }),
+  'MUTE': MuteModal,
+  'BLOCK': BlockModal,
+  'REPORT': ReportModal,
+  'SETTINGS': SettingsModal,
+  'DEPRECATED_SETTINGS': () => Promise.resolve({ default: DeprecatedSettingsModal }),
+  'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
+  'EMBED': EmbedModal,
+  'LIST_EDITOR': ListEditor,
+  'FOCAL_POINT': () => Promise.resolve({ default: FocalPointModal }),
+  'LIST_ADDER': ListAdder,
+  'PINNED_ACCOUNTS_EDITOR': PinnedAccountsEditor,
+  'COMPARE_HISTORY': CompareHistoryModal,
+  'FILTER': FilterModal,
+  'SUBSCRIBED_LANGUAGES': SubscribedLanguagesModal,
+  'INTERACTION': InteractionModal,
+  'CLOSED_REGISTRATIONS': ClosedRegistrationsModal,
+};
+
+export default class ModalRoot extends PureComponent {
+
+  static propTypes = {
+    type: PropTypes.string,
+    props: PropTypes.object,
+    onClose: PropTypes.func.isRequired,
+    ignoreFocus: PropTypes.bool,
+  };
+
+  state = {
+    backgroundColor: null,
+  };
+
+  componentDidUpdate () {
+    if (this.props.type) {
+      document.body.classList.add('with-modals--active');
+      document.documentElement.style.marginRight = `${getScrollbarWidth()}px`;
+    } else {
+      document.body.classList.remove('with-modals--active');
+      document.documentElement.style.marginRight = '0';
+    }
+  }
+
+  setBackgroundColor = color => {
+    this.setState({ backgroundColor: color });
+  };
+
+  renderLoading = modalId => () => {
+    return ['MEDIA', 'VIDEO', 'BOOST', 'FAVOURITE', 'DOODLE', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? <ModalLoading /> : null;
+  };
+
+  renderError = (props) => {
+    const { onClose } = this.props;
+
+    return <BundleModalError {...props} onClose={onClose} />;
+  };
+
+  handleClose = (ignoreFocus = false) => {
+    const { onClose } = this.props;
+    const message = this._modal?.getCloseConfirmationMessage?.();
+    onClose(message, ignoreFocus);
+  };
+
+  setModalRef = (c) => {
+    this._modal = c;
+  };
+
+  // prevent closing of modal when clicking the overlay
+  noop = () => {};
+
+  render () {
+    const { type, props, ignoreFocus } = this.props;
+    const { backgroundColor } = this.state;
+    const visible = !!type;
+
+    return (
+      <Base backgroundColor={backgroundColor} onClose={props && props.noClose ? this.noop : this.handleClose} noEsc={props ? props.noEsc : false} ignoreFocus={ignoreFocus}>
+        {visible && (
+          <>
+            <BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}>
+              {(SpecificComponent) => {
+                const ref = typeof SpecificComponent !== 'function' ? this.setModalRef : undefined;
+                return <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={ref} />;
+              }}
+            </BundleContainer>
+
+            <Helmet>
+              <meta name='robots' content='noindex' />
+            </Helmet>
+          </>
+        )}
+      </Base>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/ui/components/mute_modal.jsx b/app/javascript/flavours/blobfox/features/ui/components/mute_modal.jsx
new file mode 100644
index 00000000000000..2d95cabef84db1
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/mute_modal.jsx
@@ -0,0 +1,139 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import { connect } from 'react-redux';
+
+import Toggle from 'react-toggle';
+
+import { muteAccount } from '../../../actions/accounts';
+import { closeModal } from '../../../actions/modal';
+import { toggleHideNotifications, changeMuteDuration } from '../../../actions/mutes';
+import { Button } from '../../../components/button';
+
+const messages = defineMessages({
+  minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
+  hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
+  days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' },
+  indefinite: { id: 'mute_modal.indefinite', defaultMessage: 'Indefinite' },
+});
+
+const mapStateToProps = state => {
+  return {
+    account: state.getIn(['mutes', 'new', 'account']),
+    notifications: state.getIn(['mutes', 'new', 'notifications']),
+    muteDuration: state.getIn(['mutes', 'new', 'duration']),
+  };
+};
+
+const mapDispatchToProps = dispatch => {
+  return {
+    onConfirm(account, notifications, muteDuration) {
+      dispatch(muteAccount(account.get('id'), notifications, muteDuration));
+    },
+
+    onClose() {
+      dispatch(closeModal({
+        modalType: undefined,
+        ignoreFocus: false,
+      }));
+    },
+
+    onToggleNotifications() {
+      dispatch(toggleHideNotifications());
+    },
+
+    onChangeMuteDuration(e) {
+      dispatch(changeMuteDuration(e.target.value));
+    },
+  };
+};
+
+class MuteModal extends PureComponent {
+
+  static propTypes = {
+    account: PropTypes.object.isRequired,
+    notifications: PropTypes.bool.isRequired,
+    onClose: PropTypes.func.isRequired,
+    onConfirm: PropTypes.func.isRequired,
+    onToggleNotifications: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+    muteDuration: PropTypes.number.isRequired,
+    onChangeMuteDuration: PropTypes.func.isRequired,
+  };
+
+  handleClick = () => {
+    this.props.onClose();
+    this.props.onConfirm(this.props.account, this.props.notifications, this.props.muteDuration);
+  };
+
+  handleCancel = () => {
+    this.props.onClose();
+  };
+
+  toggleNotifications = () => {
+    this.props.onToggleNotifications();
+  };
+
+  changeMuteDuration = (e) => {
+    this.props.onChangeMuteDuration(e);
+  };
+
+  render () {
+    const { account, notifications, muteDuration, intl } = this.props;
+
+    return (
+      <div className='modal-root__modal mute-modal'>
+        <div className='mute-modal__container'>
+          <p>
+            <FormattedMessage
+              id='confirmations.mute.message'
+              defaultMessage='Are you sure you want to mute {name}?'
+              values={{ name: <strong>@{account.get('acct')}</strong> }}
+            />
+          </p>
+          <p className='mute-modal__explanation'>
+            <FormattedMessage
+              id='confirmations.mute.explanation'
+              defaultMessage='This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.'
+            />
+          </p>
+          <div className='setting-toggle'>
+            <Toggle id='mute-modal__hide-notifications-checkbox' checked={notifications} onChange={this.toggleNotifications} />
+            <label className='setting-toggle__label' htmlFor='mute-modal__hide-notifications-checkbox'>
+              <FormattedMessage id='mute_modal.hide_notifications' defaultMessage='Hide notifications from this user?' />
+            </label>
+          </div>
+          <div>
+            <span><FormattedMessage id='mute_modal.duration' defaultMessage='Duration' />: </span>
+
+            {/* eslint-disable-next-line jsx-a11y/no-onchange */}
+            <select value={muteDuration} onChange={this.changeMuteDuration}>
+              <option value={0}>{intl.formatMessage(messages.indefinite)}</option>
+              <option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option>
+              <option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option>
+              <option value={3600}>{intl.formatMessage(messages.hours, { number: 1 })}</option>
+              <option value={21600}>{intl.formatMessage(messages.hours, { number: 6 })}</option>
+              <option value={86400}>{intl.formatMessage(messages.days, { number: 1 })}</option>
+              <option value={259200}>{intl.formatMessage(messages.days, { number: 3 })}</option>
+              <option value={604800}>{intl.formatMessage(messages.days, { number: 7 })}</option>
+            </select>
+          </div>
+        </div>
+
+        <div className='mute-modal__action-bar'>
+          <Button onClick={this.handleCancel} className='mute-modal__cancel-button'>
+            <FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
+          </Button>
+          <Button onClick={this.handleClick} autoFocus>
+            <FormattedMessage id='confirmations.mute.confirm' defaultMessage='Mute' />
+          </Button>
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(MuteModal));
diff --git a/app/javascript/flavours/blobfox/features/ui/components/navigation_panel.jsx b/app/javascript/flavours/blobfox/features/ui/components/navigation_panel.jsx
new file mode 100644
index 00000000000000..5ed79d0c2b154c
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/navigation_panel.jsx
@@ -0,0 +1,127 @@
+import PropTypes from 'prop-types';
+import { Component } from 'react';
+
+import { defineMessages, injectIntl } from 'react-intl';
+
+import { NavigationPortal } from 'flavours/blobfox/components/navigation_portal';
+import { timelinePreview, trendsEnabled } from 'flavours/blobfox/initial_state';
+import { transientSingleColumn } from 'flavours/blobfox/is_mobile';
+import { preferencesLink } from 'flavours/blobfox/utils/backend_links';
+
+import ColumnLink from './column_link';
+import DisabledAccountBanner from './disabled_account_banner';
+import FollowRequestsColumnLink from './follow_requests_column_link';
+import ListPanel from './list_panel';
+import NotificationsCounterIcon from './notifications_counter_icon';
+import SignInBanner from './sign_in_banner';
+
+const messages = defineMessages({
+  home: { id: 'tabs_bar.home', defaultMessage: 'Home' },
+  notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
+  explore: { id: 'explore.title', defaultMessage: 'Explore' },
+  firehose: { id: 'column.firehose', defaultMessage: 'Live feeds' },
+  direct: { id: 'navigation_bar.direct', defaultMessage: 'Private mentions' },
+  favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' },
+  bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
+  lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
+  preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
+  followsAndFollowers: { id: 'navigation_bar.follows_and_followers', defaultMessage: 'Follows and followers' },
+  about: { id: 'navigation_bar.about', defaultMessage: 'About' },
+  search: { id: 'navigation_bar.search', defaultMessage: 'Search' },
+  advancedInterface: { id: 'navigation_bar.advanced_interface', defaultMessage: 'Open in advanced web interface' },
+  openedInClassicInterface: { id: 'navigation_bar.opened_in_classic_interface', defaultMessage: 'Posts, accounts, and other specific pages are opened by default in the classic web interface.' },
+  app_settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' },
+});
+
+class NavigationPanel extends Component {
+
+  static contextTypes = {
+    identity: PropTypes.object.isRequired,
+  };
+
+  static propTypes = {
+    intl: PropTypes.object.isRequired,
+    onOpenSettings: PropTypes.func,
+  };
+
+  isFirehoseActive = (match, location) => {
+    return match || location.pathname.startsWith('/public');
+  };
+
+  render () {
+    const { intl, onOpenSettings } = this.props;
+    const { signedIn, disabledAccountId } = this.context.identity;
+
+    let banner = undefined;
+
+    if(transientSingleColumn)
+      banner = (<div className='switch-to-advanced'>
+        {intl.formatMessage(messages.openedInClassicInterface)}
+        {" "}
+        <a href={`/deck${location.pathname}`} className='switch-to-advanced__toggle'>
+          {intl.formatMessage(messages.advancedInterface)}
+        </a>
+      </div>);
+
+    return (
+      <div className='navigation-panel'>
+        {banner &&
+          <div className='navigation-panel__banner'>
+            {banner}
+          </div>
+        }
+
+        {signedIn && (
+          <>
+            <ColumnLink transparent to='/home' icon='home' text={intl.formatMessage(messages.home)} />
+            <ColumnLink transparent to='/notifications' icon={<NotificationsCounterIcon className='column-link__icon' />} text={intl.formatMessage(messages.notifications)} />
+            <FollowRequestsColumnLink />
+          </>
+        )}
+
+        {trendsEnabled ? (
+          <ColumnLink transparent to='/explore' icon='hashtag' text={intl.formatMessage(messages.explore)} />
+        ) : (
+          <ColumnLink transparent to='/search' icon='search' text={intl.formatMessage(messages.search)} />
+        )}
+
+        {(signedIn || timelinePreview) && (
+          <ColumnLink transparent to='/public/local' isActive={this.isFirehoseActive} icon='globe' text={intl.formatMessage(messages.firehose)} />
+        )}
+
+        {!signedIn && (
+          <div className='navigation-panel__sign-in-banner'>
+            <hr />
+            { disabledAccountId ? <DisabledAccountBanner /> : <SignInBanner /> }
+          </div>
+        )}
+
+        {signedIn && (
+          <>
+            <ColumnLink transparent to='/conversations' icon='at' text={intl.formatMessage(messages.direct)} />
+            <ColumnLink transparent to='/bookmarks' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} />
+            <ColumnLink transparent to='/favourites' icon='star' text={intl.formatMessage(messages.favourites)} />
+            <ColumnLink transparent to='/lists' icon='list-ul' text={intl.formatMessage(messages.lists)} />
+
+            <ListPanel />
+
+            <hr />
+
+            {!!preferencesLink && <ColumnLink transparent href={preferencesLink} icon='cog' text={intl.formatMessage(messages.preferences)} />}
+            <ColumnLink transparent onClick={onOpenSettings} icon='cogs' text={intl.formatMessage(messages.app_settings)} />
+          </>
+        )}
+
+        <div className='navigation-panel__legal'>
+          <hr />
+          <ColumnLink transparent to='/about' icon='ellipsis-h' text={intl.formatMessage(messages.about)} />
+        </div>
+
+        <NavigationPortal />
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(NavigationPanel);
diff --git a/app/javascript/flavours/blobfox/features/ui/components/notifications_counter_icon.js b/app/javascript/flavours/blobfox/features/ui/components/notifications_counter_icon.js
new file mode 100644
index 00000000000000..1509a0962ee93d
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/notifications_counter_icon.js
@@ -0,0 +1,10 @@
+import { connect } from 'react-redux';
+
+import { IconWithBadge } from 'flavours/blobfox/components/icon_with_badge';
+
+const mapStateToProps = state => ({
+  count: state.getIn(['local_settings', 'notifications', 'tab_badge']) ? state.getIn(['notifications', 'unread']) : 0,
+  id: 'bell',
+});
+
+export default connect(mapStateToProps)(IconWithBadge);
diff --git a/app/javascript/flavours/blobfox/features/ui/components/report_modal.jsx b/app/javascript/flavours/blobfox/features/ui/components/report_modal.jsx
new file mode 100644
index 00000000000000..79029da022d5ca
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/report_modal.jsx
@@ -0,0 +1,227 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
+
+import { OrderedSet } from 'immutable';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
+import { fetchRelationships } from 'flavours/blobfox/actions/accounts';
+import { submitReport } from 'flavours/blobfox/actions/reports';
+import { fetchServer } from 'flavours/blobfox/actions/server';
+import { expandAccountTimeline } from 'flavours/blobfox/actions/timelines';
+import { IconButton } from 'flavours/blobfox/components/icon_button';
+import Category from 'flavours/blobfox/features/report/category';
+import Comment from 'flavours/blobfox/features/report/comment';
+import Rules from 'flavours/blobfox/features/report/rules';
+import Statuses from 'flavours/blobfox/features/report/statuses';
+import Thanks from 'flavours/blobfox/features/report/thanks';
+import { makeGetAccount } from 'flavours/blobfox/selectors';
+
+const messages = defineMessages({
+  close: { id: 'lightbox.close', defaultMessage: 'Close' },
+});
+
+const makeMapStateToProps = () => {
+  const getAccount = makeGetAccount();
+
+  const mapStateToProps = (state, { accountId }) => ({
+    account: getAccount(state, accountId),
+  });
+
+  return mapStateToProps;
+};
+
+class ReportModal extends ImmutablePureComponent {
+
+  static propTypes = {
+    accountId: PropTypes.string.isRequired,
+    statusId: PropTypes.string,
+    dispatch: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+    account: ImmutablePropTypes.record.isRequired,
+  };
+
+  state = {
+    step: 'category',
+    selectedStatusIds: OrderedSet(this.props.statusId ? [this.props.statusId] : []),
+    selectedDomains: OrderedSet(),
+    comment: '',
+    category: null,
+    selectedRuleIds: OrderedSet(),
+    isSubmitting: false,
+    isSubmitted: false,
+  };
+
+  handleSubmit = () => {
+    const { dispatch, accountId } = this.props;
+    const { selectedStatusIds, selectedDomains, comment, category, selectedRuleIds } = this.state;
+
+    this.setState({ isSubmitting: true });
+
+    dispatch(submitReport({
+      account_id: accountId,
+      status_ids: selectedStatusIds.toArray(),
+      forward_to_domains: selectedDomains.toArray(),
+      comment,
+      forward: selectedDomains.size > 0,
+      category,
+      rule_ids: selectedRuleIds.toArray(),
+    }, this.handleSuccess, this.handleFail));
+  };
+
+  handleSuccess = () => {
+    this.setState({ isSubmitting: false, isSubmitted: true, step: 'thanks' });
+  };
+
+  handleFail = () => {
+    this.setState({ isSubmitting: false });
+  };
+
+  handleStatusToggle = (statusId, checked) => {
+    const { selectedStatusIds } = this.state;
+
+    if (checked) {
+      this.setState({ selectedStatusIds: selectedStatusIds.add(statusId) });
+    } else {
+      this.setState({ selectedStatusIds: selectedStatusIds.remove(statusId) });
+    }
+  };
+
+  handleDomainToggle = (domain, checked) => {
+    if (checked) {
+      this.setState((state) => ({ selectedDomains: state.selectedDomains.add(domain) }));
+    } else {
+      this.setState((state) => ({ selectedDomains: state.selectedDomains.remove(domain) }));
+    }
+  };
+
+  handleRuleToggle = (ruleId, checked) => {
+    if (checked) {
+      this.setState((state) => ({ selectedRuleIds: state.selectedRuleIds.add(ruleId) }));
+    } else {
+      this.setState((state) => ({ selectedRuleIds: state.selectedRuleIds.remove(ruleId) }));
+    }
+  };
+
+  handleChangeCategory = category => {
+    this.setState({ category });
+  };
+
+  handleChangeComment = comment => {
+    this.setState({ comment });
+  };
+
+  handleNextStep = step => {
+    this.setState({ step });
+  };
+
+  componentDidMount () {
+    const { dispatch, accountId } = this.props;
+
+    dispatch(fetchRelationships([accountId]));
+    dispatch(expandAccountTimeline(accountId, { withReplies: true }));
+    dispatch(fetchServer());
+  }
+
+  render () {
+    const {
+      accountId,
+      account,
+      intl,
+      onClose,
+    } = this.props;
+
+    if (!account) {
+      return null;
+    }
+
+    const {
+      step,
+      selectedStatusIds,
+      selectedRuleIds,
+      selectedDomains,
+      comment,
+      category,
+      isSubmitting,
+      isSubmitted,
+    } = this.state;
+
+    const domain   = account.get('acct').split('@')[1];
+    const isRemote = !!domain;
+
+    let stepComponent;
+
+    switch(step) {
+    case 'category':
+      stepComponent = (
+        <Category
+          onNextStep={this.handleNextStep}
+          startedFrom={this.props.statusId ? 'status' : 'account'}
+          category={category}
+          onChangeCategory={this.handleChangeCategory}
+        />
+      );
+      break;
+    case 'rules':
+      stepComponent = (
+        <Rules
+          onNextStep={this.handleNextStep}
+          selectedRuleIds={selectedRuleIds}
+          onToggle={this.handleRuleToggle}
+        />
+      );
+      break;
+    case 'statuses':
+      stepComponent = (
+        <Statuses
+          onNextStep={this.handleNextStep}
+          accountId={accountId}
+          selectedStatusIds={selectedStatusIds}
+          onToggle={this.handleStatusToggle}
+        />
+      );
+      break;
+    case 'comment':
+      stepComponent = (
+        <Comment
+          onSubmit={this.handleSubmit}
+          isSubmitting={isSubmitting}
+          isRemote={isRemote}
+          comment={comment}
+          domain={domain}
+          onChangeComment={this.handleChangeComment}
+          statusIds={selectedStatusIds}
+          selectedDomains={selectedDomains}
+          onToggleDomain={this.handleDomainToggle}
+        />
+      );
+      break;
+    case 'thanks':
+      stepComponent = (
+        <Thanks
+          submitted={isSubmitted}
+          account={account}
+          onClose={onClose}
+        />
+      );
+    }
+
+    return (
+      <div className='modal-root__modal report-dialog-modal'>
+        <div className='report-modal__target'>
+          <IconButton className='report-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={20} />
+          <FormattedMessage id='report.target' defaultMessage='Report {target}' values={{ target: <strong>{account.get('acct')}</strong> }} />
+        </div>
+
+        <div className='report-dialog-modal__container'>
+          {stepComponent}
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default connect(makeMapStateToProps)(injectIntl(ReportModal));
diff --git a/app/javascript/flavours/blobfox/features/ui/components/sign_in_banner.jsx b/app/javascript/flavours/blobfox/features/ui/components/sign_in_banner.jsx
new file mode 100644
index 00000000000000..069e45a4f9d645
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/sign_in_banner.jsx
@@ -0,0 +1,54 @@
+import { useCallback } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+
+import { openModal } from 'flavours/blobfox/actions/modal';
+import { registrationsOpen, sso_redirect } from 'flavours/blobfox/initial_state';
+import { useAppDispatch, useAppSelector } from 'flavours/blobfox/store';
+
+const SignInBanner = () => {
+  const dispatch = useAppDispatch();
+
+  const openClosedRegistrationsModal = useCallback(
+    () => dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS' })),
+    [dispatch],
+  );
+
+  let signupButton;
+
+  const signupUrl = useAppSelector((state) => state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up');
+
+  if (sso_redirect) {
+    return (
+      <div className='sign-in-banner'>
+        <p><FormattedMessage id='sign_in_banner.text' defaultMessage='Login to follow profiles or hashtags, favorite, share and reply to posts. You can also interact from your account on a different server.' /></p>
+        <a href={sso_redirect} data-method='post' className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.sso_redirect' defaultMessage='Login or Register' /></a>
+      </div>
+    );
+  }
+
+  if (registrationsOpen) {
+    signupButton = (
+      <a href={signupUrl} className='button button--block'>
+        <FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
+      </a>
+    );
+  } else {
+    signupButton = (
+      <button className='button button--block' onClick={openClosedRegistrationsModal}>
+        <FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
+      </button>
+    );
+  }
+
+  return (
+    <div className='sign-in-banner'>
+      <p><FormattedMessage id='sign_in_banner.text' defaultMessage='Login to follow profiles or hashtags, favorite, share and reply to posts. You can also interact from your account on a different server.' /></p>
+      {signupButton}
+      <a href='/auth/sign_in' className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' /></a>
+    </div>
+  );
+};
+
+export default SignInBanner;
diff --git a/app/javascript/flavours/blobfox/features/ui/components/upload_area.jsx b/app/javascript/flavours/blobfox/features/ui/components/upload_area.jsx
new file mode 100644
index 00000000000000..b2702d35efdfd6
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/upload_area.jsx
@@ -0,0 +1,55 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import spring from 'react-motion/lib/spring';
+
+import Motion from '../util/optional_motion';
+
+export default class UploadArea extends PureComponent {
+
+  static propTypes = {
+    active: PropTypes.bool,
+    onClose: PropTypes.func,
+  };
+
+  handleKeyUp = (e) => {
+    const keyCode = e.keyCode;
+    if (this.props.active) {
+      switch(keyCode) {
+      case 27:
+        e.preventDefault();
+        e.stopPropagation();
+        this.props.onClose();
+        break;
+      }
+    }
+  };
+
+  componentDidMount () {
+    window.addEventListener('keyup', this.handleKeyUp, false);
+  }
+
+  componentWillUnmount () {
+    window.removeEventListener('keyup', this.handleKeyUp);
+  }
+
+  render () {
+    const { active } = this.props;
+
+    return (
+      <Motion defaultStyle={{ backgroundOpacity: 0, backgroundScale: 0.95 }} style={{ backgroundOpacity: spring(active ? 1 : 0, { stiffness: 150, damping: 15 }), backgroundScale: spring(active ? 1 : 0.95, { stiffness: 200, damping: 3 }) }}>
+        {({ backgroundOpacity, backgroundScale }) => (
+          <div className='upload-area' style={{ visibility: active ? 'visible' : 'hidden', opacity: backgroundOpacity }}>
+            <div className='upload-area__drop'>
+              <div className='upload-area__background' style={{ transform: `scale(${backgroundScale})` }} />
+              <div className='upload-area__content'><FormattedMessage id='upload_area.title' defaultMessage='Drag & drop to upload' /></div>
+            </div>
+          </div>
+        )}
+      </Motion>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/ui/components/video_modal.jsx b/app/javascript/flavours/blobfox/features/ui/components/video_modal.jsx
new file mode 100644
index 00000000000000..7bed641cd968c1
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/video_modal.jsx
@@ -0,0 +1,74 @@
+import PropTypes from 'prop-types';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
+import { getAverageFromBlurhash } from 'flavours/blobfox/blurhash';
+import Footer from 'flavours/blobfox/features/picture_in_picture/components/footer';
+import Video from 'flavours/blobfox/features/video';
+
+const mapStateToProps = (state, { statusId }) => ({
+  status: state.getIn(['statuses', statusId]),
+});
+
+class VideoModal extends ImmutablePureComponent {
+
+  static propTypes = {
+    media: ImmutablePropTypes.map.isRequired,
+    statusId: PropTypes.string,
+    status: ImmutablePropTypes.map,
+    options: PropTypes.shape({
+      startTime: PropTypes.number,
+      autoPlay: PropTypes.bool,
+      defaultVolume: PropTypes.number,
+    }),
+    onClose: PropTypes.func.isRequired,
+    onChangeBackgroundColor: PropTypes.func.isRequired,
+  };
+
+  componentDidMount () {
+    const { media, onChangeBackgroundColor } = this.props;
+
+    const backgroundColor = getAverageFromBlurhash(media.get('blurhash'));
+
+    if (backgroundColor) {
+      onChangeBackgroundColor(backgroundColor);
+    }
+  }
+
+  render () {
+    const { media, status, onClose } = this.props;
+    const options = this.props.options || {};
+    const language = status.getIn(['translation', 'language']) || status.get('language');
+    const description = media.getIn(['translation', 'description']) || media.get('description');
+
+    return (
+      <div className='modal-root__modal video-modal'>
+        <div className='video-modal__container'>
+          <Video
+            preview={media.get('preview_url')}
+            frameRate={media.getIn(['meta', 'original', 'frame_rate'])}
+            blurhash={media.get('blurhash')}
+            src={media.get('url')}
+            currentTime={options.startTime}
+            autoPlay={options.autoPlay}
+            volume={options.defaultVolume}
+            onCloseVideo={onClose}
+            autoFocus
+            detailed
+            alt={description}
+            lang={language}
+          />
+        </div>
+
+        <div className='media-modal__overlay'>
+          {status && <Footer statusId={status.get('id')} withOpenButton onClose={onClose} />}
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps, null, null, { forwardRef: true })(VideoModal);
diff --git a/app/javascript/flavours/blobfox/features/ui/components/zoomable_image.jsx b/app/javascript/flavours/blobfox/features/ui/components/zoomable_image.jsx
new file mode 100644
index 00000000000000..7ec3a3d6cd7f5a
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/components/zoomable_image.jsx
@@ -0,0 +1,456 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, injectIntl } from 'react-intl';
+
+import { IconButton } from 'flavours/blobfox/components/icon_button';
+
+const messages = defineMessages({
+  compress: { id: 'lightbox.compress', defaultMessage: 'Compress image view box' },
+  expand: { id: 'lightbox.expand', defaultMessage: 'Expand image view box' },
+});
+
+const MIN_SCALE = 1;
+const MAX_SCALE = 4;
+const NAV_BAR_HEIGHT = 66;
+
+const getMidpoint = (p1, p2) => ({
+  x: (p1.clientX + p2.clientX) / 2,
+  y: (p1.clientY + p2.clientY) / 2,
+});
+
+const getDistance = (p1, p2) =>
+  Math.sqrt(Math.pow(p1.clientX - p2.clientX, 2) + Math.pow(p1.clientY - p2.clientY, 2));
+
+const clamp = (min, max, value) => Math.min(max, Math.max(min, value));
+
+// Normalizing mousewheel speed across browsers
+// copy from: https://github.com/facebookarchive/fixed-data-table/blob/master/src/vendor_upstream/dom/normalizeWheel.js
+const normalizeWheel = event => {
+  // Reasonable defaults
+  const PIXEL_STEP = 10;
+  const LINE_HEIGHT = 40;
+  const PAGE_HEIGHT = 800;
+
+  let sX = 0,
+    sY = 0, // spinX, spinY
+    pX = 0,
+    pY = 0; // pixelX, pixelY
+
+  // Legacy
+  if ('detail' in event) {
+    sY = event.detail;
+  }
+  if ('wheelDelta' in event) {
+    sY = -event.wheelDelta / 120;
+  }
+  if ('wheelDeltaY' in event) {
+    sY = -event.wheelDeltaY / 120;
+  }
+  if ('wheelDeltaX' in event) {
+    sX = -event.wheelDeltaX / 120;
+  }
+
+  // side scrolling on FF with DOMMouseScroll
+  if ('axis' in event && event.axis === event.HORIZONTAL_AXIS) {
+    sX = sY;
+    sY = 0;
+  }
+
+  pX = sX * PIXEL_STEP;
+  pY = sY * PIXEL_STEP;
+
+  if ('deltaY' in event) {
+    pY = event.deltaY;
+  }
+  if ('deltaX' in event) {
+    pX = event.deltaX;
+  }
+
+  if ((pX || pY) && event.deltaMode) {
+    if (event.deltaMode === 1) { // delta in LINE units
+      pX *= LINE_HEIGHT;
+      pY *= LINE_HEIGHT;
+    } else { // delta in PAGE units
+      pX *= PAGE_HEIGHT;
+      pY *= PAGE_HEIGHT;
+    }
+  }
+
+  // Fall-back if spin cannot be determined
+  if (pX && !sX) {
+    sX = (pX < 1) ? -1 : 1;
+  }
+  if (pY && !sY) {
+    sY = (pY < 1) ? -1 : 1;
+  }
+
+  return {
+    spinX: sX,
+    spinY: sY,
+    pixelX: pX,
+    pixelY: pY,
+  };
+};
+
+class ZoomableImage extends PureComponent {
+
+  static propTypes = {
+    alt: PropTypes.string,
+    lang: PropTypes.string,
+    src: PropTypes.string.isRequired,
+    width: PropTypes.number,
+    height: PropTypes.number,
+    onClick: PropTypes.func,
+    zoomButtonHidden: PropTypes.bool,
+    intl: PropTypes.object.isRequired,
+  };
+
+  static defaultProps = {
+    alt: '',
+    lang: '',
+    width: null,
+    height: null,
+  };
+
+  state = {
+    scale: MIN_SCALE,
+    zoomMatrix: {
+      type: null, // 'width' 'height'
+      fullScreen: null, // bool
+      rate: null, // full screen scale rate
+      clientWidth: null,
+      clientHeight: null,
+      offsetWidth: null,
+      offsetHeight: null,
+      clientHeightFixed: null,
+      scrollTop: null,
+      scrollLeft: null,
+      translateX: null,
+      translateY: null,
+    },
+    zoomState: 'expand', // 'expand' 'compress'
+    navigationHidden: false,
+    dragPosition: { top: 0, left: 0, x: 0, y: 0 },
+    dragged: false,
+    lockScroll: { x: 0, y: 0 },
+    lockTranslate: { x: 0, y: 0 },
+  };
+
+  removers = [];
+  container = null;
+  image = null;
+  lastTouchEndTime = 0;
+  lastDistance = 0;
+
+  componentDidMount () {
+    let handler = this.handleTouchStart;
+    this.container.addEventListener('touchstart', handler);
+    this.removers.push(() => this.container.removeEventListener('touchstart', handler));
+    handler = this.handleTouchMove;
+    // on Chrome 56+, touch event listeners will default to passive
+    // https://www.chromestatus.com/features/5093566007214080
+    this.container.addEventListener('touchmove', handler, { passive: false });
+    this.removers.push(() => this.container.removeEventListener('touchend', handler));
+
+    handler = this.mouseDownHandler;
+    this.container.addEventListener('mousedown', handler);
+    this.removers.push(() => this.container.removeEventListener('mousedown', handler));
+
+    handler = this.mouseWheelHandler;
+    this.container.addEventListener('wheel', handler);
+    this.removers.push(() => this.container.removeEventListener('wheel', handler));
+    // Old Chrome
+    this.container.addEventListener('mousewheel', handler);
+    this.removers.push(() => this.container.removeEventListener('mousewheel', handler));
+    // Old Firefox
+    this.container.addEventListener('DOMMouseScroll', handler);
+    this.removers.push(() => this.container.removeEventListener('DOMMouseScroll', handler));
+
+    this.initZoomMatrix();
+  }
+
+  componentWillUnmount () {
+    this.removeEventListeners();
+  }
+
+  componentDidUpdate () {
+    this.setState({ zoomState: this.state.scale >= this.state.zoomMatrix.rate ? 'compress' : 'expand' });
+
+    if (this.state.scale === MIN_SCALE) {
+      this.container.style.removeProperty('cursor');
+    }
+  }
+
+  UNSAFE_componentWillReceiveProps () {
+    // reset when slide to next image
+    if (this.props.zoomButtonHidden) {
+      this.setState({
+        scale: MIN_SCALE,
+        lockTranslate: { x: 0, y: 0 },
+      }, () => {
+        this.container.scrollLeft = 0;
+        this.container.scrollTop = 0;
+      });
+    }
+  }
+
+  removeEventListeners () {
+    this.removers.forEach(listeners => listeners());
+    this.removers = [];
+  }
+
+  mouseWheelHandler = e => {
+    e.preventDefault();
+
+    const event = normalizeWheel(e);
+
+    if (this.state.zoomMatrix.type === 'width') {
+      // full width, scroll vertical
+      this.container.scrollTop = Math.max(this.container.scrollTop + event.pixelY, this.state.lockScroll.y);
+    } else {
+      // full height, scroll horizontal
+      this.container.scrollLeft = Math.max(this.container.scrollLeft + event.pixelY, this.state.lockScroll.x);
+    }
+
+    // lock horizontal scroll
+    this.container.scrollLeft = Math.max(this.container.scrollLeft + event.pixelX, this.state.lockScroll.x);
+  };
+
+  mouseDownHandler = e => {
+    this.container.style.cursor = 'grabbing';
+    this.container.style.userSelect = 'none';
+
+    this.setState({ dragPosition: {
+      left: this.container.scrollLeft,
+      top: this.container.scrollTop,
+      // Get the current mouse position
+      x: e.clientX,
+      y: e.clientY,
+    } });
+
+    this.image.addEventListener('mousemove', this.mouseMoveHandler);
+    this.image.addEventListener('mouseup', this.mouseUpHandler);
+  };
+
+  mouseMoveHandler = e => {
+    const dx = e.clientX - this.state.dragPosition.x;
+    const dy = e.clientY - this.state.dragPosition.y;
+
+    this.container.scrollLeft = Math.max(this.state.dragPosition.left - dx, this.state.lockScroll.x);
+    this.container.scrollTop = Math.max(this.state.dragPosition.top - dy, this.state.lockScroll.y);
+
+    this.setState({ dragged: true });
+  };
+
+  mouseUpHandler = () => {
+    this.container.style.cursor = 'grab';
+    this.container.style.removeProperty('user-select');
+
+    this.image.removeEventListener('mousemove', this.mouseMoveHandler);
+    this.image.removeEventListener('mouseup', this.mouseUpHandler);
+  };
+
+  handleTouchStart = e => {
+    if (e.touches.length !== 2) return;
+
+    this.lastDistance = getDistance(...e.touches);
+  };
+
+  handleTouchMove = e => {
+    const { scrollTop, scrollHeight, clientHeight } = this.container;
+    if (e.touches.length === 1 && scrollTop !== scrollHeight - clientHeight) {
+      // prevent propagating event to MediaModal
+      e.stopPropagation();
+      return;
+    }
+    if (e.touches.length !== 2) return;
+
+    e.preventDefault();
+    e.stopPropagation();
+
+    const distance = getDistance(...e.touches);
+    const midpoint = getMidpoint(...e.touches);
+    const _MAX_SCALE = Math.max(MAX_SCALE, this.state.zoomMatrix.rate);
+    const scale = clamp(MIN_SCALE, _MAX_SCALE, this.state.scale * distance / this.lastDistance);
+
+    this.zoom(scale, midpoint);
+
+    this.lastMidpoint = midpoint;
+    this.lastDistance = distance;
+  };
+
+  zoom(nextScale, midpoint) {
+    const { scale, zoomMatrix } = this.state;
+    const { scrollLeft, scrollTop } = this.container;
+
+    // math memo:
+    // x = (scrollLeft + midpoint.x) / scrollWidth
+    // x' = (nextScrollLeft + midpoint.x) / nextScrollWidth
+    // scrollWidth = clientWidth * scale
+    // scrollWidth' = clientWidth * nextScale
+    // Solve x = x' for nextScrollLeft
+    const nextScrollLeft = (scrollLeft + midpoint.x) * nextScale / scale - midpoint.x;
+    const nextScrollTop = (scrollTop + midpoint.y) * nextScale / scale - midpoint.y;
+
+    this.setState({ scale: nextScale }, () => {
+      this.container.scrollLeft = nextScrollLeft;
+      this.container.scrollTop = nextScrollTop;
+      // reset the translateX/Y constantly
+      if (nextScale < zoomMatrix.rate) {
+        this.setState({
+          lockTranslate: {
+            x: zoomMatrix.fullScreen ? 0 : zoomMatrix.translateX * ((nextScale - MIN_SCALE) / (zoomMatrix.rate - MIN_SCALE)),
+            y: zoomMatrix.fullScreen ? 0 : zoomMatrix.translateY * ((nextScale - MIN_SCALE) / (zoomMatrix.rate - MIN_SCALE)),
+          },
+        });
+      }
+    });
+  }
+
+  handleClick = e => {
+    // don't propagate event to MediaModal
+    e.stopPropagation();
+    const dragged = this.state.dragged;
+    this.setState({ dragged: false });
+    if (dragged) return;
+    const handler = this.props.onClick;
+    if (handler) handler();
+    this.setState({ navigationHidden: !this.state.navigationHidden });
+  };
+
+  handleMouseDown = e => {
+    e.preventDefault();
+  };
+
+  initZoomMatrix = () => {
+    const { width, height } = this.props;
+    const { clientWidth, clientHeight } = this.container;
+    const { offsetWidth, offsetHeight } = this.image;
+    const clientHeightFixed = clientHeight - NAV_BAR_HEIGHT;
+
+    const type = width / height < clientWidth / clientHeightFixed ? 'width' : 'height';
+    const fullScreen = type === 'width' ?  width > clientWidth : height > clientHeightFixed;
+    const rate = type === 'width' ? Math.min(clientWidth, width) / offsetWidth : Math.min(clientHeightFixed, height) / offsetHeight;
+    const scrollTop = type === 'width' ?  (clientHeight - offsetHeight) / 2 - NAV_BAR_HEIGHT : (clientHeightFixed - offsetHeight) / 2;
+    const scrollLeft = (clientWidth - offsetWidth) / 2;
+    const translateX = type === 'width' ? (width - offsetWidth) / (2 * rate) : 0;
+    const translateY = type === 'height' ? (height - offsetHeight) / (2 * rate) : 0;
+
+    this.setState({
+      zoomMatrix: {
+        type: type,
+        fullScreen: fullScreen,
+        rate: rate,
+        clientWidth: clientWidth,
+        clientHeight: clientHeight,
+        offsetWidth: offsetWidth,
+        offsetHeight: offsetHeight,
+        clientHeightFixed: clientHeightFixed,
+        scrollTop: scrollTop,
+        scrollLeft: scrollLeft,
+        translateX: translateX,
+        translateY: translateY,
+      },
+    });
+  };
+
+  handleZoomClick = e => {
+    e.preventDefault();
+    e.stopPropagation();
+
+    const { scale, zoomMatrix } = this.state;
+
+    if ( scale >= zoomMatrix.rate ) {
+      this.setState({
+        scale: MIN_SCALE,
+        lockScroll: {
+          x: 0,
+          y: 0,
+        },
+        lockTranslate: {
+          x: 0,
+          y: 0,
+        },
+      }, () => {
+        this.container.scrollLeft = 0;
+        this.container.scrollTop = 0;
+      });
+    } else {
+      this.setState({
+        scale: zoomMatrix.rate,
+        lockScroll: {
+          x: zoomMatrix.scrollLeft,
+          y: zoomMatrix.scrollTop,
+        },
+        lockTranslate: {
+          x: zoomMatrix.fullScreen ? 0 : zoomMatrix.translateX,
+          y: zoomMatrix.fullScreen ? 0 : zoomMatrix.translateY,
+        },
+      }, () => {
+        this.container.scrollLeft = zoomMatrix.scrollLeft;
+        this.container.scrollTop = zoomMatrix.scrollTop;
+      });
+    }
+
+    this.container.style.cursor = 'grab';
+    this.container.style.removeProperty('user-select');
+  };
+
+  setContainerRef = c => {
+    this.container = c;
+  };
+
+  setImageRef = c => {
+    this.image = c;
+  };
+
+  render () {
+    const { alt, lang, src, width, height, intl } = this.props;
+    const { scale, lockTranslate } = this.state;
+    const overflow = scale === MIN_SCALE ? 'hidden' : 'scroll';
+    const zoomButtonShouldHide = this.state.navigationHidden || this.props.zoomButtonHidden || this.state.zoomMatrix.rate <= MIN_SCALE ? 'media-modal__zoom-button--hidden' : '';
+    const zoomButtonTitle = this.state.zoomState === 'compress' ? intl.formatMessage(messages.compress) : intl.formatMessage(messages.expand);
+
+    return (
+      <>
+        <IconButton
+          className={`media-modal__zoom-button ${zoomButtonShouldHide}`}
+          title={zoomButtonTitle}
+          icon={this.state.zoomState}
+          onClick={this.handleZoomClick}
+          size={40}
+          style={{
+            fontSize: '30px', /* Fontawesome's fa-compress fa-expand is larger than fa-close */
+          }}
+        />
+        <div
+          className='zoomable-image'
+          ref={this.setContainerRef}
+          style={{ overflow }}
+        >
+          <img
+            role='presentation'
+            ref={this.setImageRef}
+            alt={alt}
+            title={alt}
+            lang={lang}
+            src={src}
+            width={width}
+            height={height}
+            style={{
+              transform: `scale(${scale}) translate(-${lockTranslate.x}px, -${lockTranslate.y}px)`,
+              transformOrigin: '0 0',
+            }}
+            draggable={false}
+            onClick={this.handleClick}
+            onMouseDown={this.handleMouseDown}
+          />
+        </div>
+      </>
+    );
+  }
+
+}
+
+export default injectIntl(ZoomableImage);
diff --git a/app/javascript/flavours/blobfox/features/ui/containers/bundle_container.js b/app/javascript/flavours/blobfox/features/ui/containers/bundle_container.js
new file mode 100644
index 00000000000000..6a476fe2483cf3
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/containers/bundle_container.js
@@ -0,0 +1,18 @@
+import { connect } from 'react-redux';
+
+import { fetchBundleRequest, fetchBundleSuccess, fetchBundleFail } from '../../../actions/bundles';
+import Bundle from '../components/bundle';
+
+const mapDispatchToProps = dispatch => ({
+  onFetch () {
+    dispatch(fetchBundleRequest());
+  },
+  onFetchSuccess () {
+    dispatch(fetchBundleSuccess());
+  },
+  onFetchFail (error) {
+    dispatch(fetchBundleFail(error));
+  },
+});
+
+export default connect(null, mapDispatchToProps)(Bundle);
diff --git a/app/javascript/flavours/blobfox/features/ui/containers/columns_area_container.js b/app/javascript/flavours/blobfox/features/ui/containers/columns_area_container.js
new file mode 100644
index 00000000000000..76c9a3e28cc13b
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/containers/columns_area_container.js
@@ -0,0 +1,22 @@
+import { connect } from 'react-redux';
+
+import { openModal } from 'flavours/blobfox/actions/modal';
+
+import ColumnsArea from '../components/columns_area';
+
+const mapStateToProps = state => ({
+  columns: state.getIn(['settings', 'columns']),
+});
+
+const mapDispatchToProps = dispatch => ({
+  openSettings (e) {
+    e.preventDefault();
+    e.stopPropagation();
+    dispatch(openModal({
+      modalType: 'SETTINGS',
+      modalProps: {},
+    }));
+  },
+});
+
+export default connect(mapStateToProps, mapDispatchToProps, null, { forwardRef: true })(ColumnsArea);
diff --git a/app/javascript/flavours/blobfox/features/ui/containers/loading_bar_container.js b/app/javascript/flavours/blobfox/features/ui/containers/loading_bar_container.js
new file mode 100644
index 00000000000000..7efdac55ee393f
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/containers/loading_bar_container.js
@@ -0,0 +1,9 @@
+import { connect }    from 'react-redux';
+
+import LoadingBar from 'react-redux-loading-bar';
+
+const mapStateToProps = (state, ownProps) => ({
+  loading: state.get('loadingBar')[ownProps.scope || 'default'],
+});
+
+export default connect(mapStateToProps)(LoadingBar.WrappedComponent);
diff --git a/app/javascript/flavours/blobfox/features/ui/containers/modal_container.js b/app/javascript/flavours/blobfox/features/ui/containers/modal_container.js
new file mode 100644
index 00000000000000..1c3872cd50436f
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/containers/modal_container.js
@@ -0,0 +1,36 @@
+import { connect } from 'react-redux';
+
+import { openModal, closeModal } from '../../../actions/modal';
+import ModalRoot from '../components/modal_root';
+
+const mapStateToProps = state => ({
+  ignoreFocus: state.getIn(['modal', 'ignoreFocus']),
+  type: state.getIn(['modal', 'stack', 0, 'modalType'], null),
+  props: state.getIn(['modal', 'stack', 0, 'modalProps'], {}),
+});
+
+const mapDispatchToProps = dispatch => ({
+  onClose (confirmationMessage, ignoreFocus = false) {
+    if (confirmationMessage) {
+      dispatch(
+        openModal({
+          modalType: 'CONFIRM',
+          modalProps: {
+            message: confirmationMessage.message,
+            confirm: confirmationMessage.confirm,
+            onConfirm: () => dispatch(closeModal({
+              modalType: undefined,
+              ignoreFocus: { ignoreFocus },
+            })),
+          } }),
+      );
+    } else {
+      dispatch(closeModal({
+        modalType: undefined,
+        ignoreFocus: { ignoreFocus },
+      }));
+    }
+  },
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(ModalRoot);
diff --git a/app/javascript/flavours/blobfox/features/ui/containers/notifications_container.js b/app/javascript/flavours/blobfox/features/ui/containers/notifications_container.js
new file mode 100644
index 00000000000000..3d60cfdad1b24a
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/containers/notifications_container.js
@@ -0,0 +1,33 @@
+import { injectIntl } from 'react-intl';
+
+import { connect } from 'react-redux';
+
+import { NotificationStack } from 'react-notification';
+
+import { dismissAlert } from '../../../actions/alerts';
+import { getAlerts } from '../../../selectors';
+
+const formatIfNeeded = (intl, message, values) => {
+  if (typeof message === 'object') {
+    return intl.formatMessage(message, values);
+  }
+
+  return message;
+};
+
+const mapStateToProps = (state, { intl }) => ({
+  notifications: getAlerts(state).map(alert => ({
+    ...alert,
+    action: formatIfNeeded(intl, alert.action, alert.values),
+    title: formatIfNeeded(intl, alert.title, alert.values),
+    message: formatIfNeeded(intl, alert.message, alert.values),
+  })),
+});
+
+const mapDispatchToProps = (dispatch) => ({
+  onDismiss (alert) {
+    dispatch(dismissAlert(alert));
+  },
+});
+
+export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NotificationStack));
diff --git a/app/javascript/flavours/blobfox/features/ui/containers/status_list_container.js b/app/javascript/flavours/blobfox/features/ui/containers/status_list_container.js
new file mode 100644
index 00000000000000..f34d099b24015f
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/containers/status_list_container.js
@@ -0,0 +1,89 @@
+import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+
+import { debounce } from 'lodash';
+
+import { scrollTopTimeline, loadPending } from '../../../actions/timelines';
+import StatusList from '../../../components/status_list';
+import { me } from '../../../initial_state';
+
+const getRegex = createSelector([
+  (state, { regex }) => regex,
+], (rawRegex) => {
+  let regex = null;
+
+  try {
+    regex = rawRegex && new RegExp(rawRegex.trim(), 'i');
+  } catch (e) {
+    // Bad regex, don't affect filters
+  }
+  return regex;
+});
+
+const makeGetStatusIds = (pending = false) => createSelector([
+  (state, { type }) => state.getIn(['settings', type], ImmutableMap()),
+  (state, { type }) => state.getIn(['timelines', type, pending ? 'pendingItems' : 'items'], ImmutableList()),
+  (state)           => state.get('statuses'),
+  getRegex,
+], (columnSettings, statusIds, statuses, regex) => {
+  return statusIds.filter(id => {
+    if (id === null) return true;
+
+    const statusForId = statuses.get(id);
+    let showStatus    = true;
+
+    if (statusForId.get('account') === me) return true;
+
+    if (columnSettings.getIn(['shows', 'reblog']) === false) {
+      showStatus = showStatus && statusForId.get('reblog') === null;
+    }
+
+    if (columnSettings.getIn(['shows', 'reply']) === false) {
+      showStatus = showStatus && (statusForId.get('in_reply_to_id') === null || statusForId.get('in_reply_to_account_id') === me);
+    }
+
+    if (columnSettings.getIn(['shows', 'direct']) === false) {
+      showStatus = showStatus && statusForId.get('visibility') !== 'direct';
+    }
+
+    if (showStatus && regex) {
+      const searchIndex = statusForId.get('reblog') ? statuses.getIn([statusForId.get('reblog'), 'search_index']) : statusForId.get('search_index');
+      showStatus = !regex.test(searchIndex);
+    }
+
+    return showStatus;
+  });
+});
+
+const makeMapStateToProps = () => {
+  const getStatusIds = makeGetStatusIds();
+  const getPendingStatusIds = makeGetStatusIds(true);
+
+  const mapStateToProps = (state, { timelineId, regex }) => ({
+    statusIds: getStatusIds(state, { type: timelineId, regex }),
+    lastId:    state.getIn(['timelines', timelineId, 'items'])?.last(),
+    isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true),
+    isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false),
+    hasMore:   state.getIn(['timelines', timelineId, 'hasMore']),
+    numPending: getPendingStatusIds(state, { type: timelineId }).size,
+  });
+
+  return mapStateToProps;
+};
+
+const mapDispatchToProps = (dispatch, { timelineId }) => ({
+
+  onScrollToTop: debounce(() => {
+    dispatch(scrollTopTimeline(timelineId, true));
+  }, 100),
+
+  onScroll: debounce(() => {
+    dispatch(scrollTopTimeline(timelineId, false));
+  }, 100),
+
+  onLoadPending: () => dispatch(loadPending(timelineId)),
+
+});
+
+export default connect(makeMapStateToProps, mapDispatchToProps)(StatusList);
diff --git a/app/javascript/flavours/blobfox/features/ui/index.jsx b/app/javascript/flavours/blobfox/features/ui/index.jsx
new file mode 100644
index 00000000000000..dc27f86d4251e8
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/index.jsx
@@ -0,0 +1,675 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
+
+import classNames from 'classnames';
+import { Redirect, Route, withRouter } from 'react-router-dom';
+
+import { connect } from 'react-redux';
+
+import Favico from 'favico.js';
+import { debounce } from 'lodash';
+import { HotKeys } from 'react-hotkeys';
+
+import { changeLayout } from 'flavours/blobfox/actions/app';
+import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'flavours/blobfox/actions/markers';
+import { INTRODUCTION_VERSION } from 'flavours/blobfox/actions/onboarding';
+import PermaLink from 'flavours/blobfox/components/permalink';
+import PictureInPicture from 'flavours/blobfox/features/picture_in_picture';
+import { layoutFromWindow } from 'flavours/blobfox/is_mobile';
+import { WithRouterPropTypes } from 'flavours/blobfox/utils/react_router';
+
+import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
+import { clearHeight } from '../../actions/height_cache';
+import { expandNotifications, notificationsSetVisibility } from '../../actions/notifications';
+import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server';
+import { expandHomeTimeline } from '../../actions/timelines';
+import initialState, { me, owner, singleUserMode, trendsEnabled, trendsAsLanding } from '../../initial_state';
+
+import BundleColumnError from './components/bundle_column_error';
+import Header from './components/header';
+import UploadArea from './components/upload_area';
+import ColumnsAreaContainer from './containers/columns_area_container';
+import LoadingBarContainer from './containers/loading_bar_container';
+import ModalContainer from './containers/modal_container';
+import NotificationsContainer from './containers/notifications_container';
+import {
+  Compose,
+  Status,
+  GettingStarted,
+  KeyboardShortcuts,
+  Firehose,
+  AccountTimeline,
+  AccountGallery,
+  HomeTimeline,
+  Followers,
+  Following,
+  Reblogs,
+  Favourites,
+  DirectTimeline,
+  HashtagTimeline,
+  Notifications,
+  FollowRequests,
+  FavouritedStatuses,
+  BookmarkedStatuses,
+  FollowedTags,
+  ListTimeline,
+  Blocks,
+  DomainBlocks,
+  Mutes,
+  PinnedStatuses,
+  Lists,
+  GettingStartedMisc,
+  Directory,
+  Explore,
+  Onboarding,
+  About,
+  PrivacyPolicy,
+} from './util/async-components';
+import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
+
+// Dummy import, to make sure that <Status /> ends up in the application bundle.
+// Without this it ends up in ~8 very commonly used bundles.
+import '../../components/status';
+
+const messages = defineMessages({
+  beforeUnload: { id: 'ui.beforeunload', defaultMessage: 'Your draft will be lost if you leave Mastodon.' },
+});
+
+const mapStateToProps = state => ({
+  layout: state.getIn(['meta', 'layout']),
+  hasComposingText: state.getIn(['compose', 'text']).trim().length !== 0,
+  hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0,
+  canUploadMore: !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) && state.getIn(['compose', 'media_attachments']).size < 4,
+  isWide: state.getIn(['local_settings', 'stretch']),
+  dropdownMenuIsOpen: state.dropdownMenu.openId !== null,
+  unreadNotifications: state.getIn(['notifications', 'unread']),
+  showFaviconBadge: state.getIn(['local_settings', 'notifications', 'favicon_badge']),
+  hicolorPrivacyIcons: state.getIn(['local_settings', 'hicolor_privacy_icons']),
+  moved: state.getIn(['accounts', me, 'moved']) && state.getIn(['accounts', state.getIn(['accounts', me, 'moved'])]),
+  firstLaunch: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION,
+  username: state.getIn(['accounts', me, 'username']),
+});
+
+const keyMap = {
+  help: '?',
+  new: 'n',
+  search: 's',
+  forceNew: 'option+n',
+  toggleComposeSpoilers: 'option+x',
+  focusColumn: ['1', '2', '3', '4', '5', '6', '7', '8', '9'],
+  reply: 'r',
+  favourite: 'f',
+  boost: 'b',
+  mention: 'm',
+  open: ['enter', 'o'],
+  openProfile: 'p',
+  moveDown: ['down', 'j'],
+  moveUp: ['up', 'k'],
+  back: 'backspace',
+  goToHome: 'g h',
+  goToNotifications: 'g n',
+  goToLocal: 'g l',
+  goToFederated: 'g t',
+  goToDirect: 'g d',
+  goToStart: 'g s',
+  goToFavourites: 'g f',
+  goToPinned: 'g p',
+  goToProfile: 'g u',
+  goToBlocked: 'g b',
+  goToMuted: 'g m',
+  goToRequests: 'g r',
+  toggleHidden: 'x',
+  bookmark: 'd',
+  toggleCollapse: 'shift+x',
+  toggleSensitive: 'h',
+  openMedia: 'e',
+};
+
+class SwitchingColumnsArea extends PureComponent {
+
+  static contextTypes = {
+    identity: PropTypes.object,
+  };
+
+  static propTypes = {
+    children: PropTypes.node,
+    location: PropTypes.object,
+    singleColumn: PropTypes.bool,
+  };
+
+  UNSAFE_componentWillMount () {
+    if (this.props.singleColumn) {
+      document.body.classList.toggle('layout-single-column', true);
+      document.body.classList.toggle('layout-multiple-columns', false);
+    } else {
+      document.body.classList.toggle('layout-single-column', false);
+      document.body.classList.toggle('layout-multiple-columns', true);
+    }
+  }
+
+  componentDidUpdate (prevProps) {
+    if (![this.props.location.pathname, '/'].includes(prevProps.location.pathname)) {
+      this.node.handleChildrenContentChange();
+    }
+
+    if (prevProps.singleColumn !== this.props.singleColumn) {
+      document.body.classList.toggle('layout-single-column', this.props.singleColumn);
+      document.body.classList.toggle('layout-multiple-columns', !this.props.singleColumn);
+    }
+  }
+
+  setRef = c => {
+    if (c) {
+      this.node = c;
+    }
+  };
+
+  render () {
+    const { children, singleColumn } = this.props;
+    const { signedIn } = this.context.identity;
+    const pathName = this.props.location.pathname;
+
+    let redirect;
+
+    if (signedIn) {
+      if (singleColumn) {
+        redirect = <Redirect from='/' to='/home' exact />;
+      } else {
+        redirect = <Redirect from='/' to='/deck/getting-started' exact />;
+      }
+    } else if (singleUserMode && owner && initialState?.accounts[owner]) {
+      redirect = <Redirect from='/' to={`/@${initialState.accounts[owner].username}`} exact />;
+    } else if (trendsEnabled && trendsAsLanding) {
+      redirect = <Redirect from='/' to='/explore' exact />;
+    } else {
+      redirect = <Redirect from='/' to='/about' exact />;
+    }
+
+    return (
+      <ColumnsAreaContainer ref={this.setRef} singleColumn={singleColumn}>
+        <WrappedSwitch>
+          {redirect}
+
+          {singleColumn ? <Redirect from='/deck' to='/home' exact /> : null}
+          {singleColumn && pathName.startsWith('/deck/') ? <Redirect from={pathName} to={pathName.slice(5)} /> : null}
+          {/* Redirect old bookmarks (without /deck) with home-like routes to the advanced interface */}
+          {!singleColumn && pathName === '/getting-started' ? <Redirect from='/getting-started' to='/deck/getting-started' exact /> : null}
+          {!singleColumn && pathName === '/home' ? <Redirect from='/home' to='/deck/getting-started' exact /> : null}
+
+          <WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
+          <WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
+          <WrappedRoute path='/about' component={About} content={children} />
+          <WrappedRoute path='/privacy-policy' component={PrivacyPolicy} content={children} />
+
+          <WrappedRoute path={['/home', '/timelines/home']} component={HomeTimeline} content={children} />
+          <Redirect from='/timelines/public' to='/public' exact />
+          <Redirect from='/timelines/public/local' to='/public/local' exact />
+          <WrappedRoute path='/public' exact component={Firehose} componentParams={{ feedType: 'public' }} content={children} />
+          <WrappedRoute path='/public/local' exact component={Firehose} componentParams={{ feedType: 'community' }} content={children} />
+          <WrappedRoute path='/public/remote' exact component={Firehose} componentParams={{ feedType: 'public:remote' }} content={children} />
+          <WrappedRoute path={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} />
+          <WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
+          <WrappedRoute path='/lists/:id' component={ListTimeline} content={children} />
+          <WrappedRoute path='/notifications' component={Notifications} content={children} />
+          <WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} />
+
+          <WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} />
+          <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
+
+          <WrappedRoute path='/start' exact component={Onboarding} content={children} />
+          <WrappedRoute path='/directory' component={Directory} content={children} />
+          <WrappedRoute path={['/explore', '/search']} component={Explore} content={children} />
+          <WrappedRoute path={['/publish', '/statuses/new']} component={Compose} content={children} />
+
+          <WrappedRoute path={['/@:acct', '/accounts/:id']} exact component={AccountTimeline} content={children} />
+          <WrappedRoute path='/@:acct/tagged/:tagged?' exact component={AccountTimeline} content={children} />
+          <WrappedRoute path={['/@:acct/with_replies', '/accounts/:id/with_replies']} component={AccountTimeline} content={children} componentParams={{ withReplies: true }} />
+          <WrappedRoute path={['/accounts/:id/followers', '/users/:acct/followers', '/@:acct/followers']} component={Followers} content={children} />
+          <WrappedRoute path={['/accounts/:id/following', '/users/:acct/following', '/@:acct/following']} component={Following} content={children} />
+          <WrappedRoute path={['/@:acct/media', '/accounts/:id/media']} component={AccountGallery} content={children} />
+          <WrappedRoute path='/@:acct/:statusId' exact component={Status} content={children} />
+          <WrappedRoute path='/@:acct/:statusId/reblogs' component={Reblogs} content={children} />
+          <WrappedRoute path='/@:acct/:statusId/favourites' component={Favourites} content={children} />
+
+          {/* Legacy routes, cannot be easily factored with other routes because they share a param name */}
+          <WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} />
+          <WrappedRoute path='/timelines/list/:id' component={ListTimeline} content={children} />
+          <WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} />
+          <WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} />
+          <WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} />
+
+          <WrappedRoute path='/follow_requests' component={FollowRequests} content={children} />
+          <WrappedRoute path='/blocks' component={Blocks} content={children} />
+          <WrappedRoute path='/domain_blocks' component={DomainBlocks} content={children} />
+          <WrappedRoute path='/followed_tags' component={FollowedTags} content={children} />
+          <WrappedRoute path='/mutes' component={Mutes} content={children} />
+          <WrappedRoute path='/lists' component={Lists} content={children} />
+          <WrappedRoute path='/getting-started-misc' component={GettingStartedMisc} content={children} />
+
+          <Route component={BundleColumnError} />
+        </WrappedSwitch>
+      </ColumnsAreaContainer>
+    );
+  }
+
+}
+
+class UI extends PureComponent {
+
+  static contextTypes = {
+    identity: PropTypes.object.isRequired,
+  };
+
+  static propTypes = {
+    dispatch: PropTypes.func.isRequired,
+    children: PropTypes.node,
+    isWide: PropTypes.bool,
+    systemFontUi: PropTypes.bool,
+    isComposing: PropTypes.bool,
+    hasComposingText: PropTypes.bool,
+    hasMediaAttachments: PropTypes.bool,
+    canUploadMore: PropTypes.bool,
+    intl: PropTypes.object.isRequired,
+    dropdownMenuIsOpen: PropTypes.bool,
+    unreadNotifications: PropTypes.number,
+    showFaviconBadge: PropTypes.bool,
+    hicolorPrivacyIcons: PropTypes.bool,
+    moved: PropTypes.map,
+    layout: PropTypes.string.isRequired,
+    firstLaunch: PropTypes.bool,
+    username: PropTypes.string,
+    ...WithRouterPropTypes,
+  };
+
+  state = {
+    draggingOver: false,
+  };
+
+  handleBeforeUnload = e => {
+    const { intl, dispatch, hasComposingText, hasMediaAttachments } = this.props;
+
+    dispatch(synchronouslySubmitMarkers());
+
+    if (hasComposingText || hasMediaAttachments) {
+      // Setting returnValue to any string causes confirmation dialog.
+      // Many browsers no longer display this text to users,
+      // but we set user-friendly message for other browsers, e.g. Edge.
+      e.returnValue = intl.formatMessage(messages.beforeUnload);
+    }
+  };
+
+  handleVisibilityChange = () => {
+    const visibility = !document[this.visibilityHiddenProp];
+    this.props.dispatch(notificationsSetVisibility(visibility));
+    if (visibility) {
+      this.props.dispatch(submitMarkers({ immediate: true }));
+    }
+  };
+
+  handleDragEnter = (e) => {
+    e.preventDefault();
+
+    if (!this.dragTargets) {
+      this.dragTargets = [];
+    }
+
+    if (this.dragTargets.indexOf(e.target) === -1) {
+      this.dragTargets.push(e.target);
+    }
+
+    if (e.dataTransfer && Array.from(e.dataTransfer.types).includes('Files') && this.props.canUploadMore && this.context.identity.signedIn) {
+      this.setState({ draggingOver: true });
+    }
+  };
+
+  handleDragOver = (e) => {
+    if (this.dataTransferIsText(e.dataTransfer)) return false;
+
+    e.preventDefault();
+    e.stopPropagation();
+
+    try {
+      e.dataTransfer.dropEffect = 'copy';
+    } catch (err) {
+
+    }
+
+    return false;
+  };
+
+  handleDrop = (e) => {
+    if (this.dataTransferIsText(e.dataTransfer)) return;
+
+    e.preventDefault();
+
+    this.setState({ draggingOver: false });
+    this.dragTargets = [];
+
+    if (e.dataTransfer && e.dataTransfer.files.length >= 1 && this.props.canUploadMore && this.context.identity.signedIn) {
+      this.props.dispatch(uploadCompose(e.dataTransfer.files));
+    }
+  };
+
+  handleDragLeave = (e) => {
+    e.preventDefault();
+    e.stopPropagation();
+
+    this.dragTargets = this.dragTargets.filter(el => el !== e.target && this.node.contains(el));
+
+    if (this.dragTargets.length > 0) {
+      return;
+    }
+
+    this.setState({ draggingOver: false });
+  };
+
+  dataTransferIsText = (dataTransfer) => {
+    return (dataTransfer && Array.from(dataTransfer.types).filter((type) => type === 'text/plain').length === 1);
+  };
+
+  closeUploadModal = () => {
+    this.setState({ draggingOver: false });
+  };
+
+  handleServiceWorkerPostMessage = ({ data }) => {
+    if (data.type === 'navigate') {
+      this.props.history.push(data.path);
+    } else {
+      console.warn('Unknown message type:', data.type);
+    }
+  };
+
+  handleLayoutChange = debounce(() => {
+    this.props.dispatch(clearHeight()); // The cached heights are no longer accurate, invalidate
+  }, 500, {
+    trailing: true,
+  });
+
+  handleResize = () => {
+    const layout = layoutFromWindow();
+
+    if (layout !== this.props.layout) {
+      this.handleLayoutChange.cancel();
+      this.props.dispatch(changeLayout({ layout }));
+    } else {
+      this.handleLayoutChange();
+    }
+  };
+
+  componentDidMount () {
+    const { signedIn } = this.context.identity;
+
+    window.addEventListener('beforeunload', this.handleBeforeUnload, false);
+    window.addEventListener('resize', this.handleResize, { passive: true });
+
+    document.addEventListener('dragenter', this.handleDragEnter, false);
+    document.addEventListener('dragover', this.handleDragOver, false);
+    document.addEventListener('drop', this.handleDrop, false);
+    document.addEventListener('dragleave', this.handleDragLeave, false);
+    document.addEventListener('dragend', this.handleDragEnd, false);
+
+    if ('serviceWorker' in  navigator) {
+      navigator.serviceWorker.addEventListener('message', this.handleServiceWorkerPostMessage);
+    }
+
+    this.favicon = new Favico({ animation:'none' });
+
+    if (signedIn) {
+      this.props.dispatch(fetchMarkers());
+      this.props.dispatch(expandHomeTimeline());
+      this.props.dispatch(expandNotifications());
+      this.props.dispatch(fetchServerTranslationLanguages());
+
+      setTimeout(() => this.props.dispatch(fetchServer()), 3000);
+    }
+
+    this.hotkeys.__mousetrap__.stopCallback = (e, element) => {
+      return ['TEXTAREA', 'SELECT', 'INPUT'].includes(element.tagName);
+    };
+
+    if (typeof document.hidden !== 'undefined') { // Opera 12.10 and Firefox 18 and later support
+      this.visibilityHiddenProp = 'hidden';
+      this.visibilityChange = 'visibilitychange';
+    } else if (typeof document.msHidden !== 'undefined') {
+      this.visibilityHiddenProp = 'msHidden';
+      this.visibilityChange = 'msvisibilitychange';
+    } else if (typeof document.webkitHidden !== 'undefined') {
+      this.visibilityHiddenProp = 'webkitHidden';
+      this.visibilityChange = 'webkitvisibilitychange';
+    }
+
+    if (this.visibilityChange !== undefined) {
+      document.addEventListener(this.visibilityChange, this.handleVisibilityChange, false);
+      this.handleVisibilityChange();
+    }
+  }
+
+  componentDidUpdate (prevProps) {
+    if (this.props.unreadNotifications !== prevProps.unreadNotifications ||
+        this.props.showFaviconBadge !== prevProps.showFaviconBadge) {
+      if (this.favicon) {
+        try {
+          this.favicon.badge(this.props.showFaviconBadge ? this.props.unreadNotifications : 0);
+        } catch (err) {
+          console.error(err);
+        }
+      }
+    }
+  }
+
+  componentWillUnmount () {
+    if (this.visibilityChange !== undefined) {
+      document.removeEventListener(this.visibilityChange, this.handleVisibilityChange);
+    }
+
+    window.removeEventListener('beforeunload', this.handleBeforeUnload);
+    window.removeEventListener('resize', this.handleResize);
+
+    document.removeEventListener('dragenter', this.handleDragEnter);
+    document.removeEventListener('dragover', this.handleDragOver);
+    document.removeEventListener('drop', this.handleDrop);
+    document.removeEventListener('dragleave', this.handleDragLeave);
+    document.removeEventListener('dragend', this.handleDragEnd);
+  }
+
+  setRef = c => {
+    this.node = c;
+  };
+
+  handleHotkeyNew = e => {
+    e.preventDefault();
+
+    const element = this.node.querySelector('.compose-form__autosuggest-wrapper textarea');
+
+    if (element) {
+      element.focus();
+    }
+  };
+
+  handleHotkeySearch = e => {
+    e.preventDefault();
+
+    const element = this.node.querySelector('.search__input');
+
+    if (element) {
+      element.focus();
+    }
+  };
+
+  handleHotkeyForceNew = e => {
+    this.handleHotkeyNew(e);
+    this.props.dispatch(resetCompose());
+  };
+
+  handleHotkeyToggleComposeSpoilers = e => {
+    e.preventDefault();
+    this.props.dispatch(changeComposeSpoilerness());
+  };
+
+  handleHotkeyFocusColumn = e => {
+    const index  = (e.key * 1) + 1; // First child is drawer, skip that
+    const column = this.node.querySelector(`.column:nth-child(${index})`);
+    if (!column) return;
+    const container = column.querySelector('.scrollable');
+
+    if (container) {
+      const status = container.querySelector('.focusable');
+
+      if (status) {
+        if (container.scrollTop > status.offsetTop) {
+          status.scrollIntoView(true);
+        }
+        status.focus();
+      }
+    }
+  };
+
+  handleHotkeyBack = () => {
+    const { history } = this.props;
+
+    if (history.location?.state?.fromMastodon) {
+      history.goBack();
+    } else {
+      history.push('/');
+    }
+  };
+
+  setHotkeysRef = c => {
+    this.hotkeys = c;
+  };
+
+  handleHotkeyToggleHelp = () => {
+    if (this.props.location.pathname === '/keyboard-shortcuts') {
+      this.props.history.goBack();
+    } else {
+      this.props.history.push('/keyboard-shortcuts');
+    }
+  };
+
+  handleHotkeyGoToHome = () => {
+    this.props.history.push('/home');
+  };
+
+  handleHotkeyGoToNotifications = () => {
+    this.props.history.push('/notifications');
+  };
+
+  handleHotkeyGoToLocal = () => {
+    this.props.history.push('/public/local');
+  };
+
+  handleHotkeyGoToFederated = () => {
+    this.props.history.push('/public');
+  };
+
+  handleHotkeyGoToDirect = () => {
+    this.props.history.push('/conversations');
+  };
+
+  handleHotkeyGoToStart = () => {
+    this.props.history.push('/getting-started');
+  };
+
+  handleHotkeyGoToFavourites = () => {
+    this.props.history.push('/favourites');
+  };
+
+  handleHotkeyGoToPinned = () => {
+    this.props.history.push('/pinned');
+  };
+
+  handleHotkeyGoToProfile = () => {
+    this.props.history.push(`/@${this.props.username}`);
+  };
+
+  handleHotkeyGoToBlocked = () => {
+    this.props.history.push('/blocks');
+  };
+
+  handleHotkeyGoToMuted = () => {
+    this.props.history.push('/mutes');
+  };
+
+  handleHotkeyGoToRequests = () => {
+    this.props.history.push('/follow_requests');
+  };
+
+  render () {
+    const { draggingOver } = this.state;
+    const { children, isWide, location, dropdownMenuIsOpen, layout, moved } = this.props;
+
+    const columnsClass = layout => {
+      switch (layout) {
+      case 'single':
+        return 'single-column';
+      case 'multiple':
+        return 'multi-columns';
+      default:
+        return 'auto-columns';
+      }
+    };
+
+    const className = classNames('ui', columnsClass(layout), {
+      'wide': isWide,
+      'system-font': this.props.systemFontUi,
+      'hicolor-privacy-icons': this.props.hicolorPrivacyIcons,
+    });
+
+    const handlers = {
+      help: this.handleHotkeyToggleHelp,
+      new: this.handleHotkeyNew,
+      search: this.handleHotkeySearch,
+      forceNew: this.handleHotkeyForceNew,
+      toggleComposeSpoilers: this.handleHotkeyToggleComposeSpoilers,
+      focusColumn: this.handleHotkeyFocusColumn,
+      back: this.handleHotkeyBack,
+      goToHome: this.handleHotkeyGoToHome,
+      goToNotifications: this.handleHotkeyGoToNotifications,
+      goToLocal: this.handleHotkeyGoToLocal,
+      goToFederated: this.handleHotkeyGoToFederated,
+      goToDirect: this.handleHotkeyGoToDirect,
+      goToStart: this.handleHotkeyGoToStart,
+      goToFavourites: this.handleHotkeyGoToFavourites,
+      goToPinned: this.handleHotkeyGoToPinned,
+      goToProfile: this.handleHotkeyGoToProfile,
+      goToBlocked: this.handleHotkeyGoToBlocked,
+      goToMuted: this.handleHotkeyGoToMuted,
+      goToRequests: this.handleHotkeyGoToRequests,
+    };
+
+    return (
+      <HotKeys keyMap={keyMap} handlers={handlers} ref={this.setHotkeysRef} attach={window} focused>
+        <div className={className} ref={this.setRef} style={{ pointerEvents: dropdownMenuIsOpen ? 'none' : null }}>
+          {moved && (<div className='flash-message alert'>
+            <FormattedMessage
+              id='moved_to_warning'
+              defaultMessage='This account is marked as moved to {moved_to_link}, and may thus not accept new follows.'
+              values={{ moved_to_link: (
+                <PermaLink href={moved.get('url')} to={`/@${moved.get('acct')}`}>
+                  @{moved.get('acct')}
+                </PermaLink>
+              ) }}
+            />
+          </div>)}
+
+          <Header />
+
+          <SwitchingColumnsArea location={location} singleColumn={layout === 'mobile' || layout === 'single-column'}>
+            {children}
+          </SwitchingColumnsArea>
+
+          {layout !== 'mobile' && <PictureInPicture />}
+          <NotificationsContainer />
+          <LoadingBarContainer className='loading-bar' />
+          <ModalContainer />
+          <UploadArea active={draggingOver} onClose={this.closeUploadModal} />
+        </div>
+      </HotKeys>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps)(injectIntl(withRouter(UI)));
diff --git a/app/javascript/flavours/blobfox/features/ui/util/async-components.js b/app/javascript/flavours/blobfox/features/ui/util/async-components.js
new file mode 100644
index 00000000000000..c2b4042e5a4ed4
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/util/async-components.js
@@ -0,0 +1,203 @@
+export function EmojiPicker () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/emoji_picker" */'flavours/blobfox/features/emoji/emoji_picker');
+}
+
+export function Compose () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/compose" */'flavours/blobfox/features/compose');
+}
+
+export function Notifications () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/notifications" */'flavours/blobfox/features/notifications');
+}
+
+export function HomeTimeline () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/home_timeline" */'flavours/blobfox/features/home_timeline');
+}
+
+export function PublicTimeline () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/public_timeline" */'flavours/blobfox/features/public_timeline');
+}
+
+export function CommunityTimeline () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/community_timeline" */'flavours/blobfox/features/community_timeline');
+}
+
+export function Firehose () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/firehose" */'../../firehose');
+}
+
+export function HashtagTimeline () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/hashtag_timeline" */'flavours/blobfox/features/hashtag_timeline');
+}
+
+export function ListTimeline () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/list_timeline" */'flavours/blobfox/features/list_timeline');
+}
+
+export function Lists () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/lists" */'flavours/blobfox/features/lists');
+}
+
+export function ListEditor () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/list_editor" */'flavours/blobfox/features/list_editor');
+}
+
+export function PinnedAccountsEditor () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/pinned_accounts_editor" */'flavours/blobfox/features/pinned_accounts_editor');
+}
+
+export function DirectTimeline() {
+  return import(/* webpackChunkName: "flavours/blobfox/async/direct_timeline" */'flavours/blobfox/features/direct_timeline');
+}
+
+export function Status () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/status" */'flavours/blobfox/features/status');
+}
+
+export function GettingStarted () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/getting_started" */'flavours/blobfox/features/getting_started');
+}
+
+export function KeyboardShortcuts () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/keyboard_shortcuts" */'flavours/blobfox/features/keyboard_shortcuts');
+}
+
+export function PinnedStatuses () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/pinned_statuses" */'flavours/blobfox/features/pinned_statuses');
+}
+
+export function AccountTimeline () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/account_timeline" */'flavours/blobfox/features/account_timeline');
+}
+
+export function AccountGallery () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/account_gallery" */'flavours/blobfox/features/account_gallery');
+}
+
+export function Followers () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/followers" */'flavours/blobfox/features/followers');
+}
+
+export function Following () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/following" */'flavours/blobfox/features/following');
+}
+
+export function Reblogs () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/reblogs" */'flavours/blobfox/features/reblogs');
+}
+
+export function Favourites () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/favourites" */'flavours/blobfox/features/favourites');
+}
+
+export function FollowRequests () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/follow_requests" */'flavours/blobfox/features/follow_requests');
+}
+
+export function FavouritedStatuses () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/favourited_statuses" */'flavours/blobfox/features/favourited_statuses');
+}
+
+export function FollowedTags () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/followed_tags" */'flavours/blobfox/features/followed_tags');
+}
+
+export function BookmarkedStatuses () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/bookmarked_statuses" */'flavours/blobfox/features/bookmarked_statuses');
+}
+
+export function Blocks () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/blocks" */'flavours/blobfox/features/blocks');
+}
+
+export function DomainBlocks () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/domain_blocks" */'flavours/blobfox/features/domain_blocks');
+}
+
+export function Mutes () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/mutes" */'flavours/blobfox/features/mutes');
+}
+
+export function MuteModal () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/mute_modal" */'flavours/blobfox/features/ui/components/mute_modal');
+}
+
+export function BlockModal () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/block_modal" */'flavours/blobfox/features/ui/components/block_modal');
+}
+
+export function ReportModal () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/report_modal" */'flavours/blobfox/features/ui/components/report_modal');
+}
+
+export function SettingsModal () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/settings_modal" */'flavours/blobfox/features/local_settings');
+}
+
+export function MediaGallery () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/media_gallery" */'flavours/blobfox/components/media_gallery');
+}
+
+export function Video () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/video" */'flavours/blobfox/features/video');
+}
+
+export function Audio () {
+  return import(/* webpackChunkName: "features/blobfox/async/audio" */'flavours/blobfox/features/audio');
+}
+
+export function EmbedModal () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/embed_modal" */'flavours/blobfox/features/ui/components/embed_modal');
+}
+
+export function GettingStartedMisc () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/getting_started_misc" */'flavours/blobfox/features/getting_started_misc');
+}
+
+export function ListAdder () {
+  return import(/* webpackChunkName: "features/blobfox/async/list_adder" */'flavours/blobfox/features/list_adder');
+}
+
+export function Tesseract () {
+  return import(/*webpackChunkName: "tesseract" */'tesseract.js');
+}
+
+export function Directory () {
+  return import(/* webpackChunkName: "features/blobfox/async/directory" */'flavours/blobfox/features/directory');
+}
+
+export function Onboarding () {
+  return import(/* webpackChunkName: "features/blobfox/async/onboarding" */'flavours/blobfox/features/onboarding');
+}
+
+export function CompareHistoryModal () {
+  return import(/*webpackChunkName: "flavours/blobfox/async/compare_history_modal" */'flavours/blobfox/features/ui/components/compare_history_modal');
+}
+
+export function FilterModal () {
+  return import(/*webpackChunkName: "flavours/blobfox/async/filter_modal" */'flavours/blobfox/features/ui/components/filter_modal');
+}
+
+export function Explore () {
+  return import(/* webpackChunkName: "flavours/blobfox/async/explore" */'flavours/blobfox/features/explore');
+}
+
+export function InteractionModal () {
+  return import(/*webpackChunkName: "flavours/blobfox/async/modals/interaction_modal" */'flavours/blobfox/features/interaction_modal');
+}
+
+export function SubscribedLanguagesModal () {
+  return import(/*webpackChunkName: "flavours/blobfox/async/modals/subscribed_languages_modal" */'flavours/blobfox/features/subscribed_languages_modal');
+}
+
+export function ClosedRegistrationsModal () {
+  return import(/*webpackChunkName: "flavours/blobfox/async/modals/closed_registrations_modal" */'flavours/blobfox/features/closed_registrations_modal');
+}
+
+export function About () {
+  return import(/*webpackChunkName: "features/blobfox/async/about" */'flavours/blobfox/features/about');
+}
+
+export function PrivacyPolicy () {
+  return import(/*webpackChunkName: "features/blobfox/async/privacy_policy" */'flavours/blobfox/features/privacy_policy');
+}
diff --git a/app/javascript/flavours/blobfox/features/ui/util/fullscreen.js b/app/javascript/flavours/blobfox/features/ui/util/fullscreen.js
new file mode 100644
index 00000000000000..cf5d0cf98d0cd6
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/util/fullscreen.js
@@ -0,0 +1,46 @@
+// APIs for normalizing fullscreen operations. Note that Edge uses
+// the WebKit-prefixed APIs currently (as of Edge 16).
+
+export const isFullscreen = () => document.fullscreenElement ||
+  document.webkitFullscreenElement ||
+  document.mozFullScreenElement;
+
+export const exitFullscreen = () => {
+  if (document.exitFullscreen) {
+    document.exitFullscreen();
+  } else if (document.webkitExitFullscreen) {
+    document.webkitExitFullscreen();
+  } else if (document.mozCancelFullScreen) {
+    document.mozCancelFullScreen();
+  }
+};
+
+export const requestFullscreen = el => {
+  if (el.requestFullscreen) {
+    el.requestFullscreen();
+  } else if (el.webkitRequestFullscreen) {
+    el.webkitRequestFullscreen();
+  } else if (el.mozRequestFullScreen) {
+    el.mozRequestFullScreen();
+  }
+};
+
+export const attachFullscreenListener = (listener) => {
+  if ('onfullscreenchange' in document) {
+    document.addEventListener('fullscreenchange', listener);
+  } else if ('onwebkitfullscreenchange' in document) {
+    document.addEventListener('webkitfullscreenchange', listener);
+  } else if ('onmozfullscreenchange' in document) {
+    document.addEventListener('mozfullscreenchange', listener);
+  }
+};
+
+export const detachFullscreenListener = (listener) => {
+  if ('onfullscreenchange' in document) {
+    document.removeEventListener('fullscreenchange', listener);
+  } else if ('onwebkitfullscreenchange' in document) {
+    document.removeEventListener('webkitfullscreenchange', listener);
+  } else if ('onmozfullscreenchange' in document) {
+    document.removeEventListener('mozfullscreenchange', listener);
+  }
+};
diff --git a/app/javascript/flavours/blobfox/features/ui/util/get_rect_from_entry.js b/app/javascript/flavours/blobfox/features/ui/util/get_rect_from_entry.js
new file mode 100644
index 00000000000000..c266cd7dce7374
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/util/get_rect_from_entry.js
@@ -0,0 +1,21 @@
+
+// Get the bounding client rect from an IntersectionObserver entry.
+// This is to work around a bug in Chrome: https://crbug.com/737228
+
+let hasBoundingRectBug;
+
+function getRectFromEntry(entry) {
+  if (typeof hasBoundingRectBug !== 'boolean') {
+    const boundingRect = entry.target.getBoundingClientRect();
+    const observerRect = entry.boundingClientRect;
+    hasBoundingRectBug = boundingRect.height !== observerRect.height ||
+      boundingRect.top !== observerRect.top ||
+      boundingRect.width !== observerRect.width ||
+      boundingRect.bottom !== observerRect.bottom ||
+      boundingRect.left !== observerRect.left ||
+      boundingRect.right !== observerRect.right;
+  }
+  return hasBoundingRectBug ? entry.target.getBoundingClientRect() : entry.boundingClientRect;
+}
+
+export default getRectFromEntry;
diff --git a/app/javascript/flavours/blobfox/features/ui/util/intersection_observer_wrapper.js b/app/javascript/flavours/blobfox/features/ui/util/intersection_observer_wrapper.js
new file mode 100644
index 00000000000000..2b24c65831d87c
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/util/intersection_observer_wrapper.js
@@ -0,0 +1,57 @@
+// Wrapper for IntersectionObserver in order to make working with it
+// a bit easier. We also follow this performance advice:
+// "If you need to observe multiple elements, it is both possible and
+// advised to observe multiple elements using the same IntersectionObserver
+// instance by calling observe() multiple times."
+// https://developers.google.com/web/updates/2016/04/intersectionobserver
+
+class IntersectionObserverWrapper {
+
+  callbacks = {};
+  observerBacklog = [];
+  observer = null;
+
+  connect (options) {
+    const onIntersection = (entries) => {
+      entries.forEach(entry => {
+        const id = entry.target.getAttribute('data-id');
+        if (this.callbacks[id]) {
+          this.callbacks[id](entry);
+        }
+      });
+    };
+
+    this.observer = new IntersectionObserver(onIntersection, options);
+    this.observerBacklog.forEach(([ id, node, callback ]) => {
+      this.observe(id, node, callback);
+    });
+    this.observerBacklog = null;
+  }
+
+  observe (id, node, callback) {
+    if (!this.observer) {
+      this.observerBacklog.push([ id, node, callback ]);
+    } else {
+      this.callbacks[id] = callback;
+      this.observer.observe(node);
+    }
+  }
+
+  unobserve (id, node) {
+    if (this.observer) {
+      delete this.callbacks[id];
+      this.observer.unobserve(node);
+    }
+  }
+
+  disconnect () {
+    if (this.observer) {
+      this.callbacks = {};
+      this.observer.disconnect();
+      this.observer = null;
+    }
+  }
+
+}
+
+export default IntersectionObserverWrapper;
diff --git a/app/javascript/flavours/blobfox/features/ui/util/optional_motion.js b/app/javascript/flavours/blobfox/features/ui/util/optional_motion.js
new file mode 100644
index 00000000000000..0b6d4d97f7967f
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/util/optional_motion.js
@@ -0,0 +1,7 @@
+import Motion from 'react-motion/lib/Motion';
+
+import { reduceMotion } from '../../../initial_state';
+
+import ReducedMotion from './reduced_motion';
+
+export default reduceMotion ? ReducedMotion : Motion;
diff --git a/app/javascript/flavours/blobfox/features/ui/util/react_router_helpers.jsx b/app/javascript/flavours/blobfox/features/ui/util/react_router_helpers.jsx
new file mode 100644
index 00000000000000..c0ee31bf68041b
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/util/react_router_helpers.jsx
@@ -0,0 +1,105 @@
+import PropTypes from 'prop-types';
+import { Component, cloneElement, Children } from 'react';
+
+import { Switch, Route, useLocation } from 'react-router-dom';
+
+import StackTrace from 'stacktrace-js';
+
+import BundleColumnError from '../components/bundle_column_error';
+import ColumnLoading from '../components/column_loading';
+import BundleContainer from '../containers/bundle_container';
+
+// Small wrapper to pass multiColumn to the route components
+export const WrappedSwitch = ({ multiColumn, children }) => {
+  const  location = useLocation();
+
+  const decklessLocation = multiColumn && location.pathname.startsWith('/deck')
+    ? {...location, pathname: location.pathname.slice(5)}
+    : location;
+
+  return (
+    <Switch location={decklessLocation}>
+      {Children.map(children, child => child ? cloneElement(child, { multiColumn }) : null)}
+    </Switch>
+  );
+};
+
+
+WrappedSwitch.propTypes = {
+  multiColumn: PropTypes.bool,
+  children: PropTypes.node,
+};
+
+// Small Wrapper to extract the params from the route and pass
+// them to the rendered component, together with the content to
+// be rendered inside (the children)
+export class WrappedRoute extends Component {
+
+  static propTypes = {
+    component: PropTypes.func.isRequired,
+    content: PropTypes.node,
+    multiColumn: PropTypes.bool,
+    componentParams: PropTypes.object,
+  };
+
+  static defaultProps = {
+    componentParams: {},
+  };
+
+  static getDerivedStateFromError () {
+    return {
+      hasError: true,
+    };
+  }
+
+  state = {
+    hasError: false,
+    stacktrace: '',
+  };
+
+  componentDidCatch (error) {
+    StackTrace.fromError(error).then(stackframes => {
+      this.setState({ stacktrace: error.toString() + '\n' + stackframes.map(frame => frame.toString()).join('\n') });
+    }).catch(err => {
+      console.error(err);
+    });
+  }
+
+  renderComponent = ({ match }) => {
+    const { component, content, multiColumn, componentParams } = this.props;
+    const { hasError, stacktrace } = this.state;
+
+    if (hasError) {
+      return (
+        <BundleColumnError
+          stacktrace={stacktrace}
+          multiColumn={multiColumn}
+          errorType='error'
+        />
+      );
+    }
+
+    return (
+      <BundleContainer fetchComponent={component} loading={this.renderLoading} error={this.renderError}>
+        {Component => <Component params={match.params} multiColumn={multiColumn} {...componentParams}>{content}</Component>}
+      </BundleContainer>
+    );
+  };
+
+  renderLoading = () => {
+    const { multiColumn } = this.props;
+
+    return <ColumnLoading multiColumn={multiColumn} />;
+  };
+
+  renderError = (props) => {
+    return <BundleColumnError {...props} errorType='network' />;
+  };
+
+  render () {
+    const { component: Component, content, ...rest } = this.props;
+
+    return <Route {...rest} render={this.renderComponent} />;
+  }
+
+}
diff --git a/app/javascript/flavours/blobfox/features/ui/util/reduced_motion.jsx b/app/javascript/flavours/blobfox/features/ui/util/reduced_motion.jsx
new file mode 100644
index 00000000000000..fd044497f80279
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/util/reduced_motion.jsx
@@ -0,0 +1,45 @@
+// Like react-motion's Motion, but reduces all animations to cross-fades
+// for the benefit of users with motion sickness.
+import PropTypes from 'prop-types';
+import { Component } from 'react';
+
+import Motion from 'react-motion/lib/Motion';
+
+const stylesToKeep = ['opacity', 'backgroundOpacity'];
+
+const extractValue = (value) => {
+  // This is either an object with a "val" property or it's a number
+  return (typeof value === 'object' && value && 'val' in value) ? value.val : value;
+};
+
+class ReducedMotion extends Component {
+
+  static propTypes = {
+    defaultStyle: PropTypes.object,
+    style: PropTypes.object,
+    children: PropTypes.func,
+  };
+
+  render() {
+
+    const { style, defaultStyle, children } = this.props;
+
+    Object.keys(style).forEach(key => {
+      if (stylesToKeep.includes(key)) {
+        return;
+      }
+      // If it's setting an x or height or scale or some other value, we need
+      // to preserve the end-state value without actually animating it
+      style[key] = defaultStyle[key] = extractValue(style[key]);
+    });
+
+    return (
+      <Motion style={style} defaultStyle={defaultStyle}>
+        {children}
+      </Motion>
+    );
+  }
+
+}
+
+export default ReducedMotion;
diff --git a/app/javascript/flavours/blobfox/features/ui/util/schedule_idle_task.js b/app/javascript/flavours/blobfox/features/ui/util/schedule_idle_task.js
new file mode 100644
index 00000000000000..b04d4a8eefa3b7
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/ui/util/schedule_idle_task.js
@@ -0,0 +1,29 @@
+// Wrapper to call requestIdleCallback() to schedule low-priority work.
+// See https://developer.mozilla.org/en-US/docs/Web/API/Background_Tasks_API
+// for a good breakdown of the concepts behind this.
+
+import Queue from 'tiny-queue';
+
+const taskQueue = new Queue();
+let runningRequestIdleCallback = false;
+
+function runTasks(deadline) {
+  while (taskQueue.length && deadline.timeRemaining() > 0) {
+    taskQueue.shift()();
+  }
+  if (taskQueue.length) {
+    requestIdleCallback(runTasks);
+  } else {
+    runningRequestIdleCallback = false;
+  }
+}
+
+function scheduleIdleTask(task) {
+  taskQueue.push(task);
+  if (!runningRequestIdleCallback) {
+    runningRequestIdleCallback = true;
+    requestIdleCallback(runTasks);
+  }
+}
+
+export default scheduleIdleTask;
diff --git a/app/javascript/flavours/blobfox/features/video/index.jsx b/app/javascript/flavours/blobfox/features/video/index.jsx
new file mode 100644
index 00000000000000..7fb70f33c135d1
--- /dev/null
+++ b/app/javascript/flavours/blobfox/features/video/index.jsx
@@ -0,0 +1,665 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import classNames from 'classnames';
+
+import { is } from 'immutable';
+
+import { throttle } from 'lodash';
+
+import { Blurhash } from 'flavours/blobfox/components/blurhash';
+import { Icon }  from 'flavours/blobfox/components/icon';
+import { playerSettings } from 'flavours/blobfox/settings';
+
+import { displayMedia, useBlurhash } from '../../initial_state';
+import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
+
+const messages = defineMessages({
+  play: { id: 'video.play', defaultMessage: 'Play' },
+  pause: { id: 'video.pause', defaultMessage: 'Pause' },
+  mute: { id: 'video.mute', defaultMessage: 'Mute sound' },
+  unmute: { id: 'video.unmute', defaultMessage: 'Unmute sound' },
+  hide: { id: 'video.hide', defaultMessage: 'Hide video' },
+  expand: { id: 'video.expand', defaultMessage: 'Expand video' },
+  close: { id: 'video.close', defaultMessage: 'Close video' },
+  fullscreen: { id: 'video.fullscreen', defaultMessage: 'Full screen' },
+  exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' },
+});
+
+export const formatTime = secondsNum => {
+  let hours   = Math.floor(secondsNum / 3600);
+  let minutes = Math.floor((secondsNum - (hours * 3600)) / 60);
+  let seconds = secondsNum - (hours * 3600) - (minutes * 60);
+
+  if (hours   < 10) hours   = '0' + hours;
+  if (minutes < 10) minutes = '0' + minutes;
+  if (seconds < 10) seconds = '0' + seconds;
+
+  return (hours === '00' ? '' : `${hours}:`) + `${minutes}:${seconds}`;
+};
+
+export const findElementPosition = el => {
+  let box;
+
+  if (el.getBoundingClientRect && el.parentNode) {
+    box = el.getBoundingClientRect();
+  }
+
+  if (!box) {
+    return {
+      left: 0,
+      top: 0,
+    };
+  }
+
+  const docEl = document.documentElement;
+  const body  = document.body;
+
+  const clientLeft = docEl.clientLeft || body.clientLeft || 0;
+  const scrollLeft = window.pageXOffset || body.scrollLeft;
+  const left       = (box.left + scrollLeft) - clientLeft;
+
+  const clientTop = docEl.clientTop || body.clientTop || 0;
+  const scrollTop = window.pageYOffset || body.scrollTop;
+  const top       = (box.top + scrollTop) - clientTop;
+
+  return {
+    left: Math.round(left),
+    top: Math.round(top),
+  };
+};
+
+export const getPointerPosition = (el, event) => {
+  const position = {};
+  const box = findElementPosition(el);
+  const boxW = el.offsetWidth;
+  const boxH = el.offsetHeight;
+  const boxY = box.top;
+  const boxX = box.left;
+
+  let pageY = event.pageY;
+  let pageX = event.pageX;
+
+  if (event.changedTouches) {
+    pageX = event.changedTouches[0].pageX;
+    pageY = event.changedTouches[0].pageY;
+  }
+
+  position.y = Math.max(0, Math.min(1, (pageY - boxY) / boxH));
+  position.x = Math.max(0, Math.min(1, (pageX - boxX) / boxW));
+
+  return position;
+};
+
+export const fileNameFromURL = str => {
+  const url      = new URL(str);
+  const pathname = url.pathname;
+  const index    = pathname.lastIndexOf('/');
+
+  return pathname.slice(index + 1);
+};
+
+class Video extends PureComponent {
+
+  static propTypes = {
+    preview: PropTypes.string,
+    frameRate: PropTypes.string,
+    src: PropTypes.string.isRequired,
+    alt: PropTypes.string,
+    lang: PropTypes.string,
+    sensitive: PropTypes.bool,
+    currentTime: PropTypes.number,
+    onOpenVideo: PropTypes.func,
+    onCloseVideo: PropTypes.func,
+    detailed: PropTypes.bool,
+    inline: PropTypes.bool,
+    editable: PropTypes.bool,
+    alwaysVisible: PropTypes.bool,
+    visible: PropTypes.bool,
+    letterbox: PropTypes.bool,
+    fullwidth: PropTypes.bool,
+    preventPlayback: PropTypes.bool,
+    onToggleVisibility: PropTypes.func,
+    deployPictureInPicture: PropTypes.func,
+    intl: PropTypes.object.isRequired,
+    blurhash: PropTypes.string,
+    autoPlay: PropTypes.bool,
+    volume: PropTypes.number,
+    muted: PropTypes.bool,
+    componentIndex: PropTypes.number,
+    autoFocus: PropTypes.bool,
+  };
+
+  static defaultProps = {
+    frameRate: '25',
+  };
+
+  state = {
+    currentTime: 0,
+    duration: 0,
+    volume: 0.5,
+    paused: true,
+    dragging: false,
+    fullscreen: false,
+    hovered: false,
+    muted: false,
+    revealed: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'),
+  };
+
+  setPlayerRef = c => {
+    this.player = c;
+  };
+
+  setVideoRef = c => {
+    this.video = c;
+
+    if (this.video) {
+      this.setState({ volume: this.video.volume, muted: this.video.muted });
+    }
+  };
+
+  setSeekRef = c => {
+    this.seek = c;
+  };
+
+  setVolumeRef = c => {
+    this.volume = c;
+  };
+
+  handleClickRoot = e => e.stopPropagation();
+
+  handlePlay = () => {
+    this.setState({ paused: false });
+    this._updateTime();
+  };
+
+  handlePause = () => {
+    this.setState({ paused: true });
+  };
+
+  _updateTime () {
+    requestAnimationFrame(() => {
+      if (!this.video) return;
+
+      this.handleTimeUpdate();
+
+      if (!this.state.paused) {
+        this._updateTime();
+      }
+    });
+  }
+
+  handleTimeUpdate = () => {
+    this.setState({
+      currentTime: this.video.currentTime,
+      duration:this.video.duration,
+    });
+  };
+
+  handleVolumeMouseDown = e => {
+    document.addEventListener('mousemove', this.handleMouseVolSlide, true);
+    document.addEventListener('mouseup', this.handleVolumeMouseUp, true);
+    document.addEventListener('touchmove', this.handleMouseVolSlide, true);
+    document.addEventListener('touchend', this.handleVolumeMouseUp, true);
+
+    this.handleMouseVolSlide(e);
+
+    e.preventDefault();
+    e.stopPropagation();
+  };
+
+  handleVolumeMouseUp = () => {
+    document.removeEventListener('mousemove', this.handleMouseVolSlide, true);
+    document.removeEventListener('mouseup', this.handleVolumeMouseUp, true);
+    document.removeEventListener('touchmove', this.handleMouseVolSlide, true);
+    document.removeEventListener('touchend', this.handleVolumeMouseUp, true);
+  };
+
+  handleMouseVolSlide = throttle(e => {
+    const { x } = getPointerPosition(this.volume, e);
+
+    if(!isNaN(x)) {
+      this.setState((state) => ({ volume: x, muted: state.muted && x === 0 }), () => {
+        this._syncVideoToVolumeState(x);
+        this._saveVolumeState(x);
+      });
+    }
+  }, 15);
+
+  handleMouseDown = e => {
+    document.addEventListener('mousemove', this.handleMouseMove, true);
+    document.addEventListener('mouseup', this.handleMouseUp, true);
+    document.addEventListener('touchmove', this.handleMouseMove, true);
+    document.addEventListener('touchend', this.handleMouseUp, true);
+
+    this.setState({ dragging: true });
+    this.video.pause();
+    this.handleMouseMove(e);
+
+    e.preventDefault();
+    e.stopPropagation();
+  };
+
+  handleMouseUp = () => {
+    document.removeEventListener('mousemove', this.handleMouseMove, true);
+    document.removeEventListener('mouseup', this.handleMouseUp, true);
+    document.removeEventListener('touchmove', this.handleMouseMove, true);
+    document.removeEventListener('touchend', this.handleMouseUp, true);
+
+    this.setState({ dragging: false });
+    this.video.play();
+  };
+
+  handleMouseMove = throttle(e => {
+    const { x } = getPointerPosition(this.seek, e);
+    const currentTime = this.video.duration * x;
+
+    if (!isNaN(currentTime)) {
+      this.setState({ currentTime }, () => {
+        this.video.currentTime = currentTime;
+      });
+    }
+  }, 15);
+
+  seekBy (time) {
+    const currentTime = this.video.currentTime + time;
+
+    if (!isNaN(currentTime)) {
+      this.setState({ currentTime }, () => {
+        this.video.currentTime = currentTime;
+      });
+    }
+  }
+
+  handleVideoKeyDown = e => {
+    // On the video element or the seek bar, we can safely use the space bar
+    // for playback control because there are no buttons to press
+
+    if (e.key === ' ') {
+      e.preventDefault();
+      e.stopPropagation();
+      this.togglePlay();
+    }
+  };
+
+  handleKeyDown = e => {
+    const frameTime = 1 / this.getFrameRate();
+
+    switch(e.key) {
+    case 'k':
+      e.preventDefault();
+      e.stopPropagation();
+      this.togglePlay();
+      break;
+    case 'm':
+      e.preventDefault();
+      e.stopPropagation();
+      this.toggleMute();
+      break;
+    case 'f':
+      e.preventDefault();
+      e.stopPropagation();
+      this.toggleFullscreen();
+      break;
+    case 'j':
+      e.preventDefault();
+      e.stopPropagation();
+      this.seekBy(-10);
+      break;
+    case 'l':
+      e.preventDefault();
+      e.stopPropagation();
+      this.seekBy(10);
+      break;
+    case ',':
+      e.preventDefault();
+      e.stopPropagation();
+      this.seekBy(-frameTime);
+      break;
+    case '.':
+      e.preventDefault();
+      e.stopPropagation();
+      this.seekBy(frameTime);
+      break;
+    }
+
+    // If we are in fullscreen mode, we don't want any hotkeys
+    // interacting with the UI that's not visible
+
+    if (this.state.fullscreen) {
+      e.preventDefault();
+      e.stopPropagation();
+
+      if (e.key === 'Escape') {
+        exitFullscreen();
+      }
+    }
+  };
+
+  togglePlay = () => {
+    if (this.state.paused) {
+      this.setState({ paused: false }, () => this.video.play());
+    } else {
+      this.setState({ paused: true }, () => this.video.pause());
+    }
+  };
+
+  toggleFullscreen = () => {
+    if (isFullscreen()) {
+      exitFullscreen();
+    } else {
+      requestFullscreen(this.player);
+    }
+  };
+
+  componentDidMount () {
+    document.addEventListener('fullscreenchange', this.handleFullscreenChange, true);
+    document.addEventListener('webkitfullscreenchange', this.handleFullscreenChange, true);
+    document.addEventListener('mozfullscreenchange', this.handleFullscreenChange, true);
+    document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
+
+    window.addEventListener('scroll', this.handleScroll);
+
+    this._syncVideoFromLocalStorage();
+  }
+
+  componentWillUnmount () {
+    window.removeEventListener('scroll', this.handleScroll);
+
+    document.removeEventListener('fullscreenchange', this.handleFullscreenChange, true);
+    document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange, true);
+    document.removeEventListener('mozfullscreenchange', this.handleFullscreenChange, true);
+    document.removeEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
+
+    if (!this.state.paused && this.video && this.props.deployPictureInPicture) {
+      this.props.deployPictureInPicture('video', {
+        src: this.props.src,
+        currentTime: this.video.currentTime,
+        muted: this.video.muted,
+        volume: this.video.volume,
+      });
+    }
+  }
+
+  UNSAFE_componentWillReceiveProps (nextProps) {
+    if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) {
+      this.setState({ revealed: nextProps.visible });
+    }
+  }
+
+  componentDidUpdate (prevProps) {
+    if (this.video && this.state.revealed && this.props.preventPlayback && !prevProps.preventPlayback) {
+      this.video.pause();
+    }
+  }
+
+  handleScroll = throttle(() => {
+    if (!this.video) {
+      return;
+    }
+
+    const { top, height } = this.video.getBoundingClientRect();
+    const inView = (top <= (window.innerHeight || document.documentElement.clientHeight)) && (top + height >= 0);
+
+    if (!this.state.paused && !inView) {
+      this.video.pause();
+
+      if (this.props.deployPictureInPicture) {
+        this.props.deployPictureInPicture('video', {
+          src: this.props.src,
+          currentTime: this.video.currentTime,
+          muted: this.video.muted,
+          volume: this.video.volume,
+        });
+      }
+
+      this.setState({ paused: true });
+    }
+  }, 150, { trailing: true });
+
+  handleFullscreenChange = () => {
+    this.setState({ fullscreen: isFullscreen() });
+  };
+
+  handleMouseEnter = () => {
+    this.setState({ hovered: true });
+  };
+
+  handleMouseLeave = () => {
+    this.setState({ hovered: false });
+  };
+
+  toggleMute = () => {
+    const muted = !(this.video.muted || this.state.volume === 0);
+
+    this.setState((state) => ({ muted, volume: Math.max(state.volume || 0.5, 0.05) }), () => {
+      this._syncVideoToVolumeState();
+      this._saveVolumeState();
+    });
+  };
+
+  _syncVideoToVolumeState = (volume = null, muted = null) => {
+    if (!this.video) {
+      return;
+    }
+
+    this.video.volume = volume ?? this.state.volume;
+    this.video.muted = muted ?? this.state.muted;
+  };
+
+  _saveVolumeState = (volume = null, muted = null) => {
+    playerSettings.set('volume', volume ?? this.state.volume);
+    playerSettings.set('muted', muted ?? this.state.muted);
+  };
+
+  _syncVideoFromLocalStorage = () => {
+    this.setState({ volume: playerSettings.get('volume') ?? 0.5, muted: playerSettings.get('muted') ?? false }, () => {
+      this._syncVideoToVolumeState();
+    });
+  };
+
+  toggleReveal = () => {
+    if (this.state.revealed) {
+      this.setState({ paused: true });
+    }
+
+    if (this.props.onToggleVisibility) {
+      this.props.onToggleVisibility();
+    } else {
+      this.setState({ revealed: !this.state.revealed });
+    }
+  };
+
+  handleLoadedData = () => {
+    const { currentTime, volume, muted, autoPlay } = this.props;
+
+    if (currentTime) {
+      this.video.currentTime = currentTime;
+    }
+
+    if (volume !== undefined) {
+      this.video.volume = volume;
+    }
+
+    if (muted !== undefined) {
+      this.video.muted = muted;
+    }
+
+    if (autoPlay) {
+      this.video.play();
+    }
+  };
+
+  handleProgress = () => {
+    const lastTimeRange = this.video.buffered.length - 1;
+
+    if (lastTimeRange > -1) {
+      this.setState({ buffer: Math.ceil(this.video.buffered.end(lastTimeRange) / this.video.duration * 100) });
+    }
+  };
+
+  handleVolumeChange = () => {
+    this.setState({ volume: this.video.volume, muted: this.video.muted });
+    this._saveVolumeState(this.video.volume, this.video.muted);
+  };
+
+  handleOpenVideo = () => {
+    this.video.pause();
+
+    this.props.onOpenVideo(this.props.lang, {
+      startTime: this.video.currentTime,
+      autoPlay: !this.state.paused,
+      defaultVolume: this.state.volume,
+      componentIndex: this.props.componentIndex,
+    });
+  };
+
+  handleCloseVideo = () => {
+    this.video.pause();
+    this.props.onCloseVideo();
+  };
+
+  getFrameRate () {
+    if (this.props.frameRate && isNaN(this.props.frameRate)) {
+      // The frame rate is returned as a fraction string so we
+      // need to convert it to a number
+
+      return this.props.frameRate.split('/').reduce((p, c) => p / c);
+    }
+
+    return this.props.frameRate;
+  }
+
+  render () {
+    const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, lang, letterbox, fullwidth, detailed, sensitive, editable, blurhash, autoFocus } = this.props;
+    const { currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, revealed } = this.state;
+    const progress = Math.min((currentTime / duration) * 100, 100);
+    const muted = this.state.muted || volume === 0;
+
+    const playerStyle = {};
+
+    if (inline) {
+      playerStyle.aspectRatio = '16 / 9';
+    }
+
+    let preload;
+
+    if (this.props.currentTime || fullscreen || dragging) {
+      preload = 'auto';
+    } else if (detailed) {
+      preload = 'metadata';
+    } else {
+      preload = 'none';
+    }
+
+    let warning;
+
+    if (sensitive) {
+      warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />;
+    } else {
+      warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />;
+    }
+
+    return (
+      <div
+        role='menuitem'
+        className={classNames('video-player', { inactive: !revealed, detailed, inline: inline && !fullscreen, fullscreen, editable, letterbox, 'full-width': fullwidth })}
+        style={playerStyle}
+        ref={this.setPlayerRef}
+        onMouseEnter={this.handleMouseEnter}
+        onMouseLeave={this.handleMouseLeave}
+        onClick={this.handleClickRoot}
+        onKeyDown={this.handleKeyDown}
+        tabIndex={0}
+      >
+        <Blurhash
+          hash={blurhash}
+          className={classNames('media-gallery__preview', {
+            'media-gallery__preview--hidden': revealed,
+          })}
+          dummy={!useBlurhash}
+        />
+
+        {(revealed || editable) && <video
+          ref={this.setVideoRef}
+          src={src}
+          poster={preview}
+          preload={preload}
+          role='button'
+          tabIndex={0}
+          aria-label={alt}
+          title={alt}
+          lang={lang}
+          onClick={this.togglePlay}
+          onKeyDown={this.handleVideoKeyDown}
+          onPlay={this.handlePlay}
+          onPause={this.handlePause}
+          onLoadedData={this.handleLoadedData}
+          onProgress={this.handleProgress}
+          onVolumeChange={this.handleVolumeChange}
+          style={{ ...playerStyle, width: '100%' }}
+        />}
+
+        <div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed || editable })}>
+          <button type='button' className='spoiler-button__overlay' onClick={this.toggleReveal}>
+            <span className='spoiler-button__overlay__label'>
+              {warning}
+              <span className='spoiler-button__overlay__action'><FormattedMessage id='status.media.show' defaultMessage='Click to show' /></span>
+            </span>
+          </button>
+        </div>
+
+        <div className={classNames('video-player__controls', { active: paused || hovered })}>
+          <div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}>
+            <div className='video-player__seek__buffer' style={{ width: `${buffer}%` }} />
+            <div className='video-player__seek__progress' style={{ width: `${progress}%` }} />
+
+            <span
+              className={classNames('video-player__seek__handle', { active: dragging })}
+              tabIndex={0}
+              style={{ left: `${progress}%` }}
+              onKeyDown={this.handleVideoKeyDown}
+            />
+          </div>
+
+          <div className='video-player__buttons-bar'>
+            <div className='video-player__buttons left'>
+              <button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} className='player-button' onClick={this.togglePlay} autoFocus={autoFocus}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
+              <button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} className='player-button' onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
+
+              <div className={classNames('video-player__volume', { active: this.state.hovered })} onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
+                <div className='video-player__volume__current' style={{ width: `${muted ? 0 : volume * 100}%` }} />
+
+                <span
+                  className={classNames('video-player__volume__handle')}
+                  tabIndex={0}
+                  style={{ left: `${muted ? 0 : volume * 100}%` }}
+                />
+              </div>
+
+              {(detailed || fullscreen) && (
+                <span className='video-player__time'>
+                  <span className='video-player__time-current'>{formatTime(Math.floor(currentTime))}</span>
+                  <span className='video-player__time-sep'>/</span>
+                  <span className='video-player__time-total'>{formatTime(Math.floor(duration))}</span>
+                </span>
+              )}
+            </div>
+
+            <div className='video-player__buttons right'>
+              {(!onCloseVideo && !editable && !fullscreen && !this.props.alwaysVisible) && <button type='button' title={intl.formatMessage(messages.hide)} aria-label={intl.formatMessage(messages.hide)} className='player-button' onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>}
+              {(!fullscreen && onOpenVideo) && <button type='button' title={intl.formatMessage(messages.expand)} aria-label={intl.formatMessage(messages.expand)} className='player-button' onClick={this.handleOpenVideo}><Icon id='expand' fixedWidth /></button>}
+              {onCloseVideo && <button type='button' title={intl.formatMessage(messages.close)} aria-label={intl.formatMessage(messages.close)} className='player-button' onClick={this.handleCloseVideo}><Icon id='compress' fixedWidth /></button>}
+              <button type='button' title={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} className='player-button' onClick={this.toggleFullscreen}><Icon id={fullscreen ? 'compress' : 'arrows-alt'} fixedWidth /></button>
+            </div>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(Video);
diff --git a/app/javascript/flavours/blobfox/hooks/useHovering.ts b/app/javascript/flavours/blobfox/hooks/useHovering.ts
new file mode 100644
index 00000000000000..2062e70d26ac98
--- /dev/null
+++ b/app/javascript/flavours/blobfox/hooks/useHovering.ts
@@ -0,0 +1,17 @@
+import { useCallback, useState } from 'react';
+
+export const useHovering = (animate?: boolean) => {
+  const [hovering, setHovering] = useState<boolean>(animate ?? false);
+
+  const handleMouseEnter = useCallback(() => {
+    if (animate) return;
+    setHovering(true);
+  }, [animate]);
+
+  const handleMouseLeave = useCallback(() => {
+    if (animate) return;
+    setHovering(false);
+  }, [animate]);
+
+  return { hovering, handleMouseEnter, handleMouseLeave };
+};
diff --git a/app/javascript/flavours/blobfox/images/elephant_ui_disappointed.svg b/app/javascript/flavours/blobfox/images/elephant_ui_disappointed.svg
new file mode 100644
index 00000000000000..580c15a138838e
--- /dev/null
+++ b/app/javascript/flavours/blobfox/images/elephant_ui_disappointed.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="134.11569" width="134.61565" viewBox="0 0 134.61565 134.11569"><path d="M82.69963 103.86569c6.8 1.5 11 2.4 11.3-6.200005.3-8.6-1.8-17.3-1.8-17.3l-13.6 1.1 4.1 22.400005z" class="st32" fill="#3a434e" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"/><path d="M65.39963 112.96569c-.2 10.3-.6 17.5 6.5 17.4 7.1-.1 12.6 1.1 13.6-5.3 1.1-6.3 1.9-20.6.7-28.000005" class="st32" fill="#3a434e" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"/><path d="M86.39963 97.66569c-1.4-7.5-4.1-23.2-4.1-23.2s13.2-1.5 10.4-13c-2.7-11.4-7.5-22.6-11-31.1s-14.5-16.9-28.6-15.7c-19.2 1.6-25.6 7-31.6 23.1-5.4 14.4-10.4 47.2-8.9 63.3.8 8.7 5 13.7 14.4 13.5 9.4-.2 39.8-.8 49.8-2.8.3-.1.6-.1.9-.2" class="st33" fill="#56606b"/><path d="M85.89963 97.76569l-4.1-23.2c0-.3.1-.5.4-.6 2.6-.4 5.3-1.4 7.3-3.1 1-.8 1.9-1.9 2.4-3.1.5-1.2.7-2.5.6-3.9 0-1.3-.4-2.6-.7-4-.3-1.3-.7-2.7-1.1-4-.8-2.7-1.7-5.3-2.6-7.9-1.9-5.2-4-10.4-6.1-15.5-.5-1.3-1-2.6-1.7-3.8-.6-1.2-1.4-2.3-2.3-3.4-1.7-2.1-3.8-4-6-5.5-4.6-3-10-4.7-15.4-4.9-2.7-.1-5.5.3-8.2.6-2.7.4-5.5.9-8.1 1.7-2.6.8-5.1 1.9-7.3 3.5s-4.1 3.6-5.6 5.8c-1.5 2.3-2.8 4.7-3.9 7.3-.6 1.3-1.1 2.5-1.6 3.8-.4 1.3-.9 2.6-1.3 3.9-1.6 5.3-2.8 10.7-3.9 16.1-1 5.4-1.9 10.9-2.6 16.4-.7 5.5-1.2 11-1.3 16.6-.1 2.8-.1 5.5.1 8.3.1 2.8.5 5.5 1.6 8 1 2.5 2.9 4.6 5.4 5.7 2.4 1.1 5.2 1.3 8 1.3 5.6-.1 11.1-.2 16.7-.4 11.1-.4 22.2-.8 33.2-2.3.1 0 .2.1.2.2s0 .2-.1.2c-2.7.9-5.5 1.2-8.3 1.4-2.8.2-5.6.5-8.3.6-5.6.3-11.1.6-16.7.7-5.6.2-11.1.3-16.7.4-2.8.1-5.7-.1-8.4-1.3s-4.7-3.5-5.8-6.2c-1.1-2.6-1.5-5.5-1.6-8.3-.2-2.8-.2-5.6-.1-8.4.2-5.6.7-11.1 1.3-16.7.7-5.5 1.5-11 2.6-16.5s2.3-10.9 3.9-16.3c.4-1.3.9-2.7 1.3-4 .5-1.3 1-2.6 1.6-3.9 1.1-2.6 2.4-5.1 4-7.4 1.6-2.3 3.6-4.4 5.9-6.1 2.3-1.7 4.9-2.8 7.6-3.7 2.7-.8 5.5-1.4 8.2-1.7 2.8-.3 5.5-.7 8.4-.6 5.6.2 11.2 1.9 15.9 5 2.4 1.6 4.5 3.5 6.3 5.7.9 1.1 1.7 2.3 2.4 3.5.7 1.3 1.2 2.6 1.7 3.9 2.1 5.1 4.2 10.3 6.1 15.5.9 2.6 1.8 5.3 2.6 7.9.4 1.3.8 2.7 1.1 4 .3 1.3.7 2.7.8 4.2.1 1.4-.1 2.9-.7 4.3s-1.5 2.5-2.6 3.5c-2.3 1.9-5 2.8-7.9 3.3l.4-.6 4.1 23.2c0 .3-.1.5-.4.6-.3.1-.7.5-.7.2z"/><path d="M26.49963 114.06569c-4.7 0-7.4-2.1-10-4.4-2.3-2-3.2-4.6-3.4-8.6-.1-2.700005-.6-10.000005.4-18.800005 3.8.9 9.7 3.8 13.4 7.6 5.6 5.7 17.7 6.3 22.7 6.3h1.8l.1-.4s.5-2.6 1.8-5.2l.3-.6-.7-.1c-.4-.1-10.9-1.9-9.7-10.8.7-4.9 13.3-7.9 33.9-7.9 2.2 0 3.8 0 4.2.1l3.5 2.2c-1.5.5-2.6.6-2.6.6l-.5.1.1.5c0 .2 2.8 16.4 4.1 24 0 0-7.9 13.100005-8 13.000005-.1-.1-.3-.1-.3-.1-.3 0-.7.1-.9.1-9.9 1.7-39.6 2.4-49.3 2.6l-.9-.2z" class="st34" fill="#3a434e"/><path d="M45.89963 51.36569c-.7 0-1.4-.6-1.4-1.4v-5.1c0-.7.6-1.4 1.4-1.4.7 0 1.4.6 1.4 1.4v5.1c-.1.8-.7 1.4-1.4 1.4z"/><path d="M72.89963 30.365685c-3.5.4-2.7 2.9-1.2 3.5 1.5.6 3.7.1 4.3-1.6.4-1.6-1.3-2.1-3.1-1.9z" class="st35" fill="#4f5862"/><path d="M44.29963 53.965685c-.4.7-1.5.2-2.7-.6-1.2-.8-2.1-1.5-1.6-2.2.4-.7 1.6-.4 2.8.4 1.2.8 2 1.7 1.5 2.4z" class="st34" fill="#4f5862"/><path d="M27.29963 36.165685c0-5.6-3.7-9.4-7.9-9.8-4.2-.4-9-.3-14.0000002 11.3-5.00000001 11.6-6.7 15.7-2.6 17.9 4.1 2.2 9.5000002 1.5 11.3000002-1.4 0 0 5.3 3.8 9.7-3.8" class="st36" fill="#56606b" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"/><path d="M11.19963 40.565685c-2.7000002 5.1-2.7000002 7.7-.5 8.5 2.2.8 4.1.7 6.4-3 0 0 2 .7 4.9-4.1.9-1.5-.7-2.6-.7-2.6s-4.8 1.3-7.1-5l-3 6.2z" class="st34" fill="#3a434e" fill-opacity="0"/><path d="M9.7996298 43.365685l4.4000002-9s1.8 6.3 7.8 4.9" class="st7" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"/><path d="M27.89963 67.365685c-4.9.8-9.7 4.5-9.3 15.7.4 11.2.5 18.700005 6.1 20.000005 5.5 1.3 13.8.3 14.1-7.100005.3-7.4.3-16.1.3-16.1" class="st36" fill="#53606c" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"/><path d="M28.69963 102.96569c-1.4 0-2.8-.2-4.1-.5-1.2-.3-2.2-.9-3-2 .5.2 1.1.3 1.7.3 1.2 0 5.2-.5 5.8-7.200005.7-7.4 2.8-10.9 6.6-10.9.8 0 1.6.1 2.6.4 0 3.4-.1 8.3-.2 12.7-.2 6.700005-7.2 7.200005-9.4 7.200005z" class="st34" fill="#3a434e"/><path d="M50.69963 18.965685c-5.2 2.9-14.6 4.7-18.1-1.5-3-5.4 2.1-9.6999996 7.8-9.9999996 5.7-.3 7.6 1.2 7.6 1.2s1.9-5.9 9.3-7.69999998c3.9-1 6-.1 6.2 1.19999998 0 0 3.6-.9 4 3.5 0 0 3.9-.4 3.1 5.1999996-.8 5.6-10.6 10.1-17.7 6.4 0 0-1.1 1.2-2.2 1.7z" class="st33" fill="#56606b"/><path d="M40.79963 21.665685c-2.7 0-4.8-.9-6.3-2.3-.7-1.1-.8-2.9-.3-4.3.8-1.9 2.6-3.3 4.6-3.7 1.2-.2 2.6-.4 3.9-.4 3.3 0 6.2.8 7.3 1.9l.6.6.3-.7s.7-2 2.2-2c.2 0 .5.1.8.2 2.2.9 3.5 1.2 4.6 1.2.5 0 .9-.1 1.3-.2.1-.1.4-.1.6-.1.6 0 1.5.3 1.8.8.2.3.2.6.1 1l-.2.6h.7c.4 0 1.4.2 1.8.9.2.4.2 1-.2 1.7-1.8.8-3.8 1.2-5.7 1.2-2 0-4 0-5.6-.8 0 0-1.2 1.3-2.2 1.8-3.1 1.6-7 2.6-10.1 2.6z" class="st34" fill="#3a434e" fill-opacity=".94117647"/><path d="M61.79963 18.66569c-3.1.5-6.3.1-8.9-1.5.7.2 1.5.4 2.2.5.7.2 1.4.2 2.2.3.7.1 1.4 0 2.2 0 .7-.1 1.4-.1 2.2-.2h.1c.3 0 .5.1.6.4-.1.2-.3.5-.6.5z"/><path d="M37.59963 21.26569c-2.4-.4-4.8-2.1-5.7-4.5-.5-1.2-.7-2.6-.3-3.9.3-1.3 1.1-2.4 2.1-3.3 2-1.7 4.6-2.5 7.1-2.6 1.3-.1 2.5 0 3.8.1.6.1 1.3.2 1.9.4.6.2 1.2.4 1.9.8l-.8.2c.6-1.6 1.6-3 2.8-4.2 1.2-1.2 2.6-2.2 4.1-2.9 1.5-.7 3.2-1.1 4.8-1.3.8-.1 1.7-.1 2.6.1.4.1.9.3 1.3.6s.7.8.8 1.3l-.6-.4c.6-.1 1.2-.1 1.7 0 .6.1 1.1.4 1.6.8.4.4.7.9.9 1.5.1.3.2.5.2.8l.1.8-.5-.4c1 0 1.9.3 2.6.9.7.7 1 1.6 1.1 2.5.1.9 0 1.7-.1 2.5-.2.9-.5 1.7-1 2.4-.9 1.4-2.2 2.5-3.7 3.4-1.4.9-3 1.4-4.6 1.8-.3.1-.5-.1-.6-.4-.1-.3.1-.5.4-.6 1.5-.3 3-.9 4.3-1.7 1.3-.8 2.5-1.8 3.3-3.1.4-.6.7-1.3.8-2 .1-.7.2-1.5.1-2.2-.1-.7-.3-1.4-.8-1.9-.5-.4-1.2-.7-1.8-.7-.3 0-.5-.2-.5-.4l-.1-.7c-.1-.2-.1-.4-.2-.7-.2-.4-.4-.8-.7-1.1-.3-.3-.7-.5-1.1-.6-.4-.1-.9-.1-1.3 0-.3.1-.5-.1-.6-.4-.1-.5-.7-.9-1.4-1.1-.7-.2-1.5-.2-2.2-.1-1.5.2-3.1.6-4.5 1.2-1.4.7-2.7 1.6-3.8 2.7-1.1 1.1-2 2.5-2.5 3.8-.1.3-.4.4-.6.3h-.1c-.4-.2-1-.5-1.5-.6-.6-.1-1.2-.2-1.7-.3-1.2-.1-2.4-.2-3.6-.1-2.4.1-4.7.8-6.5 2.3-.9.7-1.6 1.7-1.9 2.8-.3 1.1-.2 2.3.2 3.4s1.1 2.1 1.9 2.9c.6.9 1.7 1.5 2.9 1.9z"/><path d="M63.49963 2.1656854c0 3.5-2.6 5.5-4.3 6.1m8.3-2.6c.2 3.4-3.3 5.1999996-3.3 5.1999996" class="st7" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"/><path d="M90.29963 84.765685c2.6 2.3 3 4.3-2.4 4.8-5.3.5-25.7 2.4-28.2 2.6-2.4.3-3.4 1.7-3.4 2.8 0 1.1.5 3.2 4 3.1 3.4-.1 23.8-1.5 30.4-2.4 6.6-.8 14.4-2.4 13.4-9s-5.4-8.7-5.4-8.7l-8.4 6.8z" class="st37" fill="#737039" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"/><path d="M90.29963 84.765685c2.6 2.3 3 4.3-2.4 4.8-5.3.5-25.7 2.4-28.2 2.6-2.4.3-3.4 1.7-3.4 2.8 0 1.1.5 3.2 4 3.1 3.4-.1 23.8-1.5 30.4-2.4 6.6-.8 13.8-2.3 13.4-9-.3-5.5-3.1-7-4.4-8.1-.5-.1-1-.1-1.6-.2l-7.8 6.4z" fill="#625d28" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"/><path d="M102.69963 64.665685c5.4-.1 10.3-1.9 12.2-6.5 1.9-4.6 8.7-10.1 14.2-2.1 5.4 8.1 6.6 17.3 2.8 23.7-3.8 6.5-12.1 3.5-14.9-.5-2.7-4-8.6-2.9-14.5-2.7-5.9.2.2-11.9.2-11.9z" class="st37" fill="#737039" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"/><path d="M65.89963 54.865685s10.2 21.3 13.5 26.8c3.2 5.5 12.9 6.2 17.4 3.5 4.5-2.7 7.3-7.3 8-15.1.7-7.9-2.4-14.9-10-15.2-7.6-.3-11.9 7.6-12.1 13.7" class="st36" fill="#53606c" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"/><path d="M65.89963 54.865685s10.2 21.3 13.5 26.8c3.2 5.5 12.9 6.2 17.4 3.5 4.5-2.7 7.3-7.3 8-15.1.7-7.9-2.4-14.9-10-15.2-7.6-.3-11.9 7.6-12.1 13.7" class="st36" fill="#56606b" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"/><path d="M90.19963 86.165685c-3.7 0-8.3-1.3-10.4-4.8-.9-1.5-2.4-4.3-4.4-8.4l5.9-.1c4 7.4 5.9 9.8 8 9.8 3.9 0 6-3.4 6.9-9.5.2-1.2.3-2.3.4-3.4.5-4.6.9-7.2 3.4-7.5.3 0 .6-.1.9-.1 2.1 0 2.5 1.2 3.1 2.8.1.2.2.5.2.7.1 1.3.1 2.7 0 4.2-.7 7.3-3.1 11.9-7.7 14.7-1.6 1-3.9 1.6-6.3 1.6z" class="st34" fill="#3a434e"/><path d="M89.19963 63.86569l-.3 6.6c-.1 1.1-.2 2.2-.4 3.3-.1.6-.3 1.1-.5 1.7-.3.5-.6 1.1-1.2 1.5-.2.1-.5.1-.7-.2-.1-.2-.1-.5.2-.7.7-.4 1.1-1.5 1.3-2.5.2-1 .4-2.1.5-3.2.2-2.2.3-4.4.5-6.6 0-.2.2-.3.3-.3.2.1.3.3.3.4z"/><path d="M52.29963 68.665685c-6.3.6-11.1 3.9-10 10.7 1.1 6.8 7.6 8.1 16 7.7 8.4-.4 26.4-1.3 26.4-1.3s-3.3-1.7-4.8-3.3c-.5-.6-1-1.4-1.6-2.5-1.6.1-15.5.8-22.7 1-3.4.1-3.8-1.2-3.9-1.8-.3-1.2.5-2.7 2.8-2.8 3.1-.2 10.8-.7 21.4-.7h11.5s.9-.3 1-9.1c0-.1-29.8 1.5-36.1 2.1z" class="st37" fill="#737039" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"/><path d="M56.19963 86.665685c-8.8 0-12.6-2.3-13.5-7.4 0-.1 0-.2-.1-.4 1.2 1.5 3.5 2.7 5.6 2.7 1.4 0 2.6-.5 3.6-1.5.5.7 1.4 1.4 3.7 1.4h.4c6.9-.2 19.5-.8 22.1-.9.6 1.1 1.2 1.9 1.6 2.4.9 1 2.4 1.9 3.6 2.5-5 .3-18.1.9-24.7 1.2h-2.3z" class="st39" fill="#625d28"/><path d="M44.09963 57.865685c-2.2-.6-5.8-8.3-8.7-8.7-2.9-.3-6.6 1.6-3.2 8.5 3.4 6.9 8 10 14.3 8.2 6.3-1.8 12.7-5.1 14.5-8.3 1.8-3.2-.6-6.2-4.8-4.3-4.1 1.7-9.9 5.2-12.1 4.6z" fill="#b3bfcd" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"/><path d="M43.09963 65.865685c-4.3 0-7.7-2.7-10.4-8.4-1.4-2.8-1.7-5.1-.8-6.4.8-1.3 2.3-1.4 2.9-1.4h.6c.4 0 .8.2 1.2.6-.7.1-1.3.5-1.6 1.2-.6 1.2-.3 2.9.9 4.7l.4.6c2.1 3.1 4.1 6 7.8 6 .9 0 1.9-.2 2.9-.6 5.6-2 9.4-3.6 11.1-5.4 1.2-1.3 1.9-2.6 1.7-3.6.5.2.9.5 1.1.9.5.8.4 1.9-.2 3-1.6 2.8-7.4 6.2-14.2 8.1-1.2.5-2.3.7-3.4.7z" class="st35" fill="#93a1b5"/><path d="M13.89963 107.66569c-.1 5.1 1.3 10.2 2.3 14.8 1.3 5.5 1.3 10.1 5.2 10.7 3.9.6 10.1.9 14.4 0 4.3-.9 4.1-5.2 4.5-8.2.4-3.1 0-10.7 0-10.7s-1.1-1.4-3-1.9" class="st34" fill="#3a434e"/><path d="M14.39963 107.66569c-.1 5.2 1.3 10.3 2.5 15.4l.8 3.9c.3 1.3.6 2.5 1.1 3.6.3.5.6 1 1.1 1.4.5.3 1 .5 1.6.6 1.3.2 2.6.3 3.9.4 2.6.2 5.2.2 7.8 0 1.3-.1 2.6-.3 3.7-.7 1.1-.4 1.9-1.4 2.3-2.6.4-1.2.5-2.4.7-3.7.1-.7.2-1.3.2-1.9.1-.6.1-1.3.1-1.9 0-2.6 0-5.2-.2-7.8l.1.3c-.1-.2-.3-.4-.5-.6l-.6-.6c-.4-.4-.9-.7-1.5-.9-.1 0-.1-.1-.1-.2s.1-.1.2-.1c.6.1 1.2.3 1.7.6.3.1.5.3.8.5.3.2.5.4.8.6.1.1.1.2.1.2.1 2.6.2 5.3.2 7.9 0 .7 0 1.3-.1 2s-.2 1.3-.2 2c-.1 1.3-.3 2.7-.7 4-.5 1.3-1.5 2.6-2.8 3.1-1.4.6-2.7.7-4 .8-2.7.2-5.3.2-8 0-1.3-.1-2.6-.2-4-.4-.7-.1-1.4-.4-2-.8-.6-.5-1-1.1-1.4-1.7-.6-1.3-.9-2.6-1.2-3.9l-.8-3.9c-1.1-5.1-2.6-10.3-2.5-15.6 0-.3.2-.5.5-.5.2 0 .4.2.4.5z"/><path d="M68.19963 86.665685l.4 4.6s.3 1.5 2.4 1.5c2.1-.1 2.2-2 2.2-2l-.1-4.5-4.9.4z" class="st37" fill="#737039" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"/><path d="M110.49963 71.465685c-.5 1.8.5 2.9 3.8 4.6 3.3 1.8 4 5.1 8.2 6 4.3.9 8.2-4.5 3.8-10.1-4.5-5.6-14.1-6.5-15.8-.5z" class="st39" fill="#625d28"/><circle r="1.7" cy="57.765686" cx="126.09963" fill="#99988c"/><path d="M17.39963 115.26569s.8 3.9 1.1 6.3c.3 2.4.9 3.8 5.9 3.2 5-.6 4.9-1.5 5.1-6.4.2-4.9-.1-7.4-3.7-7.6-3.6-.2-9.1.7-8.4 4.5z" class="st33" fill="#56606b"/><path fill="#3a434e" class="st34" d="M11.19963 40.565685c-2.7000002 5.1-2.7000002 7.7-.5 8.5 2.2.8 4.1.7 6.4-3 0 0 2 .7 4.9-4.1.9-1.5-.7-2.6-.7-2.6s-4.8 1.3-7.1-5l-3 6.2z"/></svg>
\ No newline at end of file
diff --git a/app/javascript/flavours/blobfox/images/elephant_ui_working.svg b/app/javascript/flavours/blobfox/images/elephant_ui_working.svg
new file mode 100644
index 00000000000000..8ba475db0a07c4
--- /dev/null
+++ b/app/javascript/flavours/blobfox/images/elephant_ui_working.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 124.12477 127.91685" width="124.12476" height="127.91685"><path d="M72.584191 46.815676c-2.3-2.2-4.2-2.5-6.6-.6-2.4 1.9-2.1 4.8.9 7.6 3.1 2.9 4.7 4.1 6.7 5 2.1.9 5.4 2.5 10.5-2s10.2-11.1 9.4-14.7c-.8-3.6-4.1-1.8-6.8 1.2s-3.7 4-5.4 5.2c-1.5 1.3-3.8 3-8.7-1.7z" class="st0" style="fill:#93a1b5;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;fill-opacity:1"/><path d="M116.384191 75.015676c0 6.3-3.9 9.8-9.1 9.8-5.3 0-9.9-3.5-9.9-9.8 0-6.3 4.3-10.3 9.5-10.3s9.5 4 9.5 10.3z" style="fill:#3a434e;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;fill-opacity:1"/><path d="M54.184191 16.615676c-23 1.2-30.5 14.1-32.8 27.8-3 18.2-8.2 44.2-9.2 53.2s-1 16 6 22 11 5 23 7 19 0 20-8l16.8-1.1s14.5 5.5 18.8 6.9c4.3 1.4 10.6.5 12.1-7.1s.2-12.5-6.6-14.4c-6.8-1.9-10.6-2.9-10.6-2.9l4.4-30.1s17.4 1.6 22.6-20c0 0 3.9 1.1 4.8-2.8.9-3.9-2.6-6.2-5.6-4.8l-2.5-1s-.2-3.8-3.5-4.2c-2.1-.2-6 3.4-3 7.4 0 0-3.4 8.9-12 7.8-8.6-1.1-12.5-11.2-15-18.2s-10.7-18.3-27.7-17.5z" class="st2" style="fill:#56606b;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;fill-opacity:1"/><path d="M95.484191 69.915676c-.6 0-1.2 0-1.8-.1-4.2-.2-10.9-2.4-17-7.8-.7-1-.4-2-.2-2.5.5-1.1 1.7-1.8 3-1.8.8 0 1.5.3 2.2.8 3 2.2 7.8 5.1 13.8 5.1.9 0 1.8-.1 2.7-.2 7.2-1 12.1-5.8 14.3-9.9.6-1.2 1.3-2.5 1.8-3.5.2-.5.3-.7.6-.9.3-.2 1 .2 1 .2l2.1.8c-4.5 17.8-17.4 19.2-21.2 19.2h-1.3v.6z" class="st3" style="fill:#3a434e;opacity:.98;fill-opacity:1"/><path d="M48.884191 126.915676c-2.2 0-4.7-.2-7.6-.7-2.8-.5-5.1-.8-7.1-1-6.9-.9-10.3-1.3-15.6-5.9-7-6-6.8-13-5.8-21.6.3-2.3.8-5.9 1.7-11.2 3.1 1.4 6.1 2.2 8.7 2.2 3.1 0 5.4-1.2 6.6-3.4 1.6 1.9 6.9 7.3 13.3 7.3 1 0 1.9-.1 2.8-.4 3.5-1 19.8-2.1 46.9-3.4l-1.7 11.7.4.1s3.8 1 10.6 2.9c6.1 1.7 7.9 5.6 6.3 13.8-1.3 6.6-6.2 7.3-8.2 7.3-1.1 0-2.2-.2-3.3-.5-4.2-1.4-18.6-6.8-18.7-6.9h-.1l-17.3 1.2-.1.4c-.7 5.4-4.5 8.1-11.8 8.1z" class="st3" style="fill:#3a434e;fill-opacity:1"/><path d="M41.184191 103.415676c-3.8-1.4-6-1.4-7.7-1.4-1.8 0-4.6 3.3 1.4 5.4 6 2.1 10.3 3.4 10.3 3.4s1.8-2.1 3.5-2.9c1.6-.8 2.3-.9 2.3-.9l-9.8-3.6z" style="fill:#56606b;fill-opacity:1"/><path d="M27.584191 38.615676c1.2-5-2.1-8.2-5.7-9.2-3.5-1-8.4-1.7-13.9 6.9s-9.5 16.5-6.4 20.6c3.1 4.1 9.3 3.4 11.8-.8 0 0 5.7 3.8 9.5-4.2" class="st2" style="fill:#56606b;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;fill-opacity:1"/><path d="M10.884191 45.115676c-1.6 2.5-.8 5 2 5.9 2.7 1 5-1.5 6.5-3.8 1.6-2.3 3.6-5.9 3.6-5.9s-3.7 1.2-5.6-.2c-2-1.4-1.5-3.8-1.5-3.8l-5 7.8z" class="st3" style="fill:#3a434e;fill-opacity:1"/><path d="M22.684191 41.415676c-2.6 1.1-6.8.6-6.9-4.1 0 0-5.1 7.6-5.9 9.6" class="st5" style="fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10"/><path d="M67.584191 5.215676c0-3.4-3.9-2.6-3.9-2.6 0-1.2-2-3.5-8.5-.6-6.4 2.9-7.3 6-7.3 6-3.8-1.7-9.6-2.6-13.5.8-3.9 3.4-4.3 10 2.3 13.5 0 0 2.9.9 7.7-.4 4.8-1.3 7.7-3.3 7.7-3.3s3.7 2.3 9 .6c5.3-1.7 9.9-4.5 10.3-10.1.5-5.7-3.8-3.9-3.8-3.9z" style="fill:#56606b;fill-opacity:1"/><path d="M67.084191 16.315676c-1-5.5-7-3-7-3 .5-2.1-3-4.1-5.5-2.7-2.5 1.4-6.6-.1-6.6-.1-6.4-4.4-14.3-2.1-16.1 2-1.1 3.3.2 7.3 4.8 9.7 0 0 2.9.9 7.7-.4 4.8-1.3 7.7-3.3 7.7-3.3s3.7 2.3 9 .6c2.3-.6 4.3-1.5 6-2.8 0 .1 0 0 0 0z" style="fill:#3a434e;fill-opacity:1"/><path d="M36.684191 22.715676c-.1 0-.2 0-.2-.1-3.1-1.6-5-4.1-5.4-7-.3-2.7.8-5.4 3-7.3 3.9-3.3 9.5-2.8 13.6-1.1.5-1.1 2.2-3.5 7.3-5.8 4.5-2 6.9-1.5 8-.8.6.4.9.9 1.1 1.3.7-.1 2-.1 3 .7.5.4.9 1 1 1.8.7-.1 1.8-.2 2.7.4 1 .7 1.4 2.1 1.2 4.1-.4 5-3.9 8.5-10.7 10.6-5.5 1.7-9.3-.6-9.4-.7-.2-.1-.3-.5-.2-.7.1-.2.5-.3.7-.2 0 0 3.6 2.2 8.6.6 6.3-2 9.6-5.1 10-9.7.1-1.6-.2-2.8-.8-3.3-.9-.7-2.3-.1-2.3-.1-.2.1-.3 0-.5 0-.1-.1-.2-.2-.2-.4 0-.8-.2-1.3-.6-1.7-.9-.8-2.6-.4-2.7-.4-.1 0-.3 0-.4-.1-.1-.1-.2-.2-.2-.4 0-.3-.2-.7-.7-1-.6-.4-2.6-1.1-7.1.8-6.1 2.7-7 5.6-7 5.7 0 .1-.1.3-.3.3-.1.1-.3.1-.4 0-3.9-1.7-9.3-2.4-13 .8-1.9 1.7-2.9 4.1-2.6 6.4.3 2.5 2 4.7 4.8 6.2.2.1.3.4.2.7-.1.3-.3.4-.5.4z"/><path d="M40.584191 84.115676s6.3 16.8 7.1 19.3c.8 2.5 1.8 3.4 7.3 3 5.5-.4 6.7-21.5 6.7-21.5l-21.1-.8z" style="fill:#191b22;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;fill-opacity:1"/><path d="M51.084191 103.415676c-1.7-2.1-1.9-4.2-1.9-4.2l2-10.9 3-1.4-3.1 16.5zm33.9-35.3l-23.9 1.9 1.2 8 25.2 1.1 4.6-9c-2.3-.3-4.7-.9-7.1-2z" class="st9" style="fill:#191b22;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;fill-opacity:1"/><path d="M28.484191 82.915676c7.2 9.4 12.7 11.4 21.8 7.7 8.5-3.4 15.4-9 15.1-15-.3-6-2.1-10.3-9.1-9.8-2.3.2-6.8 2.8-9.6 4.4-1.8 1-4.2 2.2-6 .4-1.8-1.8-4.3-4.4-4.3-4.4" class="st2" style="fill:#56606b;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;fill-opacity:1"/><path d="M49.284191 80.515676c-.3-.2-.5-.4-.7-.6-1.2.7-2.3 1.3-2.8 1.6-2 1.2-3.8.5-4.7-.3-.9-.9-2.3-2.4-5.5-1.5-3.7 1-4.5 5.7-2.5 8.4 6.5 6.1 12.8 4.1 15.2 3.3 2.4-.8 6.3-2.7 6.3-2.7l.6-6c-2-.8-4.1-.9-5.9-2.2z" class="st3" style="fill:#3a434e;fill-opacity:1"/><path d="M28.484191 82.915676c7.2 9.4 12.7 11.4 21.8 7.7 8.5-3.4 15.4-9 15.1-15-.3-6-2.1-10.3-9.1-9.8-2.3.2-6.8 2.8-9.6 4.4-1.8 1-4.2 2.2-6 .4-1.8-1.8-4.3-4.4-4.3-4.4m35.4-8.6c6.5 8.3 15.5 12.5 21.8 12.7" class="st5" style="fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10"/><path d="M53.184191 104.415676c-1.6.1-2.7-1.1-2.4-2.7l4.9-25.9c.3-1.6 1.9-3 3.6-3.1l26.9-1.7c1.6-.1 3.6-1.4 4.4-2.9l.7-1.3c.7-1.5 2.7-2.8 4.4-2.9l4.5-.3c1.6-.1 3.1 1.1 3.3 2.8v.2c.2 1.6.5 3.7.7 4.6.2.9.3 3 .1 4.6l-2.2 21.3c-.2 1.6-1.7 3.1-3.3 3.3l-45.6 4z" style="fill:#191b22;fill-opacity:1"/><path d="M53.184191 104.415676c-1.6.1-2.7-1.1-2.4-2.7l4.9-25.9c.3-1.6 1.9-3 3.6-3.1l26.9-1.7c1.6-.1 3.6-1.4 4.4-2.9l.7-1.3c.7-1.5 2.7-2.8 4.4-2.9l4.5-.3c1.6-.1 3.1 1.1 3.3 2.8v.2c.2 1.6.5 3.7.7 4.6.2.9.3 3 .1 4.6l-2.2 21.3c-.2 1.6-1.7 3.1-3.3 3.3l-45.6 4z" class="st5" style="fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10"/><path d="M55.684191 105.915676c-1.6.1-2.3-.4-2-2l4.4-25.6c.3-1.6 1.9-3 3.6-3.1l26.9-1.7c1.6-.1 3.6-1.4 4.4-2.9l.7-1.3c.7-1.5 2.7-2.8 4.4-2.9l4.5-.3c1.6-.1 3.1 1.1 3.3 2.8v.2c.2 1.6 1.3 2.9 2.5 2.8 1.2-.1 1.9 1.2 1.6 2.8l-5.2 24.9c-.3 1.6-2 3.1-3.6 3.2l-45.5 3.1z" style="fill:#191b22;fill-opacity:1"/><path d="M53.184191 104.315676l4.9-26c.3-1.6 1.9-3 3.6-3.1l26.9-1.7c1.6-.1 3.6-1.4 4.4-2.9l.7-1.3c.7-1.5 2.7-2.8 4.4-2.9l4.5-.3c1.6-.1 3.1 1.1 3.3 2.8v.2c.2 1.6 1.3 2.9 2.5 2.8 1.2-.1 1.9 1.2 1.6 2.8l-5.2 24.9c-.3 1.6-2 3.1-3.6 3.2l-46.7 3.7m9.2-103.9c-.3 2.9-2.9 4.9-4.1 5.8m8-3.2c-.7 3.5-4 6-4 6" class="st5" style="fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10"/><path d="M39.884191 53.615676c-2.3-2.2-4.2-2.5-6.6-.6-2.4 1.9-2.1 4.8.9 7.6 3.1 2.9 4.7 4.1 6.7 5 2 .9 5.4 2.5 10.5-2s10.2-11.1 9.4-14.7c-.8-3.6-4.1-1.8-6.8 1.2s-3.7 4-5.4 5.2c-1.7 1.2-3.8 3-8.7-1.7z" class="st0" style="fill:#93a1b5;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;fill-opacity:1"/><path d="M44.384191 61.315676c-2.3 0-4.7-1.2-6.6-3.1-.9-.9-1.1-2-.7-2.9.3-.8 1.1-1.3 1.8-1.3.2 0 .5 0 .7.1 2.3 2.1 4.2 3.2 5.9 3.2 1.6 0 2.7-.8 3.5-1.4 1.7-1.3 2.8-2.3 5.5-5.3.9-1.1 1.9-1.9 2.7-2.4.3.2.6.4.7.8.2.8-.2 2-.7 2.7-.9 1.3-4.9 5.4-9 8.4-1.1.7-2.4 1.2-3.8 1.2z" style="fill:#b3bfcd;fill-opacity:1"/><path d="M45.784191 50.115676c-.7 0-1.4-.6-1.4-1.4v-6.1c0-.7.6-1.4 1.4-1.4.7 0 1.4.6 1.4 1.4v6.1c0 .8-.6 1.4-1.4 1.4z"/><path d="M61.184191 118.215676c.7-7.1-3.5-10.6-9.2-11.1-5.7-.5-10.2 6.8-9.1 13.1 1.1 6.3 6.7 7.2 6.7 7.2" class="st5" style="fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10"/><path d="M52.084191 107.515676c-2.2-.7-4.3-1.4-6.5-2.2-2.1-.8-4.3-1.5-6.4-2.3-.1 0-.2-.2-.1-.3 0-.1.2-.2.3-.2 2.2.6 4.4 1.3 6.5 1.9 2.2.7 4.3 1.3 6.5 2 .3.1.4.4.3.6-.1.4-.4.6-.6.5zm25.4 10.1c-.2-1.4-.2-2.9.1-4.2.2-1.4.6-2.7 1.1-4-.3 1.3-.5 2.7-.6 4.1 0 1.4.1 2.7.4 4 .1.3-.1.5-.3.6-.2.1-.6-.1-.7-.5 0 .1 0 .1 0 0z"/><path d="M104.284191 103.615676c-3.6-.7-8.5 2.1-9.5 9.7s2.1 10.7 5.3 11.6m15.1-83.5l-.39999 1.4m-1.90001-1.2c2.4 1.7 6.4 3.4 6.4 3.4m-1.6-2.6l-.60001 1.59999" class="st5" style="fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10"/><path d="M47.484191 79.215676c3.2 2.7 7 3.3 7 3.3" style="fill:none;stroke:#000;stroke-miterlimit:10"/><path d="M69.284191 24.315676c-3.5.4-2.7 2.9-1.2 3.5 1.5.6 3.7.1 4.3-1.6.4-1.6-1.3-2.1-3.1-1.9z" style="fill:#4f5862;fill-opacity:1"/></svg>
\ No newline at end of file
diff --git a/app/javascript/flavours/blobfox/images/glitch-preview.jpg b/app/javascript/flavours/blobfox/images/glitch-preview.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..fc5c4204359659e32ec8d758855891e297f5093c
GIT binary patch
literal 197277
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%0AZf$5*<oC
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
KIoqB2DEv269CTIy

literal 0
HcmV?d00001

diff --git a/app/javascript/flavours/blobfox/images/logo_warn_glitch.svg b/app/javascript/flavours/blobfox/images/logo_warn_glitch.svg
new file mode 100644
index 00000000000000..0fea63d2908969
--- /dev/null
+++ b/app/javascript/flavours/blobfox/images/logo_warn_glitch.svg
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   viewBox="0 0 216.41507 232.00976"
+   version="1.1"
+   id="svg6"
+   sodipodi:docname="logo_warn_blobfox.svg"
+   inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs10" />
+  <sodipodi:namedview
+     id="namedview8"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     showgrid="false"
+     inkscape:zoom="1.7951831"
+     inkscape:cx="-30.916067"
+     inkscape:cy="90.241493"
+     inkscape:window-width="1920"
+     inkscape:window-height="1011"
+     inkscape:window-x="0"
+     inkscape:window-y="32"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg6" />
+  <g
+     id="g2025">
+    <path
+       d="M211.80683 139.0875c-3.1825 16.36625-28.4925 34.2775-57.5625 37.74875-15.16 1.80875-30.0825 3.47125-45.99875 2.74125-26.0275-1.1925-46.565-6.2125-46.565-6.2125 0 2.53375.15625 4.94625.46875 7.2025 3.38375 25.68625 25.47 27.225 46.3925 27.9425 21.115.7225 39.91625-5.20625 39.91625-5.20625l.86875 19.09s-14.77 7.93125-41.08125 9.39c-14.50875.7975-32.52375-.365-53.50625-5.91875C9.23183 213.82 1.40558 165.31125.20808 116.09125c-.36375-14.61375-.14-28.39375-.14-39.91875 0-50.33 32.97625-65.0825 32.97625-65.0825C49.67058 3.45375 78.20308.2425 107.86433 0h.72875c29.66125.2425 58.21125 3.45375 74.8375 11.09 0 0 32.97625 14.7525 32.97625 65.0825 0 0 .4125 37.13375-4.6 62.915"
+       fill="#3088d4"
+       id="path2" />
+    <path
+       d="m 124.52893,137.75645 c 0,9.01375 -7.30875,16.32125 -16.3225,16.32125 -9.01375,0 -16.32125,-7.3075 -16.32125,-16.32125 0,-9.01375 7.3075,-16.3225 16.32125,-16.3225 9.01375,0 16.3225,7.30875 16.3225,16.3225"
+       fill="#ffffff"
+       id="path4"
+       sodipodi:nodetypes="csssc" />
+    <path
+       id="path1121"
+       d="m 108.20703,25.453125 c -9.013749,0 -16.322264,7.308516 -16.322264,16.322266 0,5.31808 2.555126,37.386806 6.492187,67.763669 4.100497,4.20028 15.890147,3.77063 19.660157,-0.01 3.9367,-30.375272 6.49219,-62.4364 6.49219,-67.753909 0,-9.01375 -7.30852,-16.322266 -16.32227,-16.322266 z"
+       style="fill:#ffffff"
+       sodipodi:nodetypes="ssccsss" />
+  </g>
+</svg>
diff --git a/app/javascript/flavours/blobfox/images/mbstobon-ui-0.png b/app/javascript/flavours/blobfox/images/mbstobon-ui-0.png
new file mode 100644
index 0000000000000000000000000000000000000000..25e1707c9932ea9091d2d794f3ca78b7f6b45978
GIT binary patch
literal 39646
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

literal 0
HcmV?d00001

diff --git a/app/javascript/flavours/blobfox/images/mbstobon-ui-1.png b/app/javascript/flavours/blobfox/images/mbstobon-ui-1.png
new file mode 100644
index 0000000000000000000000000000000000000000..64cf3cbf322e9a636e9fcbefa1fbe0786e27172e
GIT binary patch
literal 43609
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
BhnD~V

literal 0
HcmV?d00001

diff --git a/app/javascript/flavours/blobfox/images/mbstobon-ui-2.png b/app/javascript/flavours/blobfox/images/mbstobon-ui-2.png
new file mode 100644
index 0000000000000000000000000000000000000000..b767a9122a05a7154fb0c6d1cd3bf9dc81627e23
GIT binary patch
literal 40376
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

literal 0
HcmV?d00001

diff --git a/app/javascript/flavours/blobfox/images/mbstobon-ui-3.png b/app/javascript/flavours/blobfox/images/mbstobon-ui-3.png
new file mode 100644
index 0000000000000000000000000000000000000000..a1fb642a003b2e2b652b1e6022a84f08dc76105f
GIT binary patch
literal 32449
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%0Arc|r&DC@j0pAQE7yV9T$8x=q-T1FG
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
Cr$1f*

literal 0
HcmV?d00001

diff --git a/app/javascript/flavours/blobfox/images/wave-drawer-glitched.png b/app/javascript/flavours/blobfox/images/wave-drawer-glitched.png
new file mode 100644
index 0000000000000000000000000000000000000000..2290663db49ef6336a1bcdcd39d81811fcf4b5a8
GIT binary patch
literal 3544
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
O0lJz-8rACdvHt>8r?yZ4

literal 0
HcmV?d00001

diff --git a/app/javascript/flavours/blobfox/images/wave-drawer.png b/app/javascript/flavours/blobfox/images/wave-drawer.png
new file mode 100644
index 0000000000000000000000000000000000000000..ca9f9e1d85d222e7bf80649c55783db928cc79d4
GIT binary patch
literal 3269
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
D7raE)

literal 0
HcmV?d00001

diff --git a/app/javascript/flavours/blobfox/initial_state.js b/app/javascript/flavours/blobfox/initial_state.js
new file mode 100644
index 00000000000000..52066aac420546
--- /dev/null
+++ b/app/javascript/flavours/blobfox/initial_state.js
@@ -0,0 +1,144 @@
+// @ts-check
+
+
+/**
+ * @typedef {[code: string, name: string, localName: string]} InitialStateLanguage
+ */
+
+/**
+ * @typedef InitialStateMeta
+ * @property {string} access_token
+ * @property {boolean=} advanced_layout
+ * @property {boolean} auto_play_gif
+ * @property {boolean} activity_api_enabled
+ * @property {string} admin
+ * @property {boolean=} boost_modal
+ * @property {boolean=} favourite_modal
+ * @property {boolean} crop_images
+ * @property {boolean=} delete_modal
+ * @property {boolean=} disable_swiping
+ * @property {string=} disabled_account_id
+ * @property {string} display_media
+ * @property {string} domain
+ * @property {boolean=} expand_spoilers
+ * @property {boolean} limited_federation_mode
+ * @property {string} locale
+ * @property {string | null} mascot
+ * @property {number} max_reactions
+ * @property {string=} me
+ * @property {string=} moved_to_account_id
+ * @property {string=} owner
+ * @property {boolean} profile_directory
+ * @property {boolean} registrations_open
+ * @property {boolean} reduce_motion
+ * @property {string} repository
+ * @property {boolean} search_enabled
+ * @property {boolean} trends_enabled
+ * @property {boolean} single_user_mode
+ * @property {string} source_url
+ * @property {string} streaming_api_base_url
+ * @property {boolean} timeline_preview
+ * @property {string} title
+ * @property {boolean} show_trends
+ * @property {boolean} trends_as_landing_page
+ * @property {boolean} unfollow_modal
+ * @property {boolean} use_blurhash
+ * @property {boolean=} use_pending_items
+ * @property {string} version
+ * @property {number} visible_reactions
+ * @property {string} sso_redirect
+ * @property {number} visible_reactions
+ * @property {boolean} translation_enabled
+ * @property {string} status_page_url
+ * @property {boolean} system_emoji_font
+ * @property {string} default_content_type
+ */
+
+/** @type {string} */
+const initialPath = document.querySelector("head meta[name=initialPath]")?.getAttribute("content") ?? '';
+/** @type {boolean} */
+export const hasMultiColumnPath = initialPath === '/'
+  || initialPath === '/getting-started'
+  || initialPath === '/home'
+  || initialPath.startsWith('/deck');
+
+/**
+ * @typedef InitialState
+ * @property {Record<string, import("./api_types/accounts").ApiAccountJSON>} accounts
+ * @property {InitialStateLanguage[]} languages
+ * @property {boolean=} critical_updates_pending
+ * @property {InitialStateMeta} meta
+ * @property {object} local_settings
+ * @property {number} max_toot_chars
+ * @property {number} poll_limits
+ * @property {number} max_reactions
+ */
+
+const element = document.getElementById('initial-state');
+/** @type {InitialState | undefined} */
+const initialState = element?.textContent && JSON.parse(element.textContent);
+
+// Glitch-soc-specific “local settings”
+if (initialState) {
+  try {
+    // @ts-expect-error
+    initialState.local_settings = JSON.parse(localStorage.getItem('mastodon-settings'));
+  } catch (e) {
+    initialState.local_settings = {};
+  }
+}
+
+/**
+ * @template {keyof InitialStateMeta} K
+ * @param {K} prop
+ * @returns {InitialStateMeta[K] | undefined}
+ */
+const getMeta = (prop) => initialState?.meta && initialState.meta[prop];
+
+export const activityApiEnabled = getMeta('activity_api_enabled');
+export const autoPlayGif = getMeta('auto_play_gif');
+export const boostModal = getMeta('boost_modal');
+export const cropImages = getMeta('crop_images');
+export const deleteModal = getMeta('delete_modal');
+export const disableSwiping = getMeta('disable_swiping');
+export const disabledAccountId = getMeta('disabled_account_id');
+export const displayMedia = getMeta('display_media');
+export const domain = getMeta('domain');
+export const expandSpoilers = getMeta('expand_spoilers');
+export const forceSingleColumn = !getMeta('advanced_layout');
+export const limitedFederationMode = getMeta('limited_federation_mode');
+export const mascot = getMeta('mascot');
+export const maxReactions = (initialState && initialState.max_reactions) || 1;
+export const me = getMeta('me');
+export const movedToAccountId = getMeta('moved_to_account_id');
+export const owner = getMeta('owner');
+export const profile_directory = getMeta('profile_directory');
+export const reduceMotion = getMeta('reduce_motion');
+export const registrationsOpen = getMeta('registrations_open');
+export const repository = getMeta('repository');
+export const searchEnabled = getMeta('search_enabled');
+export const trendsEnabled = getMeta('trends_enabled');
+export const showTrends = getMeta('show_trends');
+export const singleUserMode = getMeta('single_user_mode');
+export const source_url = getMeta('source_url');
+export const timelinePreview = getMeta('timeline_preview');
+export const title = getMeta('title');
+export const trendsAsLanding = getMeta('trends_as_landing_page');
+export const unfollowModal = getMeta('unfollow_modal');
+export const useBlurhash = getMeta('use_blurhash');
+export const usePendingItems = getMeta('use_pending_items');
+export const version = getMeta('version');
+export const visibleReactions = getMeta('visible_reactions');
+export const languages = initialState?.languages;
+export const criticalUpdatesPending = initialState?.critical_updates_pending;
+export const statusPageUrl = getMeta('status_page_url');
+export const sso_redirect = getMeta('sso_redirect');
+
+// Glitch-soc-specific settings
+export const maxChars = (initialState && initialState.max_toot_chars) || 500;
+export const favouriteModal = getMeta('favourite_modal');
+export const pollLimits = (initialState && initialState.poll_limits);
+export const defaultContentType = getMeta('default_content_type');
+export const useSystemEmojiFont = getMeta('system_emoji_font');
+
+export default initialState;
diff --git a/app/javascript/flavours/blobfox/is_mobile.ts b/app/javascript/flavours/blobfox/is_mobile.ts
new file mode 100644
index 00000000000000..7f339e287bfd61
--- /dev/null
+++ b/app/javascript/flavours/blobfox/is_mobile.ts
@@ -0,0 +1,34 @@
+import { supportsPassiveEvents } from 'detect-passive-events';
+
+import { forceSingleColumn, hasMultiColumnPath } from './initial_state';
+
+const LAYOUT_BREAKPOINT = 630;
+
+export const isMobile = (width: number) => width <= LAYOUT_BREAKPOINT;
+
+export const transientSingleColumn = !forceSingleColumn && !hasMultiColumnPath;
+
+export type LayoutType = 'mobile' | 'single-column' | 'multi-column';
+export const layoutFromWindow = (): LayoutType => {
+  if (isMobile(window.innerWidth)) {
+    return 'mobile';
+  } else if (!forceSingleColumn && !transientSingleColumn) {
+    return 'multi-column';
+  } else {
+    return 'single-column';
+  }
+};
+
+const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
+
+let userTouching = false;
+
+const touchListener = () => {
+  userTouching = true;
+
+  window.removeEventListener('touchstart', touchListener);
+};
+
+window.addEventListener('touchstart', touchListener, listenerOptions);
+
+export const isUserTouching = () => userTouching;
diff --git a/app/javascript/flavours/blobfox/load_keyboard_extensions.js b/app/javascript/flavours/blobfox/load_keyboard_extensions.js
new file mode 100644
index 00000000000000..2dd0e45fa714c4
--- /dev/null
+++ b/app/javascript/flavours/blobfox/load_keyboard_extensions.js
@@ -0,0 +1,16 @@
+// On KaiOS, we may not be able to use a mouse cursor or navigate using Tab-based focus, so we install
+// special left/right focus navigation keyboard listeners, at least on public pages (i.e. so folks
+// can at least log in using KaiOS devices).
+
+function importArrowKeyNavigation() {
+  return import(/* webpackChunkName: "arrow-key-navigation" */ 'arrow-key-navigation');
+}
+
+export default function loadKeyboardExtensions() {
+  if (/KAIOS/.test(navigator.userAgent)) {
+    return importArrowKeyNavigation().then(arrowKeyNav => {
+      arrowKeyNav.register();
+    });
+  }
+  return Promise.resolve();
+}
diff --git a/app/javascript/flavours/blobfox/main.jsx b/app/javascript/flavours/blobfox/main.jsx
new file mode 100644
index 00000000000000..c6ee5617532f84
--- /dev/null
+++ b/app/javascript/flavours/blobfox/main.jsx
@@ -0,0 +1,47 @@
+import { createRoot } from 'react-dom/client';
+
+import { setupBrowserNotifications } from 'flavours/blobfox/actions/notifications';
+import Mastodon from 'flavours/blobfox/containers/mastodon';
+import { me } from 'flavours/blobfox/initial_state';
+import * as perf from 'flavours/blobfox/performance';
+import ready from 'flavours/blobfox/ready';
+import { store } from 'flavours/blobfox/store';
+
+/**
+ * @returns {Promise<void>}
+ */
+function main() {
+  perf.start('main()');
+
+  return ready(async () => {
+    const mountNode = document.getElementById('mastodon');
+    const props = JSON.parse(mountNode.getAttribute('data-props'));
+
+    const root = createRoot(mountNode);
+    root.render(<Mastodon {...props} />);
+    store.dispatch(setupBrowserNotifications());
+
+    if (process.env.NODE_ENV === 'production' && me && 'serviceWorker' in navigator) {
+      const { Workbox } = await import('workbox-window');
+      const wb = new Workbox('/sw.js');
+      /** @type {ServiceWorkerRegistration} */
+      let registration;
+
+      try {
+        registration = await wb.register();
+      } catch (err) {
+        console.error(err);
+      }
+
+      if (registration && 'Notification' in window && Notification.permission === 'granted') {
+        const registerPushNotifications = await import('flavours/blobfox/actions/push_notifications');
+
+        store.dispatch(registerPushNotifications.register());
+      }
+    }
+
+    perf.stop('main()');
+  });
+}
+
+export default main;
diff --git a/app/javascript/flavours/blobfox/models/account.ts b/app/javascript/flavours/blobfox/models/account.ts
new file mode 100644
index 00000000000000..02541f1f93fab0
--- /dev/null
+++ b/app/javascript/flavours/blobfox/models/account.ts
@@ -0,0 +1,149 @@
+import type { RecordOf } from 'immutable';
+import { List, Record as ImmutableRecord } from 'immutable';
+
+import escapeTextContentForBrowser from 'escape-html';
+
+import type {
+  ApiAccountFieldJSON,
+  ApiAccountRoleJSON,
+  ApiAccountJSON,
+} from 'flavours/blobfox/api_types/accounts';
+import type { ApiCustomEmojiJSON } from 'flavours/blobfox/api_types/custom_emoji';
+import emojify from 'flavours/blobfox/features/emoji/emoji';
+import { unescapeHTML } from 'flavours/blobfox/utils/html';
+
+import { CustomEmojiFactory } from './custom_emoji';
+import type { CustomEmoji } from './custom_emoji';
+
+// AccountField
+interface AccountFieldShape extends Required<ApiAccountFieldJSON> {
+  name_emojified: string;
+  value_emojified: string;
+  value_plain: string | null;
+}
+
+type AccountField = RecordOf<AccountFieldShape>;
+
+const AccountFieldFactory = ImmutableRecord<AccountFieldShape>({
+  name: '',
+  value: '',
+  verified_at: null,
+  name_emojified: '',
+  value_emojified: '',
+  value_plain: null,
+});
+
+// AccountRole
+export type AccountRoleShape = ApiAccountRoleJSON;
+export type AccountRole = RecordOf<AccountRoleShape>;
+
+const AccountRoleFactory = ImmutableRecord<AccountRoleShape>({
+  color: '',
+  id: '',
+  name: '',
+});
+
+// Account
+export interface AccountShape
+  extends Required<
+    Omit<ApiAccountJSON, 'emojis' | 'fields' | 'roles' | 'moved'>
+  > {
+  emojis: List<CustomEmoji>;
+  fields: List<AccountField>;
+  roles: List<AccountRole>;
+  display_name_html: string;
+  note_emojified: string;
+  note_plain: string | null;
+  hidden: boolean;
+  moved: string | null;
+}
+
+export type Account = RecordOf<AccountShape>;
+
+export const accountDefaultValues: AccountShape = {
+  acct: '',
+  avatar: '',
+  avatar_static: '',
+  bot: false,
+  created_at: '',
+  discoverable: false,
+  display_name: '',
+  display_name_html: '',
+  emojis: List<CustomEmoji>(),
+  fields: List<AccountField>(),
+  group: false,
+  header: '',
+  header_static: '',
+  id: '',
+  last_status_at: '',
+  locked: false,
+  noindex: false,
+  note: '',
+  note_emojified: '',
+  note_plain: 'string',
+  roles: List<AccountRole>(),
+  uri: '',
+  url: '',
+  username: '',
+  followers_count: 0,
+  following_count: 0,
+  statuses_count: 0,
+  hidden: false,
+  suspended: false,
+  memorial: false,
+  limited: false,
+  moved: null,
+};
+
+const AccountFactory = ImmutableRecord<AccountShape>(accountDefaultValues);
+
+type EmojiMap = Record<string, ApiCustomEmojiJSON>;
+
+function makeEmojiMap(emojis: ApiCustomEmojiJSON[]) {
+  return emojis.reduce<EmojiMap>((obj, emoji) => {
+    obj[`:${emoji.shortcode}:`] = emoji;
+    return obj;
+  }, {});
+}
+
+function createAccountField(
+  jsonField: ApiAccountFieldJSON,
+  emojiMap: EmojiMap,
+) {
+  return AccountFieldFactory({
+    ...jsonField,
+    name_emojified: emojify(
+      escapeTextContentForBrowser(jsonField.name),
+      emojiMap,
+    ),
+    value_emojified: emojify(jsonField.value, emojiMap),
+    value_plain: unescapeHTML(jsonField.value),
+  });
+}
+
+export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) {
+  const { moved, ...accountJSON } = serverJSON;
+
+  const emojiMap = makeEmojiMap(accountJSON.emojis);
+
+  const displayName =
+    accountJSON.display_name.trim().length === 0
+      ? accountJSON.username
+      : accountJSON.display_name;
+
+  return AccountFactory({
+    ...accountJSON,
+    moved: moved?.id,
+    fields: List(
+      serverJSON.fields.map((field) => createAccountField(field, emojiMap)),
+    ),
+    emojis: List(serverJSON.emojis.map((emoji) => CustomEmojiFactory(emoji))),
+    roles: List(serverJSON.roles?.map((role) => AccountRoleFactory(role))),
+    display_name_html: emojify(
+      escapeTextContentForBrowser(displayName),
+      emojiMap,
+    ),
+    note_emojified: emojify(accountJSON.note, emojiMap),
+    note_plain: unescapeHTML(accountJSON.note),
+  });
+}
diff --git a/app/javascript/flavours/blobfox/models/custom_emoji.ts b/app/javascript/flavours/blobfox/models/custom_emoji.ts
new file mode 100644
index 00000000000000..736b6b620c31c9
--- /dev/null
+++ b/app/javascript/flavours/blobfox/models/custom_emoji.ts
@@ -0,0 +1,15 @@
+import type { RecordOf } from 'immutable';
+import { Record } from 'immutable';
+
+import type { ApiCustomEmojiJSON } from 'flavours/blobfox/api_types/custom_emoji';
+
+type CustomEmojiShape = Required<ApiCustomEmojiJSON>; // no changes from server shape
+export type CustomEmoji = RecordOf<CustomEmojiShape>;
+
+export const CustomEmojiFactory = Record<CustomEmojiShape>({
+  shortcode: '',
+  static_url: '',
+  url: '',
+  category: '',
+  visible_in_picker: false,
+});
diff --git a/app/javascript/flavours/blobfox/models/relationship.ts b/app/javascript/flavours/blobfox/models/relationship.ts
new file mode 100644
index 00000000000000..a2391683ff64f3
--- /dev/null
+++ b/app/javascript/flavours/blobfox/models/relationship.ts
@@ -0,0 +1,29 @@
+import type { RecordOf } from 'immutable';
+import { Record } from 'immutable';
+
+import type { ApiRelationshipJSON } from 'flavours/blobfox/api_types/relationships';
+
+type RelationshipShape = Required<ApiRelationshipJSON>; // no changes from server shape
+export type Relationship = RecordOf<RelationshipShape>;
+
+const RelationshipFactory = Record<RelationshipShape>({
+  blocked_by: false,
+  blocking: false,
+  domain_blocking: false,
+  endorsed: false,
+  followed_by: false,
+  following: false,
+  id: '',
+  languages: null,
+  muting_notifications: false,
+  muting: false,
+  note: '',
+  notifying: false,
+  requested_by: false,
+  requested: false,
+  showing_reblogs: false,
+});
+
+export function createRelationship(attributes: Partial<RelationshipShape>) {
+  return RelationshipFactory(attributes);
+}
diff --git a/app/javascript/flavours/blobfox/names.yml b/app/javascript/flavours/blobfox/names.yml
new file mode 100644
index 00000000000000..4f743d6248edee
--- /dev/null
+++ b/app/javascript/flavours/blobfox/names.yml
@@ -0,0 +1,40 @@
+en:
+  flavours:
+    blobfox:
+      description: The default flavour for blobfoxSoc instances.
+      name: blobfox Edition
+  skins:
+    blobfox:
+      default: Default
+cs:
+  flavours:
+    blobfox:
+      description: Výchozí rozhraní instancí blobfoxSoc.
+      name: blobfox
+  skins:
+    blobfox:
+      default: Výchozí
+pl:
+  flavours:
+    blobfox:
+      description: Domyślny motyw instancji blobfoxSoc.
+  skins:
+    blobfox:
+      default: Domyślny
+es:
+  flavours:
+    blobfox:
+      description: El diseño predeterminado para las instancias con blobfoxSoc.
+      name: blobfoxsoc
+  skins:
+    blobfox:
+      default: Predeterminado
+
+ja:
+  flavours:
+    blobfox:
+      description: blobfoxSocインスタンスのデフォルトフレーバーです。
+      name: blobfox Edition
+  skins:
+    blobfox:
+      default: デフォルト
diff --git a/app/javascript/flavours/blobfox/packs/admin.jsx b/app/javascript/flavours/blobfox/packs/admin.jsx
new file mode 100644
index 00000000000000..9a33fe31e14461
--- /dev/null
+++ b/app/javascript/flavours/blobfox/packs/admin.jsx
@@ -0,0 +1,25 @@
+import 'packs/public-path';
+import { createRoot } from 'react-dom/client';
+
+import ready from 'flavours/blobfox/ready';
+
+ready(() => {
+  [].forEach.call(document.querySelectorAll('[data-admin-component]'), element => {
+    const componentName  = element.getAttribute('data-admin-component');
+    const { ...componentProps } = JSON.parse(element.getAttribute('data-props'));
+
+    import('flavours/blobfox/containers/admin_component').then(({ default: AdminComponent }) => {
+      return import('flavours/blobfox/components/admin/' + componentName).then(({ default: Component }) => {
+        const root = createRoot(element);
+
+        root.render (
+          <AdminComponent>
+            <Component {...componentProps} />
+          </AdminComponent>,
+        );
+      });
+    }).catch(error => {
+      console.error(error);
+    });
+  });
+});
diff --git a/app/javascript/flavours/blobfox/packs/common.js b/app/javascript/flavours/blobfox/packs/common.js
new file mode 100644
index 00000000000000..439ae9d27eedeb
--- /dev/null
+++ b/app/javascript/flavours/blobfox/packs/common.js
@@ -0,0 +1,8 @@
+import 'packs/public-path';
+import Rails from '@rails/ujs';
+import 'flavours/blobfox/styles/index.scss';
+
+Rails.start();
+
+//  This ensures that webpack compiles our images.
+require.context('../images', true);
diff --git a/app/javascript/flavours/blobfox/packs/error.js b/app/javascript/flavours/blobfox/packs/error.js
new file mode 100644
index 00000000000000..ab27f51435037a
--- /dev/null
+++ b/app/javascript/flavours/blobfox/packs/error.js
@@ -0,0 +1,14 @@
+import 'packs/public-path';
+import ready from 'flavours/blobfox/ready';
+
+ready(() => {
+  const image = document.querySelector('img');
+
+  image.addEventListener('mouseenter', () => {
+    image.src = '/oops.gif';
+  });
+
+  image.addEventListener('mouseleave', () => {
+    image.src = '/oops.png';
+  });
+});
diff --git a/app/javascript/flavours/blobfox/packs/home.js b/app/javascript/flavours/blobfox/packs/home.js
new file mode 100644
index 00000000000000..a6ce1876212a82
--- /dev/null
+++ b/app/javascript/flavours/blobfox/packs/home.js
@@ -0,0 +1,11 @@
+import 'packs/public-path';
+import { loadLocale } from 'flavours/blobfox/locales';
+import main from "flavours/blobfox/main";
+import { loadPolyfills } from 'flavours/blobfox/polyfills';
+
+loadPolyfills()
+  .then(loadLocale)
+  .then(main)
+  .catch(e => {
+    console.error(e);
+  });
diff --git a/app/javascript/flavours/blobfox/packs/public.jsx b/app/javascript/flavours/blobfox/packs/public.jsx
new file mode 100644
index 00000000000000..9df968e084789f
--- /dev/null
+++ b/app/javascript/flavours/blobfox/packs/public.jsx
@@ -0,0 +1,242 @@
+import 'packs/public-path';
+import { createRoot }  from 'react-dom/client';
+
+import { IntlMessageFormat } from 'intl-messageformat';
+import { defineMessages } from 'react-intl';
+
+import Rails from '@rails/ujs';
+import axios from 'axios';
+import { createBrowserHistory }  from 'history';
+import { throttle } from 'lodash';
+
+import { timeAgoString }  from 'flavours/blobfox/components/relative_timestamp';
+import emojify  from 'flavours/blobfox/features/emoji/emoji';
+import loadKeyboardExtensions from 'flavours/blobfox/load_keyboard_extensions';
+import { loadLocale, getLocale } from 'flavours/blobfox/locales';
+import { loadPolyfills } from 'flavours/blobfox/polyfills';
+
+const messages = defineMessages({
+  usernameTaken: { id: 'username.taken', defaultMessage: 'That username is taken. Try another' },
+  passwordExceedsLength: { id: 'password_confirmation.exceeds_maxlength', defaultMessage: 'Password confirmation exceeds the maximum password length' },
+  passwordDoesNotMatch: { id: 'password_confirmation.mismatching', defaultMessage: 'Password confirmation does not match' },
+});
+
+function main() {
+  const { messages: localeData } = getLocale();
+
+  const scrollToDetailedStatus = () => {
+    const history = createBrowserHistory();
+    const detailedStatuses = document.querySelectorAll('.public-layout .detailed-status');
+    const location = history.location;
+
+    if (detailedStatuses.length === 1 && (!location.state || !location.state.scrolledToDetailedStatus)) {
+      detailedStatuses[0].scrollIntoView();
+      history.replace(location.pathname, { ...location.state, scrolledToDetailedStatus: true });
+    }
+  };
+
+  const getEmojiAnimationHandler = (swapTo) => {
+    return ({ target }) => {
+      target.src = target.getAttribute(swapTo);
+    };
+  };
+
+  const locale = document.documentElement.lang;
+
+  const dateTimeFormat = new Intl.DateTimeFormat(locale, {
+    year: 'numeric',
+    month: 'long',
+    day: 'numeric',
+    hour: 'numeric',
+    minute: 'numeric',
+  });
+
+  const dateFormat = new Intl.DateTimeFormat(locale, {
+    year: 'numeric',
+    month: 'short',
+    day: 'numeric',
+    timeFormat: false,
+  });
+
+  const timeFormat = new Intl.DateTimeFormat(locale, {
+    timeStyle: 'short',
+    hour12: false,
+  });
+
+  const formatMessage = ({ id, defaultMessage }, values) => {
+    const messageFormat = new IntlMessageFormat(localeData[id] || defaultMessage, locale);
+    return messageFormat.format(values);
+  };
+
+  [].forEach.call(document.querySelectorAll('.emojify'), (content) => {
+    content.innerHTML = emojify(content.innerHTML);
+  });
+
+  [].forEach.call(document.querySelectorAll('time.formatted'), (content) => {
+    const datetime = new Date(content.getAttribute('datetime'));
+    const formattedDate = dateTimeFormat.format(datetime);
+
+    content.title = formattedDate;
+    content.textContent = formattedDate;
+  });
+
+  const isToday = date => {
+    const today = new Date();
+
+    return date.getDate() === today.getDate() &&
+      date.getMonth() === today.getMonth() &&
+      date.getFullYear() === today.getFullYear();
+  };
+  const todayFormat = new IntlMessageFormat(localeData['relative_format.today'] || 'Today at {time}', locale);
+
+  [].forEach.call(document.querySelectorAll('time.relative-formatted'), (content) => {
+    const datetime = new Date(content.getAttribute('datetime'));
+
+    let formattedContent;
+
+    if (isToday(datetime)) {
+      const formattedTime = timeFormat.format(datetime);
+
+      formattedContent = todayFormat.format({ time: formattedTime });
+    } else {
+      formattedContent = dateFormat.format(datetime);
+    }
+
+    content.title = formattedContent;
+    content.textContent = formattedContent;
+  });
+
+  [].forEach.call(document.querySelectorAll('time.time-ago'), (content) => {
+    const datetime = new Date(content.getAttribute('datetime'));
+    const now      = new Date();
+
+    const timeGiven = content.getAttribute('datetime').includes('T');
+    content.title = timeGiven ? dateTimeFormat.format(datetime) : dateFormat.format(datetime);
+    content.textContent = timeAgoString({
+      formatMessage,
+      formatDate: (date, options) => (new Intl.DateTimeFormat(locale, options)).format(date),
+    }, datetime, now, now.getFullYear(), timeGiven);
+  });
+
+  const reactComponents = document.querySelectorAll('[data-component]');
+  if (reactComponents.length > 0) {
+    import(/* webpackChunkName: "containers/media_container" */ 'flavours/blobfox/containers/media_container')
+      .then(({ default: MediaContainer }) => {
+        [].forEach.call(reactComponents, (component) => {
+          [].forEach.call(component.children, (child) => {
+            component.removeChild(child);
+          });
+        });
+
+        const content = document.createElement('div');
+
+        const root = createRoot(content);
+        root.render(<MediaContainer locale={locale} components={reactComponents} />);
+        document.body.appendChild(content);
+        scrollToDetailedStatus();
+      })
+      .catch(error => {
+        console.error(error);
+        scrollToDetailedStatus();
+      });
+  } else {
+    scrollToDetailedStatus();
+  }
+
+  Rails.delegate(document, '#user_account_attributes_username', 'input', throttle(() => {
+    const username = document.getElementById('user_account_attributes_username');
+
+    if (username.value && username.value.length > 0) {
+      axios.get('/api/v1/accounts/lookup', { params: { acct: username.value } }).then(() => {
+        username.setCustomValidity(formatMessage(messages.usernameTaken));
+      }).catch(() => {
+        username.setCustomValidity('');
+      });
+    } else {
+      username.setCustomValidity('');
+    }
+  }, 500, { leading: false, trailing: true }));
+
+  Rails.delegate(document, '#user_password,#user_password_confirmation', 'input', () => {
+    const password = document.getElementById('user_password');
+    const confirmation = document.getElementById('user_password_confirmation');
+    if (!confirmation) return;
+
+    if (confirmation.value && confirmation.value.length > password.maxLength) {
+      confirmation.setCustomValidity(formatMessage(messages.passwordExceedsLength));
+    } else if (password.value && password.value !== confirmation.value) {
+      confirmation.setCustomValidity(formatMessage(messages.passwordDoesNotMatch));
+    } else {
+      confirmation.setCustomValidity('');
+    }
+  });
+
+  Rails.delegate(document, '.custom-emoji', 'mouseover', getEmojiAnimationHandler('data-original'));
+  Rails.delegate(document, '.custom-emoji', 'mouseout', getEmojiAnimationHandler('data-static'));
+
+  Rails.delegate(document, '.status__content__spoiler-link', 'click', function() {
+    const statusEl = this.parentNode.parentNode;
+
+    if (statusEl.dataset.spoiler === 'expanded') {
+      statusEl.dataset.spoiler = 'folded';
+      this.textContent = (new IntlMessageFormat(localeData['status.show_more'] || 'Show more', locale)).format();
+    } else {
+      statusEl.dataset.spoiler = 'expanded';
+      this.textContent = (new IntlMessageFormat(localeData['status.show_less'] || 'Show less', locale)).format();
+    }
+
+    return false;
+  });
+
+  [].forEach.call(document.querySelectorAll('.status__content__spoiler-link'), (spoilerLink) => {
+    const statusEl = spoilerLink.parentNode.parentNode;
+    const message = (statusEl.dataset.spoiler === 'expanded') ? (localeData['status.show_less'] || 'Show less') : (localeData['status.show_more'] || 'Show more');
+    spoilerLink.textContent = (new IntlMessageFormat(message, locale)).format();
+  });
+
+  const toggleSidebar = () => {
+    const sidebar = document.querySelector('.sidebar ul');
+    const toggleButton = document.querySelector('.sidebar__toggle__icon');
+
+    if (sidebar.classList.contains('visible')) {
+      document.body.style.overflow = null;
+      toggleButton.setAttribute('aria-expanded', 'false');
+    } else {
+      document.body.style.overflow = 'hidden';
+      toggleButton.setAttribute('aria-expanded', 'true');
+    }
+
+    toggleButton.classList.toggle('active');
+    sidebar.classList.toggle('visible');
+  };
+
+  Rails.delegate(document, '.sidebar__toggle__icon', 'click', () => {
+    toggleSidebar();
+  });
+
+  Rails.delegate(document, '.sidebar__toggle__icon', 'keydown', e => {
+    if (e.key === ' ' || e.key === 'Enter') {
+      e.preventDefault();
+      toggleSidebar();
+    }
+  });
+
+  // Empty the honeypot fields in JS in case something like an extension
+  // automatically filled them.
+  Rails.delegate(document, '#registration_new_user,#new_user', 'submit', () => {
+    ['user_website', 'user_confirm_password', 'registration_user_website', 'registration_user_confirm_password'].forEach(id => {
+      const field = document.getElementById(id);
+      if (field) {
+        field.value = '';
+      }
+    });
+  });
+}
+
+loadPolyfills()
+  .then(loadLocale)
+  .then(main)
+  .then(loadKeyboardExtensions)
+  .catch(error => {
+    console.error(error);
+  });
diff --git a/app/javascript/flavours/blobfox/packs/settings.js b/app/javascript/flavours/blobfox/packs/settings.js
new file mode 100644
index 00000000000000..049a3a7187632b
--- /dev/null
+++ b/app/javascript/flavours/blobfox/packs/settings.js
@@ -0,0 +1,42 @@
+import 'packs/public-path';
+import Rails from '@rails/ujs';
+
+import loadKeyboardExtensions from 'flavours/blobfox/load_keyboard_extensions';
+import { loadPolyfills } from 'flavours/blobfox/polyfills';
+import 'cocoon-js-vanilla';
+
+function main() {
+  const toggleSidebar = () => {
+    const sidebar = document.querySelector('.sidebar ul');
+    const toggleButton = document.querySelector('.sidebar__toggle__icon');
+
+    if (sidebar.classList.contains('visible')) {
+      document.body.style.overflow = null;
+      toggleButton.setAttribute('aria-expanded', 'false');
+    } else {
+      document.body.style.overflow = 'hidden';
+      toggleButton.setAttribute('aria-expanded', 'true');
+    }
+
+    toggleButton.classList.toggle('active');
+    sidebar.classList.toggle('visible');
+  };
+
+  Rails.delegate(document, '.sidebar__toggle__icon', 'click', () => {
+    toggleSidebar();
+  });
+
+  Rails.delegate(document, '.sidebar__toggle__icon', 'keydown', e => {
+    if (e.key === ' ' || e.key === 'Enter') {
+      e.preventDefault();
+      toggleSidebar();
+    }
+  });
+}
+
+loadPolyfills()
+  .then(main)
+  .then(loadKeyboardExtensions)
+  .catch(error => {
+    console.error(error);
+  });
diff --git a/app/javascript/flavours/blobfox/packs/share.jsx b/app/javascript/flavours/blobfox/packs/share.jsx
new file mode 100644
index 00000000000000..73d65cb9b58764
--- /dev/null
+++ b/app/javascript/flavours/blobfox/packs/share.jsx
@@ -0,0 +1,27 @@
+import 'packs/public-path';
+import { createRoot } from 'react-dom/client';
+
+import ComposeContainer from 'flavours/blobfox/containers/compose_container';
+import { loadPolyfills } from 'flavours/blobfox/polyfills';
+import ready from 'flavours/blobfox/ready';
+
+function loaded() {
+  const mountNode = document.getElementById('mastodon-compose');
+
+  if (mountNode) {
+    const attr = mountNode.getAttribute('data-props');
+    if(!attr) return;
+
+    const props = JSON.parse(attr);
+    const root = createRoot(mountNode);
+    root.render(<ComposeContainer {...props} />);
+  }
+}
+
+function main() {
+  ready(loaded);
+}
+
+loadPolyfills().then(main).catch(error => {
+  console.error(error);
+});
diff --git a/app/javascript/flavours/blobfox/packs/sign_up.js b/app/javascript/flavours/blobfox/packs/sign_up.js
new file mode 100644
index 00000000000000..42c86aca9fc2b5
--- /dev/null
+++ b/app/javascript/flavours/blobfox/packs/sign_up.js
@@ -0,0 +1,42 @@
+import 'packs/public-path';
+import axios from 'axios';
+
+import ready from 'flavours/blobfox/ready';
+
+ready(() => {
+  setInterval(() => {
+    axios.get('/api/v1/emails/check_confirmation').then((response) => {
+      if (response.data) {
+        window.location = '/start';
+      }
+    }).catch(error => {
+      console.error(error);
+    });
+  }, 5000);
+
+  document.querySelectorAll('.timer-button').forEach(button => {
+    let counter = 30;
+
+    const container = document.createElement('span');
+
+    const updateCounter = () => {
+      container.innerText = ` (${counter})`;
+    };
+
+    updateCounter();
+
+    const countdown = setInterval(() => {
+      counter--;
+
+      if (counter === 0) {
+        button.disabled = false;
+        button.removeChild(container);
+        clearInterval(countdown);
+      } else {
+        updateCounter();
+      }
+    }, 1000);
+
+    button.appendChild(container);
+  });
+});
diff --git a/app/javascript/flavours/blobfox/performance.js b/app/javascript/flavours/blobfox/performance.js
new file mode 100644
index 00000000000000..42849c82b10378
--- /dev/null
+++ b/app/javascript/flavours/blobfox/performance.js
@@ -0,0 +1,30 @@
+//
+// Tools for performance debugging, only enabled in development mode.
+// Open up Chrome Dev Tools, then Timeline, then User Timing to see output.
+// Also see config/webpack/loaders/mark.js for the webpack loader marks.
+
+import * as marky from 'marky';
+
+if (process.env.NODE_ENV === 'development') {
+  if (typeof performance !== 'undefined' && performance.setResourceTimingBufferSize) {
+    // Increase Firefox's performance entry limit; otherwise it's capped to 150.
+    // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1331135
+    performance.setResourceTimingBufferSize(Infinity);
+  }
+
+  // allows us to easily do e.g. ReactPerf.printWasted() while debugging
+  //window.ReactPerf = require('react-addons-perf');
+  //window.ReactPerf.start();
+}
+
+export function start(name) {
+  if (process.env.NODE_ENV === 'development') {
+    marky.mark(name);
+  }
+}
+
+export function stop(name) {
+  if (process.env.NODE_ENV === 'development') {
+    marky.stop(name);
+  }
+}
diff --git a/app/javascript/flavours/blobfox/permissions.ts b/app/javascript/flavours/blobfox/permissions.ts
new file mode 100644
index 00000000000000..b583535c00e35f
--- /dev/null
+++ b/app/javascript/flavours/blobfox/permissions.ts
@@ -0,0 +1,4 @@
+export const PERMISSION_INVITE_USERS = 0x0000000000010000;
+export const PERMISSION_MANAGE_USERS = 0x0000000000000400;
+export const PERMISSION_MANAGE_FEDERATION = 0x0000000000000020;
+export const PERMISSION_MANAGE_REPORTS = 0x0000000000000010;
diff --git a/app/javascript/flavours/blobfox/polyfills/extra_polyfills.ts b/app/javascript/flavours/blobfox/polyfills/extra_polyfills.ts
new file mode 100644
index 00000000000000..a8d5530c5fcb6a
--- /dev/null
+++ b/app/javascript/flavours/blobfox/polyfills/extra_polyfills.ts
@@ -0,0 +1 @@
+import 'requestidlecallback';
diff --git a/app/javascript/flavours/blobfox/polyfills/index.ts b/app/javascript/flavours/blobfox/polyfills/index.ts
new file mode 100644
index 00000000000000..431c5b0f30f350
--- /dev/null
+++ b/app/javascript/flavours/blobfox/polyfills/index.ts
@@ -0,0 +1,21 @@
+// Convenience function to load polyfills and return a promise when it's done.
+// If there are no polyfills, then this is just Promise.resolve() which means
+// it will execute in the same tick of the event loop (i.e. near-instant).
+
+import { loadIntlPolyfills } from './intl';
+
+function importExtraPolyfills() {
+  return import(/* webpackChunkName: "extra_polyfills" */ './extra_polyfills');
+}
+
+export function loadPolyfills() {
+  // Safari does not have requestIdleCallback.
+  // This avoids shipping them all the polyfills.
+  const needsExtraPolyfills = !window.requestIdleCallback;
+
+  return Promise.all([
+    loadIntlPolyfills(),
+    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- those properties might not exist in old browsers, even if they are always here in types
+    needsExtraPolyfills && importExtraPolyfills(),
+  ]);
+}
diff --git a/app/javascript/flavours/blobfox/polyfills/intl.ts b/app/javascript/flavours/blobfox/polyfills/intl.ts
new file mode 100644
index 00000000000000..b825da66214f51
--- /dev/null
+++ b/app/javascript/flavours/blobfox/polyfills/intl.ts
@@ -0,0 +1,106 @@
+// import { shouldPolyfill as shouldPolyfillCanonicalLocales } from '@formatjs/intl-getcanonicallocales/should-polyfill';
+// import { shouldPolyfill as shouldPolyfillLocale } from '@formatjs/intl-locale/should-polyfill';
+import { shouldPolyfill as shoudPolyfillPluralRules } from '@formatjs/intl-pluralrules/should-polyfill';
+// import { shouldPolyfill as shouldPolyfillNumberFormat } from '@formatjs/intl-numberformat/should-polyfill';
+// import { shouldPolyfill as shouldPolyfillIntlDateTimeFormat } from '@formatjs/intl-datetimeformat/should-polyfill';
+// import { shouldPolyfill as shouldPolyfillIntlRelativeTimeFormat } from '@formatjs/intl-relativetimeformat/should-polyfill';
+
+// async function loadGetCanonicalLocalesPolyfill() {
+//   // This platform already supports Intl.getCanonicalLocales
+//   if (shouldPolyfillCanonicalLocales()) {
+//     await import('@formatjs/intl-getcanonicallocales/polyfill');
+//   }
+// }
+
+// async function loadLocalePolyfill() {
+//   // This platform already supports Intl.Locale
+//   if (shouldPolyfillLocale()) {
+//     await import('@formatjs/intl-locale/polyfill');
+//   }
+// }
+
+// async function loadIntlNumberFormatPolyfill(locale: string) {
+//   const unsupportedLocale = shouldPolyfillNumberFormat(locale);
+//   // This locale is supported
+//   if (!unsupportedLocale) {
+//     return;
+//   }
+//   // Load the polyfill 1st BEFORE loading data
+//   await import('@formatjs/intl-numberformat/polyfill-force');
+//   await import(`@formatjs/intl-numberformat/locale-data/${unsupportedLocale}`);
+// }
+
+// async function loadIntlDateTimeFormatPolyfill(locale: string) {
+//   const unsupportedLocale = shouldPolyfillIntlDateTimeFormat(locale);
+//   // This locale is supported
+//   if (!unsupportedLocale) {
+//     return;
+//   }
+//   // Load the polyfill 1st BEFORE loading data
+//   await import('@formatjs/intl-datetimeformat/polyfill-force');
+
+//   // Parallelize CLDR data loading
+//   const dataPolyfills = [
+//     import('@formatjs/intl-datetimeformat/add-all-tz'),
+//     import(`@formatjs/intl-datetimeformat/locale-data/${unsupportedLocale}`),
+//   ];
+//   await Promise.all(dataPolyfills);
+// }
+
+async function loadIntlPluralRulesPolyfills(locale: string) {
+  const unsupportedLocale = shoudPolyfillPluralRules(locale);
+  // This locale is supported
+  if (!unsupportedLocale) {
+    return;
+  }
+  // Load the polyfill 1st BEFORE loading data
+  await import(
+    /* webpackChunkName: "i18n-pluralrules-polyfill" */ '@formatjs/intl-pluralrules/polyfill-force'
+  );
+  await import(
+    /* webpackChunkName: "i18n-pluralrules-polyfill-[request]" */ `@formatjs/intl-pluralrules/locale-data/${unsupportedLocale}`
+  );
+}
+
+// async function loadIntlRelativeTimeFormatPolyfill(locale: string) {
+//   const unsupportedLocale = shouldPolyfillIntlRelativeTimeFormat(locale);
+//   // This locale is supported
+//   if (!unsupportedLocale) {
+//     return;
+//   }
+//   // Load the polyfill 1st BEFORE loading data
+//   await import(
+//     /* webpackChunkName: "i18n-relativetimeformat-polyfill" */
+//     '@formatjs/intl-relativetimeformat/polyfill-force'
+//   );
+//   await import(
+//     /* webpackChunkName: "i18n-relativetimeformat-polyfill-[request]" */
+//     `@formatjs/intl-relativetimeformat/locale-data/${unsupportedLocale}`
+//   );
+// }
+
+export async function loadIntlPolyfills() {
+  // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- we want to match empty strings
+  const locale = document.querySelector('html')?.lang || 'en';
+
+  // order is important here
+
+  // Supported in IE11 and most other browsers, not useful
+  // await loadGetCanonicalLocalesPolyfill()
+
+  // Supported in IE11 and most other browsers, not useful
+  // await loadLocalePolyfill()
+
+  // Supported in IE11 and most other browsers, not useful
+  // await loadIntlNumberFormatPolyfill(locale)
+
+  // Supported in IE11 and most other browsers, not useful
+  // await loadIntlDateTimeFormatPolyfill(locale)
+
+  // Supported from Safari 13+, may still be useful
+  await loadIntlPluralRulesPolyfills(locale);
+
+  // This is not used yet in the codebase yet
+  // Supported from Safari 14+
+  // await loadIntlRelativeTimeFormatPolyfill(locale);
+}
diff --git a/app/javascript/flavours/blobfox/ready.js b/app/javascript/flavours/blobfox/ready.js
new file mode 100644
index 00000000000000..e769cc756079f0
--- /dev/null
+++ b/app/javascript/flavours/blobfox/ready.js
@@ -0,0 +1,32 @@
+// @ts-check
+
+/**
+ * @param {(() => void) | (() => Promise<void>)} callback
+ * @returns {Promise<void>}
+ */
+export default function ready(callback) {
+  return new Promise((resolve, reject) => {
+    function loaded() {
+      let result;
+      try {
+        result = callback();
+      } catch (err) {
+        reject(err);
+
+        return;
+      }
+
+      if (typeof result?.then === 'function') {
+        result.then(resolve).catch(reject);
+      } else {
+        resolve();
+      }
+    }
+
+    if (['interactive', 'complete'].includes(document.readyState)) {
+      loaded();
+    } else {
+      document.addEventListener('DOMContentLoaded', loaded);
+    }
+  });
+}
diff --git a/app/javascript/flavours/blobfox/reducers/accounts.ts b/app/javascript/flavours/blobfox/reducers/accounts.ts
new file mode 100644
index 00000000000000..0fe91fd34d1ab7
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/accounts.ts
@@ -0,0 +1,84 @@
+import { Map as ImmutableMap } from 'immutable';
+
+import type { Reducer } from 'redux';
+
+import {
+  followAccountSuccess,
+  unfollowAccountSuccess,
+  importAccounts,
+  revealAccount,
+} from 'flavours/blobfox/actions/accounts_typed';
+import type { ApiAccountJSON } from 'flavours/blobfox/api_types/accounts';
+import { me } from 'flavours/blobfox/initial_state';
+import type { Account } from 'flavours/blobfox/models/account';
+import { createAccountFromServerJSON } from 'flavours/blobfox/models/account';
+
+const initialState = ImmutableMap<string, Account>();
+
+const normalizeAccount = (
+  state: typeof initialState,
+  account: ApiAccountJSON,
+) => {
+  return state.set(
+    account.id,
+    createAccountFromServerJSON(account).set(
+      'hidden',
+      state.get(account.id)?.hidden === false
+        ? false
+        : account.limited || false,
+    ),
+  );
+};
+
+const normalizeAccounts = (
+  state: typeof initialState,
+  accounts: ApiAccountJSON[],
+) => {
+  accounts.forEach((account) => {
+    state = normalizeAccount(state, account);
+  });
+
+  return state;
+};
+
+function getCurrentUser() {
+  if (!me)
+    throw new Error(
+      'No current user (me) defined when calling `accountsReducer`',
+    );
+
+  return me;
+}
+
+export const accountsReducer: Reducer<typeof initialState> = (
+  state = initialState,
+  action,
+) => {
+  if (revealAccount.match(action))
+    return state.setIn([action.payload.id, 'hidden'], false);
+  else if (importAccounts.match(action))
+    return normalizeAccounts(state, action.payload.accounts);
+  else if (followAccountSuccess.match(action)) {
+    return state
+      .update(
+        action.payload.relationship.id,
+        (account) => account?.update('followers_count', (n) => n + 1),
+      )
+      .update(
+        getCurrentUser(),
+        (account) => account?.update('following_count', (n) => n + 1),
+      );
+  } else if (unfollowAccountSuccess.match(action))
+    return state
+      .update(
+        action.payload.relationship.id,
+        (account) =>
+          account?.update('followers_count', (n) => Math.max(0, n - 1)),
+      )
+      .update(
+        getCurrentUser(),
+        (account) =>
+          account?.update('following_count', (n) => Math.max(0, n - 1)),
+      );
+  else return state;
+};
diff --git a/app/javascript/flavours/blobfox/reducers/accounts_map.js b/app/javascript/flavours/blobfox/reducers/accounts_map.js
new file mode 100644
index 00000000000000..9053dcc9c0528b
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/accounts_map.js
@@ -0,0 +1,24 @@
+import { Map as ImmutableMap } from 'immutable';
+
+import { ACCOUNT_LOOKUP_FAIL } from '../actions/accounts';
+import { importAccounts } from '../actions/accounts_typed';
+import { domain } from '../initial_state';
+
+export const normalizeForLookup = str => {
+  str = str.toLowerCase();
+  const trailingIndex = str.indexOf(`@${domain.toLowerCase()}`);
+  return (trailingIndex > 0) ? str.slice(0, trailingIndex) : str;
+};
+
+const initialState = ImmutableMap();
+
+export default function accountsMap(state = initialState, action) {
+  switch(action.type) {
+  case ACCOUNT_LOOKUP_FAIL:
+    return action.error?.response?.status === 404 ? state.set(normalizeForLookup(action.acct), null) : state;
+  case importAccounts.type:
+    return state.withMutations(map => action.payload.accounts.forEach(account => map.set(normalizeForLookup(account.acct), account.id)));
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/reducers/alerts.js b/app/javascript/flavours/blobfox/reducers/alerts.js
new file mode 100644
index 00000000000000..1ca9b62a0210b7
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/alerts.js
@@ -0,0 +1,30 @@
+import { List as ImmutableList } from 'immutable';
+
+import {
+  ALERT_SHOW,
+  ALERT_DISMISS,
+  ALERT_CLEAR,
+} from '../actions/alerts';
+
+const initialState = ImmutableList([]);
+
+let id = 0;
+
+const addAlert = (state, alert) =>
+  state.push({
+    key: id++,
+    ...alert,
+  });
+
+export default function alerts(state = initialState, action) {
+  switch(action.type) {
+  case ALERT_SHOW:
+    return addAlert(state, action.alert);
+  case ALERT_DISMISS:
+    return state.filterNot(item => item.key === action.alert.key);
+  case ALERT_CLEAR:
+    return state.clear();
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/reducers/announcements.js b/app/javascript/flavours/blobfox/reducers/announcements.js
new file mode 100644
index 00000000000000..2134b04c6df1dd
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/announcements.js
@@ -0,0 +1,103 @@
+import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
+
+import {
+  ANNOUNCEMENTS_FETCH_REQUEST,
+  ANNOUNCEMENTS_FETCH_SUCCESS,
+  ANNOUNCEMENTS_FETCH_FAIL,
+  ANNOUNCEMENTS_UPDATE,
+  ANNOUNCEMENTS_REACTION_UPDATE,
+  ANNOUNCEMENTS_REACTION_ADD_REQUEST,
+  ANNOUNCEMENTS_REACTION_ADD_FAIL,
+  ANNOUNCEMENTS_REACTION_REMOVE_REQUEST,
+  ANNOUNCEMENTS_REACTION_REMOVE_FAIL,
+  ANNOUNCEMENTS_TOGGLE_SHOW,
+  ANNOUNCEMENTS_DELETE,
+  ANNOUNCEMENTS_DISMISS_SUCCESS,
+} from '../actions/announcements';
+
+const initialState = ImmutableMap({
+  items: ImmutableList(),
+  isLoading: false,
+  show: false,
+});
+
+const updateReaction = (state, id, name, updater) => state.update('items', list => list.map(announcement => {
+  if (announcement.get('id') === id) {
+    return announcement.update('reactions', reactions => {
+      const idx = reactions.findIndex(reaction => reaction.get('name') === name);
+
+      if (idx > -1) {
+        return reactions.update(idx, reaction => updater(reaction));
+      }
+
+      return reactions.push(updater(fromJS({ name, count: 0 })));
+    });
+  }
+
+  return announcement;
+}));
+
+const updateReactionCount = (state, reaction) => updateReaction(state, reaction.announcement_id, reaction.name, x => x.set('count', reaction.count));
+
+const addReaction = (state, id, name) => updateReaction(state, id, name, x => x.set('me', true).update('count', y => y + 1));
+
+const removeReaction = (state, id, name) => updateReaction(state, id, name, x => x.set('me', false).update('count', y => y - 1));
+
+const sortAnnouncements = list => list.sortBy(x => x.get('starts_at') || x.get('published_at'));
+
+const updateAnnouncement = (state, announcement) => {
+  const idx = state.get('items').findIndex(x => x.get('id') === announcement.get('id'));
+
+  if (idx > -1) {
+    // Deep merge is used because announcements from the streaming API do not contain
+    // personalized data about which reactions have been selected by the given user,
+    // and that is information we want to preserve
+    return state.update('items', list => sortAnnouncements(list.update(idx, x => x.mergeDeep(announcement))));
+  }
+
+  return state.update('items', list => sortAnnouncements(list.unshift(announcement)));
+};
+
+export default function announcementsReducer(state = initialState, action) {
+  switch(action.type) {
+  case ANNOUNCEMENTS_TOGGLE_SHOW:
+    return state.withMutations(map => {
+      map.set('show', !map.get('show'));
+    });
+  case ANNOUNCEMENTS_FETCH_REQUEST:
+    return state.set('isLoading', true);
+  case ANNOUNCEMENTS_FETCH_SUCCESS:
+    return state.withMutations(map => {
+      const items = fromJS(action.announcements);
+
+      map.set('items', items);
+      map.set('isLoading', false);
+    });
+  case ANNOUNCEMENTS_FETCH_FAIL:
+    return state.set('isLoading', false);
+  case ANNOUNCEMENTS_UPDATE:
+    return updateAnnouncement(state, fromJS(action.announcement));
+  case ANNOUNCEMENTS_REACTION_UPDATE:
+    return updateReactionCount(state, action.reaction);
+  case ANNOUNCEMENTS_REACTION_ADD_REQUEST:
+  case ANNOUNCEMENTS_REACTION_REMOVE_FAIL:
+    return addReaction(state, action.id, action.name);
+  case ANNOUNCEMENTS_REACTION_REMOVE_REQUEST:
+  case ANNOUNCEMENTS_REACTION_ADD_FAIL:
+    return removeReaction(state, action.id, action.name);
+  case ANNOUNCEMENTS_DISMISS_SUCCESS:
+    return updateAnnouncement(state, fromJS({ 'id': action.id, 'read': true }));
+  case ANNOUNCEMENTS_DELETE:
+    return state.update('items', list => {
+      const idx = list.findIndex(x => x.get('id') === action.id);
+
+      if (idx > -1) {
+        return list.delete(idx);
+      }
+
+      return list;
+    });
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/reducers/blocks.js b/app/javascript/flavours/blobfox/reducers/blocks.js
new file mode 100644
index 00000000000000..1b65071634fb16
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/blocks.js
@@ -0,0 +1,22 @@
+import Immutable from 'immutable';
+
+import {
+  BLOCKS_INIT_MODAL,
+} from '../actions/blocks';
+
+const initialState = Immutable.Map({
+  new: Immutable.Map({
+    account_id: null,
+  }),
+});
+
+export default function mutes(state = initialState, action) {
+  switch (action.type) {
+  case BLOCKS_INIT_MODAL:
+    return state.withMutations((state) => {
+      state.setIn(['new', 'account_id'], action.account.get('id'));
+    });
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/reducers/boosts.js b/app/javascript/flavours/blobfox/reducers/boosts.js
new file mode 100644
index 00000000000000..9573748e752a25
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/boosts.js
@@ -0,0 +1,25 @@
+import Immutable from 'immutable';
+
+import {
+  BOOSTS_INIT_MODAL,
+  BOOSTS_CHANGE_PRIVACY,
+} from 'flavours/blobfox/actions/boosts';
+
+const initialState = Immutable.Map({
+  new: Immutable.Map({
+    privacy: 'public',
+  }),
+});
+
+export default function mutes(state = initialState, action) {
+  switch (action.type) {
+  case BOOSTS_INIT_MODAL:
+    return state.withMutations((state) => {
+      state.setIn(['new', 'privacy'], action.privacy);
+    });
+  case BOOSTS_CHANGE_PRIVACY:
+    return state.setIn(['new', 'privacy'], action.privacy);
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/reducers/compose.js b/app/javascript/flavours/blobfox/reducers/compose.js
new file mode 100644
index 00000000000000..4722d3fdfeb445
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/compose.js
@@ -0,0 +1,660 @@
+import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
+
+import {
+  COMPOSE_MOUNT,
+  COMPOSE_UNMOUNT,
+  COMPOSE_CHANGE,
+  COMPOSE_CYCLE_ELEFRIEND,
+  COMPOSE_REPLY,
+  COMPOSE_REPLY_CANCEL,
+  COMPOSE_DIRECT,
+  COMPOSE_MENTION,
+  COMPOSE_SUBMIT_REQUEST,
+  COMPOSE_SUBMIT_SUCCESS,
+  COMPOSE_SUBMIT_FAIL,
+  COMPOSE_UPLOAD_REQUEST,
+  COMPOSE_UPLOAD_SUCCESS,
+  COMPOSE_UPLOAD_FAIL,
+  COMPOSE_UPLOAD_UNDO,
+  COMPOSE_UPLOAD_PROGRESS,
+  COMPOSE_UPLOAD_PROCESSING,
+  THUMBNAIL_UPLOAD_REQUEST,
+  THUMBNAIL_UPLOAD_SUCCESS,
+  THUMBNAIL_UPLOAD_FAIL,
+  THUMBNAIL_UPLOAD_PROGRESS,
+  COMPOSE_SUGGESTIONS_CLEAR,
+  COMPOSE_SUGGESTIONS_READY,
+  COMPOSE_SUGGESTION_SELECT,
+  COMPOSE_SUGGESTION_IGNORE,
+  COMPOSE_SUGGESTION_TAGS_UPDATE,
+  COMPOSE_TAG_HISTORY_UPDATE,
+  COMPOSE_ADVANCED_OPTIONS_CHANGE,
+  COMPOSE_SENSITIVITY_CHANGE,
+  COMPOSE_SPOILERNESS_CHANGE,
+  COMPOSE_SPOILER_TEXT_CHANGE,
+  COMPOSE_VISIBILITY_CHANGE,
+  COMPOSE_LANGUAGE_CHANGE,
+  COMPOSE_CONTENT_TYPE_CHANGE,
+  COMPOSE_EMOJI_INSERT,
+  COMPOSE_UPLOAD_CHANGE_REQUEST,
+  COMPOSE_UPLOAD_CHANGE_SUCCESS,
+  COMPOSE_UPLOAD_CHANGE_FAIL,
+  COMPOSE_DOODLE_SET,
+  COMPOSE_RESET,
+  COMPOSE_POLL_ADD,
+  COMPOSE_POLL_REMOVE,
+  COMPOSE_POLL_OPTION_ADD,
+  COMPOSE_POLL_OPTION_CHANGE,
+  COMPOSE_POLL_OPTION_REMOVE,
+  COMPOSE_POLL_SETTINGS_CHANGE,
+  INIT_MEDIA_EDIT_MODAL,
+  COMPOSE_CHANGE_MEDIA_DESCRIPTION,
+  COMPOSE_CHANGE_MEDIA_FOCUS,
+  COMPOSE_SET_STATUS,
+  COMPOSE_FOCUS,
+} from '../actions/compose';
+import { REDRAFT } from '../actions/statuses';
+import { STORE_HYDRATE } from '../actions/store';
+import { TIMELINE_DELETE } from '../actions/timelines';
+import { me, defaultContentType } from '../initial_state';
+import { recoverHashtags } from '../utils/hashtag';
+import { unescapeHTML } from '../utils/html';
+import { overwrite } from '../utils/js_helpers';
+import { privacyPreference } from '../utils/privacy_preference';
+import { uuid } from '../uuid';
+
+const totalElefriends = 3;
+
+// ~4% chance you'll end up with an unexpected friend
+// blobfox-soc/mastodon repo created_at date: 2017-04-20T21:55:28Z
+const blobfoxProbability = 1 - 0.0420215528;
+
+const initialState = ImmutableMap({
+  mounted: 0,
+  advanced_options: ImmutableMap({
+    do_not_federate: false,
+    threaded_mode: false,
+  }),
+  sensitive: false,
+  elefriend: Math.random() < blobfoxProbability ? Math.floor(Math.random() * totalElefriends) : totalElefriends,
+  spoiler: false,
+  spoiler_text: '',
+  privacy: null,
+  id: null,
+  content_type: defaultContentType || 'text/plain',
+  text: '',
+  focusDate: null,
+  caretPosition: null,
+  preselectDate: null,
+  in_reply_to: null,
+  is_submitting: false,
+  is_uploading: false,
+  is_changing_upload: false,
+  progress: 0,
+  isUploadingThumbnail: false,
+  thumbnailProgress: 0,
+  media_attachments: ImmutableList(),
+  pending_media_attachments: 0,
+  poll: null,
+  suggestion_token: null,
+  suggestions: ImmutableList(),
+  default_advanced_options: ImmutableMap({
+    do_not_federate: false,
+    threaded_mode: null,  //  Do not reset
+  }),
+  default_privacy: 'public',
+  default_sensitive: false,
+  default_language: 'en',
+  resetFileKey: Math.floor((Math.random() * 0x10000)),
+  idempotencyKey: null,
+  tagHistory: ImmutableList(),
+  media_modal: ImmutableMap({
+    id: null,
+    description: '',
+    focusX: 0,
+    focusY: 0,
+    dirty: false,
+  }),
+  doodle: ImmutableMap({
+    fg: 'rgb(  0,    0,    0)',
+    bg: 'rgb(255,  255,  255)',
+    swapped: false,
+    mode: 'draw',
+    size: 'normal',
+    weight: 2,
+    opacity: 1,
+    adaptiveStroke: true,
+    smoothing: false,
+  }),
+});
+
+const initialPoll = ImmutableMap({
+  options: ImmutableList(['', '']),
+  expires_in: 24 * 3600,
+  multiple: false,
+});
+
+function statusToTextMentions(state, status) {
+  let set = ImmutableOrderedSet([]);
+
+  if (status.getIn(['account', 'id']) !== me) {
+    set = set.add(`@${status.getIn(['account', 'acct'])} `);
+  }
+
+  return set.union(status.get('mentions').filterNot(mention => mention.get('id') === me).map(mention => `@${mention.get('acct')} `)).join('');
+}
+
+function apiStatusToTextMentions (state, status) {
+  let set = ImmutableOrderedSet([]);
+
+  if (status.account.id !== me) {
+    set = set.add(`@${status.account.acct} `);
+  }
+
+  return set.union(status.mentions.filter(
+    mention => mention.id !== me,
+  ).map(
+    mention => `@${mention.acct} `,
+  )).join('');
+}
+
+function apiStatusToTextHashtags (state, status) {
+  const text = unescapeHTML(status.content);
+  return ImmutableOrderedSet([]).union(recoverHashtags(status.tags, text).map(
+    (name) => `#${name} `,
+  )).join('');
+}
+
+function clearAll(state) {
+  return state.withMutations(map => {
+    map.set('id', null);
+    map.set('text', '');
+    if (defaultContentType) map.set('content_type', defaultContentType);
+    map.set('spoiler', false);
+    map.set('spoiler_text', '');
+    map.set('is_submitting', false);
+    map.set('is_changing_upload', false);
+    map.set('in_reply_to', null);
+    map.update(
+      'advanced_options',
+      map => map.mergeWith(overwrite, state.get('default_advanced_options')),
+    );
+    map.set('privacy', state.get('default_privacy'));
+    map.set('sensitive', state.get('default_sensitive'));
+    map.set('language', state.get('default_language'));
+    map.update('media_attachments', list => list.clear());
+    map.set('poll', null);
+    map.set('idempotencyKey', uuid());
+  });
+}
+
+function continueThread (state, status) {
+  return state.withMutations(function (map) {
+    let text = apiStatusToTextMentions(state, status);
+    text = text + apiStatusToTextHashtags(state, status);
+    map.set('text', text);
+    if (status.spoiler_text) {
+      map.set('spoiler', true);
+      map.set('spoiler_text', status.spoiler_text);
+    } else {
+      map.set('spoiler', false);
+      map.set('spoiler_text', '');
+    }
+    map.set('is_submitting', false);
+    map.set('in_reply_to', status.id);
+    map.update(
+      'advanced_options',
+      map => map.merge(new ImmutableMap({ do_not_federate: status.local_only })),
+    );
+    map.set('privacy', status.visibility);
+    map.set('sensitive', false);
+    map.update('media_attachments', list => list.clear());
+    map.set('poll', null);
+    map.set('idempotencyKey', uuid());
+    map.set('focusDate', new Date());
+    map.set('caretPosition', null);
+    map.set('preselectDate', new Date());
+  });
+}
+
+function appendMedia(state, media, file) {
+  const prevSize = state.get('media_attachments').size;
+
+  return state.withMutations(map => {
+    if (media.get('type') === 'image') {
+      media = media.set('file', file);
+    }
+    map.update('media_attachments', list => list.push(media.set('unattached', true)));
+    map.set('is_uploading', false);
+    map.set('is_processing', false);
+    map.set('resetFileKey', Math.floor((Math.random() * 0x10000)));
+    map.set('idempotencyKey', uuid());
+    map.update('pending_media_attachments', n => n - 1);
+
+    if (prevSize === 0 && (state.get('default_sensitive') || state.get('spoiler'))) {
+      map.set('sensitive', true);
+    }
+  });
+}
+
+function removeMedia(state, mediaId) {
+  const prevSize = state.get('media_attachments').size;
+
+  return state.withMutations(map => {
+    map.update('media_attachments', list => list.filterNot(item => item.get('id') === mediaId));
+    map.set('idempotencyKey', uuid());
+
+    if (prevSize === 1) {
+      map.set('sensitive', false);
+    }
+  });
+}
+
+const insertSuggestion = (state, position, token, completion, path) => {
+  return state.withMutations(map => {
+    map.updateIn(path, oldText => `${oldText.slice(0, position)}${completion}${completion[0] === ':' ? '\u200B' : ' '}${oldText.slice(position + token.length)}`);
+    map.set('suggestion_token', null);
+    map.set('suggestions', ImmutableList());
+    if (path.length === 1 && path[0] === 'text') {
+      map.set('focusDate', new Date());
+      map.set('caretPosition', position + completion.length + 1);
+    }
+    map.set('idempotencyKey', uuid());
+  });
+};
+
+const ignoreSuggestion = (state, position, token, completion, path) => {
+  return state.withMutations(map => {
+    map.updateIn(path, oldText => `${oldText.slice(0, position + token.length)} ${oldText.slice(position + token.length)}`);
+    map.set('suggestion_token', null);
+    map.set('suggestions', ImmutableList());
+    map.set('focusDate', new Date());
+    map.set('caretPosition', position + token.length + 1);
+    map.set('idempotencyKey', uuid());
+  });
+};
+
+const sortHashtagsByUse = (state, tags) => {
+  const personalHistory = state.get('tagHistory').map(tag => tag.toLowerCase());
+
+  const tagsWithLowercase = tags.map(t => ({ ...t, lowerName: t.name.toLowerCase() }));
+  const sorted = tagsWithLowercase.sort((a, b) => {
+    const usedA = personalHistory.includes(a.lowerName);
+    const usedB = personalHistory.includes(b.lowerName);
+
+    if (usedA === usedB) {
+      return 0;
+    } else if (usedA && !usedB) {
+      return -1;
+    } else {
+      return 1;
+    }
+  });
+  sorted.forEach(tag => delete tag.lowerName);
+  return sorted;
+};
+
+const insertEmoji = (state, position, emojiData) => {
+  const emoji = emojiData.native;
+
+  return state.withMutations(map => {
+    map.update('text', oldText => `${oldText.slice(0, position)}${emoji}\u200B${oldText.slice(position)}`);
+    map.set('focusDate', new Date());
+    map.set('caretPosition', position + emoji.length + 1);
+    map.set('idempotencyKey', uuid());
+  });
+};
+
+const hydrate = (state, hydratedState) => {
+  state = clearAll(state.merge(hydratedState));
+
+  if (hydratedState.get('text')) {
+    state = state.set('text', hydratedState.get('text')).set('focusDate', new Date());
+  }
+
+  return state;
+};
+
+const domParser = new DOMParser();
+
+const expandMentions = status => {
+  const fragment = domParser.parseFromString(status.get('content'), 'text/html').documentElement;
+
+  status.get('mentions').forEach(mention => {
+    fragment.querySelector(`a[href="${mention.get('url')}"]`).textContent = `@${mention.get('acct')}`;
+  });
+
+  return fragment.innerHTML;
+};
+
+const expiresInFromExpiresAt = expires_at => {
+  if (!expires_at) return 24 * 3600;
+  const delta = (new Date(expires_at).getTime() - Date.now()) / 1000;
+  return [300, 1800, 3600, 21600, 86400, 259200, 604800].find(expires_in => expires_in >= delta) || 24 * 3600;
+};
+
+const mergeLocalHashtagResults = (suggestions, prefix, tagHistory) => {
+  prefix = prefix.toLowerCase();
+  if (suggestions.length < 4) {
+    const localTags = tagHistory.filter(tag => tag.toLowerCase().startsWith(prefix) && !suggestions.some(suggestion => suggestion.type === 'hashtag' && suggestion.name.toLowerCase() === tag.toLowerCase()));
+    return suggestions.concat(localTags.slice(0, 4 - suggestions.length).toJS().map(tag => ({ type: 'hashtag', name: tag })));
+  } else {
+    return suggestions;
+  }
+};
+
+const normalizeSuggestions = (state, { accounts, emojis, tags, token }) => {
+  if (accounts) {
+    return accounts.map(item => ({ id: item.id, type: 'account' }));
+  } else if (emojis) {
+    return emojis.map(item => ({ ...item, type: 'emoji' }));
+  } else {
+    return mergeLocalHashtagResults(sortHashtagsByUse(state, tags.map(item => ({ ...item, type: 'hashtag' }))), token.slice(1), state.get('tagHistory'));
+  }
+};
+
+const updateSuggestionTags = (state, token) => {
+  const prefix = token.slice(1);
+
+  const suggestions = state.get('suggestions').toJS();
+  return state.merge({
+    suggestions: ImmutableList(mergeLocalHashtagResults(suggestions, prefix, state.get('tagHistory'))),
+    suggestion_token: token,
+  });
+};
+
+export default function compose(state = initialState, action) {
+  switch(action.type) {
+  case STORE_HYDRATE:
+    return hydrate(state, action.state.get('compose'));
+  case COMPOSE_MOUNT:
+    return state.set('mounted', state.get('mounted') + 1);
+  case COMPOSE_UNMOUNT:
+    return state.set('mounted', Math.max(state.get('mounted') - 1, 0));
+  case COMPOSE_ADVANCED_OPTIONS_CHANGE:
+    return state
+      .set('advanced_options', state.get('advanced_options').set(action.option, !!overwrite(!state.getIn(['advanced_options', action.option]), action.value)))
+      .set('idempotencyKey', uuid());
+  case COMPOSE_SENSITIVITY_CHANGE:
+    return state.withMutations(map => {
+      if (!state.get('spoiler')) {
+        map.set('sensitive', !state.get('sensitive'));
+      }
+
+      map.set('idempotencyKey', uuid());
+    });
+  case COMPOSE_SPOILERNESS_CHANGE:
+    return state.withMutations(map => {
+      map.set('spoiler', !state.get('spoiler'));
+      map.set('idempotencyKey', uuid());
+
+      if (!state.get('sensitive') && state.get('media_attachments').size >= 1) {
+        map.set('sensitive', true);
+      }
+    });
+  case COMPOSE_SPOILER_TEXT_CHANGE:
+    return state
+      .set('spoiler_text', action.text)
+      .set('idempotencyKey', uuid());
+  case COMPOSE_VISIBILITY_CHANGE:
+    return state
+      .set('privacy', action.value)
+      .set('idempotencyKey', uuid());
+  case COMPOSE_CONTENT_TYPE_CHANGE:
+    return state
+      .set('content_type', action.value)
+      .set('idempotencyKey', uuid());
+  case COMPOSE_CHANGE:
+    return state
+      .set('text', action.text)
+      .set('idempotencyKey', uuid());
+  case COMPOSE_CYCLE_ELEFRIEND:
+    return state
+      .set('elefriend', (state.get('elefriend') + 1) % totalElefriends);
+  case COMPOSE_REPLY:
+    return state.withMutations(map => {
+      map.set('id', null);
+      map.set('in_reply_to', action.status.get('id'));
+      map.set('text', statusToTextMentions(state, action.status));
+      map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
+      map.update(
+        'advanced_options',
+        map => map.merge(new ImmutableMap({ do_not_federate: !!action.status.get('local_only') })),
+      );
+      map.set('focusDate', new Date());
+      map.set('caretPosition', null);
+      map.set('preselectDate', new Date());
+      map.set('idempotencyKey', uuid());
+
+      map.update('media_attachments', list => list.filter(media => media.get('unattached')));
+
+      if (action.status.get('language') && !action.status.has('translation')) {
+        map.set('language', action.status.get('language'));
+      } else {
+        map.set('language', state.get('default_language'));
+      }
+
+      if (action.status.get('spoiler_text').length > 0) {
+        let spoiler_text = action.status.get('spoiler_text');
+        if (action.prependCWRe && !spoiler_text.match(/^re[: ]/i)) {
+          spoiler_text = 're: '.concat(spoiler_text);
+        }
+        map.set('spoiler', true);
+        map.set('spoiler_text', spoiler_text);
+      } else {
+        map.set('spoiler', false);
+        map.set('spoiler_text', '');
+      }
+    });
+  case COMPOSE_REPLY_CANCEL:
+    state = state.setIn(['advanced_options', 'threaded_mode'], false);
+    // eslint-disable-next-line no-fallthrough -- fall-through to `COMPOSE_RESET` is intended
+  case COMPOSE_RESET:
+    return state.withMutations(map => {
+      map.set('in_reply_to', null);
+      if (defaultContentType) map.set('content_type', defaultContentType);
+      map.set('text', '');
+      map.set('spoiler', false);
+      map.set('spoiler_text', '');
+      map.set('privacy', state.get('default_privacy'));
+      map.set('id', null);
+      map.set('poll', null);
+      map.set('language', state.get('default_language'));
+      map.update(
+        'advanced_options',
+        map => map.mergeWith(overwrite, state.get('default_advanced_options')),
+      );
+      map.set('idempotencyKey', uuid());
+    });
+  case COMPOSE_SUBMIT_REQUEST:
+    return state.set('is_submitting', true);
+  case COMPOSE_UPLOAD_CHANGE_REQUEST:
+    return state.set('is_changing_upload', true);
+  case COMPOSE_SUBMIT_SUCCESS:
+    return action.status && state.getIn(['advanced_options', 'threaded_mode']) ? continueThread(state, action.status) : clearAll(state);
+  case COMPOSE_SUBMIT_FAIL:
+    return state.set('is_submitting', false);
+  case COMPOSE_UPLOAD_CHANGE_FAIL:
+    return state.set('is_changing_upload', false);
+  case COMPOSE_UPLOAD_REQUEST:
+    return state.set('is_uploading', true).update('pending_media_attachments', n => n + 1);
+  case COMPOSE_UPLOAD_PROCESSING:
+    return state.set('is_processing', true);
+  case COMPOSE_UPLOAD_SUCCESS:
+    return appendMedia(state, fromJS(action.media), action.file);
+  case COMPOSE_UPLOAD_FAIL:
+    return state.set('is_uploading', false).set('is_processing', false).update('pending_media_attachments', n => n - 1);
+  case COMPOSE_UPLOAD_UNDO:
+    return removeMedia(state, action.media_id);
+  case COMPOSE_UPLOAD_PROGRESS:
+    return state.set('progress', Math.round((action.loaded / action.total) * 100));
+  case THUMBNAIL_UPLOAD_REQUEST:
+    return state.set('isUploadingThumbnail', true);
+  case THUMBNAIL_UPLOAD_PROGRESS:
+    return state.set('thumbnailProgress', Math.round((action.loaded / action.total) * 100));
+  case THUMBNAIL_UPLOAD_FAIL:
+    return state.set('isUploadingThumbnail', false);
+  case THUMBNAIL_UPLOAD_SUCCESS:
+    return state
+      .set('isUploadingThumbnail', false)
+      .update('media_attachments', list => list.map(item => {
+        if (item.get('id') === action.media.id) {
+          return fromJS(action.media);
+        }
+
+        return item;
+      }));
+  case INIT_MEDIA_EDIT_MODAL:
+    const media =  state.get('media_attachments').find(item => item.get('id') === action.id);
+    return state.set('media_modal', ImmutableMap({
+      id: action.id,
+      description: media.get('description') || '',
+      focusX: media.getIn(['meta', 'focus', 'x'], 0),
+      focusY: media.getIn(['meta', 'focus', 'y'], 0),
+      dirty: false,
+    }));
+  case COMPOSE_CHANGE_MEDIA_DESCRIPTION:
+    return state.setIn(['media_modal', 'description'], action.description).setIn(['media_modal', 'dirty'], true);
+  case COMPOSE_CHANGE_MEDIA_FOCUS:
+    return state.setIn(['media_modal', 'focusX'], action.focusX).setIn(['media_modal', 'focusY'], action.focusY).setIn(['media_modal', 'dirty'], true);
+  case COMPOSE_MENTION:
+    return state.withMutations(map => {
+      map.update('text', text => [text.trim(), `@${action.account.get('acct')} `].filter((str) => str.length !== 0).join(' '));
+      map.set('focusDate', new Date());
+      map.set('caretPosition', null);
+      map.set('idempotencyKey', uuid());
+    });
+  case COMPOSE_DIRECT:
+    return state.withMutations(map => {
+      map.update('text', text => [text.trim(), `@${action.account.get('acct')} `].filter((str) => str.length !== 0).join(' '));
+      map.set('privacy', 'direct');
+      map.set('focusDate', new Date());
+      map.set('caretPosition', null);
+      map.set('idempotencyKey', uuid());
+    });
+  case COMPOSE_SUGGESTIONS_CLEAR:
+    return state.update('suggestions', ImmutableList(), list => list.clear()).set('suggestion_token', null);
+  case COMPOSE_SUGGESTIONS_READY:
+    return state.set('suggestions', ImmutableList(normalizeSuggestions(state, action))).set('suggestion_token', action.token);
+  case COMPOSE_SUGGESTION_SELECT:
+    return insertSuggestion(state, action.position, action.token, action.completion, action.path);
+  case COMPOSE_SUGGESTION_IGNORE:
+    return ignoreSuggestion(state, action.position, action.token, action.completion, action.path);
+  case COMPOSE_SUGGESTION_TAGS_UPDATE:
+    return updateSuggestionTags(state, action.token);
+  case COMPOSE_TAG_HISTORY_UPDATE:
+    return state.set('tagHistory', fromJS(action.tags));
+  case TIMELINE_DELETE:
+    if (action.id === state.get('in_reply_to')) {
+      return state.set('in_reply_to', null);
+    } else if (action.id === state.get('id')) {
+      return state.set('id', null);
+    } else {
+      return state;
+    }
+  case COMPOSE_EMOJI_INSERT:
+    return insertEmoji(state, action.position, action.emoji);
+  case COMPOSE_UPLOAD_CHANGE_SUCCESS:
+    return state
+      .set('is_changing_upload', false)
+      .setIn(['media_modal', 'dirty'], false)
+      .update('media_attachments', list => list.map(item => {
+        if (item.get('id') === action.media.id) {
+          return fromJS(action.media).set('unattached', !action.attached);
+        }
+
+        return item;
+      }));
+  case COMPOSE_DOODLE_SET:
+    return state.mergeIn(['doodle'], action.options);
+  case REDRAFT:
+    const do_not_federate = !!action.status.get('local_only');
+    let text = action.raw_text || unescapeHTML(expandMentions(action.status));
+    if (do_not_federate) text = text.replace(/ ?👁\ufe0f?\u200b?$/, '');
+    return state.withMutations(map => {
+      map.set('text', text);
+      map.set('content_type', action.content_type || 'text/plain');
+      map.set('in_reply_to', action.status.get('in_reply_to_id'));
+      map.set('privacy', action.status.get('visibility'));
+      map.set('media_attachments', action.status.get('media_attachments').map((media) => media.set('unattached', true)));
+      map.set('focusDate', new Date());
+      map.set('caretPosition', null);
+      map.set('idempotencyKey', uuid());
+      map.set('sensitive', action.status.get('sensitive'));
+      map.set('language', action.status.get('language'));
+      map.update(
+        'advanced_options',
+        map => map.merge(new ImmutableMap({ do_not_federate })),
+      );
+      map.set('id', null);
+
+      if (action.status.get('spoiler_text').length > 0) {
+        map.set('spoiler', true);
+        map.set('spoiler_text', action.status.get('spoiler_text'));
+
+        if (map.get('media_attachments').size >= 1) {
+          map.set('sensitive', true);
+        }
+      } else {
+        map.set('spoiler', false);
+        map.set('spoiler_text', '');
+      }
+
+      if (action.status.get('poll')) {
+        map.set('poll', ImmutableMap({
+          options: action.status.getIn(['poll', 'options']).map(x => x.get('title')),
+          multiple: action.status.getIn(['poll', 'multiple']),
+          expires_in: expiresInFromExpiresAt(action.status.getIn(['poll', 'expires_at'])),
+        }));
+      }
+    });
+  case COMPOSE_SET_STATUS:
+    return state.withMutations(map => {
+      map.set('id', action.status.get('id'));
+      map.set('text', action.text);
+      map.set('content_type', action.content_type || 'text/plain');
+      map.set('in_reply_to', action.status.get('in_reply_to_id'));
+      map.set('privacy', action.status.get('visibility'));
+      map.set('media_attachments', action.status.get('media_attachments'));
+      map.set('focusDate', new Date());
+      map.set('caretPosition', null);
+      map.set('idempotencyKey', uuid());
+      map.set('sensitive', action.status.get('sensitive'));
+      map.set('language', action.status.get('language'));
+
+      if (action.spoiler_text.length > 0) {
+        map.set('spoiler', true);
+        map.set('spoiler_text', action.spoiler_text);
+      } else {
+        map.set('spoiler', false);
+        map.set('spoiler_text', '');
+      }
+
+      if (action.status.get('poll')) {
+        map.set('poll', ImmutableMap({
+          options: action.status.getIn(['poll', 'options']).map(x => x.get('title')),
+          multiple: action.status.getIn(['poll', 'multiple']),
+          expires_in: expiresInFromExpiresAt(action.status.getIn(['poll', 'expires_at'])),
+        }));
+      }
+    });
+  case COMPOSE_POLL_ADD:
+    return state.set('poll', initialPoll);
+  case COMPOSE_POLL_REMOVE:
+    return state.set('poll', null);
+  case COMPOSE_POLL_OPTION_ADD:
+    return state.updateIn(['poll', 'options'], options => options.push(action.title));
+  case COMPOSE_POLL_OPTION_CHANGE:
+    return state.setIn(['poll', 'options', action.index], action.title);
+  case COMPOSE_POLL_OPTION_REMOVE:
+    return state.updateIn(['poll', 'options'], options => options.delete(action.index));
+  case COMPOSE_POLL_SETTINGS_CHANGE:
+    return state.update('poll', poll => poll.set('expires_in', action.expiresIn).set('multiple', action.isMultiple));
+  case COMPOSE_LANGUAGE_CHANGE:
+    return state.set('language', action.language);
+  case COMPOSE_FOCUS:
+    return state.set('focusDate', new Date()).update('text', text => text.length > 0 ? text : action.defaultText);
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/reducers/contexts.js b/app/javascript/flavours/blobfox/reducers/contexts.js
new file mode 100644
index 00000000000000..f7d7419a4e3ab9
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/contexts.js
@@ -0,0 +1,107 @@
+import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
+
+import {
+  blockAccountSuccess,
+  muteAccountSuccess,
+} from '../actions/accounts';
+import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses';
+import { TIMELINE_DELETE, TIMELINE_UPDATE } from '../actions/timelines';
+import { compareId } from '../compare_id';
+
+const initialState = ImmutableMap({
+  inReplyTos: ImmutableMap(),
+  replies: ImmutableMap(),
+});
+
+const normalizeContext = (immutableState, id, ancestors, descendants) => immutableState.withMutations(state => {
+  state.update('inReplyTos', immutableAncestors => immutableAncestors.withMutations(inReplyTos => {
+    state.update('replies', immutableDescendants => immutableDescendants.withMutations(replies => {
+      function addReply({ id, in_reply_to_id }) {
+        if (in_reply_to_id && !inReplyTos.has(id)) {
+
+          replies.update(in_reply_to_id, ImmutableList(), siblings => {
+            const index = siblings.findLastIndex(sibling => compareId(sibling, id) < 0);
+            return siblings.insert(index + 1, id);
+          });
+
+          inReplyTos.set(id, in_reply_to_id);
+        }
+      }
+
+      // We know in_reply_to_id of statuses but `id` itself.
+      // So we assume that the status of the id replies to last ancestors.
+
+      ancestors.forEach(addReply);
+
+      if (ancestors[0]) {
+        addReply({ id, in_reply_to_id: ancestors[ancestors.length - 1].id });
+      }
+
+      descendants.forEach(addReply);
+    }));
+  }));
+});
+
+const deleteFromContexts = (immutableState, ids) => immutableState.withMutations(state => {
+  state.update('inReplyTos', immutableAncestors => immutableAncestors.withMutations(inReplyTos => {
+    state.update('replies', immutableDescendants => immutableDescendants.withMutations(replies => {
+      ids.forEach(id => {
+        const inReplyToIdOfId = inReplyTos.get(id);
+        const repliesOfId = replies.get(id);
+        const siblings = replies.get(inReplyToIdOfId);
+
+        if (siblings) {
+          replies.set(inReplyToIdOfId, siblings.filterNot(sibling => sibling === id));
+        }
+
+
+        if (repliesOfId) {
+          repliesOfId.forEach(reply => inReplyTos.delete(reply));
+        }
+
+        inReplyTos.delete(id);
+        replies.delete(id);
+      });
+    }));
+  }));
+});
+
+const filterContexts = (state, relationship, statuses) => {
+  const ownedStatusIds = statuses
+    .filter(status => status.get('account') === relationship.id)
+    .map(status => status.get('id'));
+
+  return deleteFromContexts(state, ownedStatusIds);
+};
+
+const updateContext = (state, status) => {
+  if (status.in_reply_to_id) {
+    return state.withMutations(mutable => {
+      const replies = mutable.getIn(['replies', status.in_reply_to_id], ImmutableList());
+
+      mutable.setIn(['inReplyTos', status.id], status.in_reply_to_id);
+
+      if (!replies.includes(status.id)) {
+        mutable.setIn(['replies', status.in_reply_to_id], replies.push(status.id));
+      }
+    });
+  }
+
+  return state;
+};
+
+export default function replies(state = initialState, action) {
+  switch(action.type) {
+  case blockAccountSuccess.type:
+  case muteAccountSuccess.type:
+    return filterContexts(state, action.payload.relationship, action.payload.statuses);
+  case CONTEXT_FETCH_SUCCESS:
+    return normalizeContext(state, action.id, action.ancestors, action.descendants);
+  case TIMELINE_DELETE:
+    return deleteFromContexts(state, [action.id]);
+  case TIMELINE_UPDATE:
+    return updateContext(state, action.status);
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/reducers/conversations.js b/app/javascript/flavours/blobfox/reducers/conversations.js
new file mode 100644
index 00000000000000..f772305d71a457
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/conversations.js
@@ -0,0 +1,118 @@
+import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
+
+import { blockAccountSuccess, muteAccountSuccess } from 'flavours/blobfox/actions/accounts';
+import { blockDomainSuccess } from 'flavours/blobfox/actions/domain_blocks';
+
+import {
+  CONVERSATIONS_MOUNT,
+  CONVERSATIONS_UNMOUNT,
+  CONVERSATIONS_FETCH_REQUEST,
+  CONVERSATIONS_FETCH_SUCCESS,
+  CONVERSATIONS_FETCH_FAIL,
+  CONVERSATIONS_UPDATE,
+  CONVERSATIONS_READ,
+  CONVERSATIONS_DELETE_SUCCESS,
+} from '../actions/conversations';
+import { compareId } from '../compare_id';
+
+const initialState = ImmutableMap({
+  items: ImmutableList(),
+  isLoading: false,
+  hasMore: true,
+  mounted: 0,
+});
+
+const conversationToMap = item => ImmutableMap({
+  id: item.id,
+  unread: item.unread,
+  accounts: ImmutableList(item.accounts.map(a => a.id)),
+  last_status: item.last_status ? item.last_status.id : null,
+});
+
+const updateConversation = (state, item) => state.update('items', list => {
+  const index   = list.findIndex(x => x.get('id') === item.id);
+  const newItem = conversationToMap(item);
+
+  if (index === -1) {
+    return list.unshift(newItem);
+  } else {
+    return list.set(index, newItem);
+  }
+});
+
+const expandNormalizedConversations = (state, conversations, next, isLoadingRecent) => {
+  let items = ImmutableList(conversations.map(conversationToMap));
+
+  return state.withMutations(mutable => {
+    if (!items.isEmpty()) {
+      mutable.update('items', list => {
+        list = list.map(oldItem => {
+          const newItemIndex = items.findIndex(x => x.get('id') === oldItem.get('id'));
+
+          if (newItemIndex === -1) {
+            return oldItem;
+          }
+
+          const newItem = items.get(newItemIndex);
+          items = items.delete(newItemIndex);
+
+          return newItem;
+        });
+
+        list = list.concat(items);
+
+        return list.sortBy(x => x.get('last_status'), (a, b) => {
+          if(a === null || b === null) {
+            return -1;
+          }
+
+          return compareId(a, b) * -1;
+        });
+      });
+    }
+
+    if (!next && !isLoadingRecent) {
+      mutable.set('hasMore', false);
+    }
+
+    mutable.set('isLoading', false);
+  });
+};
+
+const filterConversations = (state, accountIds) => {
+  return state.update('items', list => list.filterNot(item => item.get('accounts').some(accountId => accountIds.includes(accountId))));
+};
+
+export default function conversations(state = initialState, action) {
+  switch (action.type) {
+  case CONVERSATIONS_FETCH_REQUEST:
+    return state.set('isLoading', true);
+  case CONVERSATIONS_FETCH_FAIL:
+    return state.set('isLoading', false);
+  case CONVERSATIONS_FETCH_SUCCESS:
+    return expandNormalizedConversations(state, action.conversations, action.next, action.isLoadingRecent);
+  case CONVERSATIONS_UPDATE:
+    return updateConversation(state, action.conversation);
+  case CONVERSATIONS_MOUNT:
+    return state.update('mounted', count => count + 1);
+  case CONVERSATIONS_UNMOUNT:
+    return state.update('mounted', count => count - 1);
+  case CONVERSATIONS_READ:
+    return state.update('items', list => list.map(item => {
+      if (item.get('id') === action.id) {
+        return item.set('unread', false);
+      }
+
+      return item;
+    }));
+  case blockAccountSuccess.type:
+  case muteAccountSuccess.type:
+    return filterConversations(state, [action.payload.relationship.id]);
+  case blockDomainSuccess.type:
+    return filterConversations(state, action.payload.accounts);
+  case CONVERSATIONS_DELETE_SUCCESS:
+    return state.update('items', list => list.filterNot(item => item.get('id') === action.id));
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/reducers/custom_emojis.js b/app/javascript/flavours/blobfox/reducers/custom_emojis.js
new file mode 100644
index 00000000000000..56ec80f2ffce22
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/custom_emojis.js
@@ -0,0 +1,16 @@
+import { List as ImmutableList, fromJS as ConvertToImmutable } from 'immutable';
+
+import { CUSTOM_EMOJIS_FETCH_SUCCESS } from '../actions/custom_emojis';
+import { buildCustomEmojis } from '../features/emoji/emoji';
+import { search as emojiSearch } from '../features/emoji/emoji_mart_search_light';
+
+const initialState = ImmutableList([]);
+
+export default function custom_emojis(state = initialState, action) {
+  if(action.type === CUSTOM_EMOJIS_FETCH_SUCCESS) {
+    state = ConvertToImmutable(action.custom_emojis);
+    emojiSearch('', { custom: buildCustomEmojis(state) });
+  }
+
+  return state;
+}
diff --git a/app/javascript/flavours/blobfox/reducers/domain_lists.js b/app/javascript/flavours/blobfox/reducers/domain_lists.js
new file mode 100644
index 00000000000000..5f63c77f5d4200
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/domain_lists.js
@@ -0,0 +1,26 @@
+import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
+
+import {
+  DOMAIN_BLOCKS_FETCH_SUCCESS,
+  DOMAIN_BLOCKS_EXPAND_SUCCESS,
+  unblockDomainSuccess
+} from '../actions/domain_blocks';
+
+const initialState = ImmutableMap({
+  blocks: ImmutableMap({
+    items: ImmutableOrderedSet(),
+  }),
+});
+
+export default function domainLists(state = initialState, action) {
+  switch(action.type) {
+  case DOMAIN_BLOCKS_FETCH_SUCCESS:
+    return state.setIn(['blocks', 'items'], ImmutableOrderedSet(action.domains)).setIn(['blocks', 'next'], action.next);
+  case DOMAIN_BLOCKS_EXPAND_SUCCESS:
+    return state.updateIn(['blocks', 'items'], set => set.union(action.domains)).setIn(['blocks', 'next'], action.next);
+  case unblockDomainSuccess.type:
+    return state.updateIn(['blocks', 'items'], set => set.delete(action.payload.domain));
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/reducers/dropdown_menu.ts b/app/javascript/flavours/blobfox/reducers/dropdown_menu.ts
new file mode 100644
index 00000000000000..59e19bb16d28c2
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/dropdown_menu.ts
@@ -0,0 +1,33 @@
+import { createReducer } from '@reduxjs/toolkit';
+
+import { closeDropdownMenu, openDropdownMenu } from '../actions/dropdown_menu';
+
+interface DropdownMenuState {
+  openId: string | null;
+  keyboard: boolean;
+  scrollKey: string | null;
+}
+
+const initialState: DropdownMenuState = {
+  openId: null,
+  keyboard: false,
+  scrollKey: null,
+};
+
+export const dropdownMenuReducer = createReducer(initialState, (builder) => {
+  builder
+    .addCase(
+      openDropdownMenu,
+      (state, { payload: { id, keyboard, scrollKey } }) => {
+        state.openId = id;
+        state.keyboard = keyboard;
+        state.scrollKey = scrollKey;
+      },
+    )
+    .addCase(closeDropdownMenu, (state, { payload: { id } }) => {
+      if (state.openId === id) {
+        state.openId = null;
+        state.scrollKey = null;
+      }
+    });
+});
diff --git a/app/javascript/flavours/blobfox/reducers/filters.js b/app/javascript/flavours/blobfox/reducers/filters.js
new file mode 100644
index 00000000000000..566ad0c6ca3486
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/filters.js
@@ -0,0 +1,45 @@
+import { Map as ImmutableMap, is, fromJS } from 'immutable';
+
+import { FILTERS_FETCH_SUCCESS, FILTERS_CREATE_SUCCESS } from '../actions/filters';
+import { FILTERS_IMPORT } from '../actions/importer';
+
+const normalizeFilter = (state, filter) => {
+  const normalizedFilter = fromJS({
+    id: filter.id,
+    title: filter.title,
+    context: filter.context,
+    filter_action: filter.filter_action,
+    keywords: filter.keywords,
+    expires_at: filter.expires_at ? Date.parse(filter.expires_at) : null,
+  });
+
+  if (is(state.get(filter.id), normalizedFilter)) {
+    return state;
+  } else {
+    // Do not overwrite keywords when receiving a partial filter
+    return state.update(filter.id, ImmutableMap(), (old) => (
+      old.mergeWith(((old_value, new_value) => (new_value === undefined ? old_value : new_value)), normalizedFilter)
+    ));
+  }
+};
+
+const normalizeFilters = (state, filters) => {
+  filters.forEach(filter => {
+    state = normalizeFilter(state, filter);
+  });
+
+  return state;
+};
+
+export default function filters(state = ImmutableMap(), action) {
+  switch(action.type) {
+  case FILTERS_CREATE_SUCCESS:
+    return normalizeFilter(state, action.filter);
+  case FILTERS_FETCH_SUCCESS:
+    return normalizeFilters(ImmutableMap(), action.filters);
+  case FILTERS_IMPORT:
+    return normalizeFilters(state, action.filters);
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/reducers/followed_tags.js b/app/javascript/flavours/blobfox/reducers/followed_tags.js
new file mode 100644
index 00000000000000..fa20c9ad6e4ce4
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/followed_tags.js
@@ -0,0 +1,43 @@
+import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
+
+import {
+  FOLLOWED_HASHTAGS_FETCH_REQUEST,
+  FOLLOWED_HASHTAGS_FETCH_SUCCESS,
+  FOLLOWED_HASHTAGS_FETCH_FAIL,
+  FOLLOWED_HASHTAGS_EXPAND_REQUEST,
+  FOLLOWED_HASHTAGS_EXPAND_SUCCESS,
+  FOLLOWED_HASHTAGS_EXPAND_FAIL,
+} from 'flavours/blobfox/actions/tags';
+
+const initialState = ImmutableMap({
+  items: ImmutableList(),
+  isLoading: false,
+  next: null,
+});
+
+export default function followed_tags(state = initialState, action) {
+  switch(action.type) {
+  case FOLLOWED_HASHTAGS_FETCH_REQUEST:
+    return state.set('isLoading', true);
+  case FOLLOWED_HASHTAGS_FETCH_SUCCESS:
+    return state.withMutations(map => {
+      map.set('items', fromJS(action.followed_tags));
+      map.set('isLoading', false);
+      map.set('next', action.next);
+    });
+  case FOLLOWED_HASHTAGS_FETCH_FAIL:
+    return state.set('isLoading', false);
+  case FOLLOWED_HASHTAGS_EXPAND_REQUEST:
+    return state.set('isLoading', true);
+  case FOLLOWED_HASHTAGS_EXPAND_SUCCESS:
+    return state.withMutations(map => {
+      map.update('items', set => set.concat(fromJS(action.followed_tags)));
+      map.set('isLoading', false);
+      map.set('next', action.next);
+    });
+  case FOLLOWED_HASHTAGS_EXPAND_FAIL:
+    return state.set('isLoading', false);
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/reducers/height_cache.js b/app/javascript/flavours/blobfox/reducers/height_cache.js
new file mode 100644
index 00000000000000..2664d4f82463f7
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/height_cache.js
@@ -0,0 +1,24 @@
+import { Map as ImmutableMap } from 'immutable';
+
+import { HEIGHT_CACHE_SET, HEIGHT_CACHE_CLEAR } from '../actions/height_cache';
+
+const initialState = ImmutableMap();
+
+const setHeight = (state, key, id, height) => {
+  return state.update(key, ImmutableMap(), map => map.set(id, height));
+};
+
+const clearHeights = () => {
+  return ImmutableMap();
+};
+
+export default function statuses(state = initialState, action) {
+  switch(action.type) {
+  case HEIGHT_CACHE_SET:
+    return setHeight(state, action.key, action.id, action.height);
+  case HEIGHT_CACHE_CLEAR:
+    return clearHeights();
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/reducers/history.js b/app/javascript/flavours/blobfox/reducers/history.js
new file mode 100644
index 00000000000000..023e5ecedd8af0
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/history.js
@@ -0,0 +1,29 @@
+import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
+
+import { HISTORY_FETCH_REQUEST, HISTORY_FETCH_SUCCESS, HISTORY_FETCH_FAIL } from 'flavours/blobfox/actions/history';
+
+const initialHistory = ImmutableMap({
+  loading: false,
+  items: ImmutableList(),
+});
+
+const initialState = ImmutableMap();
+
+export default function history(state = initialState, action) {
+  switch(action.type) {
+  case HISTORY_FETCH_REQUEST:
+    return state.update(action.statusId, initialHistory, history => history.withMutations(map => {
+      map.set('loading', true);
+      map.set('items', ImmutableList());
+    }));
+  case HISTORY_FETCH_SUCCESS:
+    return state.update(action.statusId, initialHistory, history => history.withMutations(map => {
+      map.set('loading', false);
+      map.set('items', fromJS(action.history.map((x, i) => ({ ...x, account: x.account.id, original: i === 0 })).reverse()));
+    }));
+  case HISTORY_FETCH_FAIL:
+    return state.update(action.statusId, initialHistory, history => history.set('loading', false));
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/reducers/index.ts b/app/javascript/flavours/blobfox/reducers/index.ts
new file mode 100644
index 00000000000000..4775c076e7837d
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/index.ts
@@ -0,0 +1,111 @@
+import { Record as ImmutableRecord } from 'immutable';
+
+import { loadingBarReducer } from 'react-redux-loading-bar';
+import { combineReducers } from 'redux-immutable';
+
+import { accountsReducer } from './accounts';
+import accounts_map from './accounts_map';
+import alerts from './alerts';
+import announcements from './announcements';
+import blocks from './blocks';
+import boosts from './boosts';
+import compose from './compose';
+import contexts from './contexts';
+import conversations from './conversations';
+import custom_emojis from './custom_emojis';
+import domain_lists from './domain_lists';
+import { dropdownMenuReducer } from './dropdown_menu';
+import filters from './filters';
+import followed_tags from './followed_tags';
+import height_cache from './height_cache';
+import history from './history';
+import listAdder from './list_adder';
+import listEditor from './list_editor';
+import lists from './lists';
+import local_settings from './local_settings';
+import markers from './markers';
+import media_attachments from './media_attachments';
+import meta from './meta';
+import { modalReducer } from './modal';
+import mutes from './mutes';
+import notifications from './notifications';
+import picture_in_picture from './picture_in_picture';
+import pinnedAccountsEditor from './pinned_accounts_editor';
+import polls from './polls';
+import push_notifications from './push_notifications';
+import { relationshipsReducer } from './relationships';
+import search from './search';
+import server from './server';
+import settings from './settings';
+import status_lists from './status_lists';
+import statuses from './statuses';
+import suggestions from './suggestions';
+import tags from './tags';
+import timelines from './timelines';
+import trends from './trends';
+import user_lists from './user_lists';
+
+const reducers = {
+  announcements,
+  dropdownMenu: dropdownMenuReducer,
+  timelines,
+  meta,
+  alerts,
+  loadingBar: loadingBarReducer,
+  modal: modalReducer,
+  user_lists,
+  domain_lists,
+  status_lists,
+  accounts: accountsReducer,
+  accounts_map,
+  statuses,
+  relationships: relationshipsReducer,
+  settings,
+  local_settings,
+  push_notifications,
+  mutes,
+  blocks,
+  boosts,
+  server,
+  contexts,
+  compose,
+  search,
+  media_attachments,
+  notifications,
+  height_cache,
+  custom_emojis,
+  lists,
+  listEditor,
+  listAdder,
+  filters,
+  conversations,
+  suggestions,
+  pinnedAccountsEditor,
+  polls,
+  trends,
+  markers,
+  picture_in_picture,
+  history,
+  tags,
+  followed_tags,
+};
+
+// We want the root state to be an ImmutableRecord, which is an object with a defined list of keys,
+// so it is properly typed and keys can be accessed using `state.<key>` syntax.
+// This will allow an easy conversion to a plain object once we no longer call `get` or `getIn` on the root state
+
+// By default with `combineReducers` it is a Collection, so we provide our own implementation to get a Record
+const initialRootState = Object.fromEntries(
+  Object.entries(reducers).map(([name, reducer]) => [
+    name,
+    reducer(undefined, {
+      // empty action
+    }),
+  ]),
+);
+
+const RootStateRecord = ImmutableRecord(initialRootState, 'RootState');
+
+const rootReducer = combineReducers(reducers, RootStateRecord);
+
+export { rootReducer };
diff --git a/app/javascript/flavours/blobfox/reducers/list_adder.js b/app/javascript/flavours/blobfox/reducers/list_adder.js
new file mode 100644
index 00000000000000..0f61273aa6c574
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/list_adder.js
@@ -0,0 +1,48 @@
+import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
+
+import {
+  LIST_ADDER_RESET,
+  LIST_ADDER_SETUP,
+  LIST_ADDER_LISTS_FETCH_REQUEST,
+  LIST_ADDER_LISTS_FETCH_SUCCESS,
+  LIST_ADDER_LISTS_FETCH_FAIL,
+  LIST_EDITOR_ADD_SUCCESS,
+  LIST_EDITOR_REMOVE_SUCCESS,
+} from '../actions/lists';
+
+const initialState = ImmutableMap({
+  accountId: null,
+
+  lists: ImmutableMap({
+    items: ImmutableList(),
+    loaded: false,
+    isLoading: false,
+  }),
+});
+
+export default function listAdderReducer(state = initialState, action) {
+  switch(action.type) {
+  case LIST_ADDER_RESET:
+    return initialState;
+  case LIST_ADDER_SETUP:
+    return state.withMutations(map => {
+      map.set('accountId', action.account.get('id'));
+    });
+  case LIST_ADDER_LISTS_FETCH_REQUEST:
+    return state.setIn(['lists', 'isLoading'], true);
+  case LIST_ADDER_LISTS_FETCH_FAIL:
+    return state.setIn(['lists', 'isLoading'], false);
+  case LIST_ADDER_LISTS_FETCH_SUCCESS:
+    return state.update('lists', lists => lists.withMutations(map => {
+      map.set('isLoading', false);
+      map.set('loaded', true);
+      map.set('items', ImmutableList(action.lists.map(item => item.id)));
+    }));
+  case LIST_EDITOR_ADD_SUCCESS:
+    return state.updateIn(['lists', 'items'], list => list.unshift(action.listId));
+  case LIST_EDITOR_REMOVE_SUCCESS:
+    return state.updateIn(['lists', 'items'], list => list.filterNot(item => item === action.listId));
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/reducers/list_editor.js b/app/javascript/flavours/blobfox/reducers/list_editor.js
new file mode 100644
index 00000000000000..d3fd62adecbced
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/list_editor.js
@@ -0,0 +1,99 @@
+import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
+
+import {
+  LIST_CREATE_REQUEST,
+  LIST_CREATE_FAIL,
+  LIST_CREATE_SUCCESS,
+  LIST_UPDATE_REQUEST,
+  LIST_UPDATE_FAIL,
+  LIST_UPDATE_SUCCESS,
+  LIST_EDITOR_RESET,
+  LIST_EDITOR_SETUP,
+  LIST_EDITOR_TITLE_CHANGE,
+  LIST_ACCOUNTS_FETCH_REQUEST,
+  LIST_ACCOUNTS_FETCH_SUCCESS,
+  LIST_ACCOUNTS_FETCH_FAIL,
+  LIST_EDITOR_SUGGESTIONS_READY,
+  LIST_EDITOR_SUGGESTIONS_CLEAR,
+  LIST_EDITOR_SUGGESTIONS_CHANGE,
+  LIST_EDITOR_ADD_SUCCESS,
+  LIST_EDITOR_REMOVE_SUCCESS,
+} from '../actions/lists';
+
+const initialState = ImmutableMap({
+  listId: null,
+  isSubmitting: false,
+  isChanged: false,
+  title: '',
+  isExclusive: false,
+
+  accounts: ImmutableMap({
+    items: ImmutableList(),
+    loaded: false,
+    isLoading: false,
+  }),
+
+  suggestions: ImmutableMap({
+    value: '',
+    items: ImmutableList(),
+  }),
+});
+
+export default function listEditorReducer(state = initialState, action) {
+  switch(action.type) {
+  case LIST_EDITOR_RESET:
+    return initialState;
+  case LIST_EDITOR_SETUP:
+    return state.withMutations(map => {
+      map.set('listId', action.list.get('id'));
+      map.set('title', action.list.get('title'));
+      map.set('isExclusive', action.list.get('is_exclusive'));
+      map.set('isSubmitting', false);
+    });
+  case LIST_EDITOR_TITLE_CHANGE:
+    return state.withMutations(map => {
+      map.set('title', action.value);
+      map.set('isChanged', true);
+    });
+  case LIST_CREATE_REQUEST:
+  case LIST_UPDATE_REQUEST:
+    return state.withMutations(map => {
+      map.set('isSubmitting', true);
+      map.set('isChanged', false);
+    });
+  case LIST_CREATE_FAIL:
+  case LIST_UPDATE_FAIL:
+    return state.set('isSubmitting', false);
+  case LIST_CREATE_SUCCESS:
+  case LIST_UPDATE_SUCCESS:
+    return state.withMutations(map => {
+      map.set('isSubmitting', false);
+      map.set('listId', action.list.id);
+    });
+  case LIST_ACCOUNTS_FETCH_REQUEST:
+    return state.setIn(['accounts', 'isLoading'], true);
+  case LIST_ACCOUNTS_FETCH_FAIL:
+    return state.setIn(['accounts', 'isLoading'], false);
+  case LIST_ACCOUNTS_FETCH_SUCCESS:
+    return state.update('accounts', accounts => accounts.withMutations(map => {
+      map.set('isLoading', false);
+      map.set('loaded', true);
+      map.set('items', ImmutableList(action.accounts.map(item => item.id)));
+    }));
+  case LIST_EDITOR_SUGGESTIONS_CHANGE:
+    return state.setIn(['suggestions', 'value'], action.value);
+  case LIST_EDITOR_SUGGESTIONS_READY:
+    return state.setIn(['suggestions', 'items'], ImmutableList(action.accounts.map(item => item.id)));
+  case LIST_EDITOR_SUGGESTIONS_CLEAR:
+    return state.update('suggestions', suggestions => suggestions.withMutations(map => {
+      map.set('items', ImmutableList());
+      map.set('value', '');
+    }));
+  case LIST_EDITOR_ADD_SUCCESS:
+    return state.updateIn(['accounts', 'items'], list => list.unshift(action.accountId));
+  case LIST_EDITOR_REMOVE_SUCCESS:
+    return state.updateIn(['accounts', 'items'], list => list.filterNot(item => item === action.accountId));
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/reducers/lists.js b/app/javascript/flavours/blobfox/reducers/lists.js
new file mode 100644
index 00000000000000..2a797772b30437
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/lists.js
@@ -0,0 +1,38 @@
+import { Map as ImmutableMap, fromJS } from 'immutable';
+
+import {
+  LIST_FETCH_SUCCESS,
+  LIST_FETCH_FAIL,
+  LISTS_FETCH_SUCCESS,
+  LIST_CREATE_SUCCESS,
+  LIST_UPDATE_SUCCESS,
+  LIST_DELETE_SUCCESS,
+} from '../actions/lists';
+
+const initialState = ImmutableMap();
+
+const normalizeList = (state, list) => state.set(list.id, fromJS(list));
+
+const normalizeLists = (state, lists) => {
+  lists.forEach(list => {
+    state = normalizeList(state, list);
+  });
+
+  return state;
+};
+
+export default function lists(state = initialState, action) {
+  switch(action.type) {
+  case LIST_FETCH_SUCCESS:
+  case LIST_CREATE_SUCCESS:
+  case LIST_UPDATE_SUCCESS:
+    return normalizeList(state, action.list);
+  case LISTS_FETCH_SUCCESS:
+    return normalizeLists(state, action.lists);
+  case LIST_DELETE_SUCCESS:
+  case LIST_FETCH_FAIL:
+    return state.set(action.id, false);
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/reducers/local_settings.js b/app/javascript/flavours/blobfox/reducers/local_settings.js
new file mode 100644
index 00000000000000..26da423839502e
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/local_settings.js
@@ -0,0 +1,79 @@
+//  Package imports.
+import { Map as ImmutableMap } from 'immutable';
+
+//  Our imports.
+import { LOCAL_SETTING_CHANGE, LOCAL_SETTING_DELETE } from 'flavours/blobfox/actions/local_settings';
+import { STORE_HYDRATE } from 'flavours/blobfox/actions/store';
+
+const initialState = ImmutableMap({
+  stretch   : true,
+  side_arm  : 'none',
+  side_arm_reply_mode : 'keep',
+  show_reply_count : false,
+  always_show_spoilers_field: false,
+  confirm_missing_media_description: false,
+  confirm_boost_missing_media_description: false,
+  confirm_before_clearing_draft: true,
+  prepend_cw_re: true,
+  preselect_on_reply: true,
+  inline_preview_cards: true,
+  hicolor_privacy_icons: false,
+  show_content_type_choice: false,
+  tag_misleading_links: true,
+  rewrite_mentions: 'no',
+  content_warnings : ImmutableMap({
+    filter       : null,
+    media_outside: false,
+    shared_state : false,
+  }),
+  collapsed : ImmutableMap({
+    enabled     : true,
+    auto        : ImmutableMap({
+      all              : false,
+      notifications    : true,
+      lengthy          : true,
+      reblogs          : false,
+      replies          : false,
+      media            : false,
+      height           : 400,
+    }),
+    backgrounds : ImmutableMap({
+      user_backgrounds : false,
+      preview_images   : false,
+    }),
+    show_action_bar : true,
+  }),
+  media     : ImmutableMap({
+    letterbox        : true,
+    fullwidth        : true,
+    reveal_behind_cw : false,
+    pop_in_player    : true,
+    pop_in_position  : 'right',
+  }),
+  notifications : ImmutableMap({
+    favicon_badge : false,
+    tab_badge     : true,
+  }),
+  status_icons : ImmutableMap({
+    language:   true,
+    reply:      true,
+    local_only: true,
+    media:      true,
+    visibility: true,
+  }),
+});
+
+const hydrate = (state, localSettings) => state.mergeDeep(localSettings);
+
+export default function localSettings(state = initialState, action) {
+  switch(action.type) {
+  case STORE_HYDRATE:
+    return hydrate(state, action.state.get('local_settings'));
+  case LOCAL_SETTING_CHANGE:
+    return state.setIn(action.key, action.value);
+  case LOCAL_SETTING_DELETE:
+    return state.deleteIn(action.key);
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/reducers/markers.js b/app/javascript/flavours/blobfox/reducers/markers.js
new file mode 100644
index 00000000000000..c7c5d99f6143f4
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/markers.js
@@ -0,0 +1,26 @@
+import { Map as ImmutableMap } from 'immutable';
+
+import {
+  MARKERS_SUBMIT_SUCCESS,
+} from '../actions/markers';
+
+
+const initialState = ImmutableMap({
+  home: '0',
+  notifications: '0',
+});
+
+export default function markers(state = initialState, action) {
+  switch(action.type) {
+  case MARKERS_SUBMIT_SUCCESS:
+    if (action.home) {
+      state = state.set('home', action.home);
+    }
+    if (action.notifications) {
+      state = state.set('notifications', action.notifications);
+    }
+    return state;
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/reducers/media_attachments.js b/app/javascript/flavours/blobfox/reducers/media_attachments.js
new file mode 100644
index 00000000000000..cbb4933bc7efa8
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/media_attachments.js
@@ -0,0 +1,16 @@
+import { Map as ImmutableMap } from 'immutable';
+
+import { STORE_HYDRATE } from '../actions/store';
+
+const initialState = ImmutableMap({
+  accept_content_types: [],
+});
+
+export default function meta(state = initialState, action) {
+  switch(action.type) {
+  case STORE_HYDRATE:
+    return state.merge(action.state.get('media_attachments'));
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/reducers/meta.js b/app/javascript/flavours/blobfox/reducers/meta.js
new file mode 100644
index 00000000000000..2680969fc98f92
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/meta.js
@@ -0,0 +1,25 @@
+import { Map as ImmutableMap } from 'immutable';
+
+import { changeLayout } from 'flavours/blobfox/actions/app';
+import { STORE_HYDRATE } from 'flavours/blobfox/actions/store';
+import { layoutFromWindow } from 'flavours/blobfox/is_mobile';
+
+const initialState = ImmutableMap({
+  streaming_api_base_url: null,
+  access_token: null,
+  layout: layoutFromWindow(),
+  permissions: '0',
+});
+
+export default function meta(state = initialState, action) {
+  switch(action.type) {
+  case STORE_HYDRATE:
+    return state.merge(action.state.get('meta'))
+      .set('permissions', action.state.getIn(['role', 'permissions']))
+      .set('layout', layoutFromWindow(action.state.getIn(['local_settings', 'layout'])));
+  case changeLayout.type:
+    return state.set('layout', action.payload.layout);
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/reducers/modal.ts b/app/javascript/flavours/blobfox/reducers/modal.ts
new file mode 100644
index 00000000000000..73a2afb916c509
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/modal.ts
@@ -0,0 +1,83 @@
+import { Record as ImmutableRecord, Stack } from 'immutable';
+
+import type { Reducer } from '@reduxjs/toolkit';
+
+import { COMPOSE_UPLOAD_CHANGE_SUCCESS } from '../actions/compose';
+import type { ModalType } from '../actions/modal';
+import { openModal, closeModal } from '../actions/modal';
+import { TIMELINE_DELETE } from '../actions/timelines';
+
+export type ModalProps = Record<string, unknown>;
+interface Modal {
+  modalType: ModalType;
+  modalProps: ModalProps;
+}
+
+const Modal = ImmutableRecord<Modal>({
+  modalType: 'ACTIONS',
+  modalProps: ImmutableRecord({})(),
+});
+
+interface ModalState {
+  ignoreFocus: boolean;
+  stack: Stack<ImmutableRecord<Modal>>;
+}
+
+const initialState = ImmutableRecord<ModalState>({
+  ignoreFocus: false,
+  stack: Stack(),
+})();
+type State = typeof initialState;
+
+interface PopModalOption {
+  modalType: ModalType | undefined;
+  ignoreFocus: boolean;
+}
+const popModal = (
+  state: State,
+  { modalType, ignoreFocus }: PopModalOption,
+): State => {
+  if (
+    modalType === undefined ||
+    modalType === state.get('stack').get(0)?.get('modalType')
+  ) {
+    return state
+      .set('ignoreFocus', !!ignoreFocus)
+      .update('stack', (stack) => stack.shift());
+  } else {
+    return state;
+  }
+};
+
+const pushModal = (
+  state: State,
+  modalType: ModalType,
+  modalProps: ModalProps,
+): State => {
+  return state.withMutations((record) => {
+    record.set('ignoreFocus', false);
+    record.update('stack', (stack) =>
+      stack.unshift(Modal({ modalType, modalProps })),
+    );
+  });
+};
+
+export const modalReducer: Reducer<State> = (state = initialState, action) => {
+  if (openModal.match(action))
+    return pushModal(
+      state,
+      action.payload.modalType,
+      action.payload.modalProps,
+    );
+  else if (closeModal.match(action)) return popModal(state, action.payload);
+  // TODO: type those actions
+  else if (action.type === COMPOSE_UPLOAD_CHANGE_SUCCESS)
+    return popModal(state, { modalType: 'FOCAL_POINT', ignoreFocus: false });
+  else if (action.type === TIMELINE_DELETE)
+    return state.update('stack', (stack) =>
+      stack.filterNot(
+        (modal) => modal.get('modalProps').statusId === action.id,
+      ),
+    );
+  else return state;
+};
diff --git a/app/javascript/flavours/blobfox/reducers/mutes.js b/app/javascript/flavours/blobfox/reducers/mutes.js
new file mode 100644
index 00000000000000..a9eb61ff834cbc
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/mutes.js
@@ -0,0 +1,31 @@
+import Immutable from 'immutable';
+
+import {
+  MUTES_INIT_MODAL,
+  MUTES_TOGGLE_HIDE_NOTIFICATIONS,
+  MUTES_CHANGE_DURATION,
+} from '../actions/mutes';
+
+const initialState = Immutable.Map({
+  new: Immutable.Map({
+    account: null,
+    notifications: true,
+    duration: 0,
+  }),
+});
+
+export default function mutes(state = initialState, action) {
+  switch (action.type) {
+  case MUTES_INIT_MODAL:
+    return state.withMutations((state) => {
+      state.setIn(['new', 'account'], action.account);
+      state.setIn(['new', 'notifications'], true);
+    });
+  case MUTES_TOGGLE_HIDE_NOTIFICATIONS:
+    return state.updateIn(['new', 'notifications'], (old) => !old);
+  case MUTES_CHANGE_DURATION:
+    return state.setIn(['new', 'duration'], Number(action.duration));
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/reducers/notifications.js b/app/javascript/flavours/blobfox/reducers/notifications.js
new file mode 100644
index 00000000000000..9c377319e350cd
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/notifications.js
@@ -0,0 +1,376 @@
+import { fromJS, Map as ImmutableMap, List as ImmutableList } from 'immutable';
+
+import { blockDomainSuccess } from 'flavours/blobfox/actions/domain_blocks';
+
+import {
+  authorizeFollowRequestSuccess,
+  blockAccountSuccess,
+  muteAccountSuccess,
+  rejectFollowRequestSuccess,
+} from '../actions/accounts';
+import {
+  MARKERS_FETCH_SUCCESS,
+} from '../actions/markers';
+import {
+  NOTIFICATIONS_MOUNT,
+  NOTIFICATIONS_UNMOUNT,
+  NOTIFICATIONS_SET_VISIBILITY,
+  notificationsUpdate,
+  NOTIFICATIONS_EXPAND_SUCCESS,
+  NOTIFICATIONS_EXPAND_REQUEST,
+  NOTIFICATIONS_EXPAND_FAIL,
+  NOTIFICATIONS_FILTER_SET,
+  NOTIFICATIONS_CLEAR,
+  NOTIFICATIONS_SCROLL_TOP,
+  NOTIFICATIONS_LOAD_PENDING,
+  NOTIFICATIONS_DELETE_MARKED_REQUEST,
+  NOTIFICATIONS_DELETE_MARKED_SUCCESS,
+  NOTIFICATION_MARK_FOR_DELETE,
+  NOTIFICATIONS_DELETE_MARKED_FAIL,
+  NOTIFICATIONS_ENTER_CLEARING_MODE,
+  NOTIFICATIONS_MARK_ALL_FOR_DELETE,
+  NOTIFICATIONS_MARK_AS_READ,
+  NOTIFICATIONS_SET_BROWSER_SUPPORT,
+  NOTIFICATIONS_SET_BROWSER_PERMISSION,
+} from '../actions/notifications';
+import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines';
+import { compareId } from '../compare_id';
+
+const initialState = ImmutableMap({
+  pendingItems: ImmutableList(),
+  items: ImmutableList(),
+  hasMore: true,
+  top: false,
+  mounted: 0,
+  unread: 0,
+  lastReadId: '0',
+  readMarkerId: '0',
+  isLoading: 0,
+  cleaningMode: false,
+  isTabVisible: true,
+  browserSupport: false,
+  browserPermission: 'default',
+  // notification removal mark of new notifs loaded whilst cleaningMode is true.
+  markNewForDelete: false,
+});
+
+const notificationToMap = (notification, markForDelete) => ImmutableMap({
+  id: notification.id,
+  type: notification.type,
+  account: notification.account.id,
+  markedForDelete: markForDelete,
+  status: notification.status ? notification.status.id : null,
+  report: notification.report ? fromJS(notification.report) : null,
+});
+
+const normalizeNotification = (state, notification, usePendingItems) => {
+  const markNewForDelete = state.get('markNewForDelete');
+  const top = state.get('top');
+
+  // Under currently unknown conditions, the client may receive duplicates from the server
+  if (state.get('pendingItems').some((item) => item?.get('id') === notification.id) || state.get('items').some((item) => item?.get('id') === notification.id)) {
+    return state;
+  }
+
+  if (usePendingItems || !state.get('pendingItems').isEmpty()) {
+    return state.update('pendingItems', list => list.unshift(notificationToMap(notification, markNewForDelete))).update('unread', unread => unread + 1);
+  }
+
+  if (shouldCountUnreadNotifications(state)) {
+    state = state.update('unread', unread => unread + 1);
+  } else {
+    state = state.set('lastReadId', notification.id);
+  }
+
+  return state.update('items', list => {
+    if (top && list.size > 40) {
+      list = list.take(20);
+    }
+
+    return list.unshift(notificationToMap(notification, markNewForDelete));
+  });
+};
+
+const expandNormalizedNotifications = (state, notifications, next, isLoadingMore, isLoadingRecent, usePendingItems) => {
+  // This method is pretty tricky because:
+  // - existing notifications might be out of order
+  // - the existing notifications may have gaps, most often explicitly noted with a `null` item
+  // - ideally, we don't want it to reorder existing items
+  // - `notifications` may include items that are already included
+  // - this function can be called either to fill in a gap, or load newer items
+
+  const markNewForDelete = state.get('markNewForDelete');
+  const lastReadId = state.get('lastReadId');
+  const newItems = ImmutableList(notifications.map((notification) => notificationToMap(notification, markNewForDelete)));
+
+  return state.withMutations(mutable => {
+    if (!newItems.isEmpty()) {
+      usePendingItems = isLoadingRecent && (usePendingItems || !mutable.get('pendingItems').isEmpty());
+
+      mutable.update(usePendingItems ? 'pendingItems' : 'items', oldItems => {
+        // If called to poll *new* notifications, we just need to add them on top without duplicates
+        if (isLoadingRecent) {
+          const idsToCheck = oldItems.map(item => item?.get('id')).toSet();
+          const insertedItems = newItems.filterNot(item => idsToCheck.includes(item.get('id')));
+          return insertedItems.concat(oldItems);
+        }
+
+        // If called to expand more (presumably older than any known to the WebUI), we just have to
+        // add them to the bottom without duplicates
+        if (isLoadingMore) {
+          const idsToCheck = oldItems.map(item => item?.get('id')).toSet();
+          const insertedItems = newItems.filterNot(item => idsToCheck.includes(item.get('id')));
+          return oldItems.concat(insertedItems);
+        }
+
+        // Now this gets tricky, as we don't necessarily know for sure where the gap to fill is,
+        // and some items in the timeline may not be properly ordered.
+
+        // However, we know that `newItems.last()` is the oldest item that was requested and that
+        // there is no “hole” between `newItems.last()` and `newItems.first()`.
+
+        // First, find the furthest (if properly sorted, oldest) item in the notifications that is
+        // newer than the oldest fetched one, as it's most likely that it delimits the gap.
+        // Start the gap *after* that item.
+        const lastIndex = oldItems.findLastIndex(item => item !== null && compareId(item.get('id'), newItems.last().get('id')) >= 0) + 1;
+
+        // Then, try to find the furthest (if properly sorted, oldest) item in the notifications that
+        // is newer than the most recent fetched one, as it delimits a section comprised of only
+        // items older or within `newItems` (or that were deleted from the server, so should be removed
+        // anyway).
+        // Stop the gap *after* that item.
+        const firstIndex = oldItems.take(lastIndex).findLastIndex(item => item !== null && compareId(item.get('id'), newItems.first().get('id')) > 0) + 1;
+
+        // At this point:
+        // - no `oldItems` after `firstIndex` is newer than any of the `newItems`
+        // - all `oldItems` after `lastIndex` are older than every of the `newItems`
+        // - it is possible for items in the replaced slice to be older than every `newItems`
+        // - it is possible for items before `firstIndex` to be in the `newItems` range
+        // Therefore:
+        // - to avoid losing items, items from the replaced slice that are older than `newItems`
+        //   should be added in the back.
+        // - to avoid duplicates, `newItems` should be checked the first `firstIndex` items of
+        //   `oldItems`
+        const idsToCheck = oldItems.take(firstIndex).map(item => item?.get('id')).toSet();
+        const insertedItems = newItems.filterNot(item => idsToCheck.includes(item.get('id')));
+        const olderItems = oldItems.slice(firstIndex, lastIndex).filter(item => item !== null && compareId(item.get('id'), newItems.last().get('id')) < 0);
+
+        return oldItems.take(firstIndex).concat(
+          insertedItems,
+          olderItems,
+          oldItems.skip(lastIndex),
+        );
+      });
+    }
+
+    if (!next) {
+      mutable.set('hasMore', false);
+    }
+
+    if (shouldCountUnreadNotifications(state)) {
+      mutable.set('unread', mutable.get('pendingItems').count(item => item !== null) + mutable.get('items').count(item => item && compareId(item.get('id'), lastReadId) > 0));
+    } else {
+      const mostRecent = newItems.find(item => item !== null);
+      if (mostRecent && compareId(lastReadId, mostRecent.get('id')) < 0) {
+        mutable.set('lastReadId', mostRecent.get('id'));
+      }
+    }
+
+    mutable.update('isLoading', (nbLoading) => nbLoading - 1);
+  });
+};
+
+const filterNotifications = (state, accountIds, type) => {
+  const helper = list => list.filterNot(item => item !== null && accountIds.includes(item.get('account')) && (type === undefined || type === item.get('type')));
+  return state.update('items', helper).update('pendingItems', helper);
+};
+
+const clearUnread = (state) => {
+  state = state.set('unread', state.get('pendingItems').size);
+  const lastNotification = state.get('items').find(item => item !== null);
+  return state.set('lastReadId', lastNotification ? lastNotification.get('id') : '0');
+};
+
+const updateTop = (state, top) => {
+  state = state.set('top', top);
+
+  if (!shouldCountUnreadNotifications(state)) {
+    state = clearUnread(state);
+  }
+
+  return state;
+};
+
+const deleteByStatus = (state, statusId) => {
+  const lastReadId = state.get('lastReadId');
+
+  if (shouldCountUnreadNotifications(state)) {
+    const deletedUnread = state.get('items').filter(item => item !== null && item.get('status') === statusId && compareId(item.get('id'), lastReadId) > 0);
+    state = state.update('unread', unread => unread - deletedUnread.size);
+  }
+
+  const helper = list => list.filterNot(item => item !== null && item.get('status') === statusId);
+  const deletedUnread = state.get('pendingItems').filter(item => item !== null && item.get('status') === statusId && compareId(item.get('id'), lastReadId) > 0);
+  state = state.update('unread', unread => unread - deletedUnread.size);
+  return state.update('items', helper).update('pendingItems', helper);
+};
+
+const markForDelete = (state, notificationId, yes) => {
+  return state.update('items', list => list.map(item => {
+    if (item === null) {
+      return null;
+    } else if(item.get('id') === notificationId) {
+      return item.set('markedForDelete', yes);
+    } else {
+      return item;
+    }
+  }));
+};
+
+const markAllForDelete = (state, yes) => {
+  return state.update('items', list => list.map(item => {
+    if (item === null) {
+      return null;
+    } else if(yes !== null) {
+      return item.set('markedForDelete', yes);
+    } else {
+      return item.set('markedForDelete', !item.get('markedForDelete'));
+    }
+  }));
+};
+
+const unmarkAllForDelete = (state) => {
+  return state.update('items', list => list.map(item => item === null ? item : item.set('markedForDelete', false)));
+};
+
+const deleteMarkedNotifs = (state) => {
+  return state.update('items', list => list.filterNot(item => item === null ? item : item.get('markedForDelete')));
+};
+
+const updateMounted = (state) => {
+  state = state.update('mounted', count => count + 1);
+  if (!shouldCountUnreadNotifications(state, state.get('mounted') === 1)) {
+    state = state.set('readMarkerId', state.get('lastReadId'));
+    state = clearUnread(state);
+  }
+  return state;
+};
+
+const updateVisibility = (state, visibility) => {
+  state = state.set('isTabVisible', visibility);
+  if (!shouldCountUnreadNotifications(state)) {
+    state = state.set('readMarkerId', state.get('lastReadId'));
+    state = clearUnread(state);
+  }
+  return state;
+};
+
+const shouldCountUnreadNotifications = (state, ignoreScroll = false) => {
+  const isTabVisible   = state.get('isTabVisible');
+  const isOnTop        = state.get('top');
+  const isMounted      = state.get('mounted') > 0;
+  const lastReadId     = state.get('lastReadId');
+  const lastItem       = state.get('items').findLast(item => item !== null);
+  const lastItemReached = !state.get('hasMore') || lastReadId === '0' || (lastItem && compareId(lastItem.get('id'), lastReadId) <= 0);
+
+  return !(isTabVisible && (ignoreScroll || isOnTop) && isMounted && lastItemReached);
+};
+
+const recountUnread = (state, last_read_id) => {
+  return state.withMutations(mutable => {
+    if (compareId(last_read_id, mutable.get('lastReadId')) > 0) {
+      mutable.set('lastReadId', last_read_id);
+    }
+
+    if (compareId(last_read_id, mutable.get('readMarkerId')) > 0) {
+      mutable.set('readMarkerId', last_read_id);
+    }
+
+    if (state.get('unread') > 0 || shouldCountUnreadNotifications(state)) {
+      mutable.set('unread', mutable.get('pendingItems').count(item => item !== null) + mutable.get('items').count(item => item && compareId(item.get('id'), last_read_id) > 0));
+    }
+  });
+};
+
+export default function notifications(state = initialState, action) {
+  let st;
+
+  switch(action.type) {
+  case MARKERS_FETCH_SUCCESS:
+    return action.markers.notifications ? recountUnread(state, action.markers.notifications.last_read_id) : state;
+  case NOTIFICATIONS_MOUNT:
+    return updateMounted(state);
+  case NOTIFICATIONS_UNMOUNT:
+    return state.update('mounted', count => count - 1);
+  case NOTIFICATIONS_SET_VISIBILITY:
+    return updateVisibility(state, action.visibility);
+  case NOTIFICATIONS_LOAD_PENDING:
+    return state.update('items', list => state.get('pendingItems').concat(list.take(40))).set('pendingItems', ImmutableList()).set('unread', 0);
+  case NOTIFICATIONS_EXPAND_REQUEST:
+  case NOTIFICATIONS_DELETE_MARKED_REQUEST:
+    return state.update('isLoading', (nbLoading) => nbLoading + 1);
+  case NOTIFICATIONS_DELETE_MARKED_FAIL:
+  case NOTIFICATIONS_EXPAND_FAIL:
+    return state.update('isLoading', (nbLoading) => nbLoading - 1);
+  case NOTIFICATIONS_FILTER_SET:
+    return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', true);
+  case NOTIFICATIONS_SCROLL_TOP:
+    return updateTop(state, action.top);
+  case notificationsUpdate.type:
+    return normalizeNotification(state, action.payload.notification, action.payload.usePendingItems);
+  case NOTIFICATIONS_EXPAND_SUCCESS:
+    return expandNormalizedNotifications(state, action.notifications, action.next, action.isLoadingMore, action.isLoadingRecent, action.usePendingItems);
+  case blockAccountSuccess.type:
+    return filterNotifications(state, [action.payload.relationship.id]);
+  case muteAccountSuccess.type:
+    return action.payload.relationship.muting_notifications ? filterNotifications(state, [action.payload.relationship.id]) : state;
+  case blockDomainSuccess.type:
+    return filterNotifications(state, action.payload.accounts);
+  case authorizeFollowRequestSuccess.type:
+  case rejectFollowRequestSuccess.type:
+    return filterNotifications(state, [action.payload.id], 'follow_request');
+  case NOTIFICATIONS_CLEAR:
+    return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', false);
+  case TIMELINE_DELETE:
+    return deleteByStatus(state, action.id);
+  case TIMELINE_DISCONNECT:
+    return action.timeline === 'home' ?
+      state.update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) :
+      state;
+  case NOTIFICATIONS_SET_BROWSER_SUPPORT:
+    return state.set('browserSupport', action.value);
+  case NOTIFICATIONS_SET_BROWSER_PERMISSION:
+    return state.set('browserPermission', action.value);
+
+  case NOTIFICATION_MARK_FOR_DELETE:
+    return markForDelete(state, action.id, action.yes);
+
+  case NOTIFICATIONS_DELETE_MARKED_SUCCESS:
+    return deleteMarkedNotifs(state).update('isLoading', (nbLoading) => nbLoading - 1);
+
+  case NOTIFICATIONS_ENTER_CLEARING_MODE:
+    st = state.set('cleaningMode', action.yes);
+    if (!action.yes) {
+      return unmarkAllForDelete(st).set('markNewForDelete', false);
+    } else {
+      return st;
+    }
+
+  case NOTIFICATIONS_MARK_ALL_FOR_DELETE:
+    st = state;
+    if (action.yes === null) {
+      // Toggle - this is a bit confusing, as it toggles the all-none mode
+      //st = st.set('markNewForDelete', !st.get('markNewForDelete'));
+    } else {
+      st = st.set('markNewForDelete', action.yes);
+    }
+    return markAllForDelete(st, action.yes);
+
+  case NOTIFICATIONS_MARK_AS_READ:
+    const lastNotification = state.get('items').find(item => item !== null);
+    return lastNotification ? recountUnread(state, lastNotification.get('id')) : state;
+
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/reducers/picture_in_picture.js b/app/javascript/flavours/blobfox/reducers/picture_in_picture.js
new file mode 100644
index 00000000000000..edf98eb9fefc10
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/picture_in_picture.js
@@ -0,0 +1,26 @@
+import { PICTURE_IN_PICTURE_DEPLOY, PICTURE_IN_PICTURE_REMOVE } from 'flavours/blobfox/actions/picture_in_picture';
+
+import { TIMELINE_DELETE } from '../actions/timelines';
+
+const initialState = {
+  statusId: null,
+  accountId: null,
+  type: null,
+  src: null,
+  muted: false,
+  volume: 0,
+  currentTime: 0,
+};
+
+export default function pictureInPicture(state = initialState, action) {
+  switch(action.type) {
+  case PICTURE_IN_PICTURE_DEPLOY:
+    return { statusId: action.statusId, accountId: action.accountId, type: action.playerType, ...action.props };
+  case PICTURE_IN_PICTURE_REMOVE:
+    return { ...initialState };
+  case TIMELINE_DELETE:
+    return (state.statusId === action.id) ? { ...initialState } : state;
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/reducers/pinned_accounts_editor.js b/app/javascript/flavours/blobfox/reducers/pinned_accounts_editor.js
new file mode 100644
index 00000000000000..352db5733bc2d6
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/pinned_accounts_editor.js
@@ -0,0 +1,58 @@
+import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
+
+import {
+  PINNED_ACCOUNTS_EDITOR_RESET,
+  PINNED_ACCOUNTS_FETCH_REQUEST,
+  PINNED_ACCOUNTS_FETCH_SUCCESS,
+  PINNED_ACCOUNTS_FETCH_FAIL,
+  PINNED_ACCOUNTS_SUGGESTIONS_FETCH_SUCCESS,
+  PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CLEAR,
+  PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE,
+  pinAccountSuccess,
+  unpinAccountSuccess,
+} from '../actions/accounts';
+
+const initialState = ImmutableMap({
+  accounts: ImmutableMap({
+    items: ImmutableList(),
+    loaded: false,
+    isLoading: false,
+  }),
+
+  suggestions: ImmutableMap({
+    value: '',
+    items: ImmutableList(),
+  }),
+});
+
+export default function listEditorReducer(state = initialState, action) {
+  switch(action.type) {
+  case PINNED_ACCOUNTS_EDITOR_RESET:
+    return initialState;
+  case PINNED_ACCOUNTS_FETCH_REQUEST:
+    return state.setIn(['accounts', 'isLoading'], true);
+  case PINNED_ACCOUNTS_FETCH_FAIL:
+    return state.setIn(['accounts', 'isLoading'], false);
+  case PINNED_ACCOUNTS_FETCH_SUCCESS:
+    return state.update('accounts', accounts => accounts.withMutations(map => {
+      map.set('isLoading', false);
+      map.set('loaded', true);
+      map.set('items', ImmutableList(action.accounts.map(item => item.id)));
+    }));
+  case PINNED_ACCOUNTS_SUGGESTIONS_FETCH_SUCCESS:
+    return state.setIn(['suggestions', 'items'], ImmutableList(action.accounts.map(item => item.id)));
+  case PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE:
+    return state.setIn(['suggestions', 'value'], action.value);
+  case PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CLEAR:
+    return state.update('suggestions', suggestions => suggestions.withMutations(map => {
+      map.set('items', ImmutableList());
+      map.set('value', '');
+    }));
+  case pinAccountSuccess.type:
+    return state.updateIn(['accounts', 'items'], list => list.unshift(action.payload.relationship.id));
+  case unpinAccountSuccess.type:
+    return state.updateIn(['accounts', 'items'], list => list.filterNot(item => item === action.payload.relationship.id));
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/reducers/polls.js b/app/javascript/flavours/blobfox/reducers/polls.js
new file mode 100644
index 00000000000000..67e204c53c1fac
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/polls.js
@@ -0,0 +1,45 @@
+import { Map as ImmutableMap, fromJS } from 'immutable';
+
+import { POLLS_IMPORT } from 'flavours/blobfox/actions/importer';
+
+import { normalizePollOptionTranslation } from '../actions/importer/normalizer';
+import { STATUS_TRANSLATE_SUCCESS, STATUS_TRANSLATE_UNDO } from '../actions/statuses';
+
+const importPolls = (state, polls) => state.withMutations(map => polls.forEach(poll => map.set(poll.id, fromJS(poll))));
+
+const statusTranslateSuccess = (state, pollTranslation) => {
+  return state.withMutations(map => {
+    if (pollTranslation) {
+      const poll = state.get(pollTranslation.id);
+
+      pollTranslation.options.forEach((item, index) => {
+        map.setIn([pollTranslation.id, 'options', index, 'translation'], fromJS(normalizePollOptionTranslation(item, poll)));
+      });
+    }
+  });
+};
+
+const statusTranslateUndo = (state, id) => {
+  return state.withMutations(map => {
+    const options = map.getIn([id, 'options']);
+
+    if (options) {
+      options.forEach((item, index) => map.deleteIn([id, 'options', index, 'translation']));
+    }
+  });
+};
+
+const initialState = ImmutableMap();
+
+export default function polls(state = initialState, action) {
+  switch(action.type) {
+  case POLLS_IMPORT:
+    return importPolls(state, action.polls);
+  case STATUS_TRANSLATE_SUCCESS:
+    return statusTranslateSuccess(state, action.translation.poll);
+  case STATUS_TRANSLATE_UNDO:
+    return statusTranslateUndo(state, action.pollId);
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/reducers/push_notifications.js b/app/javascript/flavours/blobfox/reducers/push_notifications.js
new file mode 100644
index 00000000000000..fa8af0e8ccbdaf
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/push_notifications.js
@@ -0,0 +1,54 @@
+import Immutable from 'immutable';
+
+import { SET_BROWSER_SUPPORT, SET_SUBSCRIPTION, CLEAR_SUBSCRIPTION, SET_ALERTS } from '../actions/push_notifications';
+import { STORE_HYDRATE } from '../actions/store';
+
+const initialState = Immutable.Map({
+  subscription: null,
+  alerts: new Immutable.Map({
+    follow: false,
+    follow_request: false,
+    favourite: false,
+    reblog: false,
+    mention: false,
+    poll: false,
+  }),
+  isSubscribed: false,
+  browserSupport: false,
+});
+
+export default function push_subscriptions(state = initialState, action) {
+  switch(action.type) {
+  case STORE_HYDRATE: {
+    const push_subscription = action.state.get('push_subscription');
+
+    if (push_subscription) {
+      return state
+        .set('subscription', new Immutable.Map({
+          id: push_subscription.get('id'),
+          endpoint: push_subscription.get('endpoint'),
+        }))
+        .set('alerts', push_subscription.get('alerts') || initialState.get('alerts'))
+        .set('isSubscribed', true);
+    }
+
+    return state;
+  }
+  case SET_SUBSCRIPTION:
+    return state
+      .set('subscription', new Immutable.Map({
+        id: action.subscription.id,
+        endpoint: action.subscription.endpoint,
+      }))
+      .set('alerts', new Immutable.Map(action.subscription.alerts))
+      .set('isSubscribed', true);
+  case SET_BROWSER_SUPPORT:
+    return state.set('browserSupport', action.value);
+  case CLEAR_SUBSCRIPTION:
+    return initialState;
+  case SET_ALERTS:
+    return state.setIn(action.path, action.value);
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/reducers/relationships.ts b/app/javascript/flavours/blobfox/reducers/relationships.ts
new file mode 100644
index 00000000000000..997f7fef793e75
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/relationships.ts
@@ -0,0 +1,123 @@
+import { Map as ImmutableMap } from 'immutable';
+
+import { isFulfilled } from '@reduxjs/toolkit';
+import type { Reducer } from 'redux';
+
+import type { ApiRelationshipJSON } from 'flavours/blobfox/api_types/relationships';
+import type { Account } from 'flavours/blobfox/models/account';
+import { createRelationship } from 'flavours/blobfox/models/relationship';
+import type { Relationship } from 'flavours/blobfox/models/relationship';
+
+import { submitAccountNote } from '../actions/account_notes';
+import {
+  followAccountSuccess,
+  unfollowAccountSuccess,
+  authorizeFollowRequestSuccess,
+  rejectFollowRequestSuccess,
+  followAccountRequest,
+  followAccountFail,
+  unfollowAccountRequest,
+  unfollowAccountFail,
+  blockAccountSuccess,
+  unblockAccountSuccess,
+  muteAccountSuccess,
+  unmuteAccountSuccess,
+  pinAccountSuccess,
+  unpinAccountSuccess,
+  fetchRelationshipsSuccess,
+} from '../actions/accounts_typed';
+import {
+  blockDomainSuccess,
+  unblockDomainSuccess,
+} from '../actions/domain_blocks_typed';
+import { notificationsUpdate } from '../actions/notifications_typed';
+
+const initialState = ImmutableMap<string, Relationship>();
+type State = typeof initialState;
+
+const normalizeRelationship = (
+  state: State,
+  relationship: ApiRelationshipJSON,
+) => state.set(relationship.id, createRelationship(relationship));
+
+const normalizeRelationships = (
+  state: State,
+  relationships: ApiRelationshipJSON[],
+) => {
+  relationships.forEach((relationship) => {
+    state = normalizeRelationship(state, relationship);
+  });
+
+  return state;
+};
+
+const setDomainBlocking = (
+  state: State,
+  accounts: Account[],
+  blocking: boolean,
+) => {
+  return state.withMutations((map) => {
+    accounts.forEach((id) => {
+      map.setIn([id, 'domain_blocking'], blocking);
+    });
+  });
+};
+
+export const relationshipsReducer: Reducer<State> = (
+  state = initialState,
+  action,
+) => {
+  if (authorizeFollowRequestSuccess.match(action))
+    return state
+      .setIn([action.payload.id, 'followed_by'], true)
+      .setIn([action.payload.id, 'requested_by'], false);
+  else if (rejectFollowRequestSuccess.match(action))
+    return state
+      .setIn([action.payload.id, 'followed_by'], false)
+      .setIn([action.payload.id, 'requested_by'], false);
+  else if (notificationsUpdate.match(action))
+    return action.payload.notification.type === 'follow_request'
+      ? state.setIn(
+          [action.payload.notification.account.id, 'requested_by'],
+          true,
+        )
+      : state;
+  else if (followAccountRequest.match(action))
+    return state.getIn([action.payload.id, 'following'])
+      ? state
+      : state.setIn(
+          [
+            action.payload.id,
+            action.payload.locked ? 'requested' : 'following',
+          ],
+          true,
+        );
+  else if (followAccountFail.match(action))
+    return state.setIn(
+      [action.payload.id, action.payload.locked ? 'requested' : 'following'],
+      false,
+    );
+  else if (unfollowAccountRequest.match(action))
+    return state.setIn([action.payload.id, 'following'], false);
+  else if (unfollowAccountFail.match(action))
+    return state.setIn([action.payload.id, 'following'], true);
+  else if (
+    followAccountSuccess.match(action) ||
+    unfollowAccountSuccess.match(action) ||
+    blockAccountSuccess.match(action) ||
+    unblockAccountSuccess.match(action) ||
+    muteAccountSuccess.match(action) ||
+    unmuteAccountSuccess.match(action) ||
+    pinAccountSuccess.match(action) ||
+    unpinAccountSuccess.match(action) ||
+    isFulfilled(submitAccountNote)(action)
+  )
+    return normalizeRelationship(state, action.payload.relationship);
+  else if (fetchRelationshipsSuccess.match(action))
+    return normalizeRelationships(state, action.payload.relationships);
+  else if (blockDomainSuccess.match(action))
+    return setDomainBlocking(state, action.payload.accounts, true);
+  else if (unblockDomainSuccess.match(action))
+    return setDomainBlocking(state, action.payload.accounts, false);
+  else return state;
+};
diff --git a/app/javascript/flavours/blobfox/reducers/search.js b/app/javascript/flavours/blobfox/reducers/search.js
new file mode 100644
index 00000000000000..72835eb91745f3
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/search.js
@@ -0,0 +1,82 @@
+import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
+
+import {
+  COMPOSE_MENTION,
+  COMPOSE_REPLY,
+  COMPOSE_DIRECT,
+} from '../actions/compose';
+import {
+  SEARCH_CHANGE,
+  SEARCH_CLEAR,
+  SEARCH_FETCH_REQUEST,
+  SEARCH_FETCH_FAIL,
+  SEARCH_FETCH_SUCCESS,
+  SEARCH_SHOW,
+  SEARCH_EXPAND_REQUEST,
+  SEARCH_EXPAND_SUCCESS,
+  SEARCH_EXPAND_FAIL,
+  SEARCH_HISTORY_UPDATE,
+} from '../actions/search';
+
+const initialState = ImmutableMap({
+  value: '',
+  submitted: false,
+  hidden: false,
+  results: ImmutableMap(),
+  isLoading: false,
+  searchTerm: '',
+  type: null,
+  recent: ImmutableOrderedSet(),
+});
+
+export default function search(state = initialState, action) {
+  switch(action.type) {
+  case SEARCH_CHANGE:
+    return state.set('value', action.value);
+  case SEARCH_CLEAR:
+    return state.withMutations(map => {
+      map.set('value', '');
+      map.set('results', ImmutableMap());
+      map.set('submitted', false);
+      map.set('hidden', false);
+      map.set('searchTerm', '');
+      map.set('type', null);
+    });
+  case SEARCH_SHOW:
+    return state.set('hidden', false);
+  case COMPOSE_REPLY:
+  case COMPOSE_MENTION:
+  case COMPOSE_DIRECT:
+    return state.set('hidden', true);
+  case SEARCH_FETCH_REQUEST:
+    return state.withMutations(map => {
+      map.set('isLoading', true);
+      map.set('submitted', true);
+      map.set('type', action.searchType);
+    });
+  case SEARCH_FETCH_FAIL:
+  case SEARCH_EXPAND_FAIL:
+    return state.set('isLoading', false);
+  case SEARCH_FETCH_SUCCESS:
+    return state.withMutations(map => {
+      map.set('results', ImmutableMap({
+        accounts: ImmutableOrderedSet(action.results.accounts.map(item => item.id)),
+        statuses: ImmutableOrderedSet(action.results.statuses.map(item => item.id)),
+        hashtags: ImmutableOrderedSet(fromJS(action.results.hashtags)),
+      }));
+
+      map.set('searchTerm', action.searchTerm);
+      map.set('type', action.searchType);
+      map.set('isLoading', false);
+    });
+  case SEARCH_EXPAND_REQUEST:
+    return state.set('type', action.searchType).set('isLoading', true);
+  case SEARCH_EXPAND_SUCCESS:
+    const results = action.searchType === 'hashtags' ? ImmutableOrderedSet(fromJS(action.results.hashtags)) : action.results[action.searchType].map(item => item.id);
+    return state.updateIn(['results', action.searchType], list => list.union(results)).set('isLoading', false);
+  case SEARCH_HISTORY_UPDATE:
+    return state.set('recent', ImmutableOrderedSet(fromJS(action.recent)));
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/reducers/server.js b/app/javascript/flavours/blobfox/reducers/server.js
new file mode 100644
index 00000000000000..4442e1b1141094
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/server.js
@@ -0,0 +1,63 @@
+import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
+
+import {
+  SERVER_FETCH_REQUEST,
+  SERVER_FETCH_SUCCESS,
+  SERVER_FETCH_FAIL,
+  SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST,
+  SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS,
+  SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL,
+  EXTENDED_DESCRIPTION_REQUEST,
+  EXTENDED_DESCRIPTION_SUCCESS,
+  EXTENDED_DESCRIPTION_FAIL,
+  SERVER_DOMAIN_BLOCKS_FETCH_REQUEST,
+  SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS,
+  SERVER_DOMAIN_BLOCKS_FETCH_FAIL,
+} from 'flavours/blobfox/actions/server';
+
+const initialState = ImmutableMap({
+  server: ImmutableMap({
+    isLoading: false,
+  }),
+
+  extendedDescription: ImmutableMap({
+    isLoading: false,
+  }),
+
+  domainBlocks: ImmutableMap({
+    isLoading: false,
+    isAvailable: true,
+    items: ImmutableList(),
+  }),
+});
+
+export default function server(state = initialState, action) {
+  switch (action.type) {
+  case SERVER_FETCH_REQUEST:
+    return state.setIn(['server', 'isLoading'], true);
+  case SERVER_FETCH_SUCCESS:
+    return state.set('server', fromJS(action.server)).setIn(['server', 'isLoading'], false);
+  case SERVER_FETCH_FAIL:
+    return state.setIn(['server', 'isLoading'], false);
+  case SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST:
+    return state.setIn(['translationLanguages', 'isLoading'], true);
+  case SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS:
+    return state.setIn(['translationLanguages', 'items'], fromJS(action.translationLanguages)).setIn(['translationLanguages', 'isLoading'], false);
+  case SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL:
+    return state.setIn(['translationLanguages', 'isLoading'], false);
+  case EXTENDED_DESCRIPTION_REQUEST:
+    return state.setIn(['extendedDescription', 'isLoading'], true);
+  case EXTENDED_DESCRIPTION_SUCCESS:
+    return state.set('extendedDescription', fromJS(action.description)).setIn(['extendedDescription', 'isLoading'], false);
+  case EXTENDED_DESCRIPTION_FAIL:
+    return state.setIn(['extendedDescription', 'isLoading'], false);
+  case SERVER_DOMAIN_BLOCKS_FETCH_REQUEST:
+    return state.setIn(['domainBlocks', 'isLoading'], true);
+  case SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS:
+    return state.setIn(['domainBlocks', 'items'], fromJS(action.blocks)).setIn(['domainBlocks', 'isLoading'], false).setIn(['domainBlocks', 'isAvailable'], action.isAvailable);
+  case SERVER_DOMAIN_BLOCKS_FETCH_FAIL:
+    return state.setIn(['domainBlocks', 'isLoading'], false);
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/reducers/settings.js b/app/javascript/flavours/blobfox/reducers/settings.js
new file mode 100644
index 00000000000000..e695d3bf9d714b
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/settings.js
@@ -0,0 +1,203 @@
+import { Map as ImmutableMap, fromJS } from 'immutable';
+
+import { COLUMN_ADD, COLUMN_REMOVE, COLUMN_MOVE, COLUMN_PARAMS_CHANGE } from '../actions/columns';
+import { EMOJI_USE } from '../actions/emojis';
+import { LANGUAGE_USE } from '../actions/languages';
+import { LIST_DELETE_SUCCESS, LIST_FETCH_FAIL } from '../actions/lists';
+import { NOTIFICATIONS_FILTER_SET } from '../actions/notifications';
+import { SETTING_CHANGE, SETTING_SAVE } from '../actions/settings';
+import { STORE_HYDRATE } from '../actions/store';
+import { uuid } from '../uuid';
+
+const initialState = ImmutableMap({
+  saved: true,
+
+  onboarded: false,
+  layout: 'auto',
+
+  skinTone: 1,
+
+  trends: ImmutableMap({
+    show: true,
+  }),
+
+  home: ImmutableMap({
+    shows: ImmutableMap({
+      reblog: true,
+      reply: true,
+      direct: true,
+    }),
+
+    regex: ImmutableMap({
+      body: '',
+    }),
+  }),
+
+  notifications: ImmutableMap({
+    alerts: ImmutableMap({
+      follow: false,
+      follow_request: false,
+      favourite: false,
+      reaction: false,
+      reblog: false,
+      mention: false,
+      poll: false,
+      status: false,
+      update: false,
+      'admin.sign_up': false,
+      'admin.report': false,
+    }),
+
+    quickFilter: ImmutableMap({
+      active: 'all',
+      show: true,
+      advanced: false,
+    }),
+
+    dismissPermissionBanner: false,
+    showUnread: true,
+
+    shows: ImmutableMap({
+      follow: true,
+      follow_request: false,
+      favourite: true,
+      reaction: true,
+      reblog: true,
+      reaction: true,
+      mention: true,
+      poll: true,
+      status: true,
+      update: true,
+      'admin.sign_up': true,
+      'admin.report': true,
+    }),
+
+    sounds: ImmutableMap({
+      follow: true,
+      follow_request: false,
+      favourite: true,
+      reaction: true,
+      reblog: true,
+      reaction: true,
+      mention: true,
+      poll: true,
+      status: true,
+      update: true,
+      'admin.sign_up': true,
+      'admin.report': true,
+    }),
+  }),
+
+  firehose: ImmutableMap({
+    onlyMedia: false,
+    allowLocalOnly: true,
+
+    regex: ImmutableMap({
+      body: '',
+    }),
+  }),
+
+  community: ImmutableMap({
+    regex: ImmutableMap({
+      body: '',
+    }),
+  }),
+
+  public: ImmutableMap({
+    regex: ImmutableMap({
+      body: '',
+    }),
+  }),
+
+  direct: ImmutableMap({
+    conversations: true,
+    regex: ImmutableMap({
+      body: '',
+    }),
+  }),
+
+  dismissed_banners: ImmutableMap({
+    'public_timeline': false,
+    'community_timeline': false,
+    'home.explore_prompt': false,
+    'explore/links': false,
+    'explore/statuses': false,
+    'explore/tags': false,
+  }),
+});
+
+const defaultColumns = fromJS([
+  { id: 'COMPOSE', uuid: uuid(), params: {} },
+  { id: 'HOME', uuid: uuid(), params: {} },
+  { id: 'NOTIFICATIONS', uuid: uuid(), params: {} },
+]);
+
+const hydrate = (state, settings) => state.mergeDeep(settings).update('columns', (val = defaultColumns) => val);
+
+const moveColumn = (state, uuid, direction) => {
+  const columns  = state.get('columns');
+  const index    = columns.findIndex(item => item.get('uuid') === uuid);
+  const newIndex = index + direction;
+
+  let newColumns;
+
+  newColumns = columns.splice(index, 1);
+  newColumns = newColumns.splice(newIndex, 0, columns.get(index));
+
+  return state
+    .set('columns', newColumns)
+    .set('saved', false);
+};
+
+const changeColumnParams = (state, uuid, path, value) => {
+  const columns = state.get('columns');
+  const index   = columns.findIndex(item => item.get('uuid') === uuid);
+
+  const newColumns = columns.update(index, column => column.updateIn(['params', ...path], () => value));
+
+  return state
+    .set('columns', newColumns)
+    .set('saved', false);
+};
+
+const updateFrequentEmojis = (state, emoji) => state.update('frequentlyUsedEmojis', ImmutableMap(), map => map.update(emoji.id, 0, count => count + 1)).set('saved', false);
+
+const updateFrequentLanguages = (state, language) => state.update('frequentlyUsedLanguages', ImmutableMap(), map => map.update(language, 0, count => count + 1)).set('saved', false);
+
+const filterDeadListColumns = (state, listId) => state.update('columns', columns => columns.filterNot(column => column.get('id') === 'LIST' && column.get('params').get('id') === listId));
+
+export default function settings(state = initialState, action) {
+  switch(action.type) {
+  case STORE_HYDRATE:
+    return hydrate(state, action.state.get('settings'));
+  case NOTIFICATIONS_FILTER_SET:
+  case SETTING_CHANGE:
+    return state
+      .setIn(action.path, action.value)
+      .set('saved', false);
+  case COLUMN_ADD:
+    return state
+      .update('columns', list => list.push(fromJS({ id: action.id, uuid: uuid(), params: action.params })))
+      .set('saved', false);
+  case COLUMN_REMOVE:
+    return state
+      .update('columns', list => list.filterNot(item => item.get('uuid') === action.uuid))
+      .set('saved', false);
+  case COLUMN_MOVE:
+    return moveColumn(state, action.uuid, action.direction);
+  case COLUMN_PARAMS_CHANGE:
+    return changeColumnParams(state, action.uuid, action.path, action.value);
+  case EMOJI_USE:
+    return updateFrequentEmojis(state, action.emoji);
+  case LANGUAGE_USE:
+    return updateFrequentLanguages(state, action.language);
+  case SETTING_SAVE:
+    return state.set('saved', true);
+  case LIST_FETCH_FAIL:
+    return action.error.response.status === 404 ? filterDeadListColumns(state, action.id) : state;
+  case LIST_DELETE_SUCCESS:
+    return filterDeadListColumns(state, action.id);
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/reducers/status_lists.js b/app/javascript/flavours/blobfox/reducers/status_lists.js
new file mode 100644
index 00000000000000..6cb6a937bb915e
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/status_lists.js
@@ -0,0 +1,151 @@
+import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
+
+import {
+  blockAccountSuccess,
+  muteAccountSuccess,
+} from '../actions/accounts';
+import {
+  BOOKMARKED_STATUSES_FETCH_REQUEST,
+  BOOKMARKED_STATUSES_FETCH_SUCCESS,
+  BOOKMARKED_STATUSES_FETCH_FAIL,
+  BOOKMARKED_STATUSES_EXPAND_REQUEST,
+  BOOKMARKED_STATUSES_EXPAND_SUCCESS,
+  BOOKMARKED_STATUSES_EXPAND_FAIL,
+} from '../actions/bookmarks';
+import {
+  FAVOURITED_STATUSES_FETCH_REQUEST,
+  FAVOURITED_STATUSES_FETCH_SUCCESS,
+  FAVOURITED_STATUSES_FETCH_FAIL,
+  FAVOURITED_STATUSES_EXPAND_REQUEST,
+  FAVOURITED_STATUSES_EXPAND_SUCCESS,
+  FAVOURITED_STATUSES_EXPAND_FAIL,
+} from '../actions/favourites';
+import {
+  FAVOURITE_SUCCESS,
+  UNFAVOURITE_SUCCESS,
+  BOOKMARK_SUCCESS,
+  UNBOOKMARK_SUCCESS,
+  PIN_SUCCESS,
+  UNPIN_SUCCESS,
+} from '../actions/interactions';
+import {
+  PINNED_STATUSES_FETCH_SUCCESS,
+} from '../actions/pin_statuses';
+import {
+  TRENDS_STATUSES_FETCH_REQUEST,
+  TRENDS_STATUSES_FETCH_SUCCESS,
+  TRENDS_STATUSES_FETCH_FAIL,
+  TRENDS_STATUSES_EXPAND_REQUEST,
+  TRENDS_STATUSES_EXPAND_SUCCESS,
+  TRENDS_STATUSES_EXPAND_FAIL,
+} from '../actions/trends';
+
+
+
+const initialState = ImmutableMap({
+  favourites: ImmutableMap({
+    next: null,
+    loaded: false,
+    items: ImmutableOrderedSet(),
+  }),
+  bookmarks: ImmutableMap({
+    next: null,
+    loaded: false,
+    items: ImmutableOrderedSet(),
+  }),
+  pins: ImmutableMap({
+    next: null,
+    loaded: false,
+    items: ImmutableOrderedSet(),
+  }),
+  trending: ImmutableMap({
+    next: null,
+    loaded: false,
+    items: ImmutableOrderedSet(),
+  }),
+});
+
+const normalizeList = (state, listType, statuses, next) => {
+  return state.update(listType, listMap => listMap.withMutations(map => {
+    map.set('next', next);
+    map.set('loaded', true);
+    map.set('isLoading', false);
+    map.set('items', ImmutableOrderedSet(statuses.map(item => item.id)));
+  }));
+};
+
+const appendToList = (state, listType, statuses, next) => {
+  return state.update(listType, listMap => listMap.withMutations(map => {
+    map.set('next', next);
+    map.set('isLoading', false);
+    map.set('items', map.get('items').union(statuses.map(item => item.id)));
+  }));
+};
+
+const prependOneToList = (state, listType, status) => {
+  return state.updateIn([listType, 'items'], (list) => {
+    if (list.includes(status.get('id'))) {
+      return list;
+    } else {
+      return ImmutableOrderedSet([status.get('id')]).union(list);
+    }
+  });
+};
+
+const removeOneFromList = (state, listType, status) => {
+  return state.updateIn([listType, 'items'], (list) => list.delete(status.get('id')));
+};
+
+export default function statusLists(state = initialState, action) {
+  switch(action.type) {
+  case FAVOURITED_STATUSES_FETCH_REQUEST:
+  case FAVOURITED_STATUSES_EXPAND_REQUEST:
+    return state.setIn(['favourites', 'isLoading'], true);
+  case FAVOURITED_STATUSES_FETCH_FAIL:
+  case FAVOURITED_STATUSES_EXPAND_FAIL:
+    return state.setIn(['favourites', 'isLoading'], false);
+  case FAVOURITED_STATUSES_FETCH_SUCCESS:
+    return normalizeList(state, 'favourites', action.statuses, action.next);
+  case FAVOURITED_STATUSES_EXPAND_SUCCESS:
+    return appendToList(state, 'favourites', action.statuses, action.next);
+  case BOOKMARKED_STATUSES_FETCH_REQUEST:
+  case BOOKMARKED_STATUSES_EXPAND_REQUEST:
+    return state.setIn(['bookmarks', 'isLoading'], true);
+  case BOOKMARKED_STATUSES_FETCH_FAIL:
+  case BOOKMARKED_STATUSES_EXPAND_FAIL:
+    return state.setIn(['bookmarks', 'isLoading'], false);
+  case BOOKMARKED_STATUSES_FETCH_SUCCESS:
+    return normalizeList(state, 'bookmarks', action.statuses, action.next);
+  case BOOKMARKED_STATUSES_EXPAND_SUCCESS:
+    return appendToList(state, 'bookmarks', action.statuses, action.next);
+  case TRENDS_STATUSES_FETCH_REQUEST:
+  case TRENDS_STATUSES_EXPAND_REQUEST:
+    return state.setIn(['trending', 'isLoading'], true);
+  case TRENDS_STATUSES_FETCH_FAIL:
+  case TRENDS_STATUSES_EXPAND_FAIL:
+    return state.setIn(['trending', 'isLoading'], false);
+  case TRENDS_STATUSES_FETCH_SUCCESS:
+    return normalizeList(state, 'trending', action.statuses, action.next);
+  case TRENDS_STATUSES_EXPAND_SUCCESS:
+    return appendToList(state, 'trending', action.statuses, action.next);
+  case FAVOURITE_SUCCESS:
+    return prependOneToList(state, 'favourites', action.status);
+  case UNFAVOURITE_SUCCESS:
+    return removeOneFromList(state, 'favourites', action.status);
+  case BOOKMARK_SUCCESS:
+    return prependOneToList(state, 'bookmarks', action.status);
+  case UNBOOKMARK_SUCCESS:
+    return removeOneFromList(state, 'bookmarks', action.status);
+  case PINNED_STATUSES_FETCH_SUCCESS:
+    return normalizeList(state, 'pins', action.statuses, action.next);
+  case PIN_SUCCESS:
+    return prependOneToList(state, 'pins', action.status);
+  case UNPIN_SUCCESS:
+    return removeOneFromList(state, 'pins', action.status);
+  case blockAccountSuccess.type:
+  case muteAccountSuccess.type:
+    return state.updateIn(['trending', 'items'], ImmutableOrderedSet(), list => list.filterNot(statusId => action.payload.statuses.getIn([statusId, 'account']) === action.payload.relationship.id));
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/reducers/statuses.js b/app/javascript/flavours/blobfox/reducers/statuses.js
new file mode 100644
index 00000000000000..340291594fa537
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/statuses.js
@@ -0,0 +1,191 @@
+import { Map as ImmutableMap, fromJS } from 'immutable';
+
+import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer';
+import { normalizeStatusTranslation } from '../actions/importer/normalizer';
+import {
+  REBLOG_REQUEST,
+  REBLOG_FAIL,
+  UNREBLOG_REQUEST,
+  UNREBLOG_FAIL,
+  FAVOURITE_REQUEST,
+  FAVOURITE_FAIL,
+  UNFAVOURITE_REQUEST,
+  UNFAVOURITE_FAIL,
+  BOOKMARK_REQUEST,
+  BOOKMARK_FAIL,
+  UNBOOKMARK_REQUEST,
+  UNBOOKMARK_FAIL,
+  REACTION_UPDATE,
+  REACTION_ADD_FAIL,
+  REACTION_REMOVE_FAIL,
+  REACTION_ADD_REQUEST,
+  REACTION_REMOVE_REQUEST,
+} from '../actions/interactions';
+import {
+  STATUS_MUTE_SUCCESS,
+  STATUS_UNMUTE_SUCCESS,
+  STATUS_REVEAL,
+  STATUS_HIDE,
+  STATUS_COLLAPSE,
+  STATUS_TRANSLATE_SUCCESS,
+  STATUS_TRANSLATE_UNDO,
+  STATUS_FETCH_REQUEST,
+  STATUS_FETCH_FAIL,
+} from '../actions/statuses';
+import { TIMELINE_DELETE } from '../actions/timelines';
+
+const importStatus = (state, status) => state.set(status.id, fromJS(status));
+
+const importStatuses = (state, statuses) =>
+  state.withMutations(mutable => statuses.forEach(status => importStatus(mutable, status)));
+
+const deleteStatus = (state, id, references) => {
+  references.forEach(ref => {
+    state = deleteStatus(state, ref, []);
+  });
+
+  return state.delete(id);
+};
+
+const statusTranslateSuccess = (state, id, translation) => {
+  return state.withMutations(map => {
+    map.setIn([id, 'translation'], fromJS(normalizeStatusTranslation(translation, map.get(id))));
+
+    const list = map.getIn([id, 'media_attachments']);
+    if (translation.media_attachments && list) {
+      translation.media_attachments.forEach(item => {
+        const index = list.findIndex(i => i.get('id') === item.id);
+        map.setIn([id, 'media_attachments', index, 'translation'], fromJS({ description: item.description }));
+      });
+    }
+  });
+};
+
+const statusTranslateUndo = (state, id) => {
+  return state.withMutations(map => {
+    map.deleteIn([id, 'translation']);
+    map.getIn([id, 'media_attachments']).forEach((item, index) => map.deleteIn([id, 'media_attachments', index, 'translation']));
+  });
+};
+
+const updateReaction = (state, id, name, updater) => state.update(
+  id,
+  status => status.update(
+    'reactions',
+    reactions => {
+      const index = reactions.findIndex(reaction => reaction.get('name') === name);
+      if (index > -1) {
+        return reactions.update(index, reaction => updater(reaction));
+      } else {
+        return reactions.push(updater(fromJS({ name, count: 0 })));
+      }
+    },
+  ),
+);
+
+const updateReactionCount = (state, reaction) => updateReaction(state, reaction.status_id, reaction.name, x => x.set('count', reaction.count));
+
+// The url parameter is only used when adding a new custom emoji reaction
+// (one that wasn't in the reactions list before) because we don't have its
+// URL yet.  In all other cases, it's undefined.
+const addReaction = (state, id, name, url) => updateReaction(
+  state,
+  id,
+  name,
+  x => x.set('me', true)
+    .update('count', n => n + 1)
+    .update('url', old => old ? old : url)
+    .update('static_url', old => old ? old : url),
+);
+
+const removeReaction = (state, id, name) => updateReaction(
+  state,
+  id,
+  name,
+  x => x.set('me', false).update('count', n => n - 1),
+);
+
+const initialState = ImmutableMap();
+
+export default function statuses(state = initialState, action) {
+  switch(action.type) {
+  case STATUS_FETCH_REQUEST:
+    return state.setIn([action.id, 'isLoading'], true);
+  case STATUS_FETCH_FAIL:
+    return state.delete(action.id);
+  case STATUS_IMPORT:
+    return importStatus(state, action.status);
+  case STATUSES_IMPORT:
+    return importStatuses(state, action.statuses);
+  case FAVOURITE_REQUEST:
+    return state.setIn([action.status.get('id'), 'favourited'], true);
+  case FAVOURITE_FAIL:
+    return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'favourited'], false);
+  case UNFAVOURITE_REQUEST:
+    return state.setIn([action.status.get('id'), 'favourited'], false);
+  case UNFAVOURITE_FAIL:
+    return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'favourited'], true);
+  case BOOKMARK_REQUEST:
+    return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'bookmarked'], true);
+  case BOOKMARK_FAIL:
+    return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'bookmarked'], false);
+  case UNBOOKMARK_REQUEST:
+    return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'bookmarked'], false);
+  case UNBOOKMARK_FAIL:
+    return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'bookmarked'], true);
+  case REBLOG_REQUEST:
+    return state.setIn([action.status.get('id'), 'reblogged'], true);
+  case REBLOG_FAIL:
+    return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'reblogged'], false);
+  case REACTION_UPDATE:
+    return updateReactionCount(state, action.reaction);
+  case REACTION_ADD_REQUEST:
+  case REACTION_REMOVE_FAIL:
+    return addReaction(state, action.id, action.name, action.url);
+  case REACTION_REMOVE_REQUEST:
+  case REACTION_ADD_FAIL:
+    return removeReaction(state, action.id, action.name);
+  case UNREBLOG_REQUEST:
+    return state.setIn([action.status.get('id'), 'reblogged'], false);
+  case UNREBLOG_FAIL:
+    return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'reblogged'], true);
+  case REACTION_UPDATE:
+    return updateReactionCount(state, action.reaction);
+  case REACTION_ADD_REQUEST:
+  case REACTION_REMOVE_FAIL:
+    return addReaction(state, action.id, action.name, action.url);
+  case REACTION_REMOVE_REQUEST:
+  case REACTION_ADD_FAIL:
+    return removeReaction(state, action.id, action.name);
+  case STATUS_MUTE_SUCCESS:
+    return state.setIn([action.id, 'muted'], true);
+  case STATUS_UNMUTE_SUCCESS:
+    return state.setIn([action.id, 'muted'], false);
+  case STATUS_REVEAL:
+    return state.withMutations(map => {
+      action.ids.forEach(id => {
+        if (!(state.get(id) === undefined)) {
+          map.setIn([id, 'hidden'], false);
+        }
+      });
+    });
+  case STATUS_HIDE:
+    return state.withMutations(map => {
+      action.ids.forEach(id => {
+        if (!(state.get(id) === undefined)) {
+          map.setIn([id, 'hidden'], true);
+        }
+      });
+    });
+  case STATUS_COLLAPSE:
+    return state.setIn([action.id, 'collapsed'], action.isCollapsed);
+  case TIMELINE_DELETE:
+    return deleteStatus(state, action.id, action.references);
+  case STATUS_TRANSLATE_SUCCESS:
+    return statusTranslateSuccess(state, action.id, action.translation);
+  case STATUS_TRANSLATE_UNDO:
+    return statusTranslateUndo(state, action.id);
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/reducers/suggestions.js b/app/javascript/flavours/blobfox/reducers/suggestions.js
new file mode 100644
index 00000000000000..3ab190a9682b7f
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/suggestions.js
@@ -0,0 +1,40 @@
+import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
+
+import { blockAccountSuccess, muteAccountSuccess } from 'flavours/blobfox/actions/accounts';
+import { blockDomainSuccess } from 'flavours/blobfox/actions/domain_blocks';
+
+import {
+  SUGGESTIONS_FETCH_REQUEST,
+  SUGGESTIONS_FETCH_SUCCESS,
+  SUGGESTIONS_FETCH_FAIL,
+  SUGGESTIONS_DISMISS,
+} from '../actions/suggestions';
+
+
+const initialState = ImmutableMap({
+  items: ImmutableList(),
+  isLoading: false,
+});
+
+export default function suggestionsReducer(state = initialState, action) {
+  switch(action.type) {
+  case SUGGESTIONS_FETCH_REQUEST:
+    return state.set('isLoading', true);
+  case SUGGESTIONS_FETCH_SUCCESS:
+    return state.withMutations(map => {
+      map.set('items', fromJS(action.suggestions.map(x => ({ ...x, account: x.account.id }))));
+      map.set('isLoading', false);
+    });
+  case SUGGESTIONS_FETCH_FAIL:
+    return state.set('isLoading', false);
+  case SUGGESTIONS_DISMISS:
+    return state.update('items', list => list.filterNot(x => x.account === action.id));
+  case blockAccountSuccess.type:
+  case muteAccountSuccess.type:
+    return state.update('items', list => list.filterNot(x => x.account === action.payload.relationship.id));
+  case blockDomainSuccess.type:
+    return state.update('items', list => list.filterNot(x => action.payload.accounts.includes(x.account)));
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/reducers/tags.js b/app/javascript/flavours/blobfox/reducers/tags.js
new file mode 100644
index 00000000000000..e56dd9fd8046bb
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/tags.js
@@ -0,0 +1,26 @@
+import { Map as ImmutableMap, fromJS } from 'immutable';
+
+import {
+  HASHTAG_FETCH_SUCCESS,
+  HASHTAG_FOLLOW_REQUEST,
+  HASHTAG_FOLLOW_FAIL,
+  HASHTAG_UNFOLLOW_REQUEST,
+  HASHTAG_UNFOLLOW_FAIL,
+} from 'flavours/blobfox/actions/tags';
+
+const initialState = ImmutableMap();
+
+export default function tags(state = initialState, action) {
+  switch(action.type) {
+  case HASHTAG_FETCH_SUCCESS:
+    return state.set(action.name, fromJS(action.tag));
+  case HASHTAG_FOLLOW_REQUEST:
+  case HASHTAG_UNFOLLOW_FAIL:
+    return state.setIn([action.name, 'following'], true);
+  case HASHTAG_FOLLOW_FAIL:
+  case HASHTAG_UNFOLLOW_REQUEST:
+    return state.setIn([action.name, 'following'], false);
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/reducers/timelines.js b/app/javascript/flavours/blobfox/reducers/timelines.js
new file mode 100644
index 00000000000000..6ff83aa7f0f662
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/timelines.js
@@ -0,0 +1,233 @@
+import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
+
+import {
+  blockAccountSuccess,
+  muteAccountSuccess,
+  unfollowAccountSuccess
+} from '../actions/accounts';
+import {
+  TIMELINE_UPDATE,
+  TIMELINE_DELETE,
+  TIMELINE_CLEAR,
+  TIMELINE_EXPAND_SUCCESS,
+  TIMELINE_EXPAND_REQUEST,
+  TIMELINE_EXPAND_FAIL,
+  TIMELINE_SCROLL_TOP,
+  TIMELINE_CONNECT,
+  TIMELINE_DISCONNECT,
+  TIMELINE_LOAD_PENDING,
+  TIMELINE_MARK_AS_PARTIAL,
+} from '../actions/timelines';
+import { compareId } from '../compare_id';
+
+const initialState = ImmutableMap();
+
+const initialTimeline = ImmutableMap({
+  unread: 0,
+  online: false,
+  top: true,
+  isLoading: false,
+  hasMore: true,
+  pendingItems: ImmutableList(),
+  items: ImmutableList(),
+});
+
+const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, isLoadingRecent, usePendingItems) => {
+  // This method is pretty tricky because:
+  // - existing items in the timeline might be out of order
+  // - the existing timeline may have gaps, most often explicitly noted with a `null` item
+  // - ideally, we don't want it to reorder existing items of the timeline
+  // - `statuses` may include items that are already included in the timeline
+  // - this function can be called either to fill in a gap, or load newer items
+
+  return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
+    mMap.set('isLoading', false);
+    mMap.set('isPartial', isPartial);
+
+    if (!next && !isLoadingRecent) mMap.set('hasMore', false);
+
+    if (timeline.endsWith(':pinned')) {
+      mMap.set('items', statuses.map(status => status.get('id')));
+    } else if (!statuses.isEmpty()) {
+      usePendingItems = isLoadingRecent && (usePendingItems || !mMap.get('pendingItems').isEmpty());
+
+      mMap.update(usePendingItems ? 'pendingItems' : 'items', ImmutableList(), oldIds => {
+        const newIds = statuses.map(status => status.get('id'));
+
+        // Now this gets tricky, as we don't necessarily know for sure where the gap to fill is
+        // and some items in the timeline may not be properly ordered.
+
+        // However, we know that `newIds.last()` is the oldest item that was requested and that
+        // there is no “hole” between `newIds.last()` and `newIds.first()`.
+
+        // First, find the furthest (if properly sorted, oldest) item in the timeline that is
+        // newer than the oldest fetched one, as it's most likely that it delimits the gap.
+        // Start the gap *after* that item.
+        const lastIndex = oldIds.findLastIndex(id => id !== null && compareId(id, newIds.last()) >= 0) + 1;
+
+        // Then, try to find the furthest (if properly sorted, oldest) item in the timeline that
+        // is newer than the most recent fetched one, as it delimits a section comprised of only
+        // items older or within `newIds` (or that were deleted from the server, so should be removed
+        // anyway).
+        // Stop the gap *after* that item.
+        const firstIndex = oldIds.take(lastIndex).findLastIndex(id => id !== null && compareId(id, newIds.first()) > 0) + 1;
+
+        let insertedIds = ImmutableOrderedSet(newIds).withMutations(insertedIds => {
+          // It is possible, though unlikely, that the slice we are replacing contains items older
+          // than the elements we got from the API. Get them and add them back at the back of the
+          // slice.
+          const olderIds = oldIds.slice(firstIndex, lastIndex).filter(id => id !== null && compareId(id, newIds.last()) < 0);
+          insertedIds.union(olderIds);
+
+          // Make sure we aren't inserting duplicates
+          insertedIds.subtract(oldIds.take(firstIndex), oldIds.skip(lastIndex));
+        }).toList();
+
+        // Finally, insert a gap marker if the data is marked as partial by the server
+        if (isPartial && (firstIndex === 0 || oldIds.get(firstIndex - 1) !== null)) {
+          insertedIds = insertedIds.unshift(null);
+        }
+
+        return oldIds.take(firstIndex).concat(
+          insertedIds,
+          oldIds.skip(lastIndex),
+        );
+      });
+    }
+  }));
+};
+
+const updateTimeline = (state, timeline, status, usePendingItems, filtered) => {
+  const top = state.getIn([timeline, 'top']);
+
+  if (usePendingItems || !state.getIn([timeline, 'pendingItems']).isEmpty()) {
+    if (state.getIn([timeline, 'pendingItems'], ImmutableList()).includes(status.get('id')) || state.getIn([timeline, 'items'], ImmutableList()).includes(status.get('id'))) {
+      return state;
+    }
+
+    state = state.update(timeline, initialTimeline, map => map.update('pendingItems', list => list.unshift(status.get('id'))));
+
+    if (!filtered) {
+      state = state.updateIn([timeline, 'unread'], unread => unread + 1);
+    }
+
+    return state;
+  }
+
+  const ids        = state.getIn([timeline, 'items'], ImmutableList());
+  const includesId = ids.includes(status.get('id'));
+  const unread     = state.getIn([timeline, 'unread'], 0);
+
+  if (includesId) {
+    return state;
+  }
+
+  let newIds = ids;
+
+  return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
+    if (!top && !filtered) mMap.set('unread', unread + 1);
+    if (top && ids.size > 40) newIds = newIds.take(20);
+    mMap.set('items', newIds.unshift(status.get('id')));
+  }));
+};
+
+const deleteStatus = (state, id, references, exclude_account = null) => {
+  state.keySeq().forEach(timeline => {
+    if (exclude_account === null || (timeline !== `account:${exclude_account}` && !timeline.startsWith(`account:${exclude_account}:`))) {
+      const helper = list => list.filterNot(item => item === id);
+      state = state.updateIn([timeline, 'items'], helper).updateIn([timeline, 'pendingItems'], helper);
+    }
+  });
+
+  // Remove reblogs of deleted status
+  references.forEach(ref => {
+    state = deleteStatus(state, ref, [], exclude_account);
+  });
+
+  return state;
+};
+
+const clearTimeline = (state, timeline) => {
+  return state.set(timeline, initialTimeline);
+};
+
+const filterTimelines = (state, relationship, statuses) => {
+  let references;
+
+  statuses.forEach(status => {
+    if (status.get('account') !== relationship.id) {
+      return;
+    }
+
+    references = statuses.filter(item => item.get('reblog') === status.get('id')).map(item => item.get('id'));
+    state      = deleteStatus(state, status.get('id'), references, relationship.id);
+  });
+
+  return state;
+};
+
+const filterTimeline = (timeline, state, relationship, statuses) => {
+  const helper = list => list.filterNot(statusId => statuses.getIn([statusId, 'account']) === relationship.id);
+  return state.updateIn([timeline, 'items'], ImmutableList(), helper).updateIn([timeline, 'pendingItems'], ImmutableList(), helper);
+};
+
+const updateTop = (state, timeline, top) => {
+  return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
+    if (top) mMap.set('unread', mMap.get('pendingItems').size);
+    mMap.set('top', top);
+  }));
+};
+
+const reconnectTimeline = (state, usePendingItems) => {
+  if (state.get('online')) {
+    return state;
+  }
+
+  return state.withMutations(mMap => {
+    mMap.update(usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items);
+    mMap.set('online', true);
+  });
+};
+
+export default function timelines(state = initialState, action) {
+  switch(action.type) {
+  case TIMELINE_LOAD_PENDING:
+    return state.update(action.timeline, initialTimeline, map =>
+      map.update('items', list => map.get('pendingItems').concat(list.take(40))).set('pendingItems', ImmutableList()).set('unread', 0));
+  case TIMELINE_EXPAND_REQUEST:
+    return state.update(action.timeline, initialTimeline, map => map.set('isLoading', true));
+  case TIMELINE_EXPAND_FAIL:
+    return state.update(action.timeline, initialTimeline, map => map.set('isLoading', false));
+  case TIMELINE_EXPAND_SUCCESS:
+    return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next, action.partial, action.isLoadingRecent, action.usePendingItems);
+  case TIMELINE_UPDATE:
+    return updateTimeline(state, action.timeline, fromJS(action.status), action.usePendingItems, action.filtered);
+  case TIMELINE_DELETE:
+    return deleteStatus(state, action.id, action.references, action.reblogOf);
+  case TIMELINE_CLEAR:
+    return clearTimeline(state, action.timeline);
+  case blockAccountSuccess.type:
+  case muteAccountSuccess.type:
+    return filterTimelines(state, action.payload.relationship, action.payload.statuses);
+  case unfollowAccountSuccess.type:
+    return filterTimeline('home', state, action.payload.relationship, action.payload.statuses);
+  case TIMELINE_SCROLL_TOP:
+    return updateTop(state, action.timeline, action.top);
+  case TIMELINE_CONNECT:
+    return state.update(action.timeline, initialTimeline, map => reconnectTimeline(map, action.usePendingItems));
+  case TIMELINE_DISCONNECT:
+    return state.update(
+      action.timeline,
+      initialTimeline,
+      map => map.set('online', false).update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items),
+    );
+  case TIMELINE_MARK_AS_PARTIAL:
+    return state.update(
+      action.timeline,
+      initialTimeline,
+      map => map.set('isPartial', true).set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('unread', 0),
+    );
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/reducers/trends.js b/app/javascript/flavours/blobfox/reducers/trends.js
new file mode 100644
index 00000000000000..8f6733ceb310c6
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/trends.js
@@ -0,0 +1,47 @@
+import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
+
+import {
+  TRENDS_TAGS_FETCH_REQUEST,
+  TRENDS_TAGS_FETCH_SUCCESS,
+  TRENDS_TAGS_FETCH_FAIL,
+  TRENDS_LINKS_FETCH_REQUEST,
+  TRENDS_LINKS_FETCH_SUCCESS,
+  TRENDS_LINKS_FETCH_FAIL,
+} from 'flavours/blobfox/actions/trends';
+
+const initialState = ImmutableMap({
+  tags: ImmutableMap({
+    items: ImmutableList(),
+    isLoading: false,
+  }),
+
+  links: ImmutableMap({
+    items: ImmutableList(),
+    isLoading: false,
+  }),
+});
+
+export default function trendsReducer(state = initialState, action) {
+  switch(action.type) {
+  case TRENDS_TAGS_FETCH_REQUEST:
+    return state.setIn(['tags', 'isLoading'], true);
+  case TRENDS_TAGS_FETCH_SUCCESS:
+    return state.withMutations(map => {
+      map.setIn(['tags', 'items'], fromJS(action.trends));
+      map.setIn(['tags', 'isLoading'], false);
+    });
+  case TRENDS_TAGS_FETCH_FAIL:
+    return state.setIn(['tags', 'isLoading'], false);
+  case TRENDS_LINKS_FETCH_REQUEST:
+    return state.setIn(['links', 'isLoading'], true);
+  case TRENDS_LINKS_FETCH_SUCCESS:
+    return state.withMutations(map => {
+      map.setIn(['links', 'items'], fromJS(action.trends));
+      map.setIn(['links', 'isLoading'], false);
+    });
+  case TRENDS_LINKS_FETCH_FAIL:
+    return state.setIn(['links', 'isLoading'], false);
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/reducers/user_lists.js b/app/javascript/flavours/blobfox/reducers/user_lists.js
new file mode 100644
index 00000000000000..a2bd2e8a17373f
--- /dev/null
+++ b/app/javascript/flavours/blobfox/reducers/user_lists.js
@@ -0,0 +1,216 @@
+import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
+
+import {
+  DIRECTORY_FETCH_REQUEST,
+  DIRECTORY_FETCH_SUCCESS,
+  DIRECTORY_FETCH_FAIL,
+  DIRECTORY_EXPAND_REQUEST,
+  DIRECTORY_EXPAND_SUCCESS,
+  DIRECTORY_EXPAND_FAIL,
+} from 'flavours/blobfox/actions/directory';
+import {
+  FEATURED_TAGS_FETCH_REQUEST,
+  FEATURED_TAGS_FETCH_SUCCESS,
+  FEATURED_TAGS_FETCH_FAIL,
+} from 'flavours/blobfox/actions/featured_tags';
+
+import {
+  FOLLOWERS_FETCH_REQUEST,
+  FOLLOWERS_FETCH_SUCCESS,
+  FOLLOWERS_FETCH_FAIL,
+  FOLLOWERS_EXPAND_REQUEST,
+  FOLLOWERS_EXPAND_SUCCESS,
+  FOLLOWERS_EXPAND_FAIL,
+  FOLLOWING_FETCH_REQUEST,
+  FOLLOWING_FETCH_SUCCESS,
+  FOLLOWING_FETCH_FAIL,
+  FOLLOWING_EXPAND_REQUEST,
+  FOLLOWING_EXPAND_SUCCESS,
+  FOLLOWING_EXPAND_FAIL,
+  FOLLOW_REQUESTS_FETCH_REQUEST,
+  FOLLOW_REQUESTS_FETCH_SUCCESS,
+  FOLLOW_REQUESTS_FETCH_FAIL,
+  FOLLOW_REQUESTS_EXPAND_REQUEST,
+  FOLLOW_REQUESTS_EXPAND_SUCCESS,
+  FOLLOW_REQUESTS_EXPAND_FAIL,
+  authorizeFollowRequestSuccess,
+  rejectFollowRequestSuccess,
+} from '../actions/accounts';
+import {
+  BLOCKS_FETCH_REQUEST,
+  BLOCKS_FETCH_SUCCESS,
+  BLOCKS_FETCH_FAIL,
+  BLOCKS_EXPAND_REQUEST,
+  BLOCKS_EXPAND_SUCCESS,
+  BLOCKS_EXPAND_FAIL,
+} from '../actions/blocks';
+import {
+  REBLOGS_FETCH_REQUEST,
+  REBLOGS_FETCH_SUCCESS,
+  REBLOGS_FETCH_FAIL,
+  REBLOGS_EXPAND_REQUEST,
+  REBLOGS_EXPAND_SUCCESS,
+  REBLOGS_EXPAND_FAIL,
+  FAVOURITES_FETCH_REQUEST,
+  FAVOURITES_FETCH_SUCCESS,
+  FAVOURITES_FETCH_FAIL,
+  FAVOURITES_EXPAND_REQUEST,
+  FAVOURITES_EXPAND_SUCCESS,
+  FAVOURITES_EXPAND_FAIL,
+} from '../actions/interactions';
+import {
+  MUTES_FETCH_REQUEST,
+  MUTES_FETCH_SUCCESS,
+  MUTES_FETCH_FAIL,
+  MUTES_EXPAND_REQUEST,
+  MUTES_EXPAND_SUCCESS,
+  MUTES_EXPAND_FAIL,
+} from '../actions/mutes';
+import { notificationsUpdate } from '../actions/notifications';
+
+const initialListState = ImmutableMap({
+  next: null,
+  isLoading: false,
+  items: ImmutableList(),
+});
+
+const initialState = ImmutableMap({
+  followers: initialListState,
+  following: initialListState,
+  reblogged_by: initialListState,
+  favourited_by: initialListState,
+  follow_requests: initialListState,
+  blocks: initialListState,
+  mutes: initialListState,
+  featured_tags: initialListState,
+});
+
+const normalizeList = (state, path, accounts, next) => {
+  return state.setIn(path, ImmutableMap({
+    next,
+    items: ImmutableList(accounts.map(item => item.id)),
+    isLoading: false,
+  }));
+};
+
+const appendToList = (state, path, accounts, next) => {
+  return state.updateIn(path, map => {
+    return map.set('next', next).set('isLoading', false).update('items', list => list.concat(accounts.map(item => item.id)));
+  });
+};
+
+const normalizeFollowRequest = (state, notification) => {
+  return state.updateIn(['follow_requests', 'items'], list => {
+    return list.filterNot(item => item === notification.account.id).unshift(notification.account.id);
+  });
+};
+
+const normalizeFeaturedTag = (featuredTags, accountId) => {
+  const normalizeFeaturedTag = { ...featuredTags, accountId: accountId };
+  return fromJS(normalizeFeaturedTag);
+};
+
+const normalizeFeaturedTags = (state, path, featuredTags, accountId) => {
+  return state.setIn(path, ImmutableMap({
+    items: ImmutableList(featuredTags.map(featuredTag => normalizeFeaturedTag(featuredTag, accountId)).sort((a, b) => b.get('statuses_count') - a.get('statuses_count'))),
+    isLoading: false,
+  }));
+};
+
+export default function userLists(state = initialState, action) {
+  switch(action.type) {
+  case FOLLOWERS_FETCH_SUCCESS:
+    return normalizeList(state, ['followers', action.id], action.accounts, action.next);
+  case FOLLOWERS_EXPAND_SUCCESS:
+    return appendToList(state, ['followers', action.id], action.accounts, action.next);
+  case FOLLOWERS_FETCH_REQUEST:
+  case FOLLOWERS_EXPAND_REQUEST:
+    return state.setIn(['followers', action.id, 'isLoading'], true);
+  case FOLLOWERS_FETCH_FAIL:
+  case FOLLOWERS_EXPAND_FAIL:
+    return state.setIn(['followers', action.id, 'isLoading'], false);
+  case FOLLOWING_FETCH_SUCCESS:
+    return normalizeList(state, ['following', action.id], action.accounts, action.next);
+  case FOLLOWING_EXPAND_SUCCESS:
+    return appendToList(state, ['following', action.id], action.accounts, action.next);
+  case FOLLOWING_FETCH_REQUEST:
+  case FOLLOWING_EXPAND_REQUEST:
+    return state.setIn(['following', action.id, 'isLoading'], true);
+  case FOLLOWING_FETCH_FAIL:
+  case FOLLOWING_EXPAND_FAIL:
+    return state.setIn(['following', action.id, 'isLoading'], false);
+  case REBLOGS_FETCH_SUCCESS:
+    return normalizeList(state, ['reblogged_by', action.id], action.accounts, action.next);
+  case REBLOGS_EXPAND_SUCCESS:
+    return appendToList(state, ['reblogged_by', action.id], action.accounts, action.next);
+  case REBLOGS_FETCH_REQUEST:
+  case REBLOGS_EXPAND_REQUEST:
+    return state.setIn(['reblogged_by', action.id, 'isLoading'], true);
+  case REBLOGS_FETCH_FAIL:
+  case REBLOGS_EXPAND_FAIL:
+    return state.setIn(['reblogged_by', action.id, 'isLoading'], false);
+  case FAVOURITES_FETCH_SUCCESS:
+    return normalizeList(state, ['favourited_by', action.id], action.accounts, action.next);
+  case FAVOURITES_EXPAND_SUCCESS:
+    return appendToList(state, ['favourited_by', action.id], action.accounts, action.next);
+  case FAVOURITES_FETCH_REQUEST:
+  case FAVOURITES_EXPAND_REQUEST:
+    return state.setIn(['favourited_by', action.id, 'isLoading'], true);
+  case FAVOURITES_FETCH_FAIL:
+  case FAVOURITES_EXPAND_FAIL:
+    return state.setIn(['favourited_by', action.id, 'isLoading'], false);
+  case notificationsUpdate.type:
+    return action.payload.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.payload.notification) : state;
+  case FOLLOW_REQUESTS_FETCH_SUCCESS:
+    return normalizeList(state, ['follow_requests'], action.accounts, action.next);
+  case FOLLOW_REQUESTS_EXPAND_SUCCESS:
+    return appendToList(state, ['follow_requests'], action.accounts, action.next);
+  case FOLLOW_REQUESTS_FETCH_REQUEST:
+  case FOLLOW_REQUESTS_EXPAND_REQUEST:
+    return state.setIn(['follow_requests', 'isLoading'], true);
+  case FOLLOW_REQUESTS_FETCH_FAIL:
+  case FOLLOW_REQUESTS_EXPAND_FAIL:
+    return state.setIn(['follow_requests', 'isLoading'], false);
+  case authorizeFollowRequestSuccess.type:
+  case rejectFollowRequestSuccess.type:
+    return state.updateIn(['follow_requests', 'items'], list => list.filterNot(item => item === action.payload.id));
+  case BLOCKS_FETCH_SUCCESS:
+    return normalizeList(state, ['blocks'], action.accounts, action.next);
+  case BLOCKS_EXPAND_SUCCESS:
+    return appendToList(state, ['blocks'], action.accounts, action.next);
+  case BLOCKS_FETCH_REQUEST:
+  case BLOCKS_EXPAND_REQUEST:
+    return state.setIn(['blocks', 'isLoading'], true);
+  case BLOCKS_FETCH_FAIL:
+  case BLOCKS_EXPAND_FAIL:
+    return state.setIn(['blocks', 'isLoading'], false);
+  case MUTES_FETCH_SUCCESS:
+    return normalizeList(state, ['mutes'], action.accounts, action.next);
+  case MUTES_EXPAND_SUCCESS:
+    return appendToList(state, ['mutes'], action.accounts, action.next);
+  case MUTES_FETCH_REQUEST:
+  case MUTES_EXPAND_REQUEST:
+    return state.setIn(['mutes', 'isLoading'], true);
+  case MUTES_FETCH_FAIL:
+  case MUTES_EXPAND_FAIL:
+    return state.setIn(['mutes', 'isLoading'], false);
+  case DIRECTORY_FETCH_SUCCESS:
+    return normalizeList(state, ['directory'], action.accounts, action.next);
+  case DIRECTORY_EXPAND_SUCCESS:
+    return appendToList(state, ['directory'], action.accounts, action.next);
+  case DIRECTORY_FETCH_REQUEST:
+  case DIRECTORY_EXPAND_REQUEST:
+    return state.setIn(['directory', 'isLoading'], true);
+  case DIRECTORY_FETCH_FAIL:
+  case DIRECTORY_EXPAND_FAIL:
+    return state.setIn(['directory', 'isLoading'], false);
+  case FEATURED_TAGS_FETCH_SUCCESS:
+    return normalizeFeaturedTags(state, ['featured_tags', action.id], action.tags, action.id);
+  case FEATURED_TAGS_FETCH_REQUEST:
+    return state.setIn(['featured_tags', action.id, 'isLoading'], true);
+  case FEATURED_TAGS_FETCH_FAIL:
+    return state.setIn(['featured_tags', action.id, 'isLoading'], false);
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/scroll.ts b/app/javascript/flavours/blobfox/scroll.ts
new file mode 100644
index 00000000000000..35e13a4527d1d3
--- /dev/null
+++ b/app/javascript/flavours/blobfox/scroll.ts
@@ -0,0 +1,50 @@
+const easingOutQuint = (
+  x: number,
+  t: number,
+  b: number,
+  c: number,
+  d: number,
+) => c * ((t = t / d - 1) * t * t * t * t + 1) + b;
+const scroll = (
+  node: Element,
+  key: 'scrollTop' | 'scrollLeft',
+  target: number,
+) => {
+  const startTime = Date.now();
+  const offset = node[key];
+  const gap = target - offset;
+  const duration = 1000;
+  let interrupt = false;
+
+  const step = () => {
+    const elapsed = Date.now() - startTime;
+    const percentage = elapsed / duration;
+
+    if (percentage > 1 || interrupt) {
+      return;
+    }
+
+    node[key] = easingOutQuint(0, elapsed, offset, gap, duration);
+    requestAnimationFrame(step);
+  };
+
+  step();
+
+  return () => {
+    interrupt = true;
+  };
+};
+
+const isScrollBehaviorSupported =
+  'scrollBehavior' in document.documentElement.style;
+
+export const scrollRight = (node: Element, position: number) => {
+  if (isScrollBehaviorSupported)
+    node.scrollTo({ left: position, behavior: 'smooth' });
+  else scroll(node, 'scrollLeft', position);
+};
+
+export const scrollTop = (node: Element) => {
+  if (isScrollBehaviorSupported) node.scrollTo({ top: 0, behavior: 'smooth' });
+  else scroll(node, 'scrollTop', 0);
+};
diff --git a/app/javascript/flavours/blobfox/selectors/accounts.ts b/app/javascript/flavours/blobfox/selectors/accounts.ts
new file mode 100644
index 00000000000000..af2319405df219
--- /dev/null
+++ b/app/javascript/flavours/blobfox/selectors/accounts.ts
@@ -0,0 +1,47 @@
+import { Record as ImmutableRecord } from 'immutable';
+import { createSelector } from 'reselect';
+
+import { accountDefaultValues } from 'flavours/blobfox/models/account';
+import type { Account, AccountShape } from 'flavours/blobfox/models/account';
+import type { Relationship } from 'flavours/blobfox/models/relationship';
+import type { RootState } from 'flavours/blobfox/store';
+
+const getAccountBase = (state: RootState, id: string) =>
+  state.accounts.get(id, null);
+
+const getAccountRelationship = (state: RootState, id: string) =>
+  state.relationships.get(id, null);
+
+const getAccountMoved = (state: RootState, id: string) => {
+  const movedToId = state.accounts.get(id)?.moved;
+
+  if (!movedToId) return undefined;
+
+  return state.accounts.get(movedToId);
+};
+
+interface FullAccountShape extends Omit<AccountShape, 'moved'> {
+  relationship: Relationship | null;
+  moved: Account | null;
+}
+
+const FullAccountFactory = ImmutableRecord<FullAccountShape>({
+  ...accountDefaultValues,
+  moved: null,
+  relationship: null,
+});
+
+export function makeGetAccount() {
+  return createSelector(
+    [getAccountBase, getAccountRelationship, getAccountMoved],
+    (base, relationship, moved) => {
+      if (base === null) {
+        return null;
+      }
+
+      return FullAccountFactory(base)
+        .set('relationship', relationship)
+        .set('moved', moved ?? null);
+    },
+  );
+}
diff --git a/app/javascript/flavours/blobfox/selectors/index.js b/app/javascript/flavours/blobfox/selectors/index.js
new file mode 100644
index 00000000000000..ceb940031940b0
--- /dev/null
+++ b/app/javascript/flavours/blobfox/selectors/index.js
@@ -0,0 +1,118 @@
+import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
+import { createSelector } from 'reselect';
+
+import { toServerSideType } from 'flavours/blobfox/utils/filters';
+
+import { me } from '../initial_state';
+
+export { makeGetAccount } from "./accounts";
+
+const getFilters = (state, { contextType }) => {
+  if (!contextType) return null;
+
+  const serverSideType = toServerSideType(contextType);
+  const now = new Date();
+
+  return state.get('filters').filter((filter) => filter.get('context').includes(serverSideType) && (filter.get('expires_at') === null || filter.get('expires_at') > now));
+};
+
+export const makeGetStatus = () => {
+  return createSelector(
+    [
+      (state, { id }) => state.getIn(['statuses', id]),
+      (state, { id }) => state.getIn(['statuses', state.getIn(['statuses', id, 'reblog'])]),
+      (state, { id }) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]),
+      (state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]),
+      getFilters,
+    ],
+
+    (statusBase, statusReblog, accountBase, accountReblog, filters) => {
+      if (!statusBase || statusBase.get('isLoading')) {
+        return null;
+      }
+
+      let filtered = false;
+      if ((accountReblog || accountBase).get('id') !== me && filters) {
+        let filterResults = statusReblog?.get('filtered') || statusBase.get('filtered') || ImmutableList();
+        if (filterResults.some((result) => filters.getIn([result.get('filter'), 'filter_action']) === 'hide')) {
+          return null;
+        }
+        filterResults = filterResults.filter(result => filters.has(result.get('filter')));
+        if (!filterResults.isEmpty()) {
+          filtered = filterResults.map(result => filters.getIn([result.get('filter'), 'title']));
+        }
+      }
+
+      if (statusReblog) {
+        statusReblog = statusReblog.set('account', accountReblog);
+        statusReblog = statusReblog.set('matched_filters', filtered);
+      } else {
+        statusReblog = null;
+      }
+
+      return statusBase.withMutations(map => {
+        map.set('reblog', statusReblog);
+        map.set('account', accountBase);
+        map.set('matched_filters', filtered);
+      });
+    },
+  );
+};
+
+export const makeGetPictureInPicture = () => {
+  return createSelector([
+    (state, { id }) => state.get('picture_in_picture').statusId === id,
+    (state) => state.getIn(['meta', 'layout']) !== 'mobile',
+  ], (inUse, available) => ImmutableMap({
+    inUse: inUse && available,
+    available,
+  }));
+};
+
+const ALERT_DEFAULTS = {
+  dismissAfter: 5000,
+  style: false,
+};
+
+export const getAlerts = createSelector(state => state.get('alerts'), alerts =>
+  alerts.map(item => ({
+    ...ALERT_DEFAULTS,
+    ...item,
+  })).toArray());
+
+export const makeGetNotification = () => createSelector([
+  (_, base)             => base,
+  (state, _, accountId) => state.getIn(['accounts', accountId]),
+], (base, account) => base.set('account', account));
+
+export const makeGetReport = () => createSelector([
+  (_, base) => base,
+  (state, _, targetAccountId) => state.getIn(['accounts', targetAccountId]),
+], (base, targetAccount) => base.set('target_account', targetAccount));
+
+export const getAccountGallery = createSelector([
+  (state, id) => state.getIn(['timelines', `account:${id}:media`, 'items'], ImmutableList()),
+  state       => state.get('statuses'),
+  (state, id) => state.getIn(['accounts', id]),
+], (statusIds, statuses, account) => {
+  let medias = ImmutableList();
+
+  statusIds.forEach(statusId => {
+    const status = statuses.get(statusId);
+    medias = medias.concat(status.get('media_attachments').map(media => media.set('status', status).set('account', account)));
+  });
+
+  return medias;
+});
+
+export const getAccountHidden = createSelector([
+  (state, id) => state.getIn(['accounts', id, 'hidden']),
+  (state, id) => state.getIn(['relationships', id, 'following']) || state.getIn(['relationships', id, 'requested']),
+  (state, id) => id === me,
+], (hidden, followingOrRequested, isSelf) => {
+  return hidden && !(isSelf || followingOrRequested);
+});
+
+export const getStatusList = createSelector([
+  (state, type) => state.getIn(['status_lists', type, 'items']),
+], (items) => items.toList());
diff --git a/app/javascript/flavours/blobfox/settings.js b/app/javascript/flavours/blobfox/settings.js
new file mode 100644
index 00000000000000..f31aee0afc0725
--- /dev/null
+++ b/app/javascript/flavours/blobfox/settings.js
@@ -0,0 +1,50 @@
+export default class Settings {
+
+  constructor(keyBase = null) {
+    this.keyBase = keyBase;
+  }
+
+  generateKey(id) {
+    return this.keyBase ? [this.keyBase, `id${id}`].join('.') : id;
+  }
+
+  set(id, data) {
+    const key = this.generateKey(id);
+    try {
+      const encodedData = JSON.stringify(data);
+      localStorage.setItem(key, encodedData);
+      return data;
+    } catch (e) {
+      return null;
+    }
+  }
+
+  get(id) {
+    const key = this.generateKey(id);
+    try {
+      const rawData = localStorage.getItem(key);
+      return JSON.parse(rawData);
+    } catch (e) {
+      return null;
+    }
+  }
+
+  remove(id) {
+    const data = this.get(id);
+    if (data) {
+      const key = this.generateKey(id);
+      try {
+        localStorage.removeItem(key);
+      } catch (e) {
+      }
+    }
+    return data;
+  }
+
+}
+
+export const pushNotificationsSetting = new Settings('mastodon_push_notification_data');
+export const tagHistory = new Settings('mastodon_tag_history');
+export const bannerSettings = new Settings('mastodon_banner_settings');
+export const searchHistory = new Settings('mastodon_search_history');
+export const playerSettings = new Settings('mastodon_player');
diff --git a/app/javascript/flavours/blobfox/store/index.ts b/app/javascript/flavours/blobfox/store/index.ts
new file mode 100644
index 00000000000000..c2629b0ed74050
--- /dev/null
+++ b/app/javascript/flavours/blobfox/store/index.ts
@@ -0,0 +1,8 @@
+export { store } from './store';
+export type { GetState, AppDispatch, RootState } from './store';
+
+export {
+  createAppAsyncThunk,
+  useAppDispatch,
+  useAppSelector,
+} from './typed_functions';
diff --git a/app/javascript/flavours/blobfox/store/middlewares/errors.ts b/app/javascript/flavours/blobfox/store/middlewares/errors.ts
new file mode 100644
index 00000000000000..9f28f5ff53f92b
--- /dev/null
+++ b/app/javascript/flavours/blobfox/store/middlewares/errors.ts
@@ -0,0 +1,21 @@
+import type { AnyAction, Middleware } from 'redux';
+
+import type { RootState } from '..';
+import { showAlertForError } from '../../actions/alerts';
+
+const defaultFailSuffix = 'FAIL';
+
+export const errorsMiddleware: Middleware<unknown, RootState> =
+  ({ dispatch }) =>
+  (next) =>
+  (action: AnyAction & { skipAlert?: boolean; skipNotFound?: boolean }) => {
+    if (action.type && !action.skipAlert) {
+      const isFail = new RegExp(`${defaultFailSuffix}$`, 'g');
+
+      if (typeof action.type === 'string' && action.type.match(isFail)) {
+        dispatch(showAlertForError(action.error, action.skipNotFound));
+      }
+    }
+
+    return next(action);
+  };
diff --git a/app/javascript/flavours/blobfox/store/middlewares/loading_bar.ts b/app/javascript/flavours/blobfox/store/middlewares/loading_bar.ts
new file mode 100644
index 00000000000000..83056ee49f44a9
--- /dev/null
+++ b/app/javascript/flavours/blobfox/store/middlewares/loading_bar.ts
@@ -0,0 +1,69 @@
+import {
+  isAsyncThunkAction,
+  isPending as isThunkActionPending,
+  isFulfilled as isThunkActionFulfilled,
+  isRejected as isThunkActionRejected,
+} from '@reduxjs/toolkit';
+import { showLoading, hideLoading } from 'react-redux-loading-bar';
+import type { AnyAction, Middleware } from 'redux';
+
+import type { RootState } from '..';
+
+interface Config {
+  promiseTypeSuffixes?: string[];
+}
+
+const defaultTypeSuffixes: Config['promiseTypeSuffixes'] = [
+  'PENDING',
+  'FULFILLED',
+  'REJECTED',
+];
+
+export const loadingBarMiddleware = (
+  config: Config = {},
+): Middleware<unknown, RootState> => {
+  const promiseTypeSuffixes = config.promiseTypeSuffixes ?? defaultTypeSuffixes;
+
+  return ({ dispatch }) =>
+    (next) =>
+    (action: AnyAction) => {
+      let isPending = false;
+      let isFulfilled = false;
+      let isRejected = false;
+
+      if (
+        isAsyncThunkAction(action)
+        // TODO: once we get the first use-case for it, add a check for skipLoading
+      ) {
+        if (isThunkActionPending(action)) isPending = true;
+        else if (isThunkActionFulfilled(action)) isFulfilled = true;
+        else if (isThunkActionRejected(action)) isRejected = true;
+      } else if (
+        action.type &&
+        !action.skipLoading &&
+        typeof action.type === 'string'
+      ) {
+        const [PENDING, FULFILLED, REJECTED] = promiseTypeSuffixes;
+
+        const isPendingRegexp = new RegExp(`${PENDING}$`, 'g');
+        const isFulfilledRegexp = new RegExp(`${FULFILLED}$`, 'g');
+        const isRejectedRegexp = new RegExp(`${REJECTED}$`, 'g');
+
+        if (action.type.match(isPendingRegexp)) {
+          isPending = true;
+        } else if (action.type.match(isFulfilledRegexp)) {
+          isFulfilled = true;
+        } else if (action.type.match(isRejectedRegexp)) {
+          isRejected = true;
+        }
+      }
+
+      if (isPending) {
+        dispatch(showLoading());
+      } else if (isFulfilled || isRejected) {
+        dispatch(hideLoading());
+      }
+
+      return next(action);
+    };
+};
diff --git a/app/javascript/flavours/blobfox/store/middlewares/sounds.ts b/app/javascript/flavours/blobfox/store/middlewares/sounds.ts
new file mode 100644
index 00000000000000..3b0556c9c29db4
--- /dev/null
+++ b/app/javascript/flavours/blobfox/store/middlewares/sounds.ts
@@ -0,0 +1,64 @@
+import type { Middleware, AnyAction } from 'redux';
+
+import ready from 'flavours/blobfox/ready';
+import { assetHost } from 'flavours/blobfox/utils/config';
+
+import type { RootState } from '..';
+
+interface AudioSource {
+  src: string;
+  type: string;
+}
+
+const createAudio = (sources: AudioSource[]) => {
+  const audio = new Audio();
+  sources.forEach(({ type, src }) => {
+    const source = document.createElement('source');
+    source.type = type;
+    source.src = src;
+    audio.appendChild(source);
+  });
+  return audio;
+};
+
+const play = (audio: HTMLAudioElement) => {
+  if (!audio.paused) {
+    audio.pause();
+    if (typeof audio.fastSeek === 'function') {
+      audio.fastSeek(0);
+    } else {
+      audio.currentTime = 0;
+    }
+  }
+
+  void audio.play();
+};
+
+export const soundsMiddleware = (): Middleware<unknown, RootState> => {
+  const soundCache: Record<string, HTMLAudioElement> = {};
+
+  void ready(() => {
+    soundCache.boop = createAudio([
+      {
+        src: `${assetHost}/sounds/boop.ogg`,
+        type: 'audio/ogg',
+      },
+      {
+        src: `${assetHost}/sounds/boop.mp3`,
+        type: 'audio/mpeg',
+      },
+    ]);
+  });
+
+  return () =>
+    (next) =>
+    (action: AnyAction & { meta?: { sound?: string } }) => {
+      const sound = action.meta?.sound;
+
+      if (sound && Object.hasOwn(soundCache, sound)) {
+        play(soundCache[sound]);
+      }
+
+      return next(action);
+    };
+};
diff --git a/app/javascript/flavours/blobfox/store/store.ts b/app/javascript/flavours/blobfox/store/store.ts
new file mode 100644
index 00000000000000..9f43f58a43dfa4
--- /dev/null
+++ b/app/javascript/flavours/blobfox/store/store.ts
@@ -0,0 +1,39 @@
+import { configureStore } from '@reduxjs/toolkit';
+
+import { rootReducer } from '../reducers';
+
+import { errorsMiddleware } from './middlewares/errors';
+import { loadingBarMiddleware } from './middlewares/loading_bar';
+import { soundsMiddleware } from './middlewares/sounds';
+
+export const store = configureStore({
+  reducer: rootReducer,
+  middleware: (getDefaultMiddleware) =>
+    getDefaultMiddleware({
+      // In development, Redux Toolkit enables 2 default middlewares to detect
+      // common issues with states. Unfortunately, our use of ImmutableJS for state
+      // triggers both, so lets disable them until our state is fully refactored
+
+      // https://redux-toolkit.js.org/api/serializabilityMiddleware
+      // This checks recursively that every values in the state are serializable in JSON
+      // Which is not the case, as we use ImmutableJS structures, but also File objects
+      serializableCheck: false,
+
+      // https://redux-toolkit.js.org/api/immutabilityMiddleware
+      // This checks recursively if every value in the state is immutable (ie, a JS primitive type)
+      // But this is not the case, as our Root State is an ImmutableJS map, which is an object
+      immutableCheck: false,
+    })
+      .concat(
+        loadingBarMiddleware({
+          promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'],
+        }),
+      )
+      .concat(errorsMiddleware)
+      .concat(soundsMiddleware()),
+});
+
+// Infer the `RootState` and `AppDispatch` types from the store itself
+export type RootState = ReturnType<typeof rootReducer>;
+export type AppDispatch = typeof store.dispatch;
+export type GetState = typeof store.getState;
diff --git a/app/javascript/flavours/blobfox/store/typed_functions.ts b/app/javascript/flavours/blobfox/store/typed_functions.ts
new file mode 100644
index 00000000000000..f1e71385a88ff5
--- /dev/null
+++ b/app/javascript/flavours/blobfox/store/typed_functions.ts
@@ -0,0 +1,15 @@
+import type { TypedUseSelectorHook } from 'react-redux';
+import { useDispatch, useSelector } from 'react-redux';
+
+import { createAsyncThunk } from '@reduxjs/toolkit';
+
+import type { AppDispatch, RootState } from './store';
+
+export const useAppDispatch: () => AppDispatch = useDispatch;
+export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
+
+export const createAppAsyncThunk = createAsyncThunk.withTypes<{
+  state: RootState;
+  dispatch: AppDispatch;
+  rejectValue: string;
+}>();
diff --git a/app/javascript/flavours/blobfox/stream.js b/app/javascript/flavours/blobfox/stream.js
new file mode 100644
index 00000000000000..ff3af5fd885c96
--- /dev/null
+++ b/app/javascript/flavours/blobfox/stream.js
@@ -0,0 +1,275 @@
+// @ts-check
+
+import WebSocketClient from '@gamestdio/websocket';
+
+/**
+ * @type {WebSocketClient | undefined}
+ */
+let sharedConnection;
+
+/**
+ * @typedef Subscription
+ * @property {string} channelName
+ * @property {Object.<string, string>} params
+ * @property {function(): void} onConnect
+ * @property {function(StreamEvent): void} onReceive
+ * @property {function(): void} onDisconnect
+ */
+
+/**
+ * @typedef StreamEvent
+ * @property {string} event
+ * @property {object} payload
+ */
+
+/**
+ * @type {Array.<Subscription>}
+ */
+const subscriptions = [];
+
+/**
+ * @type {Object.<string, number>}
+ */
+const subscriptionCounters = {};
+
+/**
+ * @param {Subscription} subscription
+ */
+const addSubscription = subscription => {
+  subscriptions.push(subscription);
+};
+
+/**
+ * @param {Subscription} subscription
+ */
+const removeSubscription = subscription => {
+  const index = subscriptions.indexOf(subscription);
+
+  if (index !== -1) {
+    subscriptions.splice(index, 1);
+  }
+};
+
+/**
+ * @param {Subscription} subscription
+ */
+const subscribe = ({ channelName, params, onConnect }) => {
+  const key = channelNameWithInlineParams(channelName, params);
+
+  subscriptionCounters[key] = subscriptionCounters[key] || 0;
+
+  if (subscriptionCounters[key] === 0) {
+    // @ts-expect-error
+    sharedConnection.send(JSON.stringify({ type: 'subscribe', stream: channelName, ...params }));
+  }
+
+  subscriptionCounters[key] += 1;
+  onConnect();
+};
+
+/**
+ * @param {Subscription} subscription
+ */
+const unsubscribe = ({ channelName, params, onDisconnect }) => {
+  const key = channelNameWithInlineParams(channelName, params);
+
+  subscriptionCounters[key] = subscriptionCounters[key] || 1;
+
+  // @ts-expect-error
+  if (subscriptionCounters[key] === 1 && sharedConnection.readyState === WebSocketClient.OPEN) {
+    // @ts-expect-error
+    sharedConnection.send(JSON.stringify({ type: 'unsubscribe', stream: channelName, ...params }));
+  }
+
+  subscriptionCounters[key] -= 1;
+  onDisconnect();
+};
+
+const sharedCallbacks = {
+  connected() {
+    subscriptions.forEach(subscription => subscribe(subscription));
+  },
+
+  // @ts-expect-error
+  received(data) {
+    const { stream } = data;
+
+    subscriptions.filter(({ channelName, params }) => {
+      const streamChannelName = stream[0];
+
+      if (stream.length === 1) {
+        return channelName === streamChannelName;
+      }
+
+      const streamIdentifier = stream[1];
+
+      if (['hashtag', 'hashtag:local'].includes(channelName)) {
+        return channelName === streamChannelName && params.tag === streamIdentifier;
+      } else if (channelName === 'list') {
+        return channelName === streamChannelName && params.list === streamIdentifier;
+      }
+
+      return false;
+    }).forEach(subscription => {
+      subscription.onReceive(data);
+    });
+  },
+
+  disconnected() {
+    subscriptions.forEach(subscription => unsubscribe(subscription));
+  },
+
+  reconnected() {
+  },
+};
+
+/**
+ * @param {string} channelName
+ * @param {Object.<string, string>} params
+ * @returns {string}
+ */
+const channelNameWithInlineParams = (channelName, params) => {
+  if (Object.keys(params).length === 0) {
+    return channelName;
+  }
+
+  return `${channelName}&${Object.keys(params).map(key => `${key}=${params[key]}`).join('&')}`;
+};
+
+/**
+ * @param {string} channelName
+ * @param {Object.<string, string>} params
+ * @param {function(Function, Function): { onConnect: (function(): void), onReceive: (function(StreamEvent): void), onDisconnect: (function(): void) }} callbacks
+ * @returns {function(): void}
+ */
+// @ts-expect-error
+export const connectStream = (channelName, params, callbacks) => (dispatch, getState) => {
+  const streamingAPIBaseURL = getState().getIn(['meta', 'streaming_api_base_url']);
+  const accessToken = getState().getIn(['meta', 'access_token']);
+  const { onConnect, onReceive, onDisconnect } = callbacks(dispatch, getState);
+
+  // If we cannot use a websockets connection, we must fall back
+  // to using individual connections for each channel
+  if (!streamingAPIBaseURL.startsWith('ws')) {
+    const connection = createConnection(streamingAPIBaseURL, accessToken, channelNameWithInlineParams(channelName, params), {
+      connected() {
+        onConnect();
+      },
+
+      received(data) {
+        onReceive(data);
+      },
+
+      disconnected() {
+        onDisconnect();
+      },
+
+      reconnected() {
+        onConnect();
+      },
+    });
+
+    return () => {
+      connection.close();
+    };
+  }
+
+  const subscription = {
+    channelName,
+    params,
+    onConnect,
+    onReceive,
+    onDisconnect,
+  };
+
+  addSubscription(subscription);
+
+  // If a connection is open, we can execute the subscription right now. Otherwise,
+  // because we have already registered it, it will be executed on connect
+
+  if (!sharedConnection) {
+    sharedConnection = /** @type {WebSocketClient} */ (createConnection(streamingAPIBaseURL, accessToken, '', sharedCallbacks));
+  } else if (sharedConnection.readyState === WebSocketClient.OPEN) {
+    subscribe(subscription);
+  }
+
+  return () => {
+    removeSubscription(subscription);
+    unsubscribe(subscription);
+  };
+};
+
+const KNOWN_EVENT_TYPES = [
+  'update',
+  'delete',
+  'notification',
+  'conversation',
+  'filters_changed',
+  'encrypted_message',
+  'announcement',
+  'announcement.delete',
+  'announcement.reaction',
+];
+
+/**
+ * @param {MessageEvent} e
+ * @param {function(StreamEvent): void} received
+ */
+const handleEventSourceMessage = (e, received) => {
+  received({
+    event: e.type,
+    payload: e.data,
+  });
+};
+
+/**
+ * @param {string} streamingAPIBaseURL
+ * @param {string} accessToken
+ * @param {string} channelName
+ * @param {{ connected: Function, received: function(StreamEvent): void, disconnected: Function, reconnected: Function }} callbacks
+ * @returns {WebSocketClient | EventSource}
+ */
+const createConnection = (streamingAPIBaseURL, accessToken, channelName, { connected, received, disconnected, reconnected }) => {
+  const params = channelName.split('&');
+
+  // @ts-expect-error
+  channelName = params.shift();
+
+  if (streamingAPIBaseURL.startsWith('ws')) {
+    // @ts-expect-error
+    const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?${params.join('&')}`, accessToken);
+
+    // @ts-expect-error
+    ws.onopen = connected;
+    ws.onmessage = e => received(JSON.parse(e.data));
+    // @ts-expect-error
+    ws.onclose = disconnected;
+    // @ts-expect-error
+    ws.onreconnect = reconnected;
+
+    return ws;
+  }
+
+  channelName = channelName.replace(/:/g, '/');
+
+  if (channelName.endsWith(':media')) {
+    channelName = channelName.replace('/media', '');
+    params.push('only_media=true');
+  }
+
+  params.push(`access_token=${accessToken}`);
+
+  const es = new EventSource(`${streamingAPIBaseURL}/api/v1/streaming/${channelName}?${params.join('&')}`);
+
+  es.onopen = () => {
+    connected();
+  };
+
+  KNOWN_EVENT_TYPES.forEach(type => {
+    es.addEventListener(type, e => handleEventSourceMessage(/** @type {MessageEvent} */(e), received));
+  });
+
+  es.onerror = /** @type {function(): void} */ (disconnected);
+
+  return es;
+};
diff --git a/app/javascript/flavours/blobfox/styles/_mixins.scss b/app/javascript/flavours/blobfox/styles/_mixins.scss
new file mode 100644
index 00000000000000..6643cd1aa515da
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/_mixins.scss
@@ -0,0 +1,97 @@
+@mixin avatar-radius() {
+  border-radius: $ui-avatar-border-size;
+  background-position: 50%;
+  background-clip: padding-box;
+}
+
+@mixin avatar-size($size: 48px) {
+  width: $size;
+  height: $size;
+  background-size: $size $size;
+}
+
+@mixin single-column($media, $parent: '&') {
+  .auto-columns #{$parent} {
+    @media #{$media} {
+      @content;
+    }
+  }
+  .single-column #{$parent} {
+    @content;
+  }
+}
+
+@mixin limited-single-column($media, $parent: '&') {
+  .auto-columns #{$parent},
+  .single-column #{$parent} {
+    @media #{$media} {
+      @content;
+    }
+  }
+}
+
+@mixin multi-columns($media, $parent: '&') {
+  .auto-columns #{$parent} {
+    @media #{$media} {
+      @content;
+    }
+  }
+  .multi-columns #{$parent} {
+    @content;
+  }
+}
+
+@mixin fullwidth-gallery {
+  &.full-width {
+    margin-left: -14px;
+    margin-right: -14px;
+    width: inherit;
+    max-width: none;
+    border-radius: 0;
+  }
+}
+
+@mixin search-input() {
+  outline: 0;
+  box-sizing: border-box;
+  width: 100%;
+  border: 0;
+  box-shadow: none;
+  font-family: inherit;
+  background: $ui-base-color;
+  color: $darker-text-color;
+  border-radius: 4px;
+  font-size: 14px;
+  margin: 0;
+}
+
+@mixin search-popout() {
+  background: $simple-background-color;
+  border-radius: 4px;
+  padding: 10px 14px;
+  padding-bottom: 14px;
+  margin-top: 10px;
+  color: $light-text-color;
+  box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
+
+  h4 {
+    text-transform: uppercase;
+    color: $light-text-color;
+    font-size: 13px;
+    font-weight: 500;
+    margin-bottom: 10px;
+  }
+
+  li {
+    padding: 4px 0;
+  }
+
+  ul {
+    margin-bottom: 10px;
+  }
+
+  em {
+    font-weight: 500;
+    color: $inverted-text-color;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/about.scss b/app/javascript/flavours/blobfox/styles/about.scss
new file mode 100644
index 00000000000000..0f02563b48d31e
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/about.scss
@@ -0,0 +1,56 @@
+$maximum-width: 1235px;
+$fluid-breakpoint: $maximum-width + 20px;
+
+.container {
+  box-sizing: border-box;
+  max-width: $maximum-width;
+  margin: 0 auto;
+  position: relative;
+
+  @media screen and (max-width: $fluid-breakpoint) {
+    width: 100%;
+    padding: 0 10px;
+  }
+}
+
+.brand {
+  position: relative;
+  text-decoration: none;
+}
+
+.rules-list {
+  font-size: 15px;
+  line-height: 22px;
+  color: $primary-text-color;
+  counter-reset: list-counter;
+
+  li {
+    position: relative;
+    border-bottom: 1px solid lighten($ui-base-color, 8%);
+    padding: 1em 1.75em;
+    padding-inline-start: 3em;
+    font-weight: 500;
+    counter-increment: list-counter;
+
+    &::before {
+      content: counter(list-counter);
+      position: absolute;
+      inset-inline-start: 0;
+      top: 50%;
+      transform: translateY(-50%);
+      background: $highlight-text-color;
+      color: $ui-base-color;
+      border-radius: 50%;
+      width: 4ch;
+      height: 4ch;
+      font-weight: 500;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+    }
+
+    &:last-child {
+      border-bottom: 0;
+    }
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/accessibility.scss b/app/javascript/flavours/blobfox/styles/accessibility.scss
new file mode 100644
index 00000000000000..68f4f8f1e2b3fb
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/accessibility.scss
@@ -0,0 +1,55 @@
+$emojis-requiring-inversion: 'back' 'copyright' 'curly_loop' 'currency_exchange'
+  'end' 'heavy_check_mark' 'heavy_division_sign' 'heavy_dollar_sign'
+  'heavy_minus_sign' 'heavy_multiplication_x' 'heavy_plus_sign' 'on'
+  'registered' 'soon' 'spider' 'telephone_receiver' 'tm' 'top' 'wavy_dash' !default;
+
+%emoji-color-inversion {
+  filter: invert(1);
+}
+
+.emojione {
+  @each $emoji in $emojis-requiring-inversion {
+    &[title=':#{$emoji}:'] {
+      @extend %emoji-color-inversion;
+    }
+  }
+}
+
+// Display a checkmark on active UI elements otherwise differing only by color
+.status__action-bar-button,
+.detailed-status__button .icon-button {
+  position: relative;
+
+  &.active::after {
+    position: absolute;
+    content: '\F00C';
+    font-size: 50%;
+    inset-inline-end: -0.55em;
+    top: -0.44em;
+
+    /* stylelint-disable-next-line font-family-no-missing-generic-family-keyword -- this is an icon font, this can't use a generic font */
+    font-family: FontAwesome;
+  }
+}
+
+.hicolor-privacy-icons {
+  .status__visibility-icon.fa-globe,
+  .privacy-dropdown__option .fa-globe {
+    color: #1976d2;
+  }
+
+  .status__visibility-icon.fa-unlock,
+  .privacy-dropdown__option .fa-unlock {
+    color: #388e3c;
+  }
+
+  .status__visibility-icon.fa-lock,
+  .privacy-dropdown__option .fa-lock {
+    color: #ffa000;
+  }
+
+  .status__visibility-icon.fa-envelope,
+  .privacy-dropdown__option .fa-envelope {
+    color: #d32f2f;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/accounts.scss b/app/javascript/flavours/blobfox/styles/accounts.scss
new file mode 100644
index 00000000000000..2bc0150ef45a90
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/accounts.scss
@@ -0,0 +1,381 @@
+.card {
+  & > a {
+    display: block;
+    text-decoration: none;
+    color: inherit;
+    overflow: hidden;
+    border-radius: 4px;
+
+    &:hover,
+    &:active,
+    &:focus {
+      .card__bar {
+        background: lighten($ui-base-color, 8%);
+      }
+    }
+  }
+
+  &__img {
+    height: 130px;
+    position: relative;
+    background: darken($ui-base-color, 12%);
+
+    img {
+      display: block;
+      width: 100%;
+      height: 100%;
+      margin: 0;
+      object-fit: cover;
+    }
+
+    @media screen and (width <= 600px) {
+      height: 200px;
+    }
+  }
+
+  &__bar {
+    position: relative;
+    padding: 15px;
+    display: flex;
+    justify-content: flex-start;
+    align-items: center;
+    background: lighten($ui-base-color, 4%);
+
+    .avatar {
+      flex: 0 0 auto;
+      width: 48px;
+      height: 48px;
+      @include avatar-size(48px);
+
+      padding-top: 2px;
+
+      img {
+        width: 100%;
+        height: 100%;
+        display: block;
+        margin: 0;
+        border-radius: 4px;
+        @include avatar-radius;
+
+        background: darken($ui-base-color, 8%);
+        object-fit: cover;
+      }
+    }
+
+    .display-name {
+      margin-inline-start: 15px;
+      text-align: start;
+
+      i[data-hidden] {
+        display: none;
+      }
+
+      strong {
+        font-size: 15px;
+        color: $primary-text-color;
+        font-weight: 500;
+        overflow: hidden;
+        text-overflow: ellipsis;
+      }
+
+      span {
+        display: block;
+        font-size: 14px;
+        color: $darker-text-color;
+        font-weight: 400;
+        overflow: hidden;
+        text-overflow: ellipsis;
+      }
+    }
+  }
+}
+
+.pagination {
+  padding: 30px 0;
+  text-align: center;
+  overflow: hidden;
+
+  a,
+  .current,
+  .newer,
+  .older,
+  .page,
+  .gap {
+    font-size: 14px;
+    color: $primary-text-color;
+    font-weight: 500;
+    display: inline-block;
+    padding: 6px 10px;
+    text-decoration: none;
+  }
+
+  .current {
+    background: $simple-background-color;
+    border-radius: 100px;
+    color: $inverted-text-color;
+    cursor: default;
+    margin: 0 10px;
+  }
+
+  .gap {
+    cursor: default;
+  }
+
+  .older,
+  .newer {
+    text-transform: uppercase;
+    color: $secondary-text-color;
+  }
+
+  .older {
+    float: left;
+    padding-inline-start: 0;
+
+    .fa {
+      display: inline-block;
+      margin-inline-end: 5px;
+    }
+  }
+
+  .newer {
+    float: right;
+    padding-inline-start: 0;
+
+    .fa {
+      display: inline-block;
+      margin-inline-start: 5px;
+    }
+  }
+
+  .disabled {
+    cursor: default;
+    color: lighten($inverted-text-color, 10%);
+  }
+
+  @media screen and (width <= 700px) {
+    padding: 30px 20px;
+
+    .page {
+      display: none;
+    }
+
+    .newer,
+    .older {
+      display: inline-block;
+    }
+  }
+}
+
+.nothing-here {
+  background: $ui-base-color;
+  box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
+  color: $light-text-color;
+  font-size: 14px;
+  font-weight: 500;
+  text-align: center;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  cursor: default;
+  border-radius: 4px;
+  padding: 20px;
+  min-height: 30vh;
+
+  &--under-tabs {
+    border-radius: 0 0 4px 4px;
+  }
+
+  &--flexible {
+    box-sizing: border-box;
+    min-height: 100%;
+  }
+}
+
+.account-role,
+.information-badge,
+.simple_form .overridden,
+.simple_form .recommended,
+.simple_form .not_recommended,
+.simple_form .blobfox_only {
+  display: inline-block;
+  padding: 4px 6px;
+  cursor: default;
+  border-radius: 3px;
+  font-size: 12px;
+  line-height: 12px;
+  font-weight: 500;
+  color: $ui-secondary-color;
+  background-color: var(--user-role-background, rgba($ui-secondary-color, 0.1));
+  border: 1px solid var(--user-role-border, rgba($ui-secondary-color, 0.5));
+
+  &.moderator {
+    color: $success-green;
+    background-color: rgba($success-green, 0.1);
+    border-color: rgba($success-green, 0.5);
+  }
+
+  &.admin {
+    color: lighten($error-red, 12%);
+    background-color: rgba(lighten($error-red, 12%), 0.1);
+    border-color: rgba(lighten($error-red, 12%), 0.5);
+  }
+}
+
+.simple_form .not_recommended {
+  color: lighten($error-red, 12%);
+  background-color: rgba(lighten($error-red, 12%), 0.1);
+  border-color: rgba(lighten($error-red, 12%), 0.5);
+}
+
+.simple_form .blobfox_only {
+  color: lighten($warning-red, 12%);
+  background-color: rgba(lighten($warning-red, 12%), 0.1);
+  border-color: rgba(lighten($warning-red, 12%), 0.5);
+}
+
+.account__header__fields {
+  max-width: 100vw;
+  padding: 0;
+  margin: 15px -15px -15px;
+  border: 0 none;
+  border-top: 1px solid lighten($ui-base-color, 12%);
+  border-bottom: 1px solid lighten($ui-base-color, 12%);
+  font-size: 14px;
+  line-height: 20px;
+
+  dl {
+    display: flex;
+    border-bottom: 1px solid lighten($ui-base-color, 12%);
+  }
+
+  dt,
+  dd {
+    box-sizing: border-box;
+    padding: 14px;
+    text-align: center;
+    max-height: 48px;
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+  }
+
+  dt {
+    font-weight: 500;
+    width: 120px;
+    flex: 0 0 auto;
+    color: $secondary-text-color;
+    background: rgba(darken($ui-base-color, 8%), 0.5);
+  }
+
+  dd {
+    flex: 1 1 auto;
+    color: $darker-text-color;
+  }
+
+  a {
+    color: $highlight-text-color;
+    text-decoration: none;
+
+    &:hover,
+    &:focus,
+    &:active {
+      text-decoration: underline;
+    }
+  }
+
+  .verified {
+    border: 1px solid rgba($valid-value-color, 0.5);
+    background: rgba($valid-value-color, 0.25);
+
+    a {
+      color: $valid-value-color;
+      font-weight: 500;
+    }
+
+    &__mark {
+      color: $valid-value-color;
+    }
+  }
+
+  dl:last-child {
+    border-bottom: 0;
+  }
+}
+
+.directory__tag .trends__item__current {
+  width: auto;
+}
+
+.pending-account {
+  &__header {
+    color: $darker-text-color;
+
+    a {
+      color: $ui-secondary-color;
+      text-decoration: none;
+
+      &:hover,
+      &:active,
+      &:focus {
+        text-decoration: underline;
+      }
+    }
+
+    strong {
+      color: $primary-text-color;
+      font-weight: 700;
+    }
+  }
+
+  &__body {
+    margin-top: 10px;
+  }
+}
+
+.batch-table__row--muted {
+  color: lighten($ui-base-color, 26%);
+}
+
+.batch-table__row--muted .pending-account__header,
+.batch-table__row--muted .accounts-table,
+.batch-table__row--muted .name-tag {
+  &,
+  a,
+  strong {
+    color: lighten($ui-base-color, 26%);
+  }
+}
+
+.batch-table__row--muted .name-tag .avatar {
+  opacity: 0.5;
+}
+
+.batch-table__row--muted .accounts-table {
+  tbody td.accounts-table__extra,
+  &__count,
+  &__count small {
+    color: lighten($ui-base-color, 26%);
+  }
+}
+
+.batch-table__row--attention {
+  color: $gold-star;
+}
+
+.batch-table__row--attention .pending-account__header,
+.batch-table__row--attention .accounts-table,
+.batch-table__row--attention .name-tag {
+  &,
+  a,
+  strong {
+    color: $gold-star;
+  }
+}
+
+.batch-table__row--attention .accounts-table {
+  tbody td.accounts-table__extra,
+  &__count,
+  &__count small {
+    color: $gold-star;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/admin.scss b/app/javascript/flavours/blobfox/styles/admin.scss
new file mode 100644
index 00000000000000..2f4027b03fb6ea
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/admin.scss
@@ -0,0 +1,1884 @@
+@use 'sass:math';
+
+$no-columns-breakpoint: 600px;
+$sidebar-width: 240px;
+$content-width: 840px;
+
+.admin-wrapper {
+  display: flex;
+  justify-content: center;
+  width: 100%;
+  min-height: 100vh;
+
+  .sidebar-wrapper {
+    min-height: 100vh;
+    overflow: hidden;
+    pointer-events: none;
+    flex: 1 1 auto;
+
+    &__inner {
+      display: flex;
+      justify-content: flex-end;
+      background: $ui-base-color;
+      height: 100%;
+    }
+  }
+
+  .sidebar {
+    width: $sidebar-width;
+    padding: 0;
+    pointer-events: auto;
+
+    &__toggle {
+      display: none;
+      background: darken($ui-base-color, 4%);
+      border-bottom: 1px solid lighten($ui-base-color, 4%);
+      align-items: center;
+
+      &__logo {
+        flex: 1 1 auto;
+
+        a {
+          display: block;
+          padding: 15px;
+        }
+      }
+
+      &__icon {
+        display: block;
+        color: $darker-text-color;
+        text-decoration: none;
+        flex: 0 0 auto;
+        font-size: 18px;
+        padding: 10px;
+        margin: 5px 10px;
+        border-radius: 4px;
+
+        &:focus {
+          background: $ui-base-color;
+        }
+
+        .fa-times {
+          display: none;
+        }
+
+        &.active {
+          .fa-times {
+            display: block;
+          }
+
+          .fa-bars {
+            display: none;
+          }
+        }
+      }
+    }
+
+    .logo {
+      display: block;
+      margin: 40px auto;
+      width: 100px;
+      height: 100px;
+    }
+
+    .logo--wordmark {
+      display: inherit;
+      margin: inherit;
+      width: inherit;
+      height: 25px;
+    }
+
+    @media screen and (max-width: $no-columns-breakpoint) {
+      & > a:first-child {
+        display: none;
+      }
+    }
+
+    ul {
+      list-style: none;
+      border-radius: 4px 0 0 4px;
+      overflow: hidden;
+      margin-bottom: 20px;
+
+      @media screen and (max-width: $no-columns-breakpoint) {
+        margin-bottom: 0;
+      }
+
+      a {
+        display: block;
+        padding: 15px;
+        color: $darker-text-color;
+        text-decoration: none;
+        transition: all 200ms linear;
+        transition-property: color, background-color;
+        border-radius: 4px 0 0 4px;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+
+        i.fa {
+          margin-inline-end: 5px;
+        }
+
+        &:hover {
+          color: $primary-text-color;
+          background-color: darken($ui-base-color, 5%);
+          transition: all 100ms linear;
+          transition-property: color, background-color;
+        }
+
+        &.selected {
+          border-radius: 4px 0 0;
+        }
+      }
+
+      ul {
+        background: darken($ui-base-color, 4%);
+        border-radius: 0 0 0 4px;
+        margin: 0;
+
+        a {
+          border: 0;
+          padding: 15px 35px;
+        }
+      }
+
+      .warning a {
+        color: $gold-star;
+        font-weight: 700;
+      }
+
+      .simple-navigation-active-leaf a {
+        color: $primary-text-color;
+        background-color: $ui-highlight-color;
+        border-bottom: 0;
+        border-radius: 0;
+      }
+    }
+
+    & > ul > .simple-navigation-active-leaf a {
+      border-radius: 4px 0 0 4px;
+    }
+  }
+
+  .content-wrapper {
+    box-sizing: border-box;
+    width: 100%;
+    max-width: $content-width;
+    flex: 1 1 auto;
+  }
+
+  @media screen and (max-width: $content-width + $sidebar-width) {
+    .sidebar-wrapper--empty {
+      display: none;
+    }
+
+    .sidebar-wrapper {
+      width: $sidebar-width;
+      flex: 0 0 auto;
+    }
+  }
+
+  @media screen and (max-width: $no-columns-breakpoint) {
+    .sidebar-wrapper {
+      width: 100%;
+    }
+  }
+
+  .content {
+    padding-top: 55px;
+    padding-bottom: 20px;
+    padding-inline-start: 25px;
+    padding-inline-end: 15px;
+
+    @media screen and (max-width: $no-columns-breakpoint) {
+      max-width: none;
+      padding: 15px;
+      padding-top: 30px;
+    }
+
+    &__heading {
+      margin-bottom: 45px;
+
+      &__row {
+        display: flex;
+        flex-wrap: wrap;
+        align-items: center;
+        justify-content: space-between;
+        margin-top: -15px;
+        margin-inline-end: -15px;
+
+        & > * {
+          margin-top: 15px;
+          margin-inline-end: 15px;
+        }
+      }
+
+      &__tabs {
+        margin-top: 30px;
+        width: 100%;
+
+        & > div {
+          display: flex;
+          flex-wrap: wrap;
+          gap: 5px;
+        }
+
+        a {
+          font-size: 14px;
+          display: inline-flex;
+          align-items: center;
+          padding: 7px 10px;
+          border-radius: 4px;
+          color: $darker-text-color;
+          text-decoration: none;
+          font-weight: 500;
+          gap: 5px;
+          white-space: nowrap;
+
+          &:hover,
+          &:focus,
+          &:active {
+            background: lighten($ui-base-color, 4%);
+          }
+
+          &.selected {
+            font-weight: 700;
+            color: $primary-text-color;
+            background: $ui-highlight-color;
+          }
+        }
+      }
+
+      &__actions {
+        display: inline-flex;
+        flex-flow: wrap;
+        gap: 5px;
+      }
+
+      h2 small {
+        font-size: 12px;
+        display: block;
+        font-weight: 500;
+        color: $darker-text-color;
+        line-height: 18px;
+      }
+
+      @media screen and (max-width: $no-columns-breakpoint) {
+        border-bottom: 0;
+        padding-bottom: 0;
+      }
+    }
+
+    h2 {
+      color: $secondary-text-color;
+      font-size: 24px;
+      line-height: 36px;
+      font-weight: 700;
+    }
+
+    h3 {
+      color: $secondary-text-color;
+      font-size: 20px;
+      line-height: 28px;
+      font-weight: 400;
+      margin-bottom: 30px;
+    }
+
+    h4 {
+      text-transform: uppercase;
+      font-size: 13px;
+      font-weight: 700;
+      color: $darker-text-color;
+      padding-bottom: 8px;
+      margin-bottom: 8px;
+      border-bottom: 1px solid lighten($ui-base-color, 8%);
+    }
+
+    h6 {
+      font-size: 16px;
+      color: $secondary-text-color;
+      line-height: 28px;
+      font-weight: 500;
+    }
+
+    .fields-group h6 {
+      color: $primary-text-color;
+      font-weight: 500;
+    }
+
+    .directory__tag > a,
+    .directory__tag > div {
+      box-shadow: none;
+    }
+
+    .directory__tag .table-action-link .fa {
+      color: inherit;
+    }
+
+    .directory__tag h4 {
+      font-size: 18px;
+      font-weight: 700;
+      color: $primary-text-color;
+      text-transform: none;
+      padding-bottom: 0;
+      margin-bottom: 0;
+      border-bottom: 0;
+    }
+
+    & > p {
+      font-size: 14px;
+      line-height: 21px;
+      color: $secondary-text-color;
+      margin-bottom: 20px;
+
+      strong {
+        color: $primary-text-color;
+        font-weight: 500;
+
+        @each $lang in $cjk-langs {
+          &:lang(#{$lang}) {
+            font-weight: 700;
+          }
+        }
+      }
+    }
+
+    hr {
+      width: 100%;
+      height: 0;
+      border: 0;
+      border-bottom: 1px solid rgba($ui-base-lighter-color, 0.6);
+      margin: 20px 0;
+
+      &.spacer {
+        height: 1px;
+        border: 0;
+      }
+    }
+  }
+
+  @media screen and (max-width: $no-columns-breakpoint) {
+    display: block;
+
+    .sidebar-wrapper {
+      min-height: 0;
+    }
+
+    .sidebar {
+      width: 100%;
+      padding: 0;
+      height: auto;
+
+      &__toggle {
+        display: flex;
+      }
+
+      & > ul {
+        display: none;
+
+        &.visible {
+          display: block;
+          position: fixed;
+          z-index: 10;
+          width: 100%;
+          height: calc(100% - 56px);
+          inset-inline-start: 0;
+          bottom: 0;
+          overflow-y: auto;
+          background: $ui-base-color;
+        }
+      }
+
+      ul a,
+      ul ul a {
+        border-radius: 0;
+        border-bottom: 1px solid lighten($ui-base-color, 4%);
+        transition: none;
+
+        &:hover {
+          transition: none;
+        }
+      }
+
+      ul ul {
+        border-radius: 0;
+      }
+
+      ul .simple-navigation-active-leaf a {
+        border-bottom-color: $ui-highlight-color;
+      }
+    }
+  }
+}
+
+hr.spacer {
+  width: 100%;
+  border: 0;
+  margin: 20px 0;
+  height: 1px;
+}
+
+body,
+.admin-wrapper .content {
+  .muted-hint {
+    color: $darker-text-color;
+
+    a {
+      color: $highlight-text-color;
+    }
+  }
+
+  .positive-hint,
+  .negative-hint,
+  .neutral-hint {
+    a {
+      color: inherit;
+      text-decoration: underline;
+
+      &:focus,
+      &:hover,
+      &:active {
+        text-decoration: none;
+      }
+    }
+  }
+
+  .positive-hint {
+    color: $valid-value-color;
+    font-weight: 500;
+  }
+
+  .negative-hint {
+    color: $error-value-color;
+    font-weight: 500;
+  }
+
+  .neutral-hint {
+    color: $dark-text-color;
+    font-weight: 500;
+  }
+
+  .warning-hint {
+    color: $gold-star;
+    font-weight: 500;
+  }
+}
+
+.filters {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 40px;
+
+  .filter-subset {
+    flex: 0 0 auto;
+    margin-bottom: 20px;
+
+    &:last-child {
+      margin-bottom: 30px;
+    }
+
+    ul {
+      margin-top: 5px;
+      list-style: none;
+
+      li {
+        display: inline-block;
+        margin-inline-end: 5px;
+      }
+    }
+
+    & > div {
+      display: flex;
+      gap: 5px;
+    }
+
+    strong {
+      font-weight: 500;
+      text-transform: uppercase;
+      font-size: 12px;
+
+      @each $lang in $cjk-langs {
+        &:lang(#{$lang}) {
+          font-weight: 700;
+        }
+      }
+    }
+
+    &--with-select strong {
+      display: block;
+      margin-bottom: 10px;
+    }
+
+    a {
+      display: inline-block;
+      color: $darker-text-color;
+      text-decoration: none;
+      text-transform: uppercase;
+      font-size: 12px;
+      font-weight: 500;
+      border-bottom: 2px solid $ui-base-color;
+
+      &:hover {
+        color: $primary-text-color;
+        border-bottom: 2px solid lighten($ui-base-color, 5%);
+      }
+
+      &.selected {
+        color: $highlight-text-color;
+        border-bottom: 2px solid $ui-highlight-color;
+      }
+    }
+  }
+}
+
+.flavour-screen {
+  display: block;
+  margin: 10px auto;
+  max-width: 100%;
+}
+
+.flavour-description {
+  display: block;
+  font-size: 16px;
+  margin: 10px 0;
+
+  & > p {
+    margin: 10px 0;
+  }
+}
+
+.report-accounts {
+  display: flex;
+  flex-wrap: wrap;
+  margin-bottom: 20px;
+}
+
+.report-accounts__item {
+  display: flex;
+  flex: 250px;
+  flex-direction: column;
+  margin: 0 5px;
+
+  & > strong {
+    display: block;
+    margin-top: 0;
+    margin-bottom: 10px;
+    margin-inline-end: 0;
+    margin-inline-start: -5px;
+    font-weight: 500;
+    font-size: 14px;
+    line-height: 18px;
+    color: $secondary-text-color;
+
+    @each $lang in $cjk-langs {
+      &:lang(#{$lang}) {
+        font-weight: 700;
+      }
+    }
+  }
+
+  .account-card {
+    flex: 1 1 auto;
+  }
+}
+
+.report-status,
+.account-status {
+  display: flex;
+  margin-bottom: 10px;
+
+  .activity-stream {
+    flex: 2 0 0;
+    margin-inline-end: 20px;
+    max-width: calc(100% - 60px);
+
+    .entry {
+      border-radius: 4px;
+    }
+  }
+}
+
+.report-status__actions,
+.account-status__actions {
+  flex: 0 0 auto;
+  display: flex;
+  flex-direction: column;
+
+  .icon-button {
+    font-size: 24px;
+    width: 24px;
+    text-align: center;
+    margin-bottom: 10px;
+  }
+}
+
+.simple_form.new_report_note,
+.simple_form.new_account_moderation_note {
+  max-width: 100%;
+}
+
+.simple_form {
+  .actions {
+    margin-top: 15px;
+  }
+
+  .button {
+    font-size: 15px;
+  }
+}
+
+.batch-form-box {
+  display: flex;
+  flex-wrap: wrap;
+  margin-bottom: 5px;
+
+  #form_status_batch_action {
+    margin-bottom: 5px;
+    margin-inline-end: 5px;
+    font-size: 14px;
+  }
+
+  input.button {
+    margin-bottom: 5px;
+    margin-inline-end: 5px;
+  }
+
+  .media-spoiler-toggle-buttons {
+    margin-inline-start: auto;
+
+    .button {
+      overflow: visible;
+      margin-bottom: 5px;
+      margin-inline-start: 5px;
+      float: right;
+    }
+  }
+}
+
+.back-link {
+  margin-bottom: 10px;
+  font-size: 14px;
+
+  a {
+    color: $highlight-text-color;
+    text-decoration: none;
+
+    &:hover {
+      text-decoration: underline;
+    }
+  }
+}
+
+.special-action-button,
+.back-link {
+  text-align: end;
+  flex: 1 1 auto;
+}
+
+.action-buttons {
+  display: flex;
+  overflow: hidden;
+  justify-content: space-between;
+}
+
+.spacer {
+  flex: 1 1 auto;
+}
+
+.log-entry {
+  display: block;
+  line-height: 20px;
+  padding: 15px;
+  padding-inline-start: 15px * 2 + 40px;
+  background: $ui-base-color;
+  border-bottom: 1px solid darken($ui-base-color, 8%);
+  position: relative;
+  text-decoration: none;
+  color: $darker-text-color;
+  font-size: 14px;
+
+  &:first-child {
+    border-top-left-radius: 4px;
+    border-top-right-radius: 4px;
+  }
+
+  &:last-child {
+    border-bottom-left-radius: 4px;
+    border-bottom-right-radius: 4px;
+    border-bottom: 0;
+  }
+
+  &:hover,
+  &:focus,
+  &:active {
+    background: lighten($ui-base-color, 4%);
+  }
+
+  &__avatar {
+    position: absolute;
+    inset-inline-start: 15px;
+    top: 15px;
+
+    .avatar {
+      border-radius: 4px;
+      width: 40px;
+      height: 40px;
+    }
+  }
+
+  &__title {
+    word-wrap: break-word;
+  }
+
+  &__timestamp {
+    color: $dark-text-color;
+  }
+
+  a,
+  .username,
+  .target {
+    color: $secondary-text-color;
+    text-decoration: none;
+    font-weight: 500;
+  }
+
+  a {
+    &:hover,
+    &:focus,
+    &:active {
+      text-decoration: underline;
+    }
+  }
+}
+
+a.name-tag,
+.name-tag,
+a.inline-name-tag,
+.inline-name-tag {
+  text-decoration: none;
+  color: $secondary-text-color;
+
+  .username {
+    font-weight: 500;
+  }
+
+  &.suspended {
+    .username {
+      text-decoration: line-through;
+      color: lighten($error-red, 12%);
+    }
+
+    .avatar {
+      filter: grayscale(100%);
+      opacity: 0.8;
+    }
+  }
+}
+
+a.name-tag,
+.name-tag {
+  display: inline-flex;
+  align-items: center;
+  vertical-align: top;
+
+  .avatar {
+    display: block;
+    margin: 0;
+    margin-inline-end: 5px;
+    border-radius: 50%;
+  }
+
+  &.suspended {
+    .avatar {
+      filter: grayscale(100%);
+      opacity: 0.8;
+    }
+  }
+}
+
+.speech-bubble {
+  margin-bottom: 20px;
+  border-inline-start: 4px solid $ui-highlight-color;
+
+  &.positive {
+    border-left-color: $success-green;
+  }
+
+  &.negative {
+    border-left-color: lighten($error-red, 12%);
+  }
+
+  &.warning {
+    border-left-color: $gold-star;
+  }
+
+  &__bubble {
+    padding: 16px;
+    padding-inline-start: 14px;
+    font-size: 15px;
+    line-height: 20px;
+    border-radius: 4px 4px 4px 0;
+    position: relative;
+    font-weight: 500;
+
+    a {
+      color: $darker-text-color;
+    }
+  }
+
+  &__owner {
+    padding: 8px;
+    padding-inline-start: 12px;
+  }
+
+  time {
+    color: $dark-text-color;
+  }
+}
+
+.report-card {
+  background: $ui-base-color;
+  border-radius: 4px;
+  margin-bottom: 20px;
+
+  &__profile {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 15px;
+
+    .account {
+      padding: 0;
+      border: 0;
+
+      &__avatar-wrapper {
+        margin-inline-start: 0;
+      }
+    }
+
+    &__stats {
+      flex: 0 0 auto;
+      font-weight: 500;
+      color: $darker-text-color;
+      text-transform: uppercase;
+      text-align: end;
+
+      a {
+        color: inherit;
+        text-decoration: none;
+
+        &:focus,
+        &:hover,
+        &:active {
+          color: lighten($darker-text-color, 8%);
+        }
+      }
+
+      .red {
+        color: $error-value-color;
+      }
+    }
+  }
+
+  &__summary {
+    &__item {
+      display: flex;
+      justify-content: flex-start;
+      border-top: 1px solid darken($ui-base-color, 4%);
+
+      &:hover {
+        background: lighten($ui-base-color, 2%);
+      }
+
+      &__reported-by,
+      &__assigned {
+        padding: 15px;
+        flex: 0 0 auto;
+        box-sizing: border-box;
+        width: 150px;
+        color: $darker-text-color;
+
+        &,
+        .username {
+          white-space: nowrap;
+          overflow: hidden;
+          text-overflow: ellipsis;
+        }
+      }
+
+      &__content {
+        flex: 1 1 auto;
+        max-width: calc(100% - 300px);
+
+        &__icon {
+          color: $dark-text-color;
+          margin-inline-end: 4px;
+          font-weight: 500;
+        }
+      }
+
+      &__content a {
+        display: block;
+        box-sizing: border-box;
+        width: 100%;
+        padding: 15px;
+        text-decoration: none;
+        color: $darker-text-color;
+      }
+    }
+  }
+}
+
+.one-line {
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.ellipsized-ip {
+  display: inline-block;
+  max-width: 120px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  vertical-align: middle;
+}
+
+.admin-account-bio {
+  display: flex;
+  flex-wrap: wrap;
+  margin: 0 -5px;
+  margin-top: 20px;
+
+  > div {
+    box-sizing: border-box;
+    padding: 0 5px;
+    margin-bottom: 10px;
+    flex: 1 0 50%;
+    max-width: 100%;
+  }
+
+  .account__header__fields,
+  .account__header__content {
+    background: lighten($ui-base-color, 8%);
+    border-radius: 4px;
+    height: 100%;
+  }
+
+  .account__header__fields {
+    margin: 0;
+    border: 0;
+
+    a {
+      color: $highlight-text-color;
+    }
+
+    dl:first-child .verified {
+      border-radius: 0 4px 0 0;
+    }
+
+    .verified a {
+      color: $valid-value-color;
+    }
+  }
+
+  .account__header__content {
+    box-sizing: border-box;
+    padding: 20px;
+    color: $primary-text-color;
+  }
+}
+
+.center-text {
+  text-align: center;
+}
+
+.applications-list__item,
+.filters-list__item {
+  padding: 15px 0;
+  background: $ui-base-color;
+  border: 1px solid lighten($ui-base-color, 4%);
+  border-radius: 4px;
+  margin-top: 15px;
+}
+
+.user-role {
+  color: var(--user-role-accent);
+}
+
+.announcements-list,
+.filters-list {
+  border: 1px solid lighten($ui-base-color, 4%);
+  border-radius: 4px;
+
+  &__item {
+    padding: 15px 0;
+    background: $ui-base-color;
+    border-bottom: 1px solid lighten($ui-base-color, 4%);
+
+    &__title {
+      padding: 0 15px;
+      display: block;
+      font-weight: 500;
+      font-size: 18px;
+      line-height: 1.5;
+      color: $secondary-text-color;
+      text-decoration: none;
+      margin-bottom: 10px;
+
+      .account-role {
+        vertical-align: middle;
+      }
+    }
+
+    a.announcements-list__item__title {
+      &:hover,
+      &:focus,
+      &:active {
+        color: $primary-text-color;
+      }
+    }
+
+    &__meta {
+      padding: 0 15px;
+      color: $dark-text-color;
+
+      a {
+        color: inherit;
+        text-decoration: underline;
+
+        &:hover,
+        &:focus,
+        &:active {
+          text-decoration: none;
+        }
+      }
+    }
+
+    &__action-bar {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+    }
+
+    &__permissions {
+      margin-top: 10px;
+    }
+
+    &:last-child {
+      border-bottom: 0;
+    }
+  }
+}
+
+.filters-list__item {
+  &__title {
+    display: flex;
+    justify-content: space-between;
+    margin-bottom: 0;
+  }
+
+  &__permissions {
+    margin-top: 0;
+    margin-bottom: 10px;
+  }
+
+  .expiration {
+    font-size: 13px;
+  }
+
+  &.expired {
+    .expiration {
+      color: lighten($error-red, 12%);
+    }
+
+    .permissions-list__item__icon {
+      color: $dark-text-color;
+    }
+  }
+}
+
+.dashboard__counters.admin-account-counters {
+  margin-top: 10px;
+}
+
+.account-badges {
+  margin: -2px 0;
+}
+
+.retention {
+  overflow: auto;
+
+  > h4 {
+    position: sticky;
+    inset-inline-start: 0;
+  }
+
+  &__table {
+    &__number {
+      color: $secondary-text-color;
+      padding: 10px;
+    }
+
+    &__date {
+      white-space: nowrap;
+      padding: 10px 0;
+      text-align: start;
+      min-width: 120px;
+
+      &.retention__table__average {
+        font-weight: 700;
+      }
+    }
+
+    &__size {
+      text-align: center;
+      padding: 10px;
+    }
+
+    &__label {
+      font-weight: 700;
+      color: $darker-text-color;
+    }
+
+    &__box {
+      box-sizing: border-box;
+      background: $ui-highlight-color;
+      padding: 10px;
+      font-weight: 500;
+      color: $primary-text-color;
+      width: 52px;
+      margin: 1px;
+
+      @for $i from 0 through 10 {
+        &--#{10 * $i} {
+          background-color: rgba(
+            $ui-highlight-color,
+            1 * (math.div(max(1, $i), 10))
+          );
+        }
+      }
+    }
+  }
+}
+
+.sparkline {
+  display: block;
+  text-decoration: none;
+  background: lighten($ui-base-color, 4%);
+  border-radius: 4px;
+  padding: 0;
+  position: relative;
+  padding-bottom: 55px + 20px;
+  overflow: hidden;
+
+  &__value {
+    display: flex;
+    line-height: 33px;
+    align-items: flex-end;
+    padding: 20px;
+    padding-bottom: 10px;
+
+    &__total {
+      display: block;
+      margin-inline-end: 10px;
+      font-weight: 500;
+      font-size: 28px;
+      color: $primary-text-color;
+    }
+
+    &__change {
+      display: block;
+      font-weight: 500;
+      font-size: 18px;
+      color: $darker-text-color;
+      margin-bottom: -3px;
+
+      &.positive {
+        color: $valid-value-color;
+      }
+
+      &.negative {
+        color: $error-value-color;
+      }
+    }
+  }
+
+  &__label {
+    padding: 0 20px;
+    padding-bottom: 10px;
+    text-transform: uppercase;
+    color: $darker-text-color;
+    font-weight: 500;
+  }
+
+  &__graph {
+    position: absolute;
+    bottom: 0;
+    width: 100%;
+
+    svg {
+      display: block;
+      margin: 0;
+    }
+
+    path:first-child {
+      fill: rgba($highlight-text-color, 0.25) !important;
+      fill-opacity: 1 !important;
+    }
+
+    path:last-child {
+      stroke: lighten($highlight-text-color, 6%) !important;
+      fill: none !important;
+    }
+  }
+}
+
+a.sparkline {
+  &:hover,
+  &:focus,
+  &:active {
+    background: lighten($ui-base-color, 6%);
+  }
+}
+
+.skeleton {
+  background-color: lighten($ui-base-color, 8%);
+  background-image: linear-gradient(
+    90deg,
+    lighten($ui-base-color, 8%),
+    lighten($ui-base-color, 12%),
+    lighten($ui-base-color, 8%)
+  );
+  background-size: 200px 100%;
+  background-repeat: no-repeat;
+  border-radius: 4px;
+  display: inline-block;
+  line-height: 1;
+  width: 100%;
+  animation: skeleton 1.2s ease-in-out infinite;
+}
+
+@keyframes skeleton {
+  0% {
+    background-position: -200px 0;
+  }
+
+  100% {
+    background-position: calc(200px + 100%) 0;
+  }
+}
+
+.dimension {
+  table {
+    width: 100%;
+  }
+
+  &__item {
+    border-bottom: 1px solid lighten($ui-base-color, 4%);
+
+    &__key {
+      font-weight: 500;
+      padding: 11px 10px;
+    }
+
+    &__value {
+      text-align: end;
+      color: $darker-text-color;
+      padding: 11px 10px;
+    }
+
+    &__indicator {
+      display: inline-block;
+      width: 8px;
+      height: 8px;
+      border-radius: 50%;
+      background: $ui-highlight-color;
+      margin-inline-end: 10px;
+
+      @for $i from 0 through 10 {
+        &--#{10 * $i} {
+          background-color: rgba(
+            $ui-highlight-color,
+            1 * (math.div(max(1, $i), 10))
+          );
+        }
+      }
+    }
+
+    &:last-child {
+      border-bottom: 0;
+    }
+
+    &.negative {
+      color: $error-value-color;
+      font-weight: 700;
+
+      .dimension__item__value {
+        color: $error-value-color;
+      }
+    }
+  }
+}
+
+.report-reason-selector {
+  border-radius: 4px;
+  background: $ui-base-color;
+  margin-bottom: 20px;
+
+  &__category {
+    cursor: pointer;
+    border-bottom: 1px solid darken($ui-base-color, 8%);
+
+    &:last-child {
+      border-bottom: 0;
+    }
+
+    &__label {
+      padding: 15px;
+    }
+
+    &__rules {
+      margin-inline-start: 30px;
+    }
+  }
+
+  &__rule {
+    cursor: pointer;
+    padding: 15px;
+  }
+}
+
+.report-header {
+  display: grid;
+  grid-gap: 15px;
+  grid-template-columns: minmax(0, 1fr) 300px;
+
+  &__details {
+    &__item {
+      border-bottom: 1px solid lighten($ui-base-color, 8%);
+      padding: 15px 0;
+
+      &:last-child {
+        border-bottom: 0;
+      }
+
+      &__header {
+        font-weight: 600;
+        padding: 4px 0;
+      }
+    }
+
+    &--horizontal {
+      display: grid;
+      grid-auto-columns: minmax(0, 1fr);
+      grid-auto-flow: column;
+
+      .report-header__details__item {
+        border-bottom: 0;
+      }
+    }
+  }
+
+  @media screen and (width <= 930px) {
+    grid-template-columns: minmax(0, 1fr);
+  }
+}
+
+.account-card {
+  background: $ui-base-color;
+  border-radius: 4px;
+
+  &__permalink {
+    color: inherit;
+    text-decoration: none;
+  }
+
+  &__header {
+    padding: 4px;
+    border-radius: 4px;
+    height: 128px;
+
+    img {
+      display: block;
+      margin: 0;
+      width: 100%;
+      height: 100%;
+      object-fit: cover;
+      background: darken($ui-base-color, 8%);
+    }
+  }
+
+  &__title {
+    margin-top: -(15px + 8px);
+    display: flex;
+    align-items: flex-end;
+
+    &__avatar {
+      padding: 14px;
+
+      img,
+      .account__avatar {
+        display: block;
+        margin: 0;
+        width: 56px;
+        height: 56px;
+        background-color: darken($ui-base-color, 8%);
+        border-radius: 8px;
+        border: 1px solid $ui-base-color;
+      }
+    }
+
+    .display-name {
+      color: $darker-text-color;
+      padding-bottom: 15px;
+      font-size: 15px;
+      line-height: 20px;
+
+      bdi {
+        display: block;
+        color: $primary-text-color;
+        font-weight: 700;
+      }
+    }
+  }
+
+  &__bio {
+    padding: 0 15px;
+    margin: 8px 0;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    word-wrap: break-word;
+    max-height: 21px * 2;
+    position: relative;
+    font-size: 15px;
+    line-height: 21px;
+
+    &::after {
+      display: block;
+      content: '';
+      width: 50px;
+      height: 21px;
+      position: absolute;
+      bottom: 0;
+      inset-inline-end: 15px;
+      background: linear-gradient(to left, $ui-base-color, transparent);
+      pointer-events: none;
+    }
+
+    a {
+      color: $secondary-text-color;
+      text-decoration: none;
+      unicode-bidi: isolate;
+
+      &:hover {
+        text-decoration: underline;
+      }
+
+      &.mention {
+        &:hover {
+          text-decoration: none;
+
+          span {
+            text-decoration: underline;
+          }
+        }
+      }
+    }
+  }
+
+  &__actions {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+
+    &__button {
+      flex-shrink: 1;
+      padding: 0 15px;
+      overflow: hidden;
+
+      .button {
+        min-width: 0;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+        overflow: hidden;
+        max-width: 100%;
+      }
+    }
+  }
+
+  &__counters {
+    flex: 1 1 auto;
+    display: grid;
+    grid-auto-columns: minmax(0, 1fr);
+    grid-auto-flow: column;
+    max-width: 340px;
+    min-width: 65px * 3;
+
+    &__item {
+      padding: 15px 0;
+      text-align: center;
+      color: $primary-text-color;
+      font-weight: 600;
+      font-size: 15px;
+      line-height: 21px;
+
+      small {
+        display: block;
+        color: $darker-text-color;
+        font-weight: 400;
+        font-size: 13px;
+        line-height: 18px;
+      }
+    }
+  }
+}
+
+.report-notes {
+  margin-bottom: 20px;
+
+  &__item {
+    background: $ui-base-color;
+    position: relative;
+    padding: 15px;
+    padding-inline-start: 15px * 2 + 40px;
+    border-bottom: 1px solid darken($ui-base-color, 8%);
+
+    &:first-child {
+      border-top-left-radius: 4px;
+      border-top-right-radius: 4px;
+    }
+
+    &:last-child {
+      border-bottom-left-radius: 4px;
+      border-bottom-right-radius: 4px;
+      border-bottom: 0;
+    }
+
+    &:hover {
+      background-color: lighten($ui-base-color, 4%);
+    }
+
+    &__avatar {
+      position: absolute;
+      inset-inline-start: 15px;
+      top: 15px;
+      border-radius: 4px;
+      width: 40px;
+      height: 40px;
+    }
+
+    &__header {
+      color: $darker-text-color;
+      font-size: 15px;
+      line-height: 20px;
+      margin-bottom: 4px;
+
+      .username {
+        color: $primary-text-color;
+        font-weight: 500;
+        margin-inline-end: 5px;
+
+        a {
+          color: inherit;
+          text-decoration: none;
+
+          &:hover,
+          &:focus,
+          &:active {
+            text-decoration: underline;
+          }
+        }
+      }
+
+      time {
+        margin-inline-start: 5px;
+        vertical-align: baseline;
+      }
+    }
+
+    &__content {
+      font-size: 15px;
+      line-height: 20px;
+      word-wrap: break-word;
+      font-weight: 400;
+      color: $primary-text-color;
+
+      p {
+        margin-bottom: 20px;
+        white-space: pre-wrap;
+        unicode-bidi: plaintext;
+
+        &:last-child {
+          margin-bottom: 0;
+        }
+      }
+
+      a {
+        color: $highlight-text-color;
+        text-decoration: none;
+
+        &:hover {
+          text-decoration: underline;
+        }
+      }
+    }
+
+    &__actions {
+      position: absolute;
+      top: 15px;
+      inset-inline-end: 15px;
+      text-align: end;
+    }
+  }
+}
+
+.report-actions {
+  border: 1px solid darken($ui-base-color, 8%);
+
+  &__item {
+    display: flex;
+    align-items: center;
+    line-height: 18px;
+    border-bottom: 1px solid darken($ui-base-color, 8%);
+
+    &:last-child {
+      border-bottom: 0;
+    }
+
+    &__button {
+      box-sizing: border-box;
+      flex: 0 0 auto;
+      width: 200px;
+      padding: 15px;
+      padding-inline-end: 0;
+
+      .button {
+        display: block;
+        width: 100%;
+      }
+    }
+
+    &__description {
+      padding: 15px;
+      font-size: 14px;
+      color: $dark-text-color;
+    }
+  }
+
+  @media screen and (width <= 800px) {
+    border: 0;
+
+    &__item {
+      flex-direction: column;
+      border: 0;
+
+      &__button {
+        width: 100%;
+        padding: 15px 0;
+      }
+
+      &__description {
+        padding: 0;
+        padding-bottom: 15px;
+      }
+    }
+  }
+}
+
+.section-skip-link {
+  float: right;
+
+  a {
+    color: $ui-highlight-color;
+    text-decoration: none;
+
+    &:hover,
+    &:focus,
+    &:active {
+      text-decoration: underline;
+    }
+  }
+}
+
+.strike-card {
+  padding: 15px;
+  border-radius: 4px;
+  background: $ui-base-color;
+  font-size: 15px;
+  line-height: 20px;
+  word-wrap: break-word;
+  font-weight: 400;
+  color: $primary-text-color;
+  box-sizing: border-box;
+  min-height: 100%;
+
+  a {
+    color: $highlight-text-color;
+    text-decoration: none;
+
+    &:hover {
+      text-decoration: underline;
+    }
+  }
+
+  p {
+    margin-bottom: 20px;
+    unicode-bidi: plaintext;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+
+    strong {
+      font-weight: 700;
+    }
+  }
+
+  &__rules {
+    list-style: disc;
+    padding-inline-start: 15px;
+    margin-bottom: 20px;
+    color: $darker-text-color;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+
+    &__text {
+      color: $primary-text-color;
+    }
+  }
+
+  &__statuses-list {
+    border-radius: 4px;
+    border: 1px solid darken($ui-base-color, 8%);
+    font-size: 13px;
+    line-height: 18px;
+    overflow: hidden;
+
+    &__item {
+      padding: 16px;
+      background: lighten($ui-base-color, 2%);
+      border-bottom: 1px solid darken($ui-base-color, 8%);
+
+      &:last-child {
+        border-bottom: 0;
+      }
+
+      &__meta {
+        color: $darker-text-color;
+      }
+
+      a {
+        color: inherit;
+        text-decoration: none;
+
+        &:hover,
+        &:focus,
+        &:active {
+          text-decoration: underline;
+        }
+      }
+    }
+  }
+}
+
+.availability-indicator {
+  display: flex;
+  align-items: center;
+  margin-bottom: 30px;
+  font-size: 14px;
+  line-height: 21px;
+
+  &__hint {
+    padding: 0 15px;
+  }
+
+  &__graphic {
+    display: flex;
+    margin: 0 -2px;
+
+    &__item {
+      display: block;
+      flex: 0 0 auto;
+      width: 4px;
+      height: 21px;
+      background: lighten($ui-base-color, 8%);
+      margin: 0 2px;
+      border-radius: 2px;
+
+      &.positive {
+        background: $valid-value-color;
+      }
+
+      &.negative {
+        background: $error-value-color;
+      }
+    }
+  }
+}
+
+.history {
+  counter-reset: step 0;
+  font-size: 15px;
+  line-height: 22px;
+
+  li {
+    counter-increment: step 1;
+    padding-inline-start: 2.5rem;
+    padding-bottom: 8px;
+    position: relative;
+    margin-bottom: 8px;
+
+    &::before {
+      position: absolute;
+      content: counter(step);
+      font-size: 0.625rem;
+      font-weight: 500;
+      inset-inline-start: 0;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      width: calc(1.375rem + 1px);
+      height: calc(1.375rem + 1px);
+      background: $ui-base-color;
+      border: 1px solid $highlight-text-color;
+      color: $highlight-text-color;
+      border-radius: 8px;
+    }
+
+    &::after {
+      position: absolute;
+      content: '';
+      width: 1px;
+      background: $highlight-text-color;
+      bottom: 0;
+      top: calc(1.875rem + 1px);
+      inset-inline-start: 0.6875rem;
+    }
+
+    &:last-child {
+      margin-bottom: 0;
+
+      &::after {
+        display: none;
+      }
+    }
+  }
+
+  &__entry {
+    h5 {
+      font-weight: 500;
+      color: $primary-text-color;
+      line-height: 25px;
+      margin-bottom: 16px;
+    }
+
+    .status {
+      border: 1px solid lighten($ui-base-color, 4%);
+      background: $ui-base-color;
+      border-radius: 4px;
+    }
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/basics.scss b/app/javascript/flavours/blobfox/styles/basics.scss
new file mode 100644
index 00000000000000..ff00c797c8e4dc
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/basics.scss
@@ -0,0 +1,294 @@
+@function hex-color($color) {
+  @if type-of($color) == 'color' {
+    $color: str-slice(ie-hex-str($color), 4);
+  }
+
+  @return '%23' + unquote($color);
+}
+
+body {
+  font-family: $font-sans-serif, sans-serif;
+  background: darken($ui-base-color, 7%);
+  font-size: 13px;
+  line-height: 18px;
+  font-weight: 400;
+  color: $primary-text-color;
+  text-rendering: optimizelegibility;
+  font-feature-settings: 'kern';
+  text-size-adjust: none;
+  -webkit-tap-highlight-color: rgba(0, 0, 0, 0%);
+  -webkit-tap-highlight-color: transparent;
+
+  &.system-font {
+    // system-ui => standard property (Chrome/Android WebView 56+, Opera 43+, Safari 11+)
+    // -apple-system => Safari <11 specific
+    // BlinkMacSystemFont => Chrome <56 on macOS specific
+    // Segoe UI => Windows 7/8/10
+    // Oxygen => KDE
+    // Ubuntu => Unity/Ubuntu
+    // Cantarell => GNOME
+    // Fira Sans => Firefox OS
+    // Droid Sans => Older Androids (<4.0)
+    // Helvetica Neue => Older macOS <10.11
+    // $font-sans-serif => web-font (Roboto) fallback and newer Androids (>=4.0)
+    font-family:
+      system-ui,
+      -apple-system,
+      BlinkMacSystemFont,
+      'Segoe UI',
+      Oxygen,
+      Ubuntu,
+      Cantarell,
+      'Fira Sans',
+      'Droid Sans',
+      'Helvetica Neue',
+      $font-sans-serif,
+      sans-serif;
+  }
+
+  &.app-body {
+    padding: 0;
+
+    &.layout-single-column {
+      height: auto;
+      min-height: 100vh;
+      overflow-y: scroll;
+    }
+
+    &.layout-multiple-columns {
+      position: absolute;
+      width: 100%;
+      height: 100%;
+    }
+
+    &.with-modals--active {
+      overflow-y: hidden;
+    }
+  }
+
+  &.lighter {
+    background: $ui-base-color;
+  }
+
+  &.with-modals {
+    overflow-x: hidden;
+    overflow-y: scroll;
+
+    &--active {
+      overflow-y: hidden;
+    }
+  }
+
+  &.player {
+    padding: 0;
+    margin: 0;
+    position: absolute;
+    width: 100%;
+    height: 100%;
+    overflow: hidden;
+
+    & > div {
+      height: 100%;
+    }
+
+    .video-player video {
+      width: 100%;
+      height: 100%;
+      max-height: 100vh;
+    }
+
+    .media-gallery {
+      margin-top: 0;
+      height: 100% !important;
+      border-radius: 0;
+    }
+
+    .media-gallery__item {
+      border-radius: 0;
+    }
+  }
+
+  &.embed {
+    background: lighten($ui-base-color, 4%);
+    margin: 0;
+    padding-bottom: 0;
+
+    .container {
+      position: absolute;
+      width: 100%;
+      height: 100%;
+      overflow: hidden;
+    }
+  }
+
+  &.admin {
+    background: darken($ui-base-color, 4%);
+    padding: 0;
+  }
+
+  &.error {
+    position: absolute;
+    text-align: center;
+    color: $darker-text-color;
+    background: $ui-base-color;
+    width: 100%;
+    height: 100%;
+    padding: 0;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+
+    .dialog {
+      vertical-align: middle;
+      margin: 20px;
+
+      &__illustration {
+        img {
+          display: block;
+          max-width: 470px;
+          width: 100%;
+          height: auto;
+          margin-top: -120px;
+        }
+      }
+
+      h1 {
+        font-size: 20px;
+        line-height: 28px;
+        font-weight: 400;
+      }
+    }
+  }
+}
+
+button {
+  font-family: inherit;
+  cursor: pointer;
+
+  &:focus {
+    outline: none;
+  }
+}
+
+.app-holder {
+  &,
+  & > div,
+  & > noscript {
+    display: flex;
+    width: 100%;
+    align-items: center;
+    justify-content: center;
+    outline: 0 !important;
+  }
+
+  & > noscript {
+    height: 100vh;
+  }
+}
+
+.layout-single-column .app-holder {
+  &,
+  & > div {
+    min-height: 100vh;
+  }
+}
+
+.layout-multiple-columns .app-holder {
+  &,
+  & > div {
+    height: 100%;
+  }
+}
+
+.error-boundary,
+.app-holder noscript {
+  flex-direction: column;
+  font-size: 16px;
+  font-weight: 400;
+  line-height: 1.7;
+  color: lighten($error-red, 4%);
+  text-align: center;
+
+  & > div {
+    max-width: 500px;
+  }
+
+  p {
+    margin-bottom: 0.85em;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+
+  a {
+    color: $highlight-text-color;
+
+    &:hover,
+    &:focus,
+    &:active {
+      text-decoration: none;
+    }
+  }
+
+  &__footer {
+    color: $dark-text-color;
+    font-size: 13px;
+
+    a {
+      color: $dark-text-color;
+    }
+  }
+
+  button {
+    display: inline;
+    border: 0;
+    background: transparent;
+    color: $dark-text-color;
+    font: inherit;
+    padding: 0;
+    margin: 0;
+    line-height: inherit;
+    cursor: pointer;
+    outline: 0;
+    transition: color 300ms linear;
+    text-decoration: underline;
+
+    &:hover,
+    &:focus,
+    &:active {
+      text-decoration: none;
+    }
+
+    &.copied {
+      color: $valid-value-color;
+      transition: none;
+    }
+  }
+}
+
+.logo-resources {
+  // Not using display: none because of https://bugs.chromium.org/p/chromium/issues/detail?id=258029
+  visibility: hidden;
+  user-select: none;
+  pointer-events: none;
+  width: 0;
+  height: 0;
+  overflow: hidden;
+  position: absolute;
+  top: 0;
+  inset-inline-start: 0;
+  z-index: -1000;
+}
+
+// NoScript adds a __ns__pop2top class to the full ancestry of blocked elements,
+// to set the z-index to a high value, which messes with modals and dropdowns.
+// Blocked elements can in theory only be media and frames/embeds, so they
+// should only appear in statuses, under divs and articles.
+body,
+div,
+article {
+  .__ns__pop2top {
+    z-index: unset !important;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/branding.scss b/app/javascript/flavours/blobfox/styles/branding.scss
new file mode 100644
index 00000000000000..d1bddc68b0d7f9
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/branding.scss
@@ -0,0 +1,3 @@
+.logo {
+  color: $primary-text-color;
+}
diff --git a/app/javascript/flavours/blobfox/styles/components/about.scss b/app/javascript/flavours/blobfox/styles/components/about.scss
new file mode 100644
index 00000000000000..98ff91ad18c207
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/components/about.scss
@@ -0,0 +1,295 @@
+.image {
+  position: relative;
+  overflow: hidden;
+
+  &__preview {
+    position: absolute;
+    top: 0;
+    inset-inline-start: 0;
+    width: 100%;
+    height: 100%;
+    object-fit: cover;
+  }
+
+  &.loaded &__preview {
+    display: none;
+  }
+
+  img {
+    display: block;
+    width: 100%;
+    height: 100%;
+    object-fit: cover;
+    border: 0;
+    background: transparent;
+    opacity: 0;
+  }
+
+  &.loaded img {
+    opacity: 1;
+  }
+}
+
+.link-footer {
+  flex: 0 0 auto;
+  padding: 10px;
+  padding-top: 20px;
+  z-index: 1;
+  font-size: 13px;
+
+  p {
+    color: $dark-text-color;
+    margin-bottom: 20px;
+
+    .version {
+      white-space: nowrap;
+    }
+
+    strong {
+      font-weight: 500;
+    }
+
+    a {
+      color: $dark-text-color;
+      text-decoration: underline;
+
+      &:hover,
+      &:focus,
+      &:active {
+        text-decoration: none;
+      }
+    }
+  }
+}
+
+.about {
+  padding: 20px;
+
+  @media screen and (min-width: $no-gap-breakpoint) {
+    border-radius: 4px;
+  }
+
+  &__footer {
+    color: $dark-text-color;
+    text-align: center;
+    font-size: 15px;
+    line-height: 22px;
+    margin-top: 20px;
+  }
+
+  &__header {
+    margin-bottom: 30px;
+
+    &__hero {
+      width: 100%;
+      height: auto;
+      aspect-ratio: 1.9;
+      background: lighten($ui-base-color, 4%);
+      border-radius: 8px;
+      margin-bottom: 30px;
+    }
+
+    h1,
+    p {
+      text-align: center;
+    }
+
+    h1 {
+      font-size: 24px;
+      line-height: 1.5;
+      font-weight: 700;
+      margin-bottom: 10px;
+    }
+
+    p {
+      font-size: 16px;
+      line-height: 24px;
+      font-weight: 400;
+      color: $darker-text-color;
+    }
+  }
+
+  &__meta {
+    background: lighten($ui-base-color, 4%);
+    border-radius: 4px;
+    display: flex;
+    margin-bottom: 30px;
+    font-size: 15px;
+
+    &__column {
+      box-sizing: border-box;
+      width: 50%;
+      padding: 20px;
+    }
+
+    &__divider {
+      width: 0;
+      border: 0;
+      border-style: solid;
+      border-color: lighten($ui-base-color, 8%);
+      border-left-width: 1px;
+      min-height: calc(100% - 60px);
+      flex: 0 0 auto;
+    }
+
+    h4 {
+      font-size: 15px;
+      text-transform: uppercase;
+      color: $darker-text-color;
+      font-weight: 500;
+      margin-bottom: 20px;
+    }
+
+    @media screen and (width <= 600px) {
+      display: block;
+
+      h4 {
+        text-align: center;
+      }
+
+      &__column {
+        width: 100%;
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+      }
+
+      &__divider {
+        min-height: 0;
+        width: 100%;
+        border-left-width: 0;
+        border-top-width: 1px;
+      }
+    }
+
+    .layout-multiple-columns & {
+      display: block;
+
+      h4 {
+        text-align: center;
+      }
+
+      &__column {
+        width: 100%;
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+      }
+
+      &__divider {
+        min-height: 0;
+        width: 100%;
+        border-left-width: 0;
+        border-top-width: 1px;
+      }
+    }
+  }
+
+  &__mail {
+    color: $primary-text-color;
+    text-decoration: none;
+    font-weight: 500;
+
+    &:hover,
+    &:focus,
+    &:active {
+      text-decoration: underline;
+    }
+  }
+
+  .link-footer {
+    padding: 0;
+    margin-top: 60px;
+    text-align: center;
+    font-size: 15px;
+    line-height: 22px;
+
+    @media screen and (min-width: $no-gap-breakpoint) {
+      display: none;
+    }
+  }
+
+  .account {
+    padding: 0;
+    border: 0;
+  }
+
+  .account__avatar-wrapper {
+    margin-inline-start: 0;
+  }
+
+  .account__relationship {
+    display: none;
+  }
+
+  &__section {
+    margin-bottom: 10px;
+
+    &__title {
+      font-size: 17px;
+      font-weight: 600;
+      line-height: 22px;
+      padding: 20px;
+      border-radius: 4px;
+      background: lighten($ui-base-color, 4%);
+      color: $highlight-text-color;
+      cursor: pointer;
+    }
+
+    &.active &__title {
+      border-radius: 4px 4px 0 0;
+    }
+
+    &__body {
+      border: 1px solid lighten($ui-base-color, 4%);
+      border-top: 0;
+      padding: 20px;
+      font-size: 15px;
+      line-height: 22px;
+    }
+  }
+
+  &__domain-blocks {
+    margin-top: 30px;
+    background: darken($ui-base-color, 4%);
+    border: 1px solid lighten($ui-base-color, 4%);
+    border-radius: 4px;
+
+    &__domain {
+      border-bottom: 1px solid lighten($ui-base-color, 4%);
+      padding: 10px;
+      font-size: 15px;
+      color: $darker-text-color;
+
+      &:nth-child(2n) {
+        background: darken($ui-base-color, 2%);
+      }
+
+      &:last-child {
+        border-bottom: 0;
+      }
+
+      &__header {
+        display: flex;
+        gap: 10px;
+        justify-content: space-between;
+        font-weight: 500;
+        margin-bottom: 4px;
+      }
+
+      h6 {
+        color: $secondary-text-color;
+        font-size: inherit;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+      }
+
+      p {
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+      }
+    }
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/components/accounts.scss b/app/javascript/flavours/blobfox/styles/components/accounts.scss
new file mode 100644
index 00000000000000..685421c0a87ae8
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/components/accounts.scss
@@ -0,0 +1,807 @@
+.account {
+  padding: 10px;
+  border-bottom: 1px solid lighten($ui-base-color, 8%);
+  color: inherit;
+  text-decoration: none;
+
+  .account__display-name {
+    flex: 1 1 auto;
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    color: $darker-text-color;
+    overflow: hidden;
+    text-decoration: none;
+    font-size: 14px;
+
+    .display-name {
+      margin-bottom: 4px;
+    }
+
+    .display-name strong {
+      display: inline;
+    }
+  }
+
+  &--minimal {
+    .account__display-name {
+      .display-name {
+        margin-bottom: 0;
+      }
+
+      .display-name strong {
+        display: block;
+      }
+    }
+  }
+
+  &__note {
+    font-size: 14px;
+    font-weight: 400;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    display: -webkit-box;
+    -webkit-line-clamp: 1;
+    -webkit-box-orient: vertical;
+    margin-top: 10px;
+    color: $darker-text-color;
+
+    &--missing {
+      color: $dark-text-color;
+    }
+
+    p {
+      margin-bottom: 10px;
+
+      &:last-child {
+        margin-bottom: 0;
+      }
+    }
+
+    a {
+      color: inherit;
+
+      &:hover,
+      &:focus,
+      &:active {
+        text-decoration: none;
+      }
+    }
+  }
+}
+
+.account__wrapper {
+  display: flex;
+  gap: 10px;
+  align-items: center;
+}
+
+.account__avatar-wrapper {
+  float: left;
+}
+
+.account__avatar {
+  @include avatar-radius;
+
+  display: block;
+  position: relative;
+  overflow: hidden;
+
+  img {
+    display: block;
+    width: 100%;
+    height: 100%;
+    object-fit: cover;
+  }
+
+  &-inline {
+    display: inline-block;
+    vertical-align: middle;
+    margin-inline-end: 5px;
+  }
+
+  &-composite {
+    @include avatar-radius;
+
+    overflow: hidden;
+    position: relative;
+
+    & > div {
+      @include avatar-radius;
+
+      float: left;
+      position: relative;
+      box-sizing: border-box;
+    }
+
+    .account__avatar {
+      width: 100% !important;
+      height: 100% !important;
+    }
+
+    &__label {
+      display: block;
+      position: absolute;
+      top: 50%;
+      inset-inline-start: 50%;
+      transform: translate(-50%, -50%);
+      color: $primary-text-color;
+      text-shadow: 1px 1px 2px $base-shadow-color;
+      font-weight: 700;
+      font-size: 15px;
+    }
+  }
+}
+
+.account__avatar-overlay {
+  position: relative;
+
+  &-overlay {
+    position: absolute;
+    bottom: 0;
+    inset-inline-end: 0;
+    z-index: 1;
+  }
+}
+
+.account__relationship {
+  white-space: nowrap;
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+
+.account__header__wrapper {
+  flex: 0 0 auto;
+  background: lighten($ui-base-color, 4%);
+}
+
+.account__disclaimer {
+  padding: 10px;
+  color: $dark-text-color;
+
+  strong {
+    font-weight: 500;
+
+    @each $lang in $cjk-langs {
+      &:lang(#{$lang}) {
+        font-weight: 700;
+      }
+    }
+  }
+
+  a {
+    font-weight: 500;
+    color: inherit;
+    text-decoration: underline;
+
+    &:hover,
+    &:focus,
+    &:active {
+      text-decoration: none;
+    }
+  }
+}
+
+.account__action-bar {
+  border-top: 1px solid lighten($ui-base-color, 8%);
+  border-bottom: 1px solid lighten($ui-base-color, 8%);
+  line-height: 36px;
+  overflow: hidden;
+  flex: 0 0 auto;
+  display: flex;
+}
+
+.account__action-bar-links {
+  display: flex;
+  flex: 1 1 auto;
+  line-height: 18px;
+  text-align: center;
+}
+
+.account__action-bar__tab {
+  text-decoration: none;
+  overflow: hidden;
+  flex: 0 1 100%;
+  border-inline-start: 1px solid lighten($ui-base-color, 8%);
+  padding: 10px 0;
+  border-bottom: 4px solid transparent;
+
+  &:first-child {
+    border-inline-start: 0;
+  }
+
+  &.active {
+    border-bottom: 4px solid $ui-highlight-color;
+  }
+
+  & > span {
+    display: block;
+    text-transform: uppercase;
+    font-size: 11px;
+    color: $darker-text-color;
+  }
+
+  strong {
+    display: block;
+    font-size: 15px;
+    font-weight: 500;
+    color: $primary-text-color;
+
+    @each $lang in $cjk-langs {
+      &:lang(#{$lang}) {
+        font-weight: 700;
+      }
+    }
+  }
+
+  abbr {
+    color: $highlight-text-color;
+  }
+}
+
+.account-authorize {
+  padding: 14px 10px;
+
+  .detailed-status__display-name {
+    display: block;
+    margin-bottom: 15px;
+    overflow: hidden;
+  }
+}
+
+.account-authorize__avatar {
+  float: left;
+  margin-inline-end: 10px;
+}
+
+.notification__report {
+  padding: 8px 10px;
+  padding-inline-start: 68px;
+  position: relative;
+  border-bottom: 1px solid lighten($ui-base-color, 8%);
+  min-height: 54px;
+
+  &__details {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    color: $darker-text-color;
+    font-size: 15px;
+    line-height: 22px;
+
+    strong {
+      font-weight: 500;
+    }
+  }
+
+  &__avatar {
+    position: absolute;
+    inset-inline-start: 10px;
+    top: 10px;
+  }
+}
+
+.notification__message {
+  margin-inline-start: 42px;
+  padding-top: 8px;
+  padding-inline-start: 26px;
+  cursor: default;
+  color: $darker-text-color;
+  font-size: 15px;
+  position: relative;
+
+  .fa {
+    color: $highlight-text-color;
+  }
+
+  > span {
+    display: block;
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
+}
+
+.account--panel {
+  background: lighten($ui-base-color, 4%);
+  border-top: 1px solid lighten($ui-base-color, 8%);
+  border-bottom: 1px solid lighten($ui-base-color, 8%);
+  display: flex;
+  flex-direction: row;
+  padding: 10px 0;
+}
+
+.account--panel__button,
+.detailed-status__button {
+  flex: 1 1 auto;
+  text-align: center;
+}
+
+.detailed-status__button .emoji-button {
+  padding: 0;
+}
+
+.relationship-tag {
+  color: $white;
+  margin-bottom: 4px;
+  display: block;
+  background-color: rgba($black, 0.45);
+  text-transform: uppercase;
+  font-size: 11px;
+  font-weight: 500;
+  padding: 4px;
+  border-radius: 4px;
+  opacity: 0.7;
+
+  &:hover {
+    opacity: 1;
+  }
+}
+
+.account-gallery__container {
+  display: flex;
+  flex-wrap: wrap;
+  padding: 4px 2px;
+}
+
+.account-gallery__item {
+  border: 0;
+  box-sizing: border-box;
+  display: block;
+  position: relative;
+  border-radius: 4px;
+  overflow: hidden;
+  margin: 2px;
+
+  &__icons {
+    position: absolute;
+    top: 50%;
+    inset-inline-start: 50%;
+    transform: translate(-50%, -50%);
+    font-size: 24px;
+  }
+}
+
+.notification__filter-bar,
+.account__section-headline {
+  background: darken($ui-base-color, 4%);
+  border-bottom: 1px solid lighten($ui-base-color, 8%);
+  cursor: default;
+  display: flex;
+  flex-shrink: 0;
+
+  button {
+    background: transparent;
+    border: 0;
+    margin: 0;
+  }
+
+  button,
+  a {
+    display: block;
+    flex: 1 1 auto;
+    color: $darker-text-color;
+    padding: 15px 0;
+    font-size: 14px;
+    font-weight: 500;
+    text-align: center;
+    text-decoration: none;
+    position: relative;
+
+    &.active {
+      color: $primary-text-color;
+
+      &::before {
+        display: block;
+        content: '';
+        position: absolute;
+        bottom: -1px;
+        left: 0;
+        width: 100%;
+        height: 3px;
+        border-radius: 4px;
+        background: $highlight-text-color;
+      }
+    }
+  }
+
+  &.directory__section-headline {
+    background: darken($ui-base-color, 2%);
+    border-bottom-color: transparent;
+
+    a,
+    button {
+      &.active {
+        &::before {
+          display: none;
+        }
+
+        &::after {
+          border-color: transparent transparent darken($ui-base-color, 7%);
+        }
+      }
+    }
+  }
+}
+
+.account__moved-note {
+  padding: 14px 10px;
+  padding-bottom: 16px;
+  background: lighten($ui-base-color, 4%);
+  border-top: 1px solid lighten($ui-base-color, 8%);
+  border-bottom: 1px solid lighten($ui-base-color, 8%);
+
+  &__message {
+    position: relative;
+    margin-inline-start: 58px;
+    color: $dark-text-color;
+    padding: 8px 0;
+    padding-top: 0;
+    padding-bottom: 4px;
+    font-size: 14px;
+
+    > span {
+      display: block;
+      overflow: hidden;
+      text-overflow: ellipsis;
+    }
+  }
+
+  &__icon-wrapper {
+    inset-inline-start: -26px;
+    position: absolute;
+  }
+
+  .detailed-status__display-avatar {
+    position: relative;
+  }
+
+  .detailed-status__display-name {
+    margin-bottom: 0;
+  }
+}
+
+.account__header__content {
+  color: $darker-text-color;
+  font-size: 14px;
+  font-weight: 400;
+  overflow: hidden;
+  word-break: normal;
+  word-wrap: break-word;
+
+  p {
+    margin-bottom: 20px;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+
+  a {
+    color: inherit;
+    text-decoration: underline;
+
+    &:hover {
+      text-decoration: none;
+    }
+  }
+}
+
+.account__header {
+  overflow: hidden;
+
+  &.inactive {
+    opacity: 0.5;
+
+    .account__header__image,
+    .account__avatar {
+      filter: grayscale(100%);
+    }
+  }
+
+  &__info {
+    position: absolute;
+    top: 10px;
+    inset-inline-start: 10px;
+  }
+
+  &__image {
+    overflow: hidden;
+    height: 145px;
+    position: relative;
+    background: darken($ui-base-color, 4%);
+
+    img {
+      object-fit: cover;
+      display: block;
+      width: 100%;
+      height: 100%;
+      margin: 0;
+    }
+  }
+
+  &__bar {
+    position: relative;
+    background: lighten($ui-base-color, 4%);
+    padding: 5px;
+    border-bottom: 1px solid lighten($ui-base-color, 12%);
+
+    .avatar {
+      display: block;
+      flex: 0 0 auto;
+      width: 94px;
+
+      .account__avatar {
+        background: darken($ui-base-color, 8%);
+        border: 2px solid lighten($ui-base-color, 4%);
+      }
+    }
+  }
+
+  &__tabs {
+    display: flex;
+    align-items: flex-end;
+    justify-content: space-between;
+    padding: 7px 10px;
+    margin-top: -81px;
+    height: 130px;
+    overflow: hidden;
+    margin-inline-start: -2px; // aligns the pfp with content below
+
+    .account-role {
+      margin: 0 2px;
+      padding: 4px 0;
+      box-sizing: border-box;
+      min-width: 90px;
+      text-align: center;
+    }
+
+    &__buttons {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      padding-top: 55px;
+      overflow: hidden;
+
+      .button {
+        flex-shrink: 1;
+        white-space: nowrap;
+
+        @media screen and (max-width: $no-gap-breakpoint) {
+          min-width: 0;
+        }
+      }
+
+      .icon-button {
+        border: 1px solid lighten($ui-base-color, 12%);
+        border-radius: 4px;
+        box-sizing: content-box;
+        padding: 2px;
+      }
+    }
+
+    &__name {
+      padding: 5px 10px;
+
+      .account-role {
+        vertical-align: top;
+      }
+
+      .emojione {
+        width: 22px;
+        height: 22px;
+      }
+
+      h1 {
+        font-size: 16px;
+        line-height: 24px;
+        color: $primary-text-color;
+        font-weight: 500;
+        overflow: hidden;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+
+        small {
+          display: block;
+          font-size: 14px;
+          color: $darker-text-color;
+          font-weight: 400;
+          overflow: hidden;
+          text-overflow: ellipsis;
+
+          span {
+            user-select: all;
+          }
+        }
+      }
+    }
+
+    .spacer {
+      flex: 1 1 auto;
+    }
+  }
+
+  &__bio {
+    overflow: hidden;
+    margin: 0 -5px;
+
+    .account__header__content {
+      padding: 20px 15px;
+      padding-bottom: 5px;
+      color: $primary-text-color;
+    }
+
+    .account__header__joined {
+      font-size: 14px;
+      padding: 5px 15px;
+      color: $darker-text-color;
+
+      .columns-area--mobile & {
+        padding-inline-start: 20px;
+        padding-inline-end: 20px;
+      }
+    }
+
+    .account__header__fields {
+      margin: 0;
+      border-top: 1px solid lighten($ui-base-color, 12%);
+
+      a {
+        color: lighten($ui-highlight-color, 8%);
+      }
+
+      dl:first-child .verified {
+        border-radius: 0 4px 0 0;
+      }
+
+      .verified a {
+        color: $valid-value-color;
+      }
+    }
+  }
+
+  &__extra {
+    margin-top: 4px;
+
+    &__links {
+      font-size: 14px;
+      color: $darker-text-color;
+      padding: 10px 0;
+
+      a {
+        display: inline-block;
+        color: $darker-text-color;
+        text-decoration: none;
+        padding: 5px 10px;
+        font-weight: 500;
+
+        strong {
+          font-weight: 700;
+          color: $primary-text-color;
+        }
+      }
+    }
+  }
+
+  &__account-note {
+    margin: 0 -5px;
+    padding: 10px 15px;
+    display: flex;
+    flex-direction: column;
+    font-size: 14px;
+    font-weight: 400;
+    border-top: 1px solid lighten($ui-base-color, 12%);
+
+    label {
+      display: block;
+      font-size: 12px;
+      font-weight: 500;
+      color: $darker-text-color;
+      text-transform: uppercase;
+      margin-bottom: 5px;
+    }
+
+    &__content {
+      white-space: pre-wrap;
+      padding: 10px 0;
+    }
+
+    strong {
+      font-size: 12px;
+      font-weight: 500;
+      text-transform: uppercase;
+    }
+
+    textarea {
+      display: block;
+      box-sizing: border-box;
+      width: calc(100% + 20px);
+      color: $secondary-text-color;
+      background: $ui-base-color;
+      padding: 10px;
+      margin: 0 -10px;
+      font-family: inherit;
+      font-size: 14px;
+      resize: none;
+      border: 0;
+      outline: 0;
+      border-radius: 4px;
+
+      &::placeholder {
+        color: $dark-text-color;
+        opacity: 1;
+      }
+    }
+  }
+}
+
+.account__contents {
+  overflow: hidden;
+}
+
+.account__details {
+  display: flex;
+  flex-wrap: wrap;
+  column-gap: 1em;
+}
+
+.verified-badge {
+  display: inline-flex;
+  align-items: center;
+  color: $valid-value-color;
+  gap: 4px;
+  overflow: hidden;
+  white-space: nowrap;
+
+  > span {
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
+
+  a {
+    color: inherit;
+    font-weight: 500;
+    text-decoration: none;
+  }
+}
+
+.moved-account-banner,
+.follow-request-banner,
+.account-memorial-banner {
+  padding: 20px;
+  background: lighten($ui-base-color, 4%);
+  display: flex;
+  align-items: center;
+  flex-direction: column;
+
+  &__message {
+    color: $darker-text-color;
+    padding: 8px 0;
+    padding-top: 0;
+    padding-bottom: 4px;
+    font-size: 14px;
+    font-weight: 500;
+    text-align: center;
+    margin-bottom: 16px;
+  }
+
+  &__action {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    gap: 15px;
+    width: 100%;
+  }
+
+  .detailed-status__display-name {
+    margin-bottom: 0;
+  }
+}
+
+.follow-request-banner .button {
+  width: 100%;
+}
+
+.account-memorial-banner__message {
+  margin-bottom: 0;
+}
diff --git a/app/javascript/flavours/blobfox/styles/components/announcements.scss b/app/javascript/flavours/blobfox/styles/components/announcements.scss
new file mode 100644
index 00000000000000..be27120a7d21ce
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/components/announcements.scss
@@ -0,0 +1,233 @@
+.announcements__item__content {
+  word-wrap: break-word;
+  overflow-y: auto;
+
+  .emojione {
+    width: 20px;
+    height: 20px;
+    margin: -3px 0 0;
+  }
+
+  p {
+    margin-bottom: 10px;
+    white-space: pre-wrap;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+
+  a {
+    color: $secondary-text-color;
+    text-decoration: none;
+
+    &:hover {
+      text-decoration: underline;
+    }
+
+    &.mention {
+      &:hover {
+        text-decoration: none;
+
+        span {
+          text-decoration: underline;
+        }
+      }
+    }
+
+    &.unhandled-link {
+      color: $highlight-text-color;
+    }
+  }
+}
+
+.announcements {
+  background: lighten($ui-base-color, 8%);
+  font-size: 13px;
+  display: flex;
+  align-items: flex-end;
+
+  &__mastodon {
+    width: 124px;
+    flex: 0 0 auto;
+
+    @media screen and (max-width: 124px + 300px) {
+      display: none;
+    }
+  }
+
+  &__container {
+    width: calc(100% - 124px);
+    flex: 0 0 auto;
+    position: relative;
+
+    @media screen and (max-width: 124px + 300px) {
+      width: 100%;
+    }
+  }
+
+  &__item {
+    box-sizing: border-box;
+    width: 100%;
+    padding: 15px;
+    position: relative;
+    font-size: 15px;
+    line-height: 20px;
+    word-wrap: break-word;
+    font-weight: 400;
+    max-height: 50vh;
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
+
+    &__range {
+      display: block;
+      font-weight: 500;
+      margin-bottom: 10px;
+      padding-inline-end: 18px;
+    }
+
+    &__unread {
+      position: absolute;
+      top: 19px;
+      inset-inline-end: 19px;
+      display: block;
+      background: $highlight-text-color;
+      border-radius: 50%;
+      width: 0.625rem;
+      height: 0.625rem;
+    }
+  }
+
+  &__pagination {
+    padding: 15px;
+    color: $darker-text-color;
+    position: absolute;
+    bottom: 3px;
+    inset-inline-end: 0;
+  }
+}
+
+.layout-multiple-columns .announcements__mastodon {
+  display: none;
+}
+
+.layout-multiple-columns .announcements__container {
+  width: 100%;
+}
+
+.reactions-bar {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  margin-top: 15px;
+  margin-inline-start: -2px;
+  width: calc(100% - (90px - 33px));
+
+  &__item {
+    flex-shrink: 0;
+    background: lighten($ui-base-color, 12%);
+    border: 0;
+    border-radius: 3px;
+    margin: 2px;
+    cursor: pointer;
+    user-select: none;
+    padding: 0 6px;
+    display: flex;
+    align-items: center;
+    transition: all 100ms ease-in;
+    transition-property: background-color, color;
+
+    &__emoji {
+      display: block;
+      margin: 3px 0;
+      width: 16px;
+      height: 16px;
+
+      img {
+        display: block;
+        margin: 0;
+        width: 100%;
+        height: 100%;
+        min-width: auto;
+        min-height: auto;
+        vertical-align: bottom;
+        object-fit: contain;
+      }
+    }
+
+    &__count {
+      display: block;
+      min-width: 9px;
+      font-size: 13px;
+      font-weight: 500;
+      text-align: center;
+      margin-inline-start: 6px;
+      color: $darker-text-color;
+    }
+
+    &:hover,
+    &:focus,
+    &:active {
+      background: lighten($ui-base-color, 16%);
+      transition: all 200ms ease-out;
+      transition-property: background-color, color;
+
+      &__count {
+        color: lighten($darker-text-color, 4%);
+      }
+    }
+
+    &.active {
+      transition: all 100ms ease-in;
+      transition-property: background-color, color;
+      background-color: mix(
+        lighten($ui-base-color, 12%),
+        $ui-highlight-color,
+        80%
+      );
+
+      .reactions-bar__item__count {
+        color: lighten($highlight-text-color, 8%);
+      }
+    }
+  }
+
+  .emoji-picker-dropdown {
+    margin: 2px;
+  }
+
+  &:hover .emoji-button {
+    opacity: 0.85;
+  }
+
+  .emoji-button {
+    color: $darker-text-color;
+    margin: 0;
+    font-size: 16px;
+    width: auto;
+    flex-shrink: 0;
+    padding: 0 6px;
+    height: 22px;
+    display: flex;
+    align-items: center;
+    opacity: 0.5;
+    transition: all 100ms ease-in;
+    transition-property: background-color, color;
+
+    &:hover,
+    &:active,
+    &:focus {
+      opacity: 1;
+      color: lighten($darker-text-color, 4%);
+      transition: all 200ms ease-out;
+      transition-property: background-color, color;
+    }
+  }
+
+  &--empty {
+    .emoji-button {
+      padding: 0;
+    }
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/components/boost.scss b/app/javascript/flavours/blobfox/styles/components/boost.scss
new file mode 100644
index 00000000000000..2969958e227261
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/components/boost.scss
@@ -0,0 +1,44 @@
+button.icon-button {
+  i.fa-retweet {
+    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='22' height='209'><path d='M4.97 3.16c-.1.03-.17.1-.22.18L.8 8.24c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77L5.5 3.35c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.02-2.4.02H7.1l2.32 2.85.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{hex-color($action-button-color)}' stroke-width='0'/><path d='M7.78 19.66c-.24.02-.44.25-.44.5v2.46h-.06c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v4.47c0 4.26-.56 3.62 3.65 3.62H8.5l-1.3-1.06c-.1-.08-.18-.2-.2-.3-.02-.17.06-.35.2-.45l1.33-1.1H7.28c-.44 0-.72-.3-.72-.7v-4.48c0-.44.28-.72.72-.72h.06v2.5c0 .38.54.63.82.38l4.9-3.93c.25-.18.25-.6 0-.78l-4.9-3.92c-.1-.1-.24-.14-.38-.12zm9.34 2.93c-.54-.02-1.3.02-2.4.02h-1.25l1.3 1.07c.1.07.18.2.2.33.02.16-.06.3-.2.4l-1.33 1.1h1.28c.42 0 .72.28.72.72v4.47c0 .42-.3.72-.72.72h-.1v-2.47c0-.3-.3-.53-.6-.47-.07 0-.14.05-.2.1l-4.9 3.93c-.26.18-.26.6 0 .78l4.9 3.92c.27.25.82 0 .8-.38v-2.5h.1c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.15.4-3.62-1.25-3.66zM10.34 38.66c-.24.02-.44.25-.43.5v2.47H7.3c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.47c0 3.66-.23 3.7 2.34 3.66l-1.34-1.1c-.1-.08-.18-.2-.2-.3 0-.17.07-.35.2-.45l1.96-1.6c-.03-.06-.04-.13-.04-.2v-4.48c0-.44.28-.72.72-.72H9.9v2.5c0 .36.5.6.8.38l4.93-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.08-.23-.13-.36-.12zm5.63 2.93l1.34 1.1c.1.07.18.2.2.33.02.16-.03.3-.16.4l-1.96 1.6c.02.07.06.13.06.22v4.47c0 .42-.3.72-.72.72h-2.66v-2.47c0-.3-.3-.53-.6-.47-.06.02-.12.05-.18.1l-4.94 3.93c-.24.18-.24.6 0 .78l4.94 3.92c.28.22.78-.02.78-.38v-2.5h2.66c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.66.34-3.7-2.4-3.66zM13.06 57.66c-.23.03-.4.26-.4.5v2.47H7.28c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.87l2.93-2.37v-2.5c0-.44.28-.72.72-.72h5.38v2.5c0 .36.5.6.78.38l4.94-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.1-.24-.14-.38-.12zm5.3 6.15l-2.92 2.4v2.52c0 .42-.3.72-.72.72h-5.4v-2.47c0-.3-.32-.53-.6-.47-.07.02-.13.05-.2.1L3.6 70.52c-.25.18-.25.6 0 .78l4.93 3.92c.28.22.78-.02.78-.38v-2.5h5.42c4.27 0 3.65.67 3.65-3.62v-4.47-.44zM19.25 78.8c-.1.03-.2.1-.28.17l-.9.9c-.44-.3-1.36-.25-3.35-.25H7.28c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v.7l2.93.3v-1c0-.44.28-.72.72-.72h7.44c.2 0 .37.08.5.2l-1.8 1.8c-.25.26-.08.76.27.8l6.27.7c.28.03.56-.25.53-.53l-.7-6.25c0-.27-.3-.48-.55-.44zm-17.2 6.1c-.2.07-.36.3-.33.54l.7 6.25c.02.36.58.55.83.27l.8-.8c.02 0 .04-.02.04 0 .46.24 1.37.17 3.18.17h7.44c4.27 0 3.65.67 3.65-3.62v-.75l-2.93-.3v1.05c0 .42-.3.72-.72.72H7.28c-.15 0-.3-.03-.4-.1L8.8 86.4c.3-.24.1-.8-.27-.84l-6.28-.65h-.2zM4.88 98.6c-1.33 0-1.34.48-1.3 2.3l1.14-1.37c.08-.1.22-.17.34-.2.16 0 .34.08.44.2l1.66 2.03c.04 0 .07-.03.12-.03h7.44c.34 0 .57.2.65.5h-2.43c-.34.05-.53.52-.3.78l3.92 4.95c.18.24.6.24.78 0l3.94-4.94c.22-.27-.02-.76-.37-.77H18.4c.02-3.9.6-3.4-3.66-3.4H7.28c-1.08 0-1.86-.04-2.4-.04zm.15 2.46c-.1.03-.2.1-.28.2l-3.94 4.9c-.2.28.03.77.4.78H3.6c-.02 3.94-.45 3.4 3.66 3.4h7.44c3.65 0 3.74.3 3.7-2.25l-1.1 1.34c-.1.1-.2.17-.32.2-.16 0-.34-.08-.44-.2l-1.65-2.03c-.06.02-.1.04-.18.04H7.28c-.35 0-.57-.2-.66-.5h2.44c.37 0 .63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.23-.47-.2zM4.88 117.6c-1.16 0-1.3.3-1.3 1.56l1.14-1.38c.08-.1.22-.14.34-.16.16 0 .34.04.44.16l2.22 2.75h7c.42 0 .72.28.72.72v.53h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-.53c0-4.2.72-3.63-3.66-3.63H7.28c-1.08 0-1.86-.03-2.4-.03zm.1 1.74c-.1.03-.17.1-.23.16L.8 124.44c-.2.28.03.77.4.78H3.6v.5c0 4.26-.55 3.62 3.66 3.62h7.44c1.03 0 1.74.02 2.28 0-.16.02-.34-.03-.44-.15l-2.22-2.76H7.28c-.44 0-.72-.3-.72-.72v-.5h2.5c.37.02.63-.5.4-.78L5.5 119.5c-.12-.15-.34-.22-.53-.16zm12.02 10c1.2-.02 1.4-.25 1.4-1.53l-1.1 1.36c-.07.1-.17.17-.3.18zM5.94 136.6l2.37 2.93h6.42c.42 0 .72.28.72.72v1.25h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.25c0-4.2.72-3.63-3.66-3.63H7.28c-.6 0-.92-.02-1.34-.03zm-1.72.06c-.4.08-.54.3-.6.75l.6-.74zm.84.93c-.12 0-.24.08-.3.18l-3.95 4.9c-.24.3 0 .83.4.82H3.6v1.22c0 4.26-.55 3.62 3.66 3.62h7.44c.63 0 .97.02 1.4.03l-2.37-2.93H7.28c-.44 0-.72-.3-.72-.72v-1.22h2.5c.4.04.67-.53.4-.8l-3.96-4.92c-.1-.13-.27-.2-.44-.2zm13.28 10.03l-.56.7c.36-.07.5-.3.56-.7zM17.13 155.6c-.55-.02-1.32.03-2.4.03h-8.2l2.38 2.9h5.82c.42 0 .72.28.72.72v1.97H12.9c-.32.06-.48.52-.28.78l3.94 4.94c.2.23.6.22.78-.03l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.97c0-3.15.4-3.62-1.25-3.66zm-12.1.28c-.1.02-.2.1-.28.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v1.96c0 4.26-.55 3.62 3.66 3.62h8.24l-2.36-2.9H7.28c-.44 0-.72-.3-.72-.72v-1.97h2.5c.37.02.63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.22-.47-.2zM5.13 174.5c-.15 0-.3.07-.38.2L.8 179.6c-.24.27 0 .82.4.8H3.6v2.32c0 4.26-.55 3.62 3.66 3.62h7.94l-2.35-2.9h-5.6c-.43 0-.7-.3-.7-.72v-2.3h2.5c.38.03.66-.54.4-.83l-3.97-4.9c-.1-.13-.23-.2-.38-.2zm12 .1c-.55-.02-1.32.03-2.4.03H6.83l2.35 2.9h5.52c.42 0 .72.28.72.72v2.34h-2.6c-.3.1-.43.53-.2.78l3.92 4.9c.18.24.6.24.78 0l3.94-4.9c.22-.3-.02-.78-.37-.8H18.4v-2.33c0-3.15.4-3.62-1.25-3.66zM4.97 193.16c-.1.03-.17.1-.22.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77l-3.96-4.9c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.03-2.4.03H7.1l2.32 2.84.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{hex-color($highlight-text-color)}' stroke-width='0'/></svg>");
+  }
+
+  &:hover i.fa-retweet {
+    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='22' height='209'><path d='M4.97 3.16c-.1.03-.17.1-.22.18L.8 8.24c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77L5.5 3.35c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.02-2.4.02H7.1l2.32 2.85.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{hex-color(lighten($action-button-color, 7%))}' stroke-width='0'/><path d='M7.78 19.66c-.24.02-.44.25-.44.5v2.46h-.06c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v4.47c0 4.26-.56 3.62 3.65 3.62H8.5l-1.3-1.06c-.1-.08-.18-.2-.2-.3-.02-.17.06-.35.2-.45l1.33-1.1H7.28c-.44 0-.72-.3-.72-.7v-4.48c0-.44.28-.72.72-.72h.06v2.5c0 .38.54.63.82.38l4.9-3.93c.25-.18.25-.6 0-.78l-4.9-3.92c-.1-.1-.24-.14-.38-.12zm9.34 2.93c-.54-.02-1.3.02-2.4.02h-1.25l1.3 1.07c.1.07.18.2.2.33.02.16-.06.3-.2.4l-1.33 1.1h1.28c.42 0 .72.28.72.72v4.47c0 .42-.3.72-.72.72h-.1v-2.47c0-.3-.3-.53-.6-.47-.07 0-.14.05-.2.1l-4.9 3.93c-.26.18-.26.6 0 .78l4.9 3.92c.27.25.82 0 .8-.38v-2.5h.1c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.15.4-3.62-1.25-3.66zM10.34 38.66c-.24.02-.44.25-.43.5v2.47H7.3c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.47c0 3.66-.23 3.7 2.34 3.66l-1.34-1.1c-.1-.08-.18-.2-.2-.3 0-.17.07-.35.2-.45l1.96-1.6c-.03-.06-.04-.13-.04-.2v-4.48c0-.44.28-.72.72-.72H9.9v2.5c0 .36.5.6.8.38l4.93-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.08-.23-.13-.36-.12zm5.63 2.93l1.34 1.1c.1.07.18.2.2.33.02.16-.03.3-.16.4l-1.96 1.6c.02.07.06.13.06.22v4.47c0 .42-.3.72-.72.72h-2.66v-2.47c0-.3-.3-.53-.6-.47-.06.02-.12.05-.18.1l-4.94 3.93c-.24.18-.24.6 0 .78l4.94 3.92c.28.22.78-.02.78-.38v-2.5h2.66c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.66.34-3.7-2.4-3.66zM13.06 57.66c-.23.03-.4.26-.4.5v2.47H7.28c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.87l2.93-2.37v-2.5c0-.44.28-.72.72-.72h5.38v2.5c0 .36.5.6.78.38l4.94-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.1-.24-.14-.38-.12zm5.3 6.15l-2.92 2.4v2.52c0 .42-.3.72-.72.72h-5.4v-2.47c0-.3-.32-.53-.6-.47-.07.02-.13.05-.2.1L3.6 70.52c-.25.18-.25.6 0 .78l4.93 3.92c.28.22.78-.02.78-.38v-2.5h5.42c4.27 0 3.65.67 3.65-3.62v-4.47-.44zM19.25 78.8c-.1.03-.2.1-.28.17l-.9.9c-.44-.3-1.36-.25-3.35-.25H7.28c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v.7l2.93.3v-1c0-.44.28-.72.72-.72h7.44c.2 0 .37.08.5.2l-1.8 1.8c-.25.26-.08.76.27.8l6.27.7c.28.03.56-.25.53-.53l-.7-6.25c0-.27-.3-.48-.55-.44zm-17.2 6.1c-.2.07-.36.3-.33.54l.7 6.25c.02.36.58.55.83.27l.8-.8c.02 0 .04-.02.04 0 .46.24 1.37.17 3.18.17h7.44c4.27 0 3.65.67 3.65-3.62v-.75l-2.93-.3v1.05c0 .42-.3.72-.72.72H7.28c-.15 0-.3-.03-.4-.1L8.8 86.4c.3-.24.1-.8-.27-.84l-6.28-.65h-.2zM4.88 98.6c-1.33 0-1.34.48-1.3 2.3l1.14-1.37c.08-.1.22-.17.34-.2.16 0 .34.08.44.2l1.66 2.03c.04 0 .07-.03.12-.03h7.44c.34 0 .57.2.65.5h-2.43c-.34.05-.53.52-.3.78l3.92 4.95c.18.24.6.24.78 0l3.94-4.94c.22-.27-.02-.76-.37-.77H18.4c.02-3.9.6-3.4-3.66-3.4H7.28c-1.08 0-1.86-.04-2.4-.04zm.15 2.46c-.1.03-.2.1-.28.2l-3.94 4.9c-.2.28.03.77.4.78H3.6c-.02 3.94-.45 3.4 3.66 3.4h7.44c3.65 0 3.74.3 3.7-2.25l-1.1 1.34c-.1.1-.2.17-.32.2-.16 0-.34-.08-.44-.2l-1.65-2.03c-.06.02-.1.04-.18.04H7.28c-.35 0-.57-.2-.66-.5h2.44c.37 0 .63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.23-.47-.2zM4.88 117.6c-1.16 0-1.3.3-1.3 1.56l1.14-1.38c.08-.1.22-.14.34-.16.16 0 .34.04.44.16l2.22 2.75h7c.42 0 .72.28.72.72v.53h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-.53c0-4.2.72-3.63-3.66-3.63H7.28c-1.08 0-1.86-.03-2.4-.03zm.1 1.74c-.1.03-.17.1-.23.16L.8 124.44c-.2.28.03.77.4.78H3.6v.5c0 4.26-.55 3.62 3.66 3.62h7.44c1.03 0 1.74.02 2.28 0-.16.02-.34-.03-.44-.15l-2.22-2.76H7.28c-.44 0-.72-.3-.72-.72v-.5h2.5c.37.02.63-.5.4-.78L5.5 119.5c-.12-.15-.34-.22-.53-.16zm12.02 10c1.2-.02 1.4-.25 1.4-1.53l-1.1 1.36c-.07.1-.17.17-.3.18zM5.94 136.6l2.37 2.93h6.42c.42 0 .72.28.72.72v1.25h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.25c0-4.2.72-3.63-3.66-3.63H7.28c-.6 0-.92-.02-1.34-.03zm-1.72.06c-.4.08-.54.3-.6.75l.6-.74zm.84.93c-.12 0-.24.08-.3.18l-3.95 4.9c-.24.3 0 .83.4.82H3.6v1.22c0 4.26-.55 3.62 3.66 3.62h7.44c.63 0 .97.02 1.4.03l-2.37-2.93H7.28c-.44 0-.72-.3-.72-.72v-1.22h2.5c.4.04.67-.53.4-.8l-3.96-4.92c-.1-.13-.27-.2-.44-.2zm13.28 10.03l-.56.7c.36-.07.5-.3.56-.7zM17.13 155.6c-.55-.02-1.32.03-2.4.03h-8.2l2.38 2.9h5.82c.42 0 .72.28.72.72v1.97H12.9c-.32.06-.48.52-.28.78l3.94 4.94c.2.23.6.22.78-.03l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.97c0-3.15.4-3.62-1.25-3.66zm-12.1.28c-.1.02-.2.1-.28.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v1.96c0 4.26-.55 3.62 3.66 3.62h8.24l-2.36-2.9H7.28c-.44 0-.72-.3-.72-.72v-1.97h2.5c.37.02.63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.22-.47-.2zM5.13 174.5c-.15 0-.3.07-.38.2L.8 179.6c-.24.27 0 .82.4.8H3.6v2.32c0 4.26-.55 3.62 3.66 3.62h7.94l-2.35-2.9h-5.6c-.43 0-.7-.3-.7-.72v-2.3h2.5c.38.03.66-.54.4-.83l-3.97-4.9c-.1-.13-.23-.2-.38-.2zm12 .1c-.55-.02-1.32.03-2.4.03H6.83l2.35 2.9h5.52c.42 0 .72.28.72.72v2.34h-2.6c-.3.1-.43.53-.2.78l3.92 4.9c.18.24.6.24.78 0l3.94-4.9c.22-.3-.02-.78-.37-.8H18.4v-2.33c0-3.15.4-3.62-1.25-3.66zM4.97 193.16c-.1.03-.17.1-.22.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77l-3.96-4.9c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.03-2.4.03H7.1l2.32 2.84.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{hex-color($highlight-text-color)}' stroke-width='0'/></svg>");
+  }
+
+  &.reblogPrivate {
+    i.fa-retweet {
+      background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' height='209' width='22'><path d='M 4.9707031 3.1503906 L 4.9707031 3.1601562 C 4.8707031 3.1901563 4.8 3.2598438 4.75 3.3398438 L 0.80078125 8.2402344 C 0.60078125 8.5402344 0.8292187 9.0190625 1.1992188 9.0390625 L 3.5996094 9.0390625 L 3.5996094 11.720703 C 3.5996094 15.980703 3.0497656 15.339844 7.2597656 15.339844 L 11.869141 15.339844 L 11.869141 14.119141 L 11.869141 13.523438 L 11.869141 12.441406 C 11.869141 12.441406 11.869141 12.439453 11.869141 12.439453 L 7.2695312 12.439453 C 6.8295312 12.439453 6.5507814 12.140703 6.5507812 11.720703 L 6.5507812 9.0195312 L 9.0507812 9.0195312 C 9.4207813 9.0495313 9.6792188 8.54 9.4492188 8.25 L 5.5 3.3496094 C 5.38 3.1796094 5.1607031 3.1003906 4.9707031 3.1503906 z M 17.150391 3.5800781 L 17.130859 3.5898438 C 16.580859 3.5698436 15.810469 3.609375 14.730469 3.609375 L 7.0996094 3.609375 L 9.4199219 6.4609375 L 9.4492188 6.5195312 L 14.699219 6.5195312 C 15.106887 6.5195312 15.397113 6.7872181 15.414062 7.2050781 C 15.738375 7.0991315 16.077769 7.0273437 16.435547 7.0273438 L 16.578125 7.0273438 C 17.24903 7.0273438 17.874081 7.2325787 18.400391 7.578125 L 18.400391 7.2402344 C 18.400391 4.0902344 18.800391 3.6200781 17.150391 3.5800781 z M 16.435547 8.0273438 C 15.143818 8.0273438 14.083984 9.0851838 14.083984 10.376953 L 14.083984 11.607422 L 13.570312 11.607422 C 13.375448 11.607422 13.210603 11.704118 13.119141 11.791016 C 13.027691 11.877916 12.983569 11.958238 12.951172 12.03125 C 12.886382 12.177277 12.867187 12.304789 12.867188 12.441406 L 12.867188 13.523438 L 12.867188 14.119141 L 12.867188 15.677734 L 12.867188 16.509766 L 13.570312 16.509766 L 19.472656 16.509766 L 20.173828 16.509766 L 20.173828 15.677734 L 20.173828 13.523438 L 20.173828 12.441406 C 20.173828 12.304794 20.156597 12.177281 20.091797 12.03125 C 20.059397 11.95824 20.015299 11.877916 19.923828 11.791016 C 19.832368 11.704116 19.667509 11.607422 19.472656 11.607422 L 18.927734 11.607422 L 18.927734 10.376953 C 18.927734 9.0851838 17.867902 8.0273438 16.576172 8.0273438 L 16.435547 8.0273438 z M 16.435547 9.2207031 L 16.576172 9.2207031 C 17.22782 9.2207031 17.734375 9.7251013 17.734375 10.376953 L 17.734375 11.607422 L 15.277344 11.607422 L 15.277344 10.376953 C 15.277344 9.7251013 15.7839 9.2207031 16.435547 9.2207031 z M 12.919922 9.9394531 C 12.559922 9.9594531 12.359141 10.480234 12.619141 10.740234 L 12.751953 10.904297 C 12.862211 10.870135 12.980058 10.842244 13.085938 10.802734 L 13.085938 10.378906 C 13.085938 10.228632 13.111295 10.084741 13.130859 9.9394531 L 12.919922 9.9394531 z M 19.882812 9.9394531 C 19.902378 10.084741 19.927734 10.228632 19.927734 10.378906 L 19.927734 10.791016 C 20.168811 10.875098 20.455966 10.916935 20.613281 11.066406 C 20.691227 11.140457 20.749315 11.223053 20.806641 11.302734 L 21.259766 10.740234 C 21.519766 10.460234 21.260625 9.9094531 20.890625 9.9394531 L 19.882812 9.9394531 z M 16.435547 10.220703 C 16.301234 10.220703 16.277344 10.244432 16.277344 10.378906 L 16.277344 10.607422 L 16.734375 10.607422 L 16.734375 10.378906 C 16.734375 10.244433 16.712442 10.220703 16.578125 10.220703 L 16.435547 10.220703 z ' fill='#{hex-color($action-button-color)}' stroke-width='0'/><path d='M 7.7792969 19.650391 L 7.7792969 19.660156 C 7.5392969 19.680156 7.3398437 19.910156 7.3398438 20.160156 L 7.3398438 22.619141 L 7.2792969 22.619141 C 6.1992969 22.619141 5.4208594 22.589844 4.8808594 22.589844 C 3.2408594 22.589844 3.6308594 23.020234 3.6308594 26.240234 L 3.6308594 30.710938 C 3.6308594 34.970937 3.0692969 34.330078 7.2792969 34.330078 L 8.5 34.330078 L 7.1992188 33.269531 C 7.0992188 33.189531 7.02 33.070703 7 32.970703 C 6.98 32.800703 7.0592186 32.619531 7.1992188 32.519531 L 8.5292969 31.419922 L 7.2792969 31.419922 C 6.8392969 31.419922 6.5605469 31.120703 6.5605469 30.720703 L 6.5605469 26.240234 C 6.5605469 25.800234 6.8392969 25.519531 7.2792969 25.519531 L 7.3398438 25.519531 L 7.3398438 28.019531 C 7.3398438 28.399531 7.8801564 28.650391 8.1601562 28.400391 L 13.060547 24.470703 C 13.310547 24.290703 13.310547 23.869453 13.060547 23.689453 L 8.1601562 19.769531 C 8.0601563 19.669531 7.9192969 19.630391 7.7792969 19.650391 z M 17.119141 22.580078 L 17.119141 22.589844 C 16.579141 22.569844 15.820703 22.609375 14.720703 22.609375 L 13.470703 22.609375 L 14.769531 23.679688 C 14.869531 23.749688 14.950703 23.879766 14.970703 24.009766 C 14.990703 24.169766 14.909531 24.310156 14.769531 24.410156 L 13.439453 25.509766 L 14.720703 25.509766 C 15.129702 25.509766 15.41841 25.778986 15.433594 26.199219 C 15.752266 26.097283 16.084896 26.027344 16.435547 26.027344 L 16.578125 26.027344 C 17.236645 26.027344 17.848901 26.228565 18.369141 26.5625 L 18.369141 26.240234 C 18.369141 23.090234 18.769141 22.620078 17.119141 22.580078 z M 16.435547 27.027344 C 15.143818 27.027344 14.083984 28.085184 14.083984 29.376953 L 14.083984 30.607422 L 13.570312 30.607422 C 13.375452 30.607422 13.210603 30.704118 13.119141 30.791016 C 13.027691 30.877916 12.983569 30.958238 12.951172 31.03125 C 12.886382 31.177277 12.867184 31.304789 12.867188 31.441406 L 12.867188 32.523438 L 12.867188 33.119141 L 12.867188 34.677734 L 12.867188 35.509766 L 13.570312 35.509766 L 19.472656 35.509766 L 20.173828 35.509766 L 20.173828 34.677734 L 20.173828 32.523438 L 20.173828 31.441406 C 20.173828 31.304794 20.156597 31.177281 20.091797 31.03125 C 20.059397 30.95824 20.015299 30.877916 19.923828 30.791016 C 19.832368 30.704116 19.667509 30.607422 19.472656 30.607422 L 18.927734 30.607422 L 18.927734 29.376953 C 18.927734 28.085184 17.867902 27.027344 16.576172 27.027344 L 16.435547 27.027344 z M 16.435547 28.220703 L 16.576172 28.220703 C 17.22782 28.220703 17.734375 28.725101 17.734375 29.376953 L 17.734375 30.607422 L 15.277344 30.607422 L 15.277344 29.376953 C 15.277344 28.725101 15.7839 28.220703 16.435547 28.220703 z M 13.109375 29.150391 L 8.9199219 32.509766 C 8.6599219 32.689766 8.6599219 33.109063 8.9199219 33.289062 L 11.869141 35.648438 L 11.869141 34.677734 L 11.869141 33.119141 L 11.869141 32.523438 L 11.869141 31.441406 C 11.869141 31.217489 11.912641 30.907486 12.037109 30.626953 C 12.093758 30.499284 12.228597 30.257492 12.429688 30.066406 C 12.580253 29.92335 12.859197 29.887344 13.085938 29.802734 L 13.085938 29.378906 C 13.085938 29.300761 13.104 29.227272 13.109375 29.150391 z M 16.435547 29.220703 C 16.301234 29.220703 16.277344 29.244432 16.277344 29.378906 L 16.277344 29.607422 L 16.734375 29.607422 L 16.734375 29.378906 C 16.734375 29.244433 16.712442 29.220703 16.578125 29.220703 L 16.435547 29.220703 z M 12.943359 36.509766 L 13.820312 37.210938 C 14.090314 37.460938 14.639141 37.210078 14.619141 36.830078 L 14.619141 36.509766 L 13.570312 36.509766 L 12.943359 36.509766 z M 10.330078 38.650391 L 10.339844 38.660156 C 10.099844 38.680156 9.9001562 38.910156 9.9101562 39.160156 L 9.9101562 41.630859 L 7.3007812 41.630859 C 6.2207812 41.630859 5.4403906 41.589844 4.9003906 41.589844 C 3.2603906 41.589844 3.6503906 42.020234 3.6503906 45.240234 L 3.6503906 49.710938 C 3.6503906 53.370936 3.4202344 53.409141 5.9902344 53.369141 L 4.6503906 52.269531 C 4.5503906 52.189531 4.4692187 52.070703 4.4492188 51.970703 C 4.4492188 51.800703 4.5203906 51.619531 4.6503906 51.519531 L 6.609375 49.919922 C 6.579375 49.859922 6.5703125 49.790703 6.5703125 49.720703 L 6.5703125 45.240234 C 6.5703125 44.800234 6.8490625 44.519531 7.2890625 44.519531 L 9.9003906 44.519531 L 9.9003906 47.019531 C 9.9003906 47.379531 10.399219 47.620391 10.699219 47.400391 L 15.630859 43.470703 C 15.870859 43.290703 15.870859 42.869453 15.630859 42.689453 L 10.689453 38.769531 C 10.589453 38.689531 10.460078 38.640391 10.330078 38.650391 z M 16.869141 41.585938 C 16.616211 41.581522 16.322969 41.584844 15.980469 41.589844 L 15.970703 41.589844 L 17.310547 42.689453 C 17.410547 42.759453 17.489766 42.889531 17.509766 43.019531 C 17.529766 43.179531 17.479609 43.319922 17.349609 43.419922 L 15.390625 45.019531 C 15.406724 45.075878 15.427133 45.132837 15.4375 45.197266 C 15.754974 45.096169 16.086404 45.027344 16.435547 45.027344 L 16.578125 45.027344 C 17.24129 45.027344 17.858323 45.230088 18.380859 45.568359 L 18.380859 45.25 C 18.380859 42.0475 18.639648 41.616836 16.869141 41.585938 z M 16.435547 46.027344 C 15.143818 46.027344 14.083984 47.085184 14.083984 48.376953 L 14.083984 49.607422 L 13.570312 49.607422 C 13.375448 49.607422 13.210603 49.704118 13.119141 49.791016 C 13.027691 49.877916 12.983569 49.958238 12.951172 50.03125 C 12.886382 50.177277 12.867187 50.304789 12.867188 50.441406 L 12.867188 51.523438 L 12.867188 52.119141 L 12.867188 53.677734 L 12.867188 54.509766 L 13.570312 54.509766 L 19.472656 54.509766 L 20.173828 54.509766 L 20.173828 53.677734 L 20.173828 51.523438 L 20.173828 50.441406 C 20.173828 50.304794 20.156597 50.177281 20.091797 50.03125 C 20.059397 49.95824 20.015299 49.877916 19.923828 49.791016 C 19.832368 49.704116 19.667509 49.607422 19.472656 49.607422 L 18.927734 49.607422 L 18.927734 48.376953 C 18.927734 47.085184 17.867902 46.027344 16.576172 46.027344 L 16.435547 46.027344 z M 16.435547 47.220703 L 16.576172 47.220703 C 17.22782 47.220703 17.734375 47.725101 17.734375 48.376953 L 17.734375 49.607422 L 15.277344 49.607422 L 15.277344 48.376953 C 15.277344 47.725101 15.7839 47.220703 16.435547 47.220703 z M 11.470703 47.490234 C 11.410703 47.510234 11.349063 47.539844 11.289062 47.589844 L 6.3496094 51.519531 C 6.1096094 51.699531 6.1096094 52.120781 6.3496094 52.300781 L 11.289062 56.220703 C 11.569064 56.440703 12.070312 56.199844 12.070312 55.839844 L 12.070312 55.509766 L 11.869141 55.509766 L 11.869141 53.677734 L 11.869141 52.119141 L 11.869141 51.523438 L 11.869141 50.441406 C 11.869141 50.217489 11.912641 49.907486 12.037109 49.626953 C 12.043809 49.611855 12.061451 49.584424 12.070312 49.566406 L 12.070312 47.960938 C 12.070312 47.660938 11.770703 47.430234 11.470703 47.490234 z M 16.435547 48.220703 C 16.301234 48.220703 16.277344 48.244432 16.277344 48.378906 L 16.277344 48.607422 L 16.734375 48.607422 L 16.734375 48.378906 C 16.734375 48.244433 16.712442 48.220703 16.578125 48.220703 L 16.435547 48.220703 z M 13.060547 57.650391 L 13.060547 57.660156 C 12.830547 57.690156 12.660156 57.920156 12.660156 58.160156 L 12.660156 60.630859 L 7.2792969 60.630859 C 6.1992969 60.630859 5.4208594 60.589844 4.8808594 60.589844 C 3.2408594 60.589844 3.6308594 61.020234 3.6308594 64.240234 L 3.6308594 69.109375 L 6.5605469 66.740234 L 6.5605469 64.240234 C 6.5605469 63.800234 6.8392969 63.519531 7.2792969 63.519531 L 12.660156 63.519531 L 12.660156 66.019531 C 12.660156 66.299799 12.960394 66.500006 13.226562 66.474609 C 13.625751 65.076914 14.904956 64.035678 16.421875 64.029297 L 18.380859 62.470703 C 18.620859 62.290703 18.620859 61.869453 18.380859 61.689453 L 13.439453 57.769531 C 13.339453 57.669531 13.200547 57.630391 13.060547 57.650391 z M 18.359375 63.810547 L 17.800781 64.269531 C 18.004793 64.350836 18.198411 64.450249 18.380859 64.568359 L 18.380859 64.25 L 18.380859 63.810547 L 18.359375 63.810547 z M 16.435547 65.027344 C 15.143818 65.027344 14.083984 66.085184 14.083984 67.376953 L 14.083984 68.607422 L 13.570312 68.607422 C 13.375448 68.607422 13.210603 68.704118 13.119141 68.791016 C 13.027691 68.877916 12.983569 68.958238 12.951172 69.03125 C 12.886382 69.177277 12.867187 69.304789 12.867188 69.441406 L 12.867188 70.523438 L 12.867188 71.119141 L 12.867188 72.677734 L 12.867188 73.509766 L 13.570312 73.509766 L 19.472656 73.509766 L 20.173828 73.509766 L 20.173828 72.677734 L 20.173828 70.523438 L 20.173828 69.441406 C 20.173828 69.304794 20.156597 69.177281 20.091797 69.03125 C 20.059397 68.95824 20.015299 68.877916 19.923828 68.791016 C 19.832368 68.704116 19.667509 68.607422 19.472656 68.607422 L 18.927734 68.607422 L 18.927734 67.376953 C 18.927734 66.085184 17.867902 65.027344 16.576172 65.027344 L 16.435547 65.027344 z M 16.435547 66.220703 L 16.576172 66.220703 C 17.22782 66.220703 17.734375 66.725101 17.734375 67.376953 L 17.734375 68.607422 L 15.277344 68.607422 L 15.277344 67.376953 C 15.277344 66.725101 15.7839 66.220703 16.435547 66.220703 z M 8.7207031 66.509766 C 8.6507031 66.529766 8.5895312 66.559375 8.5195312 66.609375 L 3.5996094 70.519531 C 3.3496094 70.699531 3.3496094 71.120781 3.5996094 71.300781 L 8.5292969 75.220703 C 8.8092969 75.440703 9.3105469 75.199844 9.3105469 74.839844 L 9.3105469 72.339844 L 11.869141 72.339844 L 11.869141 71.119141 L 11.869141 70.523438 L 11.869141 69.449219 L 9.3203125 69.449219 L 9.3203125 66.980469 C 9.3203125 66.680469 9.0007031 66.449766 8.7207031 66.509766 z M 16.435547 67.220703 C 16.301234 67.220703 16.277344 67.244432 16.277344 67.378906 L 16.277344 67.607422 L 16.734375 67.607422 L 16.734375 67.378906 C 16.734375 67.244433 16.712442 67.220703 16.578125 67.220703 L 16.435547 67.220703 z M 19.248047 78.800781 C 19.148558 78.831033 19.050295 78.90106 18.970703 78.970703 L 18.070312 79.869141 C 17.630312 79.569141 16.710703 79.619141 14.720703 79.619141 L 7.2792969 79.619141 C 6.1992969 79.619141 5.4208594 79.589844 4.8808594 79.589844 C 3.2408594 79.589844 3.6308594 80.020234 3.6308594 83.240234 L 3.6308594 83.939453 L 6.5605469 84.240234 L 6.5605469 83.240234 C 6.5605469 82.800234 6.8392969 82.519531 7.2792969 82.519531 L 14.720703 82.519531 C 14.920703 82.519531 15.090703 82.600703 15.220703 82.720703 L 13.419922 84.519531 C 13.279464 84.665607 13.281282 84.881022 13.363281 85.054688 C 13.880838 83.867655 15.067337 83.027344 16.435547 83.027344 L 16.578125 83.027344 C 18.290465 83.027344 19.703357 84.345788 19.890625 86.011719 L 19.960938 86.019531 C 20.240938 86.049531 20.520234 85.770234 20.490234 85.490234 L 19.789062 79.240234 C 19.789062 78.973661 19.498025 78.767523 19.25 78.800781 L 19.248047 78.800781 z M 16.435547 84.027344 C 15.143818 84.027344 14.083984 85.085184 14.083984 86.376953 L 14.083984 87.607422 L 13.570312 87.607422 C 13.375448 87.607422 13.210603 87.704118 13.119141 87.791016 C 13.027691 87.877916 12.983569 87.958238 12.951172 88.03125 C 12.886382 88.177277 12.867187 88.304789 12.867188 88.441406 L 12.867188 89.523438 L 12.867188 90.119141 L 12.867188 91.677734 L 12.867188 92.509766 L 13.570312 92.509766 L 19.472656 92.509766 L 20.173828 92.509766 L 20.173828 91.677734 L 20.173828 89.523438 L 20.173828 88.441406 C 20.173828 88.304794 20.156597 88.177281 20.091797 88.03125 C 20.059397 87.95824 20.015299 87.877916 19.923828 87.791016 C 19.832368 87.704116 19.667509 87.607422 19.472656 87.607422 L 18.927734 87.607422 L 18.927734 86.376953 C 18.927734 85.085184 17.867902 84.027344 16.576172 84.027344 L 16.435547 84.027344 z M 2.0507812 84.900391 C 1.8507824 84.970391 1.6907031 85.199453 1.7207031 85.439453 L 2.4199219 91.689453 C 2.4399219 92.049453 3 92.240929 3.25 91.960938 L 4.0507812 91.160156 C 4.0707812 91.160156 4.0898437 91.140156 4.0898438 91.160156 C 4.5498437 91.400156 5.4595313 91.330078 7.2695312 91.330078 L 11.869141 91.330078 L 11.869141 90.119141 L 11.869141 89.523438 L 11.869141 88.441406 C 11.869141 88.437991 11.871073 88.433136 11.871094 88.429688 L 7.2792969 88.429688 C 7.1292969 88.429688 6.9808594 88.400078 6.8808594 88.330078 L 8.8007812 86.400391 C 9.1007822 86.160391 8.8992969 85.600547 8.5292969 85.560547 L 2.25 84.910156 L 2.0507812 84.910156 L 2.0507812 84.900391 z M 16.435547 85.220703 L 16.576172 85.220703 C 17.22782 85.220703 17.734375 85.725101 17.734375 86.376953 L 17.734375 87.607422 L 15.277344 87.607422 L 15.277344 86.376953 C 15.277344 85.725101 15.7839 85.220703 16.435547 85.220703 z M 4.8808594 98.599609 C 3.5508594 98.599609 3.5400781 99.080402 3.5800781 100.90039 L 4.7207031 99.529297 C 4.8007031 99.429297 4.9405469 99.360078 5.0605469 99.330078 C 5.2205469 99.330078 5.4 99.409297 5.5 99.529297 L 7.1601562 101.56055 C 7.2001563 101.56055 7.2292969 101.5293 7.2792969 101.5293 L 14.720703 101.5293 C 15.060703 101.5293 15.289141 101.7293 15.369141 102.0293 L 12.939453 102.0293 C 12.599453 102.0793 12.410625 102.55055 12.640625 102.81055 L 13.470703 103.85742 C 14.029941 102.77899 15.146801 102.02734 16.435547 102.02734 L 16.578125 102.02734 C 18.158418 102.02734 19.491598 103.14879 19.835938 104.63086 L 21.279297 102.82031 C 21.499297 102.55031 21.260156 102.06078 20.910156 102.05078 L 18.400391 102.05078 C 18.420391 98.150792 19.000234 98.650391 14.740234 98.650391 L 7.2792969 98.650391 C 6.1992969 98.650391 5.4208594 98.609375 4.8808594 98.609375 L 4.8808594 98.599609 z M 5.0292969 101.06055 C 4.9292969 101.09055 4.83 101.15977 4.75 101.25977 L 0.81054688 106.16016 C 0.61054688 106.44016 0.8409375 106.92945 1.2109375 106.93945 L 3.5996094 106.93945 C 3.5796094 110.87945 3.1497656 110.33984 7.2597656 110.33984 L 11.869141 110.33984 L 11.869141 109.11914 L 11.869141 108.52344 L 11.869141 107.44141 L 11.869141 107.43945 L 7.2792969 107.43945 C 6.9292969 107.43945 6.7091406 107.23945 6.6191406 106.93945 L 9.0605469 106.93945 C 9.4305469 106.93945 9.6909375 106.44016 9.4609375 106.16016 L 5.5 101.25977 C 5.4 101.10977 5.1992969 101.03055 5.0292969 101.06055 z M 16.435547 103.02734 C 15.143818 103.02734 14.083984 104.08518 14.083984 105.37695 L 14.083984 106.60742 L 13.570312 106.60742 C 13.375448 106.60742 13.210603 106.70409 13.119141 106.79102 C 13.027691 106.87792 12.983569 106.95823 12.951172 107.03125 C 12.886382 107.17727 12.867187 107.30479 12.867188 107.44141 L 12.867188 108.52344 L 12.867188 109.11914 L 12.867188 110.67773 L 12.867188 111.50977 L 13.570312 111.50977 L 19.472656 111.50977 L 20.173828 111.50977 L 20.173828 110.67773 L 20.173828 108.52344 L 20.173828 107.44141 C 20.173828 107.3048 20.156597 107.17728 20.091797 107.03125 C 20.059397 106.95825 20.015299 106.87792 19.923828 106.79102 C 19.832368 106.70412 19.667509 106.60742 19.472656 106.60742 L 18.927734 106.60742 L 18.927734 105.37695 C 18.927734 104.08518 17.867902 103.02734 16.576172 103.02734 L 16.435547 103.02734 z M 16.435547 104.2207 L 16.576172 104.2207 C 17.22782 104.2207 17.734375 104.7251 17.734375 105.37695 L 17.734375 106.60742 L 15.277344 106.60742 L 15.277344 105.37695 C 15.277344 104.7251 15.7839 104.2207 16.435547 104.2207 z M 16.435547 105.2207 C 16.301234 105.2207 16.277344 105.24444 16.277344 105.37891 L 16.277344 105.60742 L 16.734375 105.60742 L 16.734375 105.37891 C 16.734375 105.24441 16.712442 105.2207 16.578125 105.2207 L 16.435547 105.2207 z M 4.8808594 117.58984 L 4.8808594 117.59961 C 3.7208594 117.59961 3.5800781 117.90016 3.5800781 119.16016 L 4.7207031 117.7793 C 4.8007031 117.6793 4.9405469 117.63914 5.0605469 117.61914 C 5.2205469 117.61914 5.4 117.6593 5.5 117.7793 L 7.7207031 120.5293 L 14.720703 120.5293 C 15.123595 120.5293 15.408576 120.79174 15.431641 121.20117 C 15.750992 121.09876 16.08404 121.02734 16.435547 121.02734 L 16.578125 121.02734 C 17.24903 121.02734 17.874081 121.23262 18.400391 121.57812 L 18.400391 121.25 C 18.400391 117.05 19.120234 117.61914 14.740234 117.61914 L 7.2792969 117.61914 C 6.1992969 117.61914 5.4208594 117.58984 4.8808594 117.58984 z M 4.9804688 119.33984 C 4.8804688 119.36984 4.81 119.44 4.75 119.5 L 0.80078125 124.43945 C 0.60078125 124.71945 0.8292182 125.2107 1.1992188 125.2207 L 3.5996094 125.2207 L 3.5996094 125.7207 C 3.5996094 129.9807 3.0497656 129.33984 7.2597656 129.33984 L 11.869141 129.33984 L 11.869141 128.11914 L 11.869141 127.52344 L 11.869141 126.44141 C 11.869141 126.43799 11.871073 126.43314 11.871094 126.42969 L 7.2792969 126.42969 C 6.8392969 126.42969 6.5605469 126.13094 6.5605469 125.71094 L 6.5605469 125.21094 L 9.0605469 125.21094 C 9.4305469 125.23094 9.6909375 124.70969 9.4609375 124.42969 L 5.5 119.5 C 5.3820133 119.35252 5.1682348 119.28513 4.9804688 119.33984 z M 12.839844 121.7793 C 12.539844 121.8793 12.410625 122.32055 12.640625 122.56055 L 13.267578 123.34375 C 13.473522 122.72168 13.852237 122.1828 14.353516 121.7793 L 12.839844 121.7793 z M 18.658203 121.7793 C 19.393958 122.37155 19.878978 123.25738 19.916016 124.25781 L 21.279297 122.56055 C 21.499297 122.28055 21.260156 121.7893 20.910156 121.7793 L 18.658203 121.7793 z M 16.435547 122.02734 C 15.143818 122.02734 14.083984 123.08518 14.083984 124.37695 L 14.083984 125.60742 L 13.570312 125.60742 C 13.375448 125.60742 13.210603 125.70409 13.119141 125.79102 C 13.027691 125.87792 12.983569 125.95823 12.951172 126.03125 C 12.886382 126.17727 12.867187 126.30479 12.867188 126.44141 L 12.867188 127.52344 L 12.867188 128.11914 L 12.867188 129.67773 L 12.867188 130.50977 L 13.570312 130.50977 L 19.472656 130.50977 L 20.173828 130.50977 L 20.173828 129.67773 L 20.173828 127.52344 L 20.173828 126.44141 C 20.173828 126.3048 20.156597 126.17728 20.091797 126.03125 C 20.059397 125.95825 20.015299 125.87792 19.923828 125.79102 C 19.832368 125.70412 19.667509 125.60742 19.472656 125.60742 L 18.927734 125.60742 L 18.927734 124.37695 C 18.927734 123.08518 17.867902 122.02734 16.576172 122.02734 L 16.435547 122.02734 z M 16.435547 123.2207 L 16.576172 123.2207 C 17.22782 123.2207 17.734375 123.7251 17.734375 124.37695 L 17.734375 125.60742 L 15.277344 125.60742 L 15.277344 124.37695 C 15.277344 123.7251 15.7839 123.2207 16.435547 123.2207 z M 16.435547 124.2207 C 16.301234 124.2207 16.277344 124.24444 16.277344 124.37891 L 16.277344 124.60742 L 16.734375 124.60742 L 16.734375 124.37891 C 16.734375 124.24441 16.712442 124.2207 16.578125 124.2207 L 16.435547 124.2207 z M 5.9394531 136.58984 L 5.9394531 136.59961 L 8.3105469 139.5293 L 14.730469 139.5293 C 15.131912 139.5293 15.414551 139.79039 15.439453 140.19727 C 15.756409 140.09653 16.087055 140.02734 16.435547 140.02734 L 16.578125 140.02734 C 17.24903 140.02734 17.874081 140.23261 18.400391 140.57812 L 18.400391 140.25 C 18.400391 136.05 19.120234 136.61914 14.740234 136.61914 L 7.2792969 136.61914 C 6.6792969 136.61914 6.3594531 136.59984 5.9394531 136.58984 z M 4.2207031 136.66016 C 3.8207031 136.74016 3.6791406 136.96016 3.6191406 137.41016 L 4.2207031 136.66992 L 4.2207031 136.66016 z M 5.0605469 137.57031 L 5.0605469 137.58984 C 4.9405469 137.58984 4.8197656 137.66953 4.7597656 137.76953 L 0.81054688 142.66992 C 0.57054688 142.96992 0.8109375 143.50023 1.2109375 143.49023 L 3.5996094 143.49023 L 3.5996094 144.71094 C 3.5996094 148.97094 3.0497656 148.33008 7.2597656 148.33008 L 11.869141 148.33008 L 11.869141 147.11914 L 11.869141 146.52344 L 11.869141 145.44141 C 11.869141 145.43799 11.871073 145.43314 11.871094 145.42969 L 7.2792969 145.42969 C 6.8392969 145.42969 6.5605469 145.13094 6.5605469 144.71094 L 6.5605469 143.49023 L 9.0605469 143.49023 C 9.4605469 143.53023 9.7309375 142.95945 9.4609375 142.68945 L 5.5 137.76953 C 5.4 137.63953 5.2305469 137.57031 5.0605469 137.57031 z M 16.435547 141.02734 C 15.143818 141.02734 14.083984 142.08518 14.083984 143.37695 L 14.083984 144.60742 L 13.570312 144.60742 C 13.375448 144.60742 13.210603 144.70409 13.119141 144.79102 C 13.027691 144.87792 12.983569 144.95823 12.951172 145.03125 C 12.886382 145.17727 12.867187 145.30479 12.867188 145.44141 L 12.867188 146.52344 L 12.867188 147.11914 L 12.867188 148.67773 L 12.867188 149.50977 L 13.570312 149.50977 L 19.472656 149.50977 L 20.173828 149.50977 L 20.173828 148.67773 L 20.173828 146.52344 L 20.173828 145.44141 C 20.173828 145.3048 20.156597 145.17728 20.091797 145.03125 C 20.059397 144.95825 20.015299 144.87792 19.923828 144.79102 C 19.832368 144.70412 19.667509 144.60742 19.472656 144.60742 L 18.927734 144.60742 L 18.927734 143.37695 C 18.927734 142.08518 17.867902 141.02734 16.576172 141.02734 L 16.435547 141.02734 z M 12.849609 141.5 C 12.549609 141.6 12.420391 142.0393 12.650391 142.2793 L 13.136719 142.88672 C 13.213026 142.38119 13.390056 141.90696 13.667969 141.5 L 12.849609 141.5 z M 19.34375 141.5 C 19.710704 142.03735 19.927734 142.68522 19.927734 143.37891 L 19.927734 143.79102 C 19.965561 143.80421 20.005506 143.81448 20.044922 143.82617 L 21.289062 142.2793 C 21.509062 141.9993 21.269922 141.51 20.919922 141.5 L 19.34375 141.5 z M 16.435547 142.2207 L 16.576172 142.2207 C 17.22782 142.2207 17.734375 142.7251 17.734375 143.37695 L 17.734375 144.60742 L 15.277344 144.60742 L 15.277344 143.37695 C 15.277344 142.7251 15.7839 142.2207 16.435547 142.2207 z M 16.435547 143.2207 C 16.301234 143.2207 16.277344 143.24444 16.277344 143.37891 L 16.277344 143.60742 L 16.734375 143.60742 L 16.734375 143.37891 C 16.734375 143.24441 16.712442 143.2207 16.578125 143.2207 L 16.435547 143.2207 z M 17.130859 155.59961 C 16.580859 155.57961 15.810469 155.63086 14.730469 155.63086 L 6.5292969 155.63086 L 8.9101562 158.5293 L 14.730469 158.5293 C 15.131912 158.5293 15.414551 158.79039 15.439453 159.19727 C 15.756409 159.09653 16.087055 159.02734 16.435547 159.02734 L 16.578125 159.02734 C 17.24903 159.02734 17.874081 159.23261 18.400391 159.57812 L 18.400391 159.25977 C 18.400391 156.10977 18.800391 155.63961 17.150391 155.59961 L 17.130859 155.59961 z M 5.0292969 155.86914 L 5.0292969 155.88086 C 4.9292969 155.90086 4.83 155.98055 4.75 156.06055 L 0.81054688 160.96094 C 0.61054688 161.26094 0.8409375 161.73977 1.2109375 161.75977 L 3.5996094 161.75977 L 3.5996094 163.7207 C 3.5996094 167.9807 3.0497656 167.33984 7.2597656 167.33984 L 11.869141 167.33984 L 11.869141 166.11914 L 11.869141 165.52344 L 11.869141 164.44141 L 11.869141 164.43945 L 7.2792969 164.43945 C 6.8392969 164.43945 6.5605469 164.1407 6.5605469 163.7207 L 6.5605469 161.75 L 9.0605469 161.75 C 9.4305469 161.77 9.6909375 161.2507 9.4609375 160.9707 L 5.5 156.07031 C 5.4 155.92031 5.1992969 155.84914 5.0292969 155.86914 z M 16.435547 160.02734 C 15.143818 160.02734 14.083984 161.08518 14.083984 162.37695 L 14.083984 163.60742 L 13.570312 163.60742 C 13.375448 163.60742 13.210603 163.70409 13.119141 163.79102 C 13.027691 163.87792 12.983569 163.95823 12.951172 164.03125 C 12.886382 164.17727 12.867187 164.30479 12.867188 164.44141 L 12.867188 165.52344 L 12.867188 166.11914 L 12.867188 167.67773 L 12.867188 168.50977 L 13.570312 168.50977 L 19.472656 168.50977 L 20.173828 168.50977 L 20.173828 167.67773 L 20.173828 165.52344 L 20.173828 164.44141 C 20.173828 164.3048 20.156597 164.17728 20.091797 164.03125 C 20.059397 163.95825 20.015299 163.87792 19.923828 163.79102 C 19.832368 163.70412 19.667509 163.60742 19.472656 163.60742 L 18.927734 163.60742 L 18.927734 162.37695 C 18.927734 161.08518 17.867902 160.02734 16.576172 160.02734 L 16.435547 160.02734 z M 12.900391 161.2207 C 12.580391 161.2807 12.419141 161.74 12.619141 162 L 13.085938 162.58594 L 13.085938 162.37891 C 13.085938 161.97087 13.170592 161.58376 13.306641 161.2207 L 12.900391 161.2207 z M 16.435547 161.2207 L 16.576172 161.2207 C 17.22782 161.2207 17.734375 161.7251 17.734375 162.37695 L 17.734375 163.60742 L 15.277344 163.60742 L 15.277344 162.37695 C 15.277344 161.7251 15.7839 161.2207 16.435547 161.2207 z M 19.708984 161.23047 C 19.842743 161.59081 19.927734 161.97449 19.927734 162.37891 L 19.927734 162.79102 C 20.119162 162.85779 20.322917 162.91147 20.484375 163 L 21.279297 162.00977 C 21.499297 161.72977 21.260156 161.24047 20.910156 161.23047 L 19.708984 161.23047 z M 16.435547 162.2207 C 16.301234 162.2207 16.277344 162.24444 16.277344 162.37891 L 16.277344 162.60742 L 16.734375 162.60742 L 16.734375 162.37891 C 16.734375 162.24441 16.712442 162.2207 16.578125 162.2207 L 16.435547 162.2207 z M 5.0996094 174.49023 L 5.1308594 174.5 C 4.9808594 174.5 4.83 174.56922 4.75 174.69922 L 0.80078125 179.59961 C 0.56078125 179.86961 0.7992182 180.42039 1.1992188 180.40039 L 3.5996094 180.40039 L 3.5996094 182.7207 C 3.5996094 186.9807 3.0497656 186.33984 7.2597656 186.33984 L 11.869141 186.33984 L 11.869141 185.11914 L 11.869141 184.52344 L 11.869141 183.44141 L 11.869141 183.43945 L 7.25 183.43945 C 6.82 183.43945 6.5507814 183.1407 6.5507812 182.7207 L 6.5507812 180.41992 L 9.0507812 180.41992 C 9.4307824 180.44992 9.7092187 179.87984 9.4492188 179.58984 L 5.4804688 174.68945 C 5.3804688 174.55945 5.2496094 174.49023 5.0996094 174.49023 z M 17.150391 174.58008 L 17.130859 174.59961 C 16.580859 174.57961 15.810469 174.63086 14.730469 174.63086 L 6.8300781 174.63086 L 9.1796875 177.5293 L 14.699219 177.5293 C 15.104107 177.5293 15.391475 177.79407 15.412109 178.20703 C 15.737096 178.1006 16.076913 178.02734 16.435547 178.02734 L 16.578125 178.02734 C 17.24903 178.02734 17.874081 178.2326 18.400391 178.57812 L 18.400391 178.24023 C 18.400391 175.09023 18.800391 174.62008 17.150391 174.58008 z M 16.435547 179.02734 C 15.143818 179.02734 14.083984 180.08518 14.083984 181.37695 L 14.083984 182.60742 L 13.570312 182.60742 C 13.375448 182.60742 13.210603 182.70409 13.119141 182.79102 C 13.027691 182.87792 12.983569 182.95823 12.951172 183.03125 C 12.886382 183.17727 12.867187 183.30479 12.867188 183.44141 L 12.867188 184.52344 L 12.867188 185.11914 L 12.867188 186.67773 L 12.867188 187.50977 L 13.570312 187.50977 L 19.472656 187.50977 L 20.173828 187.50977 L 20.173828 186.67773 L 20.173828 184.52344 L 20.173828 183.44141 C 20.173828 183.3048 20.156597 183.17728 20.091797 183.03125 C 20.059397 182.95825 20.015299 182.87792 19.923828 182.79102 C 19.832368 182.70412 19.667509 182.60742 19.472656 182.60742 L 18.927734 182.60742 L 18.927734 181.37695 C 18.927734 180.08518 17.867902 179.02734 16.576172 179.02734 L 16.435547 179.02734 z M 16.435547 180.2207 L 16.576172 180.2207 C 17.22782 180.2207 17.734375 180.7251 17.734375 181.37695 L 17.734375 182.60742 L 15.277344 182.60742 L 15.277344 181.37695 C 15.277344 180.7251 15.7839 180.2207 16.435547 180.2207 z M 19.816406 180.57031 C 19.882311 180.83091 19.927734 181.09907 19.927734 181.37891 L 19.927734 181.79102 C 20.168811 181.87511 20.455966 181.91694 20.613281 182.06641 C 20.630645 182.0829 20.639883 182.10199 20.65625 182.11914 L 21.259766 181.36914 C 21.479766 181.06914 21.240625 180.59031 20.890625 180.57031 L 19.816406 180.57031 z M 12.820312 180.58984 C 12.520316 180.68984 12.389141 181.11914 12.619141 181.36914 L 12.990234 181.83203 C 13.022029 181.82207 13.055579 181.81406 13.085938 181.80273 L 13.085938 181.37891 C 13.085938 181.10616 13.128698 180.84442 13.191406 180.58984 L 12.820312 180.58984 z M 16.435547 181.2207 C 16.301234 181.2207 16.277344 181.24444 16.277344 181.37891 L 16.277344 181.60742 L 16.734375 181.60742 L 16.734375 181.37891 C 16.734375 181.24441 16.712442 181.2207 16.578125 181.2207 L 16.435547 181.2207 z M 4.9609375 193.15039 L 4.9707031 193.16016 C 4.8707031 193.19016 4.8 193.25984 4.75 193.33984 L 0.81054688 198.24023 C 0.61054688 198.54023 0.8409375 199.01906 1.2109375 199.03906 L 3.5996094 199.03906 L 3.5996094 201.7207 C 3.5996094 205.9807 3.0497656 205.33984 7.2597656 205.33984 L 11.869141 205.33984 L 11.869141 204.11914 L 11.869141 203.52344 L 11.869141 202.44141 C 11.869141 202.44141 11.869141 202.43945 11.869141 202.43945 L 7.2695312 202.43945 C 6.8295312 202.43945 6.5507814 202.1407 6.5507812 201.7207 L 6.5507812 199.01953 L 9.0507812 199.01953 C 9.4207814 199.04953 9.6792188 198.54 9.4492188 198.25 L 5.4902344 193.34961 C 5.3702344 193.17961 5.1509375 193.10039 4.9609375 193.15039 z M 17.150391 193.58008 L 17.130859 193.58984 C 16.580859 193.56984 15.810469 193.61914 14.730469 193.61914 L 7.0996094 193.61914 L 9.4199219 196.46094 L 9.4492188 196.51953 L 14.699219 196.51953 C 15.106887 196.51953 15.397075 196.78718 15.414062 197.20508 C 15.738375 197.09913 16.077769 197.02734 16.435547 197.02734 L 16.578125 197.02734 C 17.24903 197.02734 17.874081 197.23259 18.400391 197.57812 L 18.400391 197.24023 C 18.400391 194.09023 18.800391 193.62008 17.150391 193.58008 z M 16.435547 198.02734 C 15.143818 198.02734 14.083984 199.08518 14.083984 200.37695 L 14.083984 201.60742 L 13.570312 201.60742 C 13.375448 201.60742 13.210603 201.70409 13.119141 201.79102 C 13.027691 201.87792 12.983569 201.95823 12.951172 202.03125 C 12.886382 202.17727 12.867187 202.30479 12.867188 202.44141 L 12.867188 203.52344 L 12.867188 204.11914 L 12.867188 205.67773 L 12.867188 206.50977 L 13.570312 206.50977 L 19.472656 206.50977 L 20.173828 206.50977 L 20.173828 205.67773 L 20.173828 203.52344 L 20.173828 202.44141 C 20.173828 202.3048 20.156597 202.17728 20.091797 202.03125 C 20.059397 201.95825 20.015299 201.87792 19.923828 201.79102 C 19.832368 201.70412 19.667509 201.60742 19.472656 201.60742 L 18.927734 201.60742 L 18.927734 200.37695 C 18.927734 199.08518 17.867902 198.02734 16.576172 198.02734 L 16.435547 198.02734 z M 16.435547 199.2207 L 16.576172 199.2207 C 17.22782 199.2207 17.734375 199.7251 17.734375 200.37695 L 17.734375 201.60742 L 15.277344 201.60742 L 15.277344 200.37695 C 15.277344 199.7251 15.7839 199.2207 16.435547 199.2207 z M 12.919922 199.93945 C 12.559922 199.95945 12.359141 200.48023 12.619141 200.74023 L 12.751953 200.9043 C 12.862211 200.87013 12.980058 200.84224 13.085938 200.80273 L 13.085938 200.37891 C 13.085938 200.22863 13.111295 200.08474 13.130859 199.93945 L 12.919922 199.93945 z M 19.882812 199.93945 C 19.902378 200.08474 19.927734 200.22863 19.927734 200.37891 L 19.927734 200.79102 C 20.168811 200.87511 20.455966 200.91694 20.613281 201.06641 C 20.691227 201.14046 20.749315 201.22305 20.806641 201.30273 L 21.259766 200.74023 C 21.519766 200.46023 21.260625 199.90945 20.890625 199.93945 L 19.882812 199.93945 z M 16.435547 200.2207 C 16.301234 200.2207 16.277344 200.24444 16.277344 200.37891 L 16.277344 200.60742 L 16.734375 200.60742 L 16.734375 200.37891 C 16.734375 200.24441 16.712442 200.2207 16.578125 200.2207 L 16.435547 200.2207 z ' fill='#{hex-color($highlight-text-color)}' stroke-width='0' /></svg>");
+    }
+
+    &:hover i.fa-retweet {
+      background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' height='209' width='22'><path d='M 4.9707031 3.1503906 L 4.9707031 3.1601562 C 4.8707031 3.1901563 4.8 3.2598438 4.75 3.3398438 L 0.80078125 8.2402344 C 0.60078125 8.5402344 0.8292187 9.0190625 1.1992188 9.0390625 L 3.5996094 9.0390625 L 3.5996094 11.720703 C 3.5996094 15.980703 3.0497656 15.339844 7.2597656 15.339844 L 11.869141 15.339844 L 11.869141 14.119141 L 11.869141 13.523438 L 11.869141 12.441406 C 11.869141 12.441406 11.869141 12.439453 11.869141 12.439453 L 7.2695312 12.439453 C 6.8295312 12.439453 6.5507814 12.140703 6.5507812 11.720703 L 6.5507812 9.0195312 L 9.0507812 9.0195312 C 9.4207813 9.0495313 9.6792188 8.54 9.4492188 8.25 L 5.5 3.3496094 C 5.38 3.1796094 5.1607031 3.1003906 4.9707031 3.1503906 z M 17.150391 3.5800781 L 17.130859 3.5898438 C 16.580859 3.5698436 15.810469 3.609375 14.730469 3.609375 L 7.0996094 3.609375 L 9.4199219 6.4609375 L 9.4492188 6.5195312 L 14.699219 6.5195312 C 15.106887 6.5195312 15.397113 6.7872181 15.414062 7.2050781 C 15.738375 7.0991315 16.077769 7.0273437 16.435547 7.0273438 L 16.578125 7.0273438 C 17.24903 7.0273438 17.874081 7.2325787 18.400391 7.578125 L 18.400391 7.2402344 C 18.400391 4.0902344 18.800391 3.6200781 17.150391 3.5800781 z M 16.435547 8.0273438 C 15.143818 8.0273438 14.083984 9.0851838 14.083984 10.376953 L 14.083984 11.607422 L 13.570312 11.607422 C 13.375448 11.607422 13.210603 11.704118 13.119141 11.791016 C 13.027691 11.877916 12.983569 11.958238 12.951172 12.03125 C 12.886382 12.177277 12.867187 12.304789 12.867188 12.441406 L 12.867188 13.523438 L 12.867188 14.119141 L 12.867188 15.677734 L 12.867188 16.509766 L 13.570312 16.509766 L 19.472656 16.509766 L 20.173828 16.509766 L 20.173828 15.677734 L 20.173828 13.523438 L 20.173828 12.441406 C 20.173828 12.304794 20.156597 12.177281 20.091797 12.03125 C 20.059397 11.95824 20.015299 11.877916 19.923828 11.791016 C 19.832368 11.704116 19.667509 11.607422 19.472656 11.607422 L 18.927734 11.607422 L 18.927734 10.376953 C 18.927734 9.0851838 17.867902 8.0273438 16.576172 8.0273438 L 16.435547 8.0273438 z M 16.435547 9.2207031 L 16.576172 9.2207031 C 17.22782 9.2207031 17.734375 9.7251013 17.734375 10.376953 L 17.734375 11.607422 L 15.277344 11.607422 L 15.277344 10.376953 C 15.277344 9.7251013 15.7839 9.2207031 16.435547 9.2207031 z M 12.919922 9.9394531 C 12.559922 9.9594531 12.359141 10.480234 12.619141 10.740234 L 12.751953 10.904297 C 12.862211 10.870135 12.980058 10.842244 13.085938 10.802734 L 13.085938 10.378906 C 13.085938 10.228632 13.111295 10.084741 13.130859 9.9394531 L 12.919922 9.9394531 z M 19.882812 9.9394531 C 19.902378 10.084741 19.927734 10.228632 19.927734 10.378906 L 19.927734 10.791016 C 20.168811 10.875098 20.455966 10.916935 20.613281 11.066406 C 20.691227 11.140457 20.749315 11.223053 20.806641 11.302734 L 21.259766 10.740234 C 21.519766 10.460234 21.260625 9.9094531 20.890625 9.9394531 L 19.882812 9.9394531 z M 16.435547 10.220703 C 16.301234 10.220703 16.277344 10.244432 16.277344 10.378906 L 16.277344 10.607422 L 16.734375 10.607422 L 16.734375 10.378906 C 16.734375 10.244433 16.712442 10.220703 16.578125 10.220703 L 16.435547 10.220703 z ' fill='#{hex-color(lighten($action-button-color, 7%))}' stroke-width='0'/><path d='M 7.7792969 19.650391 L 7.7792969 19.660156 C 7.5392969 19.680156 7.3398437 19.910156 7.3398438 20.160156 L 7.3398438 22.619141 L 7.2792969 22.619141 C 6.1992969 22.619141 5.4208594 22.589844 4.8808594 22.589844 C 3.2408594 22.589844 3.6308594 23.020234 3.6308594 26.240234 L 3.6308594 30.710938 C 3.6308594 34.970937 3.0692969 34.330078 7.2792969 34.330078 L 8.5 34.330078 L 7.1992188 33.269531 C 7.0992188 33.189531 7.02 33.070703 7 32.970703 C 6.98 32.800703 7.0592186 32.619531 7.1992188 32.519531 L 8.5292969 31.419922 L 7.2792969 31.419922 C 6.8392969 31.419922 6.5605469 31.120703 6.5605469 30.720703 L 6.5605469 26.240234 C 6.5605469 25.800234 6.8392969 25.519531 7.2792969 25.519531 L 7.3398438 25.519531 L 7.3398438 28.019531 C 7.3398438 28.399531 7.8801564 28.650391 8.1601562 28.400391 L 13.060547 24.470703 C 13.310547 24.290703 13.310547 23.869453 13.060547 23.689453 L 8.1601562 19.769531 C 8.0601563 19.669531 7.9192969 19.630391 7.7792969 19.650391 z M 17.119141 22.580078 L 17.119141 22.589844 C 16.579141 22.569844 15.820703 22.609375 14.720703 22.609375 L 13.470703 22.609375 L 14.769531 23.679688 C 14.869531 23.749688 14.950703 23.879766 14.970703 24.009766 C 14.990703 24.169766 14.909531 24.310156 14.769531 24.410156 L 13.439453 25.509766 L 14.720703 25.509766 C 15.129702 25.509766 15.41841 25.778986 15.433594 26.199219 C 15.752266 26.097283 16.084896 26.027344 16.435547 26.027344 L 16.578125 26.027344 C 17.236645 26.027344 17.848901 26.228565 18.369141 26.5625 L 18.369141 26.240234 C 18.369141 23.090234 18.769141 22.620078 17.119141 22.580078 z M 16.435547 27.027344 C 15.143818 27.027344 14.083984 28.085184 14.083984 29.376953 L 14.083984 30.607422 L 13.570312 30.607422 C 13.375452 30.607422 13.210603 30.704118 13.119141 30.791016 C 13.027691 30.877916 12.983569 30.958238 12.951172 31.03125 C 12.886382 31.177277 12.867184 31.304789 12.867188 31.441406 L 12.867188 32.523438 L 12.867188 33.119141 L 12.867188 34.677734 L 12.867188 35.509766 L 13.570312 35.509766 L 19.472656 35.509766 L 20.173828 35.509766 L 20.173828 34.677734 L 20.173828 32.523438 L 20.173828 31.441406 C 20.173828 31.304794 20.156597 31.177281 20.091797 31.03125 C 20.059397 30.95824 20.015299 30.877916 19.923828 30.791016 C 19.832368 30.704116 19.667509 30.607422 19.472656 30.607422 L 18.927734 30.607422 L 18.927734 29.376953 C 18.927734 28.085184 17.867902 27.027344 16.576172 27.027344 L 16.435547 27.027344 z M 16.435547 28.220703 L 16.576172 28.220703 C 17.22782 28.220703 17.734375 28.725101 17.734375 29.376953 L 17.734375 30.607422 L 15.277344 30.607422 L 15.277344 29.376953 C 15.277344 28.725101 15.7839 28.220703 16.435547 28.220703 z M 13.109375 29.150391 L 8.9199219 32.509766 C 8.6599219 32.689766 8.6599219 33.109063 8.9199219 33.289062 L 11.869141 35.648438 L 11.869141 34.677734 L 11.869141 33.119141 L 11.869141 32.523438 L 11.869141 31.441406 C 11.869141 31.217489 11.912641 30.907486 12.037109 30.626953 C 12.093758 30.499284 12.228597 30.257492 12.429688 30.066406 C 12.580253 29.92335 12.859197 29.887344 13.085938 29.802734 L 13.085938 29.378906 C 13.085938 29.300761 13.104 29.227272 13.109375 29.150391 z M 16.435547 29.220703 C 16.301234 29.220703 16.277344 29.244432 16.277344 29.378906 L 16.277344 29.607422 L 16.734375 29.607422 L 16.734375 29.378906 C 16.734375 29.244433 16.712442 29.220703 16.578125 29.220703 L 16.435547 29.220703 z M 12.943359 36.509766 L 13.820312 37.210938 C 14.090314 37.460938 14.639141 37.210078 14.619141 36.830078 L 14.619141 36.509766 L 13.570312 36.509766 L 12.943359 36.509766 z M 10.330078 38.650391 L 10.339844 38.660156 C 10.099844 38.680156 9.9001562 38.910156 9.9101562 39.160156 L 9.9101562 41.630859 L 7.3007812 41.630859 C 6.2207812 41.630859 5.4403906 41.589844 4.9003906 41.589844 C 3.2603906 41.589844 3.6503906 42.020234 3.6503906 45.240234 L 3.6503906 49.710938 C 3.6503906 53.370936 3.4202344 53.409141 5.9902344 53.369141 L 4.6503906 52.269531 C 4.5503906 52.189531 4.4692187 52.070703 4.4492188 51.970703 C 4.4492188 51.800703 4.5203906 51.619531 4.6503906 51.519531 L 6.609375 49.919922 C 6.579375 49.859922 6.5703125 49.790703 6.5703125 49.720703 L 6.5703125 45.240234 C 6.5703125 44.800234 6.8490625 44.519531 7.2890625 44.519531 L 9.9003906 44.519531 L 9.9003906 47.019531 C 9.9003906 47.379531 10.399219 47.620391 10.699219 47.400391 L 15.630859 43.470703 C 15.870859 43.290703 15.870859 42.869453 15.630859 42.689453 L 10.689453 38.769531 C 10.589453 38.689531 10.460078 38.640391 10.330078 38.650391 z M 16.869141 41.585938 C 16.616211 41.581522 16.322969 41.584844 15.980469 41.589844 L 15.970703 41.589844 L 17.310547 42.689453 C 17.410547 42.759453 17.489766 42.889531 17.509766 43.019531 C 17.529766 43.179531 17.479609 43.319922 17.349609 43.419922 L 15.390625 45.019531 C 15.406724 45.075878 15.427133 45.132837 15.4375 45.197266 C 15.754974 45.096169 16.086404 45.027344 16.435547 45.027344 L 16.578125 45.027344 C 17.24129 45.027344 17.858323 45.230088 18.380859 45.568359 L 18.380859 45.25 C 18.380859 42.0475 18.639648 41.616836 16.869141 41.585938 z M 16.435547 46.027344 C 15.143818 46.027344 14.083984 47.085184 14.083984 48.376953 L 14.083984 49.607422 L 13.570312 49.607422 C 13.375448 49.607422 13.210603 49.704118 13.119141 49.791016 C 13.027691 49.877916 12.983569 49.958238 12.951172 50.03125 C 12.886382 50.177277 12.867187 50.304789 12.867188 50.441406 L 12.867188 51.523438 L 12.867188 52.119141 L 12.867188 53.677734 L 12.867188 54.509766 L 13.570312 54.509766 L 19.472656 54.509766 L 20.173828 54.509766 L 20.173828 53.677734 L 20.173828 51.523438 L 20.173828 50.441406 C 20.173828 50.304794 20.156597 50.177281 20.091797 50.03125 C 20.059397 49.95824 20.015299 49.877916 19.923828 49.791016 C 19.832368 49.704116 19.667509 49.607422 19.472656 49.607422 L 18.927734 49.607422 L 18.927734 48.376953 C 18.927734 47.085184 17.867902 46.027344 16.576172 46.027344 L 16.435547 46.027344 z M 16.435547 47.220703 L 16.576172 47.220703 C 17.22782 47.220703 17.734375 47.725101 17.734375 48.376953 L 17.734375 49.607422 L 15.277344 49.607422 L 15.277344 48.376953 C 15.277344 47.725101 15.7839 47.220703 16.435547 47.220703 z M 11.470703 47.490234 C 11.410703 47.510234 11.349063 47.539844 11.289062 47.589844 L 6.3496094 51.519531 C 6.1096094 51.699531 6.1096094 52.120781 6.3496094 52.300781 L 11.289062 56.220703 C 11.569064 56.440703 12.070312 56.199844 12.070312 55.839844 L 12.070312 55.509766 L 11.869141 55.509766 L 11.869141 53.677734 L 11.869141 52.119141 L 11.869141 51.523438 L 11.869141 50.441406 C 11.869141 50.217489 11.912641 49.907486 12.037109 49.626953 C 12.043809 49.611855 12.061451 49.584424 12.070312 49.566406 L 12.070312 47.960938 C 12.070312 47.660938 11.770703 47.430234 11.470703 47.490234 z M 16.435547 48.220703 C 16.301234 48.220703 16.277344 48.244432 16.277344 48.378906 L 16.277344 48.607422 L 16.734375 48.607422 L 16.734375 48.378906 C 16.734375 48.244433 16.712442 48.220703 16.578125 48.220703 L 16.435547 48.220703 z M 13.060547 57.650391 L 13.060547 57.660156 C 12.830547 57.690156 12.660156 57.920156 12.660156 58.160156 L 12.660156 60.630859 L 7.2792969 60.630859 C 6.1992969 60.630859 5.4208594 60.589844 4.8808594 60.589844 C 3.2408594 60.589844 3.6308594 61.020234 3.6308594 64.240234 L 3.6308594 69.109375 L 6.5605469 66.740234 L 6.5605469 64.240234 C 6.5605469 63.800234 6.8392969 63.519531 7.2792969 63.519531 L 12.660156 63.519531 L 12.660156 66.019531 C 12.660156 66.299799 12.960394 66.500006 13.226562 66.474609 C 13.625751 65.076914 14.904956 64.035678 16.421875 64.029297 L 18.380859 62.470703 C 18.620859 62.290703 18.620859 61.869453 18.380859 61.689453 L 13.439453 57.769531 C 13.339453 57.669531 13.200547 57.630391 13.060547 57.650391 z M 18.359375 63.810547 L 17.800781 64.269531 C 18.004793 64.350836 18.198411 64.450249 18.380859 64.568359 L 18.380859 64.25 L 18.380859 63.810547 L 18.359375 63.810547 z M 16.435547 65.027344 C 15.143818 65.027344 14.083984 66.085184 14.083984 67.376953 L 14.083984 68.607422 L 13.570312 68.607422 C 13.375448 68.607422 13.210603 68.704118 13.119141 68.791016 C 13.027691 68.877916 12.983569 68.958238 12.951172 69.03125 C 12.886382 69.177277 12.867187 69.304789 12.867188 69.441406 L 12.867188 70.523438 L 12.867188 71.119141 L 12.867188 72.677734 L 12.867188 73.509766 L 13.570312 73.509766 L 19.472656 73.509766 L 20.173828 73.509766 L 20.173828 72.677734 L 20.173828 70.523438 L 20.173828 69.441406 C 20.173828 69.304794 20.156597 69.177281 20.091797 69.03125 C 20.059397 68.95824 20.015299 68.877916 19.923828 68.791016 C 19.832368 68.704116 19.667509 68.607422 19.472656 68.607422 L 18.927734 68.607422 L 18.927734 67.376953 C 18.927734 66.085184 17.867902 65.027344 16.576172 65.027344 L 16.435547 65.027344 z M 16.435547 66.220703 L 16.576172 66.220703 C 17.22782 66.220703 17.734375 66.725101 17.734375 67.376953 L 17.734375 68.607422 L 15.277344 68.607422 L 15.277344 67.376953 C 15.277344 66.725101 15.7839 66.220703 16.435547 66.220703 z M 8.7207031 66.509766 C 8.6507031 66.529766 8.5895312 66.559375 8.5195312 66.609375 L 3.5996094 70.519531 C 3.3496094 70.699531 3.3496094 71.120781 3.5996094 71.300781 L 8.5292969 75.220703 C 8.8092969 75.440703 9.3105469 75.199844 9.3105469 74.839844 L 9.3105469 72.339844 L 11.869141 72.339844 L 11.869141 71.119141 L 11.869141 70.523438 L 11.869141 69.449219 L 9.3203125 69.449219 L 9.3203125 66.980469 C 9.3203125 66.680469 9.0007031 66.449766 8.7207031 66.509766 z M 16.435547 67.220703 C 16.301234 67.220703 16.277344 67.244432 16.277344 67.378906 L 16.277344 67.607422 L 16.734375 67.607422 L 16.734375 67.378906 C 16.734375 67.244433 16.712442 67.220703 16.578125 67.220703 L 16.435547 67.220703 z M 19.248047 78.800781 C 19.148558 78.831033 19.050295 78.90106 18.970703 78.970703 L 18.070312 79.869141 C 17.630312 79.569141 16.710703 79.619141 14.720703 79.619141 L 7.2792969 79.619141 C 6.1992969 79.619141 5.4208594 79.589844 4.8808594 79.589844 C 3.2408594 79.589844 3.6308594 80.020234 3.6308594 83.240234 L 3.6308594 83.939453 L 6.5605469 84.240234 L 6.5605469 83.240234 C 6.5605469 82.800234 6.8392969 82.519531 7.2792969 82.519531 L 14.720703 82.519531 C 14.920703 82.519531 15.090703 82.600703 15.220703 82.720703 L 13.419922 84.519531 C 13.279464 84.665607 13.281282 84.881022 13.363281 85.054688 C 13.880838 83.867655 15.067337 83.027344 16.435547 83.027344 L 16.578125 83.027344 C 18.290465 83.027344 19.703357 84.345788 19.890625 86.011719 L 19.960938 86.019531 C 20.240938 86.049531 20.520234 85.770234 20.490234 85.490234 L 19.789062 79.240234 C 19.789062 78.973661 19.498025 78.767523 19.25 78.800781 L 19.248047 78.800781 z M 16.435547 84.027344 C 15.143818 84.027344 14.083984 85.085184 14.083984 86.376953 L 14.083984 87.607422 L 13.570312 87.607422 C 13.375448 87.607422 13.210603 87.704118 13.119141 87.791016 C 13.027691 87.877916 12.983569 87.958238 12.951172 88.03125 C 12.886382 88.177277 12.867187 88.304789 12.867188 88.441406 L 12.867188 89.523438 L 12.867188 90.119141 L 12.867188 91.677734 L 12.867188 92.509766 L 13.570312 92.509766 L 19.472656 92.509766 L 20.173828 92.509766 L 20.173828 91.677734 L 20.173828 89.523438 L 20.173828 88.441406 C 20.173828 88.304794 20.156597 88.177281 20.091797 88.03125 C 20.059397 87.95824 20.015299 87.877916 19.923828 87.791016 C 19.832368 87.704116 19.667509 87.607422 19.472656 87.607422 L 18.927734 87.607422 L 18.927734 86.376953 C 18.927734 85.085184 17.867902 84.027344 16.576172 84.027344 L 16.435547 84.027344 z M 2.0507812 84.900391 C 1.8507824 84.970391 1.6907031 85.199453 1.7207031 85.439453 L 2.4199219 91.689453 C 2.4399219 92.049453 3 92.240929 3.25 91.960938 L 4.0507812 91.160156 C 4.0707812 91.160156 4.0898437 91.140156 4.0898438 91.160156 C 4.5498437 91.400156 5.4595313 91.330078 7.2695312 91.330078 L 11.869141 91.330078 L 11.869141 90.119141 L 11.869141 89.523438 L 11.869141 88.441406 C 11.869141 88.437991 11.871073 88.433136 11.871094 88.429688 L 7.2792969 88.429688 C 7.1292969 88.429688 6.9808594 88.400078 6.8808594 88.330078 L 8.8007812 86.400391 C 9.1007822 86.160391 8.8992969 85.600547 8.5292969 85.560547 L 2.25 84.910156 L 2.0507812 84.910156 L 2.0507812 84.900391 z M 16.435547 85.220703 L 16.576172 85.220703 C 17.22782 85.220703 17.734375 85.725101 17.734375 86.376953 L 17.734375 87.607422 L 15.277344 87.607422 L 15.277344 86.376953 C 15.277344 85.725101 15.7839 85.220703 16.435547 85.220703 z M 4.8808594 98.599609 C 3.5508594 98.599609 3.5400781 99.080402 3.5800781 100.90039 L 4.7207031 99.529297 C 4.8007031 99.429297 4.9405469 99.360078 5.0605469 99.330078 C 5.2205469 99.330078 5.4 99.409297 5.5 99.529297 L 7.1601562 101.56055 C 7.2001563 101.56055 7.2292969 101.5293 7.2792969 101.5293 L 14.720703 101.5293 C 15.060703 101.5293 15.289141 101.7293 15.369141 102.0293 L 12.939453 102.0293 C 12.599453 102.0793 12.410625 102.55055 12.640625 102.81055 L 13.470703 103.85742 C 14.029941 102.77899 15.146801 102.02734 16.435547 102.02734 L 16.578125 102.02734 C 18.158418 102.02734 19.491598 103.14879 19.835938 104.63086 L 21.279297 102.82031 C 21.499297 102.55031 21.260156 102.06078 20.910156 102.05078 L 18.400391 102.05078 C 18.420391 98.150792 19.000234 98.650391 14.740234 98.650391 L 7.2792969 98.650391 C 6.1992969 98.650391 5.4208594 98.609375 4.8808594 98.609375 L 4.8808594 98.599609 z M 5.0292969 101.06055 C 4.9292969 101.09055 4.83 101.15977 4.75 101.25977 L 0.81054688 106.16016 C 0.61054688 106.44016 0.8409375 106.92945 1.2109375 106.93945 L 3.5996094 106.93945 C 3.5796094 110.87945 3.1497656 110.33984 7.2597656 110.33984 L 11.869141 110.33984 L 11.869141 109.11914 L 11.869141 108.52344 L 11.869141 107.44141 L 11.869141 107.43945 L 7.2792969 107.43945 C 6.9292969 107.43945 6.7091406 107.23945 6.6191406 106.93945 L 9.0605469 106.93945 C 9.4305469 106.93945 9.6909375 106.44016 9.4609375 106.16016 L 5.5 101.25977 C 5.4 101.10977 5.1992969 101.03055 5.0292969 101.06055 z M 16.435547 103.02734 C 15.143818 103.02734 14.083984 104.08518 14.083984 105.37695 L 14.083984 106.60742 L 13.570312 106.60742 C 13.375448 106.60742 13.210603 106.70409 13.119141 106.79102 C 13.027691 106.87792 12.983569 106.95823 12.951172 107.03125 C 12.886382 107.17727 12.867187 107.30479 12.867188 107.44141 L 12.867188 108.52344 L 12.867188 109.11914 L 12.867188 110.67773 L 12.867188 111.50977 L 13.570312 111.50977 L 19.472656 111.50977 L 20.173828 111.50977 L 20.173828 110.67773 L 20.173828 108.52344 L 20.173828 107.44141 C 20.173828 107.3048 20.156597 107.17728 20.091797 107.03125 C 20.059397 106.95825 20.015299 106.87792 19.923828 106.79102 C 19.832368 106.70412 19.667509 106.60742 19.472656 106.60742 L 18.927734 106.60742 L 18.927734 105.37695 C 18.927734 104.08518 17.867902 103.02734 16.576172 103.02734 L 16.435547 103.02734 z M 16.435547 104.2207 L 16.576172 104.2207 C 17.22782 104.2207 17.734375 104.7251 17.734375 105.37695 L 17.734375 106.60742 L 15.277344 106.60742 L 15.277344 105.37695 C 15.277344 104.7251 15.7839 104.2207 16.435547 104.2207 z M 16.435547 105.2207 C 16.301234 105.2207 16.277344 105.24444 16.277344 105.37891 L 16.277344 105.60742 L 16.734375 105.60742 L 16.734375 105.37891 C 16.734375 105.24441 16.712442 105.2207 16.578125 105.2207 L 16.435547 105.2207 z M 4.8808594 117.58984 L 4.8808594 117.59961 C 3.7208594 117.59961 3.5800781 117.90016 3.5800781 119.16016 L 4.7207031 117.7793 C 4.8007031 117.6793 4.9405469 117.63914 5.0605469 117.61914 C 5.2205469 117.61914 5.4 117.6593 5.5 117.7793 L 7.7207031 120.5293 L 14.720703 120.5293 C 15.123595 120.5293 15.408576 120.79174 15.431641 121.20117 C 15.750992 121.09876 16.08404 121.02734 16.435547 121.02734 L 16.578125 121.02734 C 17.24903 121.02734 17.874081 121.23262 18.400391 121.57812 L 18.400391 121.25 C 18.400391 117.05 19.120234 117.61914 14.740234 117.61914 L 7.2792969 117.61914 C 6.1992969 117.61914 5.4208594 117.58984 4.8808594 117.58984 z M 4.9804688 119.33984 C 4.8804688 119.36984 4.81 119.44 4.75 119.5 L 0.80078125 124.43945 C 0.60078125 124.71945 0.8292182 125.2107 1.1992188 125.2207 L 3.5996094 125.2207 L 3.5996094 125.7207 C 3.5996094 129.9807 3.0497656 129.33984 7.2597656 129.33984 L 11.869141 129.33984 L 11.869141 128.11914 L 11.869141 127.52344 L 11.869141 126.44141 C 11.869141 126.43799 11.871073 126.43314 11.871094 126.42969 L 7.2792969 126.42969 C 6.8392969 126.42969 6.5605469 126.13094 6.5605469 125.71094 L 6.5605469 125.21094 L 9.0605469 125.21094 C 9.4305469 125.23094 9.6909375 124.70969 9.4609375 124.42969 L 5.5 119.5 C 5.3820133 119.35252 5.1682348 119.28513 4.9804688 119.33984 z M 12.839844 121.7793 C 12.539844 121.8793 12.410625 122.32055 12.640625 122.56055 L 13.267578 123.34375 C 13.473522 122.72168 13.852237 122.1828 14.353516 121.7793 L 12.839844 121.7793 z M 18.658203 121.7793 C 19.393958 122.37155 19.878978 123.25738 19.916016 124.25781 L 21.279297 122.56055 C 21.499297 122.28055 21.260156 121.7893 20.910156 121.7793 L 18.658203 121.7793 z M 16.435547 122.02734 C 15.143818 122.02734 14.083984 123.08518 14.083984 124.37695 L 14.083984 125.60742 L 13.570312 125.60742 C 13.375448 125.60742 13.210603 125.70409 13.119141 125.79102 C 13.027691 125.87792 12.983569 125.95823 12.951172 126.03125 C 12.886382 126.17727 12.867187 126.30479 12.867188 126.44141 L 12.867188 127.52344 L 12.867188 128.11914 L 12.867188 129.67773 L 12.867188 130.50977 L 13.570312 130.50977 L 19.472656 130.50977 L 20.173828 130.50977 L 20.173828 129.67773 L 20.173828 127.52344 L 20.173828 126.44141 C 20.173828 126.3048 20.156597 126.17728 20.091797 126.03125 C 20.059397 125.95825 20.015299 125.87792 19.923828 125.79102 C 19.832368 125.70412 19.667509 125.60742 19.472656 125.60742 L 18.927734 125.60742 L 18.927734 124.37695 C 18.927734 123.08518 17.867902 122.02734 16.576172 122.02734 L 16.435547 122.02734 z M 16.435547 123.2207 L 16.576172 123.2207 C 17.22782 123.2207 17.734375 123.7251 17.734375 124.37695 L 17.734375 125.60742 L 15.277344 125.60742 L 15.277344 124.37695 C 15.277344 123.7251 15.7839 123.2207 16.435547 123.2207 z M 16.435547 124.2207 C 16.301234 124.2207 16.277344 124.24444 16.277344 124.37891 L 16.277344 124.60742 L 16.734375 124.60742 L 16.734375 124.37891 C 16.734375 124.24441 16.712442 124.2207 16.578125 124.2207 L 16.435547 124.2207 z M 5.9394531 136.58984 L 5.9394531 136.59961 L 8.3105469 139.5293 L 14.730469 139.5293 C 15.131912 139.5293 15.414551 139.79039 15.439453 140.19727 C 15.756409 140.09653 16.087055 140.02734 16.435547 140.02734 L 16.578125 140.02734 C 17.24903 140.02734 17.874081 140.23261 18.400391 140.57812 L 18.400391 140.25 C 18.400391 136.05 19.120234 136.61914 14.740234 136.61914 L 7.2792969 136.61914 C 6.6792969 136.61914 6.3594531 136.59984 5.9394531 136.58984 z M 4.2207031 136.66016 C 3.8207031 136.74016 3.6791406 136.96016 3.6191406 137.41016 L 4.2207031 136.66992 L 4.2207031 136.66016 z M 5.0605469 137.57031 L 5.0605469 137.58984 C 4.9405469 137.58984 4.8197656 137.66953 4.7597656 137.76953 L 0.81054688 142.66992 C 0.57054688 142.96992 0.8109375 143.50023 1.2109375 143.49023 L 3.5996094 143.49023 L 3.5996094 144.71094 C 3.5996094 148.97094 3.0497656 148.33008 7.2597656 148.33008 L 11.869141 148.33008 L 11.869141 147.11914 L 11.869141 146.52344 L 11.869141 145.44141 C 11.869141 145.43799 11.871073 145.43314 11.871094 145.42969 L 7.2792969 145.42969 C 6.8392969 145.42969 6.5605469 145.13094 6.5605469 144.71094 L 6.5605469 143.49023 L 9.0605469 143.49023 C 9.4605469 143.53023 9.7309375 142.95945 9.4609375 142.68945 L 5.5 137.76953 C 5.4 137.63953 5.2305469 137.57031 5.0605469 137.57031 z M 16.435547 141.02734 C 15.143818 141.02734 14.083984 142.08518 14.083984 143.37695 L 14.083984 144.60742 L 13.570312 144.60742 C 13.375448 144.60742 13.210603 144.70409 13.119141 144.79102 C 13.027691 144.87792 12.983569 144.95823 12.951172 145.03125 C 12.886382 145.17727 12.867187 145.30479 12.867188 145.44141 L 12.867188 146.52344 L 12.867188 147.11914 L 12.867188 148.67773 L 12.867188 149.50977 L 13.570312 149.50977 L 19.472656 149.50977 L 20.173828 149.50977 L 20.173828 148.67773 L 20.173828 146.52344 L 20.173828 145.44141 C 20.173828 145.3048 20.156597 145.17728 20.091797 145.03125 C 20.059397 144.95825 20.015299 144.87792 19.923828 144.79102 C 19.832368 144.70412 19.667509 144.60742 19.472656 144.60742 L 18.927734 144.60742 L 18.927734 143.37695 C 18.927734 142.08518 17.867902 141.02734 16.576172 141.02734 L 16.435547 141.02734 z M 12.849609 141.5 C 12.549609 141.6 12.420391 142.0393 12.650391 142.2793 L 13.136719 142.88672 C 13.213026 142.38119 13.390056 141.90696 13.667969 141.5 L 12.849609 141.5 z M 19.34375 141.5 C 19.710704 142.03735 19.927734 142.68522 19.927734 143.37891 L 19.927734 143.79102 C 19.965561 143.80421 20.005506 143.81448 20.044922 143.82617 L 21.289062 142.2793 C 21.509062 141.9993 21.269922 141.51 20.919922 141.5 L 19.34375 141.5 z M 16.435547 142.2207 L 16.576172 142.2207 C 17.22782 142.2207 17.734375 142.7251 17.734375 143.37695 L 17.734375 144.60742 L 15.277344 144.60742 L 15.277344 143.37695 C 15.277344 142.7251 15.7839 142.2207 16.435547 142.2207 z M 16.435547 143.2207 C 16.301234 143.2207 16.277344 143.24444 16.277344 143.37891 L 16.277344 143.60742 L 16.734375 143.60742 L 16.734375 143.37891 C 16.734375 143.24441 16.712442 143.2207 16.578125 143.2207 L 16.435547 143.2207 z M 17.130859 155.59961 C 16.580859 155.57961 15.810469 155.63086 14.730469 155.63086 L 6.5292969 155.63086 L 8.9101562 158.5293 L 14.730469 158.5293 C 15.131912 158.5293 15.414551 158.79039 15.439453 159.19727 C 15.756409 159.09653 16.087055 159.02734 16.435547 159.02734 L 16.578125 159.02734 C 17.24903 159.02734 17.874081 159.23261 18.400391 159.57812 L 18.400391 159.25977 C 18.400391 156.10977 18.800391 155.63961 17.150391 155.59961 L 17.130859 155.59961 z M 5.0292969 155.86914 L 5.0292969 155.88086 C 4.9292969 155.90086 4.83 155.98055 4.75 156.06055 L 0.81054688 160.96094 C 0.61054688 161.26094 0.8409375 161.73977 1.2109375 161.75977 L 3.5996094 161.75977 L 3.5996094 163.7207 C 3.5996094 167.9807 3.0497656 167.33984 7.2597656 167.33984 L 11.869141 167.33984 L 11.869141 166.11914 L 11.869141 165.52344 L 11.869141 164.44141 L 11.869141 164.43945 L 7.2792969 164.43945 C 6.8392969 164.43945 6.5605469 164.1407 6.5605469 163.7207 L 6.5605469 161.75 L 9.0605469 161.75 C 9.4305469 161.77 9.6909375 161.2507 9.4609375 160.9707 L 5.5 156.07031 C 5.4 155.92031 5.1992969 155.84914 5.0292969 155.86914 z M 16.435547 160.02734 C 15.143818 160.02734 14.083984 161.08518 14.083984 162.37695 L 14.083984 163.60742 L 13.570312 163.60742 C 13.375448 163.60742 13.210603 163.70409 13.119141 163.79102 C 13.027691 163.87792 12.983569 163.95823 12.951172 164.03125 C 12.886382 164.17727 12.867187 164.30479 12.867188 164.44141 L 12.867188 165.52344 L 12.867188 166.11914 L 12.867188 167.67773 L 12.867188 168.50977 L 13.570312 168.50977 L 19.472656 168.50977 L 20.173828 168.50977 L 20.173828 167.67773 L 20.173828 165.52344 L 20.173828 164.44141 C 20.173828 164.3048 20.156597 164.17728 20.091797 164.03125 C 20.059397 163.95825 20.015299 163.87792 19.923828 163.79102 C 19.832368 163.70412 19.667509 163.60742 19.472656 163.60742 L 18.927734 163.60742 L 18.927734 162.37695 C 18.927734 161.08518 17.867902 160.02734 16.576172 160.02734 L 16.435547 160.02734 z M 12.900391 161.2207 C 12.580391 161.2807 12.419141 161.74 12.619141 162 L 13.085938 162.58594 L 13.085938 162.37891 C 13.085938 161.97087 13.170592 161.58376 13.306641 161.2207 L 12.900391 161.2207 z M 16.435547 161.2207 L 16.576172 161.2207 C 17.22782 161.2207 17.734375 161.7251 17.734375 162.37695 L 17.734375 163.60742 L 15.277344 163.60742 L 15.277344 162.37695 C 15.277344 161.7251 15.7839 161.2207 16.435547 161.2207 z M 19.708984 161.23047 C 19.842743 161.59081 19.927734 161.97449 19.927734 162.37891 L 19.927734 162.79102 C 20.119162 162.85779 20.322917 162.91147 20.484375 163 L 21.279297 162.00977 C 21.499297 161.72977 21.260156 161.24047 20.910156 161.23047 L 19.708984 161.23047 z M 16.435547 162.2207 C 16.301234 162.2207 16.277344 162.24444 16.277344 162.37891 L 16.277344 162.60742 L 16.734375 162.60742 L 16.734375 162.37891 C 16.734375 162.24441 16.712442 162.2207 16.578125 162.2207 L 16.435547 162.2207 z M 5.0996094 174.49023 L 5.1308594 174.5 C 4.9808594 174.5 4.83 174.56922 4.75 174.69922 L 0.80078125 179.59961 C 0.56078125 179.86961 0.7992182 180.42039 1.1992188 180.40039 L 3.5996094 180.40039 L 3.5996094 182.7207 C 3.5996094 186.9807 3.0497656 186.33984 7.2597656 186.33984 L 11.869141 186.33984 L 11.869141 185.11914 L 11.869141 184.52344 L 11.869141 183.44141 L 11.869141 183.43945 L 7.25 183.43945 C 6.82 183.43945 6.5507814 183.1407 6.5507812 182.7207 L 6.5507812 180.41992 L 9.0507812 180.41992 C 9.4307824 180.44992 9.7092187 179.87984 9.4492188 179.58984 L 5.4804688 174.68945 C 5.3804688 174.55945 5.2496094 174.49023 5.0996094 174.49023 z M 17.150391 174.58008 L 17.130859 174.59961 C 16.580859 174.57961 15.810469 174.63086 14.730469 174.63086 L 6.8300781 174.63086 L 9.1796875 177.5293 L 14.699219 177.5293 C 15.104107 177.5293 15.391475 177.79407 15.412109 178.20703 C 15.737096 178.1006 16.076913 178.02734 16.435547 178.02734 L 16.578125 178.02734 C 17.24903 178.02734 17.874081 178.2326 18.400391 178.57812 L 18.400391 178.24023 C 18.400391 175.09023 18.800391 174.62008 17.150391 174.58008 z M 16.435547 179.02734 C 15.143818 179.02734 14.083984 180.08518 14.083984 181.37695 L 14.083984 182.60742 L 13.570312 182.60742 C 13.375448 182.60742 13.210603 182.70409 13.119141 182.79102 C 13.027691 182.87792 12.983569 182.95823 12.951172 183.03125 C 12.886382 183.17727 12.867187 183.30479 12.867188 183.44141 L 12.867188 184.52344 L 12.867188 185.11914 L 12.867188 186.67773 L 12.867188 187.50977 L 13.570312 187.50977 L 19.472656 187.50977 L 20.173828 187.50977 L 20.173828 186.67773 L 20.173828 184.52344 L 20.173828 183.44141 C 20.173828 183.3048 20.156597 183.17728 20.091797 183.03125 C 20.059397 182.95825 20.015299 182.87792 19.923828 182.79102 C 19.832368 182.70412 19.667509 182.60742 19.472656 182.60742 L 18.927734 182.60742 L 18.927734 181.37695 C 18.927734 180.08518 17.867902 179.02734 16.576172 179.02734 L 16.435547 179.02734 z M 16.435547 180.2207 L 16.576172 180.2207 C 17.22782 180.2207 17.734375 180.7251 17.734375 181.37695 L 17.734375 182.60742 L 15.277344 182.60742 L 15.277344 181.37695 C 15.277344 180.7251 15.7839 180.2207 16.435547 180.2207 z M 19.816406 180.57031 C 19.882311 180.83091 19.927734 181.09907 19.927734 181.37891 L 19.927734 181.79102 C 20.168811 181.87511 20.455966 181.91694 20.613281 182.06641 C 20.630645 182.0829 20.639883 182.10199 20.65625 182.11914 L 21.259766 181.36914 C 21.479766 181.06914 21.240625 180.59031 20.890625 180.57031 L 19.816406 180.57031 z M 12.820312 180.58984 C 12.520316 180.68984 12.389141 181.11914 12.619141 181.36914 L 12.990234 181.83203 C 13.022029 181.82207 13.055579 181.81406 13.085938 181.80273 L 13.085938 181.37891 C 13.085938 181.10616 13.128698 180.84442 13.191406 180.58984 L 12.820312 180.58984 z M 16.435547 181.2207 C 16.301234 181.2207 16.277344 181.24444 16.277344 181.37891 L 16.277344 181.60742 L 16.734375 181.60742 L 16.734375 181.37891 C 16.734375 181.24441 16.712442 181.2207 16.578125 181.2207 L 16.435547 181.2207 z M 4.9609375 193.15039 L 4.9707031 193.16016 C 4.8707031 193.19016 4.8 193.25984 4.75 193.33984 L 0.81054688 198.24023 C 0.61054688 198.54023 0.8409375 199.01906 1.2109375 199.03906 L 3.5996094 199.03906 L 3.5996094 201.7207 C 3.5996094 205.9807 3.0497656 205.33984 7.2597656 205.33984 L 11.869141 205.33984 L 11.869141 204.11914 L 11.869141 203.52344 L 11.869141 202.44141 C 11.869141 202.44141 11.869141 202.43945 11.869141 202.43945 L 7.2695312 202.43945 C 6.8295312 202.43945 6.5507814 202.1407 6.5507812 201.7207 L 6.5507812 199.01953 L 9.0507812 199.01953 C 9.4207814 199.04953 9.6792188 198.54 9.4492188 198.25 L 5.4902344 193.34961 C 5.3702344 193.17961 5.1509375 193.10039 4.9609375 193.15039 z M 17.150391 193.58008 L 17.130859 193.58984 C 16.580859 193.56984 15.810469 193.61914 14.730469 193.61914 L 7.0996094 193.61914 L 9.4199219 196.46094 L 9.4492188 196.51953 L 14.699219 196.51953 C 15.106887 196.51953 15.397075 196.78718 15.414062 197.20508 C 15.738375 197.09913 16.077769 197.02734 16.435547 197.02734 L 16.578125 197.02734 C 17.24903 197.02734 17.874081 197.23259 18.400391 197.57812 L 18.400391 197.24023 C 18.400391 194.09023 18.800391 193.62008 17.150391 193.58008 z M 16.435547 198.02734 C 15.143818 198.02734 14.083984 199.08518 14.083984 200.37695 L 14.083984 201.60742 L 13.570312 201.60742 C 13.375448 201.60742 13.210603 201.70409 13.119141 201.79102 C 13.027691 201.87792 12.983569 201.95823 12.951172 202.03125 C 12.886382 202.17727 12.867187 202.30479 12.867188 202.44141 L 12.867188 203.52344 L 12.867188 204.11914 L 12.867188 205.67773 L 12.867188 206.50977 L 13.570312 206.50977 L 19.472656 206.50977 L 20.173828 206.50977 L 20.173828 205.67773 L 20.173828 203.52344 L 20.173828 202.44141 C 20.173828 202.3048 20.156597 202.17728 20.091797 202.03125 C 20.059397 201.95825 20.015299 201.87792 19.923828 201.79102 C 19.832368 201.70412 19.667509 201.60742 19.472656 201.60742 L 18.927734 201.60742 L 18.927734 200.37695 C 18.927734 199.08518 17.867902 198.02734 16.576172 198.02734 L 16.435547 198.02734 z M 16.435547 199.2207 L 16.576172 199.2207 C 17.22782 199.2207 17.734375 199.7251 17.734375 200.37695 L 17.734375 201.60742 L 15.277344 201.60742 L 15.277344 200.37695 C 15.277344 199.7251 15.7839 199.2207 16.435547 199.2207 z M 12.919922 199.93945 C 12.559922 199.95945 12.359141 200.48023 12.619141 200.74023 L 12.751953 200.9043 C 12.862211 200.87013 12.980058 200.84224 13.085938 200.80273 L 13.085938 200.37891 C 13.085938 200.22863 13.111295 200.08474 13.130859 199.93945 L 12.919922 199.93945 z M 19.882812 199.93945 C 19.902378 200.08474 19.927734 200.22863 19.927734 200.37891 L 19.927734 200.79102 C 20.168811 200.87511 20.455966 200.91694 20.613281 201.06641 C 20.691227 201.14046 20.749315 201.22305 20.806641 201.30273 L 21.259766 200.74023 C 21.519766 200.46023 21.260625 199.90945 20.890625 199.93945 L 19.882812 199.93945 z M 16.435547 200.2207 C 16.301234 200.2207 16.277344 200.24444 16.277344 200.37891 L 16.277344 200.60742 L 16.734375 200.60742 L 16.734375 200.37891 C 16.734375 200.24441 16.712442 200.2207 16.578125 200.2207 L 16.435547 200.2207 z ' fill='#{hex-color($highlight-text-color)}' stroke-width='0' /></svg>");
+    }
+  }
+
+  &.disabled {
+    i.fa-retweet,
+    &:hover i.fa-retweet {
+      background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' height='209' width='22'><path d='M 18.972656 1.2011719 C 18.829825 1.1881782 18.685932 1.2302188 18.572266 1.3300781 L 15.990234 3.5996094 C 15.58109 3.6070661 15.297269 3.609375 14.730469 3.609375 L 7.0996094 3.609375 L 9.4199219 6.4609375 L 9.4492188 6.5195312 L 12.664062 6.5195312 L 6.5761719 11.867188 C 6.5674697 11.818249 6.5507813 11.773891 6.5507812 11.720703 L 6.5507812 9.0195312 L 9.0507812 9.0195312 C 9.4207813 9.0495313 9.6792188 8.54 9.4492188 8.25 L 5.5 3.3496094 C 5.38 3.1796094 5.1607031 3.1003906 4.9707031 3.1503906 L 4.9707031 3.1601562 C 4.8707031 3.1901563 4.8 3.2598438 4.75 3.3398438 L 0.80078125 8.2402344 C 0.60078125 8.5402344 0.8292187 9.0190625 1.1992188 9.0390625 L 3.5996094 9.0390625 L 3.5996094 11.720703 C 3.5996094 13.045739 3.5690668 13.895038 3.6503906 14.4375 L 2.6152344 15.347656 C 2.3879011 15.547375 2.3754917 15.901081 2.5859375 16.140625 L 3.1464844 16.78125 C 3.3569308 17.020794 3.7101667 17.053234 3.9375 16.853516 L 19.892578 2.8359375 C 20.119911 2.6362188 20.134275 2.282513 19.923828 2.0429688 L 19.361328 1.4023438 C 19.256105 1.282572 19.115488 1.2141655 18.972656 1.2011719 z M 18.410156 6.7753906 L 15.419922 9.4042969 L 15.419922 9.9394531 L 14.810547 9.9394531 L 13.148438 11.400391 L 16.539062 15.640625 C 16.719062 15.890625 17.140313 15.890625 17.320312 15.640625 L 21.259766 10.740234 C 21.519766 10.460234 21.260625 9.9094531 20.890625 9.9394531 L 18.400391 9.9394531 L 18.400391 7.2402344 C 18.400391 7.0470074 18.407711 6.9489682 18.410156 6.7753906 z M 11.966797 12.439453 L 8.6679688 15.339844 L 14.919922 15.339844 L 12.619141 12.5 C 12.589141 12.48 12.590313 12.459453 12.570312 12.439453 L 11.966797 12.439453 z' fill='#{hex-color(darken($action-button-color, 13%))}' stroke-width='0'/></svg>");
+    }
+  }
+
+  .media-modal__overlay .picture-in-picture__footer & {
+    i.fa-retweet {
+      background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='22' height='209'><path d='M4.97 3.16c-.1.03-.17.1-.22.18L.8 8.24c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77L5.5 3.35c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.02-2.4.02H7.1l2.32 2.85.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{hex-color($white)}' stroke-width='0'/><path d='M7.78 19.66c-.24.02-.44.25-.44.5v2.46h-.06c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v4.47c0 4.26-.56 3.62 3.65 3.62H8.5l-1.3-1.06c-.1-.08-.18-.2-.2-.3-.02-.17.06-.35.2-.45l1.33-1.1H7.28c-.44 0-.72-.3-.72-.7v-4.48c0-.44.28-.72.72-.72h.06v2.5c0 .38.54.63.82.38l4.9-3.93c.25-.18.25-.6 0-.78l-4.9-3.92c-.1-.1-.24-.14-.38-.12zm9.34 2.93c-.54-.02-1.3.02-2.4.02h-1.25l1.3 1.07c.1.07.18.2.2.33.02.16-.06.3-.2.4l-1.33 1.1h1.28c.42 0 .72.28.72.72v4.47c0 .42-.3.72-.72.72h-.1v-2.47c0-.3-.3-.53-.6-.47-.07 0-.14.05-.2.1l-4.9 3.93c-.26.18-.26.6 0 .78l4.9 3.92c.27.25.82 0 .8-.38v-2.5h.1c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.15.4-3.62-1.25-3.66zM10.34 38.66c-.24.02-.44.25-.43.5v2.47H7.3c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.47c0 3.66-.23 3.7 2.34 3.66l-1.34-1.1c-.1-.08-.18-.2-.2-.3 0-.17.07-.35.2-.45l1.96-1.6c-.03-.06-.04-.13-.04-.2v-4.48c0-.44.28-.72.72-.72H9.9v2.5c0 .36.5.6.8.38l4.93-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.08-.23-.13-.36-.12zm5.63 2.93l1.34 1.1c.1.07.18.2.2.33.02.16-.03.3-.16.4l-1.96 1.6c.02.07.06.13.06.22v4.47c0 .42-.3.72-.72.72h-2.66v-2.47c0-.3-.3-.53-.6-.47-.06.02-.12.05-.18.1l-4.94 3.93c-.24.18-.24.6 0 .78l4.94 3.92c.28.22.78-.02.78-.38v-2.5h2.66c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.66.34-3.7-2.4-3.66zM13.06 57.66c-.23.03-.4.26-.4.5v2.47H7.28c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.87l2.93-2.37v-2.5c0-.44.28-.72.72-.72h5.38v2.5c0 .36.5.6.78.38l4.94-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.1-.24-.14-.38-.12zm5.3 6.15l-2.92 2.4v2.52c0 .42-.3.72-.72.72h-5.4v-2.47c0-.3-.32-.53-.6-.47-.07.02-.13.05-.2.1L3.6 70.52c-.25.18-.25.6 0 .78l4.93 3.92c.28.22.78-.02.78-.38v-2.5h5.42c4.27 0 3.65.67 3.65-3.62v-4.47-.44zM19.25 78.8c-.1.03-.2.1-.28.17l-.9.9c-.44-.3-1.36-.25-3.35-.25H7.28c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v.7l2.93.3v-1c0-.44.28-.72.72-.72h7.44c.2 0 .37.08.5.2l-1.8 1.8c-.25.26-.08.76.27.8l6.27.7c.28.03.56-.25.53-.53l-.7-6.25c0-.27-.3-.48-.55-.44zm-17.2 6.1c-.2.07-.36.3-.33.54l.7 6.25c.02.36.58.55.83.27l.8-.8c.02 0 .04-.02.04 0 .46.24 1.37.17 3.18.17h7.44c4.27 0 3.65.67 3.65-3.62v-.75l-2.93-.3v1.05c0 .42-.3.72-.72.72H7.28c-.15 0-.3-.03-.4-.1L8.8 86.4c.3-.24.1-.8-.27-.84l-6.28-.65h-.2zM4.88 98.6c-1.33 0-1.34.48-1.3 2.3l1.14-1.37c.08-.1.22-.17.34-.2.16 0 .34.08.44.2l1.66 2.03c.04 0 .07-.03.12-.03h7.44c.34 0 .57.2.65.5h-2.43c-.34.05-.53.52-.3.78l3.92 4.95c.18.24.6.24.78 0l3.94-4.94c.22-.27-.02-.76-.37-.77H18.4c.02-3.9.6-3.4-3.66-3.4H7.28c-1.08 0-1.86-.04-2.4-.04zm.15 2.46c-.1.03-.2.1-.28.2l-3.94 4.9c-.2.28.03.77.4.78H3.6c-.02 3.94-.45 3.4 3.66 3.4h7.44c3.65 0 3.74.3 3.7-2.25l-1.1 1.34c-.1.1-.2.17-.32.2-.16 0-.34-.08-.44-.2l-1.65-2.03c-.06.02-.1.04-.18.04H7.28c-.35 0-.57-.2-.66-.5h2.44c.37 0 .63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.23-.47-.2zM4.88 117.6c-1.16 0-1.3.3-1.3 1.56l1.14-1.38c.08-.1.22-.14.34-.16.16 0 .34.04.44.16l2.22 2.75h7c.42 0 .72.28.72.72v.53h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-.53c0-4.2.72-3.63-3.66-3.63H7.28c-1.08 0-1.86-.03-2.4-.03zm.1 1.74c-.1.03-.17.1-.23.16L.8 124.44c-.2.28.03.77.4.78H3.6v.5c0 4.26-.55 3.62 3.66 3.62h7.44c1.03 0 1.74.02 2.28 0-.16.02-.34-.03-.44-.15l-2.22-2.76H7.28c-.44 0-.72-.3-.72-.72v-.5h2.5c.37.02.63-.5.4-.78L5.5 119.5c-.12-.15-.34-.22-.53-.16zm12.02 10c1.2-.02 1.4-.25 1.4-1.53l-1.1 1.36c-.07.1-.17.17-.3.18zM5.94 136.6l2.37 2.93h6.42c.42 0 .72.28.72.72v1.25h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.25c0-4.2.72-3.63-3.66-3.63H7.28c-.6 0-.92-.02-1.34-.03zm-1.72.06c-.4.08-.54.3-.6.75l.6-.74zm.84.93c-.12 0-.24.08-.3.18l-3.95 4.9c-.24.3 0 .83.4.82H3.6v1.22c0 4.26-.55 3.62 3.66 3.62h7.44c.63 0 .97.02 1.4.03l-2.37-2.93H7.28c-.44 0-.72-.3-.72-.72v-1.22h2.5c.4.04.67-.53.4-.8l-3.96-4.92c-.1-.13-.27-.2-.44-.2zm13.28 10.03l-.56.7c.36-.07.5-.3.56-.7zM17.13 155.6c-.55-.02-1.32.03-2.4.03h-8.2l2.38 2.9h5.82c.42 0 .72.28.72.72v1.97H12.9c-.32.06-.48.52-.28.78l3.94 4.94c.2.23.6.22.78-.03l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.97c0-3.15.4-3.62-1.25-3.66zm-12.1.28c-.1.02-.2.1-.28.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v1.96c0 4.26-.55 3.62 3.66 3.62h8.24l-2.36-2.9H7.28c-.44 0-.72-.3-.72-.72v-1.97h2.5c.37.02.63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.22-.47-.2zM5.13 174.5c-.15 0-.3.07-.38.2L.8 179.6c-.24.27 0 .82.4.8H3.6v2.32c0 4.26-.55 3.62 3.66 3.62h7.94l-2.35-2.9h-5.6c-.43 0-.7-.3-.7-.72v-2.3h2.5c.38.03.66-.54.4-.83l-3.97-4.9c-.1-.13-.23-.2-.38-.2zm12 .1c-.55-.02-1.32.03-2.4.03H6.83l2.35 2.9h5.52c.42 0 .72.28.72.72v2.34h-2.6c-.3.1-.43.53-.2.78l3.92 4.9c.18.24.6.24.78 0l3.94-4.9c.22-.3-.02-.78-.37-.8H18.4v-2.33c0-3.15.4-3.62-1.25-3.66zM4.97 193.16c-.1.03-.17.1-.22.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77l-3.96-4.9c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.03-2.4.03H7.1l2.32 2.84.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{hex-color($highlight-text-color)}' stroke-width='0'/></svg>");
+    }
+
+    &.reblogPrivate {
+      i.fa-retweet {
+        background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' height='209' width='22'><path d='M 4.9707031 3.1503906 L 4.9707031 3.1601562 C 4.8707031 3.1901563 4.8 3.2598438 4.75 3.3398438 L 0.80078125 8.2402344 C 0.60078125 8.5402344 0.8292187 9.0190625 1.1992188 9.0390625 L 3.5996094 9.0390625 L 3.5996094 11.720703 C 3.5996094 15.980703 3.0497656 15.339844 7.2597656 15.339844 L 11.869141 15.339844 L 11.869141 14.119141 L 11.869141 13.523438 L 11.869141 12.441406 C 11.869141 12.441406 11.869141 12.439453 11.869141 12.439453 L 7.2695312 12.439453 C 6.8295312 12.439453 6.5507814 12.140703 6.5507812 11.720703 L 6.5507812 9.0195312 L 9.0507812 9.0195312 C 9.4207813 9.0495313 9.6792188 8.54 9.4492188 8.25 L 5.5 3.3496094 C 5.38 3.1796094 5.1607031 3.1003906 4.9707031 3.1503906 z M 17.150391 3.5800781 L 17.130859 3.5898438 C 16.580859 3.5698436 15.810469 3.609375 14.730469 3.609375 L 7.0996094 3.609375 L 9.4199219 6.4609375 L 9.4492188 6.5195312 L 14.699219 6.5195312 C 15.106887 6.5195312 15.397113 6.7872181 15.414062 7.2050781 C 15.738375 7.0991315 16.077769 7.0273437 16.435547 7.0273438 L 16.578125 7.0273438 C 17.24903 7.0273438 17.874081 7.2325787 18.400391 7.578125 L 18.400391 7.2402344 C 18.400391 4.0902344 18.800391 3.6200781 17.150391 3.5800781 z M 16.435547 8.0273438 C 15.143818 8.0273438 14.083984 9.0851838 14.083984 10.376953 L 14.083984 11.607422 L 13.570312 11.607422 C 13.375448 11.607422 13.210603 11.704118 13.119141 11.791016 C 13.027691 11.877916 12.983569 11.958238 12.951172 12.03125 C 12.886382 12.177277 12.867187 12.304789 12.867188 12.441406 L 12.867188 13.523438 L 12.867188 14.119141 L 12.867188 15.677734 L 12.867188 16.509766 L 13.570312 16.509766 L 19.472656 16.509766 L 20.173828 16.509766 L 20.173828 15.677734 L 20.173828 13.523438 L 20.173828 12.441406 C 20.173828 12.304794 20.156597 12.177281 20.091797 12.03125 C 20.059397 11.95824 20.015299 11.877916 19.923828 11.791016 C 19.832368 11.704116 19.667509 11.607422 19.472656 11.607422 L 18.927734 11.607422 L 18.927734 10.376953 C 18.927734 9.0851838 17.867902 8.0273438 16.576172 8.0273438 L 16.435547 8.0273438 z M 16.435547 9.2207031 L 16.576172 9.2207031 C 17.22782 9.2207031 17.734375 9.7251013 17.734375 10.376953 L 17.734375 11.607422 L 15.277344 11.607422 L 15.277344 10.376953 C 15.277344 9.7251013 15.7839 9.2207031 16.435547 9.2207031 z M 12.919922 9.9394531 C 12.559922 9.9594531 12.359141 10.480234 12.619141 10.740234 L 12.751953 10.904297 C 12.862211 10.870135 12.980058 10.842244 13.085938 10.802734 L 13.085938 10.378906 C 13.085938 10.228632 13.111295 10.084741 13.130859 9.9394531 L 12.919922 9.9394531 z M 19.882812 9.9394531 C 19.902378 10.084741 19.927734 10.228632 19.927734 10.378906 L 19.927734 10.791016 C 20.168811 10.875098 20.455966 10.916935 20.613281 11.066406 C 20.691227 11.140457 20.749315 11.223053 20.806641 11.302734 L 21.259766 10.740234 C 21.519766 10.460234 21.260625 9.9094531 20.890625 9.9394531 L 19.882812 9.9394531 z M 16.435547 10.220703 C 16.301234 10.220703 16.277344 10.244432 16.277344 10.378906 L 16.277344 10.607422 L 16.734375 10.607422 L 16.734375 10.378906 C 16.734375 10.244433 16.712442 10.220703 16.578125 10.220703 L 16.435547 10.220703 z ' fill='#{hex-color($white)}' stroke-width='0'/><path d='M 7.7792969 19.650391 L 7.7792969 19.660156 C 7.5392969 19.680156 7.3398437 19.910156 7.3398438 20.160156 L 7.3398438 22.619141 L 7.2792969 22.619141 C 6.1992969 22.619141 5.4208594 22.589844 4.8808594 22.589844 C 3.2408594 22.589844 3.6308594 23.020234 3.6308594 26.240234 L 3.6308594 30.710938 C 3.6308594 34.970937 3.0692969 34.330078 7.2792969 34.330078 L 8.5 34.330078 L 7.1992188 33.269531 C 7.0992188 33.189531 7.02 33.070703 7 32.970703 C 6.98 32.800703 7.0592186 32.619531 7.1992188 32.519531 L 8.5292969 31.419922 L 7.2792969 31.419922 C 6.8392969 31.419922 6.5605469 31.120703 6.5605469 30.720703 L 6.5605469 26.240234 C 6.5605469 25.800234 6.8392969 25.519531 7.2792969 25.519531 L 7.3398438 25.519531 L 7.3398438 28.019531 C 7.3398438 28.399531 7.8801564 28.650391 8.1601562 28.400391 L 13.060547 24.470703 C 13.310547 24.290703 13.310547 23.869453 13.060547 23.689453 L 8.1601562 19.769531 C 8.0601563 19.669531 7.9192969 19.630391 7.7792969 19.650391 z M 17.119141 22.580078 L 17.119141 22.589844 C 16.579141 22.569844 15.820703 22.609375 14.720703 22.609375 L 13.470703 22.609375 L 14.769531 23.679688 C 14.869531 23.749688 14.950703 23.879766 14.970703 24.009766 C 14.990703 24.169766 14.909531 24.310156 14.769531 24.410156 L 13.439453 25.509766 L 14.720703 25.509766 C 15.129702 25.509766 15.41841 25.778986 15.433594 26.199219 C 15.752266 26.097283 16.084896 26.027344 16.435547 26.027344 L 16.578125 26.027344 C 17.236645 26.027344 17.848901 26.228565 18.369141 26.5625 L 18.369141 26.240234 C 18.369141 23.090234 18.769141 22.620078 17.119141 22.580078 z M 16.435547 27.027344 C 15.143818 27.027344 14.083984 28.085184 14.083984 29.376953 L 14.083984 30.607422 L 13.570312 30.607422 C 13.375452 30.607422 13.210603 30.704118 13.119141 30.791016 C 13.027691 30.877916 12.983569 30.958238 12.951172 31.03125 C 12.886382 31.177277 12.867184 31.304789 12.867188 31.441406 L 12.867188 32.523438 L 12.867188 33.119141 L 12.867188 34.677734 L 12.867188 35.509766 L 13.570312 35.509766 L 19.472656 35.509766 L 20.173828 35.509766 L 20.173828 34.677734 L 20.173828 32.523438 L 20.173828 31.441406 C 20.173828 31.304794 20.156597 31.177281 20.091797 31.03125 C 20.059397 30.95824 20.015299 30.877916 19.923828 30.791016 C 19.832368 30.704116 19.667509 30.607422 19.472656 30.607422 L 18.927734 30.607422 L 18.927734 29.376953 C 18.927734 28.085184 17.867902 27.027344 16.576172 27.027344 L 16.435547 27.027344 z M 16.435547 28.220703 L 16.576172 28.220703 C 17.22782 28.220703 17.734375 28.725101 17.734375 29.376953 L 17.734375 30.607422 L 15.277344 30.607422 L 15.277344 29.376953 C 15.277344 28.725101 15.7839 28.220703 16.435547 28.220703 z M 13.109375 29.150391 L 8.9199219 32.509766 C 8.6599219 32.689766 8.6599219 33.109063 8.9199219 33.289062 L 11.869141 35.648438 L 11.869141 34.677734 L 11.869141 33.119141 L 11.869141 32.523438 L 11.869141 31.441406 C 11.869141 31.217489 11.912641 30.907486 12.037109 30.626953 C 12.093758 30.499284 12.228597 30.257492 12.429688 30.066406 C 12.580253 29.92335 12.859197 29.887344 13.085938 29.802734 L 13.085938 29.378906 C 13.085938 29.300761 13.104 29.227272 13.109375 29.150391 z M 16.435547 29.220703 C 16.301234 29.220703 16.277344 29.244432 16.277344 29.378906 L 16.277344 29.607422 L 16.734375 29.607422 L 16.734375 29.378906 C 16.734375 29.244433 16.712442 29.220703 16.578125 29.220703 L 16.435547 29.220703 z M 12.943359 36.509766 L 13.820312 37.210938 C 14.090314 37.460938 14.639141 37.210078 14.619141 36.830078 L 14.619141 36.509766 L 13.570312 36.509766 L 12.943359 36.509766 z M 10.330078 38.650391 L 10.339844 38.660156 C 10.099844 38.680156 9.9001562 38.910156 9.9101562 39.160156 L 9.9101562 41.630859 L 7.3007812 41.630859 C 6.2207812 41.630859 5.4403906 41.589844 4.9003906 41.589844 C 3.2603906 41.589844 3.6503906 42.020234 3.6503906 45.240234 L 3.6503906 49.710938 C 3.6503906 53.370936 3.4202344 53.409141 5.9902344 53.369141 L 4.6503906 52.269531 C 4.5503906 52.189531 4.4692187 52.070703 4.4492188 51.970703 C 4.4492188 51.800703 4.5203906 51.619531 4.6503906 51.519531 L 6.609375 49.919922 C 6.579375 49.859922 6.5703125 49.790703 6.5703125 49.720703 L 6.5703125 45.240234 C 6.5703125 44.800234 6.8490625 44.519531 7.2890625 44.519531 L 9.9003906 44.519531 L 9.9003906 47.019531 C 9.9003906 47.379531 10.399219 47.620391 10.699219 47.400391 L 15.630859 43.470703 C 15.870859 43.290703 15.870859 42.869453 15.630859 42.689453 L 10.689453 38.769531 C 10.589453 38.689531 10.460078 38.640391 10.330078 38.650391 z M 16.869141 41.585938 C 16.616211 41.581522 16.322969 41.584844 15.980469 41.589844 L 15.970703 41.589844 L 17.310547 42.689453 C 17.410547 42.759453 17.489766 42.889531 17.509766 43.019531 C 17.529766 43.179531 17.479609 43.319922 17.349609 43.419922 L 15.390625 45.019531 C 15.406724 45.075878 15.427133 45.132837 15.4375 45.197266 C 15.754974 45.096169 16.086404 45.027344 16.435547 45.027344 L 16.578125 45.027344 C 17.24129 45.027344 17.858323 45.230088 18.380859 45.568359 L 18.380859 45.25 C 18.380859 42.0475 18.639648 41.616836 16.869141 41.585938 z M 16.435547 46.027344 C 15.143818 46.027344 14.083984 47.085184 14.083984 48.376953 L 14.083984 49.607422 L 13.570312 49.607422 C 13.375448 49.607422 13.210603 49.704118 13.119141 49.791016 C 13.027691 49.877916 12.983569 49.958238 12.951172 50.03125 C 12.886382 50.177277 12.867187 50.304789 12.867188 50.441406 L 12.867188 51.523438 L 12.867188 52.119141 L 12.867188 53.677734 L 12.867188 54.509766 L 13.570312 54.509766 L 19.472656 54.509766 L 20.173828 54.509766 L 20.173828 53.677734 L 20.173828 51.523438 L 20.173828 50.441406 C 20.173828 50.304794 20.156597 50.177281 20.091797 50.03125 C 20.059397 49.95824 20.015299 49.877916 19.923828 49.791016 C 19.832368 49.704116 19.667509 49.607422 19.472656 49.607422 L 18.927734 49.607422 L 18.927734 48.376953 C 18.927734 47.085184 17.867902 46.027344 16.576172 46.027344 L 16.435547 46.027344 z M 16.435547 47.220703 L 16.576172 47.220703 C 17.22782 47.220703 17.734375 47.725101 17.734375 48.376953 L 17.734375 49.607422 L 15.277344 49.607422 L 15.277344 48.376953 C 15.277344 47.725101 15.7839 47.220703 16.435547 47.220703 z M 11.470703 47.490234 C 11.410703 47.510234 11.349063 47.539844 11.289062 47.589844 L 6.3496094 51.519531 C 6.1096094 51.699531 6.1096094 52.120781 6.3496094 52.300781 L 11.289062 56.220703 C 11.569064 56.440703 12.070312 56.199844 12.070312 55.839844 L 12.070312 55.509766 L 11.869141 55.509766 L 11.869141 53.677734 L 11.869141 52.119141 L 11.869141 51.523438 L 11.869141 50.441406 C 11.869141 50.217489 11.912641 49.907486 12.037109 49.626953 C 12.043809 49.611855 12.061451 49.584424 12.070312 49.566406 L 12.070312 47.960938 C 12.070312 47.660938 11.770703 47.430234 11.470703 47.490234 z M 16.435547 48.220703 C 16.301234 48.220703 16.277344 48.244432 16.277344 48.378906 L 16.277344 48.607422 L 16.734375 48.607422 L 16.734375 48.378906 C 16.734375 48.244433 16.712442 48.220703 16.578125 48.220703 L 16.435547 48.220703 z M 13.060547 57.650391 L 13.060547 57.660156 C 12.830547 57.690156 12.660156 57.920156 12.660156 58.160156 L 12.660156 60.630859 L 7.2792969 60.630859 C 6.1992969 60.630859 5.4208594 60.589844 4.8808594 60.589844 C 3.2408594 60.589844 3.6308594 61.020234 3.6308594 64.240234 L 3.6308594 69.109375 L 6.5605469 66.740234 L 6.5605469 64.240234 C 6.5605469 63.800234 6.8392969 63.519531 7.2792969 63.519531 L 12.660156 63.519531 L 12.660156 66.019531 C 12.660156 66.299799 12.960394 66.500006 13.226562 66.474609 C 13.625751 65.076914 14.904956 64.035678 16.421875 64.029297 L 18.380859 62.470703 C 18.620859 62.290703 18.620859 61.869453 18.380859 61.689453 L 13.439453 57.769531 C 13.339453 57.669531 13.200547 57.630391 13.060547 57.650391 z M 18.359375 63.810547 L 17.800781 64.269531 C 18.004793 64.350836 18.198411 64.450249 18.380859 64.568359 L 18.380859 64.25 L 18.380859 63.810547 L 18.359375 63.810547 z M 16.435547 65.027344 C 15.143818 65.027344 14.083984 66.085184 14.083984 67.376953 L 14.083984 68.607422 L 13.570312 68.607422 C 13.375448 68.607422 13.210603 68.704118 13.119141 68.791016 C 13.027691 68.877916 12.983569 68.958238 12.951172 69.03125 C 12.886382 69.177277 12.867187 69.304789 12.867188 69.441406 L 12.867188 70.523438 L 12.867188 71.119141 L 12.867188 72.677734 L 12.867188 73.509766 L 13.570312 73.509766 L 19.472656 73.509766 L 20.173828 73.509766 L 20.173828 72.677734 L 20.173828 70.523438 L 20.173828 69.441406 C 20.173828 69.304794 20.156597 69.177281 20.091797 69.03125 C 20.059397 68.95824 20.015299 68.877916 19.923828 68.791016 C 19.832368 68.704116 19.667509 68.607422 19.472656 68.607422 L 18.927734 68.607422 L 18.927734 67.376953 C 18.927734 66.085184 17.867902 65.027344 16.576172 65.027344 L 16.435547 65.027344 z M 16.435547 66.220703 L 16.576172 66.220703 C 17.22782 66.220703 17.734375 66.725101 17.734375 67.376953 L 17.734375 68.607422 L 15.277344 68.607422 L 15.277344 67.376953 C 15.277344 66.725101 15.7839 66.220703 16.435547 66.220703 z M 8.7207031 66.509766 C 8.6507031 66.529766 8.5895312 66.559375 8.5195312 66.609375 L 3.5996094 70.519531 C 3.3496094 70.699531 3.3496094 71.120781 3.5996094 71.300781 L 8.5292969 75.220703 C 8.8092969 75.440703 9.3105469 75.199844 9.3105469 74.839844 L 9.3105469 72.339844 L 11.869141 72.339844 L 11.869141 71.119141 L 11.869141 70.523438 L 11.869141 69.449219 L 9.3203125 69.449219 L 9.3203125 66.980469 C 9.3203125 66.680469 9.0007031 66.449766 8.7207031 66.509766 z M 16.435547 67.220703 C 16.301234 67.220703 16.277344 67.244432 16.277344 67.378906 L 16.277344 67.607422 L 16.734375 67.607422 L 16.734375 67.378906 C 16.734375 67.244433 16.712442 67.220703 16.578125 67.220703 L 16.435547 67.220703 z M 19.248047 78.800781 C 19.148558 78.831033 19.050295 78.90106 18.970703 78.970703 L 18.070312 79.869141 C 17.630312 79.569141 16.710703 79.619141 14.720703 79.619141 L 7.2792969 79.619141 C 6.1992969 79.619141 5.4208594 79.589844 4.8808594 79.589844 C 3.2408594 79.589844 3.6308594 80.020234 3.6308594 83.240234 L 3.6308594 83.939453 L 6.5605469 84.240234 L 6.5605469 83.240234 C 6.5605469 82.800234 6.8392969 82.519531 7.2792969 82.519531 L 14.720703 82.519531 C 14.920703 82.519531 15.090703 82.600703 15.220703 82.720703 L 13.419922 84.519531 C 13.279464 84.665607 13.281282 84.881022 13.363281 85.054688 C 13.880838 83.867655 15.067337 83.027344 16.435547 83.027344 L 16.578125 83.027344 C 18.290465 83.027344 19.703357 84.345788 19.890625 86.011719 L 19.960938 86.019531 C 20.240938 86.049531 20.520234 85.770234 20.490234 85.490234 L 19.789062 79.240234 C 19.789062 78.973661 19.498025 78.767523 19.25 78.800781 L 19.248047 78.800781 z M 16.435547 84.027344 C 15.143818 84.027344 14.083984 85.085184 14.083984 86.376953 L 14.083984 87.607422 L 13.570312 87.607422 C 13.375448 87.607422 13.210603 87.704118 13.119141 87.791016 C 13.027691 87.877916 12.983569 87.958238 12.951172 88.03125 C 12.886382 88.177277 12.867187 88.304789 12.867188 88.441406 L 12.867188 89.523438 L 12.867188 90.119141 L 12.867188 91.677734 L 12.867188 92.509766 L 13.570312 92.509766 L 19.472656 92.509766 L 20.173828 92.509766 L 20.173828 91.677734 L 20.173828 89.523438 L 20.173828 88.441406 C 20.173828 88.304794 20.156597 88.177281 20.091797 88.03125 C 20.059397 87.95824 20.015299 87.877916 19.923828 87.791016 C 19.832368 87.704116 19.667509 87.607422 19.472656 87.607422 L 18.927734 87.607422 L 18.927734 86.376953 C 18.927734 85.085184 17.867902 84.027344 16.576172 84.027344 L 16.435547 84.027344 z M 2.0507812 84.900391 C 1.8507824 84.970391 1.6907031 85.199453 1.7207031 85.439453 L 2.4199219 91.689453 C 2.4399219 92.049453 3 92.240929 3.25 91.960938 L 4.0507812 91.160156 C 4.0707812 91.160156 4.0898437 91.140156 4.0898438 91.160156 C 4.5498437 91.400156 5.4595313 91.330078 7.2695312 91.330078 L 11.869141 91.330078 L 11.869141 90.119141 L 11.869141 89.523438 L 11.869141 88.441406 C 11.869141 88.437991 11.871073 88.433136 11.871094 88.429688 L 7.2792969 88.429688 C 7.1292969 88.429688 6.9808594 88.400078 6.8808594 88.330078 L 8.8007812 86.400391 C 9.1007822 86.160391 8.8992969 85.600547 8.5292969 85.560547 L 2.25 84.910156 L 2.0507812 84.910156 L 2.0507812 84.900391 z M 16.435547 85.220703 L 16.576172 85.220703 C 17.22782 85.220703 17.734375 85.725101 17.734375 86.376953 L 17.734375 87.607422 L 15.277344 87.607422 L 15.277344 86.376953 C 15.277344 85.725101 15.7839 85.220703 16.435547 85.220703 z M 4.8808594 98.599609 C 3.5508594 98.599609 3.5400781 99.080402 3.5800781 100.90039 L 4.7207031 99.529297 C 4.8007031 99.429297 4.9405469 99.360078 5.0605469 99.330078 C 5.2205469 99.330078 5.4 99.409297 5.5 99.529297 L 7.1601562 101.56055 C 7.2001563 101.56055 7.2292969 101.5293 7.2792969 101.5293 L 14.720703 101.5293 C 15.060703 101.5293 15.289141 101.7293 15.369141 102.0293 L 12.939453 102.0293 C 12.599453 102.0793 12.410625 102.55055 12.640625 102.81055 L 13.470703 103.85742 C 14.029941 102.77899 15.146801 102.02734 16.435547 102.02734 L 16.578125 102.02734 C 18.158418 102.02734 19.491598 103.14879 19.835938 104.63086 L 21.279297 102.82031 C 21.499297 102.55031 21.260156 102.06078 20.910156 102.05078 L 18.400391 102.05078 C 18.420391 98.150792 19.000234 98.650391 14.740234 98.650391 L 7.2792969 98.650391 C 6.1992969 98.650391 5.4208594 98.609375 4.8808594 98.609375 L 4.8808594 98.599609 z M 5.0292969 101.06055 C 4.9292969 101.09055 4.83 101.15977 4.75 101.25977 L 0.81054688 106.16016 C 0.61054688 106.44016 0.8409375 106.92945 1.2109375 106.93945 L 3.5996094 106.93945 C 3.5796094 110.87945 3.1497656 110.33984 7.2597656 110.33984 L 11.869141 110.33984 L 11.869141 109.11914 L 11.869141 108.52344 L 11.869141 107.44141 L 11.869141 107.43945 L 7.2792969 107.43945 C 6.9292969 107.43945 6.7091406 107.23945 6.6191406 106.93945 L 9.0605469 106.93945 C 9.4305469 106.93945 9.6909375 106.44016 9.4609375 106.16016 L 5.5 101.25977 C 5.4 101.10977 5.1992969 101.03055 5.0292969 101.06055 z M 16.435547 103.02734 C 15.143818 103.02734 14.083984 104.08518 14.083984 105.37695 L 14.083984 106.60742 L 13.570312 106.60742 C 13.375448 106.60742 13.210603 106.70409 13.119141 106.79102 C 13.027691 106.87792 12.983569 106.95823 12.951172 107.03125 C 12.886382 107.17727 12.867187 107.30479 12.867188 107.44141 L 12.867188 108.52344 L 12.867188 109.11914 L 12.867188 110.67773 L 12.867188 111.50977 L 13.570312 111.50977 L 19.472656 111.50977 L 20.173828 111.50977 L 20.173828 110.67773 L 20.173828 108.52344 L 20.173828 107.44141 C 20.173828 107.3048 20.156597 107.17728 20.091797 107.03125 C 20.059397 106.95825 20.015299 106.87792 19.923828 106.79102 C 19.832368 106.70412 19.667509 106.60742 19.472656 106.60742 L 18.927734 106.60742 L 18.927734 105.37695 C 18.927734 104.08518 17.867902 103.02734 16.576172 103.02734 L 16.435547 103.02734 z M 16.435547 104.2207 L 16.576172 104.2207 C 17.22782 104.2207 17.734375 104.7251 17.734375 105.37695 L 17.734375 106.60742 L 15.277344 106.60742 L 15.277344 105.37695 C 15.277344 104.7251 15.7839 104.2207 16.435547 104.2207 z M 16.435547 105.2207 C 16.301234 105.2207 16.277344 105.24444 16.277344 105.37891 L 16.277344 105.60742 L 16.734375 105.60742 L 16.734375 105.37891 C 16.734375 105.24441 16.712442 105.2207 16.578125 105.2207 L 16.435547 105.2207 z M 4.8808594 117.58984 L 4.8808594 117.59961 C 3.7208594 117.59961 3.5800781 117.90016 3.5800781 119.16016 L 4.7207031 117.7793 C 4.8007031 117.6793 4.9405469 117.63914 5.0605469 117.61914 C 5.2205469 117.61914 5.4 117.6593 5.5 117.7793 L 7.7207031 120.5293 L 14.720703 120.5293 C 15.123595 120.5293 15.408576 120.79174 15.431641 121.20117 C 15.750992 121.09876 16.08404 121.02734 16.435547 121.02734 L 16.578125 121.02734 C 17.24903 121.02734 17.874081 121.23262 18.400391 121.57812 L 18.400391 121.25 C 18.400391 117.05 19.120234 117.61914 14.740234 117.61914 L 7.2792969 117.61914 C 6.1992969 117.61914 5.4208594 117.58984 4.8808594 117.58984 z M 4.9804688 119.33984 C 4.8804688 119.36984 4.81 119.44 4.75 119.5 L 0.80078125 124.43945 C 0.60078125 124.71945 0.8292182 125.2107 1.1992188 125.2207 L 3.5996094 125.2207 L 3.5996094 125.7207 C 3.5996094 129.9807 3.0497656 129.33984 7.2597656 129.33984 L 11.869141 129.33984 L 11.869141 128.11914 L 11.869141 127.52344 L 11.869141 126.44141 C 11.869141 126.43799 11.871073 126.43314 11.871094 126.42969 L 7.2792969 126.42969 C 6.8392969 126.42969 6.5605469 126.13094 6.5605469 125.71094 L 6.5605469 125.21094 L 9.0605469 125.21094 C 9.4305469 125.23094 9.6909375 124.70969 9.4609375 124.42969 L 5.5 119.5 C 5.3820133 119.35252 5.1682348 119.28513 4.9804688 119.33984 z M 12.839844 121.7793 C 12.539844 121.8793 12.410625 122.32055 12.640625 122.56055 L 13.267578 123.34375 C 13.473522 122.72168 13.852237 122.1828 14.353516 121.7793 L 12.839844 121.7793 z M 18.658203 121.7793 C 19.393958 122.37155 19.878978 123.25738 19.916016 124.25781 L 21.279297 122.56055 C 21.499297 122.28055 21.260156 121.7893 20.910156 121.7793 L 18.658203 121.7793 z M 16.435547 122.02734 C 15.143818 122.02734 14.083984 123.08518 14.083984 124.37695 L 14.083984 125.60742 L 13.570312 125.60742 C 13.375448 125.60742 13.210603 125.70409 13.119141 125.79102 C 13.027691 125.87792 12.983569 125.95823 12.951172 126.03125 C 12.886382 126.17727 12.867187 126.30479 12.867188 126.44141 L 12.867188 127.52344 L 12.867188 128.11914 L 12.867188 129.67773 L 12.867188 130.50977 L 13.570312 130.50977 L 19.472656 130.50977 L 20.173828 130.50977 L 20.173828 129.67773 L 20.173828 127.52344 L 20.173828 126.44141 C 20.173828 126.3048 20.156597 126.17728 20.091797 126.03125 C 20.059397 125.95825 20.015299 125.87792 19.923828 125.79102 C 19.832368 125.70412 19.667509 125.60742 19.472656 125.60742 L 18.927734 125.60742 L 18.927734 124.37695 C 18.927734 123.08518 17.867902 122.02734 16.576172 122.02734 L 16.435547 122.02734 z M 16.435547 123.2207 L 16.576172 123.2207 C 17.22782 123.2207 17.734375 123.7251 17.734375 124.37695 L 17.734375 125.60742 L 15.277344 125.60742 L 15.277344 124.37695 C 15.277344 123.7251 15.7839 123.2207 16.435547 123.2207 z M 16.435547 124.2207 C 16.301234 124.2207 16.277344 124.24444 16.277344 124.37891 L 16.277344 124.60742 L 16.734375 124.60742 L 16.734375 124.37891 C 16.734375 124.24441 16.712442 124.2207 16.578125 124.2207 L 16.435547 124.2207 z M 5.9394531 136.58984 L 5.9394531 136.59961 L 8.3105469 139.5293 L 14.730469 139.5293 C 15.131912 139.5293 15.414551 139.79039 15.439453 140.19727 C 15.756409 140.09653 16.087055 140.02734 16.435547 140.02734 L 16.578125 140.02734 C 17.24903 140.02734 17.874081 140.23261 18.400391 140.57812 L 18.400391 140.25 C 18.400391 136.05 19.120234 136.61914 14.740234 136.61914 L 7.2792969 136.61914 C 6.6792969 136.61914 6.3594531 136.59984 5.9394531 136.58984 z M 4.2207031 136.66016 C 3.8207031 136.74016 3.6791406 136.96016 3.6191406 137.41016 L 4.2207031 136.66992 L 4.2207031 136.66016 z M 5.0605469 137.57031 L 5.0605469 137.58984 C 4.9405469 137.58984 4.8197656 137.66953 4.7597656 137.76953 L 0.81054688 142.66992 C 0.57054688 142.96992 0.8109375 143.50023 1.2109375 143.49023 L 3.5996094 143.49023 L 3.5996094 144.71094 C 3.5996094 148.97094 3.0497656 148.33008 7.2597656 148.33008 L 11.869141 148.33008 L 11.869141 147.11914 L 11.869141 146.52344 L 11.869141 145.44141 C 11.869141 145.43799 11.871073 145.43314 11.871094 145.42969 L 7.2792969 145.42969 C 6.8392969 145.42969 6.5605469 145.13094 6.5605469 144.71094 L 6.5605469 143.49023 L 9.0605469 143.49023 C 9.4605469 143.53023 9.7309375 142.95945 9.4609375 142.68945 L 5.5 137.76953 C 5.4 137.63953 5.2305469 137.57031 5.0605469 137.57031 z M 16.435547 141.02734 C 15.143818 141.02734 14.083984 142.08518 14.083984 143.37695 L 14.083984 144.60742 L 13.570312 144.60742 C 13.375448 144.60742 13.210603 144.70409 13.119141 144.79102 C 13.027691 144.87792 12.983569 144.95823 12.951172 145.03125 C 12.886382 145.17727 12.867187 145.30479 12.867188 145.44141 L 12.867188 146.52344 L 12.867188 147.11914 L 12.867188 148.67773 L 12.867188 149.50977 L 13.570312 149.50977 L 19.472656 149.50977 L 20.173828 149.50977 L 20.173828 148.67773 L 20.173828 146.52344 L 20.173828 145.44141 C 20.173828 145.3048 20.156597 145.17728 20.091797 145.03125 C 20.059397 144.95825 20.015299 144.87792 19.923828 144.79102 C 19.832368 144.70412 19.667509 144.60742 19.472656 144.60742 L 18.927734 144.60742 L 18.927734 143.37695 C 18.927734 142.08518 17.867902 141.02734 16.576172 141.02734 L 16.435547 141.02734 z M 12.849609 141.5 C 12.549609 141.6 12.420391 142.0393 12.650391 142.2793 L 13.136719 142.88672 C 13.213026 142.38119 13.390056 141.90696 13.667969 141.5 L 12.849609 141.5 z M 19.34375 141.5 C 19.710704 142.03735 19.927734 142.68522 19.927734 143.37891 L 19.927734 143.79102 C 19.965561 143.80421 20.005506 143.81448 20.044922 143.82617 L 21.289062 142.2793 C 21.509062 141.9993 21.269922 141.51 20.919922 141.5 L 19.34375 141.5 z M 16.435547 142.2207 L 16.576172 142.2207 C 17.22782 142.2207 17.734375 142.7251 17.734375 143.37695 L 17.734375 144.60742 L 15.277344 144.60742 L 15.277344 143.37695 C 15.277344 142.7251 15.7839 142.2207 16.435547 142.2207 z M 16.435547 143.2207 C 16.301234 143.2207 16.277344 143.24444 16.277344 143.37891 L 16.277344 143.60742 L 16.734375 143.60742 L 16.734375 143.37891 C 16.734375 143.24441 16.712442 143.2207 16.578125 143.2207 L 16.435547 143.2207 z M 17.130859 155.59961 C 16.580859 155.57961 15.810469 155.63086 14.730469 155.63086 L 6.5292969 155.63086 L 8.9101562 158.5293 L 14.730469 158.5293 C 15.131912 158.5293 15.414551 158.79039 15.439453 159.19727 C 15.756409 159.09653 16.087055 159.02734 16.435547 159.02734 L 16.578125 159.02734 C 17.24903 159.02734 17.874081 159.23261 18.400391 159.57812 L 18.400391 159.25977 C 18.400391 156.10977 18.800391 155.63961 17.150391 155.59961 L 17.130859 155.59961 z M 5.0292969 155.86914 L 5.0292969 155.88086 C 4.9292969 155.90086 4.83 155.98055 4.75 156.06055 L 0.81054688 160.96094 C 0.61054688 161.26094 0.8409375 161.73977 1.2109375 161.75977 L 3.5996094 161.75977 L 3.5996094 163.7207 C 3.5996094 167.9807 3.0497656 167.33984 7.2597656 167.33984 L 11.869141 167.33984 L 11.869141 166.11914 L 11.869141 165.52344 L 11.869141 164.44141 L 11.869141 164.43945 L 7.2792969 164.43945 C 6.8392969 164.43945 6.5605469 164.1407 6.5605469 163.7207 L 6.5605469 161.75 L 9.0605469 161.75 C 9.4305469 161.77 9.6909375 161.2507 9.4609375 160.9707 L 5.5 156.07031 C 5.4 155.92031 5.1992969 155.84914 5.0292969 155.86914 z M 16.435547 160.02734 C 15.143818 160.02734 14.083984 161.08518 14.083984 162.37695 L 14.083984 163.60742 L 13.570312 163.60742 C 13.375448 163.60742 13.210603 163.70409 13.119141 163.79102 C 13.027691 163.87792 12.983569 163.95823 12.951172 164.03125 C 12.886382 164.17727 12.867187 164.30479 12.867188 164.44141 L 12.867188 165.52344 L 12.867188 166.11914 L 12.867188 167.67773 L 12.867188 168.50977 L 13.570312 168.50977 L 19.472656 168.50977 L 20.173828 168.50977 L 20.173828 167.67773 L 20.173828 165.52344 L 20.173828 164.44141 C 20.173828 164.3048 20.156597 164.17728 20.091797 164.03125 C 20.059397 163.95825 20.015299 163.87792 19.923828 163.79102 C 19.832368 163.70412 19.667509 163.60742 19.472656 163.60742 L 18.927734 163.60742 L 18.927734 162.37695 C 18.927734 161.08518 17.867902 160.02734 16.576172 160.02734 L 16.435547 160.02734 z M 12.900391 161.2207 C 12.580391 161.2807 12.419141 161.74 12.619141 162 L 13.085938 162.58594 L 13.085938 162.37891 C 13.085938 161.97087 13.170592 161.58376 13.306641 161.2207 L 12.900391 161.2207 z M 16.435547 161.2207 L 16.576172 161.2207 C 17.22782 161.2207 17.734375 161.7251 17.734375 162.37695 L 17.734375 163.60742 L 15.277344 163.60742 L 15.277344 162.37695 C 15.277344 161.7251 15.7839 161.2207 16.435547 161.2207 z M 19.708984 161.23047 C 19.842743 161.59081 19.927734 161.97449 19.927734 162.37891 L 19.927734 162.79102 C 20.119162 162.85779 20.322917 162.91147 20.484375 163 L 21.279297 162.00977 C 21.499297 161.72977 21.260156 161.24047 20.910156 161.23047 L 19.708984 161.23047 z M 16.435547 162.2207 C 16.301234 162.2207 16.277344 162.24444 16.277344 162.37891 L 16.277344 162.60742 L 16.734375 162.60742 L 16.734375 162.37891 C 16.734375 162.24441 16.712442 162.2207 16.578125 162.2207 L 16.435547 162.2207 z M 5.0996094 174.49023 L 5.1308594 174.5 C 4.9808594 174.5 4.83 174.56922 4.75 174.69922 L 0.80078125 179.59961 C 0.56078125 179.86961 0.7992182 180.42039 1.1992188 180.40039 L 3.5996094 180.40039 L 3.5996094 182.7207 C 3.5996094 186.9807 3.0497656 186.33984 7.2597656 186.33984 L 11.869141 186.33984 L 11.869141 185.11914 L 11.869141 184.52344 L 11.869141 183.44141 L 11.869141 183.43945 L 7.25 183.43945 C 6.82 183.43945 6.5507814 183.1407 6.5507812 182.7207 L 6.5507812 180.41992 L 9.0507812 180.41992 C 9.4307824 180.44992 9.7092187 179.87984 9.4492188 179.58984 L 5.4804688 174.68945 C 5.3804688 174.55945 5.2496094 174.49023 5.0996094 174.49023 z M 17.150391 174.58008 L 17.130859 174.59961 C 16.580859 174.57961 15.810469 174.63086 14.730469 174.63086 L 6.8300781 174.63086 L 9.1796875 177.5293 L 14.699219 177.5293 C 15.104107 177.5293 15.391475 177.79407 15.412109 178.20703 C 15.737096 178.1006 16.076913 178.02734 16.435547 178.02734 L 16.578125 178.02734 C 17.24903 178.02734 17.874081 178.2326 18.400391 178.57812 L 18.400391 178.24023 C 18.400391 175.09023 18.800391 174.62008 17.150391 174.58008 z M 16.435547 179.02734 C 15.143818 179.02734 14.083984 180.08518 14.083984 181.37695 L 14.083984 182.60742 L 13.570312 182.60742 C 13.375448 182.60742 13.210603 182.70409 13.119141 182.79102 C 13.027691 182.87792 12.983569 182.95823 12.951172 183.03125 C 12.886382 183.17727 12.867187 183.30479 12.867188 183.44141 L 12.867188 184.52344 L 12.867188 185.11914 L 12.867188 186.67773 L 12.867188 187.50977 L 13.570312 187.50977 L 19.472656 187.50977 L 20.173828 187.50977 L 20.173828 186.67773 L 20.173828 184.52344 L 20.173828 183.44141 C 20.173828 183.3048 20.156597 183.17728 20.091797 183.03125 C 20.059397 182.95825 20.015299 182.87792 19.923828 182.79102 C 19.832368 182.70412 19.667509 182.60742 19.472656 182.60742 L 18.927734 182.60742 L 18.927734 181.37695 C 18.927734 180.08518 17.867902 179.02734 16.576172 179.02734 L 16.435547 179.02734 z M 16.435547 180.2207 L 16.576172 180.2207 C 17.22782 180.2207 17.734375 180.7251 17.734375 181.37695 L 17.734375 182.60742 L 15.277344 182.60742 L 15.277344 181.37695 C 15.277344 180.7251 15.7839 180.2207 16.435547 180.2207 z M 19.816406 180.57031 C 19.882311 180.83091 19.927734 181.09907 19.927734 181.37891 L 19.927734 181.79102 C 20.168811 181.87511 20.455966 181.91694 20.613281 182.06641 C 20.630645 182.0829 20.639883 182.10199 20.65625 182.11914 L 21.259766 181.36914 C 21.479766 181.06914 21.240625 180.59031 20.890625 180.57031 L 19.816406 180.57031 z M 12.820312 180.58984 C 12.520316 180.68984 12.389141 181.11914 12.619141 181.36914 L 12.990234 181.83203 C 13.022029 181.82207 13.055579 181.81406 13.085938 181.80273 L 13.085938 181.37891 C 13.085938 181.10616 13.128698 180.84442 13.191406 180.58984 L 12.820312 180.58984 z M 16.435547 181.2207 C 16.301234 181.2207 16.277344 181.24444 16.277344 181.37891 L 16.277344 181.60742 L 16.734375 181.60742 L 16.734375 181.37891 C 16.734375 181.24441 16.712442 181.2207 16.578125 181.2207 L 16.435547 181.2207 z M 4.9609375 193.15039 L 4.9707031 193.16016 C 4.8707031 193.19016 4.8 193.25984 4.75 193.33984 L 0.81054688 198.24023 C 0.61054688 198.54023 0.8409375 199.01906 1.2109375 199.03906 L 3.5996094 199.03906 L 3.5996094 201.7207 C 3.5996094 205.9807 3.0497656 205.33984 7.2597656 205.33984 L 11.869141 205.33984 L 11.869141 204.11914 L 11.869141 203.52344 L 11.869141 202.44141 C 11.869141 202.44141 11.869141 202.43945 11.869141 202.43945 L 7.2695312 202.43945 C 6.8295312 202.43945 6.5507814 202.1407 6.5507812 201.7207 L 6.5507812 199.01953 L 9.0507812 199.01953 C 9.4207814 199.04953 9.6792188 198.54 9.4492188 198.25 L 5.4902344 193.34961 C 5.3702344 193.17961 5.1509375 193.10039 4.9609375 193.15039 z M 17.150391 193.58008 L 17.130859 193.58984 C 16.580859 193.56984 15.810469 193.61914 14.730469 193.61914 L 7.0996094 193.61914 L 9.4199219 196.46094 L 9.4492188 196.51953 L 14.699219 196.51953 C 15.106887 196.51953 15.397075 196.78718 15.414062 197.20508 C 15.738375 197.09913 16.077769 197.02734 16.435547 197.02734 L 16.578125 197.02734 C 17.24903 197.02734 17.874081 197.23259 18.400391 197.57812 L 18.400391 197.24023 C 18.400391 194.09023 18.800391 193.62008 17.150391 193.58008 z M 16.435547 198.02734 C 15.143818 198.02734 14.083984 199.08518 14.083984 200.37695 L 14.083984 201.60742 L 13.570312 201.60742 C 13.375448 201.60742 13.210603 201.70409 13.119141 201.79102 C 13.027691 201.87792 12.983569 201.95823 12.951172 202.03125 C 12.886382 202.17727 12.867187 202.30479 12.867188 202.44141 L 12.867188 203.52344 L 12.867188 204.11914 L 12.867188 205.67773 L 12.867188 206.50977 L 13.570312 206.50977 L 19.472656 206.50977 L 20.173828 206.50977 L 20.173828 205.67773 L 20.173828 203.52344 L 20.173828 202.44141 C 20.173828 202.3048 20.156597 202.17728 20.091797 202.03125 C 20.059397 201.95825 20.015299 201.87792 19.923828 201.79102 C 19.832368 201.70412 19.667509 201.60742 19.472656 201.60742 L 18.927734 201.60742 L 18.927734 200.37695 C 18.927734 199.08518 17.867902 198.02734 16.576172 198.02734 L 16.435547 198.02734 z M 16.435547 199.2207 L 16.576172 199.2207 C 17.22782 199.2207 17.734375 199.7251 17.734375 200.37695 L 17.734375 201.60742 L 15.277344 201.60742 L 15.277344 200.37695 C 15.277344 199.7251 15.7839 199.2207 16.435547 199.2207 z M 12.919922 199.93945 C 12.559922 199.95945 12.359141 200.48023 12.619141 200.74023 L 12.751953 200.9043 C 12.862211 200.87013 12.980058 200.84224 13.085938 200.80273 L 13.085938 200.37891 C 13.085938 200.22863 13.111295 200.08474 13.130859 199.93945 L 12.919922 199.93945 z M 19.882812 199.93945 C 19.902378 200.08474 19.927734 200.22863 19.927734 200.37891 L 19.927734 200.79102 C 20.168811 200.87511 20.455966 200.91694 20.613281 201.06641 C 20.691227 201.14046 20.749315 201.22305 20.806641 201.30273 L 21.259766 200.74023 C 21.519766 200.46023 21.260625 199.90945 20.890625 199.93945 L 19.882812 199.93945 z M 16.435547 200.2207 C 16.301234 200.2207 16.277344 200.24444 16.277344 200.37891 L 16.277344 200.60742 L 16.734375 200.60742 L 16.734375 200.37891 C 16.734375 200.24441 16.712442 200.2207 16.578125 200.2207 L 16.435547 200.2207 z ' fill='#{hex-color($highlight-text-color)}' stroke-width='0' /></svg>");
+      }
+    }
+
+    &.disabled {
+      i.fa-retweet {
+        background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' height='209' width='22'><path d='M 18.972656 1.2011719 C 18.829825 1.1881782 18.685932 1.2302188 18.572266 1.3300781 L 15.990234 3.5996094 C 15.58109 3.6070661 15.297269 3.609375 14.730469 3.609375 L 7.0996094 3.609375 L 9.4199219 6.4609375 L 9.4492188 6.5195312 L 12.664062 6.5195312 L 6.5761719 11.867188 C 6.5674697 11.818249 6.5507813 11.773891 6.5507812 11.720703 L 6.5507812 9.0195312 L 9.0507812 9.0195312 C 9.4207813 9.0495313 9.6792188 8.54 9.4492188 8.25 L 5.5 3.3496094 C 5.38 3.1796094 5.1607031 3.1003906 4.9707031 3.1503906 L 4.9707031 3.1601562 C 4.8707031 3.1901563 4.8 3.2598438 4.75 3.3398438 L 0.80078125 8.2402344 C 0.60078125 8.5402344 0.8292187 9.0190625 1.1992188 9.0390625 L 3.5996094 9.0390625 L 3.5996094 11.720703 C 3.5996094 13.045739 3.5690668 13.895038 3.6503906 14.4375 L 2.6152344 15.347656 C 2.3879011 15.547375 2.3754917 15.901081 2.5859375 16.140625 L 3.1464844 16.78125 C 3.3569308 17.020794 3.7101667 17.053234 3.9375 16.853516 L 19.892578 2.8359375 C 20.119911 2.6362188 20.134275 2.282513 19.923828 2.0429688 L 19.361328 1.4023438 C 19.256105 1.282572 19.115488 1.2141655 18.972656 1.2011719 z M 18.410156 6.7753906 L 15.419922 9.4042969 L 15.419922 9.9394531 L 14.810547 9.9394531 L 13.148438 11.400391 L 16.539062 15.640625 C 16.719062 15.890625 17.140313 15.890625 17.320312 15.640625 L 21.259766 10.740234 C 21.519766 10.460234 21.260625 9.9094531 20.890625 9.9394531 L 18.400391 9.9394531 L 18.400391 7.2402344 C 18.400391 7.0470074 18.407711 6.9489682 18.410156 6.7753906 z M 11.966797 12.439453 L 8.6679688 15.339844 L 14.919922 15.339844 L 12.619141 12.5 C 12.589141 12.48 12.590313 12.459453 12.570312 12.439453 L 11.966797 12.439453 z' fill='#{hex-color($white)}' stroke-width='0'/></svg>");
+      }
+    }
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/components/columns.scss b/app/javascript/flavours/blobfox/styles/components/columns.scss
new file mode 100644
index 00000000000000..ac1658a2de9933
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/components/columns.scss
@@ -0,0 +1,1364 @@
+.column__wrapper {
+  display: flex;
+  flex: 1 1 auto;
+  position: relative;
+}
+
+.columns-area {
+  display: flex;
+  flex: 1 1 auto;
+  flex-direction: row;
+  justify-content: flex-start;
+  overflow-x: auto;
+  position: relative;
+
+  &__panels {
+    display: flex;
+    justify-content: center;
+    width: 100%;
+    height: 100%;
+    min-height: 100vh;
+
+    &__pane {
+      height: 100%;
+      overflow: hidden;
+      pointer-events: none;
+      display: flex;
+      justify-content: flex-end;
+      min-width: 285px;
+
+      &--start {
+        justify-content: flex-start;
+      }
+
+      &__inner {
+        position: fixed;
+        width: 285px;
+        pointer-events: auto;
+        height: 100%;
+      }
+    }
+
+    &__main {
+      box-sizing: border-box;
+      width: 100%;
+      flex: 0 0 auto;
+      display: flex;
+      flex-direction: column;
+
+      @media screen and (min-width: $no-gap-breakpoint) {
+        padding: 0 10px;
+        max-width: 600px;
+      }
+    }
+  }
+}
+
+$ui-header-height: 55px;
+
+.ui__header {
+  display: none;
+  box-sizing: border-box;
+  height: $ui-header-height;
+  position: sticky;
+  top: 0;
+  z-index: 3;
+  justify-content: space-between;
+  align-items: center;
+
+  &__logo {
+    display: inline-flex;
+    padding: 15px;
+
+    .logo {
+      height: $ui-header-height - 30px;
+      width: auto;
+    }
+
+    .logo--wordmark {
+      display: none;
+    }
+
+    @media screen and (width >= 320px) {
+      .logo--wordmark {
+        display: block;
+      }
+
+      .logo--icon {
+        display: none;
+      }
+    }
+  }
+
+  &__links {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    padding: 0 10px;
+    overflow: hidden;
+
+    .button {
+      flex: 0 0 auto;
+    }
+
+    .button-tertiary {
+      flex-shrink: 1;
+    }
+  }
+}
+
+.tabs-bar__wrapper {
+  background: darken($ui-base-color, 8%);
+  position: sticky;
+  top: $ui-header-height;
+  z-index: 2;
+  padding-top: 0;
+
+  @media screen and (min-width: $no-gap-breakpoint) {
+    padding-top: 10px;
+    top: 0;
+  }
+
+  .tabs-bar {
+    margin-bottom: 0;
+
+    @media screen and (min-width: $no-gap-breakpoint) {
+      margin-bottom: 10px;
+    }
+  }
+}
+
+.react-swipeable-view-container {
+  &,
+  .columns-area,
+  .column {
+    height: 100%;
+  }
+}
+
+.react-swipeable-view-container > * {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 100%;
+}
+
+.column {
+  width: 330px;
+  position: relative;
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+
+  > .scrollable {
+    background: $ui-base-color;
+  }
+}
+
+.ui {
+  flex: 0 0 auto;
+  display: flex;
+  flex-direction: column;
+  width: 100%;
+  height: 100%;
+}
+
+.column {
+  overflow: hidden;
+}
+
+.column-back-button {
+  box-sizing: border-box;
+  width: 100%;
+  background: lighten($ui-base-color, 4%);
+  border-radius: 4px 4px 0 0;
+  color: $highlight-text-color;
+  cursor: pointer;
+  flex: 0 0 auto;
+  font-size: 16px;
+  border: 0;
+  text-align: unset;
+  padding: 15px;
+  margin: 0;
+  z-index: 3;
+
+  &:hover {
+    text-decoration: underline;
+  }
+}
+
+.column-header__back-button {
+  background: lighten($ui-base-color, 4%);
+  border: 0;
+  font-family: inherit;
+  color: $highlight-text-color;
+  cursor: pointer;
+  flex: 0 0 auto;
+  font-size: 16px;
+  padding: 0;
+  padding-inline-end: 5px;
+  z-index: 3;
+
+  &:hover {
+    text-decoration: underline;
+  }
+
+  &:last-child {
+    padding: 0;
+    padding-inline-end: 15px;
+  }
+}
+
+.column-back-button__icon {
+  display: inline-block;
+  margin-inline-end: 5px;
+}
+
+.column-back-button--slim {
+  position: relative;
+}
+
+.column-back-button--slim-button {
+  cursor: pointer;
+  flex: 0 0 auto;
+  font-size: 16px;
+  padding: 15px;
+  position: absolute;
+  inset-inline-end: 0;
+  top: -48px;
+}
+
+.switch-to-advanced {
+  color: $light-text-color;
+  background-color: $ui-base-color;
+  padding: 15px;
+  border-radius: 4px;
+  margin-top: 4px;
+  margin-bottom: 12px;
+  font-size: 13px;
+  line-height: 18px;
+
+  .switch-to-advanced__toggle {
+    color: $ui-button-tertiary-color;
+    font-weight: bold;
+  }
+}
+
+.column-link {
+  background: lighten($ui-base-color, 8%);
+  color: $primary-text-color;
+  display: block;
+  font-size: 16px;
+  padding: 15px;
+  text-decoration: none;
+  overflow: hidden;
+  white-space: nowrap;
+
+  &:hover,
+  &:focus,
+  &:active {
+    background: lighten($ui-base-color, 11%);
+  }
+
+  &:focus {
+    outline: 0;
+  }
+
+  &--transparent {
+    background: transparent;
+    color: $ui-secondary-color;
+
+    &:hover,
+    &:focus,
+    &:active {
+      background: transparent;
+      color: $primary-text-color;
+    }
+
+    &.active {
+      color: $highlight-text-color;
+    }
+  }
+
+  &--logo {
+    background: transparent;
+    padding: 10px;
+
+    &:hover,
+    &:focus,
+    &:active {
+      background: transparent;
+    }
+  }
+}
+
+.column-link__icon {
+  display: inline-block;
+  margin-inline-end: 5px;
+}
+
+.column-subheading {
+  background: $ui-base-color;
+  color: $dark-text-color;
+  padding: 8px 20px;
+  font-size: 12px;
+  font-weight: 500;
+  text-transform: uppercase;
+  cursor: default;
+}
+
+.column-header__wrapper {
+  position: relative;
+  flex: 0 0 auto;
+  z-index: 1;
+
+  &.active {
+    box-shadow: 0 1px 0 rgba($highlight-text-color, 0.3);
+
+    &::before {
+      display: block;
+      content: '';
+      position: absolute;
+      bottom: -13px;
+      inset-inline-start: 0;
+      inset-inline-end: 0;
+      margin: 0 auto;
+      width: 60%;
+      pointer-events: none;
+      height: 28px;
+      z-index: 1;
+      background: radial-gradient(
+        ellipse,
+        rgba($ui-highlight-color, 0.23) 0%,
+        rgba($ui-highlight-color, 0) 60%
+      );
+    }
+  }
+
+  .announcements {
+    z-index: 1;
+    position: relative;
+  }
+}
+
+.column-header {
+  display: flex;
+  font-size: 16px;
+  background: lighten($ui-base-color, 4%);
+  border-radius: 4px 4px 0 0;
+  flex: 0 0 auto;
+  cursor: pointer;
+  position: relative;
+  z-index: 2;
+  outline: 0;
+  overflow: hidden;
+
+  & > button {
+    margin: 0;
+    border: 0;
+    padding: 15px;
+    color: inherit;
+    background: transparent;
+    font: inherit;
+    text-align: start;
+    text-overflow: ellipsis;
+    overflow: hidden;
+    white-space: nowrap;
+    flex: 1;
+  }
+
+  & > .column-header__back-button {
+    color: $highlight-text-color;
+  }
+
+  &.active {
+    .column-header__icon {
+      color: $highlight-text-color;
+      text-shadow: 0 0 10px rgba($ui-highlight-color, 0.4);
+    }
+  }
+
+  &:focus,
+  &:active {
+    outline: 0;
+  }
+}
+
+.column {
+  width: 330px;
+  position: relative;
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+
+  .wide .columns-area:not(.columns-area--mobile) & {
+    flex: auto;
+    min-width: 330px;
+    max-width: 600px;
+  }
+
+  > .scrollable {
+    background: $ui-base-color;
+    border-radius: 0 0 4px 4px;
+  }
+}
+
+.column-header__buttons {
+  height: 48px;
+  display: flex;
+  margin-inline-start: 0;
+}
+
+.column-header__links {
+  margin-bottom: 14px;
+}
+
+.column-header__links .text-btn {
+  margin-inline-end: 10px;
+}
+
+.column-header__button {
+  background: lighten($ui-base-color, 4%);
+  border: 0;
+  color: $darker-text-color;
+  cursor: pointer;
+  font-size: 16px;
+  padding: 0 15px;
+
+  &:hover {
+    color: lighten($darker-text-color, 7%);
+  }
+
+  &.active {
+    color: $primary-text-color;
+    background: lighten($ui-base-color, 8%);
+
+    &:hover {
+      color: $primary-text-color;
+      background: lighten($ui-base-color, 8%);
+    }
+  }
+
+  // blobfox - added focus ring for keyboard navigation
+  &:focus {
+    text-shadow: 0 0 4px darken($ui-highlight-color, 5%);
+  }
+
+  &:disabled {
+    color: $dark-text-color;
+    cursor: default;
+  }
+}
+
+.column-header__notif-cleaning-buttons {
+  display: flex;
+  align-items: stretch;
+  justify-content: space-around;
+
+  .column-header__button {
+    background: transparent;
+    text-align: center;
+    padding: 10px 5px;
+    font-size: 14px;
+  }
+
+  b {
+    font-weight: bold;
+  }
+}
+
+.layout-single-column .column-header__notif-cleaning-buttons {
+  @media screen and (min-width: $no-gap-breakpoint) {
+    b,
+    i {
+      margin-inline-end: 5px;
+    }
+
+    br {
+      display: none;
+    }
+
+    button {
+      padding: 15px 5px;
+    }
+  }
+}
+
+// The notifs drawer with no padding to have more space for the buttons
+.column-header__collapsible-inner.nopad-drawer {
+  padding: 0;
+}
+
+.column-header__collapsible {
+  max-height: 70vh;
+  overflow: hidden;
+  overflow-y: auto;
+  color: $darker-text-color;
+  transition:
+    max-height 150ms ease-in-out,
+    opacity 300ms linear;
+  opacity: 1;
+  z-index: 1;
+  position: relative;
+
+  &.collapsed {
+    max-height: 0;
+    opacity: 0.5;
+  }
+
+  &.animating {
+    overflow-y: hidden;
+  }
+
+  hr {
+    height: 0;
+    background: transparent;
+    border: 0;
+    border-top: 1px solid lighten($ui-base-color, 12%);
+    margin: 10px 0;
+  }
+
+  // notif cleaning drawer
+  &.ncd {
+    transition: none;
+
+    &.collapsed {
+      max-height: 0;
+      opacity: 0.7;
+    }
+  }
+}
+
+.column-header__collapsible-inner {
+  background: lighten($ui-base-color, 8%);
+  padding: 15px;
+}
+
+.column-header__setting-btn {
+  &:hover,
+  &:focus {
+    color: $darker-text-color;
+    text-decoration: underline;
+  }
+}
+
+.column-header__collapsible__extra + .column-header__setting-btn {
+  padding-top: 5px;
+}
+
+.column-header__permission-btn {
+  display: inline;
+  font-weight: inherit;
+  text-decoration: underline;
+}
+
+.column-header__setting-arrows {
+  float: right;
+
+  .column-header__setting-btn {
+    padding: 5px;
+
+    &:first-child {
+      padding-inline-end: 7px;
+    }
+
+    &:last-child {
+      padding-inline-start: 7px;
+      margin-inline-start: 5px;
+    }
+  }
+}
+
+.column-header__title {
+  display: inline-block;
+  text-overflow: ellipsis;
+  overflow: hidden;
+  white-space: nowrap;
+  flex: 1;
+}
+
+.column-header__issue-btn {
+  color: $warning-red;
+
+  &:hover {
+    color: $error-red;
+    text-decoration: underline;
+  }
+}
+
+.column-header__icon {
+  display: inline-block;
+  margin-inline-end: 5px;
+}
+
+.column-settings__pillbar {
+  display: flex;
+  overflow: hidden;
+  background-color: transparent;
+  border: 0;
+  border-radius: 4px;
+  margin-bottom: 10px;
+  align-items: stretch;
+  gap: 2px;
+}
+
+.pillbar-button {
+  border: 0;
+  color: #fafafa;
+  padding: 2px;
+  margin: 0;
+  font-size: inherit;
+  flex: auto;
+  background-color: $ui-base-color;
+  transition: all 0.2s ease;
+  transition-property: background-color, box-shadow;
+
+  &[disabled] {
+    cursor: not-allowed;
+    opacity: 0.5;
+  }
+
+  &:not([disabled]) {
+    &:hover,
+    &:focus {
+      background-color: darken($ui-base-color, 10%);
+    }
+
+    &.active {
+      background-color: darken($ui-highlight-color, 2%);
+
+      &:hover,
+      &:focus {
+        background-color: $ui-highlight-color;
+      }
+    }
+  }
+}
+
+.limited-account-hint {
+  p {
+    color: $secondary-text-color;
+    font-size: 15px;
+    font-weight: 500;
+    margin-bottom: 20px;
+  }
+}
+
+.empty-column-indicator,
+.follow_requests-unlocked_explanation {
+  color: $dark-text-color;
+  background: $ui-base-color;
+  text-align: center;
+  padding: 20px;
+  font-size: 15px;
+  font-weight: 400;
+  cursor: default;
+  display: flex;
+  flex: 1 1 auto;
+  align-items: center;
+  justify-content: center;
+
+  & > span {
+    max-width: 500px;
+  }
+
+  a {
+    color: $highlight-text-color;
+    text-decoration: none;
+
+    &:hover {
+      text-decoration: underline;
+    }
+  }
+}
+
+.follow_requests-unlocked_explanation {
+  background: darken($ui-base-color, 4%);
+  contain: initial;
+  flex-grow: 0;
+}
+
+.error-column {
+  padding: 20px;
+  background: $ui-base-color;
+  border-radius: 4px;
+  display: flex;
+  flex: 1 1 auto;
+  align-items: center;
+  justify-content: center;
+  flex-direction: column;
+  cursor: default;
+
+  &__image {
+    width: 70%;
+    max-width: 350px;
+    margin-top: -50px;
+  }
+
+  &__message {
+    text-align: center;
+    color: $darker-text-color;
+    font-size: 15px;
+    line-height: 22px;
+
+    h1 {
+      font-size: 28px;
+      line-height: 33px;
+      font-weight: 700;
+      margin-bottom: 15px;
+      color: $primary-text-color;
+    }
+
+    p {
+      max-width: 48ch;
+    }
+
+    &__actions {
+      margin-top: 30px;
+      display: flex;
+      gap: 10px;
+      align-items: center;
+      justify-content: center;
+    }
+  }
+}
+
+.column-inline-form {
+  padding: 7px 15px;
+  padding-inline-end: 5px;
+  display: flex;
+  justify-content: flex-start;
+  align-items: center;
+  background: lighten($ui-base-color, 4%);
+
+  label {
+    flex: 1 1 auto;
+
+    input {
+      width: 100%;
+      margin-bottom: 6px;
+
+      &:focus {
+        outline: 0;
+      }
+    }
+  }
+
+  .icon-button {
+    flex: 0 0 auto;
+    margin: 0 5px;
+  }
+}
+
+.column-settings__outer {
+  background: lighten($ui-base-color, 8%);
+  padding: 15px;
+}
+
+.column-settings__section {
+  color: $darker-text-color;
+  cursor: default;
+  display: block;
+  font-weight: 500;
+  margin-bottom: 10px;
+}
+
+.column-settings__row--with-margin {
+  margin-bottom: 15px;
+}
+
+.column-settings__hashtags {
+  .column-settings__row {
+    margin-bottom: 15px;
+  }
+
+  .column-select {
+    &__control {
+      @include search-input;
+
+      &::placeholder {
+        color: lighten($darker-text-color, 4%);
+      }
+
+      &::-moz-focus-inner {
+        border: 0;
+      }
+
+      &::-moz-focus-inner,
+      &:focus,
+      &:active {
+        outline: 0 !important;
+      }
+
+      &:focus {
+        background: lighten($ui-base-color, 4%);
+      }
+
+      @media screen and (width <= 600px) {
+        font-size: 16px;
+      }
+    }
+
+    &__placeholder {
+      color: $dark-text-color;
+      padding-inline-start: 2px;
+      font-size: 12px;
+    }
+
+    &__value-container {
+      padding-inline-start: 6px;
+    }
+
+    &__multi-value {
+      background: lighten($ui-base-color, 8%);
+
+      &__remove {
+        cursor: pointer;
+
+        &:hover,
+        &:active,
+        &:focus {
+          background: lighten($ui-base-color, 12%);
+          color: lighten($darker-text-color, 4%);
+        }
+      }
+    }
+
+    &__multi-value__label,
+    &__input,
+    &__input-container {
+      color: $darker-text-color;
+    }
+
+    &__clear-indicator,
+    &__dropdown-indicator {
+      cursor: pointer;
+      transition: none;
+      color: $dark-text-color;
+
+      &:hover,
+      &:active,
+      &:focus {
+        color: lighten($dark-text-color, 4%);
+      }
+    }
+
+    &__indicator-separator {
+      background-color: lighten($ui-base-color, 8%);
+    }
+
+    &__menu {
+      @include search-popout;
+
+      padding: 0;
+      background: $ui-secondary-color;
+    }
+
+    &__menu-list {
+      padding: 6px;
+    }
+
+    &__option {
+      color: $inverted-text-color;
+      border-radius: 4px;
+      font-size: 14px;
+
+      &--is-focused,
+      &--is-selected {
+        background: darken($ui-secondary-color, 10%);
+      }
+    }
+  }
+}
+
+.column-settings__row {
+  .text-btn:not(.column-header__permission-btn) {
+    margin-bottom: 15px;
+  }
+}
+
+.notifications-permission-banner {
+  padding: 30px;
+  border-bottom: 1px solid lighten($ui-base-color, 8%);
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  position: relative;
+
+  &__close {
+    position: absolute;
+    top: 10px;
+    inset-inline-end: 10px;
+  }
+
+  h2 {
+    font-size: 16px;
+    font-weight: 500;
+    margin-bottom: 15px;
+    text-align: center;
+  }
+
+  p {
+    color: $darker-text-color;
+    margin-bottom: 15px;
+    text-align: center;
+  }
+}
+
+.column-title {
+  text-align: center;
+  padding-bottom: 40px;
+
+  h3 {
+    font-size: 24px;
+    line-height: 1.5;
+    font-weight: 700;
+    margin-bottom: 10px;
+  }
+
+  p {
+    font-size: 16px;
+    line-height: 24px;
+    font-weight: 400;
+    color: $darker-text-color;
+  }
+
+  @media screen and (width >= 600px) {
+    padding: 40px;
+  }
+}
+
+.onboarding__footer {
+  margin-top: 30px;
+  color: $dark-text-color;
+  text-align: center;
+  font-size: 14px;
+
+  .link-button {
+    display: inline-block;
+    color: inherit;
+    font-size: inherit;
+  }
+}
+
+.onboarding__link {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 10px;
+  color: $highlight-text-color;
+  background: lighten($ui-base-color, 4%);
+  border-radius: 8px;
+  padding: 10px 15px;
+  box-sizing: border-box;
+  font-size: 14px;
+  font-weight: 500;
+  height: 56px;
+  text-decoration: none;
+
+  svg {
+    height: 1.5em;
+  }
+
+  &:hover,
+  &:focus,
+  &:active {
+    background: lighten($ui-base-color, 8%);
+  }
+}
+
+.onboarding__illustration {
+  display: block;
+  margin: 0 auto;
+  margin-bottom: 10px;
+  max-height: 200px;
+  width: auto;
+}
+
+.onboarding__lead {
+  font-size: 16px;
+  line-height: 24px;
+  font-weight: 400;
+  color: $darker-text-color;
+  text-align: center;
+  margin-bottom: 30px;
+
+  strong {
+    font-weight: 700;
+    color: $secondary-text-color;
+  }
+}
+
+.onboarding__links {
+  margin-bottom: 30px;
+
+  & > * {
+    margin-bottom: 2px;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+}
+
+.onboarding__steps {
+  margin-bottom: 30px;
+
+  &__item {
+    background: lighten($ui-base-color, 4%);
+    border: 0;
+    border-radius: 8px;
+    display: flex;
+    width: 100%;
+    box-sizing: border-box;
+    align-items: center;
+    gap: 10px;
+    padding: 10px;
+    padding-inline-end: 15px;
+    margin-bottom: 2px;
+    text-decoration: none;
+    text-align: start;
+
+    &:hover,
+    &:focus,
+    &:active {
+      background: lighten($ui-base-color, 8%);
+    }
+
+    &__icon {
+      flex: 0 0 auto;
+      border-radius: 50%;
+      display: none;
+      align-items: center;
+      justify-content: center;
+      width: 36px;
+      height: 36px;
+      color: $highlight-text-color;
+      font-size: 1.2rem;
+
+      @media screen and (width >= 600px) {
+        display: flex;
+      }
+    }
+
+    &__progress {
+      flex: 0 0 auto;
+      background: $valid-value-color;
+      border-radius: 50%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      width: 21px;
+      height: 21px;
+      color: $primary-text-color;
+
+      svg {
+        height: 14px;
+        width: auto;
+      }
+    }
+
+    &__go {
+      flex: 0 0 auto;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      width: 21px;
+      height: 21px;
+      color: $highlight-text-color;
+      font-size: 17px;
+
+      svg {
+        height: 1.5em;
+        width: auto;
+      }
+    }
+
+    &__description {
+      flex: 1 1 auto;
+      line-height: 20px;
+
+      h6 {
+        color: $highlight-text-color;
+        font-weight: 500;
+        font-size: 14px;
+      }
+
+      p {
+        color: $darker-text-color;
+        overflow: hidden;
+      }
+    }
+  }
+}
+
+.onboarding__progress-indicator {
+  display: flex;
+  align-items: center;
+  margin-bottom: 30px;
+  position: sticky;
+  background: $ui-base-color;
+
+  @media screen and (width >= 600) {
+    padding: 0 40px;
+  }
+
+  &__line {
+    height: 4px;
+    flex: 1 1 auto;
+    background: lighten($ui-base-color, 4%);
+  }
+
+  &__step {
+    flex: 0 0 auto;
+    width: 30px;
+    height: 30px;
+    background: lighten($ui-base-color, 4%);
+    border-radius: 50%;
+    color: $primary-text-color;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    svg {
+      width: 15px;
+      height: auto;
+    }
+
+    &.active {
+      background: $valid-value-color;
+    }
+  }
+
+  &__step.active,
+  &__line.active {
+    background: $valid-value-color;
+    background-image: linear-gradient(
+      90deg,
+      $valid-value-color,
+      lighten($valid-value-color, 8%),
+      $valid-value-color
+    );
+    background-size: 200px 100%;
+    animation: skeleton 1.2s ease-in-out infinite;
+  }
+}
+
+.follow-recommendations {
+  background: darken($ui-base-color, 4%);
+  border-radius: 8px;
+  margin-bottom: 30px;
+
+  .account:last-child {
+    border-bottom: 0;
+  }
+
+  &__empty {
+    text-align: center;
+    color: $darker-text-color;
+    font-weight: 500;
+    padding: 40px;
+  }
+}
+
+.tip-carousel {
+  border: 1px solid transparent;
+  border-radius: 8px;
+  padding: 16px;
+  margin-bottom: 30px;
+
+  &:focus {
+    outline: 0;
+    border-color: $highlight-text-color;
+  }
+
+  .media-modal__pagination {
+    margin-bottom: 0;
+  }
+}
+
+.copy-paste-text {
+  background: lighten($ui-base-color, 4%);
+  border-radius: 8px;
+  border: 1px solid lighten($ui-base-color, 8%);
+  padding: 16px;
+  color: $primary-text-color;
+  font-size: 15px;
+  line-height: 22px;
+  display: flex;
+  flex-direction: column;
+  align-items: flex-end;
+  transition: border-color 300ms linear;
+  margin-bottom: 30px;
+
+  &:focus,
+  &.focused {
+    transition: none;
+    outline: 0;
+    border-color: $highlight-text-color;
+  }
+
+  &.copied {
+    border-color: $valid-value-color;
+    transition: none;
+  }
+
+  textarea {
+    width: 100%;
+    height: auto;
+    background: transparent;
+    color: inherit;
+    font: inherit;
+    border: 0;
+    padding: 0;
+    margin-bottom: 30px;
+    resize: none;
+
+    &:focus {
+      outline: 0;
+    }
+  }
+}
+
+.compose-form__highlightable {
+  display: flex;
+  flex-direction: column;
+  flex: 0 1 auto;
+  border-radius: 4px;
+  transition: box-shadow 300ms linear;
+  min-height: 0;
+  position: relative;
+
+  &.active {
+    transition: none;
+    box-shadow: 0 0 0 6px rgba(lighten($highlight-text-color, 8%), 0.7);
+  }
+}
+
+.dismissable-banner,
+.warning-banner {
+  position: relative;
+  margin: 10px;
+  margin-bottom: 5px;
+  border-radius: 8px;
+  border: 1px solid $highlight-text-color;
+  background: rgba($highlight-text-color, 0.15);
+  overflow: hidden;
+
+  &__background-image {
+    width: 125%;
+    position: absolute;
+    bottom: -25%;
+    inset-inline-end: -25%;
+    z-index: -1;
+    opacity: 0.15;
+    mix-blend-mode: luminosity;
+  }
+
+  &__message {
+    flex: 1 1 auto;
+    padding: 15px;
+    font-size: 15px;
+    line-height: 22px;
+    font-weight: 500;
+    color: $primary-text-color;
+
+    p {
+      margin-bottom: 15px;
+
+      &:last-child {
+        margin-bottom: 0;
+      }
+    }
+
+    h1 {
+      color: $highlight-text-color;
+      font-size: 22px;
+      line-height: 33px;
+      font-weight: 700;
+      margin-bottom: 15px;
+    }
+
+    &__actions {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 4px;
+
+      &__wrapper {
+        display: flex;
+        margin-top: 30px;
+      }
+
+      .button {
+        display: block;
+        flex-grow: 1;
+      }
+    }
+
+    .button-tertiary {
+      background: rgba($ui-base-color, 0.15);
+      backdrop-filter: blur(8px);
+    }
+  }
+
+  &__action {
+    float: right;
+    padding: 15px 10px;
+
+    .icon-button {
+      color: $highlight-text-color;
+    }
+  }
+}
+
+.warning-banner {
+  border: 1px solid $warning-red;
+  background: rgba($warning-red, 0.15);
+
+  &__message {
+    h1 {
+      color: $warning-red;
+    }
+
+    a {
+      color: $primary-text-color;
+    }
+  }
+}
+
+.hashtag-header {
+  border-bottom: 1px solid lighten($ui-base-color, 8%);
+  padding: 15px;
+  font-size: 17px;
+  line-height: 22px;
+  color: $darker-text-color;
+
+  strong {
+    font-weight: 700;
+  }
+
+  &__header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 15px;
+    gap: 15px;
+
+    h1 {
+      color: $primary-text-color;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+      overflow: hidden;
+      font-size: 22px;
+      line-height: 33px;
+      font-weight: 700;
+    }
+  }
+
+  &:focus {
+    outline: 0;
+    background-color: $highlight-text-color;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/components/compose_form.scss b/app/javascript/flavours/blobfox/styles/components/compose_form.scss
new file mode 100644
index 00000000000000..0f64c0dcc10f5a
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/components/compose_form.scss
@@ -0,0 +1,685 @@
+.compose-form {
+  padding: 10px;
+
+  .emoji-picker-dropdown {
+    position: absolute;
+    top: 0;
+    inset-inline-end: 0;
+
+    ::-webkit-scrollbar-track:hover,
+    ::-webkit-scrollbar-track:active {
+      background-color: rgba($base-overlay-background, 0.3);
+    }
+  }
+}
+
+.character-counter {
+  cursor: default;
+  font-family: $font-sans-serif, sans-serif;
+  font-size: 14px;
+  font-weight: 600;
+  color: $lighter-text-color;
+
+  &.character-counter--over {
+    color: $warning-red;
+  }
+}
+
+.no-reduce-motion .spoiler-input {
+  transition:
+    height 0.4s ease,
+    opacity 0.4s ease;
+}
+
+.spoiler-input {
+  height: 0;
+  transform-origin: bottom;
+  opacity: 0;
+
+  &.spoiler-input--visible {
+    height: 36px;
+    margin-bottom: 11px;
+    opacity: 1;
+  }
+
+  input {
+    display: block;
+    box-sizing: border-box;
+    margin: 0;
+    border: 0;
+    border-radius: 4px;
+    padding: 10px;
+    width: 100%;
+    outline: 0;
+    color: $inverted-text-color;
+    background: $simple-background-color;
+    font-size: 14px;
+    font-family: inherit;
+    resize: vertical;
+
+    &::placeholder {
+      color: $dark-text-color;
+    }
+
+    &:focus {
+      outline: 0;
+    }
+    @include single-column('screen and (max-width: 630px)') {
+      font-size: 16px;
+    }
+  }
+}
+
+.compose-form__warning {
+  color: $inverted-text-color;
+  margin-bottom: 15px;
+  background: $ui-primary-color;
+  box-shadow: 0 2px 6px rgba($base-shadow-color, 0.3);
+  padding: 8px 10px;
+  border-radius: 4px;
+  font-size: 13px;
+  font-weight: 400;
+
+  a {
+    color: $lighter-text-color;
+    font-weight: 500;
+    text-decoration: underline;
+
+    &:active,
+    &:focus,
+    &:hover {
+      text-decoration: none;
+    }
+  }
+}
+
+.compose-form__sensitive-button {
+  padding: 10px;
+  padding-top: 0;
+  font-size: 14px;
+  font-weight: 500;
+
+  &.active {
+    color: $highlight-text-color;
+  }
+
+  input[type='checkbox'] {
+    appearance: none;
+    display: inline-block;
+    position: relative;
+    border: 1px solid $ui-primary-color;
+    box-sizing: border-box;
+    width: 18px;
+    height: 18px;
+    flex: 0 0 auto;
+    margin-inline-start: 5px;
+    margin-inline-end: 10px;
+    top: -1px;
+    border-radius: 4px;
+    vertical-align: middle;
+    cursor: inherit;
+
+    &:checked {
+      border-color: $highlight-text-color;
+      background: $highlight-text-color
+        url("data:image/svg+xml;utf8,<svg width='18' height='18' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M4.5 8.5L8 12l6-6' stroke='white' stroke-width='1.5'/></svg>")
+        center center no-repeat;
+    }
+  }
+}
+
+.reply-indicator {
+  margin: 0 0 10px;
+  border-radius: 4px;
+  padding: 10px;
+  background: $ui-primary-color;
+  min-height: 23px;
+  overflow-y: auto;
+  flex: 0 2 auto;
+}
+
+.reply-indicator__header {
+  margin-bottom: 5px;
+  overflow: hidden;
+
+  & > .account.small {
+    color: $inverted-text-color;
+  }
+}
+
+.reply-indicator__cancel {
+  float: right;
+  line-height: 24px;
+}
+
+.reply-indicator__content {
+  position: relative;
+  font-size: 14px;
+  line-height: 20px;
+  word-wrap: break-word;
+  font-weight: 400;
+  overflow: hidden;
+  padding-top: 5px;
+  color: $inverted-text-color;
+  white-space: pre-wrap;
+
+  p,
+  pre {
+    margin-bottom: 20px;
+    white-space: pre-wrap;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+
+  a {
+    color: $lighter-text-color;
+    text-decoration: none;
+
+    &:hover {
+      text-decoration: underline;
+    }
+
+    &.mention {
+      &:hover {
+        text-decoration: none;
+
+        span {
+          text-decoration: underline;
+        }
+      }
+    }
+  }
+
+  .emojione {
+    width: 20px;
+    height: 20px;
+    margin: -5px 0 0;
+  }
+}
+
+.compose-form .compose-form__autosuggest-wrapper {
+  position: relative;
+}
+
+.compose-form .autosuggest-textarea,
+.compose-form .autosuggest-input {
+  position: relative;
+  width: 100%;
+
+  label {
+    .autosuggest-textarea__textarea {
+      display: block;
+      box-sizing: border-box;
+      margin: 0;
+      border: 0;
+      border-radius: 4px 4px 0 0;
+      padding: 10px 32px 0 10px;
+      width: 100%;
+      min-height: 100px;
+      outline: 0;
+      color: $inverted-text-color;
+      background: $simple-background-color;
+      font-size: 14px;
+      font-family: inherit;
+      resize: none;
+      scrollbar-color: initial;
+
+      &::placeholder {
+        color: $dark-text-color;
+      }
+
+      &::-webkit-scrollbar {
+        all: unset;
+      }
+
+      &:focus {
+        outline: 0;
+      }
+
+      @include single-column('screen and (max-width: 630px)') {
+        font-size: 16px;
+      }
+
+      @include limited-single-column('screen and (max-width: 600px)') {
+        height: 100px !important; // prevent auto-resize textarea
+        resize: vertical;
+      }
+    }
+  }
+}
+
+.compose-form__textarea-icons {
+  display: block;
+  position: absolute;
+  top: 29px;
+  inset-inline-end: 5px;
+  bottom: 5px;
+  overflow: hidden;
+
+  & > .textarea_icon {
+    display: block;
+    margin-top: 2px;
+    margin-inline-start: 2px;
+    width: 24px;
+    height: 24px;
+    color: $lighter-text-color;
+    font-size: 18px;
+    line-height: 24px;
+    text-align: center;
+    opacity: 0.8;
+  }
+}
+
+.autosuggest-textarea__suggestions-wrapper {
+  position: relative;
+  height: 0;
+}
+
+.autosuggest-textarea__suggestions {
+  box-sizing: border-box;
+  display: none;
+  position: absolute;
+  top: 100%;
+  width: 100%;
+  z-index: 99;
+  box-shadow: 4px 4px 6px rgba($base-shadow-color, 0.4);
+  background: $ui-secondary-color;
+  border-radius: 0 0 4px 4px;
+  color: $inverted-text-color;
+  font-size: 14px;
+  padding: 6px;
+}
+
+.autosuggest-textarea__suggestions--visible {
+  display: block;
+}
+
+.autosuggest-textarea__suggestions__item {
+  padding: 10px;
+  cursor: pointer;
+  border-radius: 4px;
+
+  &:hover,
+  &:focus,
+  &:active,
+  &.selected {
+    background: darken($ui-secondary-color, 10%);
+  }
+
+  .autosuggest-account,
+  .autosuggest-emoji,
+  .autosuggest-hashtag {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    justify-content: flex-start;
+    line-height: 18px;
+    font-size: 14px;
+  }
+
+  .autosuggest-hashtag {
+    justify-content: space-between;
+
+    &__name {
+      flex: 1 1 auto;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+
+    strong {
+      font-weight: 500;
+    }
+
+    &__uses {
+      flex: 0 0 auto;
+      text-align: end;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+  }
+
+  .autosuggest-account-icon,
+  .autosuggest-emoji img {
+    margin-inline-end: 8px;
+  }
+
+  .autosuggest-account .display-name > span {
+    color: $lighter-text-color;
+  }
+}
+
+.compose-form__upload-wrapper {
+  overflow: hidden;
+}
+
+.compose-form__uploads-wrapper {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  font-family: inherit;
+  padding: 5px;
+  overflow: hidden;
+}
+
+.compose-form__upload {
+  flex: 1 1 0;
+  margin: 5px;
+  min-width: 40%;
+
+  .compose-form__upload-thumbnail {
+    position: relative;
+    border-radius: 4px;
+    height: 140px;
+    width: 100%;
+    background-color: $base-shadow-color;
+    background-position: center;
+    background-size: cover;
+    background-repeat: no-repeat;
+    overflow: hidden;
+
+    & > .close {
+      mix-blend-mode: difference;
+    }
+  }
+
+  .icon-button {
+    flex: 0 1 auto;
+    color: $secondary-text-color;
+    font-size: 14px;
+    font-weight: 500;
+    padding: 10px;
+    font-family: inherit;
+
+    &:hover,
+    &:focus,
+    &:active {
+      color: lighten($secondary-text-color, 7%);
+    }
+  }
+
+  &__warning {
+    position: absolute;
+    z-index: 2;
+    bottom: 0;
+    inset-inline-start: 0;
+    inset-inline-end: 0;
+    box-sizing: border-box;
+    background: linear-gradient(
+      0deg,
+      rgba($base-shadow-color, 0.8) 0,
+      rgba($base-shadow-color, 0.35) 80%,
+      transparent
+    );
+  }
+}
+
+.compose-form__upload__actions {
+  background: linear-gradient(
+    180deg,
+    rgba($base-shadow-color, 0.8) 0,
+    rgba($base-shadow-color, 0.35) 80%,
+    transparent
+  );
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+}
+
+.upload-progress {
+  display: flex;
+  padding: 10px;
+  color: $darker-text-color;
+  overflow: hidden;
+
+  .fa {
+    font-size: 34px;
+    margin-inline-end: 10px;
+  }
+
+  span {
+    display: block;
+    font-size: 12px;
+    font-weight: 500;
+    text-transform: uppercase;
+  }
+}
+
+.upload-progress__message {
+  flex: 1 1 auto;
+}
+
+.upload-progress__backdrop {
+  position: relative;
+  margin-top: 5px;
+  border-radius: 6px;
+  width: 100%;
+  height: 6px;
+  background: darken($simple-background-color, 8%);
+}
+
+.upload-progress__tracker {
+  position: absolute;
+  top: 0;
+  inset-inline-start: 0;
+  height: 6px;
+  border-radius: 6px;
+  background: $ui-highlight-color;
+}
+
+.compose-form__modifiers {
+  color: $inverted-text-color;
+  font-family: inherit;
+  font-size: 14px;
+  background: $simple-background-color;
+}
+
+.compose-form__buttons-wrapper {
+  padding: 10px;
+  background: darken($simple-background-color, 8%);
+  border-radius: 0 0 4px 4px;
+  height: 27px;
+  display: flex;
+  justify-content: space-between;
+  flex: 0 0 auto;
+}
+
+.compose-form__buttons {
+  display: flex;
+  flex: 0 0 auto;
+
+  & .icon-button,
+  & .text-icon-button {
+    display: inline-block;
+    box-sizing: content-box;
+    padding: 0 3px;
+    height: 27px;
+    line-height: 27px;
+    vertical-align: bottom;
+  }
+
+  & > hr {
+    display: inline-block;
+    margin: 0 3px;
+    border-width: 0 0 0 1px;
+    border-style: none none none solid;
+    border-color: transparent transparent transparent
+      darken($simple-background-color, 24%);
+    padding: 0;
+    width: 0;
+    height: 27px;
+    background: transparent;
+  }
+}
+
+.character-counter__wrapper {
+  align-self: center;
+  margin-inline-end: 4px;
+}
+
+.privacy-dropdown.active {
+  .privacy-dropdown__value {
+    background: $simple-background-color;
+    border-radius: 4px 4px 0 0;
+    box-shadow: 0 -4px 4px rgba($base-shadow-color, 0.1);
+
+    .icon-button {
+      transition: none;
+    }
+
+    &.active {
+      background: $ui-highlight-color;
+
+      .icon-button {
+        color: $primary-text-color;
+      }
+    }
+  }
+
+  &.top .privacy-dropdown__value {
+    border-radius: 0 0 4px 4px;
+  }
+
+  .privacy-dropdown__dropdown {
+    display: block;
+    box-shadow: 2px 4px 6px rgba($base-shadow-color, 0.1);
+  }
+}
+
+.privacy-dropdown__dropdown {
+  border-radius: 4px;
+  box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
+  background: $simple-background-color;
+  overflow: hidden;
+  transform-origin: 50% 0;
+}
+
+.privacy-dropdown__option {
+  display: flex;
+  align-items: center;
+  padding: 10px;
+  color: $inverted-text-color;
+  cursor: pointer;
+
+  .privacy-dropdown__option__content {
+    flex: 1 1 auto;
+    color: $lighter-text-color;
+
+    &:not(:first-child) {
+      margin-inline-start: 10px;
+    }
+
+    strong {
+      display: block;
+      color: $inverted-text-color;
+      font-weight: 500;
+    }
+  }
+
+  &:hover,
+  &.active {
+    background: $ui-highlight-color;
+    color: $primary-text-color;
+
+    .privacy-dropdown__option__content {
+      color: $primary-text-color;
+
+      strong {
+        color: $primary-text-color;
+      }
+    }
+  }
+
+  &.active:hover {
+    background: lighten($ui-highlight-color, 4%);
+  }
+}
+
+.compose-form__publish {
+  display: flex;
+  justify-content: flex-end;
+  min-width: 0;
+  flex: 0 0 auto;
+  column-gap: 5px;
+
+  .compose-form__publish-button-wrapper {
+    overflow: hidden;
+    padding-top: 10px;
+
+    button {
+      padding: 7px 10px;
+      text-align: center;
+    }
+
+    & > .side_arm {
+      width: 36px;
+    }
+  }
+}
+
+.language-dropdown {
+  &__dropdown {
+    background: $simple-background-color;
+    box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
+    border-radius: 4px;
+    overflow: hidden;
+    z-index: 2;
+
+    &.top {
+      transform-origin: 50% 100%;
+    }
+
+    &.bottom {
+      transform-origin: 50% 0;
+    }
+
+    .emoji-mart-search {
+      padding-inline-end: 10px;
+    }
+
+    .emoji-mart-search-icon {
+      inset-inline-end: 10px + 5px;
+    }
+
+    .emoji-mart-scroll {
+      padding: 0 10px 10px;
+    }
+
+    &__results {
+      &__item {
+        cursor: pointer;
+        color: $inverted-text-color;
+        font-weight: 500;
+        padding: 10px;
+        border-radius: 4px;
+
+        &:focus,
+        &:active,
+        &:hover {
+          background: $ui-secondary-color;
+        }
+
+        &__common-name {
+          color: $darker-text-color;
+        }
+
+        &.active {
+          background: $ui-highlight-color;
+          color: $primary-text-color;
+          outline: 0;
+
+          .language-dropdown__dropdown__results__item__common-name {
+            color: $secondary-text-color;
+          }
+
+          &:hover {
+            background: lighten($ui-highlight-color, 4%);
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/components/directory.scss b/app/javascript/flavours/blobfox/styles/components/directory.scss
new file mode 100644
index 00000000000000..db9a23bce2f550
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/components/directory.scss
@@ -0,0 +1,68 @@
+.scrollable .account-card {
+  margin: 10px;
+  background: lighten($ui-base-color, 8%);
+}
+
+.scrollable .account-card__title__avatar {
+  img,
+  .account__avatar {
+    border-color: lighten($ui-base-color, 8%);
+  }
+}
+
+.scrollable .account-card__bio::after {
+  background: linear-gradient(
+    to left,
+    lighten($ui-base-color, 8%),
+    transparent
+  );
+}
+
+.filter-form {
+  background: $ui-base-color;
+
+  &__column {
+    padding: 10px 15px;
+    padding-bottom: 0;
+  }
+
+  .radio-button {
+    display: block;
+  }
+}
+
+.radio-button {
+  font-size: 14px;
+  position: relative;
+  display: inline-block;
+  padding: 6px 0;
+  line-height: 18px;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  cursor: pointer;
+
+  input[type='radio'],
+  input[type='checkbox'] {
+    display: none;
+  }
+
+  &__input {
+    display: inline-block;
+    position: relative;
+    border: 1px solid $ui-primary-color;
+    box-sizing: border-box;
+    width: 18px;
+    height: 18px;
+    flex: 0 0 auto;
+    margin-inline-end: 10px;
+    top: -1px;
+    border-radius: 50%;
+    vertical-align: middle;
+
+    &.checked {
+      border-color: lighten($ui-highlight-color, 4%);
+      background: lighten($ui-highlight-color, 4%);
+    }
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/components/domains.scss b/app/javascript/flavours/blobfox/styles/components/domains.scss
new file mode 100644
index 00000000000000..a99ccd02b60693
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/components/domains.scss
@@ -0,0 +1,23 @@
+.domain {
+  padding: 10px;
+  border-bottom: 1px solid lighten($ui-base-color, 8%);
+
+  .domain__domain-name {
+    flex: 1 1 auto;
+    display: block;
+    color: $primary-text-color;
+    text-decoration: none;
+    font-size: 14px;
+    font-weight: 500;
+  }
+}
+
+.domain__wrapper {
+  display: flex;
+}
+
+.domain_buttons {
+  height: 18px;
+  padding: 10px;
+  white-space: nowrap;
+}
diff --git a/app/javascript/flavours/blobfox/styles/components/doodle.scss b/app/javascript/flavours/blobfox/styles/components/doodle.scss
new file mode 100644
index 00000000000000..eb053c14db53f1
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/components/doodle.scss
@@ -0,0 +1,86 @@
+$doodle-background: #d9e1e8;
+
+.doodle-modal {
+  width: unset;
+}
+
+.doodle-modal__container {
+  background: $doodle-background;
+  text-align: center;
+  line-height: 0; // remove weird gap under canvas
+  canvas {
+    border: 5px solid $doodle-background;
+  }
+}
+
+.doodle-modal__action-bar {
+  .filler {
+    flex-grow: 1;
+    margin: 0;
+    padding: 0;
+  }
+
+  .doodle-toolbar {
+    line-height: 1;
+    display: flex;
+    flex-direction: column;
+    flex-grow: 0;
+    justify-content: space-around;
+
+    &.with-inputs {
+      label {
+        display: inline-block;
+        width: 70px;
+        text-align: end;
+        margin-inline-end: 2px;
+      }
+
+      input[type='number'],
+      input[type='text'] {
+        width: 40px;
+      }
+
+      span.val {
+        display: inline-block;
+        text-align: start;
+        width: 50px;
+      }
+    }
+  }
+
+  .doodle-palette {
+    padding-inline-end: 0 !important;
+    border: 1px solid black;
+    line-height: 0.2rem;
+    flex-grow: 0;
+    background: white;
+
+    button {
+      appearance: none;
+      width: 1rem;
+      height: 1rem;
+      margin: 0;
+      padding: 0;
+      text-align: center;
+      color: black;
+      text-shadow: 0 0 1px white;
+      cursor: pointer;
+      box-shadow: inset 0 0 1px rgba(white, 0.5);
+      border: 1px solid black;
+      outline-offset: -1px;
+
+      &.foreground {
+        outline: 1px dashed white;
+      }
+
+      &.background {
+        outline: 1px dashed red;
+      }
+
+      &.foreground.background {
+        outline: 1px dashed red;
+        border-color: white;
+      }
+    }
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/components/drawer.scss b/app/javascript/flavours/blobfox/styles/components/drawer.scss
new file mode 100644
index 00000000000000..dcccc0acbb175e
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/components/drawer.scss
@@ -0,0 +1,298 @@
+.drawer {
+  width: 300px;
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+  overflow-y: hidden;
+  padding: 10px 5px;
+  flex: none;
+
+  &:first-child {
+    padding-inline-start: 10px;
+  }
+
+  &:last-child {
+    padding-inline-end: 10px;
+  }
+
+  @include single-column('screen and (max-width: 630px)') {
+    flex: auto;
+  }
+
+  @include limited-single-column('screen and (max-width: 630px)') {
+    &,
+    &:first-child,
+    &:last-child {
+      padding: 0;
+    }
+  }
+
+  .wide & {
+    min-width: 300px;
+    max-width: 400px;
+    flex: 1 1 200px;
+  }
+
+  @include single-column('screen and (max-width: 630px)') {
+    :root & {
+      //  Overrides `.wide` for single-column view
+      flex: auto;
+      width: 100%;
+      min-width: 0;
+      max-width: none;
+      padding: 0;
+    }
+  }
+
+  .react-swipeable-view-container & {
+    height: 100%;
+  }
+}
+
+.drawer__header {
+  flex: none;
+  font-size: 16px;
+  background: lighten($ui-base-color, 8%);
+  margin-bottom: 10px;
+  display: flex;
+  flex-direction: row;
+  border-radius: 4px;
+  overflow: hidden;
+
+  & > * {
+    display: block;
+    box-sizing: border-box;
+    border-bottom: 2px solid transparent;
+    padding: 15px 5px 13px;
+    height: 48px;
+    flex: 1 1 auto;
+    color: $darker-text-color;
+    text-align: center;
+    text-decoration: none;
+    cursor: pointer;
+  }
+
+  a {
+    transition: background 100ms ease-in;
+
+    &:focus,
+    &:hover {
+      outline: none;
+      background: lighten($ui-base-color, 3%);
+      transition: background 200ms ease-out;
+    }
+  }
+}
+
+.search {
+  position: relative;
+  margin-bottom: 10px;
+  flex: none;
+
+  @include limited-single-column(
+    'screen and (max-width: #{$no-gap-breakpoint})'
+  ) {
+    margin-bottom: 0;
+  }
+  @include single-column('screen and (max-width: 630px)') {
+    font-size: 16px;
+  }
+}
+
+.navigation-bar {
+  padding: 10px;
+  color: $darker-text-color;
+  display: flex;
+  align-items: center;
+
+  a {
+    color: inherit;
+    text-decoration: none;
+  }
+
+  .acct {
+    display: block;
+    color: $secondary-text-color;
+    font-weight: 500;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
+}
+
+.navigation-bar__profile {
+  flex: 1 1 auto;
+  margin-inline-start: 8px;
+  overflow: hidden;
+}
+
+.drawer--results {
+  overflow-x: hidden;
+  overflow-y: scroll;
+}
+
+.search-results__section {
+  border-bottom: 1px solid lighten($ui-base-color, 8%);
+
+  &:last-child {
+    border-bottom: 0;
+  }
+
+  &__header {
+    background: darken($ui-base-color, 4%);
+    border-bottom: 1px solid lighten($ui-base-color, 8%);
+    padding: 15px;
+    font-weight: 500;
+    font-size: 14px;
+    color: $darker-text-color;
+    display: flex;
+    justify-content: space-between;
+
+    h3 .fa {
+      margin-inline-end: 5px;
+    }
+
+    button {
+      color: $highlight-text-color;
+      padding: 0;
+      border: 0;
+      background: 0;
+      font: inherit;
+
+      &:hover,
+      &:active,
+      &:focus {
+        text-decoration: underline;
+      }
+    }
+  }
+
+  .account:last-child,
+  & > div:last-child .status {
+    border-bottom: 0;
+  }
+
+  & > .hashtag {
+    display: block;
+    padding: 10px;
+    color: $secondary-text-color;
+    text-decoration: none;
+
+    &:hover,
+    &:active,
+    &:focus {
+      color: lighten($secondary-text-color, 4%);
+      text-decoration: underline;
+    }
+  }
+}
+
+.drawer__pager {
+  box-sizing: border-box;
+  padding: 0;
+  flex-grow: 1;
+  position: relative;
+  overflow: hidden;
+  display: flex;
+  border-radius: 4px;
+}
+
+.drawer__inner {
+  position: absolute;
+  top: 0;
+  inset-inline-start: 0;
+  background: lighten($ui-base-color, 13%);
+  box-sizing: border-box;
+  padding: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  overflow-y: auto;
+  width: 100%;
+  height: 100%;
+
+  &.darker {
+    background: $ui-base-color;
+  }
+}
+
+.drawer__inner__mastodon {
+  background: lighten($ui-base-color, 13%)
+    url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color($ui-base-color)}"/></svg>')
+    no-repeat bottom / 100% auto;
+  flex: 1;
+  min-height: 47px;
+  display: none;
+
+  > img {
+    display: block;
+    object-fit: contain;
+    object-position: bottom left;
+    width: 85%;
+    height: 100%;
+    pointer-events: none;
+    user-select: none;
+  }
+
+  > .mastodon {
+    display: block;
+    width: 100%;
+    height: 100%;
+    border: 0;
+    cursor: inherit;
+  }
+
+  @media screen and (height >= 640px) {
+    display: block;
+  }
+}
+
+.pseudo-drawer {
+  background: lighten($ui-base-color, 13%);
+  font-size: 13px;
+  text-align: start;
+}
+
+.drawer__backdrop {
+  cursor: pointer;
+  position: absolute;
+  top: 0;
+  inset-inline-start: 0;
+  width: 100%;
+  height: 100%;
+  background: rgba($base-overlay-background, 0.5);
+}
+
+@for $i from 0 through 3 {
+  .mbstobon-#{$i} .drawer__inner__mastodon {
+    @if $i == 3 {
+      background:
+        url('~flavours/blobfox/images/wave-drawer.png')
+          no-repeat
+          bottom /
+          100%
+          auto,
+        lighten($ui-base-color, 13%);
+    } @else {
+      background:
+        url('~flavours/blobfox/images/wave-drawer-blobfoxed.png')
+          no-repeat
+          bottom /
+          100%
+          auto,
+        lighten($ui-base-color, 13%);
+    }
+
+    & > .mastodon {
+      background: url('~flavours/blobfox/images/mbstobon-ui-#{$i}.png')
+        no-repeat
+        left
+        bottom /
+        contain;
+
+      @if $i != 3 {
+        filter: contrast(50%) brightness(50%);
+      }
+    }
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/components/emoji.scss b/app/javascript/flavours/blobfox/styles/components/emoji.scss
new file mode 100644
index 00000000000000..f76288978d630d
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/components/emoji.scss
@@ -0,0 +1,104 @@
+.emojione {
+  font-size: inherit;
+  vertical-align: middle;
+  object-fit: contain;
+  margin: -0.2ex 0.15em 0.2ex;
+  width: 16px;
+  height: 16px;
+
+  img {
+    width: auto;
+  }
+}
+
+.emoji-picker-dropdown__menu {
+  background: $simple-background-color;
+  position: relative;
+  box-shadow: 4px 4px 6px rgba($base-shadow-color, 0.4);
+  border-radius: 4px;
+  margin-top: 5px;
+  z-index: 2;
+
+  .emoji-mart-scroll {
+    transition: opacity 200ms ease;
+  }
+
+  &.selecting .emoji-mart-scroll {
+    opacity: 0.5;
+  }
+}
+
+.emoji-picker-dropdown__modifiers {
+  position: absolute;
+  top: 60px;
+  inset-inline-end: 11px;
+  cursor: pointer;
+}
+
+.emoji-picker-dropdown__modifiers__menu {
+  position: absolute;
+  z-index: 4;
+  top: -4px;
+  inset-inline-start: -8px;
+  background: $simple-background-color;
+  border-radius: 4px;
+  box-shadow: 1px 2px 6px rgba($base-shadow-color, 0.2);
+  overflow: hidden;
+
+  button {
+    display: block;
+    cursor: pointer;
+    border: 0;
+    padding: 4px 8px;
+    background: transparent;
+
+    &:hover,
+    &:focus,
+    &:active {
+      background: rgba($ui-secondary-color, 0.4);
+    }
+  }
+
+  .emoji-mart-emoji {
+    height: 22px;
+  }
+}
+
+.emoji-mart-emoji {
+  span {
+    background-repeat: no-repeat;
+  }
+}
+
+.emoji-button {
+  display: block;
+  padding-top: 5px;
+  padding-bottom: 2px;
+  padding-inline-start: 2px;
+  padding-inline-end: 5px;
+  outline: 0;
+  cursor: pointer;
+
+  &:active,
+  &:focus {
+    outline: 0 !important;
+  }
+
+  img {
+    filter: grayscale(100%);
+    opacity: 0.8;
+    display: block;
+    margin: 0;
+    width: 22px;
+    height: 22px;
+  }
+
+  &:hover,
+  &:active,
+  &:focus {
+    img {
+      opacity: 1;
+      filter: none;
+    }
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/components/emoji_picker.scss b/app/javascript/flavours/blobfox/styles/components/emoji_picker.scss
new file mode 100644
index 00000000000000..e402838dbf9641
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/components/emoji_picker.scss
@@ -0,0 +1,261 @@
+.emoji-mart {
+  &,
+  * {
+    box-sizing: border-box;
+    line-height: 1.15;
+  }
+
+  font-size: 13px;
+  display: inline-block;
+  color: $inverted-text-color;
+
+  .emoji-mart-emoji {
+    padding: 6px;
+  }
+}
+
+.emoji-mart-bar {
+  border: 0 solid darken($ui-secondary-color, 8%);
+
+  &:first-child {
+    border-bottom-width: 1px;
+    border-top-left-radius: 5px;
+    border-top-right-radius: 5px;
+    background: $ui-secondary-color;
+  }
+
+  &:last-child {
+    border-top-width: 1px;
+    border-bottom-left-radius: 5px;
+    border-bottom-right-radius: 5px;
+    display: none;
+  }
+}
+
+.emoji-mart-anchors {
+  display: flex;
+  justify-content: space-between;
+  padding: 0 6px;
+  color: $lighter-text-color;
+  line-height: 0;
+}
+
+.emoji-mart-anchor {
+  position: relative;
+  flex: 1;
+  text-align: center;
+  padding: 12px 4px;
+  overflow: hidden;
+  transition: color 0.1s ease-out;
+  cursor: pointer;
+  background: transparent;
+  border: 0;
+
+  &:hover {
+    color: darken($lighter-text-color, 4%);
+  }
+}
+
+.emoji-mart-anchor-selected {
+  color: $highlight-text-color;
+
+  &:hover {
+    color: darken($highlight-text-color, 4%);
+  }
+
+  .emoji-mart-anchor-bar {
+    bottom: 0;
+  }
+}
+
+.emoji-mart-anchor-bar {
+  position: absolute;
+  bottom: -3px;
+  inset-inline-start: 0;
+  width: 100%;
+  height: 3px;
+  background-color: darken($ui-highlight-color, 3%);
+}
+
+.emoji-mart-anchors {
+  i {
+    display: inline-block;
+    width: 100%;
+    max-width: 22px;
+  }
+
+  svg {
+    fill: currentColor;
+    max-height: 18px;
+  }
+}
+
+.emoji-mart-scroll {
+  overflow-y: scroll;
+  height: 270px;
+  max-height: 35vh;
+  padding: 0 6px 6px;
+  background: $simple-background-color;
+  will-change: transform;
+
+  &::-webkit-scrollbar-track:hover,
+  &::-webkit-scrollbar-track:active {
+    background-color: rgba($base-overlay-background, 0.3);
+  }
+}
+
+.emoji-mart-search {
+  padding: 10px;
+  padding-inline-end: 45px;
+  background: $simple-background-color;
+  position: relative;
+
+  input {
+    font-size: 16px;
+    font-weight: 400;
+    padding: 7px 9px;
+    padding-inline-end: 25px;
+    font-family: inherit;
+    display: block;
+    width: 100%;
+    background: rgba($ui-secondary-color, 0.3);
+    color: $inverted-text-color;
+    border: 1px solid $ui-secondary-color;
+    border-radius: 4px;
+
+    &::-moz-focus-inner {
+      border: 0;
+    }
+
+    &::-moz-focus-inner,
+    &:focus,
+    &:active {
+      outline: 0 !important;
+    }
+
+    &::-webkit-search-cancel-button {
+      display: none;
+    }
+  }
+}
+
+.emoji-mart-search-icon {
+  position: absolute;
+  top: 18px;
+  inset-inline-end: 45px + 5px;
+  z-index: 2;
+  padding: 2px 5px 1px;
+  border: 0;
+  background: none;
+  transition: all 100ms linear;
+  transition-property: opacity;
+  pointer-events: auto;
+  opacity: 0.7;
+
+  &:disabled {
+    cursor: default;
+    pointer-events: none;
+    opacity: 0.3;
+  }
+
+  svg {
+    fill: $action-button-color;
+  }
+}
+
+.emoji-mart-category .emoji-mart-emoji {
+  cursor: pointer;
+
+  span {
+    z-index: 1;
+    position: relative;
+    text-align: center;
+  }
+
+  &:hover::before {
+    z-index: 0;
+    content: '';
+    position: absolute;
+    top: 0;
+    inset-inline-start: 0;
+    width: 100%;
+    height: 100%;
+    background-color: rgba($ui-secondary-color, 0.7);
+    border-radius: 100%;
+  }
+}
+
+.emoji-mart-category-label {
+  z-index: 2;
+  position: relative;
+  position: -webkit-sticky;
+  position: sticky;
+  top: 0;
+
+  span {
+    display: block;
+    width: 100%;
+    font-weight: 500;
+    padding: 5px 6px;
+    background: $simple-background-color;
+  }
+}
+
+/* For screenreaders only, via https://stackoverflow.com/a/19758620 */
+.emoji-mart-sr-only {
+  position: absolute;
+  width: 1px;
+  height: 1px;
+  padding: 0;
+  margin: -1px;
+  overflow: hidden;
+  clip: rect(0, 0, 0, 0);
+  border: 0;
+}
+
+.emoji-mart-category-list {
+  margin: 0;
+  padding: 0;
+}
+
+.emoji-mart-category-list li {
+  list-style: none;
+  margin: 0;
+  padding: 0;
+  display: inline-block;
+}
+
+.emoji-mart-emoji {
+  position: relative;
+  display: inline-block;
+  background: transparent;
+  border: 0;
+  padding: 0;
+  font-size: 0;
+
+  span {
+    width: 22px;
+    height: 22px;
+  }
+}
+
+.emoji-mart-no-results {
+  font-size: 14px;
+  color: $light-text-color;
+  text-align: center;
+  padding: 5px 6px;
+  padding-top: 70px;
+
+  .emoji-mart-no-results-label {
+    margin-top: 0.2em;
+  }
+
+  .emoji-mart-emoji:hover::before {
+    cursor: default;
+    content: none;
+  }
+}
+
+.emoji-mart-preview {
+  display: none;
+}
diff --git a/app/javascript/flavours/blobfox/styles/components/explore.scss b/app/javascript/flavours/blobfox/styles/components/explore.scss
new file mode 100644
index 00000000000000..79da9f21668365
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/components/explore.scss
@@ -0,0 +1,147 @@
+.account-card__header {
+  position: relative;
+}
+
+.explore__search-header {
+  background: darken($ui-base-color, 4%);
+  justify-content: center;
+  align-items: center;
+  padding: 15px;
+
+  .search {
+    width: 100%;
+    margin-bottom: 0;
+  }
+
+  .search__input {
+    border: 1px solid lighten($ui-base-color, 8%);
+    padding: 10px;
+  }
+
+  .search__popout {
+    border: 1px solid lighten($ui-base-color, 8%);
+  }
+
+  .search .fa {
+    top: 10px;
+    inset-inline-end: 10px;
+    color: $dark-text-color;
+  }
+
+  .search .fa-times-circle {
+    top: 12px;
+  }
+}
+
+.explore__search-results {
+  flex: 1 1 auto;
+  display: flex;
+  flex-direction: column;
+  background: $ui-base-color;
+  border-bottom-left-radius: 4px;
+  border-bottom-right-radius: 4px;
+}
+
+.story {
+  display: flex;
+  align-items: center;
+  color: $primary-text-color;
+  text-decoration: none;
+  padding: 15px;
+  border-bottom: 1px solid lighten($ui-base-color, 8%);
+  gap: 15px;
+
+  &:last-child {
+    border-bottom: 0;
+  }
+
+  &:hover,
+  &:active,
+  &:focus {
+    color: $highlight-text-color;
+
+    .story__details__publisher,
+    .story__details__shared {
+      color: $highlight-text-color;
+    }
+  }
+
+  &__details {
+    flex: 1 1 auto;
+
+    &__publisher {
+      color: $darker-text-color;
+      margin-bottom: 8px;
+    }
+
+    &__title {
+      font-size: 19px;
+      line-height: 24px;
+      font-weight: 500;
+      margin-bottom: 8px;
+    }
+
+    &__shared {
+      color: $darker-text-color;
+    }
+
+    strong {
+      font-weight: 500;
+    }
+  }
+
+  &__thumbnail {
+    flex: 0 0 auto;
+    position: relative;
+    width: 120px;
+    height: 120px;
+
+    .skeleton {
+      width: 100%;
+      height: 100%;
+    }
+
+    img {
+      border-radius: 8px;
+      display: block;
+      margin: 0;
+      width: 100%;
+      height: 100%;
+      object-fit: cover;
+    }
+
+    &__preview {
+      border-radius: 8px;
+      display: block;
+      margin: 0;
+      width: 100%;
+      height: 100%;
+      object-fit: fill;
+      position: absolute;
+      top: 0;
+      inset-inline-start: 0;
+      z-index: 0;
+
+      &--hidden {
+        display: none;
+      }
+    }
+  }
+
+  &.expanded {
+    flex-direction: column;
+
+    .story__thumbnail {
+      order: 1;
+      width: 100%;
+      height: auto;
+      aspect-ratio: 1.91 / 1;
+    }
+
+    .story__details {
+      order: 2;
+      width: 100%;
+      flex: 0 0 auto;
+    }
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/components/index.scss b/app/javascript/flavours/blobfox/styles/components/index.scss
new file mode 100644
index 00000000000000..d94f1236483ff8
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/components/index.scss
@@ -0,0 +1,25 @@
+@import 'misc';
+@import 'boost';
+@import 'accounts';
+@import 'domains';
+@import 'status';
+@import 'modal';
+@import 'compose_form';
+@import 'columns';
+@import 'regeneration_indicator';
+@import 'directory';
+@import 'search';
+@import 'emoji';
+@import 'doodle';
+@import 'drawer';
+@import 'media';
+@import 'sensitive';
+@import 'lists';
+@import 'emoji_picker';
+@import 'local_settings';
+@import 'single_column';
+@import 'announcements';
+@import 'explore';
+@import 'signed_out';
+@import 'privacy_policy';
+@import 'about';
diff --git a/app/javascript/flavours/blobfox/styles/components/lists.scss b/app/javascript/flavours/blobfox/styles/components/lists.scss
new file mode 100644
index 00000000000000..e173016b6784d7
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/components/lists.scss
@@ -0,0 +1,94 @@
+.list-editor {
+  background: $ui-base-color;
+  flex-direction: column;
+  border-radius: 8px;
+  box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
+  width: 380px;
+  overflow: hidden;
+
+  @media screen and (width <= 420px) {
+    width: 90%;
+  }
+
+  h4 {
+    padding: 15px 0;
+    background: lighten($ui-base-color, 13%);
+    font-weight: 500;
+    font-size: 16px;
+    text-align: center;
+    border-radius: 8px 8px 0 0;
+  }
+
+  .drawer__pager {
+    height: 50vh;
+  }
+
+  .drawer__inner {
+    border-radius: 0 0 8px 8px;
+
+    &.backdrop {
+      width: calc(100% - 60px);
+      box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
+      border-radius: 0 0 0 8px;
+    }
+  }
+
+  &__accounts {
+    overflow-y: auto;
+  }
+
+  .account__display-name {
+    &:hover strong {
+      text-decoration: none;
+    }
+  }
+
+  .account__avatar {
+    cursor: default;
+  }
+
+  .search {
+    margin-bottom: 0;
+  }
+}
+
+.list-adder {
+  background: $ui-base-color;
+  flex-direction: column;
+  border-radius: 8px;
+  box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
+  width: 380px;
+  overflow: hidden;
+
+  @media screen and (width <= 420px) {
+    width: 90%;
+  }
+
+  &__account {
+    background: lighten($ui-base-color, 13%);
+  }
+
+  &__lists {
+    background: lighten($ui-base-color, 13%);
+    height: 50vh;
+    border-radius: 0 0 8px 8px;
+    overflow-y: auto;
+  }
+
+  .list {
+    padding: 10px;
+    border-bottom: 1px solid lighten($ui-base-color, 8%);
+  }
+
+  .list__wrapper {
+    display: flex;
+  }
+
+  .list__display-name {
+    flex: 1 1 auto;
+    overflow: hidden;
+    text-decoration: none;
+    font-size: 16px;
+    padding: 10px;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/components/local_settings.scss b/app/javascript/flavours/blobfox/styles/components/local_settings.scss
new file mode 100644
index 00000000000000..7fa1529cbaa760
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/components/local_settings.scss
@@ -0,0 +1,173 @@
+.blobfox.local-settings {
+  position: relative;
+  display: flex;
+  flex-direction: row;
+  background: $ui-secondary-color;
+  color: $inverted-text-color;
+  border-radius: 8px;
+  height: 80vh;
+  width: 80vw;
+  max-width: 740px;
+  max-height: 450px;
+  overflow: hidden;
+
+  label,
+  legend {
+    display: block;
+    font-size: 14px;
+  }
+
+  .boolean label,
+  .radio_buttons label {
+    position: relative;
+    padding-inline-start: 28px;
+    padding-top: 3px;
+
+    input {
+      position: absolute;
+      inset-inline-start: 0;
+      top: 0;
+    }
+  }
+
+  span.hint {
+    display: block;
+    color: $lighter-text-color;
+  }
+
+  h1 {
+    font-size: 18px;
+    font-weight: 500;
+    line-height: 24px;
+    margin-bottom: 20px;
+  }
+
+  h2 {
+    font-size: 15px;
+    font-weight: 500;
+    line-height: 20px;
+    margin-top: 20px;
+    margin-bottom: 10px;
+  }
+}
+
+.blobfox.local-settings__navigation__item {
+  display: block;
+  padding: 15px 20px;
+  color: inherit;
+  background: lighten($ui-secondary-color, 8%);
+  border: 0;
+  border-bottom: 1px $ui-secondary-color solid;
+  cursor: pointer;
+  text-decoration: none;
+  outline: none;
+  transition: background 0.3s;
+  box-sizing: border-box;
+  width: 100%;
+  text-align: start;
+  font-size: inherit;
+
+  .text-icon-button {
+    color: inherit;
+    transition: unset;
+    unicode-bidi: embed;
+  }
+
+  &:hover {
+    background: $ui-secondary-color;
+  }
+
+  &.active {
+    background: $ui-highlight-color;
+    color: $primary-text-color;
+  }
+
+  &.close,
+  &.close:hover {
+    background: $error-value-color;
+    color: $primary-text-color;
+  }
+}
+
+.blobfox.local-settings__navigation {
+  background: lighten($ui-secondary-color, 8%);
+  width: 212px;
+  font-size: 15px;
+  line-height: 20px;
+  overflow-y: auto;
+}
+
+.blobfox.local-settings__page {
+  display: block;
+  flex: auto;
+  padding: 15px 20px;
+  width: 360px;
+  overflow-y: auto;
+}
+
+.blobfox.local-settings__page__item {
+  margin-bottom: 2px;
+
+  .hint a {
+    color: $lighter-text-color;
+    font-weight: 500;
+    text-decoration: underline;
+
+    &:active,
+    &:focus,
+    &:hover {
+      text-decoration: none;
+    }
+  }
+
+  #mastodon-settings--collapsed-auto-height {
+    width: calc(4ch + 20px);
+  }
+}
+
+.blobfox.local-settings__page__item.string,
+.blobfox.local-settings__page__item.radio_buttons {
+  margin-top: 10px;
+  margin-bottom: 10px;
+}
+
+@media screen and (width <= 630px) {
+  .blobfox.local-settings__navigation {
+    width: 40px;
+    flex-shrink: 0;
+  }
+
+  .blobfox.local-settings__navigation__item {
+    padding: 10px;
+
+    span:last-of-type {
+      display: none;
+    }
+  }
+}
+
+.deprecated-settings-label {
+  white-space: nowrap;
+}
+
+.deprecated-settings-info {
+  text-align: start;
+
+  ul {
+    padding: 10px;
+    margin-inline-start: 12px;
+    list-style: disc inside;
+  }
+
+  a {
+    color: $lighter-text-color;
+    font-weight: 500;
+    text-decoration: underline;
+
+    &:active,
+    &:focus,
+    &:hover {
+      text-decoration: none;
+    }
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/components/media.scss b/app/javascript/flavours/blobfox/styles/components/media.scss
new file mode 100644
index 00000000000000..535af9d0f25ce1
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/components/media.scss
@@ -0,0 +1,795 @@
+.video-error-cover {
+  align-items: center;
+  background: $base-overlay-background;
+  color: $primary-text-color;
+  cursor: pointer;
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  justify-content: center;
+  margin-top: 8px;
+  position: relative;
+  text-align: center;
+  z-index: 100;
+}
+
+.media-spoiler {
+  background: $base-overlay-background;
+  color: $darker-text-color;
+  border: 0;
+  width: 100%;
+  height: 100%;
+
+  &:hover,
+  &:active,
+  &:focus {
+    color: lighten($darker-text-color, 8%);
+  }
+
+  .status__content > & {
+    margin-top: 15px; // Add margin when used bare for NSFW video player
+  }
+  @include fullwidth-gallery;
+}
+
+.media-spoiler__warning {
+  display: block;
+  font-size: 14px;
+}
+
+.media-spoiler__trigger {
+  display: block;
+  font-size: 11px;
+  font-weight: 500;
+}
+
+.media-gallery__item__badges {
+  position: absolute;
+  bottom: 6px;
+  inset-inline-start: 6px;
+  display: flex;
+  gap: 2px;
+}
+
+.media-gallery__gifv__label {
+  display: block;
+  color: $white;
+  background: rgba($black, 0.65);
+  padding: 2px 6px;
+  border-radius: 4px;
+  font-size: 11px;
+  font-weight: 700;
+  z-index: 1;
+  pointer-events: none;
+  line-height: 18px;
+}
+
+.media-gallery {
+  box-sizing: border-box;
+  margin-top: 8px;
+  overflow: hidden;
+  border-radius: 4px;
+  position: relative;
+  width: 100%;
+  min-height: 64px;
+  display: grid;
+  grid-template-columns: 50% 50%;
+  grid-template-rows: 50% 50%;
+  gap: 2px;
+
+  @include fullwidth-gallery;
+}
+
+.media-gallery__item {
+  border: 0;
+  box-sizing: border-box;
+  display: block;
+  position: relative;
+  border-radius: 4px;
+  overflow: hidden;
+
+  &--tall {
+    grid-row: span 2;
+  }
+
+  &--wide {
+    grid-column: span 2;
+  }
+
+  .full-width & {
+    border-radius: 0;
+  }
+
+  &.letterbox {
+    background: $base-shadow-color;
+  }
+}
+
+.media-gallery__item-thumbnail {
+  cursor: zoom-in;
+  display: block;
+  text-decoration: none;
+  color: $secondary-text-color;
+  position: relative;
+  z-index: 1;
+
+  &,
+  img {
+    height: 100%;
+    width: 100%;
+    object-fit: contain;
+
+    &:not(.letterbox) {
+      height: 100%;
+      object-fit: cover;
+    }
+  }
+}
+
+.media-gallery__preview {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+  position: absolute;
+  top: 0;
+  inset-inline-start: 0;
+  z-index: 0;
+  background: $base-overlay-background;
+
+  &--hidden {
+    display: none;
+  }
+}
+
+.media-gallery__gifv {
+  height: 100%;
+  overflow: hidden;
+  position: relative;
+  width: 100%;
+  display: flex;
+  justify-content: center;
+}
+
+.media-gallery__item-gifv-thumbnail {
+  cursor: zoom-in;
+  height: 100%;
+  width: 100%;
+  object-fit: contain;
+  user-select: none;
+
+  &:not(.letterbox) {
+    height: 100%;
+    object-fit: cover;
+  }
+}
+
+.media-gallery__item-thumbnail-label {
+  clip: rect(1px 1px 1px 1px); /* IE6, IE7 */
+  clip: rect(1px, 1px, 1px, 1px);
+  overflow: hidden;
+  position: absolute;
+}
+
+.video-modal__container {
+  max-width: 100vw;
+  max-height: 100vh;
+}
+
+.audio-modal__container {
+  width: 50vw;
+}
+
+.media-modal {
+  width: 100%;
+  height: 100%;
+  position: relative;
+
+  &__close,
+  &__zoom-button {
+    color: rgba($white, 0.7);
+
+    &:hover,
+    &:focus,
+    &:active {
+      color: $white;
+      background-color: rgba($white, 0.15);
+    }
+
+    &:focus {
+      background-color: rgba($white, 0.3);
+    }
+  }
+}
+
+.media-modal__closer {
+  position: absolute;
+  top: 0;
+  inset-inline-start: 0;
+  inset-inline-end: 0;
+  bottom: 0;
+}
+
+.media-modal__navigation {
+  position: absolute;
+  top: 0;
+  inset-inline-start: 0;
+  inset-inline-end: 0;
+  bottom: 0;
+  pointer-events: none;
+  transition: opacity 0.3s linear;
+  will-change: opacity;
+
+  * {
+    pointer-events: auto;
+  }
+
+  &.media-modal__navigation--hidden {
+    opacity: 0;
+
+    * {
+      pointer-events: none;
+    }
+  }
+}
+
+.media-modal__nav {
+  background: transparent;
+  box-sizing: border-box;
+  border: 0;
+  color: rgba($white, 0.7);
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  font-size: 24px;
+  height: 20vmax;
+  margin: auto 0;
+  padding: 30px 15px;
+  position: absolute;
+  top: 0;
+  bottom: 0;
+
+  &:hover,
+  &:focus,
+  &:active {
+    color: $white;
+  }
+}
+
+.media-modal__nav--left {
+  inset-inline-start: 0;
+}
+
+.media-modal__nav--right {
+  inset-inline-end: 0;
+}
+
+.media-modal__overlay {
+  max-width: 600px;
+  position: absolute;
+  inset-inline-start: 0;
+  inset-inline-end: 0;
+  bottom: 0;
+  margin: 0 auto;
+
+  .picture-in-picture__footer {
+    border-radius: 0;
+    background: transparent;
+    padding: 20px 0;
+
+    .icon-button {
+      color: $white;
+
+      &:hover,
+      &:focus,
+      &:active {
+        color: $white;
+        background-color: rgba($white, 0.15);
+      }
+
+      &:focus {
+        background-color: rgba($white, 0.3);
+      }
+
+      &.active {
+        color: $highlight-text-color;
+
+        &:hover,
+        &:focus,
+        &:active {
+          background: rgba($highlight-text-color, 0.15);
+        }
+
+        &:focus {
+          background: rgba($highlight-text-color, 0.3);
+        }
+      }
+
+      &.star-icon.active {
+        color: $gold-star;
+
+        &:hover,
+        &:focus,
+        &:active {
+          background: rgba($gold-star, 0.15);
+        }
+
+        &:focus {
+          background: rgba($gold-star, 0.3);
+        }
+      }
+
+      &.disabled {
+        color: $white;
+        background-color: transparent;
+        cursor: default;
+        opacity: 0.4;
+      }
+    }
+  }
+}
+
+.media-modal__pagination {
+  display: flex;
+  justify-content: center;
+  margin-bottom: 20px;
+}
+
+.media-modal__page-dot {
+  flex: 0 0 auto;
+  background-color: $white;
+  opacity: 0.4;
+  height: 6px;
+  width: 6px;
+  border-radius: 50%;
+  margin: 0 4px;
+  padding: 0;
+  border: 0;
+  font-size: 0;
+  transition: opacity 0.2s ease-in-out;
+
+  &.active {
+    opacity: 1;
+  }
+}
+
+.media-modal__close {
+  position: absolute;
+  inset-inline-end: 8px;
+  top: 8px;
+  z-index: 100;
+}
+
+.detailed,
+.fullscreen {
+  .video-player__volume__current,
+  .video-player__volume::before {
+    bottom: 27px;
+  }
+
+  .video-player__volume__handle {
+    bottom: 23px;
+  }
+}
+
+.audio-player {
+  overflow: hidden;
+  box-sizing: border-box;
+  position: relative;
+  background: darken($ui-base-color, 8%);
+  border-radius: 4px;
+  padding-bottom: 44px;
+  width: 100%;
+
+  &.editable {
+    border-radius: 0;
+    height: 100%;
+  }
+
+  &.inactive {
+    audio,
+    .video-player__controls {
+      visibility: hidden;
+    }
+  }
+
+  .video-player__volume::before,
+  .video-player__seek::before {
+    background: currentColor;
+    opacity: 0.15;
+  }
+
+  .video-player__seek__buffer {
+    background: currentColor;
+    opacity: 0.2;
+  }
+
+  .video-player__buttons button,
+  .video-player__buttons a {
+    color: currentColor;
+    opacity: 0.75;
+
+    &:active,
+    &:hover,
+    &:focus {
+      color: currentColor;
+      opacity: 1;
+    }
+  }
+
+  .video-player__time-sep,
+  .video-player__time-total,
+  .video-player__time-current {
+    color: currentColor;
+  }
+
+  .video-player__seek::before,
+  .video-player__seek__buffer,
+  .video-player__seek__progress {
+    top: 0;
+  }
+
+  .video-player__seek__handle {
+    top: -4px;
+  }
+
+  .video-player__controls {
+    padding-top: 10px;
+    background: transparent;
+  }
+}
+
+.video-player {
+  overflow: hidden;
+  position: relative;
+  background: $base-shadow-color;
+  max-width: 100%;
+  border-radius: 4px;
+  box-sizing: border-box;
+  color: $white;
+  display: flex;
+  align-items: center;
+
+  &.editable {
+    border-radius: 0;
+    height: 100% !important;
+  }
+
+  &:focus {
+    outline: 0;
+  }
+
+  .detailed-status & {
+    width: 100%;
+    height: 100%;
+  }
+
+  @include fullwidth-gallery;
+
+  video {
+    display: block;
+    max-width: 100vw;
+    max-height: 80vh;
+    z-index: 1;
+    position: relative;
+  }
+
+  &.fullscreen {
+    width: 100% !important;
+    height: 100% !important;
+    margin: 0;
+
+    video {
+      max-width: 100% !important;
+      max-height: 100% !important;
+      width: 100% !important;
+      height: 100% !important;
+      outline: 0;
+    }
+  }
+
+  &.inline {
+    video {
+      object-fit: contain;
+    }
+  }
+
+  &__controls {
+    position: absolute;
+    direction: ltr;
+    z-index: 2;
+    bottom: 0;
+    inset-inline-start: 0;
+    inset-inline-end: 0;
+    box-sizing: border-box;
+    background: linear-gradient(
+      0deg,
+      rgba($base-shadow-color, 0.85) 0,
+      rgba($base-shadow-color, 0.45) 60%,
+      transparent
+    );
+    padding: 0 15px;
+    opacity: 0;
+    transition: opacity 0.1s ease;
+
+    &.active {
+      opacity: 1;
+    }
+  }
+
+  &.inactive {
+    video,
+    .video-player__controls {
+      visibility: hidden;
+    }
+  }
+
+  &__spoiler {
+    display: none;
+    position: absolute;
+    top: 0;
+    inset-inline-start: 0;
+    width: 100%;
+    height: 100%;
+    z-index: 4;
+    border: 0;
+    background: $base-shadow-color;
+    color: $darker-text-color;
+    transition: none;
+    pointer-events: none;
+
+    &.active {
+      display: block;
+      pointer-events: auto;
+
+      &:hover,
+      &:active,
+      &:focus {
+        color: lighten($darker-text-color, 7%);
+      }
+    }
+
+    &__title {
+      display: block;
+      font-size: 14px;
+    }
+
+    &__subtitle {
+      display: block;
+      font-size: 11px;
+      font-weight: 500;
+    }
+  }
+
+  &__buttons-bar {
+    display: flex;
+    justify-content: space-between;
+    padding-bottom: 8px;
+    margin: 0 -5px;
+
+    .video-player__download__icon {
+      color: inherit;
+
+      .fa,
+      &:active .fa,
+      &:hover .fa,
+      &:focus .fa {
+        color: inherit;
+      }
+    }
+  }
+
+  &__buttons {
+    display: flex;
+    flex: 0 1 auto;
+    min-width: 30px;
+    align-items: center;
+    font-size: 16px;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+
+    .player-button {
+      display: inline-block;
+      outline: 0;
+      flex: 0 0 auto;
+      background: transparent;
+      padding: 5px;
+      font-size: 16px;
+      border: 0;
+      color: rgba($white, 0.75);
+
+      &:active,
+      &:hover,
+      &:focus {
+        color: $white;
+      }
+    }
+  }
+
+  &__time {
+    display: inline;
+    flex: 0 1 auto;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    margin: 0 5px;
+  }
+
+  &__time-sep,
+  &__time-total,
+  &__time-current {
+    font-size: 14px;
+    font-weight: 500;
+  }
+
+  &__time-current {
+    color: $white;
+  }
+
+  &__time-sep {
+    display: inline-block;
+    margin: 0 6px;
+  }
+
+  &__time-sep,
+  &__time-total {
+    color: $white;
+  }
+
+  &__volume {
+    flex: 0 0 auto;
+    display: inline-flex;
+    cursor: pointer;
+    height: 24px;
+    position: relative;
+    overflow: hidden;
+
+    .no-reduce-motion & {
+      transition: all 100ms linear;
+    }
+
+    &.active {
+      overflow: visible;
+      width: 50px;
+      margin-inline-end: 16px;
+    }
+
+    &::before {
+      content: '';
+      width: 50px;
+      background: rgba($white, 0.35);
+      border-radius: 4px;
+      display: block;
+      position: absolute;
+      height: 4px;
+      inset-inline-start: 0;
+      top: 50%;
+      transform: translate(0, -50%);
+    }
+
+    &__current {
+      display: block;
+      position: absolute;
+      height: 4px;
+      border-radius: 4px;
+      inset-inline-start: 0;
+      top: 50%;
+      transform: translate(0, -50%);
+      background: lighten($ui-highlight-color, 8%);
+    }
+
+    &__handle {
+      position: absolute;
+      z-index: 3;
+      border-radius: 50%;
+      width: 12px;
+      height: 12px;
+      top: 50%;
+      inset-inline-start: 0;
+      margin-inline-start: -6px;
+      transform: translate(0, -50%);
+      background: lighten($ui-highlight-color, 8%);
+      box-shadow: 1px 2px 6px rgba($base-shadow-color, 0.2);
+      opacity: 0;
+
+      .no-reduce-motion & {
+        transition: opacity 100ms linear;
+      }
+    }
+
+    &.active &__handle {
+      opacity: 1;
+    }
+  }
+
+  &__link {
+    padding: 2px 10px;
+
+    a {
+      text-decoration: none;
+      font-size: 14px;
+      font-weight: 500;
+      color: $white;
+
+      &:hover,
+      &:active,
+      &:focus {
+        text-decoration: underline;
+      }
+    }
+  }
+
+  &__seek {
+    cursor: pointer;
+    height: 24px;
+    position: relative;
+
+    &::before {
+      content: '';
+      width: 100%;
+      background: rgba($white, 0.35);
+      border-radius: 4px;
+      display: block;
+      position: absolute;
+      height: 4px;
+      top: 14px;
+    }
+
+    &__progress,
+    &__buffer {
+      display: block;
+      position: absolute;
+      height: 4px;
+      border-radius: 4px;
+      top: 14px;
+      background: lighten($ui-highlight-color, 8%);
+    }
+
+    &__buffer {
+      background: rgba($white, 0.2);
+    }
+
+    &__handle {
+      position: absolute;
+      z-index: 3;
+      opacity: 0;
+      border-radius: 50%;
+      width: 12px;
+      height: 12px;
+      top: 10px;
+      margin-inline-start: -6px;
+      background: lighten($ui-highlight-color, 8%);
+      box-shadow: 1px 2px 6px rgba($base-shadow-color, 0.2);
+
+      .no-reduce-motion & {
+        transition: opacity 0.1s ease;
+      }
+
+      &.active {
+        opacity: 1;
+      }
+    }
+
+    &:hover {
+      .video-player__seek__handle {
+        opacity: 1;
+      }
+    }
+  }
+
+  &.detailed,
+  &.fullscreen {
+    .video-player__buttons {
+      .player-button {
+        padding-top: 10px;
+        padding-bottom: 10px;
+      }
+    }
+  }
+}
+
+.gifv {
+  video {
+    max-width: 100vw;
+    max-height: 80vh;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/components/misc.scss b/app/javascript/flavours/blobfox/styles/components/misc.scss
new file mode 100644
index 00000000000000..f6d0f5b0731907
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/components/misc.scss
@@ -0,0 +1,1740 @@
+.app-body {
+  -webkit-overflow-scrolling: touch;
+  -ms-overflow-style: -ms-autohiding-scrollbar;
+}
+
+.animated-number {
+  display: inline-flex;
+  flex-direction: column;
+  align-items: stretch;
+  overflow: hidden;
+  position: relative;
+}
+
+.inline-alert {
+  color: $valid-value-color;
+  font-weight: 400;
+
+  .no-reduce-motion & {
+    transition: opacity 200ms ease;
+  }
+}
+
+.link-button {
+  display: block;
+  font-size: 15px;
+  line-height: 20px;
+  color: $highlight-text-color;
+  border: 0;
+  background: transparent;
+  padding: 0;
+  cursor: pointer;
+  text-decoration: none;
+
+  &--destructive {
+    color: $error-value-color;
+  }
+
+  &:hover,
+  &:active {
+    text-decoration: underline;
+  }
+
+  &:disabled {
+    color: $ui-primary-color;
+    cursor: default;
+  }
+}
+
+.button {
+  background-color: $ui-button-background-color;
+  border: 10px none;
+  border-radius: 4px;
+  box-sizing: border-box;
+  color: $ui-button-color;
+  cursor: pointer;
+  display: inline-block;
+  font-family: inherit;
+  font-size: 15px;
+  font-weight: 500;
+  letter-spacing: 0;
+  line-height: 22px;
+  overflow: hidden;
+  padding: 7px 18px;
+  position: relative;
+  text-align: center;
+  text-decoration: none;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  width: auto;
+
+  &:active,
+  &:focus,
+  &:hover {
+    background-color: $ui-button-focus-background-color;
+  }
+
+  &--destructive {
+    &:active,
+    &:focus,
+    &:hover {
+      background-color: $ui-button-destructive-focus-background-color;
+      transition: none;
+    }
+  }
+
+  &:disabled {
+    background-color: $ui-primary-color;
+    cursor: default;
+  }
+
+  &.button-secondary {
+    color: $ui-button-secondary-color;
+    background: transparent;
+    padding: 6px 17px;
+    border: 1px solid $ui-button-secondary-border-color;
+
+    &:active,
+    &:focus,
+    &:hover {
+      border-color: $ui-button-secondary-focus-background-color;
+      color: $ui-button-secondary-focus-color;
+      background-color: $ui-button-secondary-focus-background-color;
+      text-decoration: none;
+    }
+
+    &:disabled {
+      opacity: 0.5;
+    }
+  }
+
+  &.button-tertiary {
+    background: transparent;
+    padding: 6px 17px;
+    color: $ui-button-tertiary-color;
+    border: 1px solid $ui-button-tertiary-border-color;
+
+    &:active,
+    &:focus,
+    &:hover {
+      background-color: $ui-button-tertiary-focus-background-color;
+      color: $ui-button-tertiary-focus-color;
+      border: 0;
+      padding: 7px 18px;
+    }
+
+    &:disabled {
+      opacity: 0.5;
+    }
+
+    &.button--confirmation {
+      color: $valid-value-color;
+      border-color: $valid-value-color;
+
+      &:active,
+      &:focus,
+      &:hover {
+        background: $valid-value-color;
+        color: $primary-text-color;
+      }
+    }
+
+    &.button--destructive {
+      color: $error-value-color;
+      border-color: $error-value-color;
+
+      &:active,
+      &:focus,
+      &:hover {
+        background: $error-value-color;
+        color: $primary-text-color;
+      }
+    }
+  }
+
+  &.button--block {
+    display: block;
+    width: 100%;
+  }
+}
+
+.icon-button {
+  display: inline-block;
+  padding: 0;
+  color: $action-button-color;
+  border: 0;
+  border-radius: 4px;
+  background: transparent;
+  cursor: pointer;
+  transition: all 100ms ease-in;
+  transition-property: background-color, color;
+  text-decoration: none;
+
+  a {
+    color: inherit;
+    text-decoration: none;
+  }
+
+  &:hover,
+  &:active,
+  &:focus {
+    color: lighten($action-button-color, 7%);
+    background-color: rgba($action-button-color, 0.15);
+    transition: all 200ms ease-out;
+    transition-property: background-color, color;
+  }
+
+  &:focus {
+    background-color: rgba($action-button-color, 0.3);
+  }
+
+  &.disabled {
+    color: darken($action-button-color, 13%);
+    background-color: transparent;
+    cursor: default;
+  }
+
+  &.active {
+    color: $highlight-text-color;
+  }
+
+  &.copyable {
+    transition: background 300ms linear;
+  }
+
+  &.copied {
+    background: $valid-value-color;
+    transition: none;
+  }
+
+  &::-moz-focus-inner {
+    border: 0;
+  }
+
+  &::-moz-focus-inner,
+  &:focus,
+  &:active {
+    outline: 0 !important;
+  }
+
+  &.inverted {
+    color: $lighter-text-color;
+
+    &:hover,
+    &:active,
+    &:focus {
+      color: darken($lighter-text-color, 7%);
+      background-color: rgba($lighter-text-color, 0.15);
+    }
+
+    &:focus {
+      background-color: rgba($lighter-text-color, 0.3);
+    }
+
+    &.disabled {
+      color: lighten($lighter-text-color, 7%);
+      background-color: transparent;
+    }
+
+    &.active {
+      color: $highlight-text-color;
+
+      &.disabled {
+        color: lighten($highlight-text-color, 13%);
+      }
+    }
+  }
+
+  &.overlayed {
+    box-sizing: content-box;
+    background: rgba($base-overlay-background, 0.6);
+    color: rgba($primary-text-color, 0.7);
+    border-radius: 4px;
+    padding: 2px;
+
+    &:hover {
+      background: rgba($base-overlay-background, 0.9);
+    }
+  }
+
+  &--with-counter {
+    display: inline-flex;
+    align-items: center;
+    width: auto !important;
+    padding: 0;
+    padding-inline-end: 4px;
+    padding-inline-start: 2px;
+  }
+
+  &__counter {
+    display: inline-block;
+    width: auto;
+    margin-inline-start: 4px;
+    font-size: 12px;
+    font-weight: 500;
+  }
+}
+
+.text-icon,
+.text-icon-button {
+  font-weight: 600;
+  font-size: 11px;
+  line-height: 27px;
+  cursor: default;
+}
+
+.text-icon-button {
+  color: $lighter-text-color;
+  border: 0;
+  border-radius: 4px;
+  background: transparent;
+  cursor: pointer;
+  padding: 0 3px;
+  white-space: nowrap;
+  outline: 0;
+  transition: all 100ms ease-in;
+  transition-property: background-color, color;
+
+  &:hover,
+  &:active,
+  &:focus {
+    color: darken($lighter-text-color, 7%);
+    background-color: rgba($lighter-text-color, 0.15);
+    transition: all 200ms ease-out;
+    transition-property: background-color, color;
+  }
+
+  &:focus {
+    background-color: rgba($lighter-text-color, 0.3);
+  }
+
+  &.disabled {
+    color: lighten($lighter-text-color, 20%);
+    background-color: transparent;
+    cursor: default;
+  }
+
+  &.active {
+    color: $highlight-text-color;
+  }
+
+  &::-moz-focus-inner {
+    border: 0;
+  }
+
+  &::-moz-focus-inner,
+  &:focus,
+  &:active {
+    outline: 0 !important;
+  }
+}
+
+body > [data-popper-placement] {
+  z-index: 3;
+}
+
+.invisible {
+  font-size: 0;
+  line-height: 0;
+  display: inline-block;
+  width: 0;
+  height: 0;
+  position: absolute;
+
+  img,
+  svg {
+    margin: 0 !important;
+    border: 0 !important;
+    padding: 0 !important;
+    width: 0 !important;
+    height: 0 !important;
+  }
+}
+
+.ellipsis {
+  &::after {
+    content: '…';
+  }
+}
+
+.notification__favourite-icon-wrapper {
+  inset-inline-start: 0;
+  position: absolute;
+
+  .fa.star-icon {
+    color: $gold-star;
+  }
+}
+
+.icon-button.star-icon.active {
+  color: $gold-star;
+}
+
+.icon-button.bookmark-icon.active {
+  color: $red-bookmark;
+}
+
+.no-reduce-motion .icon-button.star-icon {
+  &.activate {
+    & > .fa-star {
+      animation: spring-rotate-in 1s linear;
+    }
+  }
+
+  &.deactivate {
+    & > .fa-star {
+      animation: spring-rotate-out 1s linear;
+    }
+  }
+}
+
+.notification__display-name {
+  color: inherit;
+  font-weight: 500;
+  text-decoration: none;
+
+  &:hover {
+    color: $primary-text-color;
+    text-decoration: underline;
+  }
+}
+
+.display-name {
+  display: block;
+  max-width: 100%;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+
+  &__account {
+    text-overflow: ellipsis;
+    overflow: hidden;
+  }
+
+  a {
+    color: inherit;
+    text-decoration: inherit;
+  }
+
+  strong {
+    display: block;
+  }
+
+  > a:hover {
+    strong {
+      text-decoration: underline;
+    }
+  }
+
+  &.inline {
+    padding: 0;
+    height: 18px;
+    font-size: 15px;
+    line-height: 18px;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    overflow: hidden;
+
+    strong {
+      display: inline;
+      height: auto;
+      font-size: inherit;
+      line-height: inherit;
+    }
+
+    span {
+      display: inline;
+      height: auto;
+      font-size: inherit;
+      line-height: inherit;
+    }
+  }
+}
+
+.display-name__html {
+  font-weight: 500;
+}
+
+.display-name__account {
+  font-size: 14px;
+}
+
+.image-loader {
+  position: relative;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-direction: column;
+  scrollbar-width: none; /* Firefox */
+  -ms-overflow-style: none; /* IE 10+ */
+
+  * {
+    scrollbar-width: none; /* Firefox */
+    -ms-overflow-style: none; /* IE 10+ */
+  }
+
+  &::-webkit-scrollbar,
+  *::-webkit-scrollbar {
+    width: 0;
+    height: 0;
+    background: transparent; /* Chrome/Safari/Webkit */
+  }
+
+  .image-loader__preview-canvas {
+    max-width: $media-modal-media-max-width;
+    max-height: $media-modal-media-max-height;
+    background: url('~images/void.png') repeat;
+    object-fit: contain;
+  }
+
+  .loading-bar__container {
+    position: relative;
+  }
+
+  .loading-bar {
+    position: absolute;
+  }
+
+  &.image-loader--amorphous .image-loader__preview-canvas {
+    display: none;
+  }
+}
+
+.zoomable-image {
+  position: relative;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  img {
+    max-width: $media-modal-media-max-width;
+    max-height: $media-modal-media-max-height;
+    width: auto;
+    height: auto;
+    object-fit: contain;
+  }
+}
+
+.dropdown-animation {
+  animation: dropdown 150ms cubic-bezier(0.1, 0.7, 0.1, 1);
+
+  @keyframes dropdown {
+    from {
+      opacity: 0;
+    }
+
+    to {
+      opacity: 1;
+    }
+  }
+
+  .reduce-motion & {
+    animation: none;
+  }
+}
+
+.dropdown {
+  display: inline-block;
+}
+
+.dropdown__content {
+  display: none;
+  position: absolute;
+}
+
+.dropdown-menu__separator {
+  border-bottom: 1px solid var(--dropdown-border-color);
+  margin: 2px 0;
+  height: 0;
+}
+
+.dropdown-menu {
+  background: var(--dropdown-background-color);
+  border: 1px solid var(--dropdown-border-color);
+  padding: 2px;
+  border-radius: 4px;
+  box-shadow: var(--dropdown-shadow);
+  z-index: 9999;
+
+  &__text-button {
+    display: inline;
+    color: inherit;
+    background: transparent;
+    border: 0;
+    margin: 0;
+    padding: 0;
+    font-family: inherit;
+    font-size: inherit;
+    line-height: inherit;
+
+    &:focus {
+      outline: 1px dotted;
+    }
+  }
+
+  &__container {
+    &__header {
+      border-bottom: 1px solid var(--dropdown-border-color);
+      padding: 6px 14px;
+      padding-bottom: 12px;
+      margin-bottom: 4px;
+      font-size: 13px;
+      line-height: 18px;
+      color: $darker-text-color;
+    }
+
+    &__list {
+      list-style: none;
+
+      &--scrollable {
+        max-height: 300px;
+        overflow-y: scroll;
+      }
+    }
+
+    &--loading {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      padding: 30px 45px;
+    }
+  }
+}
+
+.dropdown-menu__item {
+  font-size: 13px;
+  line-height: 18px;
+  font-weight: 500;
+  display: block;
+
+  &--dangerous {
+    color: $error-value-color;
+  }
+
+  a,
+  button {
+    font: inherit;
+    display: block;
+    width: 100%;
+    padding: 6px 14px;
+    border: 0;
+    margin: 0;
+    background: transparent;
+    box-sizing: border-box;
+    text-decoration: none;
+    color: inherit;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    text-align: inherit;
+    border-radius: 4px;
+
+    &:focus,
+    &:hover,
+    &:active {
+      background: var(--dropdown-border-color);
+      outline: 0;
+    }
+  }
+}
+
+.inline-account {
+  display: inline-flex;
+  align-items: center;
+  vertical-align: top;
+
+  .account__avatar {
+    margin-inline-end: 5px;
+    border-radius: 50%;
+  }
+
+  strong {
+    font-weight: 600;
+  }
+}
+
+.static-content {
+  padding: 10px;
+  padding-top: 20px;
+  color: $dark-text-color;
+
+  h1 {
+    font-size: 16px;
+    font-weight: 500;
+    margin-bottom: 40px;
+    text-align: center;
+  }
+
+  p {
+    font-size: 13px;
+    margin-bottom: 20px;
+  }
+}
+
+.column,
+.drawer {
+  flex: 1 1 100%;
+  overflow: hidden;
+}
+
+@media screen and (width >= 631px) {
+  .columns-area {
+    padding: 0;
+  }
+
+  .column,
+  .drawer {
+    flex: 0 0 auto;
+    padding: 10px;
+    padding-inline-start: 5px;
+    padding-inline-end: 5px;
+
+    &:first-child {
+      padding-inline-start: 10px;
+    }
+
+    &:last-child {
+      padding-inline-end: 10px;
+    }
+  }
+
+  .columns-area > div {
+    .column,
+    .drawer {
+      padding-inline-start: 5px;
+      padding-inline-end: 5px;
+    }
+  }
+}
+
+.tabs-bar {
+  box-sizing: border-box;
+  display: flex;
+  background: lighten($ui-base-color, 8%);
+  flex: 0 0 auto;
+  overflow-y: auto;
+}
+
+.tabs-bar__link {
+  display: block;
+  flex: 1 1 auto;
+  padding: 15px 10px;
+  padding-bottom: 13px;
+  color: $primary-text-color;
+  text-decoration: none;
+  text-align: center;
+  font-size: 14px;
+  font-weight: 500;
+  border-bottom: 2px solid lighten($ui-base-color, 8%);
+  transition: all 50ms linear;
+  transition-property: border-bottom, background, color;
+
+  .fa {
+    font-weight: 400;
+    font-size: 16px;
+  }
+
+  &:hover,
+  &:focus,
+  &:active {
+    @include multi-columns('screen and (min-width: 631px)') {
+      background: lighten($ui-base-color, 14%);
+      border-bottom-color: lighten($ui-base-color, 14%);
+    }
+  }
+
+  &.active {
+    border-bottom: 2px solid $ui-highlight-color;
+    color: $highlight-text-color;
+  }
+
+  span {
+    margin-inline-start: 5px;
+    display: none;
+  }
+
+  span.icon {
+    margin-inline-start: 0;
+    display: inline;
+  }
+}
+
+.icon-with-badge {
+  position: relative;
+
+  &__badge {
+    position: absolute;
+    inset-inline-start: 9px;
+    top: -13px;
+    background: $ui-highlight-color;
+    border: 2px solid lighten($ui-base-color, 8%);
+    padding: 1px 6px;
+    border-radius: 6px;
+    font-size: 10px;
+    font-weight: 500;
+    line-height: 14px;
+    color: $primary-text-color;
+  }
+
+  &__issue-badge {
+    position: absolute;
+    inset-inline-start: 11px;
+    bottom: 1px;
+    display: block;
+    background: $error-red;
+    border-radius: 50%;
+    width: 0.625rem;
+    height: 0.625rem;
+  }
+}
+
+.column-link--transparent .icon-with-badge__badge {
+  border-color: darken($ui-base-color, 8%);
+}
+
+.scrollable {
+  overflow-y: scroll;
+  overflow-x: hidden;
+  flex: 1 1 auto;
+  -webkit-overflow-scrolling: touch;
+
+  &.optionally-scrollable {
+    overflow-y: auto;
+  }
+
+  @supports (display: grid) {
+    // hack to fix Chrome <57
+    contain: strict;
+  }
+
+  &--flex {
+    display: flex;
+    flex-direction: column;
+  }
+
+  &__append {
+    flex: 1 1 auto;
+    position: relative;
+    min-height: 120px;
+  }
+
+  .scrollable {
+    flex: 1 1 auto;
+  }
+}
+
+.scrollable.fullscreen {
+  @supports (display: grid) {
+    // hack to fix Chrome <57
+    contain: none;
+  }
+}
+
+.react-toggle {
+  display: inline-block;
+  position: relative;
+  cursor: pointer;
+  background-color: transparent;
+  border: 0;
+  padding: 0;
+  user-select: none;
+  -webkit-tap-highlight-color: rgba($base-overlay-background, 0);
+  -webkit-tap-highlight-color: transparent;
+}
+
+.react-toggle-screenreader-only {
+  border: 0;
+  clip: rect(0 0 0 0);
+  height: 1px;
+  margin: -1px;
+  overflow: hidden;
+  padding: 0;
+  position: absolute;
+  width: 1px;
+}
+
+.react-toggle--disabled {
+  cursor: not-allowed;
+  opacity: 0.5;
+  transition: opacity 0.25s;
+}
+
+.react-toggle-track {
+  width: 50px;
+  height: 24px;
+  padding: 0;
+  border-radius: 30px;
+  background-color: $ui-base-color;
+  transition: background-color 0.2s ease;
+}
+
+.react-toggle:is(:hover, :focus-within):not(.react-toggle--disabled)
+  .react-toggle-track {
+  background-color: darken($ui-base-color, 10%);
+}
+
+.react-toggle--checked .react-toggle-track {
+  background-color: darken($ui-highlight-color, 2%);
+}
+
+.react-toggle--checked:is(:hover, :focus-within):not(.react-toggle--disabled)
+  .react-toggle-track {
+  background-color: $ui-highlight-color;
+}
+
+.react-toggle-track-check {
+  position: absolute;
+  width: 14px;
+  height: 10px;
+  top: 0;
+  bottom: 0;
+  margin-top: auto;
+  margin-bottom: auto;
+  line-height: 0;
+  inset-inline-start: 8px;
+  opacity: 0;
+  transition: opacity 0.25s ease;
+}
+
+.react-toggle--checked .react-toggle-track-check {
+  opacity: 1;
+  transition: opacity 0.25s ease;
+}
+
+.react-toggle-track-x {
+  position: absolute;
+  width: 10px;
+  height: 10px;
+  top: 0;
+  bottom: 0;
+  margin-top: auto;
+  margin-bottom: auto;
+  line-height: 0;
+  inset-inline-end: 10px;
+  opacity: 1;
+  transition: opacity 0.25s ease;
+}
+
+.react-toggle--checked .react-toggle-track-x {
+  opacity: 0;
+}
+
+.react-toggle-thumb {
+  position: absolute;
+  top: 1px;
+  inset-inline-start: 1px;
+  width: 22px;
+  height: 22px;
+  border: 1px solid $ui-base-color;
+  border-radius: 50%;
+  background-color: darken($simple-background-color, 2%);
+  box-sizing: border-box;
+  transition: all 0.25s ease;
+  transition-property: border-color, left;
+}
+
+.react-toggle--checked .react-toggle-thumb {
+  inset-inline-start: 27px;
+  border-color: $ui-highlight-color;
+}
+
+.getting-started__wrapper,
+.getting_started,
+.flex-spacer {
+  background: $ui-base-color;
+}
+
+.getting-started__wrapper {
+  position: relative;
+  overflow-y: auto;
+}
+
+.flex-spacer {
+  flex: 1 1 auto;
+}
+
+.getting-started {
+  background: $ui-base-color;
+  flex: 1 0 auto;
+
+  p {
+    color: $secondary-text-color;
+  }
+
+  a {
+    color: $dark-text-color;
+  }
+
+  &__trends {
+    flex: 0 1 auto;
+    opacity: 1;
+    animation: fade 150ms linear;
+    margin-top: 10px;
+
+    h4 {
+      border-bottom: 1px solid lighten($ui-base-color, 8%);
+      padding: 10px;
+      font-size: 12px;
+      text-transform: uppercase;
+      font-weight: 500;
+
+      a {
+        color: $darker-text-color;
+        text-decoration: none;
+      }
+    }
+
+    @media screen and (height <= 810px) {
+      .trends__item:nth-of-type(3) {
+        display: none;
+      }
+    }
+
+    @media screen and (height <= 720px) {
+      .trends__item:nth-of-type(2) {
+        display: none;
+      }
+    }
+
+    @media screen and (height <= 670px) {
+      display: none;
+    }
+
+    .trends__item {
+      border-bottom: 0;
+      padding: 10px;
+
+      &__current {
+        color: $darker-text-color;
+      }
+    }
+  }
+}
+
+.column-link__badge {
+  display: inline-block;
+  border-radius: 4px;
+  font-size: 12px;
+  line-height: 19px;
+  font-weight: 500;
+  background: $ui-base-color;
+  padding: 4px 8px;
+  margin: -6px 10px;
+}
+
+.keyboard-shortcuts {
+  padding: 8px 0 0;
+  overflow: hidden;
+
+  thead {
+    position: absolute;
+    inset-inline-start: -9999px;
+  }
+
+  td {
+    padding: 0 10px 8px;
+  }
+
+  kbd {
+    display: inline-block;
+    padding: 3px 5px;
+    background-color: lighten($ui-base-color, 8%);
+    border: 1px solid darken($ui-base-color, 4%);
+  }
+}
+
+.setting-text {
+  color: $darker-text-color;
+  background: transparent;
+  border: 0;
+  border-bottom: 2px solid $ui-primary-color;
+  outline: 0;
+  box-sizing: border-box;
+  display: block;
+  font-family: inherit;
+  margin-bottom: 10px;
+  padding: 7px 0;
+  width: 100%;
+
+  &:focus,
+  &:active {
+    color: $primary-text-color;
+    border-bottom-color: $ui-highlight-color;
+  }
+
+  @include limited-single-column('screen and (max-width: 600px)') {
+    font-size: 16px;
+  }
+
+  &.light {
+    color: $inverted-text-color;
+    border-bottom: 2px solid lighten($ui-base-color, 27%);
+
+    &:focus,
+    &:active {
+      color: $inverted-text-color;
+      border-bottom-color: $ui-highlight-color;
+    }
+  }
+}
+
+button.icon-button i.fa-retweet {
+  background-position: 0 0;
+  height: 19px;
+  transition: background-position 0.9s steps(10);
+  transition-duration: 0s;
+  vertical-align: middle;
+  width: 22px;
+
+  &::before {
+    display: none !important;
+  }
+}
+
+button.icon-button.active i.fa-retweet {
+  transition-duration: 0.9s;
+  background-position: 0 100%;
+}
+
+.reduce-motion button.icon-button i.fa-retweet,
+.reduce-motion button.icon-button.active i.fa-retweet {
+  transition: none;
+}
+
+.reduce-motion button.icon-button.disabled i.fa-retweet {
+  color: darken($action-button-color, 13%);
+}
+
+.load-more {
+  display: block;
+  color: $dark-text-color;
+  background-color: transparent;
+  border: 0;
+  font-size: inherit;
+  text-align: center;
+  line-height: inherit;
+  margin: 0;
+  padding: 15px;
+  box-sizing: border-box;
+  width: 100%;
+  clear: both;
+  text-decoration: none;
+
+  &:hover {
+    background: lighten($ui-base-color, 2%);
+  }
+}
+
+.load-gap {
+  border-bottom: 1px solid lighten($ui-base-color, 8%);
+}
+
+.timeline-hint {
+  text-align: center;
+  color: $darker-text-color;
+  padding: 15px;
+  box-sizing: border-box;
+  width: 100%;
+  cursor: default;
+
+  strong {
+    font-weight: 500;
+  }
+
+  a {
+    color: $highlight-text-color;
+    text-decoration: none;
+
+    &:hover,
+    &:focus,
+    &:active {
+      text-decoration: underline;
+      color: lighten($highlight-text-color, 4%);
+    }
+  }
+}
+
+.missing-indicator {
+  padding-top: 20px + 48px;
+
+  .regeneration-indicator__figure {
+    background-image: url('~flavours/blobfox/images/elephant_ui_disappointed.svg');
+  }
+}
+
+.scrollable > div > :first-child .notification__dismiss-overlay > .wrappy {
+  border-top: 1px solid $ui-base-color;
+}
+
+.notification__dismiss-overlay {
+  overflow: hidden;
+  position: absolute;
+  top: 0;
+  inset-inline-end: 0;
+  bottom: -1px;
+  padding-inline-start: 15px; // space for the box shadow to be visible
+  z-index: 999;
+  align-items: center;
+  justify-content: flex-end;
+  cursor: pointer;
+  display: flex;
+
+  .wrappy {
+    width: $dismiss-overlay-width;
+    align-self: stretch;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    background: lighten($ui-base-color, 8%);
+    border-inline-start: 1px solid lighten($ui-base-color, 20%);
+    box-shadow: 0 0 5px black;
+    border-bottom: 1px solid $ui-base-color;
+  }
+
+  .ckbox {
+    border: 2px solid $ui-primary-color;
+    border-radius: 2px;
+    width: 30px;
+    height: 30px;
+    font-size: 20px;
+    color: $darker-text-color;
+    text-shadow: 0 0 5px black;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+  }
+
+  &:focus {
+    outline: 0 !important;
+
+    .ckbox {
+      box-shadow: 0 0 1px 1px $ui-highlight-color;
+    }
+  }
+}
+
+.text-btn {
+  display: inline-block;
+  padding: 0;
+  font-family: inherit;
+  font-size: inherit;
+  color: inherit;
+  border: 0;
+  background: transparent;
+  cursor: pointer;
+}
+
+.loading-indicator {
+  color: $dark-text-color;
+  font-size: 12px;
+  font-weight: 400;
+  text-transform: uppercase;
+  overflow: visible;
+  position: absolute;
+  top: 50%;
+  inset-inline-start: 50%;
+  transform: translate(-50%, -50%);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.circular-progress {
+  color: lighten($ui-base-color, 26%);
+  animation: 1.4s linear 0s infinite normal none running simple-rotate;
+
+  circle {
+    stroke: currentColor;
+    stroke-dasharray: 80px, 200px;
+    stroke-dashoffset: 0;
+    animation: circular-progress 1.4s ease-in-out infinite;
+  }
+}
+
+@keyframes circular-progress {
+  0% {
+    stroke-dasharray: 1px, 200px;
+    stroke-dashoffset: 0;
+  }
+
+  50% {
+    stroke-dasharray: 100px, 200px;
+    stroke-dashoffset: -15px;
+  }
+
+  100% {
+    stroke-dasharray: 100px, 200px;
+    stroke-dashoffset: -125px;
+  }
+}
+
+@keyframes simple-rotate {
+  0% {
+    transform: rotate(0deg);
+  }
+
+  100% {
+    transform: rotate(360deg);
+  }
+}
+
+@keyframes spring-rotate-in {
+  0% {
+    transform: rotate(0deg);
+  }
+
+  30% {
+    transform: rotate(-484.8deg);
+  }
+
+  60% {
+    transform: rotate(-316.7deg);
+  }
+
+  90% {
+    transform: rotate(-375deg);
+  }
+
+  100% {
+    transform: rotate(-360deg);
+  }
+}
+
+@keyframes spring-rotate-out {
+  0% {
+    transform: rotate(-360deg);
+  }
+
+  30% {
+    transform: rotate(124.8deg);
+  }
+
+  60% {
+    transform: rotate(-43.27deg);
+  }
+
+  90% {
+    transform: rotate(15deg);
+  }
+
+  100% {
+    transform: rotate(0deg);
+  }
+}
+
+.spoiler-button {
+  top: 0;
+  inset-inline-start: 0;
+  width: 100%;
+  height: 100%;
+  position: absolute;
+  z-index: 100;
+
+  &--minified {
+    display: flex;
+    inset-inline-start: 4px;
+    top: 4px;
+    width: auto;
+    height: auto;
+    align-items: center;
+  }
+
+  &--click-thru {
+    pointer-events: none;
+  }
+
+  &--hidden {
+    display: none;
+  }
+
+  &__overlay {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background: transparent;
+    width: 100%;
+    height: 100%;
+    padding: 0;
+    margin: 0;
+    border: 0;
+    color: $white;
+
+    &__label {
+      background-color: rgba($black, 0.45);
+      backdrop-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%);
+      border-radius: 6px;
+      padding: 10px 15px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      gap: 8px;
+      flex-direction: column;
+      font-weight: 500;
+      font-size: 14px;
+    }
+
+    &__action {
+      font-weight: 400;
+      font-size: 13px;
+    }
+
+    &:hover,
+    &:focus {
+      .spoiler-button__overlay__label {
+        background-color: rgba($black, 0.9);
+      }
+    }
+  }
+}
+
+.setting-toggle {
+  display: block;
+  line-height: 24px;
+}
+
+.setting-toggle__label,
+.setting-meta__label {
+  color: $darker-text-color;
+  display: inline-block;
+  margin-bottom: 14px;
+  margin-inline-start: 8px;
+  vertical-align: middle;
+}
+
+.column-settings__row .radio-button {
+  display: block;
+}
+
+.setting-meta__label {
+  float: right;
+}
+
+@keyframes heartbeat {
+  0% {
+    transform: scale(1);
+    transform-origin: center center;
+    animation-timing-function: ease-out;
+  }
+
+  10% {
+    transform: scale(0.91);
+    animation-timing-function: ease-in;
+  }
+
+  17% {
+    transform: scale(0.98);
+    animation-timing-function: ease-out;
+  }
+
+  33% {
+    transform: scale(0.87);
+    animation-timing-function: ease-in;
+  }
+
+  45% {
+    transform: scale(1);
+    animation-timing-function: ease-out;
+  }
+}
+
+.pulse-loading {
+  animation: heartbeat 1.5s ease-in-out infinite both;
+}
+
+.upload-area {
+  align-items: center;
+  background: rgba($base-overlay-background, 0.8);
+  display: flex;
+  height: 100vh;
+  justify-content: center;
+  inset-inline-start: 0;
+  opacity: 0;
+  position: fixed;
+  top: 0;
+  visibility: hidden;
+  width: 100vw;
+  z-index: 2000;
+
+  * {
+    pointer-events: none;
+  }
+}
+
+.upload-area__drop {
+  width: 320px;
+  height: 160px;
+  display: flex;
+  box-sizing: border-box;
+  position: relative;
+  padding: 8px;
+}
+
+.upload-area__background {
+  position: absolute;
+  top: 0;
+  inset-inline-end: 0;
+  bottom: 0;
+  inset-inline-start: 0;
+  z-index: -1;
+  border-radius: 4px;
+  background: $ui-base-color;
+  box-shadow: 0 0 5px rgba($base-shadow-color, 0.2);
+}
+
+.upload-area__content {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  text-align: center;
+  color: $secondary-text-color;
+  font-size: 18px;
+  font-weight: 500;
+  border: 2px dashed $ui-base-lighter-color;
+  border-radius: 4px;
+}
+
+.dropdown--active .emoji-button img {
+  opacity: 1;
+  filter: none;
+}
+
+.loading-bar {
+  background-color: $ui-highlight-color;
+  height: 3px;
+  position: fixed;
+  top: 0;
+  inset-inline-start: 0;
+  z-index: 9999;
+}
+
+.icon-badge-wrapper {
+  position: relative;
+}
+
+.icon-badge {
+  position: absolute;
+  display: block;
+  inset-inline-end: -0.25em;
+  top: -0.25em;
+  background-color: $ui-highlight-color;
+  border-radius: 50%;
+  font-size: 75%;
+  width: 1em;
+  height: 1em;
+}
+
+.conversation {
+  display: flex;
+  border-bottom: 1px solid lighten($ui-base-color, 8%);
+  padding: 5px;
+  padding-bottom: 0;
+
+  &:focus {
+    background: lighten($ui-base-color, 2%);
+    outline: 0;
+  }
+
+  &__avatar {
+    flex: 0 0 auto;
+    padding: 10px;
+    padding-top: 12px;
+    position: relative;
+    cursor: pointer;
+  }
+
+  &__unread {
+    display: inline-block;
+    background: $highlight-text-color;
+    border-radius: 50%;
+    width: 0.625rem;
+    height: 0.625rem;
+    margin: -0.1ex 0.15em 0.1ex;
+  }
+
+  &__content {
+    flex: 1 1 auto;
+    padding: 10px 5px;
+    padding-inline-end: 15px;
+    overflow: hidden;
+
+    &__info {
+      overflow: hidden;
+      display: flex;
+      flex-direction: row-reverse;
+      justify-content: space-between;
+    }
+
+    &__relative-time {
+      font-size: 15px;
+      color: $darker-text-color;
+      padding-inline-start: 15px;
+    }
+
+    &__names {
+      color: $darker-text-color;
+      font-size: 15px;
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      margin-bottom: 4px;
+      flex-basis: 90px;
+      flex-grow: 1;
+
+      a {
+        color: $primary-text-color;
+        text-decoration: none;
+
+        &:hover,
+        &:focus,
+        &:active {
+          text-decoration: underline;
+        }
+      }
+    }
+
+    .status__content {
+      margin: 0;
+    }
+  }
+
+  &--unread {
+    background: lighten($ui-base-color, 2%);
+
+    &:focus {
+      background: lighten($ui-base-color, 4%);
+    }
+
+    .conversation__content__info {
+      font-weight: 700;
+    }
+
+    .conversation__content__relative-time {
+      color: $primary-text-color;
+    }
+  }
+}
+
+.ui .flash-message {
+  margin-top: 10px;
+  margin-inline-start: auto;
+  margin-inline-end: auto;
+  margin-bottom: 0;
+  min-width: 75%;
+}
+
+::-webkit-scrollbar-thumb {
+  border-radius: 0;
+}
+
+noscript {
+  text-align: center;
+
+  img {
+    width: 200px;
+    opacity: 0.5;
+    animation: flicker 4s infinite;
+  }
+
+  div {
+    font-size: 14px;
+    margin: 30px auto;
+    color: $secondary-text-color;
+    max-width: 400px;
+
+    a {
+      color: $highlight-text-color;
+      text-decoration: underline;
+
+      &:hover {
+        text-decoration: none;
+      }
+    }
+
+    a {
+      word-break: break-word;
+    }
+  }
+}
+
+@keyframes flicker {
+  0% {
+    opacity: 1;
+  }
+
+  30% {
+    opacity: 0.75;
+  }
+
+  100% {
+    opacity: 1;
+  }
+}
+
+.notification-list {
+  position: fixed;
+  bottom: 2rem;
+  inset-inline-start: 0;
+  z-index: 999;
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+
+.notification-bar {
+  flex: 0 0 auto;
+  position: relative;
+  inset-inline-start: -100%;
+  width: auto;
+  padding: 15px;
+  margin: 0;
+  color: $white;
+  background: rgba($black, 0.85);
+  backdrop-filter: blur(8px);
+  border: 1px solid rgba(lighten($classic-base-color, 4%), 0.85);
+  border-radius: 8px;
+  box-shadow:
+    0 10px 15px -3px rgba($base-shadow-color, 0.25),
+    0 4px 6px -4px rgba($base-shadow-color, 0.25);
+  cursor: default;
+  font-size: 15px;
+  line-height: 21px;
+
+  &.notification-bar-active {
+    inset-inline-start: 1rem;
+  }
+
+  .no-reduce-motion & {
+    transition: 0.5s cubic-bezier(0.89, 0.01, 0.5, 1.1);
+    transform: translateZ(0);
+  }
+}
+
+.notification-bar-title {
+  margin-inline-end: 5px;
+}
+
+.notification-bar-title,
+.notification-bar-action {
+  font-weight: 700;
+}
+
+.notification-bar-action {
+  text-transform: uppercase;
+  margin-inline-start: 10px;
+  cursor: pointer;
+  color: $blurple-300;
+  border-radius: 4px;
+  padding: 0 4px;
+
+  &:hover,
+  &:focus,
+  &:active {
+    background: rgba($ui-base-color, 0.85);
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/components/modal.scss b/app/javascript/flavours/blobfox/styles/components/modal.scss
new file mode 100644
index 00000000000000..5cbe405e0ea7ac
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/components/modal.scss
@@ -0,0 +1,1502 @@
+.modal-container--preloader {
+  background: lighten($ui-base-color, 8%);
+}
+
+.modal-root {
+  position: relative;
+  z-index: 9999;
+}
+
+.modal-root__overlay {
+  position: fixed;
+  top: 0;
+  inset-inline-start: 0;
+  inset-inline-end: 0;
+  bottom: 0;
+  background: rgba($base-overlay-background, 0.7);
+  transition: background 0.5s;
+}
+
+.modal-root__container {
+  position: fixed;
+  top: 0;
+  inset-inline-start: 0;
+  width: 100%;
+  height: 100%;
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  align-content: space-around;
+  z-index: 9999;
+  pointer-events: none;
+  user-select: none;
+}
+
+.modal-root__modal {
+  pointer-events: auto;
+  user-select: text;
+  display: flex;
+}
+
+.media-modal__zoom-button {
+  position: absolute;
+  inset-inline-end: 64px;
+  top: 8px;
+  z-index: 100;
+  pointer-events: auto;
+  transition: opacity 0.3s linear;
+  will-change: opacity;
+}
+
+.media-modal__zoom-button--hidden {
+  pointer-events: none;
+  opacity: 0;
+}
+
+.onboarding-modal,
+.error-modal,
+.embed-modal {
+  background: $ui-secondary-color;
+  color: $inverted-text-color;
+  border-radius: 8px;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+}
+
+.onboarding-modal__pager {
+  height: 80vh;
+  width: 80vw;
+  max-width: 520px;
+  max-height: 470px;
+
+  .react-swipeable-view-container > div {
+    width: 100%;
+    height: 100%;
+    box-sizing: border-box;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    display: flex;
+    user-select: text;
+  }
+}
+
+.error-modal__body {
+  height: 80vh;
+  width: 80vw;
+  max-width: 520px;
+  max-height: 420px;
+  position: relative;
+
+  & > div {
+    position: absolute;
+    top: 0;
+    inset-inline-start: 0;
+    width: 100%;
+    height: 100%;
+    box-sizing: border-box;
+    padding: 25px;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    display: flex;
+    opacity: 0;
+    user-select: text;
+  }
+}
+
+.error-modal__body {
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  text-align: center;
+}
+
+@media screen and (width <= 550px) {
+  .onboarding-modal {
+    width: 100%;
+    height: 100%;
+    border-radius: 0;
+  }
+
+  .onboarding-modal__pager {
+    width: 100%;
+    height: auto;
+    max-width: none;
+    max-height: none;
+    flex: 1 1 auto;
+  }
+}
+
+.onboarding-modal__paginator,
+.error-modal__footer {
+  flex: 0 0 auto;
+  background: darken($ui-secondary-color, 8%);
+  display: flex;
+  padding: 25px;
+
+  & > div {
+    min-width: 33px;
+  }
+
+  .onboarding-modal__nav,
+  .error-modal__nav {
+    color: $lighter-text-color;
+    border: 0;
+    font-size: 14px;
+    font-weight: 500;
+    padding: 10px 25px;
+    line-height: inherit;
+    height: auto;
+    margin: -10px;
+    border-radius: 4px;
+    background-color: transparent;
+
+    &:hover,
+    &:focus,
+    &:active {
+      color: darken($lighter-text-color, 4%);
+      background-color: darken($ui-secondary-color, 16%);
+    }
+
+    &.onboarding-modal__done,
+    &.onboarding-modal__next {
+      color: $inverted-text-color;
+
+      &:hover,
+      &:focus,
+      &:active {
+        color: lighten($inverted-text-color, 4%);
+      }
+    }
+  }
+}
+
+.error-modal__footer {
+  justify-content: center;
+}
+
+.onboarding-modal__dots {
+  flex: 1 1 auto;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.onboarding-modal__dot {
+  width: 14px;
+  height: 14px;
+  border-radius: 14px;
+  background: darken($ui-secondary-color, 16%);
+  margin: 0 3px;
+  cursor: pointer;
+
+  &:hover {
+    background: darken($ui-secondary-color, 18%);
+  }
+
+  &.active {
+    cursor: default;
+    background: darken($ui-secondary-color, 24%);
+  }
+}
+
+.onboarding-modal__page__wrapper {
+  pointer-events: none;
+  padding: 25px;
+  padding-bottom: 0;
+
+  &.onboarding-modal__page__wrapper--active {
+    pointer-events: auto;
+  }
+}
+
+.onboarding-modal__page {
+  cursor: default;
+  line-height: 21px;
+
+  h1 {
+    font-size: 18px;
+    font-weight: 500;
+    color: $inverted-text-color;
+    margin-bottom: 20px;
+  }
+
+  a {
+    color: $highlight-text-color;
+
+    &:hover,
+    &:focus,
+    &:active {
+      color: lighten($highlight-text-color, 4%);
+    }
+  }
+
+  .navigation-bar a {
+    color: inherit;
+  }
+
+  p {
+    font-size: 16px;
+    color: $lighter-text-color;
+    margin-top: 10px;
+    margin-bottom: 10px;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+
+    strong {
+      font-weight: 500;
+      background: $ui-base-color;
+      color: $secondary-text-color;
+      border-radius: 4px;
+      font-size: 14px;
+      padding: 3px 6px;
+
+      @each $lang in $cjk-langs {
+        &:lang(#{$lang}) {
+          font-weight: 700;
+        }
+      }
+    }
+  }
+}
+
+.onboarding-modal__page__wrapper-0 {
+  background: url('~images/elephant_ui_greeting.svg') no-repeat left bottom /
+    auto 250px;
+  height: 100%;
+  padding: 0;
+}
+
+.onboarding-modal__page-one {
+  &__lead {
+    padding: 65px;
+    padding-top: 45px;
+    padding-bottom: 0;
+    margin-bottom: 10px;
+
+    h1 {
+      font-size: 26px;
+      line-height: 36px;
+      margin-bottom: 8px;
+    }
+
+    p {
+      margin-bottom: 0;
+    }
+  }
+
+  &__extra {
+    padding-inline-end: 65px;
+    padding-inline-start: 185px;
+    text-align: center;
+  }
+}
+
+.display-case {
+  text-align: center;
+  font-size: 15px;
+  margin-bottom: 15px;
+
+  &__label {
+    font-weight: 500;
+    color: $inverted-text-color;
+    margin-bottom: 5px;
+    text-transform: uppercase;
+    font-size: 12px;
+  }
+
+  &__case {
+    background: $ui-base-color;
+    color: $secondary-text-color;
+    font-weight: 500;
+    padding: 10px;
+    border-radius: 4px;
+  }
+}
+
+.onboarding-modal__page-two,
+.onboarding-modal__page-three,
+.onboarding-modal__page-four,
+.onboarding-modal__page-five {
+  p {
+    text-align: start;
+  }
+
+  .figure {
+    background: darken($ui-base-color, 8%);
+    color: $secondary-text-color;
+    margin-bottom: 20px;
+    border-radius: 4px;
+    padding: 10px;
+    text-align: center;
+    font-size: 14px;
+    box-shadow: 1px 2px 6px rgba($base-shadow-color, 0.3);
+
+    .onboarding-modal__image {
+      border-radius: 4px;
+      margin-bottom: 10px;
+    }
+
+    &.non-interactive {
+      pointer-events: none;
+      text-align: start;
+    }
+  }
+}
+
+.onboarding-modal__page-four__columns {
+  .row {
+    display: flex;
+    margin-bottom: 20px;
+
+    & > div {
+      flex: 1 1 0;
+      margin: 0 10px;
+
+      &:first-child {
+        margin-inline-start: 0;
+      }
+
+      &:last-child {
+        margin-inline-end: 0;
+      }
+
+      p {
+        text-align: center;
+      }
+    }
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+
+  .column-header {
+    color: $primary-text-color;
+  }
+}
+
+@media screen and (width <= 320px) and (height <= 600px) {
+  .onboarding-modal__page p {
+    font-size: 14px;
+    line-height: 20px;
+  }
+
+  .onboarding-modal__page-two .figure,
+  .onboarding-modal__page-three .figure,
+  .onboarding-modal__page-four .figure,
+  .onboarding-modal__page-five .figure {
+    font-size: 12px;
+    margin-bottom: 10px;
+  }
+
+  .onboarding-modal__page-four__columns .row {
+    margin-bottom: 10px;
+  }
+
+  .onboarding-modal__page-four__columns .column-header {
+    padding: 5px;
+    font-size: 12px;
+  }
+}
+
+.onboard-sliders {
+  display: inline-block;
+  max-width: 30px;
+  max-height: auto;
+  margin-inline-start: 10px;
+}
+
+.doodle-modal,
+.boost-modal,
+.confirmation-modal,
+.report-modal,
+.actions-modal,
+.mute-modal,
+.block-modal,
+.compare-history-modal {
+  background: lighten($ui-secondary-color, 8%);
+  color: $inverted-text-color;
+  border-radius: 8px;
+  overflow: hidden;
+  max-width: 90vw;
+  width: 480px;
+  position: relative;
+  flex-direction: column;
+
+  .status__relative-time {
+    color: $dark-text-color;
+    float: right;
+    font-size: 14px;
+    width: auto;
+    margin: initial;
+    padding: initial;
+  }
+
+  .status__visibility-icon {
+    color: $dark-text-color;
+    font-size: 14px;
+    padding: 0 4px;
+  }
+
+  .status__display-name {
+    display: flex;
+  }
+
+  .status__avatar {
+    height: 48px;
+    width: 48px;
+  }
+
+  .status__content__spoiler-link {
+    color: lighten($secondary-text-color, 8%);
+  }
+}
+
+.boost-modal .status-direct {
+  background-color: inherit;
+}
+
+.actions-modal {
+  .status {
+    background: $white;
+    border-bottom-color: $ui-secondary-color;
+    padding-top: 10px;
+    padding-bottom: 10px;
+  }
+
+  .dropdown-menu__separator {
+    border-bottom-color: $ui-secondary-color;
+  }
+}
+
+.boost-modal__container {
+  overflow-x: scroll;
+  padding: 10px;
+
+  .status {
+    user-select: text;
+    border-bottom: 0;
+  }
+}
+
+.doodle-modal__action-bar,
+.boost-modal__action-bar,
+.confirmation-modal__action-bar,
+.mute-modal__action-bar,
+.block-modal__action-bar {
+  display: flex;
+  justify-content: space-between;
+  background: $ui-secondary-color;
+  padding: 10px;
+  line-height: 36px;
+
+  & > div {
+    flex: 1 1 auto;
+    text-align: end;
+    color: $lighter-text-color;
+    padding-inline-end: 10px;
+  }
+
+  .button {
+    flex: 0 0 auto;
+  }
+}
+
+.boost-modal__status-header {
+  font-size: 15px;
+}
+
+.boost-modal__status-time {
+  float: right;
+  font-size: 14px;
+}
+
+.mute-modal,
+.block-modal {
+  line-height: 24px;
+}
+
+.mute-modal .react-toggle,
+.block-modal .react-toggle {
+  vertical-align: middle;
+}
+
+.report-modal {
+  width: 90vw;
+  max-width: 700px;
+}
+
+.report-dialog-modal {
+  max-width: 90vw;
+  width: 480px;
+  height: 80vh;
+  background: lighten($ui-secondary-color, 8%);
+  color: $inverted-text-color;
+  border-radius: 8px;
+  overflow: hidden;
+  position: relative;
+  flex-direction: column;
+  display: flex;
+
+  &__container {
+    box-sizing: border-box;
+    border-top: 1px solid $ui-secondary-color;
+    padding: 20px;
+    flex-grow: 1;
+    display: flex;
+    flex-direction: column;
+    min-height: 0;
+    overflow: auto;
+  }
+
+  &__title {
+    font-size: 28px;
+    line-height: 33px;
+    font-weight: 700;
+    margin-bottom: 15px;
+
+    @media screen and (height <= 800px) {
+      font-size: 22px;
+    }
+  }
+
+  &__subtitle {
+    font-size: 17px;
+    font-weight: 600;
+    line-height: 22px;
+    margin-bottom: 4px;
+  }
+
+  &__lead {
+    font-size: 17px;
+    line-height: 22px;
+    color: lighten($inverted-text-color, 16%);
+    margin-bottom: 30px;
+
+    a {
+      text-decoration: none;
+      color: $inverted-text-color;
+      font-weight: 500;
+
+      &:hover {
+        text-decoration: underline;
+      }
+    }
+  }
+
+  &__actions {
+    margin-top: 30px;
+    display: flex;
+
+    .button {
+      flex: 1 1 auto;
+    }
+  }
+
+  &__statuses {
+    flex-grow: 1;
+    min-height: 0;
+    overflow: auto;
+  }
+
+  .status__content a {
+    color: $highlight-text-color;
+  }
+
+  .status__content,
+  .status__content p {
+    color: $inverted-text-color;
+  }
+
+  .status__content__spoiler-link {
+    color: $primary-text-color;
+    background: $ui-primary-color;
+
+    &:hover {
+      background: lighten($ui-primary-color, 8%);
+    }
+  }
+
+  .dialog-option .poll__input {
+    border-color: $inverted-text-color;
+    color: $ui-secondary-color;
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+
+    svg {
+      width: 8px;
+      height: auto;
+    }
+
+    &:active,
+    &:focus,
+    &:hover {
+      border-color: lighten($inverted-text-color, 15%);
+      border-width: 4px;
+    }
+
+    &.active {
+      border-color: $inverted-text-color;
+      background: $inverted-text-color;
+    }
+  }
+
+  .poll__option.dialog-option {
+    padding: 15px 0;
+    flex: 0 0 auto;
+    border-bottom: 1px solid $ui-secondary-color;
+
+    &:last-child {
+      border-bottom: 0;
+    }
+
+    & > .poll__option__text {
+      font-size: 13px;
+      color: lighten($inverted-text-color, 16%);
+
+      strong {
+        font-size: 17px;
+        font-weight: 500;
+        line-height: 22px;
+        color: $inverted-text-color;
+        display: block;
+        margin-bottom: 4px;
+
+        &:last-child {
+          margin-bottom: 0;
+        }
+      }
+    }
+  }
+
+  .flex-spacer {
+    background: transparent;
+  }
+
+  &__textarea {
+    display: block;
+    box-sizing: border-box;
+    width: 100%;
+    color: $inverted-text-color;
+    background: $simple-background-color;
+    padding: 10px;
+    font-family: inherit;
+    font-size: 17px;
+    line-height: 22px;
+    resize: vertical;
+    border: 0;
+    outline: 0;
+    border-radius: 4px;
+    margin: 20px 0;
+
+    &::placeholder {
+      color: $dark-text-color;
+    }
+
+    &:focus {
+      outline: 0;
+    }
+  }
+
+  &__toggle {
+    display: flex;
+    align-items: center;
+    margin-bottom: 10px;
+
+    & > span {
+      font-size: 17px;
+      font-weight: 500;
+      margin-inline-start: 10px;
+    }
+  }
+
+  .button.button-secondary {
+    border-color: $inverted-text-color;
+    color: $inverted-text-color;
+    flex: 0 0 auto;
+
+    &:hover,
+    &:focus,
+    &:active {
+      background: transparent;
+      border-color: $ui-button-background-color;
+      color: $ui-button-background-color;
+    }
+  }
+
+  hr {
+    border: 0;
+    background: transparent;
+    margin: 15px 0;
+  }
+
+  .emoji-mart-search {
+    padding-inline-end: 10px;
+  }
+
+  .emoji-mart-search-icon {
+    inset-inline-end: 10px + 5px;
+  }
+}
+
+.report-modal__container {
+  display: flex;
+  border-top: 1px solid $ui-secondary-color;
+
+  @media screen and (width <= 480px) {
+    flex-wrap: wrap;
+    overflow-y: auto;
+  }
+}
+
+.report-modal__statuses,
+.report-modal__comment {
+  box-sizing: border-box;
+  width: 50%;
+
+  @media screen and (width <= 480px) {
+    width: 100%;
+  }
+}
+
+.report-modal__statuses,
+.focal-point-modal__content {
+  flex: 1 1 auto;
+  min-height: 20vh;
+  max-height: 80vh;
+  overflow-y: auto;
+  overflow-x: hidden;
+
+  .status__content a {
+    color: $highlight-text-color;
+  }
+
+  @media screen and (width <= 480px) {
+    max-height: 10vh;
+  }
+}
+
+.focal-point-modal__content {
+  @media screen and (width <= 480px) {
+    max-height: 40vh;
+  }
+}
+
+.setting-divider {
+  background: transparent;
+  border: 0;
+  margin: 0;
+  width: 100%;
+  height: 1px;
+  margin-bottom: 29px;
+}
+
+.report-modal__comment {
+  padding: 20px;
+  border-inline-end: 1px solid $ui-secondary-color;
+  max-width: 320px;
+
+  p {
+    font-size: 14px;
+    line-height: 20px;
+    margin-bottom: 20px;
+  }
+
+  .setting-text {
+    display: block;
+    box-sizing: border-box;
+    width: 100%;
+    margin: 0;
+    color: $inverted-text-color;
+    background: $white;
+    padding: 10px;
+    font-family: inherit;
+    font-size: 14px;
+    resize: none;
+    outline: 0;
+    border-radius: 4px;
+    border: 1px solid $ui-secondary-color;
+    min-height: 100px;
+    max-height: 50vh;
+    margin-bottom: 10px;
+
+    &:focus {
+      border: 1px solid darken($ui-secondary-color, 8%);
+    }
+
+    &__wrapper {
+      background: $white;
+      border: 1px solid $ui-secondary-color;
+      margin-bottom: 10px;
+      border-radius: 4px;
+
+      .setting-text {
+        border: 0;
+        margin-bottom: 0;
+        border-radius: 0;
+
+        &:focus {
+          border: 0;
+        }
+      }
+
+      &__modifiers {
+        color: $inverted-text-color;
+        font-family: inherit;
+        font-size: 14px;
+        background: $white;
+      }
+    }
+
+    &__toolbar {
+      display: flex;
+      justify-content: space-between;
+      margin-bottom: 20px;
+    }
+  }
+
+  .setting-text-label {
+    display: block;
+    color: $inverted-text-color;
+    font-size: 14px;
+    font-weight: 500;
+    margin-bottom: 10px;
+  }
+
+  .setting-toggle {
+    margin-top: 20px;
+    margin-bottom: 24px;
+
+    &__label {
+      color: $inverted-text-color;
+      font-size: 14px;
+    }
+  }
+
+  @media screen and (width <= 480px) {
+    padding: 10px;
+    max-width: 100%;
+    order: 2;
+
+    .setting-toggle {
+      margin-bottom: 4px;
+    }
+  }
+}
+
+.actions-modal {
+  .status {
+    overflow-y: auto;
+    max-height: 300px;
+  }
+
+  strong {
+    display: block;
+    font-weight: 500;
+  }
+
+  max-height: 80vh;
+  max-width: 80vw;
+
+  .actions-modal__item-label {
+    font-weight: 500;
+  }
+
+  ul {
+    overflow-y: auto;
+    flex-shrink: 0;
+    max-height: 80vh;
+
+    &.with-status {
+      max-height: calc(80vh - 75px);
+    }
+
+    li:empty {
+      margin: 0;
+    }
+
+    li:not(:empty) {
+      a {
+        color: $inverted-text-color;
+        display: flex;
+        padding: 12px 16px;
+        font-size: 15px;
+        align-items: center;
+        text-decoration: none;
+
+        &,
+        button {
+          transition: none;
+        }
+
+        &.active,
+        &:hover,
+        &:active,
+        &:focus {
+          &,
+          button {
+            background: $ui-highlight-color;
+            color: $primary-text-color;
+          }
+        }
+
+        & > .react-toggle,
+        & > .icon,
+        button:first-child {
+          margin-inline-end: 10px;
+        }
+      }
+    }
+  }
+}
+
+.confirmation-modal__action-bar,
+.mute-modal__action-bar,
+.block-modal__action-bar {
+  .confirmation-modal__secondary-button {
+    flex-shrink: 1;
+  }
+}
+
+.confirmation-modal__secondary-button,
+.confirmation-modal__cancel-button,
+.mute-modal__cancel-button,
+.block-modal__cancel-button {
+  background-color: transparent;
+  color: $lighter-text-color;
+  font-size: 14px;
+  font-weight: 500;
+
+  &:hover,
+  &:focus,
+  &:active {
+    color: darken($lighter-text-color, 4%);
+    background-color: transparent;
+  }
+}
+
+.confirmation-modal__do_not_ask_again {
+  padding-inline-start: 20px;
+  padding-inline-end: 20px;
+  padding-bottom: 10px;
+  font-size: 14px;
+
+  label,
+  input {
+    vertical-align: middle;
+  }
+}
+
+.confirmation-modal__container,
+.mute-modal__container,
+.block-modal__container,
+.report-modal__target {
+  padding: 30px;
+  font-size: 16px;
+
+  strong {
+    font-weight: 500;
+
+    @each $lang in $cjk-langs {
+      &:lang(#{$lang}) {
+        font-weight: 700;
+      }
+    }
+  }
+
+  select {
+    appearance: none;
+    box-sizing: border-box;
+    font-size: 14px;
+    color: $inverted-text-color;
+    display: inline-block;
+    width: auto;
+    outline: 0;
+    font-family: inherit;
+    background: $simple-background-color
+      url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(darken($simple-background-color, 14%))}'/></svg>")
+      no-repeat right 8px center / auto 16px;
+    border: 1px solid darken($simple-background-color, 14%);
+    border-radius: 4px;
+    padding: 6px 10px;
+    padding-inline-end: 30px;
+  }
+}
+
+.confirmation-modal__container,
+.report-modal__target {
+  text-align: center;
+}
+
+.block-modal,
+.mute-modal {
+  &__explanation {
+    margin-top: 20px;
+  }
+
+  .setting-toggle {
+    margin-top: 20px;
+    margin-bottom: 24px;
+    display: flex;
+    align-items: center;
+
+    &__label {
+      color: $inverted-text-color;
+      margin: 0;
+      margin-inline-start: 8px;
+    }
+  }
+}
+
+.report-modal__target {
+  padding: 15px;
+
+  .report-modal__close {
+    position: absolute;
+    top: 10px;
+    inset-inline-end: 10px;
+  }
+}
+
+.compare-history-modal {
+  .report-modal__target {
+    border-bottom: 1px solid $ui-secondary-color;
+  }
+
+  &__container {
+    padding: 30px;
+    pointer-events: all;
+    overflow-y: auto;
+  }
+
+  .status__content {
+    color: $inverted-text-color;
+    font-size: 19px;
+    line-height: 24px;
+
+    .emojione {
+      width: 24px;
+      height: 24px;
+      margin: -1px 0 0;
+    }
+
+    a {
+      color: $highlight-text-color;
+    }
+
+    hr {
+      height: 0.25rem;
+      padding: 0;
+      background-color: $ui-secondary-color;
+      border: 0;
+      margin: 20px 0;
+    }
+  }
+
+  .media-gallery,
+  .audio-player,
+  .video-player {
+    margin-top: 15px;
+  }
+}
+
+.embed-modal {
+  width: auto;
+  max-width: 80vw;
+  max-height: 80vh;
+
+  h4 {
+    padding: 30px;
+    font-weight: 500;
+    font-size: 16px;
+    text-align: center;
+  }
+
+  .embed-modal__container {
+    padding: 10px;
+
+    .hint {
+      margin-bottom: 15px;
+    }
+
+    .embed-modal__html {
+      outline: 0;
+      box-sizing: border-box;
+      display: block;
+      width: 100%;
+      border: 0;
+      padding: 10px;
+      font-family: mastodon-font-monospace, monospace;
+      background: $ui-base-color;
+      color: $primary-text-color;
+      font-size: 14px;
+      margin: 0;
+      margin-bottom: 15px;
+      border-radius: 4px;
+
+      &::-moz-focus-inner {
+        border: 0;
+      }
+
+      &::-moz-focus-inner,
+      &:focus,
+      &:active {
+        outline: 0 !important;
+      }
+
+      &:focus {
+        background: lighten($ui-base-color, 4%);
+      }
+
+      @media screen and (width <= 600px) {
+        font-size: 16px;
+      }
+    }
+
+    .embed-modal__iframe {
+      width: 400px;
+      max-width: 100%;
+      overflow: hidden;
+      border: 0;
+      border-radius: 4px;
+    }
+  }
+}
+
+.focal-point {
+  position: relative;
+  cursor: move;
+  overflow: hidden;
+  height: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  background: $base-shadow-color;
+
+  img,
+  video,
+  canvas {
+    display: block;
+    max-height: 80vh;
+    width: 100%;
+    height: auto;
+    margin: 0;
+    object-fit: contain;
+    background: $base-shadow-color;
+  }
+
+  &__reticle {
+    position: absolute;
+    width: 100px;
+    height: 100px;
+    transform: translate(-50%, -50%);
+    background: url('~images/reticle.png') no-repeat 0 0;
+    border-radius: 50%;
+    box-shadow: 0 0 0 9999em rgba($base-shadow-color, 0.35);
+  }
+
+  &__overlay {
+    position: absolute;
+    width: 100%;
+    height: 100%;
+    top: 0;
+    inset-inline-start: 0;
+  }
+
+  &__preview {
+    position: absolute;
+    bottom: 10px;
+    inset-inline-end: 10px;
+    z-index: 2;
+    cursor: move;
+    transition: opacity 0.1s ease;
+
+    &:hover {
+      opacity: 0.5;
+    }
+
+    strong {
+      color: $primary-text-color;
+      font-size: 14px;
+      font-weight: 500;
+      display: block;
+      margin-bottom: 5px;
+    }
+
+    div {
+      border-radius: 4px;
+      box-shadow: 0 0 14px rgba($base-shadow-color, 0.2);
+    }
+  }
+
+  @media screen and (width <= 480px) {
+    img,
+    video {
+      max-height: 100%;
+    }
+
+    &__preview {
+      display: none;
+    }
+  }
+}
+
+.filtered-status-info {
+  text-align: start;
+
+  .spoiler__text {
+    margin-top: 20px;
+  }
+
+  .account {
+    border-bottom: 0;
+  }
+
+  .account__display-name strong {
+    color: $inverted-text-color;
+  }
+
+  .status__content__spoiler {
+    display: none;
+
+    &--visible {
+      display: flex;
+    }
+  }
+
+  ul {
+    padding: 10px;
+    margin-inline-start: 12px;
+    list-style: disc inside;
+  }
+
+  .filtered-status-edit-link {
+    color: $action-button-color;
+    text-decoration: none;
+
+    &:hover {
+      text-decoration: underline;
+    }
+  }
+}
+
+.modal-root__container .privacy-dropdown {
+  flex-grow: 0;
+}
+
+.modal-root__container .privacy-dropdown__dropdown {
+  pointer-events: auto;
+  z-index: 9999;
+}
+
+img.modal-warning {
+  display: block;
+  margin: auto;
+  margin-bottom: 15px;
+  width: 60px;
+}
+
+.interaction-modal {
+  max-width: 90vw;
+  width: 600px;
+  background: var(--modal-background-color);
+  border: 1px solid var(--modal-border-color);
+  border-radius: 8px;
+  overflow: visible;
+  position: relative;
+  display: block;
+  padding: 40px;
+
+  h3 {
+    font-size: 22px;
+    line-height: 33px;
+    font-weight: 700;
+    text-align: center;
+  }
+
+  p {
+    font-size: 17px;
+    line-height: 22px;
+    color: $darker-text-color;
+
+    strong {
+      color: $primary-text-color;
+      font-weight: 700;
+    }
+  }
+
+  p.hint {
+    margin-bottom: 14px;
+    font-size: 14px;
+  }
+
+  &__icon {
+    color: $highlight-text-color;
+    margin: 0 5px;
+  }
+
+  &__lead {
+    margin-bottom: 20px;
+
+    h3 {
+      margin-bottom: 15px;
+    }
+  }
+
+  &__login {
+    position: relative;
+    margin-bottom: 20px;
+
+    &__input {
+      @include search-input;
+
+      border: 1px solid lighten($ui-base-color, 8%);
+      padding: 4px 6px;
+      color: $primary-text-color;
+      font-size: 16px;
+      line-height: 18px;
+      display: flex;
+      align-items: center;
+
+      input {
+        background: transparent;
+        color: inherit;
+        font: inherit;
+        border: 0;
+        padding: 15px - 4px 15px - 6px;
+        flex: 1 1 auto;
+
+        &::placeholder {
+          color: lighten($darker-text-color, 4%);
+        }
+
+        &:focus {
+          outline: 0;
+        }
+      }
+
+      .button {
+        flex: 0 0 auto;
+      }
+    }
+
+    .search__popout {
+      margin-top: -1px;
+      padding-top: 5px;
+      padding-bottom: 5px;
+      border: 1px solid lighten($ui-base-color, 8%);
+    }
+
+    &.focused &__input {
+      border-color: $highlight-text-color;
+      background: lighten($ui-base-color, 4%);
+    }
+
+    &.invalid &__input {
+      border-color: $error-red;
+    }
+
+    &.expanded .search__popout {
+      display: block;
+    }
+
+    &.expanded &__input {
+      border-radius: 4px 4px 0 0;
+    }
+  }
+
+  &__choices {
+    display: flex;
+    gap: 40px;
+
+    &__choice {
+      flex: 1;
+      box-sizing: border-box;
+
+      h3 {
+        margin-bottom: 20px;
+      }
+
+      p {
+        color: $darker-text-color;
+        margin-bottom: 20px;
+        font-size: 15px;
+      }
+
+      .button {
+        margin-bottom: 10px;
+
+        &:last-child {
+          margin-bottom: 0;
+        }
+      }
+    }
+  }
+
+  @media screen and (max-width: $no-gap-breakpoint - 1px) {
+    &__choices {
+      flex-direction: column;
+
+      &__choice {
+        margin-top: 40px;
+      }
+    }
+  }
+
+  .link-button {
+    font-size: inherit;
+    display: inline;
+  }
+}
+
+.copypaste {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+
+  input {
+    display: block;
+    font-family: inherit;
+    background: darken($ui-base-color, 8%);
+    border: 1px solid $highlight-text-color;
+    color: $darker-text-color;
+    border-radius: 4px;
+    padding: 6px 9px;
+    line-height: 22px;
+    font-size: 14px;
+    transition: border-color 300ms linear;
+    flex: 1 1 auto;
+    overflow: hidden;
+
+    &:focus {
+      outline: 0;
+      background: darken($ui-base-color, 4%);
+    }
+  }
+
+  .button {
+    flex: 0 0 auto;
+    transition: background 300ms linear;
+  }
+
+  &.copied {
+    input {
+      border: 1px solid $valid-value-color;
+      transition: none;
+    }
+
+    .button {
+      background: $valid-value-color;
+      transition: none;
+    }
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/components/privacy_policy.scss b/app/javascript/flavours/blobfox/styles/components/privacy_policy.scss
new file mode 100644
index 00000000000000..cab78402b214da
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/components/privacy_policy.scss
@@ -0,0 +1,209 @@
+.privacy-policy {
+  background: $ui-base-color;
+  padding: 20px;
+
+  @media screen and (min-width: $no-gap-breakpoint) {
+    border-radius: 4px;
+  }
+
+  &__body {
+    margin-top: 20px;
+  }
+}
+
+.prose {
+  color: $secondary-text-color;
+  font-size: 15px;
+  line-height: 22px;
+
+  p,
+  ul,
+  ol {
+    margin-top: 1.25em;
+    margin-bottom: 1.25em;
+  }
+
+  img {
+    margin-top: 2em;
+    margin-bottom: 2em;
+    max-width: 100%;
+  }
+
+  video {
+    margin-top: 2em;
+    margin-bottom: 2em;
+    max-width: 100%;
+  }
+
+  figure {
+    margin-top: 2em;
+    margin-bottom: 2em;
+
+    figcaption {
+      font-size: 0.875em;
+      line-height: 1.4285714;
+      margin-top: 0.8571429em;
+    }
+  }
+
+  figure > * {
+    margin-top: 0;
+    margin-bottom: 0;
+  }
+
+  h1 {
+    font-size: 1.5em;
+    margin-top: 0;
+    margin-bottom: 1em;
+    line-height: 1.33;
+  }
+
+  h2 {
+    font-size: 1.25em;
+    margin-top: 1.6em;
+    margin-bottom: 0.6em;
+    line-height: 1.6;
+  }
+
+  h3,
+  h4,
+  h5,
+  h6 {
+    margin-top: 1.5em;
+    margin-bottom: 0.5em;
+    line-height: 1.5;
+  }
+
+  ol {
+    counter-reset: list-counter;
+  }
+
+  li {
+    margin-top: 0.5em;
+    margin-bottom: 0.5em;
+  }
+
+  ol > li {
+    counter-increment: list-counter;
+
+    &::before {
+      content: counter(list-counter) '.';
+      position: absolute;
+      inset-inline-start: 0;
+    }
+  }
+
+  ul > li::before {
+    content: '';
+    position: absolute;
+    background-color: $darker-text-color;
+    border-radius: 50%;
+    width: 0.375em;
+    height: 0.375em;
+    top: 0.5em;
+    inset-inline-start: 0.25em;
+  }
+
+  ul > li,
+  ol > li {
+    position: relative;
+    padding-inline-start: 1.75em;
+  }
+
+  & > ul > li p {
+    margin-top: 0.75em;
+    margin-bottom: 0.75em;
+  }
+
+  & > ul > li > *:first-child {
+    margin-top: 1.25em;
+  }
+
+  & > ul > li > *:last-child {
+    margin-bottom: 1.25em;
+  }
+
+  & > ol > li > *:first-child {
+    margin-top: 1.25em;
+  }
+
+  & > ol > li > *:last-child {
+    margin-bottom: 1.25em;
+  }
+
+  ul ul,
+  ul ol,
+  ol ul,
+  ol ol {
+    margin-top: 0.75em;
+    margin-bottom: 0.75em;
+  }
+
+  h1,
+  h2,
+  h3,
+  h4,
+  h5,
+  h6,
+  strong,
+  b {
+    color: $primary-text-color;
+    font-weight: 700;
+  }
+
+  em,
+  i {
+    font-style: italic;
+  }
+
+  a {
+    color: $highlight-text-color;
+    text-decoration: underline;
+
+    &:focus,
+    &:hover,
+    &:active {
+      text-decoration: none;
+    }
+  }
+
+  code {
+    font-size: 0.875em;
+    background: darken($ui-base-color, 8%);
+    border-radius: 4px;
+    padding: 0.2em 0.3em;
+  }
+
+  hr {
+    border: 0;
+    border-top: 1px solid lighten($ui-base-color, 4%);
+    margin-top: 3em;
+    margin-bottom: 3em;
+  }
+
+  hr + * {
+    margin-top: 0;
+  }
+
+  h2 + * {
+    margin-top: 0;
+  }
+
+  h3 + * {
+    margin-top: 0;
+  }
+
+  h4 + *,
+  h5 + *,
+  h6 + * {
+    margin-top: 0;
+  }
+
+  & > :first-child {
+    margin-top: 0;
+  }
+
+  & > :last-child {
+    margin-bottom: 0;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/components/regeneration_indicator.scss b/app/javascript/flavours/blobfox/styles/components/regeneration_indicator.scss
new file mode 100644
index 00000000000000..c65e6a9afcda8c
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/components/regeneration_indicator.scss
@@ -0,0 +1,43 @@
+.regeneration-indicator {
+  text-align: center;
+  font-size: 16px;
+  font-weight: 500;
+  color: $dark-text-color;
+  background: $ui-base-color;
+  cursor: default;
+  display: flex;
+  flex: 1 1 auto;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 20px;
+
+  &__figure {
+    &,
+    img {
+      display: block;
+      width: auto;
+      height: 160px;
+      margin: 0;
+    }
+  }
+
+  &--without-header {
+    padding-top: 20px + 48px;
+  }
+
+  &__label {
+    margin-top: 30px;
+
+    strong {
+      display: block;
+      margin-bottom: 10px;
+      color: $dark-text-color;
+    }
+
+    span {
+      font-size: 15px;
+      font-weight: 400;
+    }
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/components/search.scss b/app/javascript/flavours/blobfox/styles/components/search.scss
new file mode 100644
index 00000000000000..aa54fc26db3a71
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/components/search.scss
@@ -0,0 +1,337 @@
+.search {
+  margin-bottom: 10px;
+  position: relative;
+
+  &__popout {
+    box-sizing: border-box;
+    display: none;
+    position: absolute;
+    inset-inline-start: 0;
+    margin-top: -2px;
+    width: 100%;
+    background: $ui-base-color;
+    border-radius: 0 0 4px 4px;
+    box-shadow: 4px 4px 6px rgba($base-shadow-color, 0.4);
+    z-index: 99;
+    font-size: 13px;
+    padding: 15px 5px;
+
+    h4 {
+      text-transform: uppercase;
+      color: $dark-text-color;
+      font-weight: 500;
+      padding: 0 10px;
+      margin-bottom: 10px;
+    }
+
+    &__menu {
+      margin-bottom: 20px;
+
+      &:last-child {
+        margin-bottom: 0;
+      }
+
+      &__message {
+        color: $dark-text-color;
+        padding: 0 10px;
+      }
+
+      &__item {
+        display: block;
+        box-sizing: border-box;
+        width: 100%;
+        border: 0;
+        font: inherit;
+        background: transparent;
+        color: $darker-text-color;
+        padding: 10px;
+        cursor: pointer;
+        border-radius: 4px;
+        text-align: start;
+        text-overflow: ellipsis;
+        overflow: hidden;
+        white-space: nowrap;
+
+        &--flex {
+          display: flex;
+          justify-content: space-between;
+        }
+
+        .icon-button {
+          transition: none;
+        }
+
+        &:hover,
+        &:focus,
+        &:active,
+        &.selected {
+          background: $ui-highlight-color;
+          color: $primary-text-color;
+
+          .icon-button {
+            color: $primary-text-color;
+          }
+        }
+
+        mark {
+          background: transparent;
+          font-weight: 700;
+          color: $primary-text-color;
+        }
+
+        span {
+          overflow: inherit;
+          text-overflow: inherit;
+        }
+      }
+    }
+  }
+
+  &.active {
+    .search__popout {
+      display: block;
+    }
+  }
+}
+
+.search__input {
+  @include search-input;
+
+  display: block;
+  padding: 15px;
+  padding-inline-end: 30px;
+  line-height: 18px;
+  font-size: 16px;
+
+  &::placeholder {
+    color: lighten($darker-text-color, 4%);
+  }
+
+  &::-moz-focus-inner {
+    border: 0;
+  }
+
+  &::-moz-focus-inner,
+  &:focus,
+  &:active {
+    outline: 0 !important;
+  }
+
+  &:focus {
+    background: lighten($ui-base-color, 4%);
+  }
+}
+
+.search__icon {
+  &::-moz-focus-inner {
+    border: 0;
+  }
+
+  &::-moz-focus-inner,
+  &:focus {
+    outline: 0 !important;
+  }
+
+  .fa {
+    position: absolute;
+    top: 16px;
+    inset-inline-end: 10px;
+    display: inline-block;
+    opacity: 0;
+    transition: all 100ms linear;
+    transition-property: color, transform, opacity;
+    font-size: 18px;
+    width: 18px;
+    height: 18px;
+    color: $secondary-text-color;
+    cursor: default;
+    pointer-events: none;
+
+    &.active {
+      pointer-events: auto;
+      opacity: 0.3;
+    }
+  }
+
+  .fa-search {
+    transform: rotate(0deg);
+
+    &.active {
+      pointer-events: auto;
+      opacity: 0.3;
+    }
+  }
+
+  .fa-times-circle {
+    top: 17px;
+    transform: rotate(0deg);
+    color: $action-button-color;
+    cursor: pointer;
+
+    &.active {
+      transform: rotate(90deg);
+      opacity: 1;
+    }
+
+    &:hover {
+      color: lighten($action-button-color, 7%);
+    }
+  }
+}
+
+.search-results__header {
+  color: $dark-text-color;
+  background: lighten($ui-base-color, 2%);
+  padding: 15px;
+  font-weight: 500;
+  font-size: 16px;
+  cursor: default;
+
+  .fa {
+    display: inline-block;
+    margin-inline-end: 5px;
+  }
+}
+
+.search-results__info {
+  padding: 20px;
+  color: $darker-text-color;
+  text-align: center;
+}
+
+.trends {
+  &__header {
+    color: $dark-text-color;
+    background: lighten($ui-base-color, 2%);
+    border-bottom: 1px solid darken($ui-base-color, 4%);
+    font-weight: 500;
+    padding: 15px;
+    font-size: 16px;
+    cursor: default;
+
+    .fa {
+      display: inline-block;
+      margin-inline-end: 5px;
+    }
+  }
+
+  &__item {
+    display: flex;
+    align-items: center;
+    padding: 15px;
+    border-bottom: 1px solid lighten($ui-base-color, 8%);
+    gap: 15px;
+
+    &:last-child {
+      border-bottom: 0;
+    }
+
+    &__name {
+      flex: 1 1 auto;
+      color: $dark-text-color;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+
+      strong {
+        font-weight: 500;
+      }
+
+      a {
+        color: $darker-text-color;
+        text-decoration: none;
+        font-size: 14px;
+        font-weight: 500;
+        display: block;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+
+        &:hover,
+        &:focus,
+        &:active {
+          span {
+            text-decoration: underline;
+          }
+        }
+      }
+    }
+
+    &__current {
+      flex: 0 0 auto;
+      font-size: 24px;
+      font-weight: 500;
+      text-align: end;
+      color: $secondary-text-color;
+      text-decoration: none;
+    }
+
+    &__sparkline {
+      flex: 0 0 auto;
+      width: 50px;
+
+      path:first-child {
+        fill: rgba($highlight-text-color, 0.25) !important;
+        fill-opacity: 1 !important;
+      }
+
+      path:last-child {
+        stroke: lighten($highlight-text-color, 6%) !important;
+        fill: none !important;
+      }
+    }
+
+    &--requires-review {
+      .trends__item__name {
+        color: $gold-star;
+
+        a {
+          color: $gold-star;
+        }
+      }
+
+      .trends__item__current {
+        color: $gold-star;
+      }
+
+      .trends__item__sparkline {
+        path:first-child {
+          fill: rgba($gold-star, 0.25) !important;
+        }
+
+        path:last-child {
+          stroke: lighten($gold-star, 6%) !important;
+        }
+      }
+    }
+
+    &--disabled {
+      .trends__item__name {
+        color: lighten($ui-base-color, 12%);
+
+        a {
+          color: lighten($ui-base-color, 12%);
+        }
+      }
+
+      .trends__item__current {
+        color: lighten($ui-base-color, 12%);
+      }
+
+      .trends__item__sparkline {
+        path:first-child {
+          fill: rgba(lighten($ui-base-color, 12%), 0.25) !important;
+        }
+
+        path:last-child {
+          stroke: lighten(lighten($ui-base-color, 12%), 6%) !important;
+        }
+      }
+    }
+  }
+
+  &--compact &__item {
+    padding: 10px;
+    padding-inline-end: 28px;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/components/sensitive.scss b/app/javascript/flavours/blobfox/styles/components/sensitive.scss
new file mode 100644
index 00000000000000..c77515eb70fd18
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/components/sensitive.scss
@@ -0,0 +1,26 @@
+.sensitive-info {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  position: absolute;
+  top: 4px;
+  inset-inline-start: 4px;
+  z-index: 100;
+}
+
+.sensitive-marker {
+  margin: 0 3px;
+  border-radius: 2px;
+  padding: 2px 6px;
+  color: rgba($primary-text-color, 0.8);
+  background: rgba($base-overlay-background, 0.5);
+  font-size: 12px;
+  line-height: 18px;
+  text-transform: uppercase;
+  opacity: 0.9;
+  transition: opacity 0.1s ease;
+
+  .media-gallery:hover & {
+    opacity: 1;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/components/signed_out.scss b/app/javascript/flavours/blobfox/styles/components/signed_out.scss
new file mode 100644
index 00000000000000..18492983e5887b
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/components/signed_out.scss
@@ -0,0 +1,110 @@
+.sign-in-banner {
+  padding: 10px;
+
+  p {
+    color: $darker-text-color;
+    margin-bottom: 20px;
+
+    a {
+      color: $secondary-text-color;
+      text-decoration: none;
+      unicode-bidi: isolate;
+
+      &:hover {
+        text-decoration: underline;
+
+        .fa {
+          color: lighten($dark-text-color, 7%);
+        }
+      }
+    }
+  }
+
+  .button {
+    margin-bottom: 10px;
+  }
+}
+
+.server-banner {
+  padding: 20px 0;
+
+  &__introduction {
+    color: $darker-text-color;
+    margin-bottom: 20px;
+
+    strong {
+      font-weight: 600;
+    }
+
+    a {
+      color: inherit;
+      text-decoration: underline;
+
+      &:hover,
+      &:active,
+      &:focus {
+        text-decoration: none;
+      }
+    }
+  }
+
+  &__hero {
+    display: block;
+    border-radius: 4px;
+    width: 100%;
+    height: auto;
+    margin-bottom: 20px;
+    aspect-ratio: 1.9;
+    border: 0;
+    background: $ui-base-color;
+    object-fit: cover;
+  }
+
+  &__description {
+    margin-bottom: 20px;
+  }
+
+  &__meta {
+    display: flex;
+    gap: 10px;
+    max-width: 100%;
+
+    &__column {
+      flex: 0 0 auto;
+      width: calc(50% - 5px);
+      overflow: hidden;
+    }
+  }
+
+  &__number {
+    font-weight: 600;
+    color: $primary-text-color;
+    font-size: 14px;
+  }
+
+  &__number-label {
+    color: $darker-text-color;
+    font-weight: 500;
+    font-size: 14px;
+  }
+
+  h4 {
+    text-transform: uppercase;
+    color: $darker-text-color;
+    margin-bottom: 10px;
+    font-weight: 600;
+  }
+
+  .account {
+    padding: 0;
+    border: 0;
+  }
+
+  .account__avatar-wrapper {
+    margin-inline-start: 0;
+  }
+
+  .spacer {
+    margin: 10px 0;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/components/single_column.scss b/app/javascript/flavours/blobfox/styles/components/single_column.scss
new file mode 100644
index 00000000000000..87fdd170d3b7e7
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/components/single_column.scss
@@ -0,0 +1,335 @@
+.compose-panel {
+  width: 285px;
+  margin-top: 10px;
+  display: flex;
+  flex-direction: column;
+  height: calc(100% - 10px);
+  overflow-y: hidden;
+
+  .hero-widget {
+    box-shadow: none;
+
+    &__text,
+    &__img,
+    &__img img {
+      border-radius: 0;
+    }
+
+    &__text {
+      padding: 15px;
+      color: $secondary-text-color;
+
+      strong {
+        font-weight: 700;
+        color: $primary-text-color;
+      }
+    }
+  }
+
+  .search__input {
+    line-height: 18px;
+    font-size: 16px;
+    padding: 15px;
+    padding-inline-end: 30px;
+  }
+
+  .search__icon .fa {
+    top: 15px;
+  }
+
+  .navigation-bar {
+    flex: 0 1 48px;
+  }
+
+  .compose-form {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    min-height: 310px;
+  }
+
+  .compose-form__autosuggest-wrapper {
+    overflow-y: auto;
+    background-color: $white;
+    border-radius: 4px 4px 0 0;
+    flex: 0 1 auto;
+  }
+
+  .autosuggest-textarea__textarea {
+    overflow-y: hidden;
+  }
+}
+
+.navigation-panel {
+  margin-top: 10px;
+  margin-bottom: 10px;
+  height: calc(100% - 20px);
+  overflow-y: auto;
+  display: flex;
+  flex-direction: column;
+
+  & > a {
+    flex: 0 0 auto;
+  }
+
+  .logo {
+    height: 30px;
+    width: auto;
+  }
+}
+
+.navigation-panel,
+.compose-panel {
+  hr {
+    flex: 0 0 auto;
+    border: 0;
+    background: transparent;
+    border-top: 1px solid lighten($ui-base-color, 4%);
+    margin: 10px 0;
+  }
+
+  .flex-spacer {
+    background: transparent;
+  }
+}
+
+@media screen and (width >= 600px) {
+  .tabs-bar__link {
+    span {
+      display: inline;
+    }
+  }
+}
+
+.columns-area--mobile {
+  flex-direction: column;
+  width: 100%;
+  margin: 0 auto;
+
+  .column,
+  .drawer {
+    width: 100%;
+    height: 100%;
+    padding: 0;
+  }
+
+  .account-card {
+    margin-bottom: 0;
+  }
+
+  .filter-form {
+    display: flex;
+    flex-wrap: wrap;
+  }
+
+  .autosuggest-textarea__textarea {
+    font-size: 16px;
+  }
+
+  .search__input {
+    line-height: 18px;
+    font-size: 16px;
+    padding: 15px;
+    padding-inline-end: 30px;
+  }
+
+  .search__icon .fa {
+    top: 15px;
+  }
+
+  .scrollable {
+    overflow: visible;
+
+    @supports (display: grid) {
+      contain: content;
+    }
+  }
+
+  @media screen and (min-width: $no-gap-breakpoint) {
+    padding: 10px 0;
+    padding-top: 0;
+  }
+
+  .detailed-status {
+    padding: 15px;
+
+    .media-gallery,
+    .video-player,
+    .audio-player {
+      margin-top: 15px;
+    }
+  }
+
+  .account__header__bar {
+    padding: 5px 10px;
+  }
+
+  .navigation-bar,
+  .compose-form {
+    padding: 15px;
+  }
+
+  .compose-form .compose-form__publish .compose-form__publish-button-wrapper {
+    padding-top: 15px;
+  }
+
+  .notification__report {
+    padding: 15px;
+    padding-inline-start: (48px + 15px * 2);
+    min-height: 48px + 2px;
+
+    &__avatar {
+      inset-inline-start: 15px;
+      top: 17px;
+    }
+  }
+
+  .status {
+    padding: 15px;
+    min-height: 48px + 2px;
+
+    .media-gallery,
+    &__action-bar,
+    .video-player,
+    .audio-player {
+      margin-top: 10px;
+    }
+  }
+
+  .account {
+    padding: 15px 10px;
+
+    &__header__bio {
+      margin: 0 -10px;
+    }
+  }
+
+  .notification {
+    &__message {
+      padding-top: 15px;
+    }
+
+    .status {
+      padding-top: 8px;
+    }
+
+    .account {
+      padding-top: 8px;
+    }
+  }
+}
+
+@media screen and (min-width: $no-gap-breakpoint) {
+  .tabs-bar {
+    width: 100%;
+  }
+
+  .react-swipeable-view-container .columns-area--mobile {
+    height: calc(100% - 10px) !important;
+  }
+
+  .getting-started__wrapper {
+    margin-bottom: 10px;
+  }
+
+  .tabs-bar__link.optional {
+    display: none;
+  }
+
+  .search-page .search {
+    display: none;
+  }
+
+  .navigation-panel__legal {
+    display: none;
+  }
+}
+
+@media screen and (max-width: $no-gap-breakpoint - 1px) {
+  $sidebar-width: 285px;
+
+  .columns-area__panels__main {
+    width: calc(100% - $sidebar-width);
+  }
+
+  .columns-area__panels {
+    min-height: calc(100vh - $ui-header-height);
+  }
+
+  .columns-area__panels__pane--navigational {
+    min-width: $sidebar-width;
+
+    .columns-area__panels__pane__inner {
+      width: $sidebar-width;
+    }
+
+    .navigation-panel {
+      margin: 0;
+      background: $ui-base-color;
+      border-inline-start: 1px solid lighten($ui-base-color, 8%);
+      height: 100vh;
+    }
+
+    .navigation-panel__sign-in-banner,
+    .navigation-panel__logo,
+    .navigation-panel__banner,
+    .getting-started__trends {
+      display: none;
+    }
+
+    .column-link__icon {
+      font-size: 18px;
+    }
+  }
+
+  .layout-single-column .ui__header {
+    display: flex;
+    background: $ui-base-color;
+    border-bottom: 1px solid lighten($ui-base-color, 8%);
+  }
+
+  .column-header,
+  .column-back-button,
+  .scrollable,
+  .error-column {
+    border-radius: 0 !important;
+  }
+}
+
+@media screen and (max-width: $no-gap-breakpoint - 285px - 1px) {
+  $sidebar-width: 55px;
+
+  .columns-area__panels__main {
+    width: calc(100% - $sidebar-width);
+  }
+
+  .columns-area__panels__pane--navigational {
+    min-width: $sidebar-width;
+
+    .columns-area__panels__pane__inner {
+      width: $sidebar-width;
+    }
+
+    .column-link span {
+      display: none;
+    }
+
+    .list-panel {
+      display: none;
+    }
+  }
+}
+
+.explore__search-header {
+  display: none;
+}
+
+@media screen and (max-width: $no-gap-breakpoint - 1px) {
+  .columns-area__panels__pane--compositional {
+    display: none;
+  }
+
+  .explore__search-header {
+    display: flex;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/components/status.scss b/app/javascript/flavours/blobfox/styles/components/status.scss
new file mode 100644
index 00000000000000..cb7d72e5ccf42a
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/components/status.scss
@@ -0,0 +1,1212 @@
+@keyframes spring-flip-in {
+  0% {
+    transform: rotate(0deg);
+  }
+
+  30% {
+    transform: rotate(-242.4deg);
+  }
+
+  60% {
+    transform: rotate(-158.35deg);
+  }
+
+  90% {
+    transform: rotate(-187.5deg);
+  }
+
+  100% {
+    transform: rotate(-180deg);
+  }
+}
+
+@keyframes spring-flip-out {
+  0% {
+    transform: rotate(-180deg);
+  }
+
+  30% {
+    transform: rotate(62.4deg);
+  }
+
+  60% {
+    transform: rotate(-21.635deg);
+  }
+
+  90% {
+    transform: rotate(7.5deg);
+  }
+
+  100% {
+    transform: rotate(0deg);
+  }
+}
+
+.status__content--with-action {
+  cursor: pointer;
+}
+
+.status__content {
+  position: relative;
+  margin: 10px 0;
+  font-size: 15px;
+  line-height: 20px;
+  word-wrap: break-word;
+  font-weight: 400;
+  overflow: visible;
+  padding-top: 5px;
+  clear: both;
+
+  &:focus {
+    outline: 0;
+  }
+
+  .emojione {
+    width: 20px;
+    height: 20px;
+    margin: -3px 0 0;
+  }
+
+  p,
+  pre {
+    margin-bottom: 20px;
+    white-space: pre-wrap;
+    unicode-bidi: plaintext;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+
+  a {
+    color: $secondary-text-color;
+    text-decoration: none;
+    unicode-bidi: isolate;
+
+    &:hover {
+      text-decoration: underline;
+
+      .fa {
+        color: lighten($dark-text-color, 7%);
+      }
+    }
+
+    &.mention {
+      &:hover {
+        text-decoration: none;
+
+        span {
+          text-decoration: underline;
+        }
+      }
+    }
+
+    .fa {
+      color: $dark-text-color;
+    }
+  }
+
+  .status__content__spoiler {
+    display: none;
+
+    &.status__content__spoiler--visible {
+      display: block;
+    }
+  }
+
+  a.unhandled-link {
+    color: $highlight-text-color;
+
+    .link-origin-tag {
+      color: $gold-star;
+      font-size: 0.8em;
+    }
+  }
+
+  .status__content__spoiler-link {
+    background: lighten($ui-base-color, 30%);
+
+    &:hover,
+    &:focus {
+      background: lighten($ui-base-color, 33%);
+      text-decoration: none;
+    }
+  }
+}
+
+.translate-button {
+  margin-top: 16px;
+  font-size: 15px;
+  line-height: 20px;
+  display: flex;
+  justify-content: space-between;
+  color: $dark-text-color;
+}
+
+.status__content__spoiler-link {
+  display: inline-block;
+  border-radius: 2px;
+  background: lighten($ui-base-color, 30%);
+  border: 0;
+  color: $inverted-text-color;
+  font-weight: 700;
+  font-size: 11px;
+  padding: 0 5px;
+  text-transform: uppercase;
+  line-height: inherit;
+  cursor: pointer;
+  vertical-align: top;
+
+  &:hover {
+    background: lighten($ui-base-color, 33%);
+    text-decoration: none;
+  }
+
+  .status__content__spoiler-icon {
+    display: inline-block;
+    margin-inline-start: 5px;
+    border-inline-start: 1px solid currentColor;
+    padding: 0;
+    padding-inline-start: 4px;
+    font-size: 16px;
+    vertical-align: -2px;
+  }
+}
+
+.notif-cleaning {
+  .status,
+  .notification {
+    padding-inline-end: ($dismiss-overlay-width + 0.5rem);
+  }
+}
+
+.status__wrapper--filtered {
+  color: $dark-text-color;
+  border: 0;
+  font-size: inherit;
+  text-align: center;
+  line-height: inherit;
+  margin: 0;
+  padding: 15px;
+  box-sizing: border-box;
+  width: 100%;
+  clear: both;
+  border-bottom: 1px solid lighten($ui-base-color, 8%);
+}
+
+.status__prepend-icon-wrapper {
+  inset-inline-start: -26px;
+  position: absolute;
+}
+
+.notification-follow,
+.notification-follow-request {
+  position: relative;
+
+  // same like Status
+  border-bottom: 1px solid lighten($ui-base-color, 8%);
+
+  .account {
+    border-bottom: 0 none;
+  }
+}
+
+.focusable {
+  &:focus {
+    outline: 0;
+    background: lighten($ui-base-color, 4%);
+
+    &.status.status-direct {
+      background: mix(lighten($ui-base-color, 4%), $ui-highlight-color, 95%);
+
+      &.muted {
+        background: transparent;
+      }
+    }
+
+    .detailed-status,
+    .detailed-status__action-bar {
+      background: lighten($ui-base-color, 8%);
+    }
+  }
+}
+
+.status {
+  padding: 10px 14px;
+  position: relative;
+  height: auto;
+  border-bottom: 1px solid lighten($ui-base-color, 8%);
+  cursor: auto;
+
+  @supports (-ms-overflow-style: -ms-autohiding-scrollbar) {
+    // Add margin to avoid Edge auto-hiding scrollbar appearing over content.
+    // On Edge 16 this is 16px and Edge <=15 it's 12px, so aim for 16px.
+    padding-inline-end: 28px; // 12px + 16px
+  }
+
+  @keyframes fade {
+    0% {
+      opacity: 0;
+    }
+
+    100% {
+      opacity: 1;
+    }
+  }
+
+  opacity: 1;
+  animation: fade 150ms linear;
+
+  .video-player,
+  .audio-player {
+    margin-top: 8px;
+  }
+
+  &.status-direct {
+    background: mix($ui-base-color, $ui-highlight-color, 95%);
+    border-bottom-color: lighten($ui-base-color, 12%);
+  }
+
+  &.light {
+    .status__relative-time {
+      color: $lighter-text-color;
+    }
+
+    .status__display-name {
+      color: $inverted-text-color;
+    }
+
+    .display-name {
+      color: $light-text-color;
+
+      strong {
+        color: $inverted-text-color;
+      }
+    }
+
+    .status__content {
+      color: $inverted-text-color;
+
+      a {
+        color: $highlight-text-color;
+      }
+
+      a.status__content__spoiler-link {
+        color: $primary-text-color;
+        background: $ui-primary-color;
+
+        &:hover {
+          background: lighten($ui-primary-color, 8%);
+        }
+      }
+    }
+  }
+
+  &.collapsed {
+    background-position: center;
+    background-size: cover;
+    user-select: none;
+
+    &.has-background::before {
+      display: block;
+      position: absolute;
+      inset-inline-start: 0;
+      inset-inline-end: 0;
+      top: 0;
+      bottom: 0;
+      background-image: linear-gradient(
+        to bottom,
+        rgba($base-shadow-color, 0.75),
+        rgba($base-shadow-color, 0.65) 24px,
+        rgba($base-shadow-color, 0.8)
+      );
+      pointer-events: none;
+      content: '';
+    }
+
+    .display-name:hover .display-name__html {
+      text-decoration: none;
+    }
+
+    .status__content {
+      height: 20px;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      padding-top: 0;
+
+      &::after {
+        content: '';
+        position: absolute;
+        top: 0;
+        bottom: 0;
+        inset-inline-start: 0;
+        inset-inline-end: 0;
+        background: linear-gradient(
+          rgba($ui-base-color, 0),
+          rgba($ui-base-color, 1)
+        );
+        pointer-events: none;
+      }
+
+      a:hover {
+        text-decoration: none;
+      }
+    }
+
+    &:focus > .status__content::after {
+      background: linear-gradient(
+        rgba(lighten($ui-base-color, 4%), 0),
+        rgba(lighten($ui-base-color, 4%), 1)
+      );
+    }
+
+    &.status-direct > .status__content::after {
+      background: linear-gradient(
+        rgba(mix($ui-base-color, $ui-highlight-color, 95%), 0),
+        rgba(mix($ui-base-color, $ui-highlight-color, 95%), 1)
+      );
+    }
+
+    .notification__message {
+      margin-bottom: 0;
+    }
+
+    .status__info .notification__message > span {
+      white-space: nowrap;
+    }
+  }
+
+  .notification__message {
+    margin: -10px 0 10px;
+  }
+
+  .reactions-bar--empty {
+    display: none;
+  }
+}
+
+.notification-favourite {
+  .status.status-direct {
+    background: transparent;
+
+    .icon-button.disabled {
+      color: lighten($action-button-color, 13%);
+    }
+  }
+}
+
+.status__relative-time {
+  display: inline-block;
+  color: $dark-text-color;
+  font-size: 14px;
+  text-align: end;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.status__display-name {
+  color: $dark-text-color;
+  overflow: hidden;
+}
+
+.status__info__account .status__display-name {
+  display: block;
+  max-width: 100%;
+}
+
+.status__info {
+  display: flex;
+  justify-content: space-between;
+  font-size: 15px;
+
+  > span {
+    text-overflow: ellipsis;
+    overflow: hidden;
+  }
+
+  .notification__message > span {
+    word-wrap: break-word;
+  }
+}
+
+.status__info__icons {
+  display: flex;
+  align-items: center;
+  height: 1em;
+  color: $action-button-color;
+
+  .status__media-icon,
+  .status__visibility-icon,
+  .status__reply-icon,
+  .text-icon {
+    padding-inline-start: 2px;
+    padding-inline-end: 2px;
+  }
+
+  .status__collapse-button.active > .fa-angle-double-up {
+    transform: rotate(-180deg);
+  }
+}
+
+.no-reduce-motion .status__collapse-button {
+  &.activate {
+    & > .fa-angle-double-up {
+      animation: spring-flip-in 1s linear;
+    }
+  }
+
+  &.deactivate {
+    & > .fa-angle-double-up {
+      animation: spring-flip-out 1s linear;
+    }
+  }
+}
+
+.status__info__account {
+  display: flex;
+  align-items: center;
+  justify-content: flex-start;
+}
+
+.status-check-box__status {
+  display: block;
+  box-sizing: border-box;
+  width: 100%;
+  padding: 0 10px;
+
+  .detailed-status__display-name {
+    color: lighten($inverted-text-color, 16%);
+
+    span {
+      display: inline;
+    }
+
+    &:hover strong {
+      text-decoration: none;
+    }
+  }
+
+  .media-gallery,
+  .audio-player,
+  .video-player {
+    margin-top: 15px;
+    max-width: 250px;
+  }
+
+  .status__content {
+    padding: 0;
+    white-space: normal;
+  }
+
+  .media-gallery__item-thumbnail {
+    cursor: default;
+  }
+}
+
+.status__prepend {
+  margin-top: -2px;
+  margin-bottom: 8px;
+  margin-inline-start: 58px;
+  color: $dark-text-color;
+  font-size: 14px;
+  position: relative;
+
+  .status__display-name strong {
+    color: $dark-text-color;
+  }
+
+  > span {
+    display: block;
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
+}
+
+.status__action-bar {
+  align-items: center;
+  display: flex;
+  margin-top: 8px;
+
+  & > .emoji-picker-dropdown > .emoji-button {
+    padding: 0;
+  }
+}
+
+.status__action-bar-button {
+  margin-inline-end: 18px;
+
+  &.icon-button--with-counter {
+    margin-inline-end: 14px;
+  }
+
+  .fa-plus {
+    padding-top: 1px;
+  }
+}
+
+.status__action-bar-dropdown {
+  height: 23.15px;
+  width: 23.15px;
+}
+
+.status__action-bar-spacer {
+  flex-grow: 1;
+}
+
+.detailed-status__action-bar-dropdown {
+  flex: 1 1 auto;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  position: relative;
+}
+
+.detailed-status {
+  background: lighten($ui-base-color, 4%);
+  padding: 14px 10px;
+  border-top: 1px solid lighten($ui-base-color, 8%);
+
+  &--flex {
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: space-between;
+    align-items: flex-start;
+
+    .status__content,
+    .detailed-status__meta {
+      flex: 100%;
+    }
+  }
+
+  .status__content {
+    font-size: 19px;
+    line-height: 24px;
+
+    .emojione {
+      width: 24px;
+      height: 24px;
+      margin: -1px 0 0;
+    }
+  }
+
+  .video-player,
+  .audio-player {
+    margin-top: 8px;
+  }
+}
+
+.detailed-status__meta {
+  margin-top: 15px;
+  color: $dark-text-color;
+  font-size: 14px;
+  line-height: 18px;
+}
+
+.detailed-status__action-bar {
+  background: lighten($ui-base-color, 4%);
+  border-top: 1px solid lighten($ui-base-color, 8%);
+  border-bottom: 1px solid lighten($ui-base-color, 8%);
+  display: flex;
+  flex-direction: row;
+  padding: 10px 0;
+
+  .fa-plus {
+    padding-top: 2px;
+  }
+}
+
+.detailed-status__link {
+  color: inherit;
+  text-decoration: none;
+  white-space: nowrap;
+}
+
+.detailed-status__favorites,
+.detailed-status__reblogs {
+  display: inline-block;
+  font-weight: 500;
+  font-size: 12px;
+  line-height: 17px;
+  margin-inline-start: 6px;
+}
+
+.status__display-name,
+.status__relative-time,
+.detailed-status__display-name,
+.detailed-status__datetime,
+.detailed-status__application,
+.account__display-name {
+  text-decoration: none;
+}
+
+.status__display-name,
+.account__display-name {
+  .display-name strong {
+    color: $primary-text-color;
+  }
+}
+
+.muted {
+  .emojione {
+    opacity: 0.5;
+  }
+}
+
+a.status__display-name,
+.reply-indicator__display-name,
+.detailed-status__display-name,
+.account__display-name {
+  &:hover .display-name strong {
+    text-decoration: underline;
+  }
+}
+
+.account__display-name .display-name strong {
+  display: block;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.detailed-status__application,
+.detailed-status__datetime {
+  color: inherit;
+}
+
+.detailed-status__display-name {
+  color: $secondary-text-color;
+  display: block;
+  line-height: 24px;
+  margin-bottom: 15px;
+  overflow: hidden;
+
+  strong,
+  span {
+    display: block;
+    text-overflow: ellipsis;
+    overflow: hidden;
+  }
+
+  strong {
+    font-size: 16px;
+    color: $primary-text-color;
+  }
+}
+
+.detailed-status__display-avatar {
+  float: left;
+  margin-inline-end: 10px;
+}
+
+.status__avatar {
+  flex: none;
+  margin-inline-end: 10px;
+}
+
+.muted {
+  .status__content,
+  .status__content p,
+  .status__content a,
+  .status__content__text {
+    color: $dark-text-color;
+  }
+
+  .status__display-name strong {
+    color: $dark-text-color;
+  }
+
+  .status__avatar {
+    opacity: 0.5;
+  }
+
+  a.status__content__spoiler-link {
+    background: $ui-base-lighter-color;
+    color: $inverted-text-color;
+
+    &:hover,
+    &:focus {
+      background: lighten($ui-base-color, 29%);
+      text-decoration: none;
+    }
+  }
+}
+
+.status__relative-time,
+.detailed-status__datetime {
+  &:hover {
+    text-decoration: underline;
+  }
+}
+
+.status-card {
+  position: relative;
+  display: flex;
+  font-size: 14px;
+  border: 1px solid lighten($ui-base-color, 8%);
+  border-radius: 4px;
+  color: $dark-text-color;
+  margin-top: 14px;
+  text-decoration: none;
+  overflow: hidden;
+
+  &__actions {
+    bottom: 0;
+    inset-inline-start: 0;
+    position: absolute;
+    inset-inline-end: 0;
+    top: 0;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+
+    & > div {
+      background: rgba($base-shadow-color, 0.6);
+      border-radius: 8px;
+      padding: 12px 9px;
+      flex: 0 0 auto;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+    }
+
+    button,
+    a {
+      display: inline;
+      color: $secondary-text-color;
+      background: transparent;
+      border: 0;
+      padding: 0 8px;
+      text-decoration: none;
+      font-size: 18px;
+      line-height: 18px;
+
+      &:hover,
+      &:active,
+      &:focus {
+        color: $primary-text-color;
+      }
+    }
+
+    a {
+      font-size: 19px;
+      position: relative;
+      bottom: -1px;
+    }
+
+    a .fa,
+    a:hover .fa {
+      color: inherit;
+    }
+  }
+}
+
+a.status-card {
+  cursor: pointer;
+
+  &:hover {
+    background: lighten($ui-base-color, 8%);
+  }
+}
+
+.status-card-photo {
+  cursor: zoom-in;
+  display: block;
+  text-decoration: none;
+  width: 100%;
+  height: auto;
+  margin: 0;
+}
+
+.status-card-video {
+  // Firefox has a bug where frameborder=0 iframes add some extra blank space
+  // see https://bugzilla.mozilla.org/show_bug.cgi?id=155174
+  overflow: hidden;
+
+  iframe {
+    width: 100%;
+    height: 100%;
+  }
+}
+
+.status-card__title {
+  display: block;
+  font-weight: 500;
+  margin-bottom: 5px;
+  color: $darker-text-color;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  text-decoration: none;
+}
+
+.status-card__content {
+  flex: 1 1 auto;
+  overflow: hidden;
+  padding: 14px;
+  padding-inline-start: 8px;
+}
+
+.status-card__description {
+  color: $darker-text-color;
+  overflow: hidden;
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 2;
+}
+
+.status-card__host {
+  display: block;
+  margin-top: 5px;
+  font-size: 13px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.status-card__image {
+  flex: 0 0 100px;
+  background: lighten($ui-base-color, 8%);
+  position: relative;
+
+  & > .fa {
+    font-size: 21px;
+    position: absolute;
+    transform-origin: 50% 50%;
+    top: 50%;
+    inset-inline-start: 50%;
+    transform: translate(-50%, -50%);
+  }
+}
+
+.status-card.horizontal {
+  display: block;
+
+  .status-card__image {
+    width: 100%;
+  }
+
+  .status-card__image-image,
+  .status-card__image-preview {
+    border-radius: 4px 4px 0 0;
+  }
+
+  .status-card__title {
+    white-space: inherit;
+  }
+}
+
+.status-card.compact {
+  border-color: lighten($ui-base-color, 4%);
+
+  &.interactive {
+    border: 0;
+  }
+
+  .status-card__content {
+    padding: 8px;
+    padding-top: 10px;
+  }
+
+  .status-card__title {
+    white-space: nowrap;
+  }
+
+  .status-card__image {
+    flex: 0 0 60px;
+  }
+}
+
+a.status-card.compact:hover {
+  background-color: lighten($ui-base-color, 4%);
+}
+
+.status-card__image-image {
+  border-radius: 4px 0 0 4px;
+  display: block;
+  margin: 0;
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+  background-size: cover;
+  background-position: center center;
+}
+
+.status-card__image-preview {
+  border-radius: 4px 0 0 4px;
+  display: block;
+  margin: 0;
+  width: 100%;
+  height: 100%;
+  object-fit: fill;
+  position: absolute;
+  top: 0;
+  inset-inline-start: 0;
+  z-index: 0;
+  background: $base-overlay-background;
+
+  &--hidden {
+    display: none;
+  }
+}
+
+.attachment-list {
+  display: flex;
+  font-size: 14px;
+  border: 1px solid lighten($ui-base-color, 8%);
+  border-radius: 4px;
+  margin-top: 14px;
+  overflow: hidden;
+
+  &__icon {
+    flex: 0 0 auto;
+    color: $dark-text-color;
+    padding: 8px 18px;
+    cursor: default;
+    border-inline-end: 1px solid lighten($ui-base-color, 8%);
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    font-size: 26px;
+
+    .fa {
+      display: block;
+    }
+  }
+
+  &__list {
+    list-style: none;
+    padding: 4px 0;
+    padding-inline-start: 8px;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+
+    li {
+      display: block;
+      padding: 4px 0;
+    }
+
+    a {
+      text-decoration: none;
+      color: $dark-text-color;
+      font-weight: 500;
+
+      &:hover {
+        text-decoration: underline;
+      }
+    }
+  }
+
+  &.compact {
+    border: 0;
+    margin-top: 4px;
+
+    .attachment-list__list {
+      padding: 0;
+      display: block;
+    }
+
+    .fa {
+      color: $dark-text-color;
+    }
+  }
+}
+
+.status__wrapper--filtered__button {
+  display: inline;
+  color: lighten($ui-highlight-color, 8%);
+  border: 0;
+  background: transparent;
+  padding: 0;
+  font-size: inherit;
+  line-height: inherit;
+
+  &:hover,
+  &:active {
+    text-decoration: underline;
+  }
+}
+
+.notification,
+.status {
+  position: relative;
+
+  &.unread {
+    &::before {
+      content: '';
+      position: absolute;
+      top: 0;
+      inset-inline-start: 0;
+      width: 100%;
+      height: 100%;
+      border-inline-start: 4px solid $highlight-text-color;
+      pointer-events: none;
+    }
+  }
+
+  &--in-thread {
+    border-bottom: 0;
+
+    .status__content,
+    .status__action-bar,
+    .reactions-bar {
+      margin-inline-start: 46px + 10px;
+      width: calc(100% - (46px + 10px));
+    }
+  }
+
+  &--first-in-thread {
+    border-top: 1px solid lighten($ui-base-color, 8%);
+  }
+
+  &__line {
+    height: 10px - 4px;
+    border-inline-start: 2px solid lighten($ui-base-color, 8%);
+    width: 0;
+    position: absolute;
+    top: 0;
+    inset-inline-start: 14px + ((46px - 2px) * 0.5);
+
+    &--full {
+      top: 0;
+      height: 100%;
+
+      &::before {
+        content: '';
+        display: block;
+        position: absolute;
+        top: 10px - 4px;
+        height: 46px + 4px + 4px;
+        width: 2px;
+        background: $ui-base-color;
+        inset-inline-start: -2px;
+      }
+    }
+
+    &--first {
+      top: 10px + 46px + 4px;
+      height: calc(100% - (10px + 46px + 4px));
+
+      &::before {
+        display: none;
+      }
+    }
+  }
+}
+
+.picture-in-picture {
+  position: fixed;
+  bottom: 20px;
+  inset-inline-end: 20px;
+  width: 300px;
+
+  &.left {
+    inset-inline-end: unset;
+    inset-inline-start: 20px;
+  }
+
+  &__footer {
+    border-radius: 0 0 4px 4px;
+    background: lighten($ui-base-color, 4%);
+    padding: 10px;
+    padding-top: 12px;
+    display: flex;
+    justify-content: space-between;
+  }
+
+  &__header {
+    border-radius: 4px 4px 0 0;
+    background: lighten($ui-base-color, 4%);
+    padding: 10px;
+    display: flex;
+    justify-content: space-between;
+
+    &__account {
+      display: flex;
+      text-decoration: none;
+      overflow: hidden;
+    }
+
+    .account__avatar {
+      margin-inline-end: 10px;
+    }
+
+    .display-name {
+      color: $primary-text-color;
+      text-decoration: none;
+
+      strong,
+      span {
+        display: block;
+        text-overflow: ellipsis;
+        overflow: hidden;
+      }
+
+      span {
+        color: $darker-text-color;
+      }
+    }
+  }
+
+  .video-player,
+  .audio-player {
+    border-radius: 0;
+  }
+}
+
+.picture-in-picture-placeholder {
+  box-sizing: border-box;
+  border: 2px dashed lighten($ui-base-color, 8%);
+  background: $base-shadow-color;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  margin-top: 10px;
+  font-size: 16px;
+  font-weight: 500;
+  cursor: pointer;
+  color: $darker-text-color;
+  aspect-ratio: 16 / 9;
+
+  i {
+    display: block;
+    font-size: 24px;
+    font-weight: 400;
+    margin-bottom: 10px;
+  }
+
+  &:hover,
+  &:focus,
+  &:active {
+    border-color: lighten($ui-base-color, 12%);
+  }
+}
+
+.hashtag-bar {
+  margin-top: 16px;
+  display: flex;
+  flex-wrap: wrap;
+  font-size: 14px;
+  line-height: 18px;
+  gap: 4px;
+  color: $darker-text-color;
+
+  a {
+    display: inline-flex;
+    color: inherit;
+    text-decoration: none;
+
+    &:hover span {
+      text-decoration: underline;
+    }
+  }
+
+  .link-button {
+    color: inherit;
+    font-size: inherit;
+    line-height: inherit;
+    padding: 0;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/containers.scss b/app/javascript/flavours/blobfox/styles/containers.scss
new file mode 100644
index 00000000000000..4d3d4c546c1ffe
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/containers.scss
@@ -0,0 +1,109 @@
+.container-alt {
+  width: 700px;
+  margin: 0 auto;
+
+  @media screen and (width <= 740px) {
+    width: 100%;
+    margin: 0;
+  }
+}
+
+.logo-container {
+  margin: 50px auto;
+
+  h1 {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+
+    .logo {
+      height: 42px;
+      margin-inline-end: 10px;
+    }
+
+    a {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      color: $primary-text-color;
+      text-decoration: none;
+      outline: 0;
+      padding: 12px 16px;
+      line-height: 32px;
+      font-weight: 500;
+      font-size: 14px;
+    }
+  }
+}
+
+.compose-standalone {
+  .compose-form {
+    width: 400px;
+    margin: 0 auto;
+    padding: 20px 0;
+    margin-top: 40px;
+    box-sizing: border-box;
+
+    @media screen and (width <= 400px) {
+      width: 100%;
+      margin-top: 0;
+      padding: 20px;
+    }
+  }
+}
+
+.account-header {
+  width: 400px;
+  margin: 0 auto;
+  display: flex;
+  font-size: 13px;
+  line-height: 18px;
+  box-sizing: border-box;
+  padding: 20px 0;
+  margin-top: 40px;
+  margin-bottom: 10px;
+  border-bottom: 1px solid $ui-base-color;
+
+  @media screen and (width <= 440px) {
+    width: 100%;
+    margin: 0;
+    padding: 20px;
+  }
+
+  .avatar {
+    width: 40px;
+    height: 40px;
+    @include avatar-size(40px);
+
+    margin-inline-end: 10px;
+
+    img {
+      width: 100%;
+      height: 100%;
+      display: block;
+      margin: 0;
+      border-radius: 4px;
+      @include avatar-radius;
+    }
+  }
+
+  .name {
+    flex: 1 1 auto;
+    color: $secondary-text-color;
+    width: calc(100% - 90px);
+
+    .username {
+      display: block;
+      font-weight: 500;
+      text-overflow: ellipsis;
+      overflow: hidden;
+    }
+  }
+
+  .logout-link {
+    display: block;
+    font-size: 32px;
+    line-height: 40px;
+    margin-inline-start: 10px;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/contrast.scss b/app/javascript/flavours/blobfox/styles/contrast.scss
new file mode 100644
index 00000000000000..4de31db9aec577
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/contrast.scss
@@ -0,0 +1,3 @@
+@import 'contrast/variables';
+@import 'index';
+@import 'contrast/diff';
diff --git a/app/javascript/flavours/blobfox/styles/contrast/diff.scss b/app/javascript/flavours/blobfox/styles/contrast/diff.scss
new file mode 100644
index 00000000000000..1c2386f02d2df9
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/contrast/diff.scss
@@ -0,0 +1,79 @@
+.compose-form {
+  .compose-form__modifiers {
+    .compose-form__upload {
+      &-description {
+        input {
+          &::placeholder {
+            opacity: 1;
+          }
+        }
+      }
+    }
+  }
+}
+
+.status__content a,
+.link-footer a,
+.reply-indicator__content a,
+.status__content__read-more-button,
+.status__content__translate-button {
+  text-decoration: underline;
+
+  &:hover,
+  &:focus,
+  &:active {
+    text-decoration: none;
+  }
+
+  &.mention {
+    text-decoration: none;
+
+    span {
+      text-decoration: underline;
+    }
+
+    &:hover,
+    &:focus,
+    &:active {
+      span {
+        text-decoration: none;
+      }
+    }
+  }
+}
+
+.status__content a {
+  color: $highlight-text-color;
+}
+
+.nothing-here {
+  color: $darker-text-color;
+}
+
+.compose-form__poll-wrapper .button.button-secondary,
+.compose-form .autosuggest-textarea__textarea::placeholder,
+.compose-form .spoiler-input__input::placeholder,
+.report-dialog-modal__textarea::placeholder,
+.language-dropdown__dropdown__results__item__common-name,
+.compose-form .icon-button {
+  color: $inverted-text-color;
+}
+
+.text-icon-button.active {
+  color: $ui-highlight-color;
+}
+
+.language-dropdown__dropdown__results__item.active {
+  background: $ui-highlight-color;
+  font-weight: 500;
+}
+
+.link-button:disabled {
+  cursor: not-allowed;
+
+  &:hover,
+  &:focus,
+  &:active {
+    text-decoration: none !important;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/contrast/variables.scss b/app/javascript/flavours/blobfox/styles/contrast/variables.scss
new file mode 100644
index 00000000000000..e38d24b271cf8e
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/contrast/variables.scss
@@ -0,0 +1,22 @@
+// Dependent colors
+$black: #000000;
+
+$classic-base-color: #282c37;
+$classic-primary-color: #9baec8;
+$classic-secondary-color: #d9e1e8;
+$classic-highlight-color: #6364ff;
+
+$ui-base-color: $classic-base-color !default;
+$ui-primary-color: $classic-primary-color !default;
+$ui-secondary-color: $classic-secondary-color !default;
+$ui-highlight-color: $classic-highlight-color !default;
+
+$darker-text-color: lighten($ui-primary-color, 20%) !default;
+$dark-text-color: lighten($ui-primary-color, 12%) !default;
+$secondary-text-color: lighten($ui-secondary-color, 6%) !default;
+$highlight-text-color: lighten($ui-highlight-color, 10%) !default;
+$action-button-color: lighten($ui-base-color, 50%);
+
+$inverted-text-color: $black !default;
+$lighter-text-color: darken($ui-base-color, 6%) !default;
+$light-text-color: darken($ui-primary-color, 40%) !default;
diff --git a/app/javascript/flavours/blobfox/styles/dashboard.scss b/app/javascript/flavours/blobfox/styles/dashboard.scss
new file mode 100644
index 00000000000000..36a7f44253f423
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/dashboard.scss
@@ -0,0 +1,123 @@
+.dashboard__counters {
+  display: flex;
+  flex-wrap: wrap;
+  margin: 0 -5px;
+  margin-bottom: 20px;
+
+  & > div {
+    box-sizing: border-box;
+    flex: 0 0 33.333%;
+    padding: 0 5px;
+    margin-bottom: 10px;
+
+    & > div,
+    & > a {
+      padding: 20px;
+      background: lighten($ui-base-color, 4%);
+      border-radius: 4px;
+      box-sizing: border-box;
+      height: 100%;
+    }
+
+    & > a {
+      text-decoration: none;
+      color: inherit;
+      display: block;
+
+      &:hover,
+      &:focus,
+      &:active {
+        background: lighten($ui-base-color, 8%);
+      }
+    }
+  }
+
+  &__num,
+  &__text {
+    text-align: center;
+    font-weight: 500;
+    font-size: 24px;
+    color: $primary-text-color;
+    margin-bottom: 20px;
+    line-height: 30px;
+  }
+
+  &__text {
+    font-size: 18px;
+  }
+
+  &__label {
+    font-size: 14px;
+    color: $darker-text-color;
+    text-align: center;
+    font-weight: 500;
+  }
+}
+
+.dashboard {
+  display: grid;
+  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr);
+  grid-gap: 10px;
+
+  @media screen and (width <= 1350px) {
+    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
+  }
+
+  &__item {
+    &--span-double-column {
+      grid-column: span 2;
+    }
+
+    &--span-double-row {
+      grid-row: span 2;
+    }
+
+    h4 {
+      padding-top: 20px;
+    }
+  }
+
+  &__quick-access {
+    display: flex;
+    align-items: baseline;
+    border-radius: 4px;
+    background: $ui-button-background-color;
+    color: $primary-text-color;
+    transition: all 100ms ease-in;
+    font-size: 14px;
+    padding: 0 16px;
+    line-height: 36px;
+    height: 36px;
+    text-decoration: none;
+    margin-bottom: 4px;
+
+    &:active,
+    &:focus,
+    &:hover {
+      background-color: $ui-button-focus-background-color;
+      transition: all 200ms ease-out;
+    }
+
+    &.positive {
+      background: lighten($ui-base-color, 4%);
+      color: $valid-value-color;
+    }
+
+    &.negative {
+      background: lighten($ui-base-color, 4%);
+      color: $error-value-color;
+    }
+
+    span {
+      flex: 1 1 auto;
+    }
+
+    .fa {
+      flex: 0 0 auto;
+    }
+
+    strong {
+      font-weight: 700;
+    }
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/forms.scss b/app/javascript/flavours/blobfox/styles/forms.scss
new file mode 100644
index 00000000000000..614f7fc6a08df2
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/forms.scss
@@ -0,0 +1,1224 @@
+$no-columns-breakpoint: 600px;
+
+code {
+  font-family: $font-monospace, monospace;
+  font-weight: 400;
+}
+
+.form-container {
+  max-width: 450px;
+  padding: 20px;
+  padding-bottom: 50px;
+  margin: 50px auto;
+}
+
+.indicator-icon {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 40px;
+  height: 40px;
+  border-radius: 50%;
+  color: $primary-text-color;
+
+  &.success {
+    background: $success-green;
+  }
+
+  &.failure {
+    background: $error-red;
+  }
+}
+
+.simple_form {
+  &.hidden {
+    display: none;
+  }
+
+  .input {
+    margin-bottom: 15px;
+    overflow: hidden;
+
+    &.hidden {
+      margin: 0;
+    }
+
+    &.radio_buttons {
+      .radio {
+        margin-bottom: 15px;
+
+        &:last-child {
+          margin-bottom: 0;
+        }
+      }
+
+      .radio > label {
+        position: relative;
+        padding-inline-start: 28px;
+
+        input {
+          position: absolute;
+          top: -2px;
+          inset-inline-start: 0;
+        }
+      }
+    }
+
+    &.boolean {
+      position: relative;
+      margin-bottom: 0;
+
+      .label_input > label {
+        font-family: inherit;
+        font-size: 14px;
+        padding-top: 5px;
+        color: $primary-text-color;
+        display: block;
+        width: auto;
+      }
+
+      .label_input,
+      .hint {
+        padding-inline-start: 28px;
+      }
+
+      .label_input__wrapper {
+        position: static;
+      }
+
+      label.checkbox {
+        position: absolute;
+        top: 2px;
+        inset-inline-start: 0;
+      }
+
+      label a {
+        color: $highlight-text-color;
+        text-decoration: underline;
+
+        &:hover,
+        &:active,
+        &:focus {
+          text-decoration: none;
+        }
+      }
+
+      .overridden,
+      .recommended,
+      .not_recommended,
+      .blobfox_only {
+        position: absolute;
+        margin: 0 4px;
+        margin-top: -2px;
+      }
+    }
+  }
+
+  .row {
+    display: flex;
+    margin: 0 -5px;
+
+    .input {
+      box-sizing: border-box;
+      flex: 1 1 auto;
+      width: 50%;
+      padding: 0 5px;
+    }
+  }
+
+  .title {
+    font-size: 28px;
+    line-height: 33px;
+    font-weight: 700;
+    margin-bottom: 15px;
+  }
+
+  .lead {
+    font-size: 17px;
+    line-height: 22px;
+    color: $secondary-text-color;
+    margin-bottom: 30px;
+
+    &.invited-by {
+      margin-bottom: 15px;
+    }
+
+    a {
+      color: $highlight-text-color;
+    }
+  }
+
+  .rules-list {
+    font-size: 17px;
+    line-height: 22px;
+    margin-bottom: 30px;
+  }
+
+  .hint {
+    color: $darker-text-color;
+
+    a {
+      color: $highlight-text-color;
+    }
+
+    code {
+      border-radius: 3px;
+      padding: 0.2em 0.4em;
+      background: darken($ui-base-color, 12%);
+    }
+
+    li {
+      list-style: disc;
+      margin-inline-start: 18px;
+    }
+  }
+
+  ul.hint {
+    margin-bottom: 15px;
+  }
+
+  span.hint {
+    display: block;
+    font-size: 12px;
+    margin-top: 4px;
+  }
+
+  p.hint {
+    margin-bottom: 15px;
+    color: $darker-text-color;
+
+    &.subtle-hint {
+      text-align: center;
+      font-size: 12px;
+      line-height: 18px;
+      margin-top: 15px;
+      margin-bottom: 0;
+    }
+  }
+
+  .authentication-hint {
+    margin-bottom: 25px;
+  }
+
+  .card {
+    margin-bottom: 15px;
+  }
+
+  strong {
+    font-weight: 500;
+
+    @each $lang in $cjk-langs {
+      &:lang(#{$lang}) {
+        font-weight: 700;
+      }
+    }
+  }
+
+  .input.with_floating_label {
+    .label_input {
+      display: flex;
+
+      & > label {
+        font-family: inherit;
+        font-size: 14px;
+        color: $primary-text-color;
+        font-weight: 500;
+        min-width: 150px;
+        flex: 0 0 auto;
+      }
+
+      input,
+      select {
+        flex: 1 1 auto;
+      }
+    }
+
+    &.select .hint {
+      margin-top: 6px;
+      margin-inline-start: 150px;
+    }
+  }
+
+  .input.with_label {
+    .label_input > label {
+      font-family: inherit;
+      font-size: 14px;
+      color: $primary-text-color;
+      display: block;
+      margin-bottom: 8px;
+      word-wrap: break-word;
+      font-weight: 500;
+    }
+
+    .hint {
+      margin-top: 6px;
+    }
+
+    ul {
+      flex: 390px;
+    }
+  }
+
+  .input.with_block_label {
+    max-width: none;
+
+    & > label {
+      font-family: inherit;
+      font-size: 14px;
+      color: $primary-text-color;
+      display: block;
+      font-weight: 500;
+      padding-top: 5px;
+    }
+
+    .hint {
+      margin-bottom: 15px;
+    }
+
+    ul {
+      columns: 2;
+    }
+  }
+
+  .input.with_block_label.user_role_permissions_as_keys ul {
+    columns: unset;
+  }
+
+  .input.datetime .label_input select {
+    display: inline-block;
+    width: auto;
+    flex: 0;
+  }
+
+  .required abbr {
+    text-decoration: none;
+    color: lighten($error-value-color, 12%);
+  }
+
+  .fields-group {
+    margin-bottom: 25px;
+
+    .input:last-child {
+      margin-bottom: 0;
+    }
+
+    &__thumbnail {
+      display: block;
+      margin: 0;
+      margin-bottom: 10px;
+      max-width: 100%;
+      height: auto;
+      border-radius: 4px;
+      background: url('images/void.png');
+
+      &[src$='missing.png'] {
+        visibility: hidden;
+      }
+
+      &:last-child {
+        margin-bottom: 0;
+      }
+
+      &#account_avatar-preview {
+        width: 90px;
+        height: 90px;
+        object-fit: cover;
+      }
+    }
+  }
+
+  .fields-row {
+    display: flex;
+    margin: 0 -10px;
+    padding-top: 5px;
+    margin-bottom: 25px;
+
+    .input {
+      max-width: none;
+    }
+
+    &__column {
+      box-sizing: border-box;
+      padding: 0 10px;
+      flex: 1 1 auto;
+      min-height: 1px;
+
+      &-6 {
+        max-width: 50%;
+      }
+
+      .actions {
+        margin-top: 27px;
+      }
+    }
+
+    .fields-group:last-child,
+    .fields-row__column.fields-group {
+      margin-bottom: 0;
+    }
+
+    @media screen and (max-width: $no-columns-breakpoint) {
+      display: block;
+      margin-bottom: 0;
+
+      &__column {
+        max-width: none;
+      }
+
+      .fields-group:last-child,
+      .fields-row__column.fields-group,
+      .fields-row__column {
+        margin-bottom: 25px;
+      }
+    }
+
+    .fields-group.invited-by {
+      margin-bottom: 30px;
+
+      .hint {
+        text-align: center;
+      }
+    }
+  }
+
+  .input.radio_buttons .radio label {
+    margin-bottom: 5px;
+    font-family: inherit;
+    font-size: 14px;
+    color: $primary-text-color;
+    display: block;
+    width: auto;
+  }
+
+  .check_boxes {
+    .checkbox {
+      label {
+        font-family: inherit;
+        font-size: 14px;
+        color: $primary-text-color;
+        display: inline-block;
+        width: auto;
+        position: relative;
+        padding-top: 5px;
+        padding-inline-start: 25px;
+        flex: 1 1 auto;
+      }
+
+      input[type='checkbox'] {
+        position: absolute;
+        inset-inline-start: 0;
+        top: 5px;
+        margin: 0;
+      }
+    }
+  }
+
+  .input.static .label_input__wrapper {
+    font-size: 16px;
+    padding: 10px;
+    border: 1px solid $dark-text-color;
+    border-radius: 4px;
+  }
+
+  input[type='text'],
+  input[type='number'],
+  input[type='email'],
+  input[type='password'],
+  input[type='url'],
+  input[type='datetime-local'],
+  textarea {
+    box-sizing: border-box;
+    font-size: 16px;
+    color: $primary-text-color;
+    display: block;
+    width: 100%;
+    outline: 0;
+    font-family: inherit;
+    resize: vertical;
+    background: darken($ui-base-color, 10%);
+    border: 1px solid darken($ui-base-color, 14%);
+    border-radius: 4px;
+    padding: 10px;
+
+    &::placeholder {
+      color: lighten($darker-text-color, 4%);
+    }
+
+    &:invalid {
+      box-shadow: none;
+    }
+
+    &:required:valid {
+      border-color: $valid-value-color;
+    }
+
+    &:hover {
+      border-color: darken($ui-base-color, 20%);
+    }
+
+    &:active,
+    &:focus {
+      border-color: $highlight-text-color;
+      background: darken($ui-base-color, 8%);
+    }
+  }
+
+  input[type='text'],
+  input[type='number'],
+  input[type='email'],
+  input[type='password'],
+  input[type='datetime-local'] {
+    &:focus:invalid:not(:placeholder-shown),
+    &:required:invalid:not(:placeholder-shown) {
+      border-color: lighten($error-red, 12%);
+    }
+  }
+
+  .input.field_with_errors {
+    label {
+      color: lighten($error-red, 12%);
+    }
+
+    input[type='text'],
+    input[type='number'],
+    input[type='email'],
+    input[type='password'],
+    input[type='datetime-local'],
+    textarea,
+    select {
+      border-color: lighten($error-red, 12%);
+    }
+
+    .error {
+      display: block;
+      font-weight: 500;
+      color: lighten($error-red, 12%);
+      margin-top: 4px;
+    }
+  }
+
+  .input.disabled {
+    opacity: 0.5;
+  }
+
+  .actions {
+    margin-top: 30px;
+    display: flex;
+
+    &.actions--top {
+      margin-top: 0;
+      margin-bottom: 30px;
+    }
+  }
+
+  .stacked-actions {
+    margin-top: 30px;
+    margin-bottom: 15px;
+  }
+
+  button,
+  .button,
+  .block-button {
+    display: block;
+    width: 100%;
+    border: 0;
+    border-radius: 4px;
+    background: $ui-button-background-color;
+    color: $ui-button-color;
+    font-size: 18px;
+    line-height: inherit;
+    height: auto;
+    padding: 10px;
+    text-decoration: none;
+    text-transform: uppercase;
+    text-align: center;
+    box-sizing: border-box;
+    cursor: pointer;
+    font-weight: 500;
+    outline: 0;
+    margin-bottom: 10px;
+    margin-inline-end: 10px;
+
+    &:last-child {
+      margin-inline-end: 0;
+    }
+
+    &:active,
+    &:focus,
+    &:hover {
+      background-color: $ui-button-focus-background-color;
+    }
+
+    &:disabled:hover {
+      background-color: $ui-primary-color;
+    }
+
+    &.negative {
+      background: $ui-button-destructive-background-color;
+
+      &:hover,
+      &:active,
+      &:focus {
+        background-color: $ui-button-destructive-focus-background-color;
+      }
+    }
+  }
+
+  .button.button-tertiary {
+    padding: 9px;
+
+    &:hover,
+    &:focus,
+    &:active {
+      padding: 10px;
+    }
+  }
+
+  select {
+    appearance: none;
+    box-sizing: border-box;
+    font-size: 16px;
+    color: $primary-text-color;
+    display: block;
+    width: 100%;
+    outline: 0;
+    font-family: inherit;
+    resize: vertical;
+    background: darken($ui-base-color, 10%)
+      url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(lighten($ui-base-color, 12%))}'/></svg>")
+      no-repeat right 8px center / auto 16px;
+    border: 1px solid darken($ui-base-color, 14%);
+    border-radius: 4px;
+    padding-inline-start: 10px;
+    padding-inline-end: 30px;
+    height: 41px;
+  }
+
+  h4 {
+    margin-bottom: 15px !important;
+  }
+
+  .label_input {
+    &__wrapper {
+      position: relative;
+    }
+
+    &__append {
+      position: absolute;
+      inset-inline-end: 3px;
+      top: 1px;
+      padding: 10px;
+      padding-bottom: 9px;
+      font-size: 16px;
+      color: $dark-text-color;
+      font-family: inherit;
+      pointer-events: none;
+      cursor: default;
+      max-width: 140px;
+      white-space: nowrap;
+      overflow: hidden;
+
+      &::after {
+        content: '';
+        display: block;
+        position: absolute;
+        top: 0;
+        inset-inline-end: 0;
+        bottom: 1px;
+        width: 5px;
+        background-image: linear-gradient(
+          to right,
+          rgba(darken($ui-base-color, 10%), 0),
+          darken($ui-base-color, 10%)
+        );
+      }
+    }
+  }
+}
+
+.block-icon {
+  display: block;
+  margin: 0 auto;
+  margin-bottom: 10px;
+  font-size: 24px;
+}
+
+.flash-message {
+  background: lighten($ui-base-color, 8%);
+  color: $darker-text-color;
+  border-radius: 4px;
+  padding: 15px 10px;
+  margin-bottom: 30px;
+  text-align: center;
+
+  &.notice {
+    border: 1px solid rgba($valid-value-color, 0.5);
+    background: rgba($valid-value-color, 0.25);
+    color: $valid-value-color;
+  }
+
+  &.warning {
+    border: 1px solid rgba($gold-star, 0.5);
+    background: rgba($gold-star, 0.25);
+    color: $gold-star;
+  }
+
+  &.alert {
+    border: 1px solid rgba($error-value-color, 0.5);
+    background: rgba($error-value-color, 0.1);
+    color: $error-value-color;
+  }
+
+  &.hidden {
+    display: none;
+  }
+
+  a {
+    display: inline-block;
+    color: $darker-text-color;
+    text-decoration: none;
+
+    &:hover {
+      color: $primary-text-color;
+      text-decoration: underline;
+    }
+  }
+
+  &.warning a {
+    font-weight: 700;
+    color: inherit;
+    text-decoration: underline;
+
+    &:hover,
+    &:focus,
+    &:active {
+      text-decoration: none;
+      color: inherit;
+    }
+  }
+
+  p {
+    margin-bottom: 15px;
+  }
+
+  .oauth-code {
+    outline: 0;
+    box-sizing: border-box;
+    display: block;
+    width: 100%;
+    border: 0;
+    padding: 10px;
+    font-family: $font-monospace, monospace;
+    background: $ui-base-color;
+    color: $primary-text-color;
+    font-size: 14px;
+    margin: 0;
+
+    &::-moz-focus-inner {
+      border: 0;
+    }
+
+    &::-moz-focus-inner,
+    &:focus,
+    &:active {
+      outline: 0 !important;
+    }
+
+    &:focus {
+      background: lighten($ui-base-color, 4%);
+    }
+  }
+
+  strong {
+    font-weight: 500;
+
+    @each $lang in $cjk-langs {
+      &:lang(#{$lang}) {
+        font-weight: 700;
+      }
+    }
+  }
+
+  @media screen and (width <= 740px) and (width >= 441px) {
+    margin-top: 40px;
+  }
+
+  &.translation-prompt {
+    text-align: unset;
+    color: unset;
+
+    a {
+      text-decoration: underline;
+    }
+  }
+}
+
+.flash-message-stack {
+  margin-bottom: 30px;
+
+  .flash-message {
+    border-radius: 0;
+    margin-bottom: 0;
+    border-top-width: 0;
+
+    &:first-child {
+      border-radius: 4px 4px 0 0;
+      border-top-width: 1px;
+    }
+
+    &:last-child {
+      border-radius: 0 0 4px 4px;
+
+      &:first-child {
+        border-radius: 4px;
+      }
+    }
+  }
+}
+
+.form-footer {
+  margin-top: 30px;
+  text-align: center;
+
+  a {
+    color: $darker-text-color;
+    text-decoration: none;
+
+    &:hover {
+      text-decoration: underline;
+    }
+  }
+}
+
+.quick-nav {
+  list-style: none;
+  margin-bottom: 25px;
+  font-size: 14px;
+
+  li {
+    display: inline-block;
+    margin-inline-end: 10px;
+  }
+
+  a {
+    color: $highlight-text-color;
+    text-transform: uppercase;
+    text-decoration: none;
+    font-weight: 700;
+
+    &:hover,
+    &:focus,
+    &:active {
+      color: lighten($highlight-text-color, 8%);
+    }
+  }
+}
+
+.oauth-prompt,
+.follow-prompt {
+  margin-bottom: 30px;
+  color: $darker-text-color;
+
+  h2 {
+    font-size: 16px;
+    margin-bottom: 30px;
+    text-align: center;
+  }
+
+  strong {
+    color: $secondary-text-color;
+    font-weight: 500;
+
+    @each $lang in $cjk-langs {
+      &:lang(#{$lang}) {
+        font-weight: 700;
+      }
+    }
+  }
+}
+
+.oauth-prompt {
+  h3 {
+    color: $ui-secondary-color;
+    font-size: 17px;
+    line-height: 22px;
+    font-weight: 500;
+    margin-bottom: 30px;
+  }
+
+  p {
+    font-size: 14px;
+    line-height: 18px;
+    margin-bottom: 30px;
+  }
+
+  .permissions-list {
+    border: 1px solid $ui-base-color;
+    border-radius: 4px;
+    background: darken($ui-base-color, 4%);
+    margin-bottom: 30px;
+  }
+
+  .actions {
+    margin: 0 -10px;
+    display: flex;
+
+    form {
+      box-sizing: border-box;
+      padding: 0 10px;
+      flex: 1 1 auto;
+      min-height: 1px;
+      width: 50%;
+    }
+  }
+}
+
+.qr-wrapper {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: flex-start;
+}
+
+.qr-code {
+  flex: 0 0 auto;
+  background: $simple-background-color;
+  padding: 4px;
+  margin-inline-end: 10px;
+  margin-bottom: 20px;
+  box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
+  display: inline-block;
+
+  svg {
+    display: block;
+    margin: 0;
+  }
+}
+
+.qr-alternative {
+  margin-bottom: 20px;
+  color: $secondary-text-color;
+  flex: 150px;
+
+  samp {
+    display: block;
+    font-size: 14px;
+  }
+}
+
+.simple_form {
+  .warning {
+    box-sizing: border-box;
+    background: rgba($error-value-color, 0.5);
+    color: $primary-text-color;
+    text-shadow: 1px 1px 0 rgba($base-shadow-color, 0.3);
+    box-shadow: 0 2px 6px rgba($base-shadow-color, 0.4);
+    border-radius: 4px;
+    padding: 10px;
+    margin-bottom: 15px;
+
+    a {
+      color: $primary-text-color;
+      text-decoration: underline;
+
+      &:hover,
+      &:focus,
+      &:active {
+        text-decoration: none;
+      }
+    }
+
+    strong {
+      font-weight: 600;
+      display: block;
+      margin-bottom: 5px;
+
+      @each $lang in $cjk-langs {
+        &:lang(#{$lang}) {
+          font-weight: 700;
+        }
+      }
+
+      .fa {
+        font-weight: 400;
+      }
+    }
+  }
+}
+
+.action-pagination {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+
+  .actions,
+  .pagination {
+    flex: 1 1 auto;
+  }
+
+  .actions {
+    padding: 30px 0;
+    padding-inline-end: 20px;
+    flex: 0 0 auto;
+  }
+}
+
+.post-follow-actions {
+  text-align: center;
+  color: $darker-text-color;
+
+  div {
+    margin-bottom: 4px;
+  }
+}
+
+.alternative-login {
+  margin-top: 20px;
+  margin-bottom: 20px;
+
+  h4 {
+    font-size: 16px;
+    color: $primary-text-color;
+    text-align: center;
+    margin-bottom: 20px;
+    border: 0;
+    padding: 0;
+  }
+
+  .button {
+    display: block;
+  }
+}
+
+.scope-danger {
+  color: $warning-red;
+}
+
+.form_admin_settings_site_short_description,
+.form_admin_settings_site_description,
+.form_admin_settings_site_extended_description,
+.form_admin_settings_site_terms,
+.form_admin_settings_custom_css,
+.form_admin_settings_closed_registrations_message {
+  textarea {
+    font-family: $font-monospace, monospace;
+  }
+}
+
+.input-copy {
+  background: darken($ui-base-color, 10%);
+  border: 1px solid darken($ui-base-color, 14%);
+  border-radius: 4px;
+  display: flex;
+  align-items: center;
+  padding-inline-end: 4px;
+  position: relative;
+  top: 1px;
+  transition: border-color 300ms linear;
+
+  &__wrapper {
+    flex: 1 1 auto;
+  }
+
+  input[type='text'] {
+    background: transparent;
+    border: 0;
+    padding: 10px;
+    font-size: 14px;
+    font-family: $font-monospace, monospace;
+  }
+
+  button {
+    flex: 0 0 auto;
+    margin: 4px;
+    text-transform: none;
+    font-weight: 400;
+    font-size: 14px;
+    padding: 7px 18px;
+    padding-bottom: 6px;
+    width: auto;
+    transition: background 300ms linear;
+  }
+
+  &.copied {
+    border-color: $valid-value-color;
+    transition: none;
+
+    button {
+      background: $valid-value-color;
+      transition: none;
+    }
+  }
+}
+
+.input.user_confirm_password,
+.input.user_website {
+  &:not(.field_with_errors) {
+    display: none;
+  }
+}
+
+.simple_form .h-captcha {
+  display: flex;
+  justify-content: center;
+  margin-bottom: 30px;
+}
+
+.permissions-list {
+  &__item {
+    padding: 15px;
+    color: $ui-secondary-color;
+    border-bottom: 1px solid lighten($ui-base-color, 4%);
+    display: flex;
+    align-items: center;
+
+    &__text {
+      flex: 1 1 auto;
+
+      &__title {
+        font-weight: 500;
+      }
+
+      &__type {
+        color: $darker-text-color;
+      }
+    }
+
+    &__icon {
+      flex: 0 0 auto;
+      font-size: 18px;
+      width: 30px;
+      color: $valid-value-color;
+      display: flex;
+      align-items: center;
+    }
+
+    &:last-child {
+      border-bottom: 0;
+    }
+  }
+}
+
+// Only remove padding when listing applications, to prevent styling issues on
+// the Authorization page.
+.applications-list {
+  .permissions-list__item:last-child {
+    padding-bottom: 0;
+  }
+}
+
+.keywords-table {
+  thead {
+    th {
+      white-space: nowrap;
+    }
+
+    th:first-child {
+      width: 100%;
+    }
+  }
+
+  tfoot {
+    td {
+      border: 0;
+    }
+  }
+
+  .input.string {
+    margin-bottom: 0;
+  }
+
+  .label_input__wrapper {
+    margin-top: 10px;
+  }
+
+  .table-action-link {
+    margin-top: 10px;
+    white-space: nowrap;
+  }
+}
+
+.progress-tracker {
+  display: flex;
+  align-items: center;
+  padding-bottom: 30px;
+  margin-bottom: 30px;
+
+  li {
+    flex: 0 0 auto;
+    position: relative;
+  }
+
+  .separator {
+    height: 2px;
+    background: $ui-base-lighter-color;
+    flex: 1 1 auto;
+
+    &.completed {
+      background: $highlight-text-color;
+    }
+  }
+
+  .circle {
+    box-sizing: border-box;
+    position: relative;
+    width: 30px;
+    height: 30px;
+    border-radius: 50%;
+    border: 2px solid $ui-base-lighter-color;
+    flex: 0 0 auto;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    svg {
+      width: 16px;
+    }
+  }
+
+  .label {
+    position: absolute;
+    font-size: 14px;
+    font-weight: 500;
+    color: $secondary-text-color;
+    padding-top: 10px;
+    text-align: center;
+    width: 100px;
+    left: 50%;
+    transform: translateX(-50%);
+  }
+
+  li:first-child .label {
+    inset-inline-start: 0;
+    inset-inline-end: auto;
+    text-align: start;
+    transform: none;
+  }
+
+  li:last-child .label {
+    inset-inline-start: auto;
+    inset-inline-end: 0;
+    text-align: end;
+    transform: none;
+  }
+
+  .active .circle {
+    border-color: $highlight-text-color;
+
+    &::before {
+      content: '';
+      width: 10px;
+      height: 10px;
+      border-radius: 50%;
+      background: $highlight-text-color;
+      position: absolute;
+      left: 50%;
+      top: 50%;
+      transform: translate(-50%, -50%);
+    }
+  }
+
+  .completed .circle {
+    border-color: $highlight-text-color;
+    background: $highlight-text-color;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/index.scss b/app/javascript/flavours/blobfox/styles/index.scss
new file mode 100644
index 00000000000000..1cb913c8b832ec
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/index.scss
@@ -0,0 +1,24 @@
+@import 'mixins';
+@import 'variables';
+@import 'styles/fonts/roboto';
+@import 'styles/fonts/roboto-mono';
+
+@import 'reset';
+@import 'basics';
+@import 'branding';
+@import 'containers';
+@import 'lists';
+@import 'modal';
+@import 'widgets';
+@import 'forms';
+@import 'accounts';
+@import 'statuses';
+@import 'components/index';
+@import 'polls';
+@import 'about';
+@import 'tables';
+@import 'admin';
+@import 'accessibility';
+@import 'rtl';
+@import 'dashboard';
+@import 'rich_text';
diff --git a/app/javascript/flavours/blobfox/styles/lists.scss b/app/javascript/flavours/blobfox/styles/lists.scss
new file mode 100644
index 00000000000000..6019cd800283d7
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/lists.scss
@@ -0,0 +1,19 @@
+.no-list {
+  list-style: none;
+
+  li {
+    display: inline-block;
+    margin: 0 5px;
+  }
+}
+
+.recovery-codes {
+  list-style: none;
+  margin: 0 auto;
+
+  li {
+    font-size: 125%;
+    line-height: 1.5;
+    letter-spacing: 1px;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/mastodon-light.scss b/app/javascript/flavours/blobfox/styles/mastodon-light.scss
new file mode 100644
index 00000000000000..8fc132651bdf67
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/mastodon-light.scss
@@ -0,0 +1,3 @@
+@import 'mastodon-light/variables';
+@import 'index';
+@import 'mastodon-light/diff';
diff --git a/app/javascript/flavours/blobfox/styles/mastodon-light/diff.scss b/app/javascript/flavours/blobfox/styles/mastodon-light/diff.scss
new file mode 100644
index 00000000000000..30425afba022ce
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/mastodon-light/diff.scss
@@ -0,0 +1,743 @@
+// Notes!
+// Sass color functions, "darken" and "lighten" are automatically replaced.
+
+html {
+  scrollbar-color: $ui-base-color rgba($ui-base-color, 0.25);
+}
+
+.simple_form .button.button-tertiary {
+  color: $highlight-text-color;
+}
+
+.status-card__actions button,
+.status-card__actions a {
+  color: rgba($white, 0.8);
+
+  &:hover,
+  &:active,
+  &:focus {
+    color: $white;
+  }
+}
+
+// Change default background colors of columns
+.column > .scrollable,
+.getting-started,
+.column-inline-form,
+.regeneration-indicator {
+  background: $white;
+  border: 1px solid lighten($ui-base-color, 8%);
+  border-top: 0;
+}
+
+.error-column {
+  border: 1px solid lighten($ui-base-color, 8%);
+}
+
+.column > .scrollable.about {
+  border-top: 1px solid lighten($ui-base-color, 8%);
+}
+
+.about__meta,
+.about__section__title,
+.interaction-modal {
+  background: $white;
+  border: 1px solid lighten($ui-base-color, 8%);
+}
+
+.rules-list li::before {
+  background: $ui-highlight-color;
+}
+
+.directory__card__img {
+  background: lighten($ui-base-color, 12%);
+}
+
+.filter-form {
+  background: $white;
+  border-bottom: 1px solid lighten($ui-base-color, 8%);
+}
+
+.column-back-button,
+.column-header {
+  background: $white;
+  border: 1px solid lighten($ui-base-color, 8%);
+
+  @media screen and (max-width: $no-gap-breakpoint) {
+    border-top: 0;
+  }
+
+  &--slim-button {
+    top: -50px;
+    right: 0;
+  }
+}
+
+.column-header__back-button,
+.column-header__button,
+.column-header__button.active,
+.account__header {
+  background: $white;
+}
+
+.column-header__button.active {
+  color: $ui-highlight-color;
+
+  &:hover,
+  &:active,
+  &:focus {
+    color: $ui-highlight-color;
+    background: $white;
+  }
+}
+
+.account__header__bar .avatar .account__avatar {
+  border-color: $white;
+}
+
+.getting-started__footer a {
+  color: $ui-secondary-color;
+  text-decoration: underline;
+}
+
+.confirmation-modal__secondary-button,
+.confirmation-modal__cancel-button,
+.mute-modal__cancel-button,
+.block-modal__cancel-button {
+  color: lighten($ui-base-color, 26%);
+
+  &:hover,
+  &:focus,
+  &:active {
+    color: $primary-text-color;
+  }
+}
+
+.column-subheading {
+  background: darken($ui-base-color, 4%);
+  border-bottom: 1px solid lighten($ui-base-color, 8%);
+}
+
+.getting-started,
+.scrollable {
+  .column-link {
+    background: $white;
+    border-bottom: 1px solid lighten($ui-base-color, 8%);
+
+    &:hover,
+    &:active,
+    &:focus {
+      background: $ui-base-color;
+    }
+  }
+}
+
+.getting-started .navigation-bar {
+  border-top: 1px solid lighten($ui-base-color, 8%);
+  border-bottom: 1px solid lighten($ui-base-color, 8%);
+
+  @media screen and (max-width: $no-gap-breakpoint) {
+    border-top: 0;
+  }
+}
+
+.compose-form__autosuggest-wrapper,
+.poll__option input[type='text'],
+.compose-form .spoiler-input__input,
+.compose-form__poll-wrapper select,
+.search__input,
+.setting-text,
+.report-dialog-modal__textarea,
+.audio-player {
+  border: 1px solid lighten($ui-base-color, 8%);
+}
+
+.report-dialog-modal .dialog-option .poll__input {
+  color: $white;
+}
+
+.search__input {
+  @media screen and (max-width: $no-gap-breakpoint) {
+    border-top: 0;
+    border-bottom: 0;
+  }
+}
+
+.list-editor .search .search__input {
+  border-top: 0;
+  border-bottom: 0;
+}
+
+.compose-form__poll-wrapper select {
+  background: $simple-background-color
+    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(lighten($ui-base-color, 8%))}'/></svg>")
+    no-repeat right 8px center / auto 16px;
+}
+
+.compose-form__poll-wrapper,
+.compose-form__poll-wrapper .poll__footer {
+  border-top-color: lighten($ui-base-color, 8%);
+}
+
+.notification__filter-bar {
+  border: 1px solid lighten($ui-base-color, 8%);
+  border-top: 0;
+}
+
+.compose-form .compose-form__buttons-wrapper {
+  background: $ui-base-color;
+  border: 1px solid lighten($ui-base-color, 8%);
+  border-top: 0;
+}
+
+.drawer__header,
+.drawer__inner {
+  background: $white;
+  border: 1px solid lighten($ui-base-color, 8%);
+}
+
+.drawer__inner__mastodon {
+  background: $white
+    url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color($ui-base-color)}"/></svg>')
+    no-repeat bottom / 100% auto;
+}
+
+// Change the colors used in compose-form
+.compose-form {
+  .compose-form__modifiers {
+    .compose-form__upload__actions .icon-button,
+    .compose-form__upload__warning .icon-button {
+      color: lighten($white, 7%);
+
+      &:active,
+      &:focus,
+      &:hover {
+        color: $white;
+      }
+    }
+  }
+
+  .compose-form__buttons-wrapper {
+    background: darken($ui-base-color, 6%);
+  }
+
+  .autosuggest-textarea__suggestions {
+    background: darken($ui-base-color, 6%);
+  }
+
+  .autosuggest-textarea__suggestions__item {
+    &:hover,
+    &:focus,
+    &:active,
+    &.selected {
+      background: lighten($ui-base-color, 4%);
+    }
+  }
+}
+
+.emoji-mart-bar {
+  border-color: lighten($ui-base-color, 4%);
+
+  &:first-child {
+    background: darken($ui-base-color, 6%);
+  }
+}
+
+.emoji-mart-search input {
+  background: rgba($ui-base-color, 0.3);
+  border-color: $ui-base-color;
+}
+
+.upload-progress__backdrop {
+  background: $ui-base-color;
+}
+
+// Change the background colors of statuses
+.focusable:focus {
+  background: lighten($white, 4%);
+}
+
+.detailed-status,
+.detailed-status__action-bar {
+  background: $white;
+}
+
+// Change the background colors of status__content__spoiler-link
+.reply-indicator__content .status__content__spoiler-link,
+.status__content .status__content__spoiler-link {
+  background: $ui-base-color;
+
+  &:hover,
+  &:focus {
+    background: lighten($ui-base-color, 4%);
+  }
+}
+
+// Change the background colors of media and video spoilers
+.media-spoiler,
+.video-player__spoiler {
+  background: $ui-base-color;
+}
+
+.privacy-dropdown.active .privacy-dropdown__value.active .icon-button {
+  color: $white;
+}
+
+.account-gallery__item a {
+  background-color: $ui-base-color;
+}
+
+// Change the colors used in the dropdown menu
+.dropdown-menu {
+  background: $white;
+
+  &__arrow::before {
+    background-color: $white;
+  }
+
+  &__item {
+    color: $darker-text-color;
+
+    &--dangerous {
+      color: $error-value-color;
+    }
+
+    a,
+    button {
+      background: $white;
+    }
+  }
+}
+
+// Change the text colors on inverted background
+.privacy-dropdown__option.active,
+.privacy-dropdown__option:hover,
+.privacy-dropdown__option.active .privacy-dropdown__option__content,
+.privacy-dropdown__option.active .privacy-dropdown__option__content strong,
+.privacy-dropdown__option:hover .privacy-dropdown__option__content,
+.privacy-dropdown__option:hover .privacy-dropdown__option__content strong,
+.dropdown-menu__item:not(.dropdown-menu__item--dangerous) a:active,
+.dropdown-menu__item:not(.dropdown-menu__item--dangerous) a:focus,
+.dropdown-menu__item:not(.dropdown-menu__item--dangerous) a:hover,
+.actions-modal ul li:not(:empty) a.active,
+.actions-modal ul li:not(:empty) a.active button,
+.actions-modal ul li:not(:empty) a:active,
+.actions-modal ul li:not(:empty) a:active button,
+.actions-modal ul li:not(:empty) a:focus,
+.actions-modal ul li:not(:empty) a:focus button,
+.actions-modal ul li:not(:empty) a:hover,
+.actions-modal ul li:not(:empty) a:hover button,
+.language-dropdown__dropdown__results__item.active,
+.admin-wrapper .sidebar ul .simple-navigation-active-leaf a,
+.simple_form .block-button,
+.simple_form .button,
+.simple_form button {
+  color: $white;
+}
+
+.language-dropdown__dropdown__results__item
+  .language-dropdown__dropdown__results__item__common-name {
+  color: lighten($ui-base-color, 8%);
+}
+
+.language-dropdown__dropdown__results__item.active
+  .language-dropdown__dropdown__results__item__common-name {
+  color: darken($ui-base-color, 12%);
+}
+
+.dropdown-menu__separator,
+.dropdown-menu__item.edited-timestamp__history__item,
+.dropdown-menu__container__header,
+.compare-history-modal .report-modal__target,
+.report-dialog-modal .poll__option.dialog-option {
+  border-bottom-color: lighten($ui-base-color, 4%);
+}
+
+.report-dialog-modal__container {
+  border-top-color: lighten($ui-base-color, 4%);
+}
+
+// Change the background colors of modals
+.actions-modal,
+.boost-modal,
+.confirmation-modal,
+.mute-modal,
+.block-modal,
+.report-modal,
+.report-dialog-modal,
+.embed-modal,
+.error-modal,
+.onboarding-modal,
+.compare-history-modal,
+.report-modal__comment .setting-text__wrapper,
+.report-modal__comment .setting-text,
+.announcements,
+.picture-in-picture__header,
+.picture-in-picture__footer,
+.reactions-bar__item {
+  background: $white;
+  border: 1px solid lighten($ui-base-color, 8%);
+}
+
+.reactions-bar__item:hover,
+.reactions-bar__item:focus,
+.reactions-bar__item:active,
+.language-dropdown__dropdown__results__item:hover,
+.language-dropdown__dropdown__results__item:focus,
+.language-dropdown__dropdown__results__item:active {
+  background-color: $ui-base-color;
+}
+
+.reactions-bar__item.active {
+  background-color: mix($white, $ui-highlight-color, 80%);
+  border-color: mix(lighten($ui-base-color, 8%), $ui-highlight-color, 80%);
+}
+
+.media-modal__overlay .picture-in-picture__footer {
+  border: 0;
+}
+
+.picture-in-picture__header {
+  border-bottom: 0;
+}
+
+.announcements,
+.picture-in-picture__footer {
+  border-top: 0;
+}
+
+.icon-with-badge__badge {
+  border-color: $white;
+  color: $white;
+}
+
+.report-modal__comment {
+  border-right-color: lighten($ui-base-color, 8%);
+}
+
+.report-modal__container {
+  border-top-color: lighten($ui-base-color, 8%);
+}
+
+.column-header__collapsible-inner {
+  background: darken($ui-base-color, 4%);
+  border: 1px solid lighten($ui-base-color, 8%);
+  border-top: 0;
+}
+
+.column-settings__hashtags .column-select__option {
+  color: $white;
+}
+
+.dashboard__quick-access,
+.focal-point__preview strong,
+.admin-wrapper .content__heading__tabs a.selected {
+  color: $white;
+}
+
+.flash-message.warning {
+  color: lighten($gold-star, 16%);
+}
+
+.boost-modal__action-bar,
+.confirmation-modal__action-bar,
+.mute-modal__action-bar,
+.block-modal__action-bar,
+.onboarding-modal__paginator,
+.error-modal__footer {
+  background: darken($ui-base-color, 6%);
+
+  .onboarding-modal__nav,
+  .error-modal__nav {
+    &:hover,
+    &:focus,
+    &:active {
+      background-color: darken($ui-base-color, 12%);
+    }
+  }
+}
+
+.display-case__case {
+  background: $white;
+}
+
+.embed-modal .embed-modal__container .embed-modal__html {
+  background: $white;
+  border: 1px solid lighten($ui-base-color, 8%);
+
+  &:focus {
+    border-color: lighten($ui-base-color, 12%);
+    background: $white;
+  }
+}
+
+.react-toggle-track {
+  background: $ui-secondary-color;
+}
+
+.react-toggle:hover:not(.react-toggle--disabled) .react-toggle-track {
+  background: darken($ui-secondary-color, 10%);
+}
+
+.react-toggle.react-toggle--checked:hover:not(.react-toggle--disabled)
+  .react-toggle-track {
+  background: lighten($ui-highlight-color, 10%);
+}
+
+// Change the default color used for the text in an empty column or on the error column
+.empty-column-indicator,
+.error-column {
+  color: $primary-text-color;
+  background: $white;
+}
+
+// Change the default colors used on some parts of the profile pages
+.activity-stream-tabs {
+  background: $account-background-color;
+  border-bottom-color: lighten($ui-base-color, 8%);
+}
+
+.nothing-here,
+.page-header,
+.directory__tag > a,
+.directory__tag > div {
+  background: $white;
+  border: 1px solid lighten($ui-base-color, 8%);
+
+  @media screen and (max-width: $no-gap-breakpoint) {
+    border-left: 0;
+    border-right: 0;
+    border-top: 0;
+  }
+}
+
+.simple_form {
+  input[type='text'],
+  input[type='number'],
+  input[type='email'],
+  input[type='password'],
+  textarea {
+    &:hover {
+      border-color: lighten($ui-base-color, 12%);
+    }
+  }
+}
+
+.picture-in-picture-placeholder {
+  background: $white;
+  border-color: lighten($ui-base-color, 8%);
+  color: lighten($ui-base-color, 8%);
+}
+
+.directory__tag > a {
+  &:hover,
+  &:active,
+  &:focus {
+    background: $ui-base-color;
+  }
+
+  @media screen and (max-width: $no-gap-breakpoint) {
+    border: 0;
+  }
+}
+
+.batch-table {
+  &__toolbar,
+  &__row,
+  .nothing-here {
+    border-color: lighten($ui-base-color, 8%);
+  }
+}
+
+.activity-stream {
+  border: 1px solid lighten($ui-base-color, 8%);
+
+  &--under-tabs {
+    border-top: 0;
+  }
+
+  .entry {
+    background: $account-background-color;
+
+    .detailed-status.light,
+    .more.light,
+    .status.light {
+      border-bottom-color: lighten($ui-base-color, 8%);
+    }
+  }
+
+  .status.light {
+    .status__content {
+      color: $primary-text-color;
+    }
+
+    .display-name {
+      strong {
+        color: $primary-text-color;
+      }
+    }
+  }
+}
+
+.accounts-grid {
+  .account-grid-card {
+    .controls {
+      .icon-button {
+        color: $darker-text-color;
+      }
+    }
+
+    .name {
+      a {
+        color: $primary-text-color;
+      }
+    }
+
+    .username {
+      color: $darker-text-color;
+    }
+
+    .account__header__content {
+      color: $primary-text-color;
+    }
+  }
+}
+
+.simple_form {
+  .warning {
+    box-shadow: none;
+    background: rgba($error-red, 0.5);
+    text-shadow: none;
+  }
+
+  .recommended {
+    border-color: $ui-highlight-color;
+    color: $ui-highlight-color;
+    background-color: rgba($ui-highlight-color, 0.1);
+  }
+}
+
+.compose-form .compose-form__warning {
+  border-color: $ui-highlight-color;
+  background-color: rgba($ui-highlight-color, 0.1);
+
+  &,
+  a {
+    color: $ui-highlight-color;
+  }
+}
+
+.reply-indicator {
+  background: transparent;
+  border: 1px solid lighten($ui-base-color, 8%);
+}
+
+.status__content,
+.reply-indicator__content {
+  a {
+    color: $highlight-text-color;
+  }
+}
+
+.notification__filter-bar button.active::after,
+.account__section-headline a.active::after {
+  border-color: transparent transparent $white;
+}
+
+.hero-widget,
+.moved-account-widget,
+.memoriam-widget,
+.activity-stream,
+.nothing-here,
+.directory__tag > a,
+.directory__tag > div,
+.card > a,
+.page-header,
+.compose-form .compose-form__warning {
+  box-shadow: none;
+}
+
+.mute-modal select {
+  border: 1px solid lighten($ui-base-color, 8%);
+  background: $simple-background-color
+    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(lighten($ui-base-color, 8%))}'/></svg>")
+    no-repeat right 8px center / auto 16px;
+}
+
+// blobfox-soc-specific changes
+
+.pillbar-button {
+  background: $ui-secondary-color;
+
+  &:not([disabled]) {
+    &:hover,
+    &:focus {
+      background: darken($ui-secondary-color, 10%);
+    }
+
+    &.active {
+      background-color: darken($ui-highlight-color, 2%);
+
+      &:hover,
+      &:focus {
+        background: lighten($ui-highlight-color, 10%);
+      }
+    }
+  }
+}
+
+.blobfox.local-settings {
+  background: $ui-base-color;
+  border: 1px solid lighten($ui-base-color, 8%);
+}
+
+.blobfox.local-settings__navigation {
+  background: darken($ui-base-color, 8%);
+}
+
+.blobfox.local-settings__navigation__item {
+  background: darken($ui-base-color, 8%);
+  border-bottom: 1px lighten($ui-base-color, 8%) solid;
+
+  &:hover {
+    background: $ui-base-color;
+  }
+
+  &.active {
+    background: $ui-highlight-color;
+    color: $white;
+  }
+
+  &.close,
+  &.close:hover {
+    background: $error-value-color;
+    color: $primary-text-color;
+  }
+}
+
+.notification__dismiss-overlay {
+  .wrappy {
+    box-shadow: unset;
+
+    .ckbox {
+      text-shadow: unset;
+    }
+  }
+}
+
+.status.collapsed .status__content::after {
+  background: linear-gradient(
+    rgba(darken($ui-base-color, 13%), 0),
+    rgba(darken($ui-base-color, 13%), 1)
+  );
+}
+
+.drawer__inner__mastodon {
+  background: $white
+    url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color($ui-base-color)}"/></svg>')
+    no-repeat bottom / 100% auto !important;
+
+  .mastodon {
+    filter: contrast(75%) brightness(75%) !important;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/mastodon-light/variables.scss b/app/javascript/flavours/blobfox/styles/mastodon-light/variables.scss
new file mode 100644
index 00000000000000..250e200fc6d258
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/mastodon-light/variables.scss
@@ -0,0 +1,57 @@
+// Dependent colors
+$black: #000000;
+$white: #ffffff;
+
+$classic-base-color: #282c37;
+$classic-primary-color: #9baec8;
+$classic-secondary-color: #d9e1e8;
+$classic-highlight-color: #6364ff;
+
+$blurple-600: #563acc; // Iris
+$blurple-500: #6364ff; // Brand purple
+$blurple-300: #858afa; // Faded Blue
+$grey-600: #4e4c5a; // Trout
+$grey-100: #dadaf3; // Topaz
+
+// Differences
+$success-green: lighten(#3c754d, 8%);
+
+$base-overlay-background: $white !default;
+$valid-value-color: $success-green !default;
+
+$ui-base-color: $classic-secondary-color !default;
+$ui-base-lighter-color: #b0c0cf;
+$ui-primary-color: #9bcbed;
+$ui-secondary-color: $classic-base-color !default;
+$ui-highlight-color: $classic-highlight-color !default;
+
+$ui-button-secondary-color: $grey-600 !default;
+$ui-button-secondary-border-color: $grey-600 !default;
+$ui-button-secondary-focus-color: $white !default;
+
+$ui-button-tertiary-color: $blurple-500 !default;
+$ui-button-tertiary-border-color: $blurple-500 !default;
+
+$primary-text-color: $black !default;
+$darker-text-color: $classic-base-color !default;
+$highlight-text-color: darken($ui-highlight-color, 8%) !default;
+$dark-text-color: #444b5d;
+$action-button-color: #606984;
+
+$inverted-text-color: $black !default;
+$lighter-text-color: $classic-base-color !default;
+$light-text-color: #444b5d;
+
+// Newly added colors
+$account-background-color: $white !default;
+
+// Invert darkened and lightened colors
+@function darken($color, $amount) {
+  @return hsl(hue($color), saturation($color), lightness($color) + $amount);
+}
+
+@function lighten($color, $amount) {
+  @return hsl(hue($color), saturation($color), lightness($color) - $amount);
+}
+
+$emojis-requiring-inversion: 'chains';
diff --git a/app/javascript/flavours/blobfox/styles/modal.scss b/app/javascript/flavours/blobfox/styles/modal.scss
new file mode 100644
index 00000000000000..0b7220b21d1115
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/modal.scss
@@ -0,0 +1,37 @@
+.modal-layout {
+  background: $ui-base-color
+    url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color($ui-base-lighter-color)}33"/></svg>')
+    repeat-x bottom fixed;
+  display: flex;
+  flex-direction: column;
+  height: 100vh;
+  padding: 0;
+}
+
+.modal-layout__mastodon {
+  display: flex;
+  flex: 1;
+  flex-direction: column;
+  justify-content: flex-end;
+
+  > div {
+    flex: 1;
+    max-height: 235px;
+    position: relative;
+
+    img {
+      max-height: 100%;
+      max-width: 100%;
+      height: 100%;
+      position: absolute;
+      bottom: 0;
+      inset-inline-start: 0;
+    }
+  }
+}
+
+@media screen and (width <= 600px) {
+  .account-header {
+    margin-top: 0;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/polls.scss b/app/javascript/flavours/blobfox/styles/polls.scss
new file mode 100644
index 00000000000000..4566a013a630fb
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/polls.scss
@@ -0,0 +1,314 @@
+.poll {
+  margin-top: 16px;
+  font-size: 14px;
+
+  ul,
+  .e-content & ul {
+    margin: 0;
+    list-style: none;
+  }
+
+  li {
+    margin-bottom: 10px;
+    position: relative;
+  }
+
+  &__chart {
+    border-radius: 4px;
+    display: block;
+    background: darken($ui-primary-color, 5%);
+    height: 5px;
+    min-width: 1%;
+
+    &.leading {
+      background: $ui-highlight-color;
+    }
+  }
+
+  progress {
+    border: 0;
+    display: block;
+    width: 100%;
+    height: 5px;
+    appearance: none;
+    background: transparent;
+
+    &::-webkit-progress-bar {
+      background: transparent;
+    }
+
+    // Those rules need to be entirely separate or they won't work, hence the
+    // duplication
+    &::-moz-progress-bar {
+      border-radius: 4px;
+      background: darken($ui-primary-color, 5%);
+    }
+
+    &::-ms-fill {
+      border-radius: 4px;
+      background: darken($ui-primary-color, 5%);
+    }
+
+    &::-webkit-progress-value {
+      border-radius: 4px;
+      background: darken($ui-primary-color, 5%);
+    }
+  }
+
+  &__option {
+    position: relative;
+    display: flex;
+    padding: 6px 0;
+    line-height: 18px;
+    cursor: default;
+    overflow: hidden;
+
+    &__text {
+      display: inline-block;
+      word-wrap: break-word;
+      overflow-wrap: break-word;
+      max-width: calc(100% - 45px - 25px);
+    }
+
+    input[type='radio'],
+    input[type='checkbox'] {
+      display: none;
+    }
+
+    .autosuggest-input {
+      flex: 1 1 auto;
+    }
+
+    input[type='text'] {
+      display: block;
+      box-sizing: border-box;
+      width: 100%;
+      font-size: 14px;
+      color: $inverted-text-color;
+      outline: 0;
+      font-family: inherit;
+      background: $simple-background-color;
+      border: 1px solid darken($simple-background-color, 14%);
+      border-radius: 4px;
+      padding: 6px 10px;
+
+      &:focus {
+        border-color: $highlight-text-color;
+      }
+    }
+
+    &.selectable {
+      cursor: pointer;
+    }
+
+    &.editable {
+      display: flex;
+      align-items: center;
+      overflow: visible;
+    }
+  }
+
+  &__input {
+    display: inline-block;
+    position: relative;
+    border: 1px solid $ui-primary-color;
+    box-sizing: border-box;
+    width: 18px;
+    height: 18px;
+    margin-inline-end: 10px;
+    top: -1px;
+    border-radius: 50%;
+    vertical-align: middle;
+    margin-top: auto;
+    margin-bottom: auto;
+    flex: 0 0 18px;
+
+    &.checkbox {
+      border-radius: 4px;
+    }
+
+    &:active,
+    &:focus,
+    &:hover {
+      border-color: lighten($valid-value-color, 15%);
+      border-width: 4px;
+    }
+
+    &.active {
+      background-color: $valid-value-color;
+      border-color: $valid-value-color;
+    }
+
+    &::-moz-focus-inner {
+      outline: 0 !important;
+      border: 0;
+    }
+
+    &:focus,
+    &:active {
+      outline: 0 !important;
+    }
+
+    &.disabled {
+      border-color: $dark-text-color;
+
+      &.active {
+        background: $dark-text-color;
+      }
+
+      &:active,
+      &:focus,
+      &:hover {
+        border-color: $dark-text-color;
+        border-width: 1px;
+      }
+    }
+  }
+
+  &__number {
+    display: inline-block;
+    width: 45px;
+    font-weight: 700;
+    flex: 0 0 45px;
+  }
+
+  &__voted {
+    padding: 0 5px;
+    display: inline-block;
+
+    &__mark {
+      font-size: 18px;
+    }
+  }
+
+  &__footer {
+    padding-top: 6px;
+    padding-bottom: 5px;
+    color: $dark-text-color;
+  }
+
+  &__link {
+    display: inline;
+    background: transparent;
+    padding: 0;
+    margin: 0;
+    border: 0;
+    color: $dark-text-color;
+    text-decoration: underline;
+    font-size: inherit;
+
+    &:hover {
+      text-decoration: none;
+    }
+
+    &:active,
+    &:focus {
+      background-color: rgba($dark-text-color, 0.1);
+    }
+  }
+
+  .button {
+    height: 36px;
+    padding: 0 16px;
+    margin-inline-end: 10px;
+    font-size: 14px;
+  }
+}
+
+.compose-form__poll-wrapper {
+  border-top: 1px solid darken($simple-background-color, 8%);
+  overflow-x: hidden;
+
+  ul {
+    padding: 10px;
+  }
+
+  .poll__input {
+    &:active,
+    &:focus,
+    &:hover {
+      border-color: $ui-button-focus-background-color;
+    }
+  }
+
+  .poll__footer {
+    border-top: 1px solid darken($simple-background-color, 8%);
+    padding: 10px;
+    display: flex;
+    align-items: center;
+
+    button,
+    select {
+      width: 100%;
+      flex: 1 1 50%;
+
+      &:focus {
+        border-color: $highlight-text-color;
+      }
+    }
+  }
+
+  .button.button-secondary {
+    font-size: 14px;
+    font-weight: 400;
+    padding: 6px 10px;
+    height: auto;
+    line-height: inherit;
+    color: $action-button-color;
+    border-color: $action-button-color;
+    margin-inline-end: 5px;
+
+    &:hover,
+    &:focus,
+    &.active {
+      border-color: $action-button-color;
+      background-color: $action-button-color;
+      color: $ui-button-color;
+    }
+  }
+
+  li {
+    display: flex;
+    align-items: center;
+
+    .poll__option {
+      flex: 0 0 auto;
+      width: calc(100% - (23px + 6px));
+      margin-inline-end: 6px;
+    }
+  }
+
+  select {
+    appearance: none;
+    box-sizing: border-box;
+    font-size: 14px;
+    color: $inverted-text-color;
+    display: inline-block;
+    width: auto;
+    outline: 0;
+    font-family: inherit;
+    background: $simple-background-color
+      url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(darken($simple-background-color, 14%))}'/></svg>")
+      no-repeat right 8px center / auto 16px;
+    border: 1px solid darken($simple-background-color, 14%);
+    border-radius: 4px;
+    padding: 6px 10px;
+    padding-inline-end: 30px;
+  }
+
+  .icon-button.disabled {
+    color: darken($simple-background-color, 14%);
+  }
+}
+
+.muted .poll {
+  color: $dark-text-color;
+
+  &__chart {
+    background: rgba(darken($ui-primary-color, 14%), 0.7);
+
+    &.leading {
+      background: rgba($ui-highlight-color, 0.5);
+    }
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/reset.scss b/app/javascript/flavours/blobfox/styles/reset.scss
new file mode 100644
index 00000000000000..f54ed5bc79b1a7
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/reset.scss
@@ -0,0 +1,95 @@
+/* http://meyerweb.com/eric/tools/css/reset/
+   v2.0 | 20110126
+   License: none (public domain)
+*/
+
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, embed,
+figure, figcaption, footer, header, hgroup,
+menu, nav, output, ruby, section, summary,
+time, mark, audio, video {
+  margin: 0;
+  padding: 0;
+  border: 0;
+  font-size: 100%;
+  font: inherit;
+  vertical-align: baseline;
+}
+
+/* HTML5 display-role reset for older browsers */
+article, aside, details, figcaption, figure,
+footer, header, hgroup, menu, nav, section {
+  display: block;
+}
+
+body {
+  line-height: 1;
+}
+
+ol, ul {
+  list-style: none;
+}
+
+blockquote, q {
+  quotes: none;
+}
+
+blockquote:before, blockquote:after,
+q:before, q:after {
+  content: '';
+  content: none;
+}
+
+table {
+  border-collapse: collapse;
+  border-spacing: 0;
+}
+
+html {
+  scrollbar-color: lighten($ui-base-color, 4%) rgba($base-overlay-background, 0.1);
+}
+
+::-webkit-scrollbar {
+  width: 12px;
+  height: 12px;
+}
+
+::-webkit-scrollbar-thumb {
+  background: lighten($ui-base-color, 4%);
+  border: 0px none $base-border-color;
+  border-radius: 50px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+  background: lighten($ui-base-color, 6%);
+}
+
+::-webkit-scrollbar-thumb:active {
+  background: lighten($ui-base-color, 4%);
+}
+
+::-webkit-scrollbar-track {
+  border: 0px none $base-border-color;
+  border-radius: 0;
+  background: rgba($base-overlay-background, 0.1);
+}
+
+::-webkit-scrollbar-track:hover {
+  background: $ui-base-color;
+}
+
+::-webkit-scrollbar-track:active {
+  background: $ui-base-color;
+}
+
+::-webkit-scrollbar-corner {
+  background: transparent;
+}
diff --git a/app/javascript/flavours/blobfox/styles/rich_text.scss b/app/javascript/flavours/blobfox/styles/rich_text.scss
new file mode 100644
index 00000000000000..6224302ee3f5f9
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/rich_text.scss
@@ -0,0 +1,99 @@
+.status__content__text,
+.e-content,
+.reply-indicator__content {
+  pre,
+  blockquote {
+    margin-bottom: 20px;
+    white-space: pre-wrap;
+    unicode-bidi: plaintext;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+
+  blockquote {
+    padding-inline-start: 10px;
+    border-inline-start: 3px solid $darker-text-color;
+    color: $darker-text-color;
+    white-space: normal;
+
+    p:last-child {
+      margin-bottom: 0;
+    }
+  }
+
+  & > ul,
+  & > ol {
+    margin-bottom: 20px;
+  }
+
+  h1,
+  h2,
+  h3,
+  h4,
+  h5 {
+    margin-top: 20px;
+    margin-bottom: 20px;
+  }
+
+  h1,
+  h2 {
+    font-weight: 700;
+    font-size: 1.2em;
+  }
+
+  h2 {
+    font-size: 1.1em;
+  }
+
+  h3,
+  h4,
+  h5 {
+    font-weight: 500;
+  }
+
+  b,
+  strong {
+    font-weight: 700;
+  }
+
+  em,
+  i {
+    font-style: italic;
+  }
+
+  sub {
+    font-size: smaller;
+    vertical-align: sub;
+  }
+
+  sup {
+    font-size: smaller;
+    vertical-align: super;
+  }
+
+  ul,
+  ol {
+    margin-inline-start: 2em;
+
+    p {
+      margin: 0;
+    }
+  }
+
+  ul {
+    list-style-type: disc;
+  }
+
+  ol {
+    list-style-type: decimal;
+  }
+}
+
+.reply-indicator__content {
+  blockquote {
+    border-inline-start-color: $inverted-text-color;
+    color: $inverted-text-color;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/rtl.scss b/app/javascript/flavours/blobfox/styles/rtl.scss
new file mode 100644
index 00000000000000..e69d5d789118cf
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/rtl.scss
@@ -0,0 +1,123 @@
+body.rtl {
+  direction: rtl;
+
+  .reactions-bar {
+    direction: rtl;
+  }
+
+  .drawer__inner__mastodon > img {
+    transform: scaleX(-1);
+  }
+
+  .boost-modal__status-time {
+    float: left;
+  }
+
+  .compose-form .autosuggest-textarea__textarea {
+    padding-right: 10px;
+    padding-left: 10px + 22px;
+  }
+
+  .columns-area {
+    direction: rtl;
+  }
+
+  .react-swipeable-view-container > * {
+    direction: rtl;
+  }
+
+  .account__avatar-wrapper {
+    float: right;
+  }
+
+  .column-header__setting-arrows {
+    float: left;
+  }
+
+  .setting-meta__label {
+    float: left;
+  }
+
+  .activity-stream .status.light {
+    padding-left: 10px;
+    padding-right: 68px;
+  }
+
+  .status__info .status__display-name,
+  .activity-stream .status.light .status__display-name {
+    padding-left: 25px;
+    padding-right: 0;
+  }
+
+  .activity-stream .pre-header {
+    padding-right: 68px;
+    padding-left: 0;
+  }
+
+  .activity-stream .pre-header .pre-header__icon {
+    left: auto;
+    right: 42px;
+  }
+
+  .account__header__tabs__buttons > .icon-button {
+    margin-right: 0;
+    margin-left: 8px;
+  }
+
+  .status__relative-time,
+  .activity-stream .status.light .status__header .status__meta {
+    float: left;
+    text-align: left;
+  }
+
+  .status__action-bar-button {
+    float: right;
+  }
+
+  .status__action-bar-dropdown {
+    float: right;
+  }
+
+  .detailed-status__display-name .display-name {
+    text-align: right;
+  }
+
+  .detailed-status__display-avatar {
+    float: right;
+  }
+
+  .admin-wrapper {
+    direction: rtl;
+  }
+
+  .simple_form .label_input__append {
+    &::after {
+      background-image: linear-gradient(
+        to left,
+        rgba(darken($ui-base-color, 10%), 0),
+        darken($ui-base-color, 10%)
+      );
+    }
+  }
+
+  .simple_form select {
+    background: darken($ui-base-color, 10%)
+      url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(lighten($ui-base-color, 12%))}'/></svg>")
+      no-repeat left 8px center / auto 16px;
+  }
+
+  .fa-chevron-left::before {
+    content: '\F054';
+  }
+
+  .fa-chevron-right::before {
+    content: '\F053';
+  }
+
+  .dismissable-banner,
+  .warning-banner {
+    &__action {
+      float: left;
+    }
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/statuses.scss b/app/javascript/flavours/blobfox/styles/statuses.scss
new file mode 100644
index 00000000000000..0a46cf855fb74b
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/statuses.scss
@@ -0,0 +1,232 @@
+.activity-stream {
+  box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
+  border-radius: 4px;
+  overflow: hidden;
+  margin-bottom: 10px;
+
+  @media screen and (max-width: $no-gap-breakpoint) {
+    margin-bottom: 0;
+    border-radius: 0;
+    box-shadow: none;
+  }
+
+  &--headless {
+    border-radius: 0;
+    margin: 0;
+    box-shadow: none;
+
+    .detailed-status,
+    .status {
+      border-radius: 0 !important;
+    }
+  }
+
+  div[data-component] {
+    width: 100%;
+  }
+
+  .entry {
+    background: $ui-base-color;
+
+    .detailed-status,
+    .status,
+    .load-more {
+      animation: none;
+    }
+
+    &:last-child {
+      .detailed-status,
+      .status,
+      .load-more {
+        border-bottom: 0;
+        border-radius: 0 0 4px 4px;
+      }
+    }
+
+    &:first-child {
+      .detailed-status,
+      .status,
+      .load-more {
+        border-radius: 4px 4px 0 0;
+      }
+
+      &:last-child {
+        .detailed-status,
+        .status,
+        .load-more {
+          border-radius: 4px;
+        }
+      }
+    }
+
+    @media screen and (width <= 740px) {
+      .detailed-status,
+      .status,
+      .load-more {
+        border-radius: 0 !important;
+      }
+    }
+  }
+
+  &--highlighted .entry {
+    background: lighten($ui-base-color, 8%);
+  }
+}
+
+.button.logo-button svg {
+  width: 20px;
+  height: auto;
+  vertical-align: middle;
+  margin-inline-end: 5px;
+  fill: $primary-text-color;
+
+  @media screen and (max-width: $no-gap-breakpoint) {
+    display: none;
+  }
+}
+
+.embed {
+  .status__content[data-spoiler='folded'] {
+    .e-content {
+      display: none;
+    }
+
+    p:first-child {
+      margin-bottom: 0;
+    }
+  }
+
+  .detailed-status {
+    padding: 15px;
+
+    .detailed-status__display-avatar .account__avatar {
+      width: 48px;
+      height: 48px;
+    }
+  }
+
+  .status {
+    padding: 15px;
+    padding-inline-start: (48px + 15px * 2);
+    min-height: 48px + 2px;
+
+    &__avatar {
+      inset-inline-start: 15px;
+      top: 17px;
+
+      .account__avatar {
+        width: 48px;
+        height: 48px;
+      }
+    }
+
+    &__content {
+      padding-top: 5px;
+    }
+
+    &__prepend {
+      padding: 8px 0;
+      padding-bottom: 2px;
+      margin: initial;
+      margin-inline-start: 48px + 15px * 2;
+      padding-top: 15px;
+    }
+
+    &__prepend-icon-wrapper {
+      position: absolute;
+      margin: initial;
+      float: initial;
+      width: auto;
+      inset-inline-start: -32px;
+    }
+
+    .media-gallery,
+    &__action-bar,
+    .video-player {
+      margin-top: 10px;
+    }
+
+    &__action-bar-button {
+      font-size: 18px;
+      width: 23.1429px;
+      height: 23.1429px;
+      line-height: 23.15px;
+    }
+  }
+}
+
+// Styling from upstream's WebUI, as public pages use the same layout
+.embed {
+  .status {
+    .status__info {
+      font-size: 15px;
+      display: initial;
+    }
+
+    .status__relative-time {
+      color: $dark-text-color;
+      float: right;
+      font-size: 14px;
+      width: auto;
+      margin: initial;
+      padding: initial;
+      padding-bottom: 1px;
+    }
+
+    .status__visibility-icon {
+      padding: 0 4px;
+    }
+
+    .status__info .status__display-name {
+      display: block;
+      max-width: 100%;
+      padding: 6px 0;
+      padding-right: 25px;
+      margin: initial;
+    }
+
+    .status__avatar {
+      height: 48px;
+      position: absolute;
+      width: 48px;
+      margin: initial;
+    }
+  }
+}
+
+.rtl {
+  .embed {
+    .status {
+      padding-left: 10px;
+      padding-right: 68px;
+
+      .status__info .status__display-name {
+        padding-left: 25px;
+        padding-right: 0;
+      }
+
+      .status__relative-time,
+      .status__visibility-icon {
+        float: left;
+      }
+    }
+  }
+}
+
+.status__content__read-more-button,
+.status__content__translate-button {
+  display: block;
+  font-size: 15px;
+  line-height: 20px;
+  color: $highlight-text-color;
+  border: 0;
+  background: transparent;
+  padding: 0;
+  padding-top: 16px;
+  text-decoration: none;
+
+  &:hover,
+  &:active {
+    text-decoration: underline;
+  }
+}
diff --git a/app/javascript/flavours/blobfox/styles/tables.scss b/app/javascript/flavours/blobfox/styles/tables.scss
new file mode 100644
index 00000000000000..44ef00ba7378ed
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/tables.scss
@@ -0,0 +1,376 @@
+.table {
+  width: 100%;
+  max-width: 100%;
+  border-spacing: 0;
+  border-collapse: collapse;
+
+  th,
+  td {
+    padding: 8px;
+    line-height: 18px;
+    vertical-align: top;
+    border-top: 1px solid $ui-base-color;
+    text-align: start;
+    background: darken($ui-base-color, 4%);
+
+    &.critical {
+      font-weight: 700;
+      color: $gold-star;
+    }
+  }
+
+  & > thead > tr > th {
+    vertical-align: bottom;
+    border-bottom: 2px solid $ui-base-color;
+    border-top: 0;
+    font-weight: 500;
+  }
+
+  & > tbody > tr > th {
+    font-weight: 500;
+  }
+
+  & > tbody > tr:nth-child(odd) > td,
+  & > tbody > tr:nth-child(odd) > th {
+    background: $ui-base-color;
+  }
+
+  a {
+    color: $highlight-text-color;
+    text-decoration: underline;
+
+    &:hover {
+      text-decoration: none;
+    }
+  }
+
+  strong {
+    font-weight: 500;
+
+    @each $lang in $cjk-langs {
+      &:lang(#{$lang}) {
+        font-weight: 700;
+      }
+    }
+  }
+
+  &.inline-table {
+    & > tbody > tr:nth-child(odd) {
+      & > td,
+      & > th {
+        background: transparent;
+      }
+    }
+
+    & > tbody > tr:first-child {
+      & > td,
+      & > th {
+        border-top: 0;
+      }
+    }
+  }
+
+  &.horizontal-table {
+    border-collapse: collapse;
+    border-style: hidden;
+
+    & > tbody > tr > th,
+    & > tbody > tr > td {
+      padding: 11px 10px;
+      background: transparent;
+      border: 1px solid lighten($ui-base-color, 8%);
+      color: $secondary-text-color;
+    }
+
+    & > tbody > tr > th {
+      color: $darker-text-color;
+      font-weight: 600;
+    }
+  }
+
+  &.batch-table {
+    & > thead > tr > th {
+      background: $ui-base-color;
+      border-top: 1px solid darken($ui-base-color, 8%);
+      border-bottom: 1px solid darken($ui-base-color, 8%);
+
+      &:first-child {
+        border-radius: 4px 0 0;
+        border-inline-start: 1px solid darken($ui-base-color, 8%);
+      }
+
+      &:last-child {
+        border-radius: 0 4px 0 0;
+        border-inline-end: 1px solid darken($ui-base-color, 8%);
+      }
+    }
+  }
+
+  &--invites tbody td {
+    vertical-align: middle;
+  }
+}
+
+.table-wrapper {
+  overflow: auto;
+  margin-bottom: 20px;
+}
+
+samp {
+  font-family: $font-monospace, monospace;
+}
+
+button.table-action-link {
+  background: transparent;
+  border: 0;
+  font: inherit;
+}
+
+button.table-action-link,
+a.table-action-link {
+  text-decoration: none;
+  display: inline-block;
+  margin-inline-end: 5px;
+  padding: 0 10px;
+  color: $darker-text-color;
+  font-weight: 500;
+
+  &:hover {
+    color: $primary-text-color;
+  }
+
+  i.fa {
+    font-weight: 400;
+    margin-inline-end: 5px;
+  }
+
+  &:first-child {
+    padding-inline-start: 0;
+  }
+}
+
+.batch-table {
+  &__toolbar,
+  &__row {
+    display: flex;
+
+    &__select {
+      box-sizing: border-box;
+      padding: 8px 16px;
+      cursor: pointer;
+      min-height: 100%;
+
+      input {
+        margin-top: 8px;
+      }
+
+      &--aligned {
+        display: flex;
+        align-items: center;
+
+        input {
+          margin-top: 0;
+        }
+      }
+    }
+
+    &__actions,
+    &__content {
+      padding: 8px 0;
+      padding-inline-end: 16px;
+      flex: 1 1 auto;
+    }
+  }
+
+  &__toolbar {
+    position: sticky;
+    top: 0;
+    z-index: 1;
+    border: 1px solid darken($ui-base-color, 8%);
+    background: $ui-base-color;
+    border-radius: 4px 0 0;
+    height: 47px;
+    align-items: center;
+
+    &__actions {
+      text-align: end;
+      padding-inline-end: 16px - 5px;
+    }
+  }
+
+  &__select-all {
+    background: $ui-base-color;
+    height: 47px;
+    align-items: center;
+    justify-content: center;
+    border: 1px solid darken($ui-base-color, 8%);
+    border-top: 0;
+    color: $secondary-text-color;
+    display: none;
+
+    &.active {
+      display: flex;
+    }
+
+    .selected,
+    .not-selected {
+      display: none;
+
+      &.active {
+        display: block;
+      }
+    }
+
+    strong {
+      font-weight: 700;
+    }
+
+    span {
+      padding: 8px;
+      display: inline-block;
+    }
+
+    button {
+      background: transparent;
+      border: 0;
+      font: inherit;
+      color: $highlight-text-color;
+      border-radius: 4px;
+      font-weight: 700;
+      padding: 8px;
+
+      &:hover,
+      &:focus,
+      &:active {
+        background: lighten($ui-base-color, 8%);
+      }
+    }
+  }
+
+  &__form {
+    padding: 16px;
+    border: 1px solid darken($ui-base-color, 8%);
+    border-top: 0;
+    background: $ui-base-color;
+
+    .fields-row {
+      padding-top: 0;
+      margin-bottom: 0;
+    }
+  }
+
+  &__row {
+    border: 1px solid darken($ui-base-color, 8%);
+    border-top: 0;
+    background: darken($ui-base-color, 4%);
+
+    @media screen and (max-width: $no-gap-breakpoint) {
+      .optional &:first-child {
+        border-top: 1px solid darken($ui-base-color, 8%);
+      }
+    }
+
+    &:hover {
+      background: darken($ui-base-color, 2%);
+    }
+
+    &:nth-child(even) {
+      background: $ui-base-color;
+
+      &:hover {
+        background: lighten($ui-base-color, 2%);
+      }
+    }
+
+    &__content {
+      padding-top: 12px;
+      padding-bottom: 16px;
+      overflow: hidden;
+
+      &--unpadded {
+        padding: 0;
+      }
+
+      &--with-image {
+        display: flex;
+        align-items: center;
+      }
+
+      &__image {
+        flex: 0 0 auto;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        margin-inline-end: 10px;
+
+        .emojione {
+          width: 32px;
+          height: 32px;
+        }
+      }
+
+      &__text {
+        flex: 1 1 auto;
+      }
+
+      &__quote {
+        padding: 12px;
+        padding-top: 0;
+      }
+
+      &__extra {
+        flex: 0 0 auto;
+        text-align: end;
+        color: $darker-text-color;
+        font-weight: 500;
+      }
+    }
+
+    .directory__tag {
+      margin: 0;
+      width: 100%;
+
+      a {
+        background: transparent;
+        border-radius: 0;
+      }
+    }
+  }
+
+  &.optional .batch-table__toolbar,
+  &.optional .batch-table__row__select {
+    @media screen and (max-width: $no-gap-breakpoint) {
+      display: none;
+    }
+  }
+
+  .status__content {
+    padding-top: 0;
+
+    strong {
+      font-weight: 700;
+    }
+  }
+
+  .nothing-here {
+    border: 1px solid darken($ui-base-color, 8%);
+    border-top: 0;
+    box-shadow: none;
+
+    @media screen and (max-width: $no-gap-breakpoint) {
+      border-top: 1px solid darken($ui-base-color, 8%);
+    }
+  }
+
+  @media screen and (width <= 870px) {
+    .accounts-table tbody td.optional {
+      display: none;
+    }
+  }
+}
+
+.one-liner {
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
diff --git a/app/javascript/flavours/blobfox/styles/variables.scss b/app/javascript/flavours/blobfox/styles/variables.scss
new file mode 100644
index 00000000000000..0b5d6f4067b4d6
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/variables.scss
@@ -0,0 +1,103 @@
+// Commonly used web colors
+$black: #000000; // Black
+$white: #ffffff; // White
+$red-600: #b7253d !default; // Deep Carmine
+$red-500: #df405a !default; // Cerise
+$blurple-600: #563acc; // Iris
+$blurple-500: #6364ff; // Brand purple
+$blurple-300: #858afa; // Faded Blue
+$grey-600: #4e4c5a; // Trout
+$grey-100: #dadaf3; // Topaz
+
+$success-green: #79bd9a !default; // Padua
+$error-red: $red-500 !default; // Cerise
+$warning-red: #ff5050 !default; // Sunset Orange
+$gold-star: #ca8f04 !default; // Dark Goldenrod
+
+$red-bookmark: $warning-red;
+
+// Values from the classic Mastodon UI
+$classic-base-color: #282c37; // Midnight Express
+$classic-primary-color: #9baec8; // Echo Blue
+$classic-secondary-color: #d9e1e8; // Pattens Blue
+$classic-highlight-color: #6364ff; // Brand purple
+
+// Variables for defaults in UI
+$base-shadow-color: $black !default;
+$base-overlay-background: $black !default;
+$base-border-color: $white !default;
+$simple-background-color: $white !default;
+$valid-value-color: $success-green !default;
+$error-value-color: $error-red !default;
+
+// Tell UI to use selected colors
+$ui-base-color: $classic-base-color !default; // Darkest
+$ui-base-lighter-color: lighten(
+  $ui-base-color,
+  26%
+) !default; // Lighter darkest
+$ui-primary-color: $classic-primary-color !default; // Lighter
+$ui-secondary-color: $classic-secondary-color !default; // Lightest
+$ui-highlight-color: $classic-highlight-color !default;
+$ui-button-color: $white !default;
+$ui-button-background-color: $blurple-500 !default;
+$ui-button-focus-background-color: $blurple-600 !default;
+
+$ui-button-secondary-color: $grey-100 !default;
+$ui-button-secondary-border-color: $grey-100 !default;
+$ui-button-secondary-focus-background-color: $grey-600 !default;
+$ui-button-secondary-focus-color: $white !default;
+
+$ui-button-tertiary-color: $blurple-300 !default;
+$ui-button-tertiary-border-color: $blurple-300 !default;
+$ui-button-tertiary-focus-background-color: $blurple-600 !default;
+$ui-button-tertiary-focus-color: $white !default;
+
+$ui-button-destructive-background-color: $red-500 !default;
+$ui-button-destructive-focus-background-color: $red-600 !default;
+
+// Variables for texts
+$primary-text-color: $white !default;
+$darker-text-color: $ui-primary-color !default;
+$dark-text-color: $ui-base-lighter-color !default;
+$secondary-text-color: $ui-secondary-color !default;
+$highlight-text-color: lighten($ui-highlight-color, 8%) !default;
+$action-button-color: $ui-base-lighter-color !default;
+$action-button-focus-color: lighten($ui-base-lighter-color, 4%) !default;
+$passive-text-color: $gold-star !default;
+$active-passive-text-color: $success-green !default;
+
+// For texts on inverted backgrounds
+$inverted-text-color: $ui-base-color !default;
+$lighter-text-color: $ui-base-lighter-color !default;
+$light-text-color: $ui-primary-color !default;
+
+// Language codes that uses CJK fonts
+$cjk-langs: ja, ko, zh-CN, zh-HK, zh-TW;
+
+// Variables for components
+$media-modal-media-max-width: 100%;
+
+// put margins on top and bottom of image to avoid the screen covered by image.
+$media-modal-media-max-height: 80%;
+
+$no-gap-breakpoint: 1175px;
+
+$font-sans-serif: 'mastodon-font-sans-serif' !default;
+$font-display: 'mastodon-font-display' !default;
+$font-monospace: 'mastodon-font-monospace' !default;
+
+// Avatar border size (8% default, 100% for rounded avatars)
+$ui-avatar-border-size: 8%;
+
+// More variables
+$dismiss-overlay-width: 4rem;
+
+:root {
+  --dropdown-border-color: #{lighten($ui-base-color, 12%)};
+  --dropdown-background-color: #{lighten($ui-base-color, 4%)};
+  --dropdown-shadow: 0 20px 25px -5px #{rgba($base-shadow-color, 0.25)},
+    0 8px 10px -6px #{rgba($base-shadow-color, 0.25)};
+  --modal-background-color: #{darken($ui-base-color, 4%)};
+  --modal-border-color: #{lighten($ui-base-color, 4%)};
+}
diff --git a/app/javascript/flavours/blobfox/styles/widgets.scss b/app/javascript/flavours/blobfox/styles/widgets.scss
new file mode 100644
index 00000000000000..f54d2f2e587620
--- /dev/null
+++ b/app/javascript/flavours/blobfox/styles/widgets.scss
@@ -0,0 +1,402 @@
+@use 'sass:math';
+
+.hero-widget {
+  margin-bottom: 10px;
+  box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
+
+  &__img {
+    width: 100%;
+    position: relative;
+    overflow: hidden;
+    border-radius: 4px 4px 0 0;
+    background: $base-shadow-color;
+
+    img {
+      object-fit: cover;
+      display: block;
+      width: 100%;
+      height: 100%;
+      margin: 0;
+      border-radius: 4px 4px 0 0;
+    }
+  }
+
+  &__text {
+    background: $ui-base-color;
+    padding: 20px;
+    border-radius: 0 0 4px 4px;
+    font-size: 15px;
+    color: $darker-text-color;
+    line-height: 20px;
+    word-wrap: break-word;
+    font-weight: 400;
+
+    .emojione {
+      width: 20px;
+      height: 20px;
+      margin: -3px 0 0;
+    }
+
+    p {
+      margin-bottom: 20px;
+
+      &:last-child {
+        margin-bottom: 0;
+      }
+    }
+
+    em {
+      display: inline;
+      margin: 0;
+      padding: 0;
+      font-weight: 700;
+      background: transparent;
+      font-family: inherit;
+      font-size: inherit;
+      line-height: inherit;
+      color: lighten($darker-text-color, 10%);
+    }
+
+    a {
+      color: $secondary-text-color;
+      text-decoration: none;
+
+      &:hover {
+        text-decoration: underline;
+      }
+    }
+  }
+
+  @media screen and (max-width: $no-gap-breakpoint) {
+    display: none;
+  }
+}
+
+.endorsements-widget {
+  margin-bottom: 10px;
+  padding-bottom: 10px;
+
+  h4 {
+    padding: 10px;
+    text-transform: uppercase;
+    font-weight: 700;
+    font-size: 13px;
+    color: $darker-text-color;
+  }
+
+  .account {
+    padding: 10px 0;
+
+    &:last-child {
+      border-bottom: 0;
+    }
+
+    .account__display-name {
+      display: flex;
+      align-items: center;
+    }
+  }
+
+  .trends__item {
+    padding: 10px;
+  }
+}
+
+.trends-widget {
+  h4 {
+    color: $darker-text-color;
+  }
+}
+
+.placeholder-widget {
+  padding: 16px;
+  border-radius: 4px;
+  border: 2px dashed $dark-text-color;
+  text-align: center;
+  color: $darker-text-color;
+  margin-bottom: 10px;
+}
+
+.moved-account-widget {
+  padding: 15px;
+  padding-bottom: 20px;
+  border-radius: 4px;
+  background: $ui-base-color;
+  box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
+  color: $secondary-text-color;
+  font-weight: 400;
+  margin-bottom: 10px;
+
+  strong,
+  a {
+    font-weight: 500;
+
+    @each $lang in $cjk-langs {
+      &:lang(#{$lang}) {
+        font-weight: 700;
+      }
+    }
+  }
+
+  a {
+    color: inherit;
+    text-decoration: underline;
+
+    &.mention {
+      text-decoration: none;
+
+      span {
+        text-decoration: none;
+      }
+
+      &:focus,
+      &:hover,
+      &:active {
+        text-decoration: none;
+
+        span {
+          text-decoration: underline;
+        }
+      }
+    }
+  }
+
+  &__message {
+    margin-bottom: 15px;
+
+    .fa {
+      margin-inline-end: 5px;
+      color: $darker-text-color;
+    }
+  }
+
+  &__card {
+    .detailed-status__display-avatar {
+      position: relative;
+      cursor: pointer;
+    }
+
+    .detailed-status__display-name {
+      margin-bottom: 0;
+      text-decoration: none;
+
+      span {
+        font-weight: 400;
+      }
+    }
+  }
+}
+
+.memoriam-widget {
+  padding: 20px;
+  border-radius: 4px;
+  background: $base-shadow-color;
+  box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
+  font-size: 14px;
+  color: $darker-text-color;
+  margin-bottom: 10px;
+}
+
+.directory {
+  background: $ui-base-color;
+  border-radius: 4px;
+  box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
+
+  &__tag {
+    box-sizing: border-box;
+    margin-bottom: 10px;
+
+    & > a,
+    & > div {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      background: $ui-base-color;
+      border-radius: 4px;
+      padding: 15px;
+      text-decoration: none;
+      color: inherit;
+      box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
+    }
+
+    & > a {
+      &:hover,
+      &:active,
+      &:focus {
+        background: lighten($ui-base-color, 8%);
+      }
+    }
+
+    &.active > a {
+      background: $ui-highlight-color;
+      cursor: default;
+    }
+
+    &.disabled > div {
+      opacity: 0.5;
+      cursor: default;
+    }
+
+    h4 {
+      flex: 1 1 auto;
+      font-size: 18px;
+      font-weight: 700;
+      color: $primary-text-color;
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+
+      .fa {
+        color: $darker-text-color;
+      }
+
+      small {
+        display: block;
+        font-weight: 400;
+        font-size: 15px;
+        margin-top: 8px;
+        color: $darker-text-color;
+      }
+    }
+
+    &.active h4 {
+      &,
+      .fa,
+      small {
+        color: $primary-text-color;
+      }
+    }
+
+    .avatar-stack {
+      flex: 0 0 auto;
+      width: (36px + 4px) * 3;
+    }
+
+    &.active .avatar-stack .account__avatar {
+      border-color: $ui-highlight-color;
+    }
+  }
+}
+
+.accounts-table {
+  width: 100%;
+
+  .account {
+    padding: 0;
+    border: 0;
+  }
+
+  strong {
+    font-weight: 700;
+  }
+
+  thead th {
+    text-align: center;
+    text-transform: uppercase;
+    color: $darker-text-color;
+    font-weight: 700;
+    padding: 10px;
+
+    &:first-child {
+      text-align: start;
+    }
+  }
+
+  tbody td {
+    padding: 15px 0;
+    vertical-align: middle;
+    border-bottom: 1px solid lighten($ui-base-color, 8%);
+  }
+
+  tbody tr:last-child td {
+    border-bottom: 0;
+  }
+
+  &__count {
+    width: 120px;
+    text-align: center;
+    font-size: 15px;
+    font-weight: 500;
+    color: $primary-text-color;
+
+    small {
+      display: block;
+      color: $darker-text-color;
+      font-weight: 400;
+      font-size: 14px;
+    }
+  }
+
+  tbody td.accounts-table__extra {
+    width: 120px;
+    text-align: end;
+    color: $darker-text-color;
+    padding-inline-end: 16px;
+
+    a {
+      text-decoration: none;
+      color: inherit;
+
+      &:focus,
+      &:hover,
+      &:active {
+        text-decoration: underline;
+      }
+    }
+  }
+
+  &__comment {
+    width: 50%;
+    vertical-align: initial !important;
+  }
+
+  &__interrelationships {
+    width: 21px;
+  }
+
+  .fa {
+    font-size: 16px;
+
+    &.active {
+      color: $highlight-text-color;
+    }
+
+    &.passive {
+      color: $passive-text-color;
+    }
+
+    &.active.passive {
+      color: $active-passive-text-color;
+    }
+  }
+
+  @media screen and (max-width: $no-gap-breakpoint) {
+    tbody td.optional {
+      display: none;
+    }
+  }
+}
+
+.moved-account-widget,
+.memoriam-widget,
+.directory {
+  @media screen and (max-width: $no-gap-breakpoint) {
+    margin-bottom: 0;
+    box-shadow: none;
+    border-radius: 0;
+  }
+}
+
+.placeholder-widget {
+  a {
+    text-decoration: none;
+    font-weight: 500;
+    color: $ui-highlight-color;
+
+    &:hover,
+    &:focus,
+    &:active {
+      text-decoration: underline;
+    }
+  }
+}
diff --git a/app/javascript/flavours/blobfox/theme.yml b/app/javascript/flavours/blobfox/theme.yml
new file mode 100644
index 00000000000000..fc7a21dcc63e03
--- /dev/null
+++ b/app/javascript/flavours/blobfox/theme.yml
@@ -0,0 +1,48 @@
+#  (REQUIRED) The location of the pack files.
+pack:
+  admin:
+    - packs/admin.jsx
+    - packs/public.jsx
+  auth: packs/public.jsx
+  common:
+    filename: packs/common.js
+    stylesheet: true
+  embed: packs/public.jsx
+  error: packs/error.js
+  home:
+    filename: packs/home.js
+    preload:
+      - flavours/blobfox/async/compose
+      - flavours/blobfox/async/home_timeline
+      - flavours/blobfox/async/notifications
+  mailer:
+  modal:
+  public: packs/public.jsx
+  settings: packs/settings.js
+  sign_up: packs/sign_up.js
+  share: packs/share.jsx
+
+#  (OPTIONAL) The directory which contains localization files for
+#  the flavour, relative to this directory. The contents of this
+#  directory must be `.json` files whose names correspond to
+#  language tags and whose default exports are a messages object.
+locales: locales
+
+#  (OPTIONAL) Which flavour to inherit locales from
+inherit_locales: vanilla
+
+#  (OPTIONAL) A file to use as the preview screenshot for the flavour,
+#  or an array thereof. These are the full path from `app/javascript/`.
+screenshot: flavours/blobfox/images/blobfox-preview.jpg
+
+#  (OPTIONAL) The directory which contains the pack files.
+#  Defaults to the theme directory (`app/javascript/themes/[theme]`),
+#  which should be sufficient for like 99% of use-cases lol.
+
+#      pack_directory: app/javascript/packs
+
+#  (OPTIONAL) By default the theme will fallback to the default theme
+#  if a particular pack is not provided. You can specify different
+#  fallbacks here, or disable fallback behaviours altogether by
+#  specifying a `null` value.
+fallback:
diff --git a/app/javascript/flavours/blobfox/types/util.ts b/app/javascript/flavours/blobfox/types/util.ts
new file mode 100644
index 00000000000000..5f2cf2cf07f3f9
--- /dev/null
+++ b/app/javascript/flavours/blobfox/types/util.ts
@@ -0,0 +1 @@
+export type ValueOf<T> = T[keyof T];
diff --git a/app/javascript/flavours/blobfox/utils/backend_links.js b/app/javascript/flavours/blobfox/utils/backend_links.js
new file mode 100644
index 00000000000000..2028a1e60852b7
--- /dev/null
+++ b/app/javascript/flavours/blobfox/utils/backend_links.js
@@ -0,0 +1,18 @@
+export const preferencesLink = '/settings/preferences';
+export const profileLink = '/settings/profile';
+export const signOutLink = '/auth/sign_out';
+export const privacyPolicyLink = '/privacy-policy';
+export const accountAdminLink = (id) => `/admin/accounts/${id}`;
+export const statusAdminLink = (account_id, status_id) => `/admin/accounts/${account_id}/statuses/${status_id}`;
+export const filterEditLink = (id) => `/filters/${id}/edit`;
+export const relationshipsLink = '/relationships';
+export const securityLink = '/auth/edit';
+export const preferenceLink = (setting_name) => {
+  switch (setting_name) {
+  case 'user_setting_expand_spoilers':
+  case 'user_setting_disable_swiping':
+    return `/settings/preferences/appearance#${setting_name}`;
+  default:
+    return preferencesLink;
+  }
+};
diff --git a/app/javascript/flavours/blobfox/utils/base64.ts b/app/javascript/flavours/blobfox/utils/base64.ts
new file mode 100644
index 00000000000000..5a595ee12b5098
--- /dev/null
+++ b/app/javascript/flavours/blobfox/utils/base64.ts
@@ -0,0 +1,10 @@
+export const decode = (base64: string): Uint8Array => {
+  const rawData = window.atob(base64);
+  const outputArray = new Uint8Array(rawData.length);
+
+  for (let i = 0; i < rawData.length; ++i) {
+    outputArray[i] = rawData.charCodeAt(i);
+  }
+
+  return outputArray;
+};
diff --git a/app/javascript/flavours/blobfox/utils/config.js b/app/javascript/flavours/blobfox/utils/config.js
new file mode 100644
index 00000000000000..932cd0cbf543e1
--- /dev/null
+++ b/app/javascript/flavours/blobfox/utils/config.js
@@ -0,0 +1,10 @@
+import ready from '../ready';
+
+export let assetHost = '';
+
+ready(() => {
+  const cdnHost = document.querySelector('meta[name=cdn-host]');
+  if (cdnHost) {
+    assetHost = cdnHost.content || '';
+  }
+});
diff --git a/app/javascript/flavours/blobfox/utils/content_warning.js b/app/javascript/flavours/blobfox/utils/content_warning.js
new file mode 100644
index 00000000000000..18c21e795f819c
--- /dev/null
+++ b/app/javascript/flavours/blobfox/utils/content_warning.js
@@ -0,0 +1,31 @@
+import { expandSpoilers } from 'flavours/blobfox/initial_state';
+
+function _autoUnfoldCW(spoiler_text, skip_unfold_regex) {
+  if (!expandSpoilers)
+    return false;
+
+  if (!skip_unfold_regex)
+    return true;
+
+  let regex = null;
+
+  try {
+    regex = new RegExp(skip_unfold_regex.trim(), 'i');
+  } catch (e) {
+    // Bad regex, skip filters
+    return true;
+  }
+
+  return !regex.test(spoiler_text);
+}
+
+export function autoHideCW(settings, spoiler_text) {
+  return !_autoUnfoldCW(spoiler_text, settings.getIn(['content_warnings', 'filter']));
+}
+
+export function autoUnfoldCW(settings, status) {
+  if (!status)
+    return false;
+
+  return _autoUnfoldCW(status.get('spoiler_text'), settings.getIn(['content_warnings', 'filter']));
+}
diff --git a/app/javascript/flavours/blobfox/utils/filters.ts b/app/javascript/flavours/blobfox/utils/filters.ts
new file mode 100644
index 00000000000000..d299e80c40a91e
--- /dev/null
+++ b/app/javascript/flavours/blobfox/utils/filters.ts
@@ -0,0 +1,16 @@
+export const toServerSideType = (columnType: string) => {
+  switch (columnType) {
+    case 'home':
+    case 'notifications':
+    case 'public':
+    case 'thread':
+    case 'account':
+      return columnType;
+    default:
+      if (columnType.includes('list:')) {
+        return 'home';
+      } else {
+        return 'public'; // community, account, hashtag
+      }
+  }
+};
diff --git a/app/javascript/flavours/blobfox/utils/hashtag.js b/app/javascript/flavours/blobfox/utils/hashtag.js
new file mode 100644
index 00000000000000..6c529dce8e5d72
--- /dev/null
+++ b/app/javascript/flavours/blobfox/utils/hashtag.js
@@ -0,0 +1,8 @@
+export function recoverHashtags (recognizedTags, text) {
+  return recognizedTags.map(tag => {
+    const re = new RegExp(`(?:^|[^/)\\w])#(${tag.name})`, 'i');
+    const matched_hashtag = text.match(re);
+    return matched_hashtag ? matched_hashtag[1] : null;
+  },
+  ).filter(x => x !== null);
+}
diff --git a/app/javascript/flavours/blobfox/utils/hashtags.ts b/app/javascript/flavours/blobfox/utils/hashtags.ts
new file mode 100644
index 00000000000000..0c5505c6c9a088
--- /dev/null
+++ b/app/javascript/flavours/blobfox/utils/hashtags.ts
@@ -0,0 +1,29 @@
+const HASHTAG_SEPARATORS = '_\\u00b7\\u200c';
+const ALPHA = '\\p{L}\\p{M}';
+const WORD = '\\p{L}\\p{M}\\p{N}\\p{Pc}';
+
+const buildHashtagPatternRegex = () => {
+  try {
+    return new RegExp(
+      `(?:^|[^\\/\\)\\w])#(([${WORD}_][${WORD}${HASHTAG_SEPARATORS}]*[${ALPHA}${HASHTAG_SEPARATORS}][${WORD}${HASHTAG_SEPARATORS}]*[${WORD}_])|([${WORD}_]*[${ALPHA}][${WORD}_]*))`,
+      'iu',
+    );
+  } catch {
+    return /(?:^|[^/)\w])#(\w*[a-zA-Z·]\w*)/i;
+  }
+};
+
+const buildHashtagRegex = () => {
+  try {
+    return new RegExp(
+      `^(([${WORD}_][${WORD}${HASHTAG_SEPARATORS}]*[${ALPHA}${HASHTAG_SEPARATORS}][${WORD}${HASHTAG_SEPARATORS}]*[${WORD}_])|([${WORD}_]*[${ALPHA}][${WORD}_]*))$`,
+      'iu',
+    );
+  } catch {
+    return /^(\w*[a-zA-Z·]\w*)$/i;
+  }
+};
+
+export const HASHTAG_PATTERN_REGEX = buildHashtagPatternRegex();
+
+export const HASHTAG_REGEX = buildHashtagRegex();
diff --git a/app/javascript/flavours/blobfox/utils/html.js b/app/javascript/flavours/blobfox/utils/html.js
new file mode 100644
index 00000000000000..247e98c88a7f31
--- /dev/null
+++ b/app/javascript/flavours/blobfox/utils/html.js
@@ -0,0 +1,6 @@
+// NB: This function can still return unsafe HTML
+export const unescapeHTML = (html) => {
+  const wrapper = document.createElement('div');
+  wrapper.innerHTML = html.replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n').replace(/<[^>]*>/g, '');
+  return wrapper.textContent;
+};
diff --git a/app/javascript/flavours/blobfox/utils/icons.jsx b/app/javascript/flavours/blobfox/utils/icons.jsx
new file mode 100644
index 00000000000000..be566032e06445
--- /dev/null
+++ b/app/javascript/flavours/blobfox/utils/icons.jsx
@@ -0,0 +1,13 @@
+// Copied from emoji-mart for consistency with emoji picker and since
+// they don't export the icons in the package
+export const loupeIcon = (
+  <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' width='13' height='13'>
+    <path d='M12.9 14.32a8 8 0 1 1 1.41-1.41l5.35 5.33-1.42 1.42-5.33-5.34zM8 14A6 6 0 1 0 8 2a6 6 0 0 0 0 12z' />
+  </svg>
+);
+
+export const deleteIcon = (
+  <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' width='13' height='13'>
+    <path d='M10 8.586L2.929 1.515 1.515 2.929 8.586 10l-7.071 7.071 1.414 1.414L10 11.414l7.071 7.071 1.414-1.414L11.414 10l7.071-7.071-1.414-1.414L10 8.586z' />
+  </svg>
+);
diff --git a/app/javascript/flavours/blobfox/utils/idna.js b/app/javascript/flavours/blobfox/utils/idna.js
new file mode 100644
index 00000000000000..efab5bacf77a0c
--- /dev/null
+++ b/app/javascript/flavours/blobfox/utils/idna.js
@@ -0,0 +1,10 @@
+import punycode from 'punycode';
+
+const IDNA_PREFIX = 'xn--';
+
+export const decode = domain => {
+  return domain
+    .split('.')
+    .map(part => part.indexOf(IDNA_PREFIX) === 0 ? punycode.decode(part.slice(IDNA_PREFIX.length)) : part)
+    .join('.');
+};
diff --git a/app/javascript/flavours/blobfox/utils/js_helpers.js b/app/javascript/flavours/blobfox/utils/js_helpers.js
new file mode 100644
index 00000000000000..2ebd5b6c55ea68
--- /dev/null
+++ b/app/javascript/flavours/blobfox/utils/js_helpers.js
@@ -0,0 +1,5 @@
+//  This function returns the new value unless it is `null` or
+//  `undefined`, in which case it returns the old one.
+export function overwrite (oldVal, newVal) {
+  return newVal === null || typeof newVal === 'undefined' ? oldVal : newVal;
+}
diff --git a/app/javascript/flavours/blobfox/utils/log_out.js b/app/javascript/flavours/blobfox/utils/log_out.js
new file mode 100644
index 00000000000000..28123890f1480a
--- /dev/null
+++ b/app/javascript/flavours/blobfox/utils/log_out.js
@@ -0,0 +1,35 @@
+import Rails from '@rails/ujs';
+
+import { signOutLink } from 'flavours/blobfox/utils/backend_links';
+
+export const logOut = () => {
+  const form = document.createElement('form');
+
+  const methodInput = document.createElement('input');
+  methodInput.setAttribute('name', '_method');
+  methodInput.setAttribute('value', 'delete');
+  methodInput.setAttribute('type', 'hidden');
+  form.appendChild(methodInput);
+
+  const csrfToken = Rails.csrfToken();
+  const csrfParam = Rails.csrfParam();
+
+  if (csrfParam && csrfToken) {
+    const csrfInput = document.createElement('input');
+    csrfInput.setAttribute('name', csrfParam);
+    csrfInput.setAttribute('value', csrfToken);
+    csrfInput.setAttribute('type', 'hidden');
+    form.appendChild(csrfInput);
+  }
+
+  const submitButton = document.createElement('input');
+  submitButton.setAttribute('type', 'submit');
+  form.appendChild(submitButton);
+
+  form.method = 'post';
+  form.action = signOutLink;
+  form.style.display = 'none';
+
+  document.body.appendChild(form);
+  submitButton.click();
+};
diff --git a/app/javascript/flavours/blobfox/utils/notifications.js b/app/javascript/flavours/blobfox/utils/notifications.js
new file mode 100644
index 00000000000000..42623ac7c6898c
--- /dev/null
+++ b/app/javascript/flavours/blobfox/utils/notifications.js
@@ -0,0 +1,30 @@
+// Handles browser quirks, based on
+// https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API/Using_the_Notifications_API
+
+const checkNotificationPromise = () => {
+  try {
+    // eslint-disable-next-line promise/valid-params, promise/catch-or-return
+    Notification.requestPermission().then();
+  } catch(e) {
+    return false;
+  }
+
+  return true;
+};
+
+const handlePermission = (permission, callback) => {
+  // Whatever the user answers, we make sure Chrome stores the information
+  if(!('permission' in Notification)) {
+    Notification.permission = permission;
+  }
+
+  callback(Notification.permission);
+};
+
+export const requestNotificationPermission = (callback) => {
+  if (checkNotificationPromise()) {
+    Notification.requestPermission().then((permission) => handlePermission(permission, callback)).catch(console.warn);
+  } else {
+    Notification.requestPermission((permission) => handlePermission(permission, callback));
+  }
+};
diff --git a/app/javascript/flavours/blobfox/utils/numbers.ts b/app/javascript/flavours/blobfox/utils/numbers.ts
new file mode 100644
index 00000000000000..35bcde83e2491a
--- /dev/null
+++ b/app/javascript/flavours/blobfox/utils/numbers.ts
@@ -0,0 +1,71 @@
+import type { ValueOf } from '../types/util';
+
+export const DECIMAL_UNITS = Object.freeze({
+  ONE: 1,
+  TEN: 10,
+  HUNDRED: 100,
+  THOUSAND: 1_000,
+  MILLION: 1_000_000,
+  BILLION: 1_000_000_000,
+  TRILLION: 1_000_000_000_000,
+});
+export type DecimalUnits = ValueOf<typeof DECIMAL_UNITS>;
+
+const TEN_THOUSAND = DECIMAL_UNITS.THOUSAND * 10;
+const TEN_MILLIONS = DECIMAL_UNITS.MILLION * 10;
+
+export type ShortNumber = [number, DecimalUnits, 0 | 1]; // Array of: shorten number, unit of shorten number and maximum fraction digits
+
+/**
+ * @param sourceNumber Number to convert to short number
+ * @returns Calculated short number
+ * @example
+ * shortNumber(5936);
+ * // => [5.936, 1000, 1]
+ */
+export function toShortNumber(sourceNumber: number): ShortNumber {
+  if (sourceNumber < DECIMAL_UNITS.THOUSAND) {
+    return [sourceNumber, DECIMAL_UNITS.ONE, 0];
+  } else if (sourceNumber < DECIMAL_UNITS.MILLION) {
+    return [
+      sourceNumber / DECIMAL_UNITS.THOUSAND,
+      DECIMAL_UNITS.THOUSAND,
+      sourceNumber < TEN_THOUSAND ? 1 : 0,
+    ];
+  } else if (sourceNumber < DECIMAL_UNITS.BILLION) {
+    return [
+      sourceNumber / DECIMAL_UNITS.MILLION,
+      DECIMAL_UNITS.MILLION,
+      sourceNumber < TEN_MILLIONS ? 1 : 0,
+    ];
+  } else if (sourceNumber < DECIMAL_UNITS.TRILLION) {
+    return [sourceNumber / DECIMAL_UNITS.BILLION, DECIMAL_UNITS.BILLION, 0];
+  }
+
+  return [sourceNumber, DECIMAL_UNITS.ONE, 0];
+}
+
+/**
+ * @param sourceNumber Original number that is shortened
+ * @param division The scale in which short number is displayed
+ * @returns Number that can be used for plurals when short form used
+ * @example
+ * pluralReady(1793, DECIMAL_UNITS.THOUSAND)
+ * // => 1790
+ */
+export function pluralReady(
+  sourceNumber: number,
+  division: DecimalUnits | null,
+): number {
+  if (division == null || division < DECIMAL_UNITS.HUNDRED) {
+    return sourceNumber;
+  }
+
+  const closestScale = division / DECIMAL_UNITS.TEN;
+
+  return Math.trunc(sourceNumber / closestScale) * closestScale;
+}
+
+export function roundTo10(num: number): number {
+  return Math.round(num * 0.1) / 0.1;
+}
diff --git a/app/javascript/flavours/blobfox/utils/privacy_preference.js b/app/javascript/flavours/blobfox/utils/privacy_preference.js
new file mode 100644
index 00000000000000..51bdf072d78e84
--- /dev/null
+++ b/app/javascript/flavours/blobfox/utils/privacy_preference.js
@@ -0,0 +1,5 @@
+export const order = ['public', 'unlisted', 'private', 'direct'];
+
+export function privacyPreference (a, b) {
+  return order[Math.max(order.indexOf(a), order.indexOf(b), 0)];
+}
diff --git a/app/javascript/flavours/blobfox/utils/react_helpers.js b/app/javascript/flavours/blobfox/utils/react_helpers.js
new file mode 100644
index 00000000000000..ea11acdb6188e2
--- /dev/null
+++ b/app/javascript/flavours/blobfox/utils/react_helpers.js
@@ -0,0 +1,21 @@
+//  This function binds the given `handlers` to the `target`.
+export function assignHandlers (target, handlers) {
+  if (!target || !handlers) {
+    return;
+  }
+
+  //  We just bind each handler to the `target`.
+  const handle = target.handlers = {};
+  Object.keys(handlers).forEach(
+    key => handle[key] = handlers[key].bind(target),
+  );
+}
+
+//  This function only returns the component if the result of calling
+//  `test` with `data` is `true`.  Useful with funciton binding.
+export function conditionalRender (test, data, component) {
+  return test(data) ? component : null;
+}
+
+//  This object provides props to make the component not visible.
+export const hiddenComponent = { style: { display: 'none' } };
diff --git a/app/javascript/flavours/blobfox/utils/react_router.jsx b/app/javascript/flavours/blobfox/utils/react_router.jsx
new file mode 100644
index 00000000000000..fa8f0db2b5cbd6
--- /dev/null
+++ b/app/javascript/flavours/blobfox/utils/react_router.jsx
@@ -0,0 +1,61 @@
+import PropTypes from "prop-types";
+
+import { __RouterContext } from "react-router";
+
+import hoistStatics from "hoist-non-react-statics";
+
+export const WithRouterPropTypes = {
+  match: PropTypes.object.isRequired,
+  location: PropTypes.object.isRequired,
+  history: PropTypes.object.isRequired,
+};
+
+export const WithOptionalRouterPropTypes = {
+  match: PropTypes.object,
+  location: PropTypes.object,
+  history: PropTypes.object,
+};
+
+// This is copied from https://github.com/remix-run/react-router/blob/v5.3.4/packages/react-router/modules/withRouter.js
+// but does not fail if called outside of a React Router context
+export function withOptionalRouter(Component) {
+  const displayName = `withRouter(${Component.displayName || Component.name})`;
+  const C = props => {
+    const { wrappedComponentRef, ...remainingProps } = props;
+
+    return (
+      <__RouterContext.Consumer>
+        {context => {
+          if(context)
+            return (
+              <Component
+                {...remainingProps}
+                {...context}
+                ref={wrappedComponentRef}
+              />
+            );
+          else
+            return (
+              <Component
+                {...remainingProps}
+                ref={wrappedComponentRef}
+              />
+            );
+        }}
+      </__RouterContext.Consumer>
+    );
+  };
+
+  C.displayName = displayName;
+  C.WrappedComponent = Component;
+  C.propTypes = {
+    ...Component.propTypes,
+    wrappedComponentRef: PropTypes.oneOfType([
+      PropTypes.string,
+      PropTypes.func,
+      PropTypes.object
+    ])
+  };
+
+  return hoistStatics(C, Component);
+}
diff --git a/app/javascript/flavours/blobfox/utils/resize_image.js b/app/javascript/flavours/blobfox/utils/resize_image.js
new file mode 100644
index 00000000000000..e3d4e6a354b9e9
--- /dev/null
+++ b/app/javascript/flavours/blobfox/utils/resize_image.js
@@ -0,0 +1,191 @@
+import EXIF from 'exif-js';
+
+const MAX_IMAGE_PIXELS = 2073600; // 1920x1080px
+
+const _browser_quirks = {};
+
+// Some browsers will automatically draw images respecting their EXIF orientation
+// while others won't, and the safest way to detect that is to examine how it
+// is done on a known image.
+// See https://github.com/w3c/csswg-drafts/issues/4666
+// and https://github.com/blueimp/JavaScript-Load-Image/commit/1e4df707821a0afcc11ea0720ee403b8759f3881
+const dropOrientationIfNeeded = (orientation) => new Promise(resolve => {
+  switch (_browser_quirks['image-orientation-automatic']) {
+  case true:
+    resolve(1);
+    break;
+  case false:
+    resolve(orientation);
+    break;
+  default:
+    // black 2x1 JPEG, with the following meta information set:
+    // - EXIF Orientation: 6 (Rotated 90° CCW)
+    const testImageURL =
+      'data:image/jpeg;base64,/9j/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAYAAAA' +
+      'AAAD/2wCEAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA' +
+      'QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE' +
+      'BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAf/AABEIAAEAAgMBEQACEQEDEQH/x' +
+      'ABKAAEAAAAAAAAAAAAAAAAAAAALEAEAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAA' +
+      'AAAAAEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwA/8H//2Q==';
+    const img = new Image();
+    img.onload = () => {
+      const automatic = (img.width === 1 && img.height === 2);
+      _browser_quirks['image-orientation-automatic'] = automatic;
+      resolve(automatic ? 1 : orientation);
+    };
+    img.onerror = () => {
+      _browser_quirks['image-orientation-automatic'] = false;
+      resolve(orientation);
+    };
+    img.src = testImageURL;
+  }
+});
+
+// Some browsers don't allow reading from a canvas and instead return all-white
+// or randomized data. Use a pre-defined image to check if reading the canvas
+// works.
+const checkCanvasReliability = () => new Promise((resolve, reject) => {
+  switch(_browser_quirks['canvas-read-unreliable']) {
+  case true:
+    reject('Canvas reading unreliable');
+    break;
+  case false:
+    resolve();
+    break;
+  default:
+    // 2×2 GIF with white, red, green and blue pixels
+    const testImageURL =
+      'data:image/gif;base64,R0lGODdhAgACAKEDAAAA//8AAAD/AP///ywAAAAAAgACAAACA1wEBQA7';
+    const refData =
+      [255, 255, 255, 255,  255, 0, 0, 255,  0, 255, 0, 255,  0, 0, 255, 255];
+    const img = new Image();
+    img.onload = () => {
+      const canvas  = document.createElement('canvas');
+      const context = canvas.getContext('2d');
+      context.drawImage(img, 0, 0, 2, 2);
+      const imageData = context.getImageData(0, 0, 2, 2);
+      if (imageData.data.every((x, i) => refData[i] === x)) {
+        _browser_quirks['canvas-read-unreliable'] = false;
+        resolve();
+      } else {
+        _browser_quirks['canvas-read-unreliable'] = true;
+        reject('Canvas reading unreliable');
+      }
+    };
+    img.onerror = () => {
+      _browser_quirks['canvas-read-unreliable'] = true;
+      reject('Failed to load test image');
+    };
+    img.src = testImageURL;
+  }
+});
+
+const getImageUrl = inputFile => new Promise((resolve, reject) => {
+  if (window.URL && URL.createObjectURL) {
+    try {
+      resolve(URL.createObjectURL(inputFile));
+    } catch (error) {
+      reject(error);
+    }
+    return;
+  }
+
+  const reader = new FileReader();
+  reader.onerror = (...args) => reject(...args);
+  reader.onload  = ({ target }) => resolve(target.result);
+
+  reader.readAsDataURL(inputFile);
+});
+
+const loadImage = inputFile => new Promise((resolve, reject) => {
+  getImageUrl(inputFile).then(url => {
+    const img = new Image();
+
+    img.onerror = (...args) => reject(...args);
+    img.onload  = () => resolve(img);
+
+    img.src = url;
+  }).catch(reject);
+});
+
+const getOrientation = (img, type = 'image/png') => new Promise(resolve => {
+  if (!['image/jpeg', 'image/webp'].includes(type)) {
+    resolve(1);
+    return;
+  }
+
+  EXIF.getData(img, () => {
+    const orientation = EXIF.getTag(img, 'Orientation');
+    if (orientation !== 1) {
+      dropOrientationIfNeeded(orientation).then(resolve).catch(() => resolve(orientation));
+    } else {
+      resolve(orientation);
+    }
+  });
+});
+
+const processImage = (img, { width, height, orientation, type = 'image/png' }) => new Promise(resolve => {
+  const canvas  = document.createElement('canvas');
+
+  if (4 < orientation && orientation < 9) {
+    canvas.width  = height;
+    canvas.height = width;
+  } else {
+    canvas.width  = width;
+    canvas.height = height;
+  }
+
+  const context = canvas.getContext('2d');
+
+  switch (orientation) {
+  case 2: context.transform(-1, 0, 0, 1, width, 0); break;
+  case 3: context.transform(-1, 0, 0, -1, width, height); break;
+  case 4: context.transform(1, 0, 0, -1, 0, height); break;
+  case 5: context.transform(0, 1, 1, 0, 0, 0); break;
+  case 6: context.transform(0, 1, -1, 0, height, 0); break;
+  case 7: context.transform(0, -1, -1, 0, height, width); break;
+  case 8: context.transform(0, -1, 1, 0, 0, width); break;
+  }
+
+  context.drawImage(img, 0, 0, width, height);
+
+  canvas.toBlob(resolve, type);
+});
+
+const resizeImage = (img, type = 'image/png') => new Promise((resolve, reject) => {
+  const { width, height } = img;
+
+  const newWidth  = Math.round(Math.sqrt(MAX_IMAGE_PIXELS * (width / height)));
+  const newHeight = Math.round(Math.sqrt(MAX_IMAGE_PIXELS * (height / width)));
+
+  checkCanvasReliability()
+    .then(getOrientation(img, type))
+    .then(orientation => processImage(img, {
+      width: newWidth,
+      height: newHeight,
+      orientation,
+      type,
+    }))
+    .then(resolve)
+    .catch(reject);
+});
+
+const resizeFile = (inputFile) => new Promise((resolve) => {
+  if (!inputFile.type.match(/image.*/) || inputFile.type === 'image/gif') {
+    resolve(inputFile);
+    return;
+  }
+
+  loadImage(inputFile).then(img => {
+    if (img.width * img.height < MAX_IMAGE_PIXELS) {
+      resolve(inputFile);
+      return;
+    }
+
+    resizeImage(img, inputFile.type)
+      .then(resolve)
+      .catch(() => resolve(inputFile));
+  }).catch(() => resolve(inputFile));
+});
+
+export default resizeFile;
diff --git a/app/javascript/flavours/blobfox/utils/scrollbar.js b/app/javascript/flavours/blobfox/utils/scrollbar.js
new file mode 100644
index 00000000000000..b3f543ffb38a2e
--- /dev/null
+++ b/app/javascript/flavours/blobfox/utils/scrollbar.js
@@ -0,0 +1,34 @@
+/** @type {number | null} */
+let cachedScrollbarWidth = null;
+
+/**
+ * @returns {number}
+ */
+const getActualScrollbarWidth = () => {
+  const outer = document.createElement('div');
+  outer.style.visibility = 'hidden';
+  outer.style.overflow = 'scroll';
+  document.body.appendChild(outer);
+
+  const inner = document.createElement('div');
+  outer.appendChild(inner);
+
+  const scrollbarWidth = outer.offsetWidth - inner.offsetWidth;
+  outer.parentNode.removeChild(outer);
+
+  return scrollbarWidth;
+};
+
+/**
+ * @returns {number}
+ */
+export const getScrollbarWidth = () => {
+  if (cachedScrollbarWidth !== null) {
+    return cachedScrollbarWidth;
+  }
+
+  const scrollbarWidth = getActualScrollbarWidth();
+  cachedScrollbarWidth = scrollbarWidth;
+
+  return scrollbarWidth;
+};
diff --git a/app/javascript/flavours/blobfox/uuid.ts b/app/javascript/flavours/blobfox/uuid.ts
new file mode 100644
index 00000000000000..4d0a8a803637ba
--- /dev/null
+++ b/app/javascript/flavours/blobfox/uuid.ts
@@ -0,0 +1,8 @@
+export function uuid(a?: string): string {
+  return a
+    ? (
+        (a as unknown as number) ^
+        ((Math.random() * 16) >> ((a as unknown as number) / 4))
+      ).toString(16)
+    : ('' + 1e7 + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, uuid);
+}
diff --git a/app/javascript/skins/blobfox/contrast/common.scss b/app/javascript/skins/blobfox/contrast/common.scss
new file mode 100644
index 00000000000000..cc090db08813e1
--- /dev/null
+++ b/app/javascript/skins/blobfox/contrast/common.scss
@@ -0,0 +1 @@
+@import 'flavours/blobfox/styles/contrast';
diff --git a/app/javascript/skins/blobfox/contrast/names.yml b/app/javascript/skins/blobfox/contrast/names.yml
new file mode 100644
index 00000000000000..b78cc72b9c935a
--- /dev/null
+++ b/app/javascript/skins/blobfox/contrast/names.yml
@@ -0,0 +1,12 @@
+en:
+  skins:
+    blobfox:
+      contrast: High contrast
+cs:
+  skins:
+    blobfox:
+      contrast: Vysoký kontrast
+es:
+  skins:
+    blobfox:
+      contrast: Alto contraste
diff --git a/app/javascript/skins/blobfox/mastodon-light/common.scss b/app/javascript/skins/blobfox/mastodon-light/common.scss
new file mode 100644
index 00000000000000..be496e0c4213d0
--- /dev/null
+++ b/app/javascript/skins/blobfox/mastodon-light/common.scss
@@ -0,0 +1 @@
+@import 'flavours/blobfox/styles/mastodon-light';
diff --git a/app/javascript/skins/blobfox/mastodon-light/names.yml b/app/javascript/skins/blobfox/mastodon-light/names.yml
new file mode 100644
index 00000000000000..8b9b81374c12b1
--- /dev/null
+++ b/app/javascript/skins/blobfox/mastodon-light/names.yml
@@ -0,0 +1,12 @@
+en:
+  skins:
+    blobfox:
+      mastodon-light: Mastodon (light)
+cs:
+  skins:
+    blobfox:
+      mastodon-light: Mastodon (světlý)
+es:
+  skins:
+    blobfox:
+      mastodon-light: Mastodon (claro)
diff --git a/app/javascript/skins/blobfox/queens-pink-contrast/common.scss b/app/javascript/skins/blobfox/queens-pink-contrast/common.scss
new file mode 100644
index 00000000000000..c1475586fa558b
--- /dev/null
+++ b/app/javascript/skins/blobfox/queens-pink-contrast/common.scss
@@ -0,0 +1,3 @@
+@import 'variables'; // var data
+@import 'flavours/blobfox/styles/index'; // vanilla style
+@import 'diff'; // modifications
diff --git a/app/javascript/skins/blobfox/queens-pink-contrast/diff.scss b/app/javascript/skins/blobfox/queens-pink-contrast/diff.scss
new file mode 100644
index 00000000000000..08812be8035c3d
--- /dev/null
+++ b/app/javascript/skins/blobfox/queens-pink-contrast/diff.scss
@@ -0,0 +1,381 @@
+@import 'variables';
+
+// This file houses all of the modifications to the base style. Ergo, anything omitted will use the default mastodon styling. This is useful if we just want to change a few things.
+
+body {
+  background-color: darken($ui-base-color, 5%);
+}
+
+@mixin pane-style {
+  @media screen and (max-width: 600px) {
+  // small screen
+    margin: .1rem;
+    padding: .1rem;
+  }
+  @media screen and (min-width: 600px) {
+    margin: 1rem;
+    padding: 1rem;
+  }
+}
+
+.icon-with-badge {
+  .icon-with-badge__badge {
+    border-radius: $queen-radius;
+    transform: scale(1.0);
+    transition: transform .5s ease-in-out;
+  }
+  .icon-with-badge__badge:hover {
+    transform: scale(1.5);
+  }
+}
+
+
+.drawer__inner__mastodon {
+  background-color: transparent !important;
+}
+
+.single-column,
+.auto-columns {
+  background: darken($ui-base-color, 5%);
+  margin: .1rem;
+
+  .column-header {
+    background: $ui-base-color;
+    border-radius: $queen-radius;
+    margin: .1rem;
+  }
+
+  .tabs-bar__wrapper {
+    background-color: transparent;
+  }
+
+
+  .columns-area__panels__pane__inner {
+    // left & right panels
+    @include pane-style;
+
+    background-color: $black;
+    border-radius: $queen-radius;
+    color: $queen-highlight-color;
+  }
+  .columns-area {
+    background-color: transparent;
+  }
+
+  .search {
+    margin: 1rem;
+    .search__input {
+      border-radius: $queen-radius;
+    }
+  }
+
+  .report-modal {
+    background-color: $ui-base-color;
+    color: $white;
+    border-radius: $queen-radius;
+  }
+
+  .column-header__button,
+  .column-header__back-button,
+  .button,
+  .content__heading__actions.button {
+    background-color: $ui-base-color;
+    border-radius: $queen-radius;
+    padding: .7rem;
+    border-width: 2px;
+    border-style: outset;
+    border-color: lighten($queen-highlight-color, 5%);
+    color: $white;
+
+    box-shadow: inset 0 0 0 0 $queen-highlight-color;
+    transition: box-shadow 1s ease-in-out;
+    // transition: background-color .3s;
+    // transition: color .3s;
+  }
+
+  .column-header__button:hover,
+  .column-header__back-button:hover,
+  .button:hover,
+  .content__heading__actions.button:hover {
+    box-shadow: inset 100px 0 0 0 $queen-highlight-color;
+    // background-color: $queen-highlight-color;
+    // color: $black;
+  }
+
+  .drawer__header {
+    background-color: $ui-base-color;
+    border-radius: $queen-radius;
+    i.fa {
+      color: $white;
+    }
+  }
+
+  .drawer__inner {
+    background-color: transparent;
+  }
+
+  .scrollable {
+    background-color: transparent;
+  }
+  .columns-area__panels__main {
+    background-color: transparent;
+    border-bottom-left-radius: $queen-radius;
+    border-bottom-right-radius: $queen-radius;
+    padding: 0%;
+  }
+
+  .status__info {
+    padding-bottom: .5rem;
+  }
+
+  // .account__avatar{
+  //   border-radius: $queen-radius-pfp;
+  //   border-style: solid;
+  //   border-color: $queen-highlight-color;
+  //   // border-width: 3px;
+  //   border-top-width: $queen-border-thickness;
+  //   border-left-width: $queen-border-thickness;
+  //   transform: none;
+  // }
+
+  .status__avatar {
+    // margin: 2rem;
+    border-radius: $queen-radius-pfp;
+    background-image: linear-gradient(45deg, $queen-highlight-color, $ui-base-color);
+    padding: .5rem;
+    border-width: $queen-border-thickness;
+    border-style: solid;
+    border-color: $queen-highlight-color;
+  }
+
+  .status__display-name {
+    .display-name {
+      background-image: linear-gradient(to right, $queen-highlight-color, $ui-base-color);
+      padding: .5rem;
+      padding-left: 1rem;
+      padding-right: 1rem;
+      border-radius: $queen-radius;
+      .display-name__account {
+        color: $white;
+      }
+    }
+  }
+
+  .account__avatar,
+  .account__avatar-overlay-base,
+  .account__avatar-overlay-overlay {
+    border-radius: $queen-radius-pfp;
+  }
+
+  .tabs-bar__wrapper {
+    background-color: transparent;
+  }
+
+  .status,
+  .detailed-status {
+    background-color: $black;
+    border-radius: $queen-radius;
+    border-style: solid;
+    border-color: desaturate($queen-highlight-color, 25%);
+    // border-width: 3px;
+    border-bottom-width: $queen-border-thickness;
+    border-right-width: $queen-border-thickness;
+    padding: 1rem;
+    margin: 1rem;
+
+  }
+
+  .status__content {
+    text-align: center;
+    align-content: center;
+    vertical-align: middle;
+    span {
+      padding: 1rem;
+    }
+  }
+  .status__content__text {
+    text-align: left;
+  }
+
+  .status__content--with-spoiler {
+    background-color: $ui-base-color;
+    padding: 1rem;
+    border-radius: $queen-radius;
+  }
+
+  .status__content__spoiler-link {
+    // dear god why isn't there a class for the spoiler text!?!?
+    padding: .5rem;
+    border-radius: $queen-radius;
+    background-color: $queen-highlight-color;
+    color: $white;
+    transition: background-color 1s;
+  }
+  .status__content__spoiler-link:hover {
+    background-color: $black;
+  }
+
+  .dropdown-animation {
+    padding: .5rem;
+    border-radius: $queen-radius;
+    background-color: $queen-highlight-color;
+    color: $white;
+  }
+  .privacy-dropdown__option {
+    border-radius: $queen-radius;
+    background-color: $queen-highlight-color;
+    color: $white;
+  }
+
+  .detailed-status-direct,
+  .status-direct,
+  .status-direct::selection,
+  .detailed-status-direct::selection {
+    padding-left: 2rem;
+    border-radius: $queen-radius;
+    border: .3rem solid $queen-highlight-color;
+    border-top-right-radius: 0%;
+    background-color: transparent;
+    background-image: linear-gradient(140deg, darken($queen-highlight-color, 15%), $black, $black);
+  }
+
+  .compose-form {
+    background-color: transparent;
+
+    .compose-form__autosuggest-wrapper {
+      background-color: transparent;
+
+      .autosuggest-textarea {
+        background-color: transparent;
+
+        .autosuggest-textarea__textarea {
+          background-color: $ui-base-color;
+          color: lighten($queen-highlight-color, 25%);
+          border-radius: $queen-radius;
+          padding: 1rem;
+        }
+      }
+    }
+  }
+  .spoiler-input__input {
+    background-color: $ui-base-color;
+    color: lighten($queen-highlight-color, 25%);
+    border-radius: $queen-radius;
+    // padding: 1rem;
+  }
+
+  .navigation-bar {
+    padding: .5rem;
+    background-color: $ui-base-color;
+    border-radius: $queen-radius;
+  }
+.autosuggest-textarea__suggestions-wrapper {
+  background-color: lighten($ui-base-color, 5%);
+}
+.compose-form__buttons-wrapper {
+  background-color: lighten($ui-base-color, 5%);
+  padding: .5rem;
+  margin-top: .5rem;
+  border-radius: $queen-radius;
+}
+
+  .hashtag,
+  .status-link,
+  .mention {
+    background-color: $queen-highlight-color;
+    border-radius: $queen-radius;
+
+    color: $white;
+    text-decoration-color: $white;
+    text-emphasis-color: $white;
+    transition: background-color 1s;
+    // clear values from the spoi
+    padding: .2rem;
+    margin: .3rem;
+    border: 2px outset $white;
+  }
+  .hashtag:hover,
+  .mention:hover {
+    background-color: $black;
+  }
+
+  .unhandled-link {
+    background-color: $black;
+    border-color: $queen-highlight-color;
+  }
+  .mention {
+    border-color: lighten($queen-highlight-color, 25%);
+  }
+  .hashtag {
+    border-color: darken($queen-highlight-color, 25%);
+  }
+  .status-card {
+    background-color: $ui-base-color;
+    border-radius: $queen-radius;
+    border-color: $queen-highlight-color;
+    margin: .5rem;
+    .status-card__image {
+      border-radius: $queen-radius;
+      border-color: $queen-highlight-color;
+      border-width: .1rem;
+      border-style: solid;
+      // padding: .5rem;
+    }
+  }
+
+  .reactions-bar__item,
+  .status__content__text,
+  .display-name {
+    .emojione {
+      transition: transform .3s;
+      transform: scale(1.0);
+    }
+    .emojione:hover {
+      transform: scale(3);
+    }
+  }
+
+  .display-name {
+    clip-path: none;
+  }
+
+  .status__action-bar,
+  .status__info__icons,
+  .columns-area__panels__pane__inner,
+  .notification__message {
+    i.fa,
+    .text-icon {
+      transform: scale(1);
+      transition: transform .3s ease-in-out;
+    }
+
+    i.fa:hover,
+    .text-icon:hover,
+    i.fa:active {
+      transform: scale(1.3);
+    }
+  }
+
+  .column-link {
+    background-color: transparent;
+    box-shadow: inset 0 0 0 0 $queen-highlight-color;
+    transition: box-shadow .3s;
+    border-radius: $queen-radius;
+  }
+  .column-link:hover {
+    box-shadow: inset 10px 0px 30px 0px $queen-highlight-color;
+  }
+
+  .getting-started__wrapper {
+    border-radius: $queen-radius;
+    margin: .5rem;
+  }
+
+  .reply-indicator,
+  .status__content__text {
+    p {
+      line-height: 2rem;
+    }
+  }
+
+}
diff --git a/app/javascript/skins/blobfox/queens-pink-contrast/names.yml b/app/javascript/skins/blobfox/queens-pink-contrast/names.yml
new file mode 100644
index 00000000000000..729cf44e755adc
--- /dev/null
+++ b/app/javascript/skins/blobfox/queens-pink-contrast/names.yml
@@ -0,0 +1,4 @@
+en:
+  skins:
+    blobfox:
+      queens-pink-contrast: Queen's Pink Contrast
diff --git a/app/javascript/skins/blobfox/queens-pink-contrast/screenshot.jpg b/app/javascript/skins/blobfox/queens-pink-contrast/screenshot.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..a31cfac43068b76d45a03a6609ba11464fee19e7
GIT binary patch
literal 173565
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
J=>y~cn*d?ZOQ!$;

literal 0
HcmV?d00001

diff --git a/app/javascript/skins/blobfox/queens-pink-contrast/variables.scss b/app/javascript/skins/blobfox/queens-pink-contrast/variables.scss
new file mode 100644
index 00000000000000..c1de386ce702a8
--- /dev/null
+++ b/app/javascript/skins/blobfox/queens-pink-contrast/variables.scss
@@ -0,0 +1,81 @@
+// my theme customizations
+$queen-highlight-color: #b82493;
+$queen-bg: #3a002f;
+$queen-radius: 16px;
+$queen-radius-pfp: 25%;
+$queen-border-thickness: .1rem;
+
+
+
+// built-in defaults
+$black: #000000;
+$white: #ffffff;
+$success-green: #79bd9a;
+$error-red: #df405a;
+$warning-red: #ff5050;
+$gold-star: #ca8f04; // favorite colour
+
+
+// Values from the classic Mastodon UI
+$classic-base-color: $queen-bg;         // Midnight Express
+$classic-primary-color: desaturate($queen-highlight-color, 15%);      // Echo Blue
+$classic-secondary-color: #d9e1e8;    // Pattens Blue
+$classic-highlight-color: #2b90d9;    // Summer Sky
+
+
+// Variables for defaults in UI
+$base-shadow-color: $black !default;
+$base-overlay-background: $black !default;
+$base-border-color: $white !default;
+$simple-background-color: $white !default;
+$valid-value-color: $success-green !default;
+$error-value-color: $error-red !default;
+
+$ui-base-color: $queen-bg !default;
+$ui-base-lighter-color: lighten($queen-highlight-color, 25%) !default; // Lighter darkest
+$ui-primary-color: $classic-primary-color !default;
+$ui-secondary-color: $classic-secondary-color !default;
+$ui-highlight-color: $classic-highlight-color !default;
+
+$ui-highlight-color: $queen-highlight-color;
+
+
+// Variables for texts
+$primary-text-color: $white !default;
+$darker-text-color: $ui-primary-color !default;
+$dark-text-color: $ui-base-lighter-color !default;
+$secondary-text-color: $ui-secondary-color !default;
+$highlight-text-color: $ui-highlight-color !default;
+$action-button-color: $ui-base-lighter-color !default;
+$action-button-color-lighter: lighten($ui-base-lighter-color, 10%) !default;
+$action-button-color-darker: darken($ui-base-lighter-color, 5%) !default;
+
+$passive-text-color: $gold-star !default;
+$active-passive-text-color: $success-green !default;
+// For texts on inverted backgrounds
+$inverted-text-color: $ui-base-color !default;
+$lighter-text-color: $ui-base-lighter-color !default;
+$light-text-color: $ui-primary-color !default;
+
+$solar-disabled: darken($action-button-color, 10%) !default;
+
+// Language codes that uses CJK fonts
+$cjk-langs: ja, ko, zh-CN, zh-HK, zh-TW;
+
+// Variables for components
+$media-modal-media-max-width: 100%;
+// put margins on top and bottom of image to avoid the screen covered by image.
+$media-modal-media-max-height: 80%;
+
+$no-gap-breakpoint: 415px;
+
+$font-sans-serif: 'mastodon-font-sans-serif' !default;
+$font-display: 'mastodon-font-display' !default;
+$font-monospace: 'mastodon-font-monospace' !default;
+
+// Avatar border size (8% default, 100% for rounded avatars)
+$ui-avatar-border-size: 8%;
+
+// More variables
+$dismiss-overlay-width: 4rem;
+
diff --git a/app/javascript/skins/blobfox/solarpunk/common.scss b/app/javascript/skins/blobfox/solarpunk/common.scss
new file mode 100644
index 00000000000000..8dcb5f04f0386a
--- /dev/null
+++ b/app/javascript/skins/blobfox/solarpunk/common.scss
@@ -0,0 +1,3 @@
+@import 'variables';
+@import 'flavours/blobfox/styles/index';
+@import 'diff';
diff --git a/app/javascript/skins/blobfox/solarpunk/diff.scss b/app/javascript/skins/blobfox/solarpunk/diff.scss
new file mode 100644
index 00000000000000..bac39ce234f104
--- /dev/null
+++ b/app/javascript/skins/blobfox/solarpunk/diff.scss
@@ -0,0 +1,143 @@
+@import 'variables';
+
+body {
+  background-color: $solar-bg;
+}
+
+.media-modal__overlay
+.picture-in-picture__footer 
+button.icon-button {
+  i.fa-retweet {
+    background: url("data:image/svg+xml;utf8,<svg width='18.7' height='94' enable-background='new 0 0 628.254 613.516' version='1.1' viewBox='0 0 657.07 3289.9' xml:space='preserve' xmlns='http://www.w3.org/2000/svg'><g transform='translate(-.77735 -6.0669)'><g transform='translate(29.186 28.067)' fill='#{hex-color($white)}'><g fill='#{hex-color($white)}'><g fill='#{hex-color($white)}'><path d='m99.777 446.09c-6.699 12.031-12.031 30.133-12.031 41.539 0 2.648 0 6.016 0.656 10.688l-83.726-143.99c-2.68-4.672-4.676-11.375-4.676-17.414 0-6.047 1.996-13.398 4.676-18.078l40.195-70.328-44.871-25.43 146.02-2.703 70.984 127.91-45.527-26.117zm64.313-405.86c12.715-22.125 33.496-34.18 58.926-34.18 27.48 0 48.918 12.742 64.312 38.828l22.777 38.172-79.051 136.66-127.91-74.352zm9.351 521.72c-38.172 0-69.645-31.477-69.645-69.648 0-10.719 4.703-28.82 11.402-40.195l21.41-38.172h158.76v148.02h-121.92zm127.29-525.76c-10.036-17.391-23.434-29.477-39.512-36.18h164.75c14.738 0 26.113 6.047 32.84 17.445l40.852 69.648 44.191-26.141-71.016 127.28-145.3-2.047 44.871-25.43zm253.86 379.09c20.07 0 36.832-5.359 50.887-16.055l-83.07 144.65c-6.699 11.375-18.73 18.078-32.789 18.078h-78.395v51.57l-75.004-125.23 75.004-125.27v52.258h143.37zm64.258-120.56c6.043 10.719 9.406 22.094 9.406 34.156 0 24.117-15.422 49.57-36.832 61.602-10.062 5.391-24.145 8.75-38.172 8.75h-44.242l-79-136.66 127.92-73.008z' fill='#{hex-color($white)}'/></g></g></g><g fill='#{hex-color($solar-recycling)}'><g transform='translate(27.733 1347)'><g fill='#{hex-color($solar-recycling)}'><g fill='#{hex-color($solar-recycling)}'><path d='m99.777 446.09c-6.699 12.031-12.031 30.133-12.031 41.539 0 2.648 0 6.016 0.656 10.688l-83.726-143.99c-2.68-4.672-4.676-11.375-4.676-17.414 0-6.047 1.996-13.398 4.676-18.078l97.831-167.4-44.871-25.43 146.02-2.703 70.984 127.91-45.527-26.117zm64.313-405.86c12.715-22.125 33.496-34.18 58.926-34.18 27.48 0 48.918 12.742 64.312 38.828l22.777 38.172-21.415 39.593-127.91-74.352zm9.351 521.72c-38.172 0-69.645-31.477-69.645-69.648 0-10.719 4.703-28.82 11.402-40.195l21.41-38.172 40.453-1.5167v148.02l-3.6203 1.5167zm127.29-525.76c-10.036-17.391-23.434-29.477-39.512-36.18h164.75c14.738 0 26.113 6.047 32.84 17.445l110.62 189.47 44.191-26.141-71.016 127.28-145.3-2.047 44.871-25.43zm253.86 379.09c20.07 0 36.832-5.359 50.887-16.055l-83.07 144.65c-6.699 11.375-18.73 18.078-32.789 18.078l-196.7-1.5167v51.57l-75.004-125.23 75.004-125.27v52.258l261.67 1.5167zm64.258-120.56c6.043 10.719 9.406 22.094 9.406 34.156 0 24.117-15.422 49.57-36.832 61.602-10.062 5.391-24.145 8.75-38.172 8.75h-44.242l-9.2306-16.843 127.92-73.008z' fill='#{hex-color($solar-recycling)}'/></g></g></g><g transform='translate(691.01 5483.9)'><g fill='#{hex-color($solar-recycling)}'/></g><g transform='translate(30.324 2009.8)'><g fill='#{hex-color($solar-recycling)}'><g fill='#{hex-color($solar-recycling)}'><path d='m99.777 446.09c-6.699 12.031-12.031 30.133-12.031 41.539 0 2.648 0 6.016 0.656 10.688l-83.726-143.99c-2.68-4.672-4.676-11.375-4.676-17.414 0-6.047 1.996-13.398 4.676-18.078l11.377-17.243-44.871-25.43 146.02-2.703 70.984 127.91-45.527-26.117zm64.313-405.86c12.715-22.125 33.496-34.18 58.926-34.18 27.48 0 48.918 12.742 64.312 38.828l22.777 38.172-107.87 189.75-127.91-74.352zm9.351 521.72c-38.172 0-69.645-31.477-69.645-69.648 0-10.719 4.703-28.82 11.402-40.195l21.41-38.172h216.39v148.02h-179.56zm127.29-525.76c-10.036-17.391-23.434-29.477-39.512-36.18h164.75c14.738 0 26.113 6.047 32.84 17.445l13.551 2.912 44.191-26.141-71.016 127.28-145.3-2.047 44.871-25.43zm253.86 379.09c20.07 0 36.832-5.359 50.887-16.055l-83.07 144.65c-6.699 11.375-18.73 18.078-32.789 18.078h-20.759v51.57l-75.004-125.23 75.004-125.27v52.258h85.731zm64.258-120.56c6.043 10.719 9.406 22.094 9.406 34.156 0 24.117-15.422 49.57-36.832 61.602-10.062 5.391-24.145 8.75-38.172 8.75h-44.242l-116.92-210.98 127.92-73.008z' fill='#{hex-color($solar-recycling)}'/></g></g></g><g transform='translate(27.915 2669.7)'><g fill='#{hex-color($solar-recycling)}'><g fill='#{hex-color($solar-recycling)}'><path d='m99.777 446.09c-6.699 12.031-12.031 30.133-12.031 41.539 0 2.648 0 6.016 0.656 10.688l-83.726-143.99c-2.68-4.672-4.676-11.375-4.676-17.414 0-6.047 1.996-13.398 4.676-18.078l40.195-70.328-44.871-25.43 146.02-2.703 70.984 127.91-45.527-26.117zm64.313-405.86c12.715-22.125 33.496-34.18 58.926-34.18 27.48 0 48.918 12.742 64.312 38.828l22.777 38.172-79.051 136.66-127.91-74.352zm9.351 521.72c-38.172 0-69.645-31.477-69.645-69.648 0-10.719 4.703-28.82 11.402-40.195l21.41-38.172h158.76v148.02h-121.92zm127.29-525.76c-10.036-17.391-23.434-29.477-39.512-36.18h164.75c14.738 0 26.113 6.047 32.84 17.445l40.852 69.648 44.191-26.141-71.016 127.28-145.3-2.047 44.871-25.43zm253.86 379.09c20.07 0 36.832-5.359 50.887-16.055l-83.07 144.65c-6.699 11.375-18.73 18.078-32.789 18.078h-78.395v51.57l-75.004-125.23 75.004-125.27v52.258h143.37zm64.258-120.56c6.043 10.719 9.406 22.094 9.406 34.156 0 24.117-15.422 49.57-36.832 61.602-10.062 5.391-24.145 8.75-38.172 8.75h-44.242l-79-136.66 127.92-73.008z' fill='#{hex-color($solar-recycling)}'/></g></g></g></g><g transform='translate(29.186 687.36)' fill='#{hex-color($white)}'><g fill='#{hex-color($white)}'><g fill='#{hex-color($white)}'><path d='m99.777 446.09c-6.699 12.031-12.031 30.133-12.031 41.539 0 2.648 0 6.016 0.656 10.688l-83.726-143.99c-2.68-4.672-4.676-11.375-4.676-17.414 0-6.047 1.996-13.398 4.676-18.078l76.66-134.68-44.871-25.43 146.02-2.703 70.984 127.91-45.527-26.117zm64.313-405.86c12.715-22.125 33.496-34.18 58.926-34.18 27.48 0 48.918 12.742 64.312 38.828l22.777 38.172-42.586 72.315-127.91-74.352zm9.351 521.72c-38.172 0-69.645-31.477-69.645-69.648 0-10.719 4.703-28.82 11.402-40.195l21.41-38.172h111.57v148.02h-74.736zm127.29-525.76c-10.036-17.391-23.434-29.477-39.512-36.18h164.75c14.738 0 26.113 6.047 32.84 17.445l73.027 129.71 44.191-26.141-71.016 127.28-145.3-2.047 44.871-25.43zm253.86 379.09c20.07 0 36.832-5.359 50.887-16.055l-83.07 144.65c-6.699 11.375-18.73 18.078-32.789 18.078h-125.58v51.57l-75.004-125.23 75.004-125.27v52.258h190.56zm64.258-120.56c6.043 10.719 9.406 22.094 9.406 34.156 0 24.117-15.422 49.57-36.832 61.602-10.062 5.391-24.145 8.75-38.172 8.75h-44.242l-46.825-76.605 127.92-73.008z' fill='#{hex-color($solar-recycling)}'/></g></g></g></g></svg>") no-repeat;
+  }
+
+  &.reblogPrivate i.fa-retweet {
+    background: url("data:image/svg+xml;utf8,<svg width='19' height='19' enable-background='new 0 0 628.254 613.516' version='1.1' viewBox='0 0 630.25 615.51' xml:space='preserve' xmlns='http://www.w3.org/2000/svg'><defs><mask id='mask-powermask-path-effect929' maskUnits='userSpaceOnUse'><path id='mask-powermask-path-effect929_box' d='m-1-1h630.25v615.51h-630.25z' fill='%23fff'/><g transform='matrix(1.8197 0 0 1.8197 467.68 255.09)' style=''><rect x='-219.52' y='91.198' width='114.31' height='71.892' fill='none' stroke='%23000' stroke-width='34.869'/><path d='m-211.31 76.273c0-2.2289-0.45078-28.654-0.20046-30.833 1.9095-16.621 12.096-32.827 25.749-41.276 15.443-9.5572 34.438-9.4492 49.787 0.28295 13.762 8.7258 22.704 23.991 24.271 40.915 0.18075 1.9519-0.0259 28.68-0.0453 30.668' fill='none' stroke='%23000' stroke-width='28.408'/></g></mask></defs><g transform='translate(-28.186 -5.0669)'><g transform='translate(29.186 6.0669)' fill='#{hex-color($white)}' mask='url(%23mask-powermask-path-effect929)'><path d='m99.777 446.09c-6.699 12.031-12.031 30.133-12.031 41.539 0 2.648 0 6.016 0.656 10.688l-83.726-143.99c-2.68-4.672-4.676-11.375-4.676-17.414 0-6.047 1.996-13.398 4.676-18.078l40.195-70.328-44.871-25.43 146.02-2.703 70.984 127.91-45.527-26.117zm64.313-405.86c12.715-22.125 33.496-34.18 58.926-34.18 27.48 0 48.918 12.742 64.312 38.828l22.777 38.172-79.051 136.66-127.91-74.352zm9.351 521.72c-38.172 0-69.645-31.477-69.645-69.648 0-10.719 4.703-28.82 11.402-40.195l21.41-38.172h158.76v148.02h-121.92zm127.29-525.76c-10.036-17.391-23.434-29.477-39.512-36.18h164.75c14.738 0 26.113 6.047 32.84 17.445l40.852 69.648 44.191-26.141-71.016 127.28-145.3-2.047 44.871-25.43zm253.86 379.09c20.07 0 36.832-5.359 50.887-16.055l-83.07 144.65c-6.699 11.375-18.73 18.078-32.789 18.078h-78.395v51.57l-75.004-125.23 75.004-125.27v52.258h143.37zm64.258-120.56c6.043 10.719 9.406 22.094 9.406 34.156 0 24.117-15.422 49.57-36.832 61.602-10.062 5.391-24.145 8.75-38.172 8.75h-44.242l-79-136.66 127.92-73.008z' fill='#{hex-color($white)}' mask='none'/></g><path d='m28.186 5.0669h630.25v615.51h-630.25z' fill='none'/><g transform='translate(691.01 5483.9)' fill='%23009700'><g fill='%23009700'/></g><g transform='matrix(1.8197 0 0 1.8197 537.9 285.64)' stroke='#{hex-color($white)}'><rect x='-219.52' y='91.198' width='114.31' height='71.892' fill='#{hex-color($white)}' stroke-width='34.869'/><path d='m-211.31 76.273c0-2.2289-0.45078-28.654-0.20046-30.833 1.9095-16.621 12.096-32.827 25.749-41.276 15.443-9.5572 34.438-9.4492 49.787 0.28295 13.762 8.7258 22.704 23.991 24.271 40.915 0.18075 1.9519-0.0259 28.68-0.0453 30.668' fill='none' stroke-width='28.408'/></g></g></svg>");
+  }
+
+  i.fa-star {
+    background: url("data:image/svg+xml;utf8,<svg width='6.299mm' height='28.591mm' version='1.1' viewBox='0 0 4.9132 22.301' xmlns='http://www.w3.org/2000/svg'><g transform='translate(-72.638 -93.656)'><g transform='translate(.47668 5.3239)'><path transform='matrix(.04592 .26057 -.26057 .04592 76.244 95.826)' d='m-0.09375 11.156-3.1493-2.2515-1.9189 3.3623-0.63485-3.819-3.7344 1.0206 2.2515-3.1493-3.3623-1.9189 3.819-0.63485-1.0206-3.7344 3.1493 2.2515 1.9189-3.3623 0.63485 3.819 3.7344-1.0206-2.2515 3.1493 3.3623 1.9189-3.819 0.63485z' fill='%23ffb271'/><g fill='%23ff8e7c'><path d='m74.63 92.871-0.44504 1.5146 0.31204 0.77128 0.57329-0.77889z' stroke-width='1.0644'/><path d='m74.205 95.719 0.41002-0.27757 0.41153 0.30391-0.37017 1.3919' stroke-width='1.1036'/><path d='m72.271 95.052 1.6527 0.40144 0.90943-0.37981-0.85337-0.45872z'/><path d='m76.865 95.04-1.5894-0.43145-0.8651 0.42185 0.90715 0.47274z'/></g><circle cx='74.637' cy='95.054' r='.86816' fill='%23f9e59a' stroke-width='.20427'/></g><g transform='translate(.025125 .41834)'><g transform='translate(.49227 .42869)'><path transform='matrix(.04592 .26057 -.26057 .04592 76.244 95.826)' d='m-0.09375 11.156-3.0678-2.4605c-1.0077-0.58067-1.6372-0.66239-2.5754-0.46052l-3.7943 1.2335 2.2515-3.1493-3.3623-1.9189 3.819-0.63485-1.0206-3.7344 3.1493 2.2515c1.573 0.58073 1.5464 0.26082 2.5538 0.4567l3.7344-1.0206-2.2515 3.1493c-1.1607 1.0484-0.49695 1.609-0.4567 2.5538z' fill='#{hex-color($white)}'/><g fill='#{hex-color($white)}'><path d='m74.612 93.227-0.34726 1.0005 0.25632 0.51263 0.43822-0.52917z'/><path d='m74.249 95.856 0.27765-0.50436 0.42515 0.5209-0.34112 1.0253'/><path d='m72.802 95.054 1.0348 0.36811 0.59405-0.35157-0.59987-0.37789z'/><path d='m76.403 95.054-0.95912-0.339-0.66973 0.32246 0.66973 0.37207z'/></g><circle cx='74.637' cy='95.054' r='.86816' fill='#{hex-color($white)}' stroke-width='.20427'/></g></g><g transform='translate(.51611 14.249)'><path transform='matrix(.04592 .26057 -.26057 .04592 76.244 95.826)' d='m2.4281 13.801-6.2849-5.3807-1.3051 3.847-0.17157-4.0062-6.9762 2.8655 5.1741-5.3193-3.5064-1.4065 3.8314-0.094818-3.2175-7.6125 5.8984 5.7429 1.3543-3.5156 0.19915 3.8497 7.3536-2.8701-5.754 5.5572 3.6813 1.3297-4.0124-0.09844z' fill='%23ffb271'/><g fill='%23ff8e7c'><path d='m74.612 93.227-0.34726 1.0005 0.25632 0.51263 0.43822-0.52917z'/><path d='m74.249 95.856 0.27765-0.50436 0.42515 0.5209-0.34112 1.0253'/><path d='m72.802 95.054 0.95912 0.339 0.66973-0.32246-0.66973-0.37207z'/><path d='m76.403 95.054-0.95912-0.339-0.66973 0.32246 0.66973 0.37207z'/></g><circle cx='74.637' cy='95.054' r='.86816' fill='%23f9e59a' stroke-width='.20427'/></g><g transform='translate(.51271 18.705)'><path transform='matrix(.04592 .26057 -.26057 .04592 76.244 95.826)' d='m-0.09375 11.156-3.1493-2.2515-1.9189 3.3623-0.63485-3.819-3.7344 1.0206 2.2515-3.1493-3.3623-1.9189 3.819-0.63485-1.0206-3.7344 3.1493 2.2515 1.9189-3.3623 0.63485 3.819 3.7344-1.0206-2.2515 3.1493 3.3623 1.9189-3.819 0.63485z' fill='%23ffb271'/><g fill='%23ff8e7c'><path d='m74.612 93.227-0.34726 1.0005 0.25632 0.51263 0.43822-0.52917z'/><path d='m74.249 95.856 0.27765-0.50436 0.42515 0.5209-0.34112 1.0253'/><path d='m72.802 95.054 0.95912 0.339 0.66973-0.32246-0.66973-0.37207z'/><path d='m76.403 95.054-0.95912-0.339-0.66973 0.32246 0.66973 0.37207z'/></g><circle cx='74.637' cy='95.054' r='.86816' fill='%23f9e59a' stroke-width='.20427'/></g><g transform='translate(.50609 9.7746)'><path transform='matrix(.04592 .26057 -.26057 .04592 76.244 95.826)' d='m-0.09375 11.156-3.1493-2.2515-1.9189 3.3623-0.63485-3.819-3.7344 1.0206 2.2515-3.1493-3.3623-1.9189 3.819-0.63485-1.0206-3.7344 3.1493 2.2515 1.9189-3.3623 0.63485 3.819 3.7344-1.0206-2.2515 3.1493 3.3623 1.9189-3.819 0.63485z' fill='%23ffb271'/><g fill='%23ff8e7c'><path d='m74.612 93.227-0.34726 1.0005 0.25632 0.51263 0.43822-0.52917z'/><path d='m74.249 95.856 0.27765-0.50436 0.42515 0.5209-0.34112 1.0428'/><path d='m72.802 95.054 0.95912 0.339 0.66973-0.32246-0.66973-0.37207z'/><path d='m76.403 95.054-0.95912-0.339-0.66973 0.32246 0.66973 0.37207z'/></g><circle cx='74.637' cy='95.054' r='.86816' fill='%23f9e59a' stroke-width='.20427'/></g></g></svg>") no-repeat top;
+  }
+}
+
+.media-modal__overlay
+.picture-in-picture__footer 
+button.icon-button.active {
+  &.reblogPrivate.active i.fa-retweet,
+  i.fa-retweet {
+    background: url("data:image/svg+xml;utf8,<svg width='18.7' height='94' enable-background='new 0 0 628.254 613.516' version='1.1' viewBox='0 0 657.07 3289.9' xml:space='preserve' xmlns='http://www.w3.org/2000/svg'><g transform='translate(-.77735 -6.0669)'><g transform='translate(29.186 28.067)' fill='#{hex-color($white)}'><g fill='#{hex-color($white)}'><g fill='#{hex-color($white)}'><path d='m99.777 446.09c-6.699 12.031-12.031 30.133-12.031 41.539 0 2.648 0 6.016 0.656 10.688l-83.726-143.99c-2.68-4.672-4.676-11.375-4.676-17.414 0-6.047 1.996-13.398 4.676-18.078l40.195-70.328-44.871-25.43 146.02-2.703 70.984 127.91-45.527-26.117zm64.313-405.86c12.715-22.125 33.496-34.18 58.926-34.18 27.48 0 48.918 12.742 64.312 38.828l22.777 38.172-79.051 136.66-127.91-74.352zm9.351 521.72c-38.172 0-69.645-31.477-69.645-69.648 0-10.719 4.703-28.82 11.402-40.195l21.41-38.172h158.76v148.02h-121.92zm127.29-525.76c-10.036-17.391-23.434-29.477-39.512-36.18h164.75c14.738 0 26.113 6.047 32.84 17.445l40.852 69.648 44.191-26.141-71.016 127.28-145.3-2.047 44.871-25.43zm253.86 379.09c20.07 0 36.832-5.359 50.887-16.055l-83.07 144.65c-6.699 11.375-18.73 18.078-32.789 18.078h-78.395v51.57l-75.004-125.23 75.004-125.27v52.258h143.37zm64.258-120.56c6.043 10.719 9.406 22.094 9.406 34.156 0 24.117-15.422 49.57-36.832 61.602-10.062 5.391-24.145 8.75-38.172 8.75h-44.242l-79-136.66 127.92-73.008z' fill='#{hex-color($white)}'/></g></g></g><g fill='#{hex-color($solar-recycling)}'><g transform='translate(27.733 1347)'><g fill='#{hex-color($solar-recycling)}'><g fill='#{hex-color($solar-recycling)}'><path d='m99.777 446.09c-6.699 12.031-12.031 30.133-12.031 41.539 0 2.648 0 6.016 0.656 10.688l-83.726-143.99c-2.68-4.672-4.676-11.375-4.676-17.414 0-6.047 1.996-13.398 4.676-18.078l97.831-167.4-44.871-25.43 146.02-2.703 70.984 127.91-45.527-26.117zm64.313-405.86c12.715-22.125 33.496-34.18 58.926-34.18 27.48 0 48.918 12.742 64.312 38.828l22.777 38.172-21.415 39.593-127.91-74.352zm9.351 521.72c-38.172 0-69.645-31.477-69.645-69.648 0-10.719 4.703-28.82 11.402-40.195l21.41-38.172 40.453-1.5167v148.02l-3.6203 1.5167zm127.29-525.76c-10.036-17.391-23.434-29.477-39.512-36.18h164.75c14.738 0 26.113 6.047 32.84 17.445l110.62 189.47 44.191-26.141-71.016 127.28-145.3-2.047 44.871-25.43zm253.86 379.09c20.07 0 36.832-5.359 50.887-16.055l-83.07 144.65c-6.699 11.375-18.73 18.078-32.789 18.078l-196.7-1.5167v51.57l-75.004-125.23 75.004-125.27v52.258l261.67 1.5167zm64.258-120.56c6.043 10.719 9.406 22.094 9.406 34.156 0 24.117-15.422 49.57-36.832 61.602-10.062 5.391-24.145 8.75-38.172 8.75h-44.242l-9.2306-16.843 127.92-73.008z' fill='#{hex-color($solar-recycling)}'/></g></g></g><g transform='translate(691.01 5483.9)'><g fill='#{hex-color($solar-recycling)}'/></g><g transform='translate(30.324 2009.8)'><g fill='#{hex-color($solar-recycling)}'><g fill='#{hex-color($solar-recycling)}'><path d='m99.777 446.09c-6.699 12.031-12.031 30.133-12.031 41.539 0 2.648 0 6.016 0.656 10.688l-83.726-143.99c-2.68-4.672-4.676-11.375-4.676-17.414 0-6.047 1.996-13.398 4.676-18.078l11.377-17.243-44.871-25.43 146.02-2.703 70.984 127.91-45.527-26.117zm64.313-405.86c12.715-22.125 33.496-34.18 58.926-34.18 27.48 0 48.918 12.742 64.312 38.828l22.777 38.172-107.87 189.75-127.91-74.352zm9.351 521.72c-38.172 0-69.645-31.477-69.645-69.648 0-10.719 4.703-28.82 11.402-40.195l21.41-38.172h216.39v148.02h-179.56zm127.29-525.76c-10.036-17.391-23.434-29.477-39.512-36.18h164.75c14.738 0 26.113 6.047 32.84 17.445l13.551 2.912 44.191-26.141-71.016 127.28-145.3-2.047 44.871-25.43zm253.86 379.09c20.07 0 36.832-5.359 50.887-16.055l-83.07 144.65c-6.699 11.375-18.73 18.078-32.789 18.078h-20.759v51.57l-75.004-125.23 75.004-125.27v52.258h85.731zm64.258-120.56c6.043 10.719 9.406 22.094 9.406 34.156 0 24.117-15.422 49.57-36.832 61.602-10.062 5.391-24.145 8.75-38.172 8.75h-44.242l-116.92-210.98 127.92-73.008z' fill='#{hex-color($solar-recycling)}'/></g></g></g><g transform='translate(27.915 2669.7)'><g fill='#{hex-color($solar-recycling)}'><g fill='#{hex-color($solar-recycling)}'><path d='m99.777 446.09c-6.699 12.031-12.031 30.133-12.031 41.539 0 2.648 0 6.016 0.656 10.688l-83.726-143.99c-2.68-4.672-4.676-11.375-4.676-17.414 0-6.047 1.996-13.398 4.676-18.078l40.195-70.328-44.871-25.43 146.02-2.703 70.984 127.91-45.527-26.117zm64.313-405.86c12.715-22.125 33.496-34.18 58.926-34.18 27.48 0 48.918 12.742 64.312 38.828l22.777 38.172-79.051 136.66-127.91-74.352zm9.351 521.72c-38.172 0-69.645-31.477-69.645-69.648 0-10.719 4.703-28.82 11.402-40.195l21.41-38.172h158.76v148.02h-121.92zm127.29-525.76c-10.036-17.391-23.434-29.477-39.512-36.18h164.75c14.738 0 26.113 6.047 32.84 17.445l40.852 69.648 44.191-26.141-71.016 127.28-145.3-2.047 44.871-25.43zm253.86 379.09c20.07 0 36.832-5.359 50.887-16.055l-83.07 144.65c-6.699 11.375-18.73 18.078-32.789 18.078h-78.395v51.57l-75.004-125.23 75.004-125.27v52.258h143.37zm64.258-120.56c6.043 10.719 9.406 22.094 9.406 34.156 0 24.117-15.422 49.57-36.832 61.602-10.062 5.391-24.145 8.75-38.172 8.75h-44.242l-79-136.66 127.92-73.008z' fill='#{hex-color($solar-recycling)}'/></g></g></g></g><g transform='translate(29.186 687.36)' fill='#{hex-color($white)}'><g fill='#{hex-color($white)}'><g fill='#{hex-color($white)}'><path d='m99.777 446.09c-6.699 12.031-12.031 30.133-12.031 41.539 0 2.648 0 6.016 0.656 10.688l-83.726-143.99c-2.68-4.672-4.676-11.375-4.676-17.414 0-6.047 1.996-13.398 4.676-18.078l76.66-134.68-44.871-25.43 146.02-2.703 70.984 127.91-45.527-26.117zm64.313-405.86c12.715-22.125 33.496-34.18 58.926-34.18 27.48 0 48.918 12.742 64.312 38.828l22.777 38.172-42.586 72.315-127.91-74.352zm9.351 521.72c-38.172 0-69.645-31.477-69.645-69.648 0-10.719 4.703-28.82 11.402-40.195l21.41-38.172h111.57v148.02h-74.736zm127.29-525.76c-10.036-17.391-23.434-29.477-39.512-36.18h164.75c14.738 0 26.113 6.047 32.84 17.445l73.027 129.71 44.191-26.141-71.016 127.28-145.3-2.047 44.871-25.43zm253.86 379.09c20.07 0 36.832-5.359 50.887-16.055l-83.07 144.65c-6.699 11.375-18.73 18.078-32.789 18.078h-125.58v51.57l-75.004-125.23 75.004-125.27v52.258h190.56zm64.258-120.56c6.043 10.719 9.406 22.094 9.406 34.156 0 24.117-15.422 49.57-36.832 61.602-10.062 5.391-24.145 8.75-38.172 8.75h-44.242l-46.825-76.605 127.92-73.008z' fill='#{hex-color($solar-recycling)}'/></g></g></g></g></svg>") no-repeat;
+    background-position: 50% 100%;
+  }
+
+  i.fa-star {
+    background-position: 50% 100%;
+  }
+}
+
+button.icon-button {
+  &:hover i.fa-retweet,
+  &.reblogPrivate.active i.fa-retweet,
+  &:hover.reblogPrivate.active i.fa-retweet,
+  i.fa-retweet {
+      background: url("data:image/svg+xml;utf8,<svg width='18.7' height='94' enable-background='new 0 0 628.254 613.516' version='1.1' viewBox='0 0 657.07 3289.9' xml:space='preserve' xmlns='http://www.w3.org/2000/svg'><g transform='translate(-.77735 -6.0669)'><g transform='translate(29.186 28.067)' fill='#{hex-color($action-button-color)}'><g fill='#{hex-color($action-button-color)}'><g fill='#{hex-color($action-button-color)}'><path d='m99.777 446.09c-6.699 12.031-12.031 30.133-12.031 41.539 0 2.648 0 6.016 0.656 10.688l-83.726-143.99c-2.68-4.672-4.676-11.375-4.676-17.414 0-6.047 1.996-13.398 4.676-18.078l40.195-70.328-44.871-25.43 146.02-2.703 70.984 127.91-45.527-26.117zm64.313-405.86c12.715-22.125 33.496-34.18 58.926-34.18 27.48 0 48.918 12.742 64.312 38.828l22.777 38.172-79.051 136.66-127.91-74.352zm9.351 521.72c-38.172 0-69.645-31.477-69.645-69.648 0-10.719 4.703-28.82 11.402-40.195l21.41-38.172h158.76v148.02h-121.92zm127.29-525.76c-10.036-17.391-23.434-29.477-39.512-36.18h164.75c14.738 0 26.113 6.047 32.84 17.445l40.852 69.648 44.191-26.141-71.016 127.28-145.3-2.047 44.871-25.43zm253.86 379.09c20.07 0 36.832-5.359 50.887-16.055l-83.07 144.65c-6.699 11.375-18.73 18.078-32.789 18.078h-78.395v51.57l-75.004-125.23 75.004-125.27v52.258h143.37zm64.258-120.56c6.043 10.719 9.406 22.094 9.406 34.156 0 24.117-15.422 49.57-36.832 61.602-10.062 5.391-24.145 8.75-38.172 8.75h-44.242l-79-136.66 127.92-73.008z' fill='#{hex-color($action-button-color)}'/></g></g></g><g fill='#{hex-color($solar-recycling)}'><g transform='translate(27.733 1347)'><g fill='#{hex-color($solar-recycling)}'><g fill='#{hex-color($solar-recycling)}'><path d='m99.777 446.09c-6.699 12.031-12.031 30.133-12.031 41.539 0 2.648 0 6.016 0.656 10.688l-83.726-143.99c-2.68-4.672-4.676-11.375-4.676-17.414 0-6.047 1.996-13.398 4.676-18.078l97.831-167.4-44.871-25.43 146.02-2.703 70.984 127.91-45.527-26.117zm64.313-405.86c12.715-22.125 33.496-34.18 58.926-34.18 27.48 0 48.918 12.742 64.312 38.828l22.777 38.172-21.415 39.593-127.91-74.352zm9.351 521.72c-38.172 0-69.645-31.477-69.645-69.648 0-10.719 4.703-28.82 11.402-40.195l21.41-38.172 40.453-1.5167v148.02l-3.6203 1.5167zm127.29-525.76c-10.036-17.391-23.434-29.477-39.512-36.18h164.75c14.738 0 26.113 6.047 32.84 17.445l110.62 189.47 44.191-26.141-71.016 127.28-145.3-2.047 44.871-25.43zm253.86 379.09c20.07 0 36.832-5.359 50.887-16.055l-83.07 144.65c-6.699 11.375-18.73 18.078-32.789 18.078l-196.7-1.5167v51.57l-75.004-125.23 75.004-125.27v52.258l261.67 1.5167zm64.258-120.56c6.043 10.719 9.406 22.094 9.406 34.156 0 24.117-15.422 49.57-36.832 61.602-10.062 5.391-24.145 8.75-38.172 8.75h-44.242l-9.2306-16.843 127.92-73.008z' fill='#{hex-color($solar-recycling)}'/></g></g></g><g transform='translate(691.01 5483.9)'><g fill='#{hex-color($solar-recycling)}'/></g><g transform='translate(30.324 2009.8)'><g fill='#{hex-color($solar-recycling)}'><g fill='#{hex-color($solar-recycling)}'><path d='m99.777 446.09c-6.699 12.031-12.031 30.133-12.031 41.539 0 2.648 0 6.016 0.656 10.688l-83.726-143.99c-2.68-4.672-4.676-11.375-4.676-17.414 0-6.047 1.996-13.398 4.676-18.078l11.377-17.243-44.871-25.43 146.02-2.703 70.984 127.91-45.527-26.117zm64.313-405.86c12.715-22.125 33.496-34.18 58.926-34.18 27.48 0 48.918 12.742 64.312 38.828l22.777 38.172-107.87 189.75-127.91-74.352zm9.351 521.72c-38.172 0-69.645-31.477-69.645-69.648 0-10.719 4.703-28.82 11.402-40.195l21.41-38.172h216.39v148.02h-179.56zm127.29-525.76c-10.036-17.391-23.434-29.477-39.512-36.18h164.75c14.738 0 26.113 6.047 32.84 17.445l13.551 2.912 44.191-26.141-71.016 127.28-145.3-2.047 44.871-25.43zm253.86 379.09c20.07 0 36.832-5.359 50.887-16.055l-83.07 144.65c-6.699 11.375-18.73 18.078-32.789 18.078h-20.759v51.57l-75.004-125.23 75.004-125.27v52.258h85.731zm64.258-120.56c6.043 10.719 9.406 22.094 9.406 34.156 0 24.117-15.422 49.57-36.832 61.602-10.062 5.391-24.145 8.75-38.172 8.75h-44.242l-116.92-210.98 127.92-73.008z' fill='#{hex-color($solar-recycling)}'/></g></g></g><g transform='translate(27.915 2669.7)'><g fill='#{hex-color($solar-recycling)}'><g fill='#{hex-color($solar-recycling)}'><path d='m99.777 446.09c-6.699 12.031-12.031 30.133-12.031 41.539 0 2.648 0 6.016 0.656 10.688l-83.726-143.99c-2.68-4.672-4.676-11.375-4.676-17.414 0-6.047 1.996-13.398 4.676-18.078l40.195-70.328-44.871-25.43 146.02-2.703 70.984 127.91-45.527-26.117zm64.313-405.86c12.715-22.125 33.496-34.18 58.926-34.18 27.48 0 48.918 12.742 64.312 38.828l22.777 38.172-79.051 136.66-127.91-74.352zm9.351 521.72c-38.172 0-69.645-31.477-69.645-69.648 0-10.719 4.703-28.82 11.402-40.195l21.41-38.172h158.76v148.02h-121.92zm127.29-525.76c-10.036-17.391-23.434-29.477-39.512-36.18h164.75c14.738 0 26.113 6.047 32.84 17.445l40.852 69.648 44.191-26.141-71.016 127.28-145.3-2.047 44.871-25.43zm253.86 379.09c20.07 0 36.832-5.359 50.887-16.055l-83.07 144.65c-6.699 11.375-18.73 18.078-32.789 18.078h-78.395v51.57l-75.004-125.23 75.004-125.27v52.258h143.37zm64.258-120.56c6.043 10.719 9.406 22.094 9.406 34.156 0 24.117-15.422 49.57-36.832 61.602-10.062 5.391-24.145 8.75-38.172 8.75h-44.242l-79-136.66 127.92-73.008z' fill='#{hex-color($solar-recycling)}'/></g></g></g></g><g transform='translate(29.186 687.36)' fill='#{hex-color($action-button-color)}'><g fill='#{hex-color($action-button-color)}'><g fill='#{hex-color($action-button-color)}'><path d='m99.777 446.09c-6.699 12.031-12.031 30.133-12.031 41.539 0 2.648 0 6.016 0.656 10.688l-83.726-143.99c-2.68-4.672-4.676-11.375-4.676-17.414 0-6.047 1.996-13.398 4.676-18.078l76.66-134.68-44.871-25.43 146.02-2.703 70.984 127.91-45.527-26.117zm64.313-405.86c12.715-22.125 33.496-34.18 58.926-34.18 27.48 0 48.918 12.742 64.312 38.828l22.777 38.172-42.586 72.315-127.91-74.352zm9.351 521.72c-38.172 0-69.645-31.477-69.645-69.648 0-10.719 4.703-28.82 11.402-40.195l21.41-38.172h111.57v148.02h-74.736zm127.29-525.76c-10.036-17.391-23.434-29.477-39.512-36.18h164.75c14.738 0 26.113 6.047 32.84 17.445l73.027 129.71 44.191-26.141-71.016 127.28-145.3-2.047 44.871-25.43zm253.86 379.09c20.07 0 36.832-5.359 50.887-16.055l-83.07 144.65c-6.699 11.375-18.73 18.078-32.789 18.078h-125.58v51.57l-75.004-125.23 75.004-125.27v52.258h190.56zm64.258-120.56c6.043 10.719 9.406 22.094 9.406 34.156 0 24.117-15.422 49.57-36.832 61.602-10.062 5.391-24.145 8.75-38.172 8.75h-44.242l-46.825-76.605 127.92-73.008z' fill='#{hex-color($solar-recycling)}'/></g></g></g></g></svg>") no-repeat;
+    width: 19px;
+    height: 18.5px;
+    transition: background-position .6s steps(4);
+  }
+
+  &.reblogPrivate:hover i.fa-retweet,
+  &.reblogPrivate i.fa-retweet {
+    background: url("data:image/svg+xml;utf8,<svg width='19' height='19' enable-background='new 0 0 628.254 613.516' version='1.1' viewBox='0 0 630.25 615.51' xml:space='preserve' xmlns='http://www.w3.org/2000/svg'><defs><mask id='mask-powermask-path-effect929' maskUnits='userSpaceOnUse'><path id='mask-powermask-path-effect929_box' d='m-1-1h630.25v615.51h-630.25z' fill='%23fff'/><g transform='matrix(1.8197 0 0 1.8197 467.68 255.09)' style=''><rect x='-219.52' y='91.198' width='114.31' height='71.892' fill='none' stroke='%23000' stroke-width='34.869'/><path d='m-211.31 76.273c0-2.2289-0.45078-28.654-0.20046-30.833 1.9095-16.621 12.096-32.827 25.749-41.276 15.443-9.5572 34.438-9.4492 49.787 0.28295 13.762 8.7258 22.704 23.991 24.271 40.915 0.18075 1.9519-0.0259 28.68-0.0453 30.668' fill='none' stroke='%23000' stroke-width='28.408'/></g></mask></defs><g transform='translate(-28.186 -5.0669)'><g transform='translate(29.186 6.0669)' fill='#{hex-color($action-button-color)}' mask='url(%23mask-powermask-path-effect929)'><path d='m99.777 446.09c-6.699 12.031-12.031 30.133-12.031 41.539 0 2.648 0 6.016 0.656 10.688l-83.726-143.99c-2.68-4.672-4.676-11.375-4.676-17.414 0-6.047 1.996-13.398 4.676-18.078l40.195-70.328-44.871-25.43 146.02-2.703 70.984 127.91-45.527-26.117zm64.313-405.86c12.715-22.125 33.496-34.18 58.926-34.18 27.48 0 48.918 12.742 64.312 38.828l22.777 38.172-79.051 136.66-127.91-74.352zm9.351 521.72c-38.172 0-69.645-31.477-69.645-69.648 0-10.719 4.703-28.82 11.402-40.195l21.41-38.172h158.76v148.02h-121.92zm127.29-525.76c-10.036-17.391-23.434-29.477-39.512-36.18h164.75c14.738 0 26.113 6.047 32.84 17.445l40.852 69.648 44.191-26.141-71.016 127.28-145.3-2.047 44.871-25.43zm253.86 379.09c20.07 0 36.832-5.359 50.887-16.055l-83.07 144.65c-6.699 11.375-18.73 18.078-32.789 18.078h-78.395v51.57l-75.004-125.23 75.004-125.27v52.258h143.37zm64.258-120.56c6.043 10.719 9.406 22.094 9.406 34.156 0 24.117-15.422 49.57-36.832 61.602-10.062 5.391-24.145 8.75-38.172 8.75h-44.242l-79-136.66 127.92-73.008z' fill='#{hex-color($action-button-color)}' mask='none'/></g><path d='m28.186 5.0669h630.25v615.51h-630.25z' fill='none'/><g transform='translate(691.01 5483.9)' fill='%23009700'><g fill='%23009700'/></g><g transform='matrix(1.8197 0 0 1.8197 537.9 285.64)' stroke='#{hex-color($action-button-color)}'><rect x='-219.52' y='91.198' width='114.31' height='71.892' fill='#{hex-color($action-button-color)}' stroke-width='34.869'/><path d='m-211.31 76.273c0-2.2289-0.45078-28.654-0.20046-30.833 1.9095-16.621 12.096-32.827 25.749-41.276 15.443-9.5572 34.438-9.4492 49.787 0.28295 13.762 8.7258 22.704 23.991 24.271 40.915 0.18075 1.9519-0.0259 28.68-0.0453 30.668' fill='none' stroke-width='28.408'/></g></g></svg>");
+  }
+
+  &.reblogPrivate.active i.fa-retweet,
+  &:hover.reblogPrivate.active i.fa-retweet {
+    background-position: bottom;
+  }
+
+  &:hover i.fa-star,
+  i.fa-star {
+    background: url("data:image/svg+xml;utf8,<svg width='6.299mm' height='28.591mm' version='1.1' viewBox='0 0 4.9132 22.301' xmlns='http://www.w3.org/2000/svg'><g transform='translate(-72.638 -93.656)'><g transform='translate(.47668 5.3239)'><path transform='matrix(.04592 .26057 -.26057 .04592 76.244 95.826)' d='m-0.09375 11.156-3.1493-2.2515-1.9189 3.3623-0.63485-3.819-3.7344 1.0206 2.2515-3.1493-3.3623-1.9189 3.819-0.63485-1.0206-3.7344 3.1493 2.2515 1.9189-3.3623 0.63485 3.819 3.7344-1.0206-2.2515 3.1493 3.3623 1.9189-3.819 0.63485z' fill='%23ffb271'/><g fill='%23ff8e7c'><path d='m74.63 92.871-0.44504 1.5146 0.31204 0.77128 0.57329-0.77889z' stroke-width='1.0644'/><path d='m74.205 95.719 0.41002-0.27757 0.41153 0.30391-0.37017 1.3919' stroke-width='1.1036'/><path d='m72.271 95.052 1.6527 0.40144 0.90943-0.37981-0.85337-0.45872z'/><path d='m76.865 95.04-1.5894-0.43145-0.8651 0.42185 0.90715 0.47274z'/></g><circle cx='74.637' cy='95.054' r='.86816' fill='%23f9e59a' stroke-width='.20427'/></g><g transform='translate(.025125 .41834)'><g transform='translate(.49227 .42869)'><path transform='matrix(.04592 .26057 -.26057 .04592 76.244 95.826)' d='m-0.09375 11.156-3.0678-2.4605c-1.0077-0.58067-1.6372-0.66239-2.5754-0.46052l-3.7943 1.2335 2.2515-3.1493-3.3623-1.9189 3.819-0.63485-1.0206-3.7344 3.1493 2.2515c1.573 0.58073 1.5464 0.26082 2.5538 0.4567l3.7344-1.0206-2.2515 3.1493c-1.1607 1.0484-0.49695 1.609-0.4567 2.5538z' fill='#{hex-color($action-button-color)}'/><g fill='#{hex-color($action-button-color-darker)}'><path d='m74.612 93.227-0.34726 1.0005 0.25632 0.51263 0.43822-0.52917z'/><path d='m74.249 95.856 0.27765-0.50436 0.42515 0.5209-0.34112 1.0253'/><path d='m72.802 95.054 1.0348 0.36811 0.59405-0.35157-0.59987-0.37789z'/><path d='m76.403 95.054-0.95912-0.339-0.66973 0.32246 0.66973 0.37207z'/></g><circle cx='74.637' cy='95.054' r='.86816' fill='#{hex-color($action-button-color-lighter)}' stroke-width='.20427'/></g></g><g transform='translate(.51611 14.249)'><path transform='matrix(.04592 .26057 -.26057 .04592 76.244 95.826)' d='m2.4281 13.801-6.2849-5.3807-1.3051 3.847-0.17157-4.0062-6.9762 2.8655 5.1741-5.3193-3.5064-1.4065 3.8314-0.094818-3.2175-7.6125 5.8984 5.7429 1.3543-3.5156 0.19915 3.8497 7.3536-2.8701-5.754 5.5572 3.6813 1.3297-4.0124-0.09844z' fill='%23ffb271'/><g fill='%23ff8e7c'><path d='m74.612 93.227-0.34726 1.0005 0.25632 0.51263 0.43822-0.52917z'/><path d='m74.249 95.856 0.27765-0.50436 0.42515 0.5209-0.34112 1.0253'/><path d='m72.802 95.054 0.95912 0.339 0.66973-0.32246-0.66973-0.37207z'/><path d='m76.403 95.054-0.95912-0.339-0.66973 0.32246 0.66973 0.37207z'/></g><circle cx='74.637' cy='95.054' r='.86816' fill='%23f9e59a' stroke-width='.20427'/></g><g transform='translate(.51271 18.705)'><path transform='matrix(.04592 .26057 -.26057 .04592 76.244 95.826)' d='m-0.09375 11.156-3.1493-2.2515-1.9189 3.3623-0.63485-3.819-3.7344 1.0206 2.2515-3.1493-3.3623-1.9189 3.819-0.63485-1.0206-3.7344 3.1493 2.2515 1.9189-3.3623 0.63485 3.819 3.7344-1.0206-2.2515 3.1493 3.3623 1.9189-3.819 0.63485z' fill='%23ffb271'/><g fill='%23ff8e7c'><path d='m74.612 93.227-0.34726 1.0005 0.25632 0.51263 0.43822-0.52917z'/><path d='m74.249 95.856 0.27765-0.50436 0.42515 0.5209-0.34112 1.0253'/><path d='m72.802 95.054 0.95912 0.339 0.66973-0.32246-0.66973-0.37207z'/><path d='m76.403 95.054-0.95912-0.339-0.66973 0.32246 0.66973 0.37207z'/></g><circle cx='74.637' cy='95.054' r='.86816' fill='%23f9e59a' stroke-width='.20427'/></g><g transform='translate(.50609 9.7746)'><path transform='matrix(.04592 .26057 -.26057 .04592 76.244 95.826)' d='m-0.09375 11.156-3.1493-2.2515-1.9189 3.3623-0.63485-3.819-3.7344 1.0206 2.2515-3.1493-3.3623-1.9189 3.819-0.63485-1.0206-3.7344 3.1493 2.2515 1.9189-3.3623 0.63485 3.819 3.7344-1.0206-2.2515 3.1493 3.3623 1.9189-3.819 0.63485z' fill='%23ffb271'/><g fill='%23ff8e7c'><path d='m74.612 93.227-0.34726 1.0005 0.25632 0.51263 0.43822-0.52917z'/><path d='m74.249 95.856 0.27765-0.50436 0.42515 0.5209-0.34112 1.0428'/><path d='m72.802 95.054 0.95912 0.339 0.66973-0.32246-0.66973-0.37207z'/><path d='m76.403 95.054-0.95912-0.339-0.66973 0.32246 0.66973 0.37207z'/></g><circle cx='74.637' cy='95.054' r='.86816' fill='%23f9e59a' stroke-width='.20427'/></g></g></svg>") no-repeat top;
+    height: 21.4px;
+    margin: 1px 0;
+    transition: background-position .6s steps(4);
+  }
+
+  &.active i.fa-star {
+    background-position: 50% 100%;
+  }
+
+  /* Remove the font-awesome icon */
+  .fa-star::before {
+    content: "";
+  }
+
+  &.disabled {
+    i.fa-retweet,
+    &:hover i.fa-retweet {
+      background-image: url("data:image/svg+xml;utf8,<svg width='18.7' height='18.7' enable-background='new 0 0 628.254 613.516' version='1.1' viewBox='0 0 628.25 613.51' xml:space='preserve' xmlns='http://www.w3.org/2000/svg'><defs><mask id='mask5629' maskUnits='userSpaceOnUse'><rect x='-156.41' y='-66.33' width='941.77' height='797.17' fill='#{hex-color($white)}' stroke-width='35'/><g transform='matrix(.33928 .4906 -.82248 .5688 533.91 -40.843)' fill='#{hex-color($solar-disabled)}'><rect x='285' y='13.387' width='105.67' height='767.51' fill='#{hex-color($black)}' stroke-width='35'/></g></mask></defs><g transform='translate(-29.186 -6.0669)'><g transform='translate(29.186 6.0669)'><g fill='#{hex-color($solar-disabled)}' mask='url(%23mask5629)'><g fill='#{hex-color($solar-disabled)}' mask='none'><path d='m99.777 446.09c-6.699 12.031-12.031 30.133-12.031 41.539 0 2.648 0 6.016 0.656 10.688l-83.726-143.99c-2.68-4.672-4.676-11.375-4.676-17.414 0-6.047 1.996-13.398 4.676-18.078l40.195-70.328-44.871-25.43 146.02-2.703 70.984 127.91-45.527-26.117zm64.313-405.86c12.715-22.125 33.496-34.18 58.926-34.18 27.48 0 48.918 12.742 64.312 38.828l22.777 38.172-79.051 136.66-127.91-74.352zm9.351 521.72c-38.172 0-69.645-31.477-69.645-69.648 0-10.719 4.703-28.82 11.402-40.195l21.41-38.172h158.76v148.02h-121.92zm127.29-525.76c-10.036-17.391-23.434-29.477-39.512-36.18h164.75c14.738 0 26.113 6.047 32.84 17.445l40.852 69.648 44.191-26.141-71.016 127.28-145.3-2.047 44.871-25.43zm253.86 379.09c20.07 0 36.832-5.359 50.887-16.055l-83.07 144.65c-6.699 11.375-18.73 18.078-32.789 18.078h-78.395v51.57l-75.004-125.23 75.004-125.27v52.258h143.37zm64.258-120.56c6.043 10.719 9.406 22.094 9.406 34.156 0 24.117-15.422 49.57-36.832 61.602-10.062 5.391-24.145 8.75-38.172 8.75h-44.242l-79-136.66 127.92-73.008z' fill='#{hex-color($solar-disabled)}'/></g></g><rect transform='matrix(.57488 .81824 -.81993 .57247 0 0)' x='409.24' y='-521.16' width='66.549' height='882.4' ry='6.6154' fill='#{hex-color($solar-disabled)}' stroke-width='46.779'/></g><g transform='translate(691.01 5483.9)'></g></g></svg>");
+    }
+  }
+
+  &.active i.fa-retweet {
+    background-position: 50% 100%;
+  }
+}
+
+.no-reduce-motion .icon-button.star-icon {
+  &.deactivate > .fa-star,
+  &.activate > .fa-star {
+    animation: none;
+    -webkit-animation: none;
+  }
+}
+
+.detailed-status__link {
+  .fa-star {
+    background: url("data:image/svg+xml;utf8,<svg width='5mm' height='20mm' version='1.1' viewBox='0 0 4.9132 22.301' xmlns='http://www.w3.org/2000/svg'><g transform='translate(-72.638 -93.656)'><g transform='translate(.47668 5.3239)'><path transform='matrix(.04592 .26057 -.26057 .04592 76.244 95.826)' d='m-0.09375 11.156-3.1493-2.2515-1.9189 3.3623-0.63485-3.819-3.7344 1.0206 2.2515-3.1493-3.3623-1.9189 3.819-0.63485-1.0206-3.7344 3.1493 2.2515 1.9189-3.3623 0.63485 3.819 3.7344-1.0206-2.2515 3.1493 3.3623 1.9189-3.819 0.63485z' fill='%23ffb271'/><g fill='%23ff8e7c'><path d='m74.63 92.871-0.44504 1.5146 0.31204 0.77128 0.57329-0.77889z' stroke-width='1.0644'/><path d='m74.205 95.719 0.41002-0.27757 0.41153 0.30391-0.37017 1.3919' stroke-width='1.1036'/><path d='m72.271 95.052 1.6527 0.40144 0.90943-0.37981-0.85337-0.45872z'/><path d='m76.865 95.04-1.5894-0.43145-0.8651 0.42185 0.90715 0.47274z'/></g><circle cx='74.637' cy='95.054' r='.86816' fill='%23f9e59a' stroke-width='.20427'/></g><g transform='translate(.025125 .41834)'><g transform='translate(.49227 .42869)'><path transform='matrix(.04592 .26057 -.26057 .04592 76.244 95.826)' d='m-0.09375 11.156-3.0678-2.4605c-1.0077-0.58067-1.6372-0.66239-2.5754-0.46052l-3.7943 1.2335 2.2515-3.1493-3.3623-1.9189 3.819-0.63485-1.0206-3.7344 3.1493 2.2515c1.573 0.58073 1.5464 0.26082 2.5538 0.4567l3.7344-1.0206-2.2515 3.1493c-1.1607 1.0484-0.49695 1.609-0.4567 2.5538z' fill='#{hex-color($action-button-color)}'/><g fill='#{hex-color($action-button-color-darker)}'><path d='m74.612 93.227-0.34726 1.0005 0.25632 0.51263 0.43822-0.52917z'/><path d='m74.249 95.856 0.27765-0.50436 0.42515 0.5209-0.34112 1.0253'/><path d='m72.802 95.054 1.0348 0.36811 0.59405-0.35157-0.59987-0.37789z'/><path d='m76.403 95.054-0.95912-0.339-0.66973 0.32246 0.66973 0.37207z'/></g><circle cx='74.637' cy='95.054' r='.86816' fill='#{hex-color($action-button-color-lighter)}' stroke-width='.20427'/></g></g><g transform='translate(.51611 14.249)'><path transform='matrix(.04592 .26057 -.26057 .04592 76.244 95.826)' d='m2.4281 13.801-6.2849-5.3807-1.3051 3.847-0.17157-4.0062-6.9762 2.8655 5.1741-5.3193-3.5064-1.4065 3.8314-0.094818-3.2175-7.6125 5.8984 5.7429 1.3543-3.5156 0.19915 3.8497 7.3536-2.8701-5.754 5.5572 3.6813 1.3297-4.0124-0.09844z' fill='%23ffb271'/><g fill='%23ff8e7c'><path d='m74.612 93.227-0.34726 1.0005 0.25632 0.51263 0.43822-0.52917z'/><path d='m74.249 95.856 0.27765-0.50436 0.42515 0.5209-0.34112 1.0253'/><path d='m72.802 95.054 0.95912 0.339 0.66973-0.32246-0.66973-0.37207z'/><path d='m76.403 95.054-0.95912-0.339-0.66973 0.32246 0.66973 0.37207z'/></g><circle cx='74.637' cy='95.054' r='.86816' fill='%23f9e59a' stroke-width='.20427'/></g><g transform='translate(.51271 18.705)'><path transform='matrix(.04592 .26057 -.26057 .04592 76.244 95.826)' d='m-0.09375 11.156-3.1493-2.2515-1.9189 3.3623-0.63485-3.819-3.7344 1.0206 2.2515-3.1493-3.3623-1.9189 3.819-0.63485-1.0206-3.7344 3.1493 2.2515 1.9189-3.3623 0.63485 3.819 3.7344-1.0206-2.2515 3.1493 3.3623 1.9189-3.819 0.63485z' fill='%23ffb271'/><g fill='%23ff8e7c'><path d='m74.612 93.227-0.34726 1.0005 0.25632 0.51263 0.43822-0.52917z'/><path d='m74.249 95.856 0.27765-0.50436 0.42515 0.5209-0.34112 1.0253'/><path d='m72.802 95.054 0.95912 0.339 0.66973-0.32246-0.66973-0.37207z'/><path d='m76.403 95.054-0.95912-0.339-0.66973 0.32246 0.66973 0.37207z'/></g><circle cx='74.637' cy='95.054' r='.86816' fill='%23f9e59a' stroke-width='.20427'/></g><g transform='translate(.50609 9.7746)'><path transform='matrix(.04592 .26057 -.26057 .04592 76.244 95.826)' d='m-0.09375 11.156-3.1493-2.2515-1.9189 3.3623-0.63485-3.819-3.7344 1.0206 2.2515-3.1493-3.3623-1.9189 3.819-0.63485-1.0206-3.7344 3.1493 2.2515 1.9189-3.3623 0.63485 3.819 3.7344-1.0206-2.2515 3.1493 3.3623 1.9189-3.819 0.63485z' fill='%23ffb271'/><g fill='%23ff8e7c'><path d='m74.612 93.227-0.34726 1.0005 0.25632 0.51263 0.43822-0.52917z'/><path d='m74.249 95.856 0.27765-0.50436 0.42515 0.5209-0.34112 1.0428'/><path d='m72.802 95.054 0.95912 0.339 0.66973-0.32246-0.66973-0.37207z'/><path d='m76.403 95.054-0.95912-0.339-0.66973 0.32246 0.66973 0.37207z'/></g><circle cx='74.637' cy='95.054' r='.86816' fill='%23f9e59a' stroke-width='.20427'/></g></g></svg>") no-repeat top;
+    width: 12px;
+    height: 13px;
+    margin-bottom: -1px;
+
+    &::before {
+      content: "";
+    }
+  }
+
+  .fa-retweet {
+      background: url("data:image/svg+xml;utf8,<svg width='3.5mm' height='70' enable-background='new 0 0 628.254 613.516' version='1.1' viewBox='0 0 657.07 3289.9' xml:space='preserve' xmlns='http://www.w3.org/2000/svg'><g transform='translate(-.77735 -6.0669)'><g transform='translate(29.186 28.067)' fill='#{hex-color($action-button-color)}'><g fill='#{hex-color($action-button-color)}'><g fill='#{hex-color($action-button-color)}'><path d='m99.777 446.09c-6.699 12.031-12.031 30.133-12.031 41.539 0 2.648 0 6.016 0.656 10.688l-83.726-143.99c-2.68-4.672-4.676-11.375-4.676-17.414 0-6.047 1.996-13.398 4.676-18.078l40.195-70.328-44.871-25.43 146.02-2.703 70.984 127.91-45.527-26.117zm64.313-405.86c12.715-22.125 33.496-34.18 58.926-34.18 27.48 0 48.918 12.742 64.312 38.828l22.777 38.172-79.051 136.66-127.91-74.352zm9.351 521.72c-38.172 0-69.645-31.477-69.645-69.648 0-10.719 4.703-28.82 11.402-40.195l21.41-38.172h158.76v148.02h-121.92zm127.29-525.76c-10.036-17.391-23.434-29.477-39.512-36.18h164.75c14.738 0 26.113 6.047 32.84 17.445l40.852 69.648 44.191-26.141-71.016 127.28-145.3-2.047 44.871-25.43zm253.86 379.09c20.07 0 36.832-5.359 50.887-16.055l-83.07 144.65c-6.699 11.375-18.73 18.078-32.789 18.078h-78.395v51.57l-75.004-125.23 75.004-125.27v52.258h143.37zm64.258-120.56c6.043 10.719 9.406 22.094 9.406 34.156 0 24.117-15.422 49.57-36.832 61.602-10.062 5.391-24.145 8.75-38.172 8.75h-44.242l-79-136.66 127.92-73.008z' fill='#{hex-color($action-button-color)}'/></g></g></g><g fill='#{hex-color($solar-recycling)}'><g transform='translate(27.733 1347)'><g fill='#{hex-color($solar-recycling)}'><g fill='#{hex-color($solar-recycling)}'><path d='m99.777 446.09c-6.699 12.031-12.031 30.133-12.031 41.539 0 2.648 0 6.016 0.656 10.688l-83.726-143.99c-2.68-4.672-4.676-11.375-4.676-17.414 0-6.047 1.996-13.398 4.676-18.078l97.831-167.4-44.871-25.43 146.02-2.703 70.984 127.91-45.527-26.117zm64.313-405.86c12.715-22.125 33.496-34.18 58.926-34.18 27.48 0 48.918 12.742 64.312 38.828l22.777 38.172-21.415 39.593-127.91-74.352zm9.351 521.72c-38.172 0-69.645-31.477-69.645-69.648 0-10.719 4.703-28.82 11.402-40.195l21.41-38.172 40.453-1.5167v148.02l-3.6203 1.5167zm127.29-525.76c-10.036-17.391-23.434-29.477-39.512-36.18h164.75c14.738 0 26.113 6.047 32.84 17.445l110.62 189.47 44.191-26.141-71.016 127.28-145.3-2.047 44.871-25.43zm253.86 379.09c20.07 0 36.832-5.359 50.887-16.055l-83.07 144.65c-6.699 11.375-18.73 18.078-32.789 18.078l-196.7-1.5167v51.57l-75.004-125.23 75.004-125.27v52.258l261.67 1.5167zm64.258-120.56c6.043 10.719 9.406 22.094 9.406 34.156 0 24.117-15.422 49.57-36.832 61.602-10.062 5.391-24.145 8.75-38.172 8.75h-44.242l-9.2306-16.843 127.92-73.008z' fill='#{hex-color($solar-recycling)}'/></g></g></g><g transform='translate(691.01 5483.9)'><g fill='#{hex-color($solar-recycling)}'/></g><g transform='translate(30.324 2009.8)'><g fill='#{hex-color($solar-recycling)}'><g fill='#{hex-color($solar-recycling)}'><path d='m99.777 446.09c-6.699 12.031-12.031 30.133-12.031 41.539 0 2.648 0 6.016 0.656 10.688l-83.726-143.99c-2.68-4.672-4.676-11.375-4.676-17.414 0-6.047 1.996-13.398 4.676-18.078l11.377-17.243-44.871-25.43 146.02-2.703 70.984 127.91-45.527-26.117zm64.313-405.86c12.715-22.125 33.496-34.18 58.926-34.18 27.48 0 48.918 12.742 64.312 38.828l22.777 38.172-107.87 189.75-127.91-74.352zm9.351 521.72c-38.172 0-69.645-31.477-69.645-69.648 0-10.719 4.703-28.82 11.402-40.195l21.41-38.172h216.39v148.02h-179.56zm127.29-525.76c-10.036-17.391-23.434-29.477-39.512-36.18h164.75c14.738 0 26.113 6.047 32.84 17.445l13.551 2.912 44.191-26.141-71.016 127.28-145.3-2.047 44.871-25.43zm253.86 379.09c20.07 0 36.832-5.359 50.887-16.055l-83.07 144.65c-6.699 11.375-18.73 18.078-32.789 18.078h-20.759v51.57l-75.004-125.23 75.004-125.27v52.258h85.731zm64.258-120.56c6.043 10.719 9.406 22.094 9.406 34.156 0 24.117-15.422 49.57-36.832 61.602-10.062 5.391-24.145 8.75-38.172 8.75h-44.242l-116.92-210.98 127.92-73.008z' fill='#{hex-color($solar-recycling)}'/></g></g></g><g transform='translate(27.915 2669.7)'><g fill='#{hex-color($solar-recycling)}'><g fill='#{hex-color($solar-recycling)}'><path d='m99.777 446.09c-6.699 12.031-12.031 30.133-12.031 41.539 0 2.648 0 6.016 0.656 10.688l-83.726-143.99c-2.68-4.672-4.676-11.375-4.676-17.414 0-6.047 1.996-13.398 4.676-18.078l40.195-70.328-44.871-25.43 146.02-2.703 70.984 127.91-45.527-26.117zm64.313-405.86c12.715-22.125 33.496-34.18 58.926-34.18 27.48 0 48.918 12.742 64.312 38.828l22.777 38.172-79.051 136.66-127.91-74.352zm9.351 521.72c-38.172 0-69.645-31.477-69.645-69.648 0-10.719 4.703-28.82 11.402-40.195l21.41-38.172h158.76v148.02h-121.92zm127.29-525.76c-10.036-17.391-23.434-29.477-39.512-36.18h164.75c14.738 0 26.113 6.047 32.84 17.445l40.852 69.648 44.191-26.141-71.016 127.28-145.3-2.047 44.871-25.43zm253.86 379.09c20.07 0 36.832-5.359 50.887-16.055l-83.07 144.65c-6.699 11.375-18.73 18.078-32.789 18.078h-78.395v51.57l-75.004-125.23 75.004-125.27v52.258h143.37zm64.258-120.56c6.043 10.719 9.406 22.094 9.406 34.156 0 24.117-15.422 49.57-36.832 61.602-10.062 5.391-24.145 8.75-38.172 8.75h-44.242l-79-136.66 127.92-73.008z' fill='#{hex-color($solar-recycling)}'/></g></g></g></g><g transform='translate(29.186 687.36)' fill='#{hex-color($action-button-color)}'><g fill='#{hex-color($action-button-color)}'><g fill='#{hex-color($action-button-color)}'><path d='m99.777 446.09c-6.699 12.031-12.031 30.133-12.031 41.539 0 2.648 0 6.016 0.656 10.688l-83.726-143.99c-2.68-4.672-4.676-11.375-4.676-17.414 0-6.047 1.996-13.398 4.676-18.078l76.66-134.68-44.871-25.43 146.02-2.703 70.984 127.91-45.527-26.117zm64.313-405.86c12.715-22.125 33.496-34.18 58.926-34.18 27.48 0 48.918 12.742 64.312 38.828l22.777 38.172-42.586 72.315-127.91-74.352zm9.351 521.72c-38.172 0-69.645-31.477-69.645-69.648 0-10.719 4.703-28.82 11.402-40.195l21.41-38.172h111.57v148.02h-74.736zm127.29-525.76c-10.036-17.391-23.434-29.477-39.512-36.18h164.75c14.738 0 26.113 6.047 32.84 17.445l73.027 129.71 44.191-26.141-71.016 127.28-145.3-2.047 44.871-25.43zm253.86 379.09c20.07 0 36.832-5.359 50.887-16.055l-83.07 144.65c-6.699 11.375-18.73 18.078-32.789 18.078h-125.58v51.57l-75.004-125.23 75.004-125.27v52.258h190.56zm64.258-120.56c6.043 10.719 9.406 22.094 9.406 34.156 0 24.117-15.422 49.57-36.832 61.602-10.062 5.391-24.145 8.75-38.172 8.75h-44.242l-46.825-76.605 127.92-73.008z' fill='#{hex-color($solar-recycling)}'/></g></g></g></g></svg>") no-repeat;
+      width: 13px;
+      height: 14px;
+      margin-bottom: -1px;
+
+      &::before {
+        content: "";
+      }
+   }
+}
+
+.drawer .drawer__inner {
+  background: lighten($ui-base-color, 13%) url("data:image/svg+xml;utf8,<svg width='391.91mm' height='62.185mm' version='1.1' viewBox='0 0 391.91 62.185' xmlns='http://www.w3.org/2000/svg'><defs><clipPath id='clipPath1458'><g display='none'><rect x='20.515' y='65.617' width='3.914' height='73.915' d='m 20.514772,65.61702 h 3.91404 v 73.91547 h -3.91404 z' stroke-width='7.5232'/><rect x='60.408' y='65.617' width='3.914' height='73.915' d='m 60.408058,65.61702 h 3.91404 v 73.91547 h -3.91404 z' stroke-width='7.5232'/><rect x='33.813' y='65.617' width='3.914' height='73.915' d='m 33.812534,65.61702 h 3.91404 v 73.91547 h -3.91404 z' stroke-width='7.5232'/><rect x='47.11' y='65.617' width='3.914' height='73.915' d='m 47.110298,65.61702 h 3.91404 v 73.91547 h -3.91404 z' stroke-width='7.5232'/></g><path class='powerclip' d='m9.7406 68.901h65.371v66.185h-65.371zm10.774-3.2844v73.915h3.914v-73.915zm39.893 0v73.915h3.914v-73.915zm-26.596 0v73.915h3.914v-73.915zm13.298 0v73.915h3.914v-73.915z'/></clipPath><clipPath id='clipath_lpe_path-effect2057'><g display='none'><rect x='20.515' y='65.617' width='3.914' height='73.915' d='m 20.514772,65.61702 h 3.91404 v 73.91547 h -3.91404 z' stroke-width='7.5232'/><rect x='60.408' y='65.617' width='3.914' height='73.915' d='m 60.408058,65.61702 h 3.91404 v 73.91547 h -3.91404 z' stroke-width='7.5232'/><rect x='33.813' y='65.617' width='3.914' height='73.915' d='m 33.812534,65.61702 h 3.91404 v 73.91547 h -3.91404 z' stroke-width='7.5232'/><rect x='47.11' y='65.617' width='3.914' height='73.915' d='m 47.110298,65.61702 h 3.91404 v 73.91547 h -3.91404 z' stroke-width='7.5232'/></g><path class='powerclip' d='m9.7406 68.901h65.371v66.185h-65.371zm10.774-3.2844v73.915h3.914v-73.915zm39.893 0v73.915h3.914v-73.915zm-26.596 0v73.915h3.914v-73.915zm13.298 0v73.915h3.914v-73.915z'/></clipPath><clipPath id='clipath_lpe_path-effect2076'><g display='none'><rect x='20.515' y='65.617' width='3.914' height='73.915' d='m 20.514772,65.61702 h 3.91404 v 73.91547 h -3.91404 z' stroke-width='7.5232'/><rect x='60.408' y='65.617' width='3.914' height='73.915' d='m 60.408058,65.61702 h 3.91404 v 73.91547 h -3.91404 z' stroke-width='7.5232'/><rect x='33.813' y='65.617' width='3.914' height='73.915' d='m 33.812534,65.61702 h 3.91404 v 73.91547 h -3.91404 z' stroke-width='7.5232'/><rect x='47.11' y='65.617' width='3.914' height='73.915' d='m 47.110298,65.61702 h 3.91404 v 73.91547 h -3.91404 z' stroke-width='7.5232'/></g><path class='powerclip' d='m9.7406 68.901h65.371v66.185h-65.371zm10.774-3.2844v73.915h3.914v-73.915zm39.893 0v73.915h3.914v-73.915zm-26.596 0v73.915h3.914v-73.915zm13.298 0v73.915h3.914v-73.915z'/></clipPath><clipPath id='clipath_lpe_path-effect2095'><g display='none'><rect x='20.515' y='65.617' width='3.914' height='73.915' d='m 20.514772,65.61702 h 3.91404 v 73.91547 h -3.91404 z' stroke-width='7.5232'/><rect x='60.408' y='65.617' width='3.914' height='73.915' d='m 60.408058,65.61702 h 3.91404 v 73.91547 h -3.91404 z' stroke-width='7.5232'/><rect x='33.813' y='65.617' width='3.914' height='73.915' d='m 33.812534,65.61702 h 3.91404 v 73.91547 h -3.91404 z' stroke-width='7.5232'/><rect x='47.11' y='65.617' width='3.914' height='73.915' d='m 47.110298,65.61702 h 3.91404 v 73.91547 h -3.91404 z' stroke-width='7.5232'/></g><path class='powerclip' d='m9.7406 68.901h65.371v66.185h-65.371zm10.774-3.2844v73.915h3.914v-73.915zm39.893 0v73.915h3.914v-73.915zm-26.596 0v73.915h3.914v-73.915zm13.298 0v73.915h3.914v-73.915z'/></clipPath><clipPath id='clipath_lpe_path-effect2114'><g display='none'><rect x='20.515' y='65.617' width='3.914' height='73.915' d='m 20.514772,65.61702 h 3.91404 v 73.91547 h -3.91404 z' stroke-width='7.5232'/><rect x='60.408' y='65.617' width='3.914' height='73.915' d='m 60.408058,65.61702 h 3.91404 v 73.91547 h -3.91404 z' stroke-width='7.5232'/><rect x='33.813' y='65.617' width='3.914' height='73.915' d='m 33.812534,65.61702 h 3.91404 v 73.91547 h -3.91404 z' stroke-width='7.5232'/><rect x='47.11' y='65.617' width='3.914' height='73.915' d='m 47.110298,65.61702 h 3.91404 v 73.91547 h -3.91404 z' stroke-width='7.5232'/></g><path class='powerclip' d='m9.7406 68.901h65.371v66.185h-65.371zm10.774-3.2844v73.915h3.914v-73.915zm39.893 0v73.915h3.914v-73.915zm-26.596 0v73.915h3.914v-73.915zm13.298 0v73.915h3.914v-73.915z'/></clipPath><clipPath id='clipath_lpe_path-effect2133'><g display='none'><rect x='20.515' y='65.617' width='3.914' height='73.915' d='m 20.514772,65.61702 h 3.91404 v 73.91547 h -3.91404 z' stroke-width='7.5232'/><rect x='60.408' y='65.617' width='3.914' height='73.915' d='m 60.408058,65.61702 h 3.91404 v 73.91547 h -3.91404 z' stroke-width='7.5232'/><rect x='33.813' y='65.617' width='3.914' height='73.915' d='m 33.812534,65.61702 h 3.91404 v 73.91547 h -3.91404 z' stroke-width='7.5232'/><rect x='47.11' y='65.617' width='3.914' height='73.915' d='m 47.110298,65.61702 h 3.91404 v 73.91547 h -3.91404 z' stroke-width='7.5232'/></g><path class='powerclip' d='m9.7406 68.901h65.371v66.185h-65.371zm10.774-3.2844v73.915h3.914v-73.915zm39.893 0v73.915h3.914v-73.915zm-26.596 0v73.915h3.914v-73.915zm13.298 0v73.915h3.914v-73.915z'/></clipPath></defs><g transform='translate(-4.7406 -70.901)' fill='%2321234a' stroke='%2321234a' stroke-linejoin='bevel' stroke-width='10.222'><path x='19.851646' y='79.012375' width='45.14875' height='45.963497' d='m19.852 79.012h45.149v45.963h-45.149z' clip-path='url(%23clipPath1458)' style='paint-order:normal'/><path transform='translate(63.308)' x='19.851646' y='79.012375' width='45.14875' height='45.963497' d='m19.852 79.012h45.149v45.963h-45.149z' clip-path='url(%23clipath_lpe_path-effect2057)' style='paint-order:normal'/><path transform='translate(126.62)' x='19.851646' y='79.012375' width='45.14875' height='45.963497' d='m19.852 79.012h45.149v45.963h-45.149z' clip-path='url(%23clipath_lpe_path-effect2076)' style='paint-order:normal'/><path transform='translate(189.92)' x='19.851646' y='79.012375' width='45.14875' height='45.963497' d='m19.852 79.012h45.149v45.963h-45.149z' clip-path='url(%23clipath_lpe_path-effect2095)' style='paint-order:normal'/><path transform='translate(253.23)' x='19.851646' y='79.012375' width='45.14875' height='45.963497' d='m19.852 79.012h45.149v45.963h-45.149z' clip-path='url(%23clipath_lpe_path-effect2114)' style='paint-order:normal'/><path transform='translate(316.54)' x='19.851646' y='79.012375' width='45.14875' height='45.963497' d='m19.852 79.012h45.149v45.963h-45.149z' clip-path='url(%23clipath_lpe_path-effect2133)' style='paint-order:normal'/></g></svg>") 0 9px;
+  background-size: 100%;
+
+  &.darker {
+    background: $ui-base-color;
+  }
+}
+
+.drawer__inner__mastodon {
+  background-color: transparent !important;
+}
+
+.single-column,
+.auto-columns {
+  .column-header {
+    background: lighten($ui-base-color, 4%) url("data:image/svg+xml;utf8,<svg width='61.371mm' height='62.185mm' version='1.1' viewBox='0 0 61.371 62.185' xmlns='http://www.w3.org/2000/svg'><defs><clipPath id='clipPath1458'><g display='none'><rect x='20.515' y='65.617' width='3.914' height='73.915' d='m 20.514772,65.61702 h 3.91404 v 73.91547 h -3.91404 z' stroke-width='7.5232'/><rect x='60.408' y='65.617' width='3.914' height='73.915' d='m 60.408058,65.61702 h 3.91404 v 73.91547 h -3.91404 z' stroke-width='7.5232'/><rect x='33.813' y='65.617' width='3.914' height='73.915' d='m 33.812534,65.61702 h 3.91404 v 73.91547 h -3.91404 z' stroke-width='7.5232'/><rect x='47.11' y='65.617' width='3.914' height='73.915' d='m 47.110298,65.61702 h 3.91404 v 73.91547 h -3.91404 z' stroke-width='7.5232'/></g><path class='powerclip' d='m9.7406 68.901h65.371v66.185h-65.371zm10.774-3.2844v73.915h3.914v-73.915zm39.893 0v73.915h3.914v-73.915zm-26.596 0v73.915h3.914v-73.915zm13.298 0v73.915h3.914v-73.915z'/></clipPath></defs><g transform='translate(-11.741 -70.901)'><path x='19.851646' y='79.012375' width='45.14875' height='45.963497' d='m19.852 79.012h45.149v45.963h-45.149z' clip-path='url(%23clipPath1458)' fill='#{hex-color($solar-panel-header)}' stroke='#{hex-color($solar-panel-header)}' stroke-linejoin='bevel' stroke-width='10.222' style='paint-order:normal'/></g></svg>") 2px 2px;
+    background-size: 44.25px;
+  }
+
+  .column-header__button,
+  .column-header__back-button {
+    background-color: transparent;
+  }
+}
diff --git a/app/javascript/skins/blobfox/solarpunk/names.yml b/app/javascript/skins/blobfox/solarpunk/names.yml
new file mode 100644
index 00000000000000..dd6e2af12deb2b
--- /dev/null
+++ b/app/javascript/skins/blobfox/solarpunk/names.yml
@@ -0,0 +1,4 @@
+en:
+  skins:
+    blobfox:
+      solarpunk: Solarpunk
diff --git a/app/javascript/skins/blobfox/solarpunk/variables.scss b/app/javascript/skins/blobfox/solarpunk/variables.scss
new file mode 100644
index 00000000000000..038f6d15bfca87
--- /dev/null
+++ b/app/javascript/skins/blobfox/solarpunk/variables.scss
@@ -0,0 +1,75 @@
+$black: #000000;
+$white: #ffffff;
+$success-green: #79bd9a;
+$error-red: #df405a;
+$warning-red: #ff5050;
+$gold-star: #ca8f04;
+
+// Values from the classic Mastodon UI
+$classic-base-color: #282c37;         // Midnight Express
+$classic-primary-color: #9baec8;      // Echo Blue
+$classic-secondary-color: #d9e1e8;    // Pattens Blue
+$classic-highlight-color: #2b90d9;    // Summer Sky
+
+// our theme customizations
+$solar-highlight-color: #378d2a;
+$solar-bg: #060e0c; // dark green
+$solar-recycling: #009700;
+$solar-panel-header: #202c4d;
+
+// Variables for defaults in UI
+$base-shadow-color: $black !default;
+$base-overlay-background: $black !default;
+$base-border-color: $white !default;
+$simple-background-color: $white !default;
+$valid-value-color: $success-green !default;
+$error-value-color: $error-red !default;
+
+$ui-base-color: #161c2c;
+$ui-base-lighter-color: #5370a2 !default; // Lighter darkest
+$ui-primary-color: $classic-primary-color !default;
+$ui-secondary-color: $classic-secondary-color !default;
+$ui-highlight-color: $classic-highlight-color !default;
+
+$ui-highlight-color: $solar-highlight-color;
+
+
+// Variables for texts
+$primary-text-color: $white !default;
+$darker-text-color: $ui-primary-color !default;
+$dark-text-color: $ui-base-lighter-color !default;
+$secondary-text-color: $ui-secondary-color !default;
+$highlight-text-color: $ui-highlight-color !default;
+$action-button-color: $ui-base-lighter-color !default;
+$action-button-color-lighter: lighten($ui-base-lighter-color, 10%) !default;
+$action-button-color-darker: darken($ui-base-lighter-color, 5%) !default;
+
+$passive-text-color: $gold-star !default;
+$active-passive-text-color: $success-green !default;
+// For texts on inverted backgrounds
+$inverted-text-color: $ui-base-color !default;
+$lighter-text-color: $ui-base-lighter-color !default;
+$light-text-color: $ui-primary-color !default;
+
+$solar-disabled: darken($action-button-color, 10%) !default;
+
+// Language codes that uses CJK fonts
+$cjk-langs: ja, ko, zh-CN, zh-HK, zh-TW;
+
+// Variables for components
+$media-modal-media-max-width: 100%;
+// put margins on top and bottom of image to avoid the screen covered by image.
+$media-modal-media-max-height: 80%;
+
+$no-gap-breakpoint: 415px;
+
+$font-sans-serif: 'mastodon-font-sans-serif' !default;
+$font-display: 'mastodon-font-display' !default;
+$font-monospace: 'mastodon-font-monospace' !default;
+
+// Avatar border size (8% default, 100% for rounded avatars)
+$ui-avatar-border-size: 8%;
+
+// More variables
+$dismiss-overlay-width: 4rem;
+