about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.gitmodules3
-rw-r--r--.travis.yml6
-rw-r--r--Dockerfile4
-rw-r--r--Gemfile1
-rw-r--r--Gemfile.lock3
-rw-r--r--app/controllers/about_controller.rb5
-rw-r--r--app/controllers/accounts_controller.rb1
-rw-r--r--app/controllers/admin/base_controller.rb7
-rw-r--r--app/controllers/api/v1/accounts/search_controller.rb9
-rw-r--r--app/controllers/api/v1/accounts_controller.rb2
-rw-r--r--app/controllers/api/v1/lists/accounts_controller.rb18
-rw-r--r--app/controllers/application_controller.rb82
-rw-r--r--app/controllers/auth/confirmations_controller.rb7
-rw-r--r--app/controllers/auth/passwords_controller.rb5
-rw-r--r--app/controllers/auth/registrations_controller.rb5
-rw-r--r--app/controllers/auth/sessions_controller.rb5
-rw-r--r--app/controllers/authorize_follows_controller.rb5
-rw-r--r--app/controllers/follower_accounts_controller.rb4
-rw-r--r--app/controllers/following_accounts_controller.rb4
-rw-r--r--app/controllers/home_controller.rb5
-rw-r--r--app/controllers/invites_controller.rb5
-rw-r--r--app/controllers/remote_follow_controller.rb5
-rw-r--r--app/controllers/settings/applications_controller.rb4
-rw-r--r--app/controllers/settings/base_controller.rb12
-rw-r--r--app/controllers/settings/deletes_controller.rb6
-rw-r--r--app/controllers/settings/exports_controller.rb6
-rw-r--r--app/controllers/settings/follower_domains_controller.rb6
-rw-r--r--app/controllers/settings/imports_controller.rb5
-rw-r--r--app/controllers/settings/keyword_mutes_controller.rb5
-rw-r--r--app/controllers/settings/notifications_controller.rb6
-rw-r--r--app/controllers/settings/preferences_controller.rb9
-rw-r--r--app/controllers/settings/profiles_controller.rb5
-rw-r--r--app/controllers/settings/sessions_controller.rb1
-rw-r--r--app/controllers/settings/two_factor_authentications_controller.rb5
-rw-r--r--app/controllers/shares_controller.rb5
-rw-r--r--app/controllers/statuses_controller.rb2
-rw-r--r--app/controllers/stream_entries_controller.rb1
-rw-r--r--app/controllers/tags_controller.rb1
-rw-r--r--app/controllers/well_known/webfinger_controller.rb19
-rw-r--r--app/helpers/admin/filter_helper.rb2
-rw-r--r--app/helpers/application_helper.rb2
-rw-r--r--app/helpers/routing_helper.rb2
-rw-r--r--app/javascript/core/admin.js (renamed from app/javascript/packs/admin.js)2
-rw-r--r--app/javascript/core/common.js8
-rw-r--r--app/javascript/core/embed.js23
-rw-r--r--app/javascript/core/public.js25
-rw-r--r--app/javascript/core/settings.js43
-rw-r--r--app/javascript/core/theme.yml16
-rw-r--r--app/javascript/flavours/glitch/actions/accounts.js (renamed from app/javascript/themes/glitch/actions/accounts.js)2
-rw-r--r--app/javascript/flavours/glitch/actions/alerts.js (renamed from app/javascript/themes/glitch/actions/alerts.js)0
-rw-r--r--app/javascript/flavours/glitch/actions/blocks.js (renamed from app/javascript/themes/glitch/actions/blocks.js)2
-rw-r--r--app/javascript/flavours/glitch/actions/bundles.js (renamed from app/javascript/themes/glitch/actions/bundles.js)0
-rw-r--r--app/javascript/flavours/glitch/actions/cards.js (renamed from app/javascript/themes/glitch/actions/cards.js)2
-rw-r--r--app/javascript/flavours/glitch/actions/columns.js (renamed from app/javascript/themes/glitch/actions/columns.js)0
-rw-r--r--app/javascript/flavours/glitch/actions/compose.js (renamed from app/javascript/themes/glitch/actions/compose.js)4
-rw-r--r--app/javascript/flavours/glitch/actions/domain_blocks.js (renamed from app/javascript/themes/glitch/actions/domain_blocks.js)2
-rw-r--r--app/javascript/flavours/glitch/actions/emojis.js (renamed from app/javascript/themes/glitch/actions/emojis.js)0
-rw-r--r--app/javascript/flavours/glitch/actions/favourites.js (renamed from app/javascript/themes/glitch/actions/favourites.js)2
-rw-r--r--app/javascript/flavours/glitch/actions/height_cache.js (renamed from app/javascript/themes/glitch/actions/height_cache.js)0
-rw-r--r--app/javascript/flavours/glitch/actions/interactions.js (renamed from app/javascript/themes/glitch/actions/interactions.js)2
-rw-r--r--app/javascript/flavours/glitch/actions/local_settings.js (renamed from app/javascript/themes/glitch/actions/local_settings.js)0
-rw-r--r--app/javascript/flavours/glitch/actions/modal.js (renamed from app/javascript/themes/glitch/actions/modal.js)0
-rw-r--r--app/javascript/flavours/glitch/actions/mutes.js (renamed from app/javascript/themes/glitch/actions/mutes.js)4
-rw-r--r--app/javascript/flavours/glitch/actions/notifications.js (renamed from app/javascript/themes/glitch/actions/notifications.js)2
-rw-r--r--app/javascript/flavours/glitch/actions/onboarding.js (renamed from app/javascript/themes/glitch/actions/onboarding.js)0
-rw-r--r--app/javascript/flavours/glitch/actions/pin_statuses.js (renamed from app/javascript/themes/glitch/actions/pin_statuses.js)4
-rw-r--r--app/javascript/flavours/glitch/actions/push_notifications.js (renamed from app/javascript/themes/glitch/actions/push_notifications.js)0
-rw-r--r--app/javascript/flavours/glitch/actions/reports.js (renamed from app/javascript/themes/glitch/actions/reports.js)2
-rw-r--r--app/javascript/flavours/glitch/actions/search.js (renamed from app/javascript/themes/glitch/actions/search.js)2
-rw-r--r--app/javascript/flavours/glitch/actions/settings.js (renamed from app/javascript/themes/glitch/actions/settings.js)0
-rw-r--r--app/javascript/flavours/glitch/actions/statuses.js (renamed from app/javascript/themes/glitch/actions/statuses.js)2
-rw-r--r--app/javascript/flavours/glitch/actions/store.js (renamed from app/javascript/themes/glitch/actions/store.js)0
-rw-r--r--app/javascript/flavours/glitch/actions/streaming.js (renamed from app/javascript/themes/glitch/actions/streaming.js)2
-rw-r--r--app/javascript/flavours/glitch/actions/timelines.js (renamed from app/javascript/themes/glitch/actions/timelines.js)2
-rw-r--r--app/javascript/flavours/glitch/components/account.js (renamed from app/javascript/themes/glitch/components/account.js)6
-rw-r--r--app/javascript/flavours/glitch/components/attachment_list.js (renamed from app/javascript/themes/glitch/components/attachment_list.js)0
-rw-r--r--app/javascript/flavours/glitch/components/autosuggest_emoji.js (renamed from app/javascript/themes/glitch/components/autosuggest_emoji.js)2
-rw-r--r--app/javascript/flavours/glitch/components/autosuggest_textarea.js (renamed from app/javascript/themes/glitch/components/autosuggest_textarea.js)4
-rw-r--r--app/javascript/flavours/glitch/components/avatar.js (renamed from app/javascript/themes/glitch/components/avatar.js)0
-rw-r--r--app/javascript/flavours/glitch/components/avatar_overlay.js (renamed from app/javascript/themes/glitch/components/avatar_overlay.js)0
-rw-r--r--app/javascript/flavours/glitch/components/button.js (renamed from app/javascript/themes/glitch/components/button.js)0
-rw-r--r--app/javascript/flavours/glitch/components/collapsable.js (renamed from app/javascript/themes/glitch/components/collapsable.js)2
-rw-r--r--app/javascript/flavours/glitch/components/column.js (renamed from app/javascript/themes/glitch/components/column.js)2
-rw-r--r--app/javascript/flavours/glitch/components/column_back_button.js (renamed from app/javascript/themes/glitch/components/column_back_button.js)0
-rw-r--r--app/javascript/flavours/glitch/components/column_back_button_slim.js (renamed from app/javascript/themes/glitch/components/column_back_button_slim.js)0
-rw-r--r--app/javascript/flavours/glitch/components/column_header.js (renamed from app/javascript/themes/glitch/components/column_header.js)3
-rw-r--r--app/javascript/flavours/glitch/components/display_name.js (renamed from app/javascript/themes/glitch/components/display_name.js)0
-rw-r--r--app/javascript/flavours/glitch/components/dropdown_menu.js (renamed from app/javascript/themes/glitch/components/dropdown_menu.js)2
-rw-r--r--app/javascript/flavours/glitch/components/extended_video_player.js (renamed from app/javascript/themes/glitch/components/extended_video_player.js)0
-rw-r--r--app/javascript/flavours/glitch/components/icon_button.js (renamed from app/javascript/themes/glitch/components/icon_button.js)2
-rw-r--r--app/javascript/flavours/glitch/components/intersection_observer_article.js (renamed from app/javascript/themes/glitch/components/intersection_observer_article.js)4
-rw-r--r--app/javascript/flavours/glitch/components/load_more.js (renamed from app/javascript/themes/glitch/components/load_more.js)0
-rw-r--r--app/javascript/flavours/glitch/components/loading_indicator.js (renamed from app/javascript/themes/glitch/components/loading_indicator.js)0
-rw-r--r--app/javascript/flavours/glitch/components/media_gallery.js (renamed from app/javascript/themes/glitch/components/media_gallery.js)4
-rw-r--r--app/javascript/flavours/glitch/components/missing_indicator.js (renamed from app/javascript/themes/glitch/components/missing_indicator.js)0
-rw-r--r--app/javascript/flavours/glitch/components/notification_purge_buttons.js (renamed from app/javascript/themes/glitch/components/notification_purge_buttons.js)0
-rw-r--r--app/javascript/flavours/glitch/components/permalink.js (renamed from app/javascript/themes/glitch/components/permalink.js)0
-rw-r--r--app/javascript/flavours/glitch/components/relative_timestamp.js (renamed from app/javascript/themes/glitch/components/relative_timestamp.js)0
-rw-r--r--app/javascript/flavours/glitch/components/scrollable_list.js (renamed from app/javascript/themes/glitch/components/scrollable_list.js)6
-rw-r--r--app/javascript/flavours/glitch/components/setting_text.js (renamed from app/javascript/themes/glitch/components/setting_text.js)0
-rw-r--r--app/javascript/flavours/glitch/components/status.js (renamed from app/javascript/themes/glitch/components/status.js)4
-rw-r--r--app/javascript/flavours/glitch/components/status_action_bar.js (renamed from app/javascript/themes/glitch/components/status_action_bar.js)7
-rw-r--r--app/javascript/flavours/glitch/components/status_content.js (renamed from app/javascript/themes/glitch/components/status_content.js)2
-rw-r--r--app/javascript/flavours/glitch/components/status_header.js (renamed from app/javascript/themes/glitch/components/status_header.js)0
-rw-r--r--app/javascript/flavours/glitch/components/status_list.js (renamed from app/javascript/themes/glitch/components/status_list.js)2
-rw-r--r--app/javascript/flavours/glitch/components/status_prepend.js (renamed from app/javascript/themes/glitch/components/status_prepend.js)0
-rw-r--r--app/javascript/flavours/glitch/components/status_visibility_icon.js (renamed from app/javascript/themes/glitch/components/status_visibility_icon.js)0
-rw-r--r--app/javascript/flavours/glitch/containers/account_container.js (renamed from app/javascript/themes/glitch/containers/account_container.js)12
-rw-r--r--app/javascript/flavours/glitch/containers/card_container.js (renamed from app/javascript/themes/glitch/containers/card_container.js)2
-rw-r--r--app/javascript/flavours/glitch/containers/compose_container.js (renamed from app/javascript/themes/glitch/containers/compose_container.js)8
-rw-r--r--app/javascript/flavours/glitch/containers/dropdown_menu_container.js (renamed from app/javascript/themes/glitch/containers/dropdown_menu_container.js)6
-rw-r--r--app/javascript/flavours/glitch/containers/intersection_observer_article_container.js (renamed from app/javascript/themes/glitch/containers/intersection_observer_article_container.js)4
-rw-r--r--app/javascript/flavours/glitch/containers/mastodon.js (renamed from app/javascript/themes/glitch/containers/mastodon.js)14
-rw-r--r--app/javascript/flavours/glitch/containers/media_gallery_container.js (renamed from app/javascript/themes/glitch/containers/media_gallery_container.js)2
-rw-r--r--app/javascript/flavours/glitch/containers/notification_purge_buttons_container.js (renamed from app/javascript/themes/glitch/containers/notification_purge_buttons_container.js)6
-rw-r--r--app/javascript/flavours/glitch/containers/status_container.js (renamed from app/javascript/themes/glitch/containers/status_container.js)20
-rw-r--r--app/javascript/flavours/glitch/containers/timeline_container.js (renamed from app/javascript/themes/glitch/containers/timeline_container.js)10
-rw-r--r--app/javascript/flavours/glitch/containers/video_container.js (renamed from app/javascript/themes/glitch/containers/video_container.js)2
-rw-r--r--app/javascript/flavours/glitch/features/account/components/action_bar.js (renamed from app/javascript/themes/glitch/features/account/components/action_bar.js)9
-rw-r--r--app/javascript/flavours/glitch/features/account/components/header.js (renamed from app/javascript/themes/glitch/features/account/components/header.js)10
-rw-r--r--app/javascript/flavours/glitch/features/account_gallery/components/media_item.js (renamed from app/javascript/themes/glitch/features/account_gallery/components/media_item.js)2
-rw-r--r--app/javascript/flavours/glitch/features/account_gallery/index.js (renamed from app/javascript/themes/glitch/features/account_gallery/index.js)16
-rw-r--r--app/javascript/flavours/glitch/features/account_timeline/components/header.js (renamed from app/javascript/themes/glitch/features/account_timeline/components/header.js)6
-rw-r--r--app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js (renamed from app/javascript/themes/glitch/features/account_timeline/containers/header_container.js)18
-rw-r--r--app/javascript/flavours/glitch/features/account_timeline/index.js (renamed from app/javascript/themes/glitch/features/account_timeline/index.js)4
-rw-r--r--app/javascript/flavours/glitch/features/blocks/index.js (renamed from app/javascript/themes/glitch/features/blocks/index.js)10
-rw-r--r--app/javascript/flavours/glitch/features/community_timeline/components/column_settings.js (renamed from app/javascript/themes/glitch/features/community_timeline/components/column_settings.js)2
-rw-r--r--app/javascript/flavours/glitch/features/community_timeline/containers/column_settings_container.js (renamed from app/javascript/themes/glitch/features/community_timeline/containers/column_settings_container.js)2
-rw-r--r--app/javascript/flavours/glitch/features/community_timeline/index.js (renamed from app/javascript/themes/glitch/features/community_timeline/index.js)12
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/advanced_options.js (renamed from app/javascript/themes/glitch/features/compose/components/advanced_options.js)0
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/advanced_options_toggle.js (renamed from app/javascript/themes/glitch/features/compose/components/advanced_options_toggle.js)0
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/attach_options.js (renamed from app/javascript/themes/glitch/features/compose/components/attach_options.js)4
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/autosuggest_account.js (renamed from app/javascript/themes/glitch/features/compose/components/autosuggest_account.js)4
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/character_counter.js (renamed from app/javascript/themes/glitch/features/compose/components/character_counter.js)0
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/compose_form.js (renamed from app/javascript/themes/glitch/features/compose/components/compose_form.js)12
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/dropdown.js (renamed from app/javascript/themes/glitch/features/compose/components/dropdown.js)2
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.js (renamed from app/javascript/themes/glitch/features/compose/components/emoji_picker_dropdown.js)4
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/navigation_bar.js (renamed from app/javascript/themes/glitch/features/compose/components/navigation_bar.js)6
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js (renamed from app/javascript/themes/glitch/features/compose/components/privacy_dropdown.js)4
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/reply_indicator.js (renamed from app/javascript/themes/glitch/features/compose/components/reply_indicator.js)6
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/search.js (renamed from app/javascript/themes/glitch/features/compose/components/search.js)2
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/search_results.js (renamed from app/javascript/themes/glitch/features/compose/components/search_results.js)4
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/text_icon_button.js (renamed from app/javascript/themes/glitch/features/compose/components/text_icon_button.js)0
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/upload.js (renamed from app/javascript/themes/glitch/features/compose/components/upload.js)4
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/upload_button.js (renamed from app/javascript/themes/glitch/features/compose/components/upload_button.js)2
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/upload_form.js (renamed from app/javascript/themes/glitch/features/compose/components/upload_form.js)0
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/upload_progress.js (renamed from app/javascript/themes/glitch/features/compose/components/upload_progress.js)2
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/warning.js (renamed from app/javascript/themes/glitch/features/compose/components/warning.js)2
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/advanced_options_container.js (renamed from app/javascript/themes/glitch/features/compose/containers/advanced_options_container.js)2
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/autosuggest_account_container.js (renamed from app/javascript/themes/glitch/features/compose/containers/autosuggest_account_container.js)2
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js (renamed from app/javascript/themes/glitch/features/compose/containers/compose_form_container.js)4
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/emoji_picker_dropdown_container.js (renamed from app/javascript/themes/glitch/features/compose/containers/emoji_picker_dropdown_container.js)4
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/navigation_container.js (renamed from app/javascript/themes/glitch/features/compose/containers/navigation_container.js)2
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/privacy_dropdown_container.js (renamed from app/javascript/themes/glitch/features/compose/containers/privacy_dropdown_container.js)6
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/reply_indicator_container.js (renamed from app/javascript/themes/glitch/features/compose/containers/reply_indicator_container.js)4
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/search_container.js (renamed from app/javascript/themes/glitch/features/compose/containers/search_container.js)2
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/search_results_container.js (renamed from app/javascript/themes/glitch/features/compose/containers/search_results_container.js)0
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.js (renamed from app/javascript/themes/glitch/features/compose/containers/sensitive_button_container.js)6
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/spoiler_button_container.js (renamed from app/javascript/themes/glitch/features/compose/containers/spoiler_button_container.js)2
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/upload_button_container.js (renamed from app/javascript/themes/glitch/features/compose/containers/upload_button_container.js)2
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/upload_container.js (renamed from app/javascript/themes/glitch/features/compose/containers/upload_container.js)2
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/upload_form_container.js (renamed from app/javascript/themes/glitch/features/compose/containers/upload_form_container.js)0
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/upload_progress_container.js (renamed from app/javascript/themes/glitch/features/compose/containers/upload_progress_container.js)0
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/warning_container.js (renamed from app/javascript/themes/glitch/features/compose/containers/warning_container.js)2
-rw-r--r--app/javascript/flavours/glitch/features/compose/index.js (renamed from app/javascript/themes/glitch/features/compose/index.js)10
-rw-r--r--app/javascript/flavours/glitch/features/direct_timeline/containers/column_settings_container.js (renamed from app/javascript/themes/glitch/features/direct_timeline/containers/column_settings_container.js)4
-rw-r--r--app/javascript/flavours/glitch/features/direct_timeline/index.js (renamed from app/javascript/themes/glitch/features/direct_timeline/index.js)12
-rw-r--r--app/javascript/flavours/glitch/features/favourited_statuses/index.js (renamed from app/javascript/themes/glitch/features/favourited_statuses/index.js)10
-rw-r--r--app/javascript/flavours/glitch/features/favourites/index.js (renamed from app/javascript/themes/glitch/features/favourites/index.js)10
-rw-r--r--app/javascript/flavours/glitch/features/follow_requests/components/account_authorize.js (renamed from app/javascript/themes/glitch/features/follow_requests/components/account_authorize.js)8
-rw-r--r--app/javascript/flavours/glitch/features/follow_requests/containers/account_authorize_container.js (renamed from app/javascript/themes/glitch/features/follow_requests/containers/account_authorize_container.js)4
-rw-r--r--app/javascript/flavours/glitch/features/follow_requests/index.js (renamed from app/javascript/themes/glitch/features/follow_requests/index.js)8
-rw-r--r--app/javascript/flavours/glitch/features/followers/index.js (renamed from app/javascript/themes/glitch/features/followers/index.js)14
-rw-r--r--app/javascript/flavours/glitch/features/following/index.js (renamed from app/javascript/themes/glitch/features/following/index.js)14
-rw-r--r--app/javascript/flavours/glitch/features/generic_not_found/index.js11
-rw-r--r--app/javascript/flavours/glitch/features/getting_started/index.js (renamed from app/javascript/themes/glitch/features/getting_started/index.js)10
-rw-r--r--app/javascript/flavours/glitch/features/hashtag_timeline/index.js (renamed from app/javascript/themes/glitch/features/hashtag_timeline/index.js)12
-rw-r--r--app/javascript/flavours/glitch/features/home_timeline/components/column_settings.js (renamed from app/javascript/themes/glitch/features/home_timeline/components/column_settings.js)8
-rw-r--r--app/javascript/flavours/glitch/features/home_timeline/containers/column_settings_container.js (renamed from app/javascript/themes/glitch/features/home_timeline/containers/column_settings_container.js)2
-rw-r--r--app/javascript/flavours/glitch/features/home_timeline/index.js (renamed from app/javascript/themes/glitch/features/home_timeline/index.js)10
-rw-r--r--app/javascript/flavours/glitch/features/local_settings/index.js (renamed from app/javascript/themes/glitch/features/local_settings/index.js)7
-rw-r--r--app/javascript/flavours/glitch/features/local_settings/navigation/index.js (renamed from app/javascript/themes/glitch/features/local_settings/navigation/index.js)3
-rw-r--r--app/javascript/flavours/glitch/features/local_settings/navigation/item/index.js (renamed from app/javascript/themes/glitch/features/local_settings/navigation/item/index.js)3
-rw-r--r--app/javascript/flavours/glitch/features/local_settings/page/index.js (renamed from app/javascript/themes/glitch/features/local_settings/page/index.js)3
-rw-r--r--app/javascript/flavours/glitch/features/local_settings/page/item/index.js (renamed from app/javascript/themes/glitch/features/local_settings/page/item/index.js)3
-rw-r--r--app/javascript/flavours/glitch/features/mutes/index.js (renamed from app/javascript/themes/glitch/features/mutes/index.js)10
-rw-r--r--app/javascript/flavours/glitch/features/notifications/components/clear_column_button.js (renamed from app/javascript/themes/glitch/features/notifications/components/clear_column_button.js)0
-rw-r--r--app/javascript/flavours/glitch/features/notifications/components/column_settings.js (renamed from app/javascript/themes/glitch/features/notifications/components/column_settings.js)0
-rw-r--r--app/javascript/flavours/glitch/features/notifications/components/follow.js (renamed from app/javascript/themes/glitch/features/notifications/components/follow.js)4
-rw-r--r--app/javascript/flavours/glitch/features/notifications/components/notification.js (renamed from app/javascript/themes/glitch/features/notifications/components/notification.js)2
-rw-r--r--app/javascript/flavours/glitch/features/notifications/components/overlay.js (renamed from app/javascript/themes/glitch/features/notifications/components/overlay.js)0
-rw-r--r--app/javascript/flavours/glitch/features/notifications/components/setting_toggle.js (renamed from app/javascript/themes/glitch/features/notifications/components/setting_toggle.js)0
-rw-r--r--app/javascript/flavours/glitch/features/notifications/containers/column_settings_container.js (renamed from app/javascript/themes/glitch/features/notifications/containers/column_settings_container.js)8
-rw-r--r--app/javascript/flavours/glitch/features/notifications/containers/notification_container.js (renamed from app/javascript/themes/glitch/features/notifications/containers/notification_container.js)4
-rw-r--r--app/javascript/flavours/glitch/features/notifications/containers/overlay_container.js (renamed from app/javascript/themes/glitch/features/notifications/containers/overlay_container.js)2
-rw-r--r--app/javascript/flavours/glitch/features/notifications/index.js (renamed from app/javascript/themes/glitch/features/notifications/index.js)10
-rw-r--r--app/javascript/flavours/glitch/features/pinned_statuses/index.js (renamed from app/javascript/themes/glitch/features/pinned_statuses/index.js)8
-rw-r--r--app/javascript/flavours/glitch/features/public_timeline/containers/column_settings_container.js (renamed from app/javascript/themes/glitch/features/public_timeline/containers/column_settings_container.js)4
-rw-r--r--app/javascript/flavours/glitch/features/public_timeline/index.js (renamed from app/javascript/themes/glitch/features/public_timeline/index.js)12
-rw-r--r--app/javascript/flavours/glitch/features/reblogs/index.js (renamed from app/javascript/themes/glitch/features/reblogs/index.js)10
-rw-r--r--app/javascript/flavours/glitch/features/report/components/status_check_box.js (renamed from app/javascript/themes/glitch/features/report/components/status_check_box.js)0
-rw-r--r--app/javascript/flavours/glitch/features/report/containers/status_check_box_container.js (renamed from app/javascript/themes/glitch/features/report/containers/status_check_box_container.js)2
-rw-r--r--app/javascript/flavours/glitch/features/standalone/compose/index.js20
-rw-r--r--app/javascript/flavours/glitch/features/standalone/hashtag_timeline/index.js (renamed from app/javascript/themes/glitch/features/standalone/hashtag_timeline/index.js)8
-rw-r--r--app/javascript/flavours/glitch/features/standalone/public_timeline/index.js (renamed from app/javascript/themes/glitch/features/standalone/public_timeline/index.js)8
-rw-r--r--app/javascript/flavours/glitch/features/status/components/action_bar.js (renamed from app/javascript/themes/glitch/features/status/components/action_bar.js)6
-rw-r--r--app/javascript/flavours/glitch/features/status/components/card.js (renamed from app/javascript/themes/glitch/features/status/components/card.js)0
-rw-r--r--app/javascript/flavours/glitch/features/status/components/detailed_status.js (renamed from app/javascript/themes/glitch/features/status/components/detailed_status.js)14
-rw-r--r--app/javascript/flavours/glitch/features/status/containers/card_container.js (renamed from app/javascript/themes/glitch/features/status/containers/card_container.js)0
-rw-r--r--app/javascript/flavours/glitch/features/status/index.js (renamed from app/javascript/themes/glitch/features/status/index.js)26
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/actions_modal.js (renamed from app/javascript/themes/glitch/features/ui/components/actions_modal.js)10
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/boost_modal.js (renamed from app/javascript/themes/glitch/features/ui/components/boost_modal.js)10
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/bundle.js (renamed from app/javascript/themes/glitch/features/ui/components/bundle.js)0
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/bundle_column_error.js (renamed from app/javascript/themes/glitch/features/ui/components/bundle_column_error.js)4
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/bundle_modal_error.js (renamed from app/javascript/themes/glitch/features/ui/components/bundle_modal_error.js)2
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/column.js (renamed from app/javascript/themes/glitch/features/ui/components/column.js)4
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/column_header.js (renamed from app/javascript/themes/glitch/features/ui/components/column_header.js)0
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/column_link.js (renamed from app/javascript/themes/glitch/features/ui/components/column_link.js)0
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/column_loading.js (renamed from app/javascript/themes/glitch/features/ui/components/column_loading.js)4
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/column_subheading.js (renamed from app/javascript/themes/glitch/features/ui/components/column_subheading.js)0
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/columns_area.js (renamed from app/javascript/themes/glitch/features/ui/components/columns_area.js)4
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/confirmation_modal.js (renamed from app/javascript/themes/glitch/features/ui/components/confirmation_modal.js)2
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/doodle_modal.js (renamed from app/javascript/themes/glitch/features/ui/components/doodle_modal.js)6
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/drawer_loading.js (renamed from app/javascript/themes/glitch/features/ui/components/drawer_loading.js)0
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/embed_modal.js (renamed from app/javascript/themes/glitch/features/ui/components/embed_modal.js)0
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/image_loader.js (renamed from app/javascript/themes/glitch/features/ui/components/image_loader.js)0
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/media_modal.js (renamed from app/javascript/themes/glitch/features/ui/components/media_modal.js)4
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/modal_loading.js (renamed from app/javascript/themes/glitch/features/ui/components/modal_loading.js)2
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/modal_root.js (renamed from app/javascript/themes/glitch/features/ui/components/modal_root.js)2
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/mute_modal.js (renamed from app/javascript/themes/glitch/features/ui/components/mute_modal.js)8
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/onboarding_modal.js (renamed from app/javascript/themes/glitch/features/ui/components/onboarding_modal.js)10
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/report_modal.js (renamed from app/javascript/themes/glitch/features/ui/components/report_modal.js)10
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/tabs_bar.js (renamed from app/javascript/themes/glitch/features/ui/components/tabs_bar.js)2
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/upload_area.js (renamed from app/javascript/themes/glitch/features/ui/components/upload_area.js)2
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/video_modal.js (renamed from app/javascript/themes/glitch/features/ui/components/video_modal.js)2
-rw-r--r--app/javascript/flavours/glitch/features/ui/containers/bundle_container.js (renamed from app/javascript/themes/glitch/features/ui/containers/bundle_container.js)2
-rw-r--r--app/javascript/flavours/glitch/features/ui/containers/columns_area_container.js (renamed from app/javascript/themes/glitch/features/ui/containers/columns_area_container.js)0
-rw-r--r--app/javascript/flavours/glitch/features/ui/containers/loading_bar_container.js (renamed from app/javascript/themes/glitch/features/ui/containers/loading_bar_container.js)0
-rw-r--r--app/javascript/flavours/glitch/features/ui/containers/modal_container.js (renamed from app/javascript/themes/glitch/features/ui/containers/modal_container.js)2
-rw-r--r--app/javascript/flavours/glitch/features/ui/containers/notifications_container.js (renamed from app/javascript/themes/glitch/features/ui/containers/notifications_container.js)4
-rw-r--r--app/javascript/flavours/glitch/features/ui/containers/status_list_container.js (renamed from app/javascript/themes/glitch/features/ui/containers/status_list_container.js)10
-rw-r--r--app/javascript/flavours/glitch/features/ui/index.js (renamed from app/javascript/themes/glitch/features/ui/index.js)16
-rw-r--r--app/javascript/flavours/glitch/features/video/index.js (renamed from app/javascript/themes/glitch/features/video/index.js)2
-rw-r--r--app/javascript/flavours/glitch/middleware/errors.js (renamed from app/javascript/themes/glitch/middleware/errors.js)2
-rw-r--r--app/javascript/flavours/glitch/middleware/loading_bar.js (renamed from app/javascript/themes/glitch/middleware/loading_bar.js)0
-rw-r--r--app/javascript/flavours/glitch/middleware/sounds.js (renamed from app/javascript/themes/glitch/middleware/sounds.js)0
-rw-r--r--app/javascript/flavours/glitch/packs/about.js22
-rw-r--r--app/javascript/flavours/glitch/packs/common.js1
-rw-r--r--app/javascript/flavours/glitch/packs/home.js7
-rw-r--r--app/javascript/flavours/glitch/packs/public.js75
-rw-r--r--app/javascript/flavours/glitch/packs/share.js22
-rw-r--r--app/javascript/flavours/glitch/reducers/accounts.js (renamed from app/javascript/themes/glitch/reducers/accounts.js)24
-rw-r--r--app/javascript/flavours/glitch/reducers/accounts_counters.js (renamed from app/javascript/themes/glitch/reducers/accounts_counters.js)22
-rw-r--r--app/javascript/flavours/glitch/reducers/alerts.js (renamed from app/javascript/themes/glitch/reducers/alerts.js)2
-rw-r--r--app/javascript/flavours/glitch/reducers/cards.js (renamed from app/javascript/themes/glitch/reducers/cards.js)2
-rw-r--r--app/javascript/flavours/glitch/reducers/compose.js (renamed from app/javascript/themes/glitch/reducers/compose.js)10
-rw-r--r--app/javascript/flavours/glitch/reducers/contexts.js (renamed from app/javascript/themes/glitch/reducers/contexts.js)4
-rw-r--r--app/javascript/flavours/glitch/reducers/custom_emojis.js (renamed from app/javascript/themes/glitch/reducers/custom_emojis.js)6
-rw-r--r--app/javascript/flavours/glitch/reducers/height_cache.js (renamed from app/javascript/themes/glitch/reducers/height_cache.js)2
-rw-r--r--app/javascript/flavours/glitch/reducers/index.js (renamed from app/javascript/themes/glitch/reducers/index.js)0
-rw-r--r--app/javascript/flavours/glitch/reducers/local_settings.js (renamed from app/javascript/themes/glitch/reducers/local_settings.js)4
-rw-r--r--app/javascript/flavours/glitch/reducers/media_attachments.js (renamed from app/javascript/themes/glitch/reducers/media_attachments.js)2
-rw-r--r--app/javascript/flavours/glitch/reducers/meta.js (renamed from app/javascript/themes/glitch/reducers/meta.js)2
-rw-r--r--app/javascript/flavours/glitch/reducers/modal.js (renamed from app/javascript/themes/glitch/reducers/modal.js)2
-rw-r--r--app/javascript/flavours/glitch/reducers/mutes.js (renamed from app/javascript/themes/glitch/reducers/mutes.js)2
-rw-r--r--app/javascript/flavours/glitch/reducers/notifications.js (renamed from app/javascript/themes/glitch/reducers/notifications.js)6
-rw-r--r--app/javascript/flavours/glitch/reducers/push_notifications.js (renamed from app/javascript/themes/glitch/reducers/push_notifications.js)4
-rw-r--r--app/javascript/flavours/glitch/reducers/relationships.js (renamed from app/javascript/themes/glitch/reducers/relationships.js)4
-rw-r--r--app/javascript/flavours/glitch/reducers/reports.js (renamed from app/javascript/themes/glitch/reducers/reports.js)2
-rw-r--r--app/javascript/flavours/glitch/reducers/search.js (renamed from app/javascript/themes/glitch/reducers/search.js)4
-rw-r--r--app/javascript/flavours/glitch/reducers/settings.js (renamed from app/javascript/themes/glitch/reducers/settings.js)11
-rw-r--r--app/javascript/flavours/glitch/reducers/status_lists.js (renamed from app/javascript/themes/glitch/reducers/status_lists.js)6
-rw-r--r--app/javascript/flavours/glitch/reducers/statuses.js (renamed from app/javascript/themes/glitch/reducers/statuses.js)18
-rw-r--r--app/javascript/flavours/glitch/reducers/timelines.js (renamed from app/javascript/themes/glitch/reducers/timelines.js)4
-rw-r--r--app/javascript/flavours/glitch/reducers/user_lists.js (renamed from app/javascript/themes/glitch/reducers/user_lists.js)8
-rw-r--r--app/javascript/flavours/glitch/selectors/index.js (renamed from app/javascript/themes/glitch/selectors/index.js)0
-rw-r--r--app/javascript/flavours/glitch/service_worker/entry.js (renamed from app/javascript/themes/glitch/service_worker/entry.js)0
-rw-r--r--app/javascript/flavours/glitch/service_worker/web_push_notifications.js (renamed from app/javascript/themes/glitch/service_worker/web_push_notifications.js)0
-rw-r--r--app/javascript/flavours/glitch/store/configureStore.js (renamed from app/javascript/themes/glitch/store/configureStore.js)0
-rw-r--r--app/javascript/flavours/glitch/styles/_mixins.scss (renamed from app/javascript/themes/glitch/styles/_mixins.scss)0
-rw-r--r--app/javascript/flavours/glitch/styles/about.scss (renamed from app/javascript/themes/glitch/styles/about.scss)0
-rw-r--r--app/javascript/flavours/glitch/styles/accounts.scss (renamed from app/javascript/themes/glitch/styles/accounts.scss)0
-rw-r--r--app/javascript/flavours/glitch/styles/admin.scss (renamed from app/javascript/themes/glitch/styles/admin.scss)0
-rw-r--r--app/javascript/flavours/glitch/styles/basics.scss (renamed from app/javascript/themes/glitch/styles/basics.scss)0
-rw-r--r--app/javascript/flavours/glitch/styles/compact_header.scss (renamed from app/javascript/themes/glitch/styles/compact_header.scss)0
-rw-r--r--app/javascript/flavours/glitch/styles/components/boost.scss (renamed from app/javascript/themes/glitch/styles/boost.scss)0
-rw-r--r--app/javascript/flavours/glitch/styles/components/doodle.scss (renamed from app/javascript/themes/glitch/styles/doodle.scss)0
-rw-r--r--app/javascript/flavours/glitch/styles/components/emoji_picker.scss (renamed from app/javascript/themes/glitch/styles/emoji_picker.scss)0
-rw-r--r--app/javascript/flavours/glitch/styles/components/index.scss (renamed from app/javascript/themes/glitch/styles/components.scss)4
-rw-r--r--app/javascript/flavours/glitch/styles/components/local_settings.scss81
-rw-r--r--app/javascript/flavours/glitch/styles/containers.scss (renamed from app/javascript/themes/glitch/styles/containers.scss)0
-rw-r--r--app/javascript/flavours/glitch/styles/footer.scss (renamed from app/javascript/themes/glitch/styles/footer.scss)0
-rw-r--r--app/javascript/flavours/glitch/styles/forms.scss (renamed from app/javascript/themes/glitch/styles/forms.scss)0
-rw-r--r--app/javascript/flavours/glitch/styles/index.scss (renamed from app/javascript/themes/glitch/styles/index.scss)3
-rw-r--r--app/javascript/flavours/glitch/styles/landing_strip.scss (renamed from app/javascript/themes/glitch/styles/landing_strip.scss)0
-rw-r--r--app/javascript/flavours/glitch/styles/lists.scss (renamed from app/javascript/themes/glitch/styles/lists.scss)0
-rw-r--r--app/javascript/flavours/glitch/styles/reset.scss (renamed from app/javascript/themes/glitch/styles/reset.scss)0
-rw-r--r--app/javascript/flavours/glitch/styles/rtl.scss (renamed from app/javascript/themes/glitch/styles/rtl.scss)0
-rw-r--r--app/javascript/flavours/glitch/styles/stream_entries.scss (renamed from app/javascript/themes/glitch/styles/stream_entries.scss)0
-rw-r--r--app/javascript/flavours/glitch/styles/tables.scss (renamed from app/javascript/themes/glitch/styles/tables.scss)0
-rw-r--r--app/javascript/flavours/glitch/styles/variables.scss (renamed from app/javascript/themes/glitch/styles/variables.scss)0
-rw-r--r--app/javascript/flavours/glitch/theme.yml33
-rw-r--r--app/javascript/flavours/glitch/util/api.js (renamed from app/javascript/themes/glitch/util/api.js)0
-rw-r--r--app/javascript/flavours/glitch/util/async-components.js115
-rw-r--r--app/javascript/flavours/glitch/util/base_polyfills.js (renamed from app/javascript/themes/glitch/util/base_polyfills.js)0
-rw-r--r--app/javascript/flavours/glitch/util/bio_metadata.js (renamed from app/javascript/themes/glitch/util/bio_metadata.js)0
-rw-r--r--app/javascript/flavours/glitch/util/counter.js (renamed from app/javascript/themes/glitch/util/counter.js)0
-rw-r--r--app/javascript/flavours/glitch/util/emoji/emoji_compressed.js (renamed from app/javascript/themes/glitch/util/emoji/emoji_compressed.js)0
-rw-r--r--app/javascript/flavours/glitch/util/emoji/emoji_map.json (renamed from app/javascript/themes/glitch/util/emoji/emoji_map.json)0
-rw-r--r--app/javascript/flavours/glitch/util/emoji/emoji_mart_data_light.js (renamed from app/javascript/themes/glitch/util/emoji/emoji_mart_data_light.js)0
-rw-r--r--app/javascript/flavours/glitch/util/emoji/emoji_mart_search_light.js (renamed from app/javascript/themes/glitch/util/emoji/emoji_mart_search_light.js)0
-rw-r--r--app/javascript/flavours/glitch/util/emoji/emoji_picker.js (renamed from app/javascript/themes/glitch/util/emoji/emoji_picker.js)0
-rw-r--r--app/javascript/flavours/glitch/util/emoji/emoji_unicode_mapping_light.js (renamed from app/javascript/themes/glitch/util/emoji/emoji_unicode_mapping_light.js)0
-rw-r--r--app/javascript/flavours/glitch/util/emoji/emoji_utils.js (renamed from app/javascript/themes/glitch/util/emoji/emoji_utils.js)0
-rw-r--r--app/javascript/flavours/glitch/util/emoji/index.js (renamed from app/javascript/themes/glitch/util/emoji/index.js)2
-rw-r--r--app/javascript/flavours/glitch/util/emoji/unicode_to_filename.js (renamed from app/javascript/themes/glitch/util/emoji/unicode_to_filename.js)0
-rw-r--r--app/javascript/flavours/glitch/util/emoji/unicode_to_unified_name.js (renamed from app/javascript/themes/glitch/util/emoji/unicode_to_unified_name.js)0
-rw-r--r--app/javascript/flavours/glitch/util/extra_polyfills.js (renamed from app/javascript/themes/glitch/util/extra_polyfills.js)0
-rw-r--r--app/javascript/flavours/glitch/util/fullscreen.js (renamed from app/javascript/themes/glitch/util/fullscreen.js)0
-rw-r--r--app/javascript/flavours/glitch/util/get_rect_from_entry.js (renamed from app/javascript/themes/glitch/util/get_rect_from_entry.js)0
-rw-r--r--app/javascript/flavours/glitch/util/initial_state.js (renamed from app/javascript/themes/glitch/util/initial_state.js)0
-rw-r--r--app/javascript/flavours/glitch/util/intersection_observer_wrapper.js (renamed from app/javascript/themes/glitch/util/intersection_observer_wrapper.js)0
-rw-r--r--app/javascript/flavours/glitch/util/is_mobile.js (renamed from app/javascript/themes/glitch/util/is_mobile.js)0
-rw-r--r--app/javascript/flavours/glitch/util/link_header.js (renamed from app/javascript/themes/glitch/util/link_header.js)0
-rw-r--r--app/javascript/flavours/glitch/util/load_polyfills.js (renamed from app/javascript/themes/glitch/util/load_polyfills.js)0
-rw-r--r--app/javascript/flavours/glitch/util/main.js (renamed from app/javascript/themes/glitch/util/main.js)2
-rw-r--r--app/javascript/flavours/glitch/util/optional_motion.js (renamed from app/javascript/themes/glitch/util/optional_motion.js)2
-rw-r--r--app/javascript/flavours/glitch/util/performance.js (renamed from app/javascript/themes/glitch/util/performance.js)0
-rw-r--r--app/javascript/flavours/glitch/util/react_router_helpers.js (renamed from app/javascript/themes/glitch/util/react_router_helpers.js)6
-rw-r--r--app/javascript/flavours/glitch/util/ready.js (renamed from app/javascript/themes/glitch/util/ready.js)0
-rw-r--r--app/javascript/flavours/glitch/util/reduced_motion.js (renamed from app/javascript/themes/glitch/util/reduced_motion.js)0
-rw-r--r--app/javascript/flavours/glitch/util/rtl.js (renamed from app/javascript/themes/glitch/util/rtl.js)0
-rw-r--r--app/javascript/flavours/glitch/util/schedule_idle_task.js (renamed from app/javascript/themes/glitch/util/schedule_idle_task.js)0
-rw-r--r--app/javascript/flavours/glitch/util/scroll.js (renamed from app/javascript/themes/glitch/util/scroll.js)0
-rw-r--r--app/javascript/flavours/glitch/util/stream.js (renamed from app/javascript/themes/glitch/util/stream.js)0
-rw-r--r--app/javascript/flavours/glitch/util/url_regex.js (renamed from app/javascript/themes/glitch/util/url_regex.js)0
-rw-r--r--app/javascript/flavours/glitch/util/uuid.js (renamed from app/javascript/themes/glitch/util/uuid.js)0
-rw-r--r--app/javascript/flavours/glitch/util/web_push_subscription.js (renamed from app/javascript/themes/glitch/util/web_push_subscription.js)4
-rw-r--r--app/javascript/flavours/vanilla/theme.yml33
-rw-r--r--app/javascript/fonts/premillenium/MSSansSerif.ttfbin0 -> 626300 bytes
-rw-r--r--app/javascript/glitch/locales/en.json2
-rw-r--r--app/javascript/images/icon_about.pngbin0 -> 497 bytes
-rw-r--r--app/javascript/images/icon_blocks.pngbin0 -> 356 bytes
-rw-r--r--app/javascript/images/icon_home.pngbin0 -> 328 bytes
-rw-r--r--app/javascript/images/icon_likes.pngbin0 -> 326 bytes
-rw-r--r--app/javascript/images/icon_local.pngbin0 -> 599 bytes
-rw-r--r--app/javascript/images/icon_logout.pngbin0 -> 383 bytes
-rw-r--r--app/javascript/images/icon_mutes.pngbin0 -> 411 bytes
-rw-r--r--app/javascript/images/icon_pin.pngbin0 -> 337 bytes
-rw-r--r--app/javascript/images/icon_public.pngbin0 -> 688 bytes
-rw-r--r--app/javascript/images/icon_settings.pngbin0 -> 639 bytes
-rw-r--r--app/javascript/images/start.pngbin0 -> 263 bytes
-rw-r--r--app/javascript/locales/index.js9
-rw-r--r--app/javascript/mastodon/actions/lists.js289
-rw-r--r--app/javascript/mastodon/components/account.js12
-rw-r--r--app/javascript/mastodon/components/autosuggest_textarea.js1
-rw-r--r--app/javascript/mastodon/features/account/components/action_bar.js5
-rw-r--r--app/javascript/mastodon/features/account_timeline/containers/header_container.js2
-rw-r--r--app/javascript/mastodon/features/getting_started/index.js4
-rw-r--r--app/javascript/mastodon/features/list_editor/components/account.js77
-rw-r--r--app/javascript/mastodon/features/list_editor/components/search.js75
-rw-r--r--app/javascript/mastodon/features/list_editor/index.js80
-rw-r--r--app/javascript/mastodon/features/list_timeline/index.js72
-rw-r--r--app/javascript/mastodon/features/lists/components/new_list_form.js78
-rw-r--r--app/javascript/mastodon/features/lists/index.js76
-rw-r--r--app/javascript/mastodon/features/status/components/card.js2
-rw-r--r--app/javascript/mastodon/features/ui/components/modal_root.js2
-rw-r--r--app/javascript/mastodon/features/ui/index.js2
-rw-r--r--app/javascript/mastodon/features/ui/util/async-components.js8
-rw-r--r--app/javascript/mastodon/locales/ar.json56
-rw-r--r--app/javascript/mastodon/locales/bg.json12
-rw-r--r--app/javascript/mastodon/locales/ca.json52
-rw-r--r--app/javascript/mastodon/locales/de.json54
-rw-r--r--app/javascript/mastodon/locales/defaultMessages.json68
-rw-r--r--app/javascript/mastodon/locales/en.json12
-rw-r--r--app/javascript/mastodon/locales/eo.json12
-rw-r--r--app/javascript/mastodon/locales/es.json82
-rw-r--r--app/javascript/mastodon/locales/fa.json12
-rw-r--r--app/javascript/mastodon/locales/fi.json12
-rw-r--r--app/javascript/mastodon/locales/fr.json44
-rw-r--r--app/javascript/mastodon/locales/he.json156
-rw-r--r--app/javascript/mastodon/locales/hr.json12
-rw-r--r--app/javascript/mastodon/locales/hu.json12
-rw-r--r--app/javascript/mastodon/locales/id.json12
-rw-r--r--app/javascript/mastodon/locales/index.js10
-rw-r--r--app/javascript/mastodon/locales/io.json12
-rw-r--r--app/javascript/mastodon/locales/it.json12
-rw-r--r--app/javascript/mastodon/locales/ja.json30
-rw-r--r--app/javascript/mastodon/locales/ko.json12
-rw-r--r--app/javascript/mastodon/locales/nl.json68
-rw-r--r--app/javascript/mastodon/locales/no.json84
-rw-r--r--app/javascript/mastodon/locales/oc.json58
-rw-r--r--app/javascript/mastodon/locales/pl.json12
-rw-r--r--app/javascript/mastodon/locales/pt-BR.json38
-rw-r--r--app/javascript/mastodon/locales/pt.json248
-rw-r--r--app/javascript/mastodon/locales/ru.json12
-rw-r--r--app/javascript/mastodon/locales/sv.json12
-rw-r--r--app/javascript/mastodon/locales/th.json12
-rw-r--r--app/javascript/mastodon/locales/tr.json12
-rw-r--r--app/javascript/mastodon/locales/uk.json12
-rw-r--r--app/javascript/mastodon/locales/zh-CN.json52
-rw-r--r--app/javascript/mastodon/locales/zh-HK.json12
-rw-r--r--app/javascript/mastodon/locales/zh-TW.json12
-rw-r--r--app/javascript/mastodon/reducers/accounts.js6
-rw-r--r--app/javascript/mastodon/reducers/accounts_counters.js6
-rw-r--r--app/javascript/mastodon/reducers/index.js2
-rw-r--r--app/javascript/mastodon/reducers/list_editor.js89
-rw-r--r--app/javascript/mastodon/reducers/lists.js24
-rw-r--r--app/javascript/mastodon/reducers/settings.js7
-rw-r--r--app/javascript/packs/about.js8
-rw-r--r--app/javascript/packs/application.js11
-rw-r--r--app/javascript/packs/common.js7
-rw-r--r--app/javascript/packs/public.js88
-rw-r--r--app/javascript/packs/share.js8
-rw-r--r--app/javascript/skins/vanilla/win95.scss1
-rw-r--r--app/javascript/styles/common.scss5
-rw-r--r--app/javascript/styles/mastodon/about.scss2
-rw-r--r--app/javascript/styles/mastodon/accounts.scss6
-rw-r--r--app/javascript/styles/mastodon/admin.scss18
-rw-r--r--app/javascript/styles/mastodon/components.scss163
-rw-r--r--app/javascript/styles/mastodon/emoji_picker.scss5
-rw-r--r--app/javascript/styles/mastodon/forms.scss30
-rw-r--r--app/javascript/styles/mastodon/landing_strip.scss12
-rw-r--r--app/javascript/styles/mastodon/stream_entries.scss12
-rw-r--r--app/javascript/styles/mastodon/tables.scss6
-rw-r--r--app/javascript/styles/mastodon/variables.scss3
-rw-r--r--app/javascript/styles/win95.scss1655
-rw-r--r--app/javascript/themes/glitch/features/generic_not_found/index.js11
-rw-r--r--app/javascript/themes/glitch/features/local_settings/navigation/item/style.scss27
-rw-r--r--app/javascript/themes/glitch/features/local_settings/navigation/style.scss10
-rw-r--r--app/javascript/themes/glitch/features/local_settings/page/item/style.scss7
-rw-r--r--app/javascript/themes/glitch/features/local_settings/page/style.scss9
-rw-r--r--app/javascript/themes/glitch/features/local_settings/style.scss34
-rw-r--r--app/javascript/themes/glitch/features/standalone/compose/index.js20
-rw-r--r--app/javascript/themes/glitch/index.js14
-rw-r--r--app/javascript/themes/glitch/styles/reset copy.scss91
-rw-r--r--app/javascript/themes/glitch/theme.yml18
-rw-r--r--app/javascript/themes/glitch/util/async-components.js115
m---------app/javascript/themes/mastodon-go0
-rw-r--r--app/javascript/themes/vanilla/theme.yml18
-rw-r--r--app/lib/activitypub/activity.rb4
-rw-r--r--app/lib/extractor.rb2
-rw-r--r--app/lib/formatter.rb2
-rw-r--r--app/lib/ostatus/activity/base.rb2
-rw-r--r--app/lib/ostatus/atom_serializer.rb2
-rw-r--r--app/lib/provider_discovery.rb2
-rw-r--r--app/lib/request.rb2
-rw-r--r--app/lib/themes.rb43
-rw-r--r--app/lib/user_settings_decorator.rb13
-rw-r--r--app/mailers/notification_mailer.rb2
-rw-r--r--app/mailers/user_mailer.rb6
-rw-r--r--app/models/account.rb68
-rw-r--r--app/models/list.rb19
-rw-r--r--app/models/list_account.rb2
-rw-r--r--app/models/preview_card.rb1
-rw-r--r--app/models/remote_follow.rb4
-rw-r--r--app/models/session_activation.rb12
-rw-r--r--app/models/user.rb2
-rw-r--r--app/presenters/account_relationships_presenter.rb2
-rw-r--r--app/presenters/status_relationships_presenter.rb2
-rw-r--r--app/serializers/rest/preview_card_serializer.rb2
-rw-r--r--app/serializers/rest/relationship_serializer.rb20
-rw-r--r--app/serializers/webfinger_serializer.rb26
-rw-r--r--app/services/account_search_service.rb22
-rw-r--r--app/services/activitypub/process_collection_service.rb2
-rw-r--r--app/services/authorize_follow_service.rb2
-rw-r--r--app/services/fetch_link_card_service.rb9
-rw-r--r--app/services/post_status_service.rb2
-rw-r--r--app/services/process_feed_service.rb2
-rw-r--r--app/services/remove_status_service.rb2
-rw-r--r--app/services/search_service.rb2
-rw-r--r--app/services/suspend_account_service.rb2
-rw-r--r--app/views/about/more.html.haml1
-rw-r--r--app/views/about/show.html.haml1
-rw-r--r--app/views/admin/reports/show.html.haml3
-rw-r--r--app/views/admin/statuses/index.html.haml3
-rw-r--r--app/views/home/index.html.haml6
-rw-r--r--app/views/layouts/_theme.html.haml13
-rw-r--r--app/views/layouts/admin.html.haml3
-rwxr-xr-xapp/views/layouts/application.html.haml10
-rw-r--r--app/views/layouts/auth.html.haml3
-rw-r--r--app/views/layouts/embedded.html.haml8
-rw-r--r--app/views/layouts/error.html.haml4
-rw-r--r--app/views/layouts/modal.html.haml3
-rw-r--r--app/views/layouts/public.html.haml3
-rw-r--r--app/views/settings/preferences/show.html.haml5
-rw-r--r--app/views/shares/show.html.haml1
-rw-r--r--app/views/tags/show.html.haml1
-rw-r--r--app/views/well_known/webfinger/show.json.rabl18
-rw-r--r--app/views/well_known/webfinger/show.xml.ruby4
-rw-r--r--app/workers/scheduler/feed_cleanup_scheduler.rb40
-rw-r--r--config/initializers/oj.rb1
-rw-r--r--config/initializers/rabl_init.rb7
-rw-r--r--config/locales/activerecord.de.yml2
-rw-r--r--config/locales/activerecord.nl.yml2
-rw-r--r--config/locales/ar.yml37
-rw-r--r--config/locales/ca.yml100
-rw-r--r--config/locales/de.yml41
-rw-r--r--config/locales/devise.ar.yml6
-rw-r--r--config/locales/devise.he.yml6
-rw-r--r--config/locales/devise.nl.yml3
-rw-r--r--config/locales/doorkeeper.ar.yml6
-rw-r--r--config/locales/doorkeeper.ca.yml19
-rw-r--r--config/locales/doorkeeper.es.yml14
-rw-r--r--config/locales/doorkeeper.fr.yml5
-rw-r--r--config/locales/doorkeeper.he.yml16
-rw-r--r--config/locales/doorkeeper.nl.yml13
-rw-r--r--config/locales/doorkeeper.pt-BR.yml6
-rw-r--r--config/locales/doorkeeper.pt.yml11
-rw-r--r--config/locales/es.yml165
-rw-r--r--config/locales/fr.yml132
-rw-r--r--config/locales/he.yml81
-rw-r--r--config/locales/ja.yml10
-rw-r--r--config/locales/ko.yml12
-rw-r--r--config/locales/nl.yml36
-rw-r--r--config/locales/oc.yml41
-rw-r--r--config/locales/pl.yml6
-rw-r--r--config/locales/pt-BR.yml59
-rw-r--r--config/locales/pt.yml72
-rw-r--r--config/locales/simple_form.ca.yml4
-rw-r--r--config/locales/simple_form.de.yml3
-rw-r--r--config/locales/simple_form.en.yml6
-rw-r--r--config/locales/simple_form.es.yml16
-rw-r--r--config/locales/simple_form.fr.yml3
-rw-r--r--config/locales/simple_form.he.yml16
-rw-r--r--config/locales/simple_form.nl.yml13
-rw-r--r--config/locales/simple_form.pt-BR.yml7
-rw-r--r--config/locales/simple_form.pt.yml1
-rw-r--r--config/locales/simple_form.zh-CN.yml2
-rw-r--r--config/locales/zh-CN.yml88
-rw-r--r--config/settings.yml3
-rw-r--r--config/webpack/configuration.js55
-rw-r--r--config/webpack/generateLocalePacks.js2
-rw-r--r--config/webpack/shared.js60
-rw-r--r--config/webpacker.yml12
-rw-r--r--db/migrate/20171130000000_add_embed_url_to_preview_cards.rb18
-rw-r--r--db/schema.rb3
-rw-r--r--lib/mastodon/migration_helpers.rb8
-rw-r--r--lib/mastodon/version.rb4
-rw-r--r--lib/tasks/mastodon.rake12
-rw-r--r--spec/controllers/api/v1/accounts/relationships_controller_spec.rb6
-rw-r--r--spec/controllers/api/v1/accounts_controller_spec.rb8
-rw-r--r--spec/services/account_search_service_spec.rb10
-rw-r--r--spec/services/post_status_service_spec.rb2
-rw-r--r--spec/services/search_service_spec.rb2
545 files changed, 6254 insertions, 1894 deletions
diff --git a/.gitmodules b/.gitmodules
index 35b0cd787..e69de29bb 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +0,0 @@
-[submodule "app/javascript/themes/mastodon-go"]
-	path = app/javascript/themes/mastodon-go
-	url = https://github.com/marrus-sh/mastodon-go
diff --git a/.travis.yml b/.travis.yml
index 5c2c2c889..777ca581c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -27,11 +27,14 @@ addons:
   apt:
     sources:
     - trusty-media
+    - sourceline: deb https://dl.yarnpkg.com/debian/ stable main
+      key_url: https://dl.yarnpkg.com/debian/pubkey.gpg
     packages:
     - ffmpeg
+    - libicu-dev
     - libprotobuf-dev
     - protobuf-compiler
-    - libicu-dev
+    - yarn
 
 rvm:
   - 2.3.4
@@ -42,7 +45,6 @@ services:
 
 install:
   - nvm install
-  - npm install -g yarn
   - bundle install --path=vendor/bundle --without development production --retry=3 --jobs=16
   - yarn install
 
diff --git a/Dockerfile b/Dockerfile
index f9354ac46..7cca02ecf 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -7,8 +7,8 @@ ENV UID=991 GID=991 \
     RAILS_SERVE_STATIC_FILES=true \
     RAILS_ENV=production NODE_ENV=production
 
-ARG YARN_VERSION=1.1.0
-ARG YARN_DOWNLOAD_SHA256=171c1f9ee93c488c0d774ac6e9c72649047c3f896277d88d0f805266519430f3
+ARG YARN_VERSION=1.3.2
+ARG YARN_DOWNLOAD_SHA256=6cfe82e530ef0837212f13e45c1565ba53f5199eec2527b85ecbcd88bf26821d
 ARG LIBICONV_VERSION=1.15
 ARG LIBICONV_DOWNLOAD_SHA256=ccf536620a45458d26ba83887a983b96827001e92a13847b45e4925cc8913178
 
diff --git a/Gemfile b/Gemfile
index d0b7aaef1..e43b1a256 100644
--- a/Gemfile
+++ b/Gemfile
@@ -49,7 +49,6 @@ gem 'oj', '~> 3.3'
 gem 'ostatus2', '~> 2.0'
 gem 'ox', '~> 2.8'
 gem 'pundit', '~> 1.1'
-gem 'rabl', '~> 0.13'
 gem 'rack-attack', '~> 5.0'
 gem 'rack-cors', '~> 0.4', require: 'rack/cors'
 gem 'rack-timeout', '~> 0.4'
diff --git a/Gemfile.lock b/Gemfile.lock
index 37fc77fdf..5f050d031 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -334,8 +334,6 @@ GEM
     puma (3.11.0)
     pundit (1.1.0)
       activesupport (>= 3.0.0)
-    rabl (0.13.1)
-      activesupport (>= 2.3.14)
     rack (2.0.3)
     rack-attack (5.0.1)
       rack
@@ -606,7 +604,6 @@ DEPENDENCIES
   pry-rails (~> 0.3)
   puma (~> 3.10)
   pundit (~> 1.1)
-  rabl (~> 0.13)
   rack-attack (~> 5.0)
   rack-cors (~> 0.4)
   rack-timeout (~> 0.4)
diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb
index 47690e81e..8785df14e 100644
--- a/app/controllers/about_controller.rb
+++ b/app/controllers/about_controller.rb
@@ -1,6 +1,7 @@
 # frozen_string_literal: true
 
 class AboutController < ApplicationController
+  before_action :set_pack
   before_action :set_body_classes
   before_action :set_instance_presenter, only: [:show, :more, :terms]
 
@@ -21,6 +22,10 @@ class AboutController < ApplicationController
 
   helper_method :new_user
 
+  def set_pack
+    use_pack action_name == 'show' ? 'about' : 'common'
+  end
+
   def set_instance_presenter
     @instance_presenter = InstancePresenter.new
   end
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index 75915b337..309cb65da 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -7,6 +7,7 @@ class AccountsController < ApplicationController
   def show
     respond_to do |format|
       format.html do
+        use_pack 'public'
         @pinned_statuses = []
 
         if current_account && @account.blocking?(current_account)
diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb
index 7fb69d578..fc299f74c 100644
--- a/app/controllers/admin/base_controller.rb
+++ b/app/controllers/admin/base_controller.rb
@@ -5,8 +5,13 @@ module Admin
     include Authorization
     include AccountableConcern
 
+    layout 'admin'
+
     before_action :require_staff!
+    before_action :set_pack
 
-    layout 'admin'
+    def set_pack
+      use_pack 'admin'
+    end
   end
 end
diff --git a/app/controllers/api/v1/accounts/search_controller.rb b/app/controllers/api/v1/accounts/search_controller.rb
index 2a5cac547..11e647c3c 100644
--- a/app/controllers/api/v1/accounts/search_controller.rb
+++ b/app/controllers/api/v1/accounts/search_controller.rb
@@ -17,12 +17,13 @@ class Api::V1::Accounts::SearchController < Api::BaseController
     AccountSearchService.new.call(
       params[:q],
       limit_param(DEFAULT_ACCOUNTS_LIMIT),
-      resolving_search?,
-      current_account
+      current_account,
+      resolve: truthy_param?(:resolve),
+      following: truthy_param?(:following)
     )
   end
 
-  def resolving_search?
-    params[:resolve] == 'true'
+  def truthy_param?(key)
+    params[key] == 'true'
   end
 end
diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb
index b1a2ed573..4e73e9e8b 100644
--- a/app/controllers/api/v1/accounts_controller.rb
+++ b/app/controllers/api/v1/accounts_controller.rb
@@ -51,7 +51,7 @@ class Api::V1::AccountsController < Api::BaseController
     @account = Account.find(params[:id])
   end
 
-  def relationships(options = {})
+  def relationships(**options)
     AccountRelationshipsPresenter.new([@account.id], current_user.account_id, options)
   end
 end
diff --git a/app/controllers/api/v1/lists/accounts_controller.rb b/app/controllers/api/v1/lists/accounts_controller.rb
index 40c485e8d..c29c73b3e 100644
--- a/app/controllers/api/v1/lists/accounts_controller.rb
+++ b/app/controllers/api/v1/lists/accounts_controller.rb
@@ -10,7 +10,7 @@ class Api::V1::Lists::AccountsController < Api::BaseController
   after_action :insert_pagination_headers, only: :show
 
   def show
-    @accounts = @list.accounts.paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id])
+    @accounts = load_accounts
     render json: @accounts, each_serializer: REST::AccountSerializer
   end
 
@@ -35,6 +35,14 @@ class Api::V1::Lists::AccountsController < Api::BaseController
     @list = List.where(account: current_account).find(params[:list_id])
   end
 
+  def load_accounts
+    if unlimited?
+      @list.accounts.all
+    else
+      @list.accounts.paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id])
+    end
+  end
+
   def list_accounts
     Account.find(account_ids)
   end
@@ -52,12 +60,16 @@ class Api::V1::Lists::AccountsController < Api::BaseController
   end
 
   def next_path
+    return if unlimited?
+
     if records_continue?
       api_v1_list_accounts_url pagination_params(max_id: pagination_max_id)
     end
   end
 
   def prev_path
+    return if unlimited?
+
     unless @accounts.empty?
       api_v1_list_accounts_url pagination_params(since_id: pagination_since_id)
     end
@@ -78,4 +90,8 @@ class Api::V1::Lists::AccountsController < Api::BaseController
   def pagination_params(core_params)
     params.permit(:limit).merge(core_params)
   end
+
+  def unlimited?
+    params[:limit] == '0'
+  end
 end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index f5dbe837e..c6d148c8c 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -12,8 +12,8 @@ class ApplicationController < ActionController::Base
 
   helper_method :current_account
   helper_method :current_session
-  helper_method :current_theme
-  helper_method :theme_data
+  helper_method :current_flavour
+  helper_method :current_skin
   helper_method :single_user_mode?
 
   rescue_from ActionController::RoutingError, with: :not_found
@@ -54,6 +54,73 @@ class ApplicationController < ActionController::Base
     new_user_session_path
   end
 
+  def pack(data, pack_name, skin = 'default')
+    return nil unless pack?(data, pack_name)
+    pack_data = {
+      common: pack_name == 'common' ? nil : resolve_pack(data['name'] ? Themes.instance.flavour(current_flavour) : Themes.instance.core, 'common', skin),
+      flavour: data['name'],
+      pack: pack_name,
+      preload: nil,
+      skin: nil,
+    }
+    if data['pack'][pack_name].is_a?(Hash)
+      pack_data[:common] = nil if data['pack'][pack_name]['use_common'] == false
+      pack_data[:pack] = nil unless data['pack'][pack_name]['filename']
+      if data['pack'][pack_name]['preload']
+        pack_data[:preload] = [data['pack'][pack_name]['preload']] if data['pack'][pack_name]['preload'].is_a?(String)
+        pack_data[:preload] = data['pack'][pack_name]['preload'] if data['pack'][pack_name]['preload'].is_a?(Array)
+      end
+      if skin != 'default' && data['skin'][skin]
+        pack_data[:skin] = skin if data['skin'][skin].include?(pack_name)
+      else  #  default skin
+        pack_data[:skin] = 'default' if data['pack'][pack_name]['stylesheet']
+      end
+    end
+    pack_data
+  end
+
+  def pack?(data, pack_name)
+    if data['pack'].is_a?(Hash) && data['pack'].key?(pack_name)
+      return true if data['pack'][pack_name].is_a?(String) || data['pack'][pack_name].is_a?(Hash)
+    end
+    false
+  end
+
+  def nil_pack(data, pack_name, skin = 'default')
+    {
+      common: pack_name == 'common' ? nil : resolve_pack(data['name'] ? Themes.instance.flavour(current_flavour) : Themes.instance.core, 'common', skin),
+      flavour: data['name'],
+      pack: nil,
+      preload: nil,
+      skin: nil,
+    }
+  end
+
+  def resolve_pack(data, pack_name, skin = 'default')
+    result = pack(data, pack_name, skin)
+    unless result
+      if data['name'] && data.key?('fallback')
+        if data['fallback'].nil?
+          return nil_pack(data, pack_name, skin)
+        elsif data['fallback'].is_a?(String) && Themes.instance.flavour(data['fallback'])
+          return resolve_pack(Themes.instance.flavour(data['fallback']), pack_name)
+        elsif data['fallback'].is_a?(Array)
+          data['fallback'].each do |fallback|
+            return resolve_pack(Themes.instance.flavour(fallback), pack_name) if Themes.instance.flavour(fallback)
+          end
+        end
+        return nil_pack(data, pack_name, skin)
+      end
+      return data.key?('name') && data['name'] != Setting.default_settings['flavour'] ? resolve_pack(Themes.instance.flavour(Setting.default_settings['flavour']), pack_name) : nil_pack(data, pack_name, skin)
+    end
+    result
+  end
+
+  def use_pack(pack_name)
+    @core = resolve_pack(Themes.instance.core, pack_name)
+    @theme = resolve_pack(Themes.instance.flavour(current_flavour), pack_name, current_skin)
+  end
+
   protected
 
   def forbidden
@@ -84,13 +151,14 @@ class ApplicationController < ActionController::Base
     @current_session ||= SessionActivation.find_by(session_id: cookies.signed['_session_id'])
   end
 
-  def current_theme
-    return Setting.default_settings['theme'] unless Themes.instance.names.include? current_user&.setting_theme
-    current_user.setting_theme
+  def current_flavour
+    return Setting.default_settings['flavour'] unless Themes.instance.flavours.include? current_user&.setting_flavour
+    current_user.setting_flavour
   end
 
-  def theme_data
-    Themes.instance.get(current_theme)
+  def current_skin
+    return 'default' unless Themes.instance.skins_for(current_flavour).include? current_user&.setting_skin
+    current_user.setting_skin
   end
 
   def cache_collection(raw, klass)
diff --git a/app/controllers/auth/confirmations_controller.rb b/app/controllers/auth/confirmations_controller.rb
index d5e8e58ed..5ffa1c9a3 100644
--- a/app/controllers/auth/confirmations_controller.rb
+++ b/app/controllers/auth/confirmations_controller.rb
@@ -2,10 +2,17 @@
 
 class Auth::ConfirmationsController < Devise::ConfirmationsController
   layout 'auth'
+  before_action :set_pack
 
   def show
     super do |user|
       BootstrapTimelineWorker.perform_async(user.account_id) if user.errors.empty?
     end
   end
+  
+  private
+
+  def set_pack
+    use_pack 'auth'
+  end
 end
diff --git a/app/controllers/auth/passwords_controller.rb b/app/controllers/auth/passwords_controller.rb
index 171b997dc..e0400aa3d 100644
--- a/app/controllers/auth/passwords_controller.rb
+++ b/app/controllers/auth/passwords_controller.rb
@@ -2,6 +2,7 @@
 
 class Auth::PasswordsController < Devise::PasswordsController
   before_action :check_validity_of_reset_password_token, only: :edit
+  before_action :set_pack
 
   layout 'auth'
 
@@ -17,4 +18,8 @@ class Auth::PasswordsController < Devise::PasswordsController
   def reset_password_token_is_valid?
     resource_class.with_reset_password_token(params[:reset_password_token]).present?
   end
+
+  def set_pack
+    use_pack 'auth'
+  end
 end
diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb
index da0b6512f..f4247fd95 100644
--- a/app/controllers/auth/registrations_controller.rb
+++ b/app/controllers/auth/registrations_controller.rb
@@ -5,6 +5,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
 
   before_action :check_enabled_registrations, only: [:new, :create]
   before_action :configure_sign_up_params, only: [:create]
+  before_action :set_pack
   before_action :set_sessions, only: [:edit, :update]
   before_action :set_instance_presenter, only: [:new, :create, :update]
 
@@ -55,6 +56,10 @@ class Auth::RegistrationsController < Devise::RegistrationsController
 
   private
 
+  def set_pack
+    use_pack %w(edit update).include?(action_name) ? 'admin' : 'auth'
+  end
+
   def set_instance_presenter
     @instance_presenter = InstancePresenter.new
   end
diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb
index a5acb6c36..72d544102 100644
--- a/app/controllers/auth/sessions_controller.rb
+++ b/app/controllers/auth/sessions_controller.rb
@@ -9,6 +9,7 @@ class Auth::SessionsController < Devise::SessionsController
   skip_before_action :check_suspension, only: [:destroy]
   prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create]
   before_action :set_instance_presenter, only: [:new]
+  before_action :set_pack
 
   def create
     super do |resource|
@@ -85,6 +86,10 @@ class Auth::SessionsController < Devise::SessionsController
 
   private
 
+  def set_pack
+    use_pack 'auth'
+  end
+
   def set_instance_presenter
     @instance_presenter = InstancePresenter.new
   end
diff --git a/app/controllers/authorize_follows_controller.rb b/app/controllers/authorize_follows_controller.rb
index 78b564183..2d29bd379 100644
--- a/app/controllers/authorize_follows_controller.rb
+++ b/app/controllers/authorize_follows_controller.rb
@@ -4,6 +4,7 @@ class AuthorizeFollowsController < ApplicationController
   layout 'modal'
 
   before_action :authenticate_user!
+  before_action :set_pack
 
   def show
     @account = located_account || render(:error)
@@ -23,6 +24,10 @@ class AuthorizeFollowsController < ApplicationController
 
   private
 
+  def set_pack
+    use_pack 'modal'
+  end
+
   def follow_attempt
     FollowService.new.call(current_account, acct_without_prefix)
   end
diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb
index 399e79665..080cbde11 100644
--- a/app/controllers/follower_accounts_controller.rb
+++ b/app/controllers/follower_accounts_controller.rb
@@ -7,7 +7,9 @@ class FollowerAccountsController < ApplicationController
     @follows = Follow.where(target_account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account)
 
     respond_to do |format|
-      format.html
+      format.html do
+        use_pack 'public'
+      end
 
       format.json do
         render json: collection_presenter,
diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb
index 1e73d4bd4..74e83ad81 100644
--- a/app/controllers/following_accounts_controller.rb
+++ b/app/controllers/following_accounts_controller.rb
@@ -7,7 +7,9 @@ class FollowingAccountsController < ApplicationController
     @follows = Follow.where(account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account)
 
     respond_to do |format|
-      format.html
+      format.html do
+        use_pack 'public'
+      end
 
       format.json do
         render json: collection_presenter,
diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb
index 21dde20ce..7437a647e 100644
--- a/app/controllers/home_controller.rb
+++ b/app/controllers/home_controller.rb
@@ -2,6 +2,7 @@
 
 class HomeController < ApplicationController
   before_action :authenticate_user!
+  before_action :set_pack
   before_action :set_initial_state_json
 
   def index
@@ -37,6 +38,10 @@ class HomeController < ApplicationController
     redirect_to(default_redirect_path)
   end
 
+  def set_pack
+    use_pack 'home'
+  end
+
   def set_initial_state_json
     serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
     @initial_state_json   = serializable_resource.to_json
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
index 38d6c8d73..189e4072e 100644
--- a/app/controllers/invites_controller.rb
+++ b/app/controllers/invites_controller.rb
@@ -6,6 +6,7 @@ class InvitesController < ApplicationController
   layout 'admin'
 
   before_action :authenticate_user!
+  before_action :set_pack
 
   def index
     authorize :invite, :create?
@@ -37,6 +38,10 @@ class InvitesController < ApplicationController
 
   private
 
+  def set_pack
+    use_pack 'settings'
+  end
+
   def resource_params
     params.require(:invite).permit(:max_uses, :expires_in)
   end
diff --git a/app/controllers/remote_follow_controller.rb b/app/controllers/remote_follow_controller.rb
index 48b026aa5..e6f379886 100644
--- a/app/controllers/remote_follow_controller.rb
+++ b/app/controllers/remote_follow_controller.rb
@@ -4,6 +4,7 @@ class RemoteFollowController < ApplicationController
   layout 'modal'
 
   before_action :set_account
+  before_action :set_pack
   before_action :gone, if: :suspended_account?
 
   def new
@@ -31,6 +32,10 @@ class RemoteFollowController < ApplicationController
     { acct: session[:remote_follow] }
   end
 
+  def set_pack
+    use_pack 'modal'
+  end
+
   def set_account
     @account = Account.find_local!(params[:account_username])
   end
diff --git a/app/controllers/settings/applications_controller.rb b/app/controllers/settings/applications_controller.rb
index 8fc9a0fa9..35a6f7f9e 100644
--- a/app/controllers/settings/applications_controller.rb
+++ b/app/controllers/settings/applications_controller.rb
@@ -1,9 +1,7 @@
 # frozen_string_literal: true
 
-class Settings::ApplicationsController < ApplicationController
-  layout 'admin'
+class Settings::ApplicationsController < Settings::BaseController
 
-  before_action :authenticate_user!
   before_action :set_application, only: [:show, :update, :destroy, :regenerate]
   before_action :prepare_scopes, only: [:create, :update]
 
diff --git a/app/controllers/settings/base_controller.rb b/app/controllers/settings/base_controller.rb
new file mode 100644
index 000000000..7322d461b
--- /dev/null
+++ b/app/controllers/settings/base_controller.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class Settings::BaseController < ApplicationController
+  layout 'admin'
+
+  before_action :authenticate_user!
+  before_action :set_pack
+
+  def set_pack
+    use_pack 'settings'
+  end
+end
diff --git a/app/controllers/settings/deletes_controller.rb b/app/controllers/settings/deletes_controller.rb
index 80002b995..4c1121471 100644
--- a/app/controllers/settings/deletes_controller.rb
+++ b/app/controllers/settings/deletes_controller.rb
@@ -1,10 +1,8 @@
 # frozen_string_literal: true
 
-class Settings::DeletesController < ApplicationController
-  layout 'admin'
+class Settings::DeletesController < Settings::BaseController
 
-  before_action :check_enabled_deletion
-  before_action :authenticate_user!
+  prepend_before_action :check_enabled_deletion
 
   def show
     @confirmation = Form::DeleteConfirmation.new
diff --git a/app/controllers/settings/exports_controller.rb b/app/controllers/settings/exports_controller.rb
index ae62f00c1..9c03ece86 100644
--- a/app/controllers/settings/exports_controller.rb
+++ b/app/controllers/settings/exports_controller.rb
@@ -1,10 +1,6 @@
 # frozen_string_literal: true
 
-class Settings::ExportsController < ApplicationController
-  layout 'admin'
-
-  before_action :authenticate_user!
-
+class Settings::ExportsController < Settings::BaseController
   def show
     @export = Export.new(current_account)
   end
diff --git a/app/controllers/settings/follower_domains_controller.rb b/app/controllers/settings/follower_domains_controller.rb
index 9968504e5..141b2270d 100644
--- a/app/controllers/settings/follower_domains_controller.rb
+++ b/app/controllers/settings/follower_domains_controller.rb
@@ -2,11 +2,7 @@
 
 require 'sidekiq-bulk'
 
-class Settings::FollowerDomainsController < ApplicationController
-  layout 'admin'
-
-  before_action :authenticate_user!
-
+class Settings::FollowerDomainsController < Settings::BaseController
   def show
     @account = current_account
     @domains = current_account.followers.reorder('MIN(follows.id) DESC').group('accounts.domain').select('accounts.domain, count(accounts.id) as accounts_from_domain').page(params[:page]).per(10)
diff --git a/app/controllers/settings/imports_controller.rb b/app/controllers/settings/imports_controller.rb
index 0db13d1ca..dbd136ebe 100644
--- a/app/controllers/settings/imports_controller.rb
+++ b/app/controllers/settings/imports_controller.rb
@@ -1,9 +1,6 @@
 # frozen_string_literal: true
 
-class Settings::ImportsController < ApplicationController
-  layout 'admin'
-
-  before_action :authenticate_user!
+class Settings::ImportsController < Settings::BaseController
   before_action :set_account
 
   def show
diff --git a/app/controllers/settings/keyword_mutes_controller.rb b/app/controllers/settings/keyword_mutes_controller.rb
index f79e1b320..699b8a3ef 100644
--- a/app/controllers/settings/keyword_mutes_controller.rb
+++ b/app/controllers/settings/keyword_mutes_controller.rb
@@ -1,9 +1,6 @@
 # frozen_string_literal: true
 
-class Settings::KeywordMutesController < ApplicationController
-  layout 'admin'
-
-  before_action :authenticate_user!
+class Settings::KeywordMutesController < Settings::BaseController
   before_action :load_keyword_mute, only: [:edit, :update, :destroy]
 
   def index
diff --git a/app/controllers/settings/notifications_controller.rb b/app/controllers/settings/notifications_controller.rb
index ce2530c54..6286e3ebf 100644
--- a/app/controllers/settings/notifications_controller.rb
+++ b/app/controllers/settings/notifications_controller.rb
@@ -1,10 +1,6 @@
 # frozen_string_literal: true
 
-class Settings::NotificationsController < ApplicationController
-  layout 'admin'
-
-  before_action :authenticate_user!
-
+class Settings::NotificationsController < Settings::BaseController
   def show; end
 
   def update
diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb
index 069026715..277f0f657 100644
--- a/app/controllers/settings/preferences_controller.rb
+++ b/app/controllers/settings/preferences_controller.rb
@@ -1,10 +1,6 @@
 # frozen_string_literal: true
 
-class Settings::PreferencesController < ApplicationController
-  layout 'admin'
-
-  before_action :authenticate_user!
-
+class Settings::PreferencesController < Settings::BaseController
   def show; end
 
   def update
@@ -42,7 +38,8 @@ class Settings::PreferencesController < ApplicationController
       :setting_reduce_motion,
       :setting_system_font_ui,
       :setting_noindex,
-      :setting_theme,
+      :setting_flavour,
+      :setting_skin,
       notification_emails: %i(follow follow_request reblog favourite mention digest),
       interactions: %i(must_be_follower must_be_following)
     )
diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb
index 28f78a4fb..dadc3d911 100644
--- a/app/controllers/settings/profiles_controller.rb
+++ b/app/controllers/settings/profiles_controller.rb
@@ -1,11 +1,8 @@
 # frozen_string_literal: true
 
-class Settings::ProfilesController < ApplicationController
+class Settings::ProfilesController < Settings::BaseController
   include ObfuscateFilename
 
-  layout 'admin'
-
-  before_action :authenticate_user!
   before_action :set_account
 
   obfuscate_filename [:account, :avatar]
diff --git a/app/controllers/settings/sessions_controller.rb b/app/controllers/settings/sessions_controller.rb
index 0da1b027b..780ea64b4 100644
--- a/app/controllers/settings/sessions_controller.rb
+++ b/app/controllers/settings/sessions_controller.rb
@@ -1,5 +1,6 @@
 # frozen_string_literal: true
 
+#  Intentionally does not inherit from BaseController
 class Settings::SessionsController < ApplicationController
   before_action :set_session, only: :destroy
 
diff --git a/app/controllers/settings/two_factor_authentications_controller.rb b/app/controllers/settings/two_factor_authentications_controller.rb
index 863cc7351..8c7737e9d 100644
--- a/app/controllers/settings/two_factor_authentications_controller.rb
+++ b/app/controllers/settings/two_factor_authentications_controller.rb
@@ -1,10 +1,7 @@
 # frozen_string_literal: true
 
 module Settings
-  class TwoFactorAuthenticationsController < ApplicationController
-    layout 'admin'
-
-    before_action :authenticate_user!
+  class TwoFactorAuthenticationsController < BaseController
     before_action :verify_otp_required, only: [:create]
 
     def show
diff --git a/app/controllers/shares_controller.rb b/app/controllers/shares_controller.rb
index 994742c3d..81d279c8b 100644
--- a/app/controllers/shares_controller.rb
+++ b/app/controllers/shares_controller.rb
@@ -4,6 +4,7 @@ class SharesController < ApplicationController
   layout 'modal'
 
   before_action :authenticate_user!
+  before_action :set_pack
   before_action :set_body_classes
 
   def show
@@ -24,6 +25,10 @@ class SharesController < ApplicationController
     }
   end
 
+  def set_pack
+    use_pack 'share'
+  end
+
   def set_body_classes
     @body_classes = 'compose-standalone'
   end
diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb
index e8a360fb5..84c9e7685 100644
--- a/app/controllers/statuses_controller.rb
+++ b/app/controllers/statuses_controller.rb
@@ -14,6 +14,7 @@ class StatusesController < ApplicationController
   def show
     respond_to do |format|
       format.html do
+        use_pack 'public'
         @ancestors   = @status.reply? ? cache_collection(@status.ancestors(current_account), Status) : []
         @descendants = cache_collection(@status.descendants(current_account), Status)
 
@@ -37,6 +38,7 @@ class StatusesController < ApplicationController
   end
 
   def embed
+    use_pack 'embed'
     response.headers['X-Frame-Options'] = 'ALLOWALL'
     render 'stream_entries/embed', layout: 'embedded'
   end
diff --git a/app/controllers/stream_entries_controller.rb b/app/controllers/stream_entries_controller.rb
index 5f61e2182..b597ba4bb 100644
--- a/app/controllers/stream_entries_controller.rb
+++ b/app/controllers/stream_entries_controller.rb
@@ -14,6 +14,7 @@ class StreamEntriesController < ApplicationController
   def show
     respond_to do |format|
       format.html do
+        use_pack 'public'
         @ancestors   = @stream_entry.activity.reply? ? cache_collection(@stream_entry.activity.ancestors(current_account), Status) : []
         @descendants = cache_collection(@stream_entry.activity.descendants(current_account), Status)
       end
diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb
index 9f3090e37..5d11a8139 100644
--- a/app/controllers/tags_controller.rb
+++ b/app/controllers/tags_controller.rb
@@ -9,6 +9,7 @@ class TagsController < ApplicationController
 
     respond_to do |format|
       format.html do
+        use_pack 'about'
         serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
         @initial_state_json   = serializable_resource.to_json
       end
diff --git a/app/controllers/well_known/webfinger_controller.rb b/app/controllers/well_known/webfinger_controller.rb
index 1c27b2b18..5cc606808 100644
--- a/app/controllers/well_known/webfinger_controller.rb
+++ b/app/controllers/well_known/webfinger_controller.rb
@@ -6,12 +6,10 @@ module WellKnown
 
     def show
       @account = Account.find_local!(username_from_resource)
-      @canonical_account_uri = @account.to_webfinger_s
-      @magic_key = pem_to_magic_key(@account.keypair.public_key)
 
       respond_to do |format|
         format.any(:json, :html) do
-          render formats: :json, content_type: 'application/jrd+json'
+          render json: @account, serializer: WebfingerSerializer, content_type: 'application/jrd+json'
         end
 
         format.xml do
@@ -35,21 +33,6 @@ module WellKnown
       WebfingerResource.new(resource_user).username
     end
 
-    def pem_to_magic_key(public_key)
-      modulus, exponent = [public_key.n, public_key.e].map do |component|
-        result = []
-
-        until component.zero?
-          result << [component % 256].pack('C')
-          component >>= 8
-        end
-
-        result.reverse.join
-      end
-
-      (['RSA'] + [modulus, exponent].map { |n| Base64.urlsafe_encode64(n) }).join('.')
-    end
-
     def resource_param
       params.require(:resource)
     end
diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb
index 73250cbf5..9443934b3 100644
--- a/app/helpers/admin/filter_helper.rb
+++ b/app/helpers/admin/filter_helper.rb
@@ -13,7 +13,7 @@ module Admin::FilterHelper
     link_to text, new_url, class: filter_link_class(new_class)
   end
 
-  def table_link_to(icon, text, path, options = {})
+  def table_link_to(icon, text, path, **options)
     link_to safe_join([fa_icon(icon), text]), path, options.merge(class: 'table-action-link')
   end
 
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 7dfab1df1..8ed5c8bda 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -5,7 +5,7 @@ module ApplicationHelper
     current_page?(path) ? 'active' : ''
   end
 
-  def active_link_to(label, path, options = {})
+  def active_link_to(label, path, **options)
     link_to label, path, options.merge(class: active_nav_class(path))
   end
 
diff --git a/app/helpers/routing_helper.rb b/app/helpers/routing_helper.rb
index f4693358c..11894a895 100644
--- a/app/helpers/routing_helper.rb
+++ b/app/helpers/routing_helper.rb
@@ -11,7 +11,7 @@ module RoutingHelper
     end
   end
 
-  def full_asset_url(source, options = {})
+  def full_asset_url(source, **options)
     source = ActionController::Base.helpers.asset_url(source, options) unless use_storage?
 
     URI.join(root_url, source).to_s
diff --git a/app/javascript/packs/admin.js b/app/javascript/core/admin.js
index 993827db5..c0bd09bdd 100644
--- a/app/javascript/packs/admin.js
+++ b/app/javascript/core/admin.js
@@ -1,3 +1,5 @@
+//  This file will be loaded on admin pages, regardless of theme.
+
 import { delegate } from 'rails-ujs';
 
 function handleDeleteStatus(event) {
diff --git a/app/javascript/core/common.js b/app/javascript/core/common.js
new file mode 100644
index 000000000..a7073ef0e
--- /dev/null
+++ b/app/javascript/core/common.js
@@ -0,0 +1,8 @@
+//  This file will be loaded on all pages, regardless of theme.
+
+import { start } from 'rails-ujs';
+import 'font-awesome/css/font-awesome.css';
+
+require.context('../images/', true);
+
+start();
diff --git a/app/javascript/core/embed.js b/app/javascript/core/embed.js
new file mode 100644
index 000000000..6146e6592
--- /dev/null
+++ b/app/javascript/core/embed.js
@@ -0,0 +1,23 @@
+//  This file will be loaded on embed pages, regardless of theme.
+
+window.addEventListener('message', e => {
+  const data = e.data || {};
+
+  if (!window.parent || data.type !== 'setHeight') {
+    return;
+  }
+
+  function setEmbedHeight () {
+    window.parent.postMessage({
+      type: 'setHeight',
+      id: data.id,
+      height: document.getElementsByTagName('html')[0].scrollHeight,
+    }, '*');
+  };
+
+  if (['interactive', 'complete'].includes(document.readyState)) {
+    setEmbedHeight();
+  } else {
+    document.addEventListener('DOMContentLoaded', setEmbedHeight);
+  }
+});
diff --git a/app/javascript/core/public.js b/app/javascript/core/public.js
new file mode 100644
index 000000000..47c34a259
--- /dev/null
+++ b/app/javascript/core/public.js
@@ -0,0 +1,25 @@
+//  This file will be loaded on public pages, regardless of theme.
+
+const { delegate } = require('rails-ujs');
+
+delegate(document, '.webapp-btn', 'click', ({ target, button }) => {
+  if (button !== 0) {
+    return true;
+  }
+  window.location.href = target.href;
+  return false;
+});
+
+delegate(document, '.status__content__spoiler-link', 'click', ({ target }) => {
+  const contentEl = target.parentNode.parentNode.querySelector('.e-content');
+
+  if (contentEl.style.display === 'block') {
+    contentEl.style.display = 'none';
+    target.parentNode.style.marginBottom = 0;
+  } else {
+    contentEl.style.display = 'block';
+    target.parentNode.style.marginBottom = null;
+  }
+
+  return false;
+});
diff --git a/app/javascript/core/settings.js b/app/javascript/core/settings.js
new file mode 100644
index 000000000..ada5fba2b
--- /dev/null
+++ b/app/javascript/core/settings.js
@@ -0,0 +1,43 @@
+//  This file will be loaded on settings pages, regardless of theme.
+
+const { length } = require('stringz');
+const { delegate } = require('rails-ujs');
+
+import { processBio } from 'flavours/glitch/util/bio_metadata';
+
+delegate(document, '.account_display_name', 'input', ({ target }) => {
+  const nameCounter = document.querySelector('.name-counter');
+
+  if (nameCounter) {
+    nameCounter.textContent = 30 - length(target.value);
+  }
+});
+
+delegate(document, '.account_note', 'input', ({ target }) => {
+  const noteCounter = document.querySelector('.note-counter');
+
+  if (noteCounter) {
+    const noteWithoutMetadata = processBio(target.value).text;
+    noteCounter.textContent = 500 - length(noteWithoutMetadata);
+  }
+});
+
+delegate(document, '#account_avatar', 'change', ({ target }) => {
+  const avatar = document.querySelector('.card.compact .avatar img');
+  const [file] = target.files || [];
+  const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc;
+
+  avatar.src = url;
+});
+
+delegate(document, '#account_header', 'change', ({ target }) => {
+  const header = document.querySelector('.card.compact');
+  const [file] = target.files || [];
+  const url = file ? URL.createObjectURL(file) : header.dataset.originalSrc;
+
+  header.style.backgroundImage = `url(${url})`;
+});
+
+delegate(document, '#user_setting_flavour, #user_setting_skin', 'change', ({ target }) => {
+  target.form.submit();
+});
diff --git a/app/javascript/core/theme.yml b/app/javascript/core/theme.yml
new file mode 100644
index 000000000..0dc05a149
--- /dev/null
+++ b/app/javascript/core/theme.yml
@@ -0,0 +1,16 @@
+#  These packs will be loaded on every appropriate page, regardless of
+#  theme.
+pack:
+  about:
+  admin: admin.js
+  auth:
+  common:
+    filename: common.js
+    stylesheet: true
+  embed: embed.js
+  error:
+  home:
+  modal:
+  public: public.js
+  settings: settings.js
+  share:
diff --git a/app/javascript/themes/glitch/actions/accounts.js b/app/javascript/flavours/glitch/actions/accounts.js
index f1a8c5471..8ab92f9e7 100644
--- a/app/javascript/themes/glitch/actions/accounts.js
+++ b/app/javascript/flavours/glitch/actions/accounts.js
@@ -1,4 +1,4 @@
-import api, { getLinks } from 'themes/glitch/util/api';
+import api, { getLinks } from 'flavours/glitch/util/api';
 
 export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
 export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS';
diff --git a/app/javascript/themes/glitch/actions/alerts.js b/app/javascript/flavours/glitch/actions/alerts.js
index f37fdeeb6..f37fdeeb6 100644
--- a/app/javascript/themes/glitch/actions/alerts.js
+++ b/app/javascript/flavours/glitch/actions/alerts.js
diff --git a/app/javascript/themes/glitch/actions/blocks.js b/app/javascript/flavours/glitch/actions/blocks.js
index 6ba9460f0..fe44ca19a 100644
--- a/app/javascript/themes/glitch/actions/blocks.js
+++ b/app/javascript/flavours/glitch/actions/blocks.js
@@ -1,4 +1,4 @@
-import api, { getLinks } from 'themes/glitch/util/api';
+import api, { getLinks } from 'flavours/glitch/util/api';
 import { fetchRelationships } from './accounts';
 
 export const BLOCKS_FETCH_REQUEST = 'BLOCKS_FETCH_REQUEST';
diff --git a/app/javascript/themes/glitch/actions/bundles.js b/app/javascript/flavours/glitch/actions/bundles.js
index ecc9c8f7d..ecc9c8f7d 100644
--- a/app/javascript/themes/glitch/actions/bundles.js
+++ b/app/javascript/flavours/glitch/actions/bundles.js
diff --git a/app/javascript/themes/glitch/actions/cards.js b/app/javascript/flavours/glitch/actions/cards.js
index 2a1bc369a..c897daf58 100644
--- a/app/javascript/themes/glitch/actions/cards.js
+++ b/app/javascript/flavours/glitch/actions/cards.js
@@ -1,4 +1,4 @@
-import api from 'themes/glitch/util/api';
+import api from 'flavours/glitch/util/api';
 
 export const STATUS_CARD_FETCH_REQUEST = 'STATUS_CARD_FETCH_REQUEST';
 export const STATUS_CARD_FETCH_SUCCESS = 'STATUS_CARD_FETCH_SUCCESS';
diff --git a/app/javascript/themes/glitch/actions/columns.js b/app/javascript/flavours/glitch/actions/columns.js
index bcb0cdf98..bcb0cdf98 100644
--- a/app/javascript/themes/glitch/actions/columns.js
+++ b/app/javascript/flavours/glitch/actions/columns.js
diff --git a/app/javascript/themes/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js
index 07c469477..32746f27b 100644
--- a/app/javascript/themes/glitch/actions/compose.js
+++ b/app/javascript/flavours/glitch/actions/compose.js
@@ -1,6 +1,6 @@
-import api from 'themes/glitch/util/api';
+import api from 'flavours/glitch/util/api';
 import { throttle } from 'lodash';
-import { search as emojiSearch } from 'themes/glitch/util/emoji/emoji_mart_search_light';
+import { search as emojiSearch } from 'flavours/glitch/util/emoji/emoji_mart_search_light';
 import { useEmoji } from './emojis';
 
 import {
diff --git a/app/javascript/themes/glitch/actions/domain_blocks.js b/app/javascript/flavours/glitch/actions/domain_blocks.js
index 0a880394a..8506df91c 100644
--- a/app/javascript/themes/glitch/actions/domain_blocks.js
+++ b/app/javascript/flavours/glitch/actions/domain_blocks.js
@@ -1,4 +1,4 @@
-import api, { getLinks } from 'themes/glitch/util/api';
+import api, { getLinks } from 'flavours/glitch/util/api';
 
 export const DOMAIN_BLOCK_REQUEST = 'DOMAIN_BLOCK_REQUEST';
 export const DOMAIN_BLOCK_SUCCESS = 'DOMAIN_BLOCK_SUCCESS';
diff --git a/app/javascript/themes/glitch/actions/emojis.js b/app/javascript/flavours/glitch/actions/emojis.js
index 7cd9d4b7b..7cd9d4b7b 100644
--- a/app/javascript/themes/glitch/actions/emojis.js
+++ b/app/javascript/flavours/glitch/actions/emojis.js
diff --git a/app/javascript/themes/glitch/actions/favourites.js b/app/javascript/flavours/glitch/actions/favourites.js
index e9b3559af..decdcee4f 100644
--- a/app/javascript/themes/glitch/actions/favourites.js
+++ b/app/javascript/flavours/glitch/actions/favourites.js
@@ -1,4 +1,4 @@
-import api, { getLinks } from 'themes/glitch/util/api';
+import api, { getLinks } from 'flavours/glitch/util/api';
 
 export const FAVOURITED_STATUSES_FETCH_REQUEST = 'FAVOURITED_STATUSES_FETCH_REQUEST';
 export const FAVOURITED_STATUSES_FETCH_SUCCESS = 'FAVOURITED_STATUSES_FETCH_SUCCESS';
diff --git a/app/javascript/themes/glitch/actions/height_cache.js b/app/javascript/flavours/glitch/actions/height_cache.js
index 4c752993f..4c752993f 100644
--- a/app/javascript/themes/glitch/actions/height_cache.js
+++ b/app/javascript/flavours/glitch/actions/height_cache.js
diff --git a/app/javascript/themes/glitch/actions/interactions.js b/app/javascript/flavours/glitch/actions/interactions.js
index d61a7ba2a..ceeb2773b 100644
--- a/app/javascript/themes/glitch/actions/interactions.js
+++ b/app/javascript/flavours/glitch/actions/interactions.js
@@ -1,4 +1,4 @@
-import api from 'themes/glitch/util/api';
+import api from 'flavours/glitch/util/api';
 
 export const REBLOG_REQUEST = 'REBLOG_REQUEST';
 export const REBLOG_SUCCESS = 'REBLOG_SUCCESS';
diff --git a/app/javascript/themes/glitch/actions/local_settings.js b/app/javascript/flavours/glitch/actions/local_settings.js
index 28660a4e8..28660a4e8 100644
--- a/app/javascript/themes/glitch/actions/local_settings.js
+++ b/app/javascript/flavours/glitch/actions/local_settings.js
diff --git a/app/javascript/themes/glitch/actions/modal.js b/app/javascript/flavours/glitch/actions/modal.js
index 80e15c28e..80e15c28e 100644
--- a/app/javascript/themes/glitch/actions/modal.js
+++ b/app/javascript/flavours/glitch/actions/modal.js
diff --git a/app/javascript/themes/glitch/actions/mutes.js b/app/javascript/flavours/glitch/actions/mutes.js
index bb19e8657..e06130533 100644
--- a/app/javascript/themes/glitch/actions/mutes.js
+++ b/app/javascript/flavours/glitch/actions/mutes.js
@@ -1,6 +1,6 @@
-import api, { getLinks } from 'themes/glitch/util/api';
+import api, { getLinks } from 'flavours/glitch/util/api';
 import { fetchRelationships } from './accounts';
-import { openModal } from 'themes/glitch/actions/modal';
+import { openModal } from 'flavours/glitch/actions/modal';
 
 export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST';
 export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS';
diff --git a/app/javascript/themes/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js
index fbf06f7c4..9b9ebf86d 100644
--- a/app/javascript/themes/glitch/actions/notifications.js
+++ b/app/javascript/flavours/glitch/actions/notifications.js
@@ -1,4 +1,4 @@
-import api, { getLinks } from 'themes/glitch/util/api';
+import api, { getLinks } from 'flavours/glitch/util/api';
 import { List as ImmutableList } from 'immutable';
 import IntlMessageFormat from 'intl-messageformat';
 import { fetchRelationships } from './accounts';
diff --git a/app/javascript/themes/glitch/actions/onboarding.js b/app/javascript/flavours/glitch/actions/onboarding.js
index a161c50ef..a161c50ef 100644
--- a/app/javascript/themes/glitch/actions/onboarding.js
+++ b/app/javascript/flavours/glitch/actions/onboarding.js
diff --git a/app/javascript/themes/glitch/actions/pin_statuses.js b/app/javascript/flavours/glitch/actions/pin_statuses.js
index b3e064e58..d3d1a154f 100644
--- a/app/javascript/themes/glitch/actions/pin_statuses.js
+++ b/app/javascript/flavours/glitch/actions/pin_statuses.js
@@ -1,10 +1,10 @@
-import api from 'themes/glitch/util/api';
+import api from 'flavours/glitch/util/api';
 
 export const PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST';
 export const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS';
 export const PINNED_STATUSES_FETCH_FAIL = 'PINNED_STATUSES_FETCH_FAIL';
 
-import { me } from 'themes/glitch/util/initial_state';
+import { me } from 'flavours/glitch/util/initial_state';
 
 export function fetchPinnedStatuses() {
   return (dispatch, getState) => {
diff --git a/app/javascript/themes/glitch/actions/push_notifications.js b/app/javascript/flavours/glitch/actions/push_notifications.js
index 55661d2b0..55661d2b0 100644
--- a/app/javascript/themes/glitch/actions/push_notifications.js
+++ b/app/javascript/flavours/glitch/actions/push_notifications.js
diff --git a/app/javascript/themes/glitch/actions/reports.js b/app/javascript/flavours/glitch/actions/reports.js
index 93f9085b2..ad4fd18a9 100644
--- a/app/javascript/themes/glitch/actions/reports.js
+++ b/app/javascript/flavours/glitch/actions/reports.js
@@ -1,4 +1,4 @@
-import api from 'themes/glitch/util/api';
+import api from 'flavours/glitch/util/api';
 import { openModal, closeModal } from './modal';
 
 export const REPORT_INIT   = 'REPORT_INIT';
diff --git a/app/javascript/themes/glitch/actions/search.js b/app/javascript/flavours/glitch/actions/search.js
index 414e4755e..e86bd848e 100644
--- a/app/javascript/themes/glitch/actions/search.js
+++ b/app/javascript/flavours/glitch/actions/search.js
@@ -1,4 +1,4 @@
-import api from 'themes/glitch/util/api';
+import api from 'flavours/glitch/util/api';
 
 export const SEARCH_CHANGE = 'SEARCH_CHANGE';
 export const SEARCH_CLEAR  = 'SEARCH_CLEAR';
diff --git a/app/javascript/themes/glitch/actions/settings.js b/app/javascript/flavours/glitch/actions/settings.js
index 79adca18c..79adca18c 100644
--- a/app/javascript/themes/glitch/actions/settings.js
+++ b/app/javascript/flavours/glitch/actions/settings.js
diff --git a/app/javascript/themes/glitch/actions/statuses.js b/app/javascript/flavours/glitch/actions/statuses.js
index 702f4e9b6..8b49083ac 100644
--- a/app/javascript/themes/glitch/actions/statuses.js
+++ b/app/javascript/flavours/glitch/actions/statuses.js
@@ -1,4 +1,4 @@
-import api from 'themes/glitch/util/api';
+import api from 'flavours/glitch/util/api';
 
 import { deleteFromTimelines } from './timelines';
 import { fetchStatusCard } from './cards';
diff --git a/app/javascript/themes/glitch/actions/store.js b/app/javascript/flavours/glitch/actions/store.js
index a1db0fdd5..a1db0fdd5 100644
--- a/app/javascript/themes/glitch/actions/store.js
+++ b/app/javascript/flavours/glitch/actions/store.js
diff --git a/app/javascript/themes/glitch/actions/streaming.js b/app/javascript/flavours/glitch/actions/streaming.js
index ccf6c27d8..595eefa41 100644
--- a/app/javascript/themes/glitch/actions/streaming.js
+++ b/app/javascript/flavours/glitch/actions/streaming.js
@@ -1,4 +1,4 @@
-import { connectStream } from 'themes/glitch/util/stream';
+import { connectStream } from 'flavours/glitch/util/stream';
 import {
   updateTimeline,
   deleteFromTimelines,
diff --git a/app/javascript/themes/glitch/actions/timelines.js b/app/javascript/flavours/glitch/actions/timelines.js
index 5ce14fbe9..3fabf3885 100644
--- a/app/javascript/themes/glitch/actions/timelines.js
+++ b/app/javascript/flavours/glitch/actions/timelines.js
@@ -1,4 +1,4 @@
-import api, { getLinks } from 'themes/glitch/util/api';
+import api, { getLinks } from 'flavours/glitch/util/api';
 import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 
 export const TIMELINE_UPDATE  = 'TIMELINE_UPDATE';
diff --git a/app/javascript/themes/glitch/components/account.js b/app/javascript/flavours/glitch/components/account.js
index d0ff77050..ac07051db 100644
--- a/app/javascript/themes/glitch/components/account.js
+++ b/app/javascript/flavours/glitch/components/account.js
@@ -7,7 +7,7 @@ import Permalink from './permalink';
 import IconButton from './icon_button';
 import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import { me } from 'themes/glitch/util/initial_state';
+import { me } from 'flavours/glitch/util/initial_state';
 
 const messages = defineMessages({
   follow: { id: 'account.follow', defaultMessage: 'Follow' },
@@ -81,7 +81,7 @@ export default class Account extends ImmutablePureComponent {
         buttons = <IconButton active icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
       } else if (muting) {
         let hidingNotificationsButton;
-        if (muting.get('notifications')) {
+        if (account.getIn(['relationship', 'muting_notifications'])) {
           hidingNotificationsButton = <IconButton active icon='bell' title={intl.formatMessage(messages.unmute_notifications, { name: account.get('username') })} onClick={this.handleUnmuteNotifications} />;
         } else {
           hidingNotificationsButton = <IconButton active icon='bell-slash' title={intl.formatMessage(messages.mute_notifications, { name: account.get('username')  })} onClick={this.handleMuteNotifications} />;
@@ -93,7 +93,7 @@ export default class Account extends ImmutablePureComponent {
           </div>
         );
       } else {
-        buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following ? true : false} />;
+        buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
       }
     }
 
diff --git a/app/javascript/themes/glitch/components/attachment_list.js b/app/javascript/flavours/glitch/components/attachment_list.js
index b3d00b335..b3d00b335 100644
--- a/app/javascript/themes/glitch/components/attachment_list.js
+++ b/app/javascript/flavours/glitch/components/attachment_list.js
diff --git a/app/javascript/themes/glitch/components/autosuggest_emoji.js b/app/javascript/flavours/glitch/components/autosuggest_emoji.js
index 3c6f915e4..79e113d9c 100644
--- a/app/javascript/themes/glitch/components/autosuggest_emoji.js
+++ b/app/javascript/flavours/glitch/components/autosuggest_emoji.js
@@ -1,6 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import unicodeMapping from 'themes/glitch/util/emoji/emoji_unicode_mapping_light';
+import unicodeMapping from 'flavours/glitch/util/emoji/emoji_unicode_mapping_light';
 
 const assetHost = process.env.CDN_HOST || '';
 
diff --git a/app/javascript/themes/glitch/components/autosuggest_textarea.js b/app/javascript/flavours/glitch/components/autosuggest_textarea.js
index fa93847a2..551528e5a 100644
--- a/app/javascript/themes/glitch/components/autosuggest_textarea.js
+++ b/app/javascript/flavours/glitch/components/autosuggest_textarea.js
@@ -1,9 +1,9 @@
 import React from 'react';
-import AutosuggestAccountContainer from 'themes/glitch/features/compose/containers/autosuggest_account_container';
+import AutosuggestAccountContainer from 'flavours/glitch/features/compose/containers/autosuggest_account_container';
 import AutosuggestEmoji from './autosuggest_emoji';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
-import { isRtl } from 'themes/glitch/util/rtl';
+import { isRtl } from 'flavours/glitch/util/rtl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import Textarea from 'react-textarea-autosize';
 import classNames from 'classnames';
diff --git a/app/javascript/themes/glitch/components/avatar.js b/app/javascript/flavours/glitch/components/avatar.js
index dd155f059..dd155f059 100644
--- a/app/javascript/themes/glitch/components/avatar.js
+++ b/app/javascript/flavours/glitch/components/avatar.js
diff --git a/app/javascript/themes/glitch/components/avatar_overlay.js b/app/javascript/flavours/glitch/components/avatar_overlay.js
index 2ecf9fa44..2ecf9fa44 100644
--- a/app/javascript/themes/glitch/components/avatar_overlay.js
+++ b/app/javascript/flavours/glitch/components/avatar_overlay.js
diff --git a/app/javascript/themes/glitch/components/button.js b/app/javascript/flavours/glitch/components/button.js
index 16868010c..16868010c 100644
--- a/app/javascript/themes/glitch/components/button.js
+++ b/app/javascript/flavours/glitch/components/button.js
diff --git a/app/javascript/themes/glitch/components/collapsable.js b/app/javascript/flavours/glitch/components/collapsable.js
index 8bc0a54f4..fe125a729 100644
--- a/app/javascript/themes/glitch/components/collapsable.js
+++ b/app/javascript/flavours/glitch/components/collapsable.js
@@ -1,5 +1,5 @@
 import React from 'react';
-import Motion from 'themes/glitch/util/optional_motion';
+import Motion from 'flavours/glitch/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import PropTypes from 'prop-types';
 
diff --git a/app/javascript/themes/glitch/components/column.js b/app/javascript/flavours/glitch/components/column.js
index adeba9cc1..57c4c7a40 100644
--- a/app/javascript/themes/glitch/components/column.js
+++ b/app/javascript/flavours/glitch/components/column.js
@@ -1,7 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import detectPassiveEvents from 'detect-passive-events';
-import { scrollTop } from 'themes/glitch/util/scroll';
+import { scrollTop } from 'flavours/glitch/util/scroll';
 
 export default class Column extends React.PureComponent {
 
diff --git a/app/javascript/themes/glitch/components/column_back_button.js b/app/javascript/flavours/glitch/components/column_back_button.js
index 50c3bf11f..50c3bf11f 100644
--- a/app/javascript/themes/glitch/components/column_back_button.js
+++ b/app/javascript/flavours/glitch/components/column_back_button.js
diff --git a/app/javascript/themes/glitch/components/column_back_button_slim.js b/app/javascript/flavours/glitch/components/column_back_button_slim.js
index 2cdf1b25b..2cdf1b25b 100644
--- a/app/javascript/themes/glitch/components/column_back_button_slim.js
+++ b/app/javascript/flavours/glitch/components/column_back_button_slim.js
diff --git a/app/javascript/themes/glitch/components/column_header.js b/app/javascript/flavours/glitch/components/column_header.js
index e601082c8..ae90b6f81 100644
--- a/app/javascript/themes/glitch/components/column_header.js
+++ b/app/javascript/flavours/glitch/components/column_header.js
@@ -4,8 +4,7 @@ import classNames from 'classnames';
 import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 
-// Glitch imports
-import NotificationPurgeButtonsContainer from 'themes/glitch/containers/notification_purge_buttons_container';
+import NotificationPurgeButtonsContainer from 'flavours/glitch/containers/notification_purge_buttons_container';
 
 const messages = defineMessages({
   show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
diff --git a/app/javascript/themes/glitch/components/display_name.js b/app/javascript/flavours/glitch/components/display_name.js
index 2cf84f8f4..2cf84f8f4 100644
--- a/app/javascript/themes/glitch/components/display_name.js
+++ b/app/javascript/flavours/glitch/components/display_name.js
diff --git a/app/javascript/themes/glitch/components/dropdown_menu.js b/app/javascript/flavours/glitch/components/dropdown_menu.js
index d30dc2aaf..d4a886a8b 100644
--- a/app/javascript/themes/glitch/components/dropdown_menu.js
+++ b/app/javascript/flavours/glitch/components/dropdown_menu.js
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import IconButton from './icon_button';
 import Overlay from 'react-overlays/lib/Overlay';
-import Motion from 'themes/glitch/util/optional_motion';
+import Motion from 'flavours/glitch/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import detectPassiveEvents from 'detect-passive-events';
 
diff --git a/app/javascript/themes/glitch/components/extended_video_player.js b/app/javascript/flavours/glitch/components/extended_video_player.js
index f8bd067e8..f8bd067e8 100644
--- a/app/javascript/themes/glitch/components/extended_video_player.js
+++ b/app/javascript/flavours/glitch/components/extended_video_player.js
diff --git a/app/javascript/themes/glitch/components/icon_button.js b/app/javascript/flavours/glitch/components/icon_button.js
index 31cdf4703..13b91e8a1 100644
--- a/app/javascript/themes/glitch/components/icon_button.js
+++ b/app/javascript/flavours/glitch/components/icon_button.js
@@ -1,5 +1,5 @@
 import React from 'react';
-import Motion from 'themes/glitch/util/optional_motion';
+import Motion from 'flavours/glitch/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import PropTypes from 'prop-types';
 import classNames from 'classnames';
diff --git a/app/javascript/themes/glitch/components/intersection_observer_article.js b/app/javascript/flavours/glitch/components/intersection_observer_article.js
index f0139ac75..8b06f9a8c 100644
--- a/app/javascript/themes/glitch/components/intersection_observer_article.js
+++ b/app/javascript/flavours/glitch/components/intersection_observer_article.js
@@ -1,7 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import scheduleIdleTask from 'themes/glitch/util/schedule_idle_task';
-import getRectFromEntry from 'themes/glitch/util/get_rect_from_entry';
+import scheduleIdleTask from 'flavours/glitch/util/schedule_idle_task';
+import getRectFromEntry from 'flavours/glitch/util/get_rect_from_entry';
 import { is } from 'immutable';
 
 // Diff these props in the "rendered" state
diff --git a/app/javascript/themes/glitch/components/load_more.js b/app/javascript/flavours/glitch/components/load_more.js
index c4c8c94a2..c4c8c94a2 100644
--- a/app/javascript/themes/glitch/components/load_more.js
+++ b/app/javascript/flavours/glitch/components/load_more.js
diff --git a/app/javascript/themes/glitch/components/loading_indicator.js b/app/javascript/flavours/glitch/components/loading_indicator.js
index d6a5adb6f..d6a5adb6f 100644
--- a/app/javascript/themes/glitch/components/loading_indicator.js
+++ b/app/javascript/flavours/glitch/components/loading_indicator.js
diff --git a/app/javascript/themes/glitch/components/media_gallery.js b/app/javascript/flavours/glitch/components/media_gallery.js
index b6b40c585..d2e80de49 100644
--- a/app/javascript/themes/glitch/components/media_gallery.js
+++ b/app/javascript/flavours/glitch/components/media_gallery.js
@@ -4,9 +4,9 @@ import PropTypes from 'prop-types';
 import { is } from 'immutable';
 import IconButton from './icon_button';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import { isIOS } from 'themes/glitch/util/is_mobile';
+import { isIOS } from 'flavours/glitch/util/is_mobile';
 import classNames from 'classnames';
-import { autoPlayGif } from 'themes/glitch/util/initial_state';
+import { autoPlayGif } from 'flavours/glitch/util/initial_state';
 
 const messages = defineMessages({
   toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
diff --git a/app/javascript/themes/glitch/components/missing_indicator.js b/app/javascript/flavours/glitch/components/missing_indicator.js
index 87df7f61c..87df7f61c 100644
--- a/app/javascript/themes/glitch/components/missing_indicator.js
+++ b/app/javascript/flavours/glitch/components/missing_indicator.js
diff --git a/app/javascript/themes/glitch/components/notification_purge_buttons.js b/app/javascript/flavours/glitch/components/notification_purge_buttons.js
index e0c1543b0..e0c1543b0 100644
--- a/app/javascript/themes/glitch/components/notification_purge_buttons.js
+++ b/app/javascript/flavours/glitch/components/notification_purge_buttons.js
diff --git a/app/javascript/themes/glitch/components/permalink.js b/app/javascript/flavours/glitch/components/permalink.js
index d726d37a2..d726d37a2 100644
--- a/app/javascript/themes/glitch/components/permalink.js
+++ b/app/javascript/flavours/glitch/components/permalink.js
diff --git a/app/javascript/themes/glitch/components/relative_timestamp.js b/app/javascript/flavours/glitch/components/relative_timestamp.js
index 51588e78c..51588e78c 100644
--- a/app/javascript/themes/glitch/components/relative_timestamp.js
+++ b/app/javascript/flavours/glitch/components/relative_timestamp.js
diff --git a/app/javascript/themes/glitch/components/scrollable_list.js b/app/javascript/flavours/glitch/components/scrollable_list.js
index ccdcd7c85..8b1e3c93d 100644
--- a/app/javascript/themes/glitch/components/scrollable_list.js
+++ b/app/javascript/flavours/glitch/components/scrollable_list.js
@@ -1,13 +1,13 @@
 import React, { PureComponent } from 'react';
 import { ScrollContainer } from 'react-router-scroll-4';
 import PropTypes from 'prop-types';
-import IntersectionObserverArticleContainer from 'themes/glitch/containers/intersection_observer_article_container';
+import IntersectionObserverArticleContainer from 'flavours/glitch/containers/intersection_observer_article_container';
 import LoadMore from './load_more';
-import IntersectionObserverWrapper from 'themes/glitch/util/intersection_observer_wrapper';
+import IntersectionObserverWrapper from 'flavours/glitch/util/intersection_observer_wrapper';
 import { throttle } from 'lodash';
 import { List as ImmutableList } from 'immutable';
 import classNames from 'classnames';
-import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from 'themes/glitch/util/fullscreen';
+import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from 'flavours/glitch/util/fullscreen';
 
 export default class ScrollableList extends PureComponent {
 
diff --git a/app/javascript/themes/glitch/components/setting_text.js b/app/javascript/flavours/glitch/components/setting_text.js
index a6dde4c0f..a6dde4c0f 100644
--- a/app/javascript/themes/glitch/components/setting_text.js
+++ b/app/javascript/flavours/glitch/components/setting_text.js
diff --git a/app/javascript/themes/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js
index 9288bcafa..b0d9e3757 100644
--- a/app/javascript/themes/glitch/components/status.js
+++ b/app/javascript/flavours/glitch/components/status.js
@@ -6,9 +6,9 @@ import StatusHeader from './status_header';
 import StatusContent from './status_content';
 import StatusActionBar from './status_action_bar';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import { MediaGallery, Video } from 'themes/glitch/util/async-components';
+import { MediaGallery, Video } from 'flavours/glitch/util/async-components';
 import { HotKeys } from 'react-hotkeys';
-import NotificationOverlayContainer from 'themes/glitch/features/notifications/containers/overlay_container';
+import NotificationOverlayContainer from 'flavours/glitch/features/notifications/containers/overlay_container';
 import classNames from 'classnames';
 
 // We use the component (and not the container) since we do not want
diff --git a/app/javascript/themes/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js
index 9d615ed7c..5a06782be 100644
--- a/app/javascript/themes/glitch/components/status_action_bar.js
+++ b/app/javascript/flavours/glitch/components/status_action_bar.js
@@ -1,14 +1,11 @@
-//  THIS FILE EXISTS FOR UPSTREAM COMPATIBILITY & SHOULDN'T BE USED !!
-//  SEE INSTEAD : glitch/components/status/action_bar
-
 import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
 import IconButton from './icon_button';
-import DropdownMenuContainer from 'themes/glitch/containers/dropdown_menu_container';
+import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
 import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import { me } from 'themes/glitch/util/initial_state';
+import { me } from 'flavours/glitch/util/initial_state';
 import RelativeTimestamp from './relative_timestamp';
 
 const messages = defineMessages({
diff --git a/app/javascript/themes/glitch/components/status_content.js b/app/javascript/flavours/glitch/components/status_content.js
index 3eba6eaa0..0c40e62cc 100644
--- a/app/javascript/themes/glitch/components/status_content.js
+++ b/app/javascript/flavours/glitch/components/status_content.js
@@ -1,7 +1,7 @@
 import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
-import { isRtl } from 'themes/glitch/util/rtl';
+import { isRtl } from 'flavours/glitch/util/rtl';
 import { FormattedMessage } from 'react-intl';
 import Permalink from './permalink';
 import classnames from 'classnames';
diff --git a/app/javascript/themes/glitch/components/status_header.js b/app/javascript/flavours/glitch/components/status_header.js
index bfa996cd5..bfa996cd5 100644
--- a/app/javascript/themes/glitch/components/status_header.js
+++ b/app/javascript/flavours/glitch/components/status_header.js
diff --git a/app/javascript/themes/glitch/components/status_list.js b/app/javascript/flavours/glitch/components/status_list.js
index ddb1354c6..f190ba6ab 100644
--- a/app/javascript/themes/glitch/components/status_list.js
+++ b/app/javascript/flavours/glitch/components/status_list.js
@@ -1,7 +1,7 @@
 import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
-import StatusContainer from 'themes/glitch/containers/status_container';
+import StatusContainer from 'flavours/glitch/containers/status_container';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import ScrollableList from './scrollable_list';
 
diff --git a/app/javascript/themes/glitch/components/status_prepend.js b/app/javascript/flavours/glitch/components/status_prepend.js
index bd2559e46..bd2559e46 100644
--- a/app/javascript/themes/glitch/components/status_prepend.js
+++ b/app/javascript/flavours/glitch/components/status_prepend.js
diff --git a/app/javascript/themes/glitch/components/status_visibility_icon.js b/app/javascript/flavours/glitch/components/status_visibility_icon.js
index 017b69cbb..017b69cbb 100644
--- a/app/javascript/themes/glitch/components/status_visibility_icon.js
+++ b/app/javascript/flavours/glitch/components/status_visibility_icon.js
diff --git a/app/javascript/themes/glitch/containers/account_container.js b/app/javascript/flavours/glitch/containers/account_container.js
index c1ce49987..bc84d299b 100644
--- a/app/javascript/themes/glitch/containers/account_container.js
+++ b/app/javascript/flavours/glitch/containers/account_container.js
@@ -1,8 +1,8 @@
 import React from 'react';
 import { connect } from 'react-redux';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import { makeGetAccount } from 'themes/glitch/selectors';
-import Account from 'themes/glitch/components/account';
+import { makeGetAccount } from 'flavours/glitch/selectors';
+import Account from 'flavours/glitch/components/account';
 import {
   followAccount,
   unfollowAccount,
@@ -10,10 +10,10 @@ import {
   unblockAccount,
   muteAccount,
   unmuteAccount,
-} from 'themes/glitch/actions/accounts';
-import { openModal } from 'themes/glitch/actions/modal';
-import { initMuteModal } from 'themes/glitch/actions/mutes';
-import { unfollowModal } from 'themes/glitch/util/initial_state';
+} from 'flavours/glitch/actions/accounts';
+import { openModal } from 'flavours/glitch/actions/modal';
+import { initMuteModal } from 'flavours/glitch/actions/mutes';
+import { unfollowModal } from 'flavours/glitch/util/initial_state';
 
 const messages = defineMessages({
   unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
diff --git a/app/javascript/themes/glitch/containers/card_container.js b/app/javascript/flavours/glitch/containers/card_container.js
index 8285437bb..dec7df522 100644
--- a/app/javascript/themes/glitch/containers/card_container.js
+++ b/app/javascript/flavours/glitch/containers/card_container.js
@@ -1,6 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import Card from 'themes/glitch/features/status/components/card';
+import Card from 'flavours/glitch/features/status/components/card';
 import { fromJS } from 'immutable';
 
 export default class CardContainer extends React.PureComponent {
diff --git a/app/javascript/themes/glitch/containers/compose_container.js b/app/javascript/flavours/glitch/containers/compose_container.js
index 82980ee36..60f6a9c9f 100644
--- a/app/javascript/themes/glitch/containers/compose_container.js
+++ b/app/javascript/flavours/glitch/containers/compose_container.js
@@ -1,12 +1,12 @@
 import React from 'react';
 import { Provider } from 'react-redux';
 import PropTypes from 'prop-types';
-import configureStore from 'themes/glitch/store/configureStore';
-import { hydrateStore } from 'themes/glitch/actions/store';
+import configureStore from 'flavours/glitch/store/configureStore';
+import { hydrateStore } from 'flavours/glitch/actions/store';
 import { IntlProvider, addLocaleData } from 'react-intl';
 import { getLocale } from 'mastodon/locales';
-import Compose from 'themes/glitch/features/standalone/compose';
-import initialState from 'themes/glitch/util/initial_state';
+import Compose from 'flavours/glitch/features/standalone/compose';
+import initialState from 'flavours/glitch/util/initial_state';
 
 const { localeData, messages } = getLocale();
 addLocaleData(localeData);
diff --git a/app/javascript/themes/glitch/containers/dropdown_menu_container.js b/app/javascript/flavours/glitch/containers/dropdown_menu_container.js
index 15e8da2e3..0b4f72fa1 100644
--- a/app/javascript/themes/glitch/containers/dropdown_menu_container.js
+++ b/app/javascript/flavours/glitch/containers/dropdown_menu_container.js
@@ -1,7 +1,7 @@
-import { openModal, closeModal } from 'themes/glitch/actions/modal';
+import { openModal, closeModal } from 'flavours/glitch/actions/modal';
 import { connect } from 'react-redux';
-import DropdownMenu from 'themes/glitch/components/dropdown_menu';
-import { isUserTouching } from 'themes/glitch/util/is_mobile';
+import DropdownMenu from 'flavours/glitch/components/dropdown_menu';
+import { isUserTouching } from 'flavours/glitch/util/is_mobile';
 
 const mapStateToProps = state => ({
   isModalOpen: state.get('modal').modalType === 'ACTIONS',
diff --git a/app/javascript/themes/glitch/containers/intersection_observer_article_container.js b/app/javascript/flavours/glitch/containers/intersection_observer_article_container.js
index 6ede64738..f2741f2d4 100644
--- a/app/javascript/themes/glitch/containers/intersection_observer_article_container.js
+++ b/app/javascript/flavours/glitch/containers/intersection_observer_article_container.js
@@ -1,6 +1,6 @@
 import { connect } from 'react-redux';
-import IntersectionObserverArticle from 'themes/glitch/components/intersection_observer_article';
-import { setHeight } from 'themes/glitch/actions/height_cache';
+import IntersectionObserverArticle from 'flavours/glitch/components/intersection_observer_article';
+import { setHeight } from 'flavours/glitch/actions/height_cache';
 
 const makeMapStateToProps = (state, props) => ({
   cachedHeight: state.getIn(['height_cache', props.saveHeightKey, props.id]),
diff --git a/app/javascript/themes/glitch/containers/mastodon.js b/app/javascript/flavours/glitch/containers/mastodon.js
index 348470637..1c98cd5f7 100644
--- a/app/javascript/themes/glitch/containers/mastodon.js
+++ b/app/javascript/flavours/glitch/containers/mastodon.js
@@ -1,16 +1,16 @@
 import React from 'react';
 import { Provider } from 'react-redux';
 import PropTypes from 'prop-types';
-import configureStore from 'themes/glitch/store/configureStore';
-import { showOnboardingOnce } from 'themes/glitch/actions/onboarding';
+import configureStore from 'flavours/glitch/store/configureStore';
+import { showOnboardingOnce } from 'flavours/glitch/actions/onboarding';
 import { BrowserRouter, Route } from 'react-router-dom';
 import { ScrollContext } from 'react-router-scroll-4';
-import UI from 'themes/glitch/features/ui';
-import { hydrateStore } from 'themes/glitch/actions/store';
-import { connectUserStream } from 'themes/glitch/actions/streaming';
+import UI from 'flavours/glitch/features/ui';
+import { hydrateStore } from 'flavours/glitch/actions/store';
+import { connectUserStream } from 'flavours/glitch/actions/streaming';
 import { IntlProvider, addLocaleData } from 'react-intl';
-import { getLocale } from 'mastodon/locales';
-import initialState from 'themes/glitch/util/initial_state';
+import { getLocale } from 'locales';
+import initialState from 'flavours/glitch/util/initial_state';
 
 const { localeData, messages } = getLocale();
 addLocaleData(localeData);
diff --git a/app/javascript/themes/glitch/containers/media_gallery_container.js b/app/javascript/flavours/glitch/containers/media_gallery_container.js
index 86965f73b..54bfbf453 100644
--- a/app/javascript/themes/glitch/containers/media_gallery_container.js
+++ b/app/javascript/flavours/glitch/containers/media_gallery_container.js
@@ -2,7 +2,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { IntlProvider, addLocaleData } from 'react-intl';
 import { getLocale } from 'mastodon/locales';
-import MediaGallery from 'themes/glitch/components/media_gallery';
+import MediaGallery from 'flavours/glitch/components/media_gallery';
 import { fromJS } from 'immutable';
 
 const { localeData, messages } = getLocale();
diff --git a/app/javascript/themes/glitch/containers/notification_purge_buttons_container.js b/app/javascript/flavours/glitch/containers/notification_purge_buttons_container.js
index ee4cb84cd..2570cf4a5 100644
--- a/app/javascript/themes/glitch/containers/notification_purge_buttons_container.js
+++ b/app/javascript/flavours/glitch/containers/notification_purge_buttons_container.js
@@ -3,13 +3,13 @@ import { connect } from 'react-redux';
 import { defineMessages, injectIntl } from 'react-intl';
 
 //  Our imports.
-import NotificationPurgeButtons from 'themes/glitch/components/notification_purge_buttons';
+import NotificationPurgeButtons from 'flavours/glitch/components/notification_purge_buttons';
 import {
   deleteMarkedNotifications,
   enterNotificationClearingMode,
   markAllNotifications,
-} from 'themes/glitch/actions/notifications';
-import { openModal } from 'themes/glitch/actions/modal';
+} from 'flavours/glitch/actions/notifications';
+import { openModal } from 'flavours/glitch/actions/modal';
 
 const messages = defineMessages({
   clearMessage: { id: 'notifications.marked_clear_confirmation', defaultMessage: 'Are you sure you want to permanently clear all selected notifications?' },
diff --git a/app/javascript/themes/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js
index 0cee73b9a..b753de7b3 100644
--- a/app/javascript/themes/glitch/containers/status_container.js
+++ b/app/javascript/flavours/glitch/containers/status_container.js
@@ -1,11 +1,11 @@
 import React from 'react';
 import { connect } from 'react-redux';
-import Status from 'themes/glitch/components/status';
-import { makeGetStatus } from 'themes/glitch/selectors';
+import Status from 'flavours/glitch/components/status';
+import { makeGetStatus } from 'flavours/glitch/selectors';
 import {
   replyCompose,
   mentionCompose,
-} from 'themes/glitch/actions/compose';
+} from 'flavours/glitch/actions/compose';
 import {
   reblog,
   favourite,
@@ -13,14 +13,14 @@ import {
   unfavourite,
   pin,
   unpin,
-} from 'themes/glitch/actions/interactions';
-import { blockAccount } from 'themes/glitch/actions/accounts';
-import { muteStatus, unmuteStatus, deleteStatus } from 'themes/glitch/actions/statuses';
-import { initMuteModal } from 'themes/glitch/actions/mutes';
-import { initReport } from 'themes/glitch/actions/reports';
-import { openModal } from 'themes/glitch/actions/modal';
+} from 'flavours/glitch/actions/interactions';
+import { blockAccount } from 'flavours/glitch/actions/accounts';
+import { muteStatus, unmuteStatus, deleteStatus } from 'flavours/glitch/actions/statuses';
+import { initMuteModal } from 'flavours/glitch/actions/mutes';
+import { initReport } from 'flavours/glitch/actions/reports';
+import { openModal } from 'flavours/glitch/actions/modal';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import { boostModal, deleteModal } from 'themes/glitch/util/initial_state';
+import { boostModal, deleteModal } from 'flavours/glitch/util/initial_state';
 
 const messages = defineMessages({
   deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
diff --git a/app/javascript/themes/glitch/containers/timeline_container.js b/app/javascript/flavours/glitch/containers/timeline_container.js
index a75f8808d..c5ffe1b63 100644
--- a/app/javascript/themes/glitch/containers/timeline_container.js
+++ b/app/javascript/flavours/glitch/containers/timeline_container.js
@@ -1,13 +1,13 @@
 import React from 'react';
 import { Provider } from 'react-redux';
 import PropTypes from 'prop-types';
-import configureStore from 'themes/glitch/store/configureStore';
-import { hydrateStore } from 'themes/glitch/actions/store';
+import configureStore from 'flavours/glitch/store/configureStore';
+import { hydrateStore } from 'flavours/glitch/actions/store';
 import { IntlProvider, addLocaleData } from 'react-intl';
 import { getLocale } from 'mastodon/locales';
-import PublicTimeline from 'themes/glitch/features/standalone/public_timeline';
-import HashtagTimeline from 'themes/glitch/features/standalone/hashtag_timeline';
-import initialState from 'themes/glitch/util/initial_state';
+import PublicTimeline from 'flavours/glitch/features/standalone/public_timeline';
+import HashtagTimeline from 'flavours/glitch/features/standalone/hashtag_timeline';
+import initialState from 'flavours/glitch/util/initial_state';
 
 const { localeData, messages } = getLocale();
 addLocaleData(localeData);
diff --git a/app/javascript/themes/glitch/containers/video_container.js b/app/javascript/flavours/glitch/containers/video_container.js
index 2b0e98666..b206e9a10 100644
--- a/app/javascript/themes/glitch/containers/video_container.js
+++ b/app/javascript/flavours/glitch/containers/video_container.js
@@ -2,7 +2,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { IntlProvider, addLocaleData } from 'react-intl';
 import { getLocale } from 'mastodon/locales';
-import Video from 'themes/glitch/features/video';
+import Video from 'flavours/glitch/features/video';
 
 const { localeData, messages } = getLocale();
 addLocaleData(localeData);
diff --git a/app/javascript/themes/glitch/features/account/components/action_bar.js b/app/javascript/flavours/glitch/features/account/components/action_bar.js
index 0edd5c848..df8cb3733 100644
--- a/app/javascript/themes/glitch/features/account/components/action_bar.js
+++ b/app/javascript/flavours/glitch/features/account/components/action_bar.js
@@ -1,10 +1,10 @@
 import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
-import DropdownMenuContainer from 'themes/glitch/containers/dropdown_menu_container';
+import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
 import { Link } from 'react-router-dom';
 import { defineMessages, injectIntl, FormattedMessage, FormattedNumber } from 'react-intl';
-import { me } from 'themes/glitch/util/initial_state';
+import { me } from 'flavours/glitch/util/initial_state';
 
 const messages = defineMessages({
   mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' },
@@ -63,9 +63,8 @@ export default class ActionBar extends React.PureComponent {
     if (account.get('id') === me) {
       menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' });
     } else {
-      const following = account.getIn(['relationship', 'following']);
-      if (following) {
-        if (following.get('reblogs')) {
+      if (account.getIn(['relationship', 'following'])) {
+        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 });
diff --git a/app/javascript/themes/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js
index 696bb1991..f5ecaae71 100644
--- a/app/javascript/themes/glitch/features/account/components/header.js
+++ b/app/javascript/flavours/glitch/features/account/components/header.js
@@ -4,12 +4,12 @@ import PropTypes from 'prop-types';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
-import Avatar from 'themes/glitch/components/avatar';
-import IconButton from 'themes/glitch/components/icon_button';
+import Avatar from 'flavours/glitch/components/avatar';
+import IconButton from 'flavours/glitch/components/icon_button';
 
-import emojify from 'themes/glitch/util/emoji';
-import { me } from 'themes/glitch/util/initial_state';
-import { processBio } from 'themes/glitch/util/bio_metadata';
+import emojify from 'flavours/glitch/util/emoji';
+import { me } from 'flavours/glitch/util/initial_state';
+import { processBio } from 'flavours/glitch/util/bio_metadata';
 
 const messages = defineMessages({
   unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
diff --git a/app/javascript/themes/glitch/features/account_gallery/components/media_item.js b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js
index 88c9156b5..e52d3b0bb 100644
--- a/app/javascript/themes/glitch/features/account_gallery/components/media_item.js
+++ b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js
@@ -1,7 +1,7 @@
 import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import Permalink from 'themes/glitch/components/permalink';
+import Permalink from 'flavours/glitch/components/permalink';
 
 export default class MediaItem extends ImmutablePureComponent {
 
diff --git a/app/javascript/themes/glitch/features/account_gallery/index.js b/app/javascript/flavours/glitch/features/account_gallery/index.js
index a21c089da..951e019e3 100644
--- a/app/javascript/themes/glitch/features/account_gallery/index.js
+++ b/app/javascript/flavours/glitch/features/account_gallery/index.js
@@ -2,18 +2,18 @@ import React from 'react';
 import { connect } from 'react-redux';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
-import { fetchAccount } from 'themes/glitch/actions/accounts';
-import { refreshAccountMediaTimeline, expandAccountMediaTimeline } from 'themes/glitch/actions/timelines';
-import LoadingIndicator from 'themes/glitch/components/loading_indicator';
-import Column from 'themes/glitch/features/ui/components/column';
-import ColumnBackButton from 'themes/glitch/components/column_back_button';
+import { fetchAccount } from 'flavours/glitch/actions/accounts';
+import { refreshAccountMediaTimeline, expandAccountMediaTimeline } from 'flavours/glitch/actions/timelines';
+import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
+import Column from 'flavours/glitch/features/ui/components/column';
+import ColumnBackButton from 'flavours/glitch/components/column_back_button';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import { getAccountGallery } from 'themes/glitch/selectors';
+import { getAccountGallery } from 'flavours/glitch/selectors';
 import MediaItem from './components/media_item';
-import HeaderContainer from 'themes/glitch/features/account_timeline/containers/header_container';
+import HeaderContainer from 'flavours/glitch/features/account_timeline/containers/header_container';
 import { FormattedMessage } from 'react-intl';
 import { ScrollContainer } from 'react-router-scroll-4';
-import LoadMore from 'themes/glitch/components/load_more';
+import LoadMore from 'flavours/glitch/components/load_more';
 
 const mapStateToProps = (state, props) => ({
   medias: getAccountGallery(state, props.params.accountId),
diff --git a/app/javascript/themes/glitch/features/account_timeline/components/header.js b/app/javascript/flavours/glitch/features/account_timeline/components/header.js
index c719a7bcb..4ad677fbe 100644
--- a/app/javascript/themes/glitch/features/account_timeline/components/header.js
+++ b/app/javascript/flavours/glitch/features/account_timeline/components/header.js
@@ -1,9 +1,9 @@
 import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
-import InnerHeader from 'themes/glitch/features/account/components/header';
-import ActionBar from 'themes/glitch/features/account/components/action_bar';
-import MissingIndicator from 'themes/glitch/components/missing_indicator';
+import InnerHeader from 'flavours/glitch/features/account/components/header';
+import ActionBar from 'flavours/glitch/features/account/components/action_bar';
+import MissingIndicator from 'flavours/glitch/components/missing_indicator';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 export default class Header extends ImmutablePureComponent {
diff --git a/app/javascript/themes/glitch/features/account_timeline/containers/header_container.js b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js
index 766b57b56..37ff445b2 100644
--- a/app/javascript/themes/glitch/features/account_timeline/containers/header_container.js
+++ b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js
@@ -1,6 +1,6 @@
 import React from 'react';
 import { connect } from 'react-redux';
-import { makeGetAccount } from 'themes/glitch/selectors';
+import { makeGetAccount } from 'flavours/glitch/selectors';
 import Header from '../components/header';
 import {
   followAccount,
@@ -8,14 +8,14 @@ import {
   blockAccount,
   unblockAccount,
   unmuteAccount,
-} from 'themes/glitch/actions/accounts';
-import { mentionCompose } from 'themes/glitch/actions/compose';
-import { initMuteModal } from 'themes/glitch/actions/mutes';
-import { initReport } from 'themes/glitch/actions/reports';
-import { openModal } from 'themes/glitch/actions/modal';
-import { blockDomain, unblockDomain } from 'themes/glitch/actions/domain_blocks';
+} from 'flavours/glitch/actions/accounts';
+import { mentionCompose } from 'flavours/glitch/actions/compose';
+import { initMuteModal } from 'flavours/glitch/actions/mutes';
+import { initReport } from 'flavours/glitch/actions/reports';
+import { openModal } from 'flavours/glitch/actions/modal';
+import { blockDomain, unblockDomain } from 'flavours/glitch/actions/domain_blocks';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import { unfollowModal } from 'themes/glitch/util/initial_state';
+import { unfollowModal } from 'flavours/glitch/util/initial_state';
 
 const messages = defineMessages({
   unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
@@ -68,7 +68,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
   },
 
   onReblogToggle (account) {
-    if (account.getIn(['relationship', 'following', 'reblogs'])) {
+    if (account.getIn(['relationship', 'showing_reblogs'])) {
       dispatch(followAccount(account.get('id'), false));
     } else {
       dispatch(followAccount(account.get('id'), true));
diff --git a/app/javascript/themes/glitch/features/account_timeline/index.js b/app/javascript/flavours/glitch/features/account_timeline/index.js
index 81336ef3a..75dba5049 100644
--- a/app/javascript/themes/glitch/features/account_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/account_timeline/index.js
@@ -2,8 +2,8 @@ import React from 'react';
 import { connect } from 'react-redux';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
-import { fetchAccount } from 'themes/glitch/actions/accounts';
-import { refreshAccountTimeline, expandAccountTimeline } from 'themes/glitch/actions/timelines';
+import { fetchAccount } from 'flavours/glitch/actions/accounts';
+import { refreshAccountTimeline, expandAccountTimeline } from 'flavours/glitch/actions/timelines';
 import StatusList from '../../components/status_list';
 import LoadingIndicator from '../../components/loading_indicator';
 import Column from '../ui/components/column';
diff --git a/app/javascript/themes/glitch/features/blocks/index.js b/app/javascript/flavours/glitch/features/blocks/index.js
index 70630818c..edd448921 100644
--- a/app/javascript/themes/glitch/features/blocks/index.js
+++ b/app/javascript/flavours/glitch/features/blocks/index.js
@@ -2,12 +2,12 @@ import React from 'react';
 import { connect } from 'react-redux';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
-import LoadingIndicator from 'themes/glitch/components/loading_indicator';
+import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
 import { ScrollContainer } from 'react-router-scroll-4';
-import Column from 'themes/glitch/features/ui/components/column';
-import ColumnBackButtonSlim from 'themes/glitch/components/column_back_button_slim';
-import AccountContainer from 'themes/glitch/containers/account_container';
-import { fetchBlocks, expandBlocks } from 'themes/glitch/actions/blocks';
+import Column from 'flavours/glitch/features/ui/components/column';
+import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim';
+import AccountContainer from 'flavours/glitch/containers/account_container';
+import { fetchBlocks, expandBlocks } from 'flavours/glitch/actions/blocks';
 import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
diff --git a/app/javascript/themes/glitch/features/community_timeline/components/column_settings.js b/app/javascript/flavours/glitch/features/community_timeline/components/column_settings.js
index 988e36308..6dc292ee5 100644
--- a/app/javascript/themes/glitch/features/community_timeline/components/column_settings.js
+++ b/app/javascript/flavours/glitch/features/community_timeline/components/column_settings.js
@@ -2,7 +2,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import SettingText from 'themes/glitch/components/setting_text';
+import SettingText from 'flavours/glitch/components/setting_text';
 
 const messages = defineMessages({
   filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' },
diff --git a/app/javascript/themes/glitch/features/community_timeline/containers/column_settings_container.js b/app/javascript/flavours/glitch/features/community_timeline/containers/column_settings_container.js
index cd9c34365..84234a836 100644
--- a/app/javascript/themes/glitch/features/community_timeline/containers/column_settings_container.js
+++ b/app/javascript/flavours/glitch/features/community_timeline/containers/column_settings_container.js
@@ -1,6 +1,6 @@
 import { connect } from 'react-redux';
 import ColumnSettings from '../components/column_settings';
-import { changeSetting } from 'themes/glitch/actions/settings';
+import { changeSetting } from 'flavours/glitch/actions/settings';
 
 const mapStateToProps = state => ({
   settings: state.getIn(['settings', 'community']),
diff --git a/app/javascript/themes/glitch/features/community_timeline/index.js b/app/javascript/flavours/glitch/features/community_timeline/index.js
index 9d255bd01..55355414f 100644
--- a/app/javascript/themes/glitch/features/community_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/community_timeline/index.js
@@ -1,17 +1,17 @@
 import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
-import StatusListContainer from 'themes/glitch/features/ui/containers/status_list_container';
-import Column from 'themes/glitch/components/column';
-import ColumnHeader from 'themes/glitch/components/column_header';
+import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
+import Column from 'flavours/glitch/components/column';
+import ColumnHeader from 'flavours/glitch/components/column_header';
 import {
   refreshCommunityTimeline,
   expandCommunityTimeline,
-} from 'themes/glitch/actions/timelines';
-import { addColumn, removeColumn, moveColumn } from 'themes/glitch/actions/columns';
+} from 'flavours/glitch/actions/timelines';
+import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import ColumnSettingsContainer from './containers/column_settings_container';
-import { connectCommunityStream } from 'themes/glitch/actions/streaming';
+import { connectCommunityStream } from 'flavours/glitch/actions/streaming';
 
 const messages = defineMessages({
   title: { id: 'column.community', defaultMessage: 'Local timeline' },
diff --git a/app/javascript/themes/glitch/features/compose/components/advanced_options.js b/app/javascript/flavours/glitch/features/compose/components/advanced_options.js
index 045bad2e5..045bad2e5 100644
--- a/app/javascript/themes/glitch/features/compose/components/advanced_options.js
+++ b/app/javascript/flavours/glitch/features/compose/components/advanced_options.js
diff --git a/app/javascript/themes/glitch/features/compose/components/advanced_options_toggle.js b/app/javascript/flavours/glitch/features/compose/components/advanced_options_toggle.js
index 98b3b6a44..98b3b6a44 100644
--- a/app/javascript/themes/glitch/features/compose/components/advanced_options_toggle.js
+++ b/app/javascript/flavours/glitch/features/compose/components/advanced_options_toggle.js
diff --git a/app/javascript/themes/glitch/features/compose/components/attach_options.js b/app/javascript/flavours/glitch/features/compose/components/attach_options.js
index c396714f3..6c7a1f55f 100644
--- a/app/javascript/themes/glitch/features/compose/components/attach_options.js
+++ b/app/javascript/flavours/glitch/features/compose/components/attach_options.js
@@ -6,10 +6,10 @@ import { injectIntl, defineMessages } from 'react-intl';
 
 //  Our imports  //
 import ComposeDropdown from './dropdown';
-import { uploadCompose } from 'themes/glitch/actions/compose';
+import { uploadCompose } from 'flavours/glitch/actions/compose';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import { openModal } from 'themes/glitch/actions/modal';
+import { openModal } from 'flavours/glitch/actions/modal';
 
 const messages = defineMessages({
   upload :
diff --git a/app/javascript/themes/glitch/features/compose/components/autosuggest_account.js b/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.js
index 4a98d89fe..3d474af30 100644
--- a/app/javascript/themes/glitch/features/compose/components/autosuggest_account.js
+++ b/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.js
@@ -1,6 +1,6 @@
 import React from 'react';
-import Avatar from 'themes/glitch/components/avatar';
-import DisplayName from 'themes/glitch/components/display_name';
+import Avatar from 'flavours/glitch/components/avatar';
+import DisplayName from 'flavours/glitch/components/display_name';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
diff --git a/app/javascript/themes/glitch/features/compose/components/character_counter.js b/app/javascript/flavours/glitch/features/compose/components/character_counter.js
index 0ecfc9141..0ecfc9141 100644
--- a/app/javascript/themes/glitch/features/compose/components/character_counter.js
+++ b/app/javascript/flavours/glitch/features/compose/components/character_counter.js
diff --git a/app/javascript/themes/glitch/features/compose/components/compose_form.js b/app/javascript/flavours/glitch/features/compose/components/compose_form.js
index 54b1944a4..67ce935f4 100644
--- a/app/javascript/themes/glitch/features/compose/components/compose_form.js
+++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.js
@@ -1,12 +1,12 @@
 import React from 'react';
 import CharacterCounter from './character_counter';
-import Button from 'themes/glitch/components/button';
+import Button from 'flavours/glitch/components/button';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
 import ReplyIndicatorContainer from '../containers/reply_indicator_container';
-import AutosuggestTextarea from 'themes/glitch/components/autosuggest_textarea';
+import AutosuggestTextarea from 'flavours/glitch/components/autosuggest_textarea';
 import { defineMessages, injectIntl } from 'react-intl';
-import Collapsable from 'themes/glitch/components/collapsable';
+import Collapsable from 'flavours/glitch/components/collapsable';
 import SpoilerButtonContainer from '../containers/spoiler_button_container';
 import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
 import ComposeAdvancedOptionsContainer from '../containers/advanced_options_container';
@@ -14,12 +14,12 @@ import SensitiveButtonContainer from '../containers/sensitive_button_container';
 import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
 import UploadFormContainer from '../containers/upload_form_container';
 import WarningContainer from '../containers/warning_container';
-import { isMobile } from 'themes/glitch/util/is_mobile';
+import { isMobile } from 'flavours/glitch/util/is_mobile';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { length } from 'stringz';
-import { countableText } from 'themes/glitch/util/counter';
+import { countableText } from 'flavours/glitch/util/counter';
 import ComposeAttachOptions from './attach_options';
-import initialState from 'themes/glitch/util/initial_state';
+import initialState from 'flavours/glitch/util/initial_state';
 
 const maxChars = initialState.max_toot_chars;
 
diff --git a/app/javascript/themes/glitch/features/compose/components/dropdown.js b/app/javascript/flavours/glitch/features/compose/components/dropdown.js
index f3d9f094e..1b0000fb7 100644
--- a/app/javascript/themes/glitch/features/compose/components/dropdown.js
+++ b/app/javascript/flavours/glitch/features/compose/components/dropdown.js
@@ -3,7 +3,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 
 //  Our imports.
-import IconButton from 'themes/glitch/components/icon_button';
+import IconButton from 'flavours/glitch/components/icon_button';
 
 const iconStyle = {
   height     : null,
diff --git a/app/javascript/themes/glitch/features/compose/components/emoji_picker_dropdown.js b/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.js
index fd59efb85..cf89f91d3 100644
--- a/app/javascript/themes/glitch/features/compose/components/emoji_picker_dropdown.js
+++ b/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.js
@@ -1,12 +1,12 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import { defineMessages, injectIntl } from 'react-intl';
-import { EmojiPicker as EmojiPickerAsync } from 'themes/glitch/util/async-components';
+import { EmojiPicker as EmojiPickerAsync } from 'flavours/glitch/util/async-components';
 import Overlay from 'react-overlays/lib/Overlay';
 import classNames from 'classnames';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import detectPassiveEvents from 'detect-passive-events';
-import { buildCustomEmojis } from 'themes/glitch/util/emoji';
+import { buildCustomEmojis } from 'flavours/glitch/util/emoji';
 
 const messages = defineMessages({
   emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
diff --git a/app/javascript/themes/glitch/features/compose/components/navigation_bar.js b/app/javascript/flavours/glitch/features/compose/components/navigation_bar.js
index 24a70949b..1b6d74123 100644
--- a/app/javascript/themes/glitch/features/compose/components/navigation_bar.js
+++ b/app/javascript/flavours/glitch/features/compose/components/navigation_bar.js
@@ -1,9 +1,9 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import Avatar from 'themes/glitch/components/avatar';
-import IconButton from 'themes/glitch/components/icon_button';
-import Permalink from 'themes/glitch/components/permalink';
+import Avatar from 'flavours/glitch/components/avatar';
+import IconButton from 'flavours/glitch/components/icon_button';
+import Permalink from 'flavours/glitch/components/permalink';
 import { FormattedMessage } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
diff --git a/app/javascript/themes/glitch/features/compose/components/privacy_dropdown.js b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js
index 0cd92d174..90f062f8f 100644
--- a/app/javascript/themes/glitch/features/compose/components/privacy_dropdown.js
+++ b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js
@@ -1,9 +1,9 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import { injectIntl, defineMessages } from 'react-intl';
-import IconButton from 'themes/glitch/components/icon_button';
+import IconButton from 'flavours/glitch/components/icon_button';
 import Overlay from 'react-overlays/lib/Overlay';
-import Motion from 'themes/glitch/util/optional_motion';
+import Motion from 'flavours/glitch/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import detectPassiveEvents from 'detect-passive-events';
 import classNames from 'classnames';
diff --git a/app/javascript/themes/glitch/features/compose/components/reply_indicator.js b/app/javascript/flavours/glitch/features/compose/components/reply_indicator.js
index 9a8d10ceb..cb28e51be 100644
--- a/app/javascript/themes/glitch/features/compose/components/reply_indicator.js
+++ b/app/javascript/flavours/glitch/features/compose/components/reply_indicator.js
@@ -1,9 +1,9 @@
 import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
-import Avatar from 'themes/glitch/components/avatar';
-import IconButton from 'themes/glitch/components/icon_button';
-import DisplayName from 'themes/glitch/components/display_name';
+import Avatar from 'flavours/glitch/components/avatar';
+import IconButton from 'flavours/glitch/components/icon_button';
+import DisplayName from 'flavours/glitch/components/display_name';
 import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
diff --git a/app/javascript/themes/glitch/features/compose/components/search.js b/app/javascript/flavours/glitch/features/compose/components/search.js
index c3218137f..1ce66b19d 100644
--- a/app/javascript/themes/glitch/features/compose/components/search.js
+++ b/app/javascript/flavours/glitch/features/compose/components/search.js
@@ -2,7 +2,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import Overlay from 'react-overlays/lib/Overlay';
-import Motion from 'themes/glitch/util/optional_motion';
+import Motion from 'flavours/glitch/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 
 const messages = defineMessages({
diff --git a/app/javascript/themes/glitch/features/compose/components/search_results.js b/app/javascript/flavours/glitch/features/compose/components/search_results.js
index 3fdafa5f3..2a4818d4e 100644
--- a/app/javascript/themes/glitch/features/compose/components/search_results.js
+++ b/app/javascript/flavours/glitch/features/compose/components/search_results.js
@@ -1,8 +1,8 @@
 import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import { FormattedMessage } from 'react-intl';
-import AccountContainer from 'themes/glitch/containers/account_container';
-import StatusContainer from 'themes/glitch/containers/status_container';
+import AccountContainer from 'flavours/glitch/containers/account_container';
+import StatusContainer from 'flavours/glitch/containers/status_container';
 import { Link } from 'react-router-dom';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
diff --git a/app/javascript/themes/glitch/features/compose/components/text_icon_button.js b/app/javascript/flavours/glitch/features/compose/components/text_icon_button.js
index 9c8ffab1f..9c8ffab1f 100644
--- a/app/javascript/themes/glitch/features/compose/components/text_icon_button.js
+++ b/app/javascript/flavours/glitch/features/compose/components/text_icon_button.js
diff --git a/app/javascript/themes/glitch/features/compose/components/upload.js b/app/javascript/flavours/glitch/features/compose/components/upload.js
index ded376ada..a1fc93234 100644
--- a/app/javascript/themes/glitch/features/compose/components/upload.js
+++ b/app/javascript/flavours/glitch/features/compose/components/upload.js
@@ -1,8 +1,8 @@
 import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
-import IconButton from 'themes/glitch/components/icon_button';
-import Motion from 'themes/glitch/util/optional_motion';
+import IconButton from 'flavours/glitch/components/icon_button';
+import Motion from 'flavours/glitch/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { defineMessages, injectIntl } from 'react-intl';
diff --git a/app/javascript/themes/glitch/features/compose/components/upload_button.js b/app/javascript/flavours/glitch/features/compose/components/upload_button.js
index d7742adfe..f06167a2a 100644
--- a/app/javascript/themes/glitch/features/compose/components/upload_button.js
+++ b/app/javascript/flavours/glitch/features/compose/components/upload_button.js
@@ -1,5 +1,5 @@
 import React from 'react';
-import IconButton from 'themes/glitch/components/icon_button';
+import IconButton from 'flavours/glitch/components/icon_button';
 import PropTypes from 'prop-types';
 import { defineMessages, injectIntl } from 'react-intl';
 import { connect } from 'react-redux';
diff --git a/app/javascript/themes/glitch/features/compose/components/upload_form.js b/app/javascript/flavours/glitch/features/compose/components/upload_form.js
index b7f112205..b7f112205 100644
--- a/app/javascript/themes/glitch/features/compose/components/upload_form.js
+++ b/app/javascript/flavours/glitch/features/compose/components/upload_form.js
diff --git a/app/javascript/themes/glitch/features/compose/components/upload_progress.js b/app/javascript/flavours/glitch/features/compose/components/upload_progress.js
index b923d0a22..2a3b8ceb4 100644
--- a/app/javascript/themes/glitch/features/compose/components/upload_progress.js
+++ b/app/javascript/flavours/glitch/features/compose/components/upload_progress.js
@@ -1,6 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import Motion from 'themes/glitch/util/optional_motion';
+import Motion from 'flavours/glitch/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import { FormattedMessage } from 'react-intl';
 
diff --git a/app/javascript/themes/glitch/features/compose/components/warning.js b/app/javascript/flavours/glitch/features/compose/components/warning.js
index 82df55a31..4962e76c8 100644
--- a/app/javascript/themes/glitch/features/compose/components/warning.js
+++ b/app/javascript/flavours/glitch/features/compose/components/warning.js
@@ -1,6 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import Motion from 'themes/glitch/util/optional_motion';
+import Motion from 'flavours/glitch/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 
 export default class Warning extends React.PureComponent {
diff --git a/app/javascript/themes/glitch/features/compose/containers/advanced_options_container.js b/app/javascript/flavours/glitch/features/compose/containers/advanced_options_container.js
index 9f168942a..da381568b 100644
--- a/app/javascript/themes/glitch/features/compose/containers/advanced_options_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/advanced_options_container.js
@@ -2,7 +2,7 @@
 import { connect } from 'react-redux';
 
 //  Our imports.
-import { toggleComposeAdvancedOption } from 'themes/glitch/actions/compose';
+import { toggleComposeAdvancedOption } from 'flavours/glitch/actions/compose';
 import ComposeAdvancedOptions from '../components/advanced_options';
 
 const mapStateToProps = state => ({
diff --git a/app/javascript/themes/glitch/features/compose/containers/autosuggest_account_container.js b/app/javascript/flavours/glitch/features/compose/containers/autosuggest_account_container.js
index 96eb70c18..0e1c328fe 100644
--- a/app/javascript/themes/glitch/features/compose/containers/autosuggest_account_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/autosuggest_account_container.js
@@ -1,6 +1,6 @@
 import { connect } from 'react-redux';
 import AutosuggestAccount from '../components/autosuggest_account';
-import { makeGetAccount } from 'themes/glitch/selectors';
+import { makeGetAccount } from 'flavours/glitch/selectors';
 
 const makeMapStateToProps = () => {
   const getAccount = makeGetAccount();
diff --git a/app/javascript/themes/glitch/features/compose/containers/compose_form_container.js b/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js
index 7afa988f1..e2e93e44b 100644
--- a/app/javascript/themes/glitch/features/compose/containers/compose_form_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js
@@ -1,6 +1,6 @@
 import { connect } from 'react-redux';
 import ComposeForm from '../components/compose_form';
-import { changeComposeVisibility, uploadCompose } from 'themes/glitch/actions/compose';
+import { changeComposeVisibility, uploadCompose } from 'flavours/glitch/actions/compose';
 import {
   changeCompose,
   submitCompose,
@@ -9,7 +9,7 @@ import {
   selectComposeSuggestion,
   changeComposeSpoilerText,
   insertEmojiCompose,
-} from 'themes/glitch/actions/compose';
+} from 'flavours/glitch/actions/compose';
 
 const mapStateToProps = state => ({
   text: state.getIn(['compose', 'text']),
diff --git a/app/javascript/themes/glitch/features/compose/containers/emoji_picker_dropdown_container.js b/app/javascript/flavours/glitch/features/compose/containers/emoji_picker_dropdown_container.js
index 55a13bd65..ba85edd87 100644
--- a/app/javascript/themes/glitch/features/compose/containers/emoji_picker_dropdown_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/emoji_picker_dropdown_container.js
@@ -1,9 +1,9 @@
 import { connect } from 'react-redux';
 import EmojiPickerDropdown from '../components/emoji_picker_dropdown';
-import { changeSetting } from 'themes/glitch/actions/settings';
+import { changeSetting } from 'flavours/glitch/actions/settings';
 import { createSelector } from 'reselect';
 import { Map as ImmutableMap } from 'immutable';
-import { useEmoji } from 'themes/glitch/actions/emojis';
+import { useEmoji } from 'flavours/glitch/actions/emojis';
 
 const perLine = 8;
 const lines   = 2;
diff --git a/app/javascript/themes/glitch/features/compose/containers/navigation_container.js b/app/javascript/flavours/glitch/features/compose/containers/navigation_container.js
index b6d737b46..eb630ffbb 100644
--- a/app/javascript/themes/glitch/features/compose/containers/navigation_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/navigation_container.js
@@ -1,6 +1,6 @@
 import { connect }   from 'react-redux';
 import NavigationBar from '../components/navigation_bar';
-import { me } from 'themes/glitch/util/initial_state';
+import { me } from 'flavours/glitch/util/initial_state';
 
 const mapStateToProps = state => {
   return {
diff --git a/app/javascript/themes/glitch/features/compose/containers/privacy_dropdown_container.js b/app/javascript/flavours/glitch/features/compose/containers/privacy_dropdown_container.js
index 9636ceab2..cb94fcc80 100644
--- a/app/javascript/themes/glitch/features/compose/containers/privacy_dropdown_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/privacy_dropdown_container.js
@@ -1,8 +1,8 @@
 import { connect } from 'react-redux';
 import PrivacyDropdown from '../components/privacy_dropdown';
-import { changeComposeVisibility } from 'themes/glitch/actions/compose';
-import { openModal, closeModal } from 'themes/glitch/actions/modal';
-import { isUserTouching } from 'themes/glitch/util/is_mobile';
+import { changeComposeVisibility } from 'flavours/glitch/actions/compose';
+import { openModal, closeModal } from 'flavours/glitch/actions/modal';
+import { isUserTouching } from 'flavours/glitch/util/is_mobile';
 
 const mapStateToProps = state => ({
   isModalOpen: state.get('modal').modalType === 'ACTIONS',
diff --git a/app/javascript/themes/glitch/features/compose/containers/reply_indicator_container.js b/app/javascript/flavours/glitch/features/compose/containers/reply_indicator_container.js
index 6dcabb3cd..a7c82d135 100644
--- a/app/javascript/themes/glitch/features/compose/containers/reply_indicator_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/reply_indicator_container.js
@@ -1,6 +1,6 @@
 import { connect } from 'react-redux';
-import { cancelReplyCompose } from 'themes/glitch/actions/compose';
-import { makeGetStatus } from 'themes/glitch/selectors';
+import { cancelReplyCompose } from 'flavours/glitch/actions/compose';
+import { makeGetStatus } from 'flavours/glitch/selectors';
 import ReplyIndicator from '../components/reply_indicator';
 
 const makeMapStateToProps = () => {
diff --git a/app/javascript/themes/glitch/features/compose/containers/search_container.js b/app/javascript/flavours/glitch/features/compose/containers/search_container.js
index a450d27e7..8f4bfcf08 100644
--- a/app/javascript/themes/glitch/features/compose/containers/search_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/search_container.js
@@ -4,7 +4,7 @@ import {
   clearSearch,
   submitSearch,
   showSearch,
-} from 'themes/glitch/actions/search';
+} from 'flavours/glitch/actions/search';
 import Search from '../components/search';
 
 const mapStateToProps = state => ({
diff --git a/app/javascript/themes/glitch/features/compose/containers/search_results_container.js b/app/javascript/flavours/glitch/features/compose/containers/search_results_container.js
index 16d95d417..16d95d417 100644
--- a/app/javascript/themes/glitch/features/compose/containers/search_results_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/search_results_container.js
diff --git a/app/javascript/themes/glitch/features/compose/containers/sensitive_button_container.js b/app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.js
index a710dd104..cf6706c0e 100644
--- a/app/javascript/themes/glitch/features/compose/containers/sensitive_button_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.js
@@ -2,9 +2,9 @@ import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
 import classNames from 'classnames';
-import IconButton from 'themes/glitch/components/icon_button';
-import { changeComposeSensitivity } from 'themes/glitch/actions/compose';
-import Motion from 'themes/glitch/util/optional_motion';
+import IconButton from 'flavours/glitch/components/icon_button';
+import { changeComposeSensitivity } from 'flavours/glitch/actions/compose';
+import Motion from 'flavours/glitch/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import { injectIntl, defineMessages } from 'react-intl';
 
diff --git a/app/javascript/themes/glitch/features/compose/containers/spoiler_button_container.js b/app/javascript/flavours/glitch/features/compose/containers/spoiler_button_container.js
index 160e71ba9..d7b4246bc 100644
--- a/app/javascript/themes/glitch/features/compose/containers/spoiler_button_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/spoiler_button_container.js
@@ -1,6 +1,6 @@
 import { connect } from 'react-redux';
 import TextIconButton from '../components/text_icon_button';
-import { changeComposeSpoilerness } from 'themes/glitch/actions/compose';
+import { changeComposeSpoilerness } from 'flavours/glitch/actions/compose';
 import { injectIntl, defineMessages } from 'react-intl';
 
 const messages = defineMessages({
diff --git a/app/javascript/themes/glitch/features/compose/containers/upload_button_container.js b/app/javascript/flavours/glitch/features/compose/containers/upload_button_container.js
index f332eae1a..4c1cb49e9 100644
--- a/app/javascript/themes/glitch/features/compose/containers/upload_button_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/upload_button_container.js
@@ -1,6 +1,6 @@
 import { connect } from 'react-redux';
 import UploadButton from '../components/upload_button';
-import { uploadCompose } from 'themes/glitch/actions/compose';
+import { uploadCompose } from 'flavours/glitch/actions/compose';
 
 const mapStateToProps = state => ({
   disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => m.get('type') === 'video')),
diff --git a/app/javascript/themes/glitch/features/compose/containers/upload_container.js b/app/javascript/flavours/glitch/features/compose/containers/upload_container.js
index eea514bf5..368038425 100644
--- a/app/javascript/themes/glitch/features/compose/containers/upload_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/upload_container.js
@@ -1,6 +1,6 @@
 import { connect } from 'react-redux';
 import Upload from '../components/upload';
-import { undoUploadCompose, changeUploadCompose } from 'themes/glitch/actions/compose';
+import { undoUploadCompose, changeUploadCompose } from 'flavours/glitch/actions/compose';
 
 const mapStateToProps = (state, { id }) => ({
   media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
diff --git a/app/javascript/themes/glitch/features/compose/containers/upload_form_container.js b/app/javascript/flavours/glitch/features/compose/containers/upload_form_container.js
index a6798bf51..a6798bf51 100644
--- a/app/javascript/themes/glitch/features/compose/containers/upload_form_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/upload_form_container.js
diff --git a/app/javascript/themes/glitch/features/compose/containers/upload_progress_container.js b/app/javascript/flavours/glitch/features/compose/containers/upload_progress_container.js
index 0cfee96da..0cfee96da 100644
--- a/app/javascript/themes/glitch/features/compose/containers/upload_progress_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/upload_progress_container.js
diff --git a/app/javascript/themes/glitch/features/compose/containers/warning_container.js b/app/javascript/flavours/glitch/features/compose/containers/warning_container.js
index 225d6a1dd..f20c75b91 100644
--- a/app/javascript/themes/glitch/features/compose/containers/warning_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/warning_container.js
@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
 import Warning from '../components/warning';
 import PropTypes from 'prop-types';
 import { FormattedMessage } from 'react-intl';
-import { me } from 'themes/glitch/util/initial_state';
+import { me } from 'flavours/glitch/util/initial_state';
 
 const mapStateToProps = state => ({
   needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']),
diff --git a/app/javascript/themes/glitch/features/compose/index.js b/app/javascript/flavours/glitch/features/compose/index.js
index 3fcaf416f..02b97ad00 100644
--- a/app/javascript/themes/glitch/features/compose/index.js
+++ b/app/javascript/flavours/glitch/features/compose/index.js
@@ -4,16 +4,16 @@ import NavigationContainer from './containers/navigation_container';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import { connect } from 'react-redux';
-import { mountCompose, unmountCompose } from 'themes/glitch/actions/compose';
-import { openModal } from 'themes/glitch/actions/modal';
-import { changeLocalSetting } from 'themes/glitch/actions/local_settings';
+import { mountCompose, unmountCompose } from 'flavours/glitch/actions/compose';
+import { openModal } from 'flavours/glitch/actions/modal';
+import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
 import { Link } from 'react-router-dom';
 import { injectIntl, defineMessages } from 'react-intl';
 import SearchContainer from './containers/search_container';
-import Motion from 'themes/glitch/util/optional_motion';
+import Motion from 'flavours/glitch/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import SearchResultsContainer from './containers/search_results_container';
-import { changeComposing } from 'themes/glitch/actions/compose';
+import { changeComposing } from 'flavours/glitch/actions/compose';
 
 const messages = defineMessages({
   start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
diff --git a/app/javascript/themes/glitch/features/direct_timeline/containers/column_settings_container.js b/app/javascript/flavours/glitch/features/direct_timeline/containers/column_settings_container.js
index 2a40c65a5..d3e4b4216 100644
--- a/app/javascript/themes/glitch/features/direct_timeline/containers/column_settings_container.js
+++ b/app/javascript/flavours/glitch/features/direct_timeline/containers/column_settings_container.js
@@ -1,6 +1,6 @@
 import { connect } from 'react-redux';
-import ColumnSettings from 'themes/glitch/features/community_timeline/components/column_settings';
-import { changeSetting } from 'themes/glitch/actions/settings';
+import ColumnSettings from 'flavours/glitch/features/community_timeline/components/column_settings';
+import { changeSetting } from 'flavours/glitch/actions/settings';
 
 const mapStateToProps = state => ({
   settings: state.getIn(['settings', 'direct']),
diff --git a/app/javascript/themes/glitch/features/direct_timeline/index.js b/app/javascript/flavours/glitch/features/direct_timeline/index.js
index 6b29cf94d..81096c0ec 100644
--- a/app/javascript/themes/glitch/features/direct_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/direct_timeline/index.js
@@ -1,17 +1,17 @@
 import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
-import StatusListContainer from 'themes/glitch/features/ui/containers/status_list_container';
-import Column from 'themes/glitch/components/column';
-import ColumnHeader from 'themes/glitch/components/column_header';
+import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
+import Column from 'flavours/glitch/components/column';
+import ColumnHeader from 'flavours/glitch/components/column_header';
 import {
   refreshDirectTimeline,
   expandDirectTimeline,
-} from 'themes/glitch/actions/timelines';
-import { addColumn, removeColumn, moveColumn } from 'themes/glitch/actions/columns';
+} from 'flavours/glitch/actions/timelines';
+import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import ColumnSettingsContainer from './containers/column_settings_container';
-import { connectDirectStream } from 'themes/glitch/actions/streaming';
+import { connectDirectStream } from 'flavours/glitch/actions/streaming';
 
 const messages = defineMessages({
   title: { id: 'column.direct', defaultMessage: 'Direct messages' },
diff --git a/app/javascript/themes/glitch/features/favourited_statuses/index.js b/app/javascript/flavours/glitch/features/favourited_statuses/index.js
index 80345e0e2..e20dda718 100644
--- a/app/javascript/themes/glitch/features/favourited_statuses/index.js
+++ b/app/javascript/flavours/glitch/features/favourited_statuses/index.js
@@ -2,11 +2,11 @@ import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import { fetchFavouritedStatuses, expandFavouritedStatuses } from 'themes/glitch/actions/favourites';
-import Column from 'themes/glitch/features/ui/components/column';
-import ColumnHeader from 'themes/glitch/components/column_header';
-import { addColumn, removeColumn, moveColumn } from 'themes/glitch/actions/columns';
-import StatusList from 'themes/glitch/components/status_list';
+import { fetchFavouritedStatuses, expandFavouritedStatuses } from 'flavours/glitch/actions/favourites';
+import Column from 'flavours/glitch/features/ui/components/column';
+import ColumnHeader from 'flavours/glitch/components/column_header';
+import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
+import StatusList from 'flavours/glitch/components/status_list';
 import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
diff --git a/app/javascript/themes/glitch/features/favourites/index.js b/app/javascript/flavours/glitch/features/favourites/index.js
index d7b8ac3b1..055a15ccb 100644
--- a/app/javascript/themes/glitch/features/favourites/index.js
+++ b/app/javascript/flavours/glitch/features/favourites/index.js
@@ -2,12 +2,12 @@ import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import LoadingIndicator from 'themes/glitch/components/loading_indicator';
-import { fetchFavourites } from 'themes/glitch/actions/interactions';
+import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
+import { fetchFavourites } from 'flavours/glitch/actions/interactions';
 import { ScrollContainer } from 'react-router-scroll-4';
-import AccountContainer from 'themes/glitch/containers/account_container';
-import Column from 'themes/glitch/features/ui/components/column';
-import ColumnBackButton from 'themes/glitch/components/column_back_button';
+import AccountContainer from 'flavours/glitch/containers/account_container';
+import Column from 'flavours/glitch/features/ui/components/column';
+import ColumnBackButton from 'flavours/glitch/components/column_back_button';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 const mapStateToProps = (state, props) => ({
diff --git a/app/javascript/themes/glitch/features/follow_requests/components/account_authorize.js b/app/javascript/flavours/glitch/features/follow_requests/components/account_authorize.js
index ce386d888..dead0753f 100644
--- a/app/javascript/themes/glitch/features/follow_requests/components/account_authorize.js
+++ b/app/javascript/flavours/glitch/features/follow_requests/components/account_authorize.js
@@ -1,10 +1,10 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import Permalink from 'themes/glitch/components/permalink';
-import Avatar from 'themes/glitch/components/avatar';
-import DisplayName from 'themes/glitch/components/display_name';
-import IconButton from 'themes/glitch/components/icon_button';
+import Permalink from 'flavours/glitch/components/permalink';
+import Avatar from 'flavours/glitch/components/avatar';
+import DisplayName from 'flavours/glitch/components/display_name';
+import IconButton from 'flavours/glitch/components/icon_button';
 import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
diff --git a/app/javascript/themes/glitch/features/follow_requests/containers/account_authorize_container.js b/app/javascript/flavours/glitch/features/follow_requests/containers/account_authorize_container.js
index 78ae77eee..693e98e8c 100644
--- a/app/javascript/themes/glitch/features/follow_requests/containers/account_authorize_container.js
+++ b/app/javascript/flavours/glitch/features/follow_requests/containers/account_authorize_container.js
@@ -1,7 +1,7 @@
 import { connect } from 'react-redux';
-import { makeGetAccount } from 'themes/glitch/selectors';
+import { makeGetAccount } from 'flavours/glitch/selectors';
 import AccountAuthorize from '../components/account_authorize';
-import { authorizeFollowRequest, rejectFollowRequest } from 'themes/glitch/actions/accounts';
+import { authorizeFollowRequest, rejectFollowRequest } from 'flavours/glitch/actions/accounts';
 
 const makeMapStateToProps = () => {
   const getAccount = makeGetAccount();
diff --git a/app/javascript/themes/glitch/features/follow_requests/index.js b/app/javascript/flavours/glitch/features/follow_requests/index.js
index 3f44f518a..04ff3f111 100644
--- a/app/javascript/themes/glitch/features/follow_requests/index.js
+++ b/app/javascript/flavours/glitch/features/follow_requests/index.js
@@ -2,12 +2,12 @@ import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import LoadingIndicator from 'themes/glitch/components/loading_indicator';
+import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
 import { ScrollContainer } from 'react-router-scroll-4';
-import Column from 'themes/glitch/features/ui/components/column';
-import ColumnBackButtonSlim from 'themes/glitch/components/column_back_button_slim';
+import Column from 'flavours/glitch/features/ui/components/column';
+import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim';
 import AccountAuthorizeContainer from './containers/account_authorize_container';
-import { fetchFollowRequests, expandFollowRequests } from 'themes/glitch/actions/accounts';
+import { fetchFollowRequests, expandFollowRequests } from 'flavours/glitch/actions/accounts';
 import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
diff --git a/app/javascript/themes/glitch/features/followers/index.js b/app/javascript/flavours/glitch/features/followers/index.js
index d586bf41d..f0ef29ff6 100644
--- a/app/javascript/themes/glitch/features/followers/index.js
+++ b/app/javascript/flavours/glitch/features/followers/index.js
@@ -2,18 +2,18 @@ import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import LoadingIndicator from 'themes/glitch/components/loading_indicator';
+import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
 import {
   fetchAccount,
   fetchFollowers,
   expandFollowers,
-} from 'themes/glitch/actions/accounts';
+} from 'flavours/glitch/actions/accounts';
 import { ScrollContainer } from 'react-router-scroll-4';
-import AccountContainer from 'themes/glitch/containers/account_container';
-import Column from 'themes/glitch/features/ui/components/column';
-import HeaderContainer from 'themes/glitch/features/account_timeline/containers/header_container';
-import LoadMore from 'themes/glitch/components/load_more';
-import ColumnBackButton from 'themes/glitch/components/column_back_button';
+import AccountContainer from 'flavours/glitch/containers/account_container';
+import Column from 'flavours/glitch/features/ui/components/column';
+import HeaderContainer from 'flavours/glitch/features/account_timeline/containers/header_container';
+import LoadMore from 'flavours/glitch/components/load_more';
+import ColumnBackButton from 'flavours/glitch/components/column_back_button';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 const mapStateToProps = (state, props) => ({
diff --git a/app/javascript/themes/glitch/features/following/index.js b/app/javascript/flavours/glitch/features/following/index.js
index c306faf21..f30f7b0d9 100644
--- a/app/javascript/themes/glitch/features/following/index.js
+++ b/app/javascript/flavours/glitch/features/following/index.js
@@ -2,18 +2,18 @@ import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import LoadingIndicator from 'themes/glitch/components/loading_indicator';
+import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
 import {
   fetchAccount,
   fetchFollowing,
   expandFollowing,
-} from 'themes/glitch/actions/accounts';
+} from 'flavours/glitch/actions/accounts';
 import { ScrollContainer } from 'react-router-scroll-4';
-import AccountContainer from 'themes/glitch/containers/account_container';
-import Column from 'themes/glitch/features/ui/components/column';
-import HeaderContainer from 'themes/glitch/features/account_timeline/containers/header_container';
-import LoadMore from 'themes/glitch/components/load_more';
-import ColumnBackButton from 'themes/glitch/components/column_back_button';
+import AccountContainer from 'flavours/glitch/containers/account_container';
+import Column from 'flavours/glitch/features/ui/components/column';
+import HeaderContainer from 'flavours/glitch/features/account_timeline/containers/header_container';
+import LoadMore from 'flavours/glitch/components/load_more';
+import ColumnBackButton from 'flavours/glitch/components/column_back_button';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 const mapStateToProps = (state, props) => ({
diff --git a/app/javascript/flavours/glitch/features/generic_not_found/index.js b/app/javascript/flavours/glitch/features/generic_not_found/index.js
new file mode 100644
index 000000000..d01a1ba47
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/generic_not_found/index.js
@@ -0,0 +1,11 @@
+import React from 'react';
+import Column from 'flavours/glitch/features/ui/components/column';
+import MissingIndicator from 'flavours/glitch/components/missing_indicator';
+
+const GenericNotFound = () => (
+  <Column>
+    <MissingIndicator />
+  </Column>
+);
+
+export default GenericNotFound;
diff --git a/app/javascript/themes/glitch/features/getting_started/index.js b/app/javascript/flavours/glitch/features/getting_started/index.js
index 74b019cf1..2af0fcf48 100644
--- a/app/javascript/themes/glitch/features/getting_started/index.js
+++ b/app/javascript/flavours/glitch/features/getting_started/index.js
@@ -1,14 +1,14 @@
 import React from 'react';
-import Column from 'themes/glitch/features/ui/components/column';
-import ColumnLink from 'themes/glitch/features/ui/components/column_link';
-import ColumnSubheading from 'themes/glitch/features/ui/components/column_subheading';
+import Column from 'flavours/glitch/features/ui/components/column';
+import ColumnLink from 'flavours/glitch/features/ui/components/column_link';
+import ColumnSubheading from 'flavours/glitch/features/ui/components/column_subheading';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import { connect } from 'react-redux';
-import { openModal } from 'themes/glitch/actions/modal';
+import { openModal } from 'flavours/glitch/actions/modal';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import { me } from 'themes/glitch/util/initial_state';
+import { me } from 'flavours/glitch/util/initial_state';
 
 const messages = defineMessages({
   heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
diff --git a/app/javascript/themes/glitch/features/hashtag_timeline/index.js b/app/javascript/flavours/glitch/features/hashtag_timeline/index.js
index a878931b3..9f3c9bec7 100644
--- a/app/javascript/themes/glitch/features/hashtag_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/hashtag_timeline/index.js
@@ -1,16 +1,16 @@
 import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
-import StatusListContainer from 'themes/glitch/features/ui/containers/status_list_container';
-import Column from 'themes/glitch/components/column';
-import ColumnHeader from 'themes/glitch/components/column_header';
+import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
+import Column from 'flavours/glitch/components/column';
+import ColumnHeader from 'flavours/glitch/components/column_header';
 import {
   refreshHashtagTimeline,
   expandHashtagTimeline,
-} from 'themes/glitch/actions/timelines';
-import { addColumn, removeColumn, moveColumn } from 'themes/glitch/actions/columns';
+} from 'flavours/glitch/actions/timelines';
+import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
 import { FormattedMessage } from 'react-intl';
-import { connectHashtagStream } from 'themes/glitch/actions/streaming';
+import { connectHashtagStream } from 'flavours/glitch/actions/streaming';
 
 const mapStateToProps = (state, props) => ({
   hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}`, 'unread']) > 0,
diff --git a/app/javascript/themes/glitch/features/home_timeline/components/column_settings.js b/app/javascript/flavours/glitch/features/home_timeline/components/column_settings.js
index 533da6c36..3ff0a1daf 100644
--- a/app/javascript/themes/glitch/features/home_timeline/components/column_settings.js
+++ b/app/javascript/flavours/glitch/features/home_timeline/components/column_settings.js
@@ -2,8 +2,8 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import SettingToggle from 'themes/glitch/features/notifications/components/setting_toggle';
-import SettingText from 'themes/glitch/components/setting_text';
+import SettingToggle from 'flavours/glitch/features/notifications/components/setting_toggle';
+import SettingText from 'flavours/glitch/components/setting_text';
 
 const messages = defineMessages({
   filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' },
@@ -34,6 +34,10 @@ export default class ColumnSettings extends React.PureComponent {
           <SettingToggle prefix='home_timeline' settings={settings} settingKey={['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} settingKey={['shows', 'direct']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_direct' defaultMessage='Show DMs' />} />
+        </div>
+
         <span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
 
         <div className='column-settings__row'>
diff --git a/app/javascript/themes/glitch/features/home_timeline/containers/column_settings_container.js b/app/javascript/flavours/glitch/features/home_timeline/containers/column_settings_container.js
index a0062f564..19a8e792f 100644
--- a/app/javascript/themes/glitch/features/home_timeline/containers/column_settings_container.js
+++ b/app/javascript/flavours/glitch/features/home_timeline/containers/column_settings_container.js
@@ -1,6 +1,6 @@
 import { connect } from 'react-redux';
 import ColumnSettings from '../components/column_settings';
-import { changeSetting, saveSettings } from 'themes/glitch/actions/settings';
+import { changeSetting, saveSettings } from 'flavours/glitch/actions/settings';
 
 const mapStateToProps = state => ({
   settings: state.getIn(['settings', 'home']),
diff --git a/app/javascript/themes/glitch/features/home_timeline/index.js b/app/javascript/flavours/glitch/features/home_timeline/index.js
index 8a65891cd..2dfec6bbe 100644
--- a/app/javascript/themes/glitch/features/home_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/home_timeline/index.js
@@ -1,11 +1,11 @@
 import React from 'react';
 import { connect } from 'react-redux';
-import { expandHomeTimeline } from 'themes/glitch/actions/timelines';
+import { expandHomeTimeline } from 'flavours/glitch/actions/timelines';
 import PropTypes from 'prop-types';
-import StatusListContainer from 'themes/glitch/features/ui/containers/status_list_container';
-import Column from 'themes/glitch/components/column';
-import ColumnHeader from 'themes/glitch/components/column_header';
-import { addColumn, removeColumn, moveColumn } from 'themes/glitch/actions/columns';
+import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
+import Column from 'flavours/glitch/components/column';
+import ColumnHeader from 'flavours/glitch/components/column_header';
+import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import ColumnSettingsContainer from './containers/column_settings_container';
 import { Link } from 'react-router-dom';
diff --git a/app/javascript/themes/glitch/features/local_settings/index.js b/app/javascript/flavours/glitch/features/local_settings/index.js
index 6c5d51413..4e4605ea9 100644
--- a/app/javascript/themes/glitch/features/local_settings/index.js
+++ b/app/javascript/flavours/glitch/features/local_settings/index.js
@@ -7,11 +7,8 @@ import { connect } from 'react-redux';
 //  Our imports
 import LocalSettingsPage from './page';
 import LocalSettingsNavigation from './navigation';
-import { closeModal } from 'themes/glitch/actions/modal';
-import { changeLocalSetting } from 'themes/glitch/actions/local_settings';
-
-//  Stylesheet imports
-import './style.scss';
+import { closeModal } from 'flavours/glitch/actions/modal';
+import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
 
 const mapStateToProps = state => ({
   settings: state.get('local_settings'),
diff --git a/app/javascript/themes/glitch/features/local_settings/navigation/index.js b/app/javascript/flavours/glitch/features/local_settings/navigation/index.js
index fa35e83c7..fc2167c0c 100644
--- a/app/javascript/themes/glitch/features/local_settings/navigation/index.js
+++ b/app/javascript/flavours/glitch/features/local_settings/navigation/index.js
@@ -6,9 +6,6 @@ import { injectIntl, defineMessages } from 'react-intl';
 //  Our imports
 import LocalSettingsNavigationItem from './item';
 
-//  Stylesheet imports
-import './style.scss';
-
 //  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 
 const messages = defineMessages({
diff --git a/app/javascript/themes/glitch/features/local_settings/navigation/item/index.js b/app/javascript/flavours/glitch/features/local_settings/navigation/item/index.js
index a352d5fb2..b67d479e7 100644
--- a/app/javascript/themes/glitch/features/local_settings/navigation/item/index.js
+++ b/app/javascript/flavours/glitch/features/local_settings/navigation/item/index.js
@@ -3,9 +3,6 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import classNames from 'classnames';
 
-//  Stylesheet imports
-import './style.scss';
-
 //  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 
 export default class LocalSettingsPage extends React.PureComponent {
diff --git a/app/javascript/themes/glitch/features/local_settings/page/index.js b/app/javascript/flavours/glitch/features/local_settings/page/index.js
index 498230f7b..62bf410c6 100644
--- a/app/javascript/themes/glitch/features/local_settings/page/index.js
+++ b/app/javascript/flavours/glitch/features/local_settings/page/index.js
@@ -7,9 +7,6 @@ import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
 //  Our imports
 import LocalSettingsPageItem from './item';
 
-//  Stylesheet imports
-import './style.scss';
-
 //  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 
 const messages = defineMessages({
diff --git a/app/javascript/themes/glitch/features/local_settings/page/item/index.js b/app/javascript/flavours/glitch/features/local_settings/page/item/index.js
index 37e28c084..66e84dfe1 100644
--- a/app/javascript/themes/glitch/features/local_settings/page/item/index.js
+++ b/app/javascript/flavours/glitch/features/local_settings/page/item/index.js
@@ -3,9 +3,6 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 
-//  Stylesheet imports
-import './style.scss';
-
 //  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 
 export default class LocalSettingsPageItem extends React.PureComponent {
diff --git a/app/javascript/themes/glitch/features/mutes/index.js b/app/javascript/flavours/glitch/features/mutes/index.js
index 1158b8262..87517eec9 100644
--- a/app/javascript/themes/glitch/features/mutes/index.js
+++ b/app/javascript/flavours/glitch/features/mutes/index.js
@@ -2,12 +2,12 @@ import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import LoadingIndicator from 'themes/glitch/components/loading_indicator';
+import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
 import { ScrollContainer } from 'react-router-scroll-4';
-import Column from 'themes/glitch/features/ui/components/column';
-import ColumnBackButtonSlim from 'themes/glitch/components/column_back_button_slim';
-import AccountContainer from 'themes/glitch/containers/account_container';
-import { fetchMutes, expandMutes } from 'themes/glitch/actions/mutes';
+import Column from 'flavours/glitch/features/ui/components/column';
+import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim';
+import AccountContainer from 'flavours/glitch/containers/account_container';
+import { fetchMutes, expandMutes } from 'flavours/glitch/actions/mutes';
 import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
diff --git a/app/javascript/themes/glitch/features/notifications/components/clear_column_button.js b/app/javascript/flavours/glitch/features/notifications/components/clear_column_button.js
index 22a10753f..22a10753f 100644
--- a/app/javascript/themes/glitch/features/notifications/components/clear_column_button.js
+++ b/app/javascript/flavours/glitch/features/notifications/components/clear_column_button.js
diff --git a/app/javascript/themes/glitch/features/notifications/components/column_settings.js b/app/javascript/flavours/glitch/features/notifications/components/column_settings.js
index 88a29d4d3..88a29d4d3 100644
--- a/app/javascript/themes/glitch/features/notifications/components/column_settings.js
+++ b/app/javascript/flavours/glitch/features/notifications/components/column_settings.js
diff --git a/app/javascript/themes/glitch/features/notifications/components/follow.js b/app/javascript/flavours/glitch/features/notifications/components/follow.js
index 96cfe83e6..54506f67c 100644
--- a/app/javascript/themes/glitch/features/notifications/components/follow.js
+++ b/app/javascript/flavours/glitch/features/notifications/components/follow.js
@@ -7,8 +7,8 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
 import { HotKeys } from 'react-hotkeys';
 
 // Our imports.
-import Permalink from 'themes/glitch/components/permalink';
-import AccountContainer from 'themes/glitch/containers/account_container';
+import Permalink from 'flavours/glitch/components/permalink';
+import AccountContainer from 'flavours/glitch/containers/account_container';
 import NotificationOverlayContainer from '../containers/overlay_container';
 
 export default class NotificationFollow extends ImmutablePureComponent {
diff --git a/app/javascript/themes/glitch/features/notifications/components/notification.js b/app/javascript/flavours/glitch/features/notifications/components/notification.js
index 47f5770bf..cc77426d3 100644
--- a/app/javascript/themes/glitch/features/notifications/components/notification.js
+++ b/app/javascript/flavours/glitch/features/notifications/components/notification.js
@@ -5,7 +5,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 //  Our imports,
-import StatusContainer from 'themes/glitch/containers/status_container';
+import StatusContainer from 'flavours/glitch/containers/status_container';
 import NotificationFollow from './follow';
 
 export default class Notification extends ImmutablePureComponent {
diff --git a/app/javascript/themes/glitch/features/notifications/components/overlay.js b/app/javascript/flavours/glitch/features/notifications/components/overlay.js
index e56f9c628..e56f9c628 100644
--- a/app/javascript/themes/glitch/features/notifications/components/overlay.js
+++ b/app/javascript/flavours/glitch/features/notifications/components/overlay.js
diff --git a/app/javascript/themes/glitch/features/notifications/components/setting_toggle.js b/app/javascript/flavours/glitch/features/notifications/components/setting_toggle.js
index 281359d2a..281359d2a 100644
--- a/app/javascript/themes/glitch/features/notifications/components/setting_toggle.js
+++ b/app/javascript/flavours/glitch/features/notifications/components/setting_toggle.js
diff --git a/app/javascript/themes/glitch/features/notifications/containers/column_settings_container.js b/app/javascript/flavours/glitch/features/notifications/containers/column_settings_container.js
index ddc8495f4..ce502700c 100644
--- a/app/javascript/themes/glitch/features/notifications/containers/column_settings_container.js
+++ b/app/javascript/flavours/glitch/features/notifications/containers/column_settings_container.js
@@ -1,10 +1,10 @@
 import { connect } from 'react-redux';
 import { defineMessages, injectIntl } from 'react-intl';
 import ColumnSettings from '../components/column_settings';
-import { changeSetting, saveSettings } from 'themes/glitch/actions/settings';
-import { clearNotifications } from 'themes/glitch/actions/notifications';
-import { changeAlerts as changePushNotifications, saveSettings as savePushNotificationSettings } from 'themes/glitch/actions/push_notifications';
-import { openModal } from 'themes/glitch/actions/modal';
+import { changeSetting, saveSettings } from 'flavours/glitch/actions/settings';
+import { clearNotifications } from 'flavours/glitch/actions/notifications';
+import { changeAlerts as changePushNotifications, saveSettings as savePushNotificationSettings } from 'flavours/glitch/actions/push_notifications';
+import { openModal } from 'flavours/glitch/actions/modal';
 
 const messages = defineMessages({
   clearMessage: { id: 'notifications.clear_confirmation', defaultMessage: 'Are you sure you want to permanently clear all your notifications?' },
diff --git a/app/javascript/themes/glitch/features/notifications/containers/notification_container.js b/app/javascript/flavours/glitch/features/notifications/containers/notification_container.js
index 033c61e3d..be007f30b 100644
--- a/app/javascript/themes/glitch/features/notifications/containers/notification_container.js
+++ b/app/javascript/flavours/glitch/features/notifications/containers/notification_container.js
@@ -2,9 +2,9 @@
 import { connect } from 'react-redux';
 
 //  Our imports.
-import { makeGetNotification } from 'themes/glitch/selectors';
+import { makeGetNotification } from 'flavours/glitch/selectors';
 import Notification from '../components/notification';
-import { mentionCompose } from 'themes/glitch/actions/compose';
+import { mentionCompose } from 'flavours/glitch/actions/compose';
 
 const makeMapStateToProps = () => {
   const getNotification = makeGetNotification();
diff --git a/app/javascript/themes/glitch/features/notifications/containers/overlay_container.js b/app/javascript/flavours/glitch/features/notifications/containers/overlay_container.js
index 52649cdd7..ee2d19814 100644
--- a/app/javascript/themes/glitch/features/notifications/containers/overlay_container.js
+++ b/app/javascript/flavours/glitch/features/notifications/containers/overlay_container.js
@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
 
 //  Our imports.
 import NotificationOverlay from '../components/overlay';
-import { markNotificationForDelete } from 'themes/glitch/actions/notifications';
+import { markNotificationForDelete } from 'flavours/glitch/actions/notifications';
 
 const mapDispatchToProps = dispatch => ({
   onMarkForDelete(id, yes) {
diff --git a/app/javascript/themes/glitch/features/notifications/index.js b/app/javascript/flavours/glitch/features/notifications/index.js
index 1ecde660a..12b0b5b83 100644
--- a/app/javascript/themes/glitch/features/notifications/index.js
+++ b/app/javascript/flavours/glitch/features/notifications/index.js
@@ -2,21 +2,21 @@ import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import Column from 'themes/glitch/components/column';
-import ColumnHeader from 'themes/glitch/components/column_header';
+import Column from 'flavours/glitch/components/column';
+import ColumnHeader from 'flavours/glitch/components/column_header';
 import {
   enterNotificationClearingMode,
   expandNotifications,
   scrollTopNotifications,
-} from 'themes/glitch/actions/notifications';
-import { addColumn, removeColumn, moveColumn } from 'themes/glitch/actions/columns';
+} from 'flavours/glitch/actions/notifications';
+import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
 import NotificationContainer from './containers/notification_container';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import ColumnSettingsContainer from './containers/column_settings_container';
 import { createSelector } from 'reselect';
 import { List as ImmutableList } from 'immutable';
 import { debounce } from 'lodash';
-import ScrollableList from 'themes/glitch/components/scrollable_list';
+import ScrollableList from 'flavours/glitch/components/scrollable_list';
 
 const messages = defineMessages({
   title: { id: 'column.notifications', defaultMessage: 'Notifications' },
diff --git a/app/javascript/themes/glitch/features/pinned_statuses/index.js b/app/javascript/flavours/glitch/features/pinned_statuses/index.js
index 0a3997850..f56d70176 100644
--- a/app/javascript/themes/glitch/features/pinned_statuses/index.js
+++ b/app/javascript/flavours/glitch/features/pinned_statuses/index.js
@@ -2,10 +2,10 @@ import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import { fetchPinnedStatuses } from 'themes/glitch/actions/pin_statuses';
-import Column from 'themes/glitch/features/ui/components/column';
-import ColumnBackButtonSlim from 'themes/glitch/components/column_back_button_slim';
-import StatusList from 'themes/glitch/components/status_list';
+import { fetchPinnedStatuses } from 'flavours/glitch/actions/pin_statuses';
+import Column from 'flavours/glitch/features/ui/components/column';
+import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim';
+import StatusList from 'flavours/glitch/components/status_list';
 import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
diff --git a/app/javascript/themes/glitch/features/public_timeline/containers/column_settings_container.js b/app/javascript/flavours/glitch/features/public_timeline/containers/column_settings_container.js
index 0185a7724..b13e20645 100644
--- a/app/javascript/themes/glitch/features/public_timeline/containers/column_settings_container.js
+++ b/app/javascript/flavours/glitch/features/public_timeline/containers/column_settings_container.js
@@ -1,6 +1,6 @@
 import { connect } from 'react-redux';
-import ColumnSettings from 'themes/glitch/features/community_timeline/components/column_settings';
-import { changeSetting } from 'themes/glitch/actions/settings';
+import ColumnSettings from 'flavours/glitch/features/community_timeline/components/column_settings';
+import { changeSetting } from 'flavours/glitch/actions/settings';
 
 const mapStateToProps = state => ({
   settings: state.getIn(['settings', 'public']),
diff --git a/app/javascript/themes/glitch/features/public_timeline/index.js b/app/javascript/flavours/glitch/features/public_timeline/index.js
index f5b3865af..bbdd4612e 100644
--- a/app/javascript/themes/glitch/features/public_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/public_timeline/index.js
@@ -1,17 +1,17 @@
 import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
-import StatusListContainer from 'themes/glitch/features/ui/containers/status_list_container';
-import Column from 'themes/glitch/components/column';
-import ColumnHeader from 'themes/glitch/components/column_header';
+import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
+import Column from 'flavours/glitch/components/column';
+import ColumnHeader from 'flavours/glitch/components/column_header';
 import {
   refreshPublicTimeline,
   expandPublicTimeline,
-} from 'themes/glitch/actions/timelines';
-import { addColumn, removeColumn, moveColumn } from 'themes/glitch/actions/columns';
+} from 'flavours/glitch/actions/timelines';
+import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import ColumnSettingsContainer from './containers/column_settings_container';
-import { connectPublicStream } from 'themes/glitch/actions/streaming';
+import { connectPublicStream } from 'flavours/glitch/actions/streaming';
 
 const messages = defineMessages({
   title: { id: 'column.public', defaultMessage: 'Federated timeline' },
diff --git a/app/javascript/themes/glitch/features/reblogs/index.js b/app/javascript/flavours/glitch/features/reblogs/index.js
index 8723f7c7c..25b792b39 100644
--- a/app/javascript/themes/glitch/features/reblogs/index.js
+++ b/app/javascript/flavours/glitch/features/reblogs/index.js
@@ -2,12 +2,12 @@ import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import LoadingIndicator from 'themes/glitch/components/loading_indicator';
-import { fetchReblogs } from 'themes/glitch/actions/interactions';
+import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
+import { fetchReblogs } from 'flavours/glitch/actions/interactions';
 import { ScrollContainer } from 'react-router-scroll-4';
-import AccountContainer from 'themes/glitch/containers/account_container';
-import Column from 'themes/glitch/features/ui/components/column';
-import ColumnBackButton from 'themes/glitch/components/column_back_button';
+import AccountContainer from 'flavours/glitch/containers/account_container';
+import Column from 'flavours/glitch/features/ui/components/column';
+import ColumnBackButton from 'flavours/glitch/components/column_back_button';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 const mapStateToProps = (state, props) => ({
diff --git a/app/javascript/themes/glitch/features/report/components/status_check_box.js b/app/javascript/flavours/glitch/features/report/components/status_check_box.js
index cc9232201..cc9232201 100644
--- a/app/javascript/themes/glitch/features/report/components/status_check_box.js
+++ b/app/javascript/flavours/glitch/features/report/components/status_check_box.js
diff --git a/app/javascript/themes/glitch/features/report/containers/status_check_box_container.js b/app/javascript/flavours/glitch/features/report/containers/status_check_box_container.js
index 40d55fb3c..9bfd41ffc 100644
--- a/app/javascript/themes/glitch/features/report/containers/status_check_box_container.js
+++ b/app/javascript/flavours/glitch/features/report/containers/status_check_box_container.js
@@ -1,6 +1,6 @@
 import { connect } from 'react-redux';
 import StatusCheckBox from '../components/status_check_box';
-import { toggleStatusReport } from 'themes/glitch/actions/reports';
+import { toggleStatusReport } from 'flavours/glitch/actions/reports';
 import { Set as ImmutableSet } from 'immutable';
 
 const mapStateToProps = (state, { id }) => ({
diff --git a/app/javascript/flavours/glitch/features/standalone/compose/index.js b/app/javascript/flavours/glitch/features/standalone/compose/index.js
new file mode 100644
index 000000000..b33c21cb5
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/standalone/compose/index.js
@@ -0,0 +1,20 @@
+import React from 'react';
+import ComposeFormContainer from 'flavours/glitch/features/compose/containers/compose_form_container';
+import NotificationsContainer from 'flavours/glitch/features/ui/containers/notifications_container';
+import LoadingBarContainer from 'flavours/glitch/features/ui/containers/loading_bar_container';
+import ModalContainer from 'flavours/glitch/features/ui/containers/modal_container';
+
+export default class Compose extends React.PureComponent {
+
+  render () {
+    return (
+      <div>
+        <ComposeFormContainer />
+        <NotificationsContainer />
+        <ModalContainer />
+        <LoadingBarContainer className='loading-bar' />
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/themes/glitch/features/standalone/hashtag_timeline/index.js b/app/javascript/flavours/glitch/features/standalone/hashtag_timeline/index.js
index 7c56f264f..0ad2cef80 100644
--- a/app/javascript/themes/glitch/features/standalone/hashtag_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/standalone/hashtag_timeline/index.js
@@ -1,13 +1,13 @@
 import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
-import StatusListContainer from 'themes/glitch/features/ui/containers/status_list_container';
+import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
 import {
   refreshHashtagTimeline,
   expandHashtagTimeline,
-} from 'themes/glitch/actions/timelines';
-import Column from 'themes/glitch/components/column';
-import ColumnHeader from 'themes/glitch/components/column_header';
+} from 'flavours/glitch/actions/timelines';
+import Column from 'flavours/glitch/components/column';
+import ColumnHeader from 'flavours/glitch/components/column_header';
 
 @connect()
 export default class HashtagTimeline extends React.PureComponent {
diff --git a/app/javascript/themes/glitch/features/standalone/public_timeline/index.js b/app/javascript/flavours/glitch/features/standalone/public_timeline/index.js
index b3fb55288..717f6fcaf 100644
--- a/app/javascript/themes/glitch/features/standalone/public_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/standalone/public_timeline/index.js
@@ -1,13 +1,13 @@
 import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
-import StatusListContainer from 'themes/glitch/features/ui/containers/status_list_container';
+import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
 import {
   refreshPublicTimeline,
   expandPublicTimeline,
-} from 'themes/glitch/actions/timelines';
-import Column from 'themes/glitch/components/column';
-import ColumnHeader from 'themes/glitch/components/column_header';
+} from 'flavours/glitch/actions/timelines';
+import Column from 'flavours/glitch/components/column';
+import ColumnHeader from 'flavours/glitch/components/column_header';
 import { defineMessages, injectIntl } from 'react-intl';
 
 const messages = defineMessages({
diff --git a/app/javascript/themes/glitch/features/status/components/action_bar.js b/app/javascript/flavours/glitch/features/status/components/action_bar.js
index 6cda988d1..4d660ee3c 100644
--- a/app/javascript/themes/glitch/features/status/components/action_bar.js
+++ b/app/javascript/flavours/glitch/features/status/components/action_bar.js
@@ -1,10 +1,10 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import IconButton from 'themes/glitch/components/icon_button';
+import IconButton from 'flavours/glitch/components/icon_button';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import DropdownMenuContainer from 'themes/glitch/containers/dropdown_menu_container';
+import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
 import { defineMessages, injectIntl } from 'react-intl';
-import { me } from 'themes/glitch/util/initial_state';
+import { me } from 'flavours/glitch/util/initial_state';
 
 const messages = defineMessages({
   delete: { id: 'status.delete', defaultMessage: 'Delete' },
diff --git a/app/javascript/themes/glitch/features/status/components/card.js b/app/javascript/flavours/glitch/features/status/components/card.js
index bb83374b9..bb83374b9 100644
--- a/app/javascript/themes/glitch/features/status/components/card.js
+++ b/app/javascript/flavours/glitch/features/status/components/card.js
diff --git a/app/javascript/themes/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js
index df78ce4b6..0cb5238b0 100644
--- a/app/javascript/themes/glitch/features/status/components/detailed_status.js
+++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js
@@ -1,17 +1,17 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import Avatar from 'themes/glitch/components/avatar';
-import DisplayName from 'themes/glitch/components/display_name';
-import StatusContent from 'themes/glitch/components/status_content';
-import StatusGallery from 'themes/glitch/components/media_gallery';
-import AttachmentList from 'themes/glitch/components/attachment_list';
+import Avatar from 'flavours/glitch/components/avatar';
+import DisplayName from 'flavours/glitch/components/display_name';
+import StatusContent from 'flavours/glitch/components/status_content';
+import StatusGallery from 'flavours/glitch/components/media_gallery';
+import AttachmentList from 'flavours/glitch/components/attachment_list';
 import { Link } from 'react-router-dom';
 import { FormattedDate, FormattedNumber } from 'react-intl';
 import CardContainer from '../containers/card_container';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import Video from 'themes/glitch/features/video';
-import VisibilityIcon from 'themes/glitch/components/status_visibility_icon';
+import Video from 'flavours/glitch/features/video';
+import VisibilityIcon from 'flavours/glitch/components/status_visibility_icon';
 
 export default class DetailedStatus extends ImmutablePureComponent {
 
diff --git a/app/javascript/themes/glitch/features/status/containers/card_container.js b/app/javascript/flavours/glitch/features/status/containers/card_container.js
index a97404de1..a97404de1 100644
--- a/app/javascript/themes/glitch/features/status/containers/card_container.js
+++ b/app/javascript/flavours/glitch/features/status/containers/card_container.js
diff --git a/app/javascript/themes/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js
index 8561bd4de..93b0fe9d9 100644
--- a/app/javascript/themes/glitch/features/status/index.js
+++ b/app/javascript/flavours/glitch/features/status/index.js
@@ -3,11 +3,11 @@ import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
 import classNames from 'classnames';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import { fetchStatus } from 'themes/glitch/actions/statuses';
-import MissingIndicator from 'themes/glitch/components/missing_indicator';
+import { fetchStatus } from 'flavours/glitch/actions/statuses';
+import MissingIndicator from 'flavours/glitch/components/missing_indicator';
 import DetailedStatus from './components/detailed_status';
 import ActionBar from './components/action_bar';
-import Column from 'themes/glitch/features/ui/components/column';
+import Column from 'flavours/glitch/features/ui/components/column';
 import {
   favourite,
   unfavourite,
@@ -15,23 +15,23 @@ import {
   unreblog,
   pin,
   unpin,
-} from 'themes/glitch/actions/interactions';
+} from 'flavours/glitch/actions/interactions';
 import {
   replyCompose,
   mentionCompose,
-} from 'themes/glitch/actions/compose';
-import { deleteStatus } from 'themes/glitch/actions/statuses';
-import { initReport } from 'themes/glitch/actions/reports';
-import { makeGetStatus } from 'themes/glitch/selectors';
+} from 'flavours/glitch/actions/compose';
+import { deleteStatus } from 'flavours/glitch/actions/statuses';
+import { initReport } from 'flavours/glitch/actions/reports';
+import { makeGetStatus } from 'flavours/glitch/selectors';
 import { ScrollContainer } from 'react-router-scroll-4';
-import ColumnBackButton from 'themes/glitch/components/column_back_button';
-import StatusContainer from 'themes/glitch/containers/status_container';
-import { openModal } from 'themes/glitch/actions/modal';
+import ColumnBackButton from 'flavours/glitch/components/column_back_button';
+import StatusContainer from 'flavours/glitch/containers/status_container';
+import { openModal } from 'flavours/glitch/actions/modal';
 import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { HotKeys } from 'react-hotkeys';
-import { boostModal, deleteModal } from 'themes/glitch/util/initial_state';
-import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from 'themes/glitch/util/fullscreen';
+import { boostModal, deleteModal } from 'flavours/glitch/util/initial_state';
+import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from 'flavours/glitch/util/fullscreen';
 
 const messages = defineMessages({
   deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
diff --git a/app/javascript/themes/glitch/features/ui/components/actions_modal.js b/app/javascript/flavours/glitch/features/ui/components/actions_modal.js
index 7a2b78b63..0873c282f 100644
--- a/app/javascript/themes/glitch/features/ui/components/actions_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/actions_modal.js
@@ -2,11 +2,11 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import StatusContent from 'themes/glitch/components/status_content';
-import Avatar from 'themes/glitch/components/avatar';
-import RelativeTimestamp from 'themes/glitch/components/relative_timestamp';
-import DisplayName from 'themes/glitch/components/display_name';
-import IconButton from 'themes/glitch/components/icon_button';
+import StatusContent from 'flavours/glitch/components/status_content';
+import Avatar from 'flavours/glitch/components/avatar';
+import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
+import DisplayName from 'flavours/glitch/components/display_name';
+import IconButton from 'flavours/glitch/components/icon_button';
 import classNames from 'classnames';
 
 export default class ActionsModal extends ImmutablePureComponent {
diff --git a/app/javascript/themes/glitch/features/ui/components/boost_modal.js b/app/javascript/flavours/glitch/features/ui/components/boost_modal.js
index 49781db10..9652bcb2d 100644
--- a/app/javascript/themes/glitch/features/ui/components/boost_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/boost_modal.js
@@ -2,11 +2,11 @@ import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import Button from 'themes/glitch/components/button';
-import StatusContent from 'themes/glitch/components/status_content';
-import Avatar from 'themes/glitch/components/avatar';
-import RelativeTimestamp from 'themes/glitch/components/relative_timestamp';
-import DisplayName from 'themes/glitch/components/display_name';
+import Button from 'flavours/glitch/components/button';
+import StatusContent from 'flavours/glitch/components/status_content';
+import Avatar from 'flavours/glitch/components/avatar';
+import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
+import DisplayName from 'flavours/glitch/components/display_name';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 const messages = defineMessages({
diff --git a/app/javascript/themes/glitch/features/ui/components/bundle.js b/app/javascript/flavours/glitch/features/ui/components/bundle.js
index fc88e0c70..fc88e0c70 100644
--- a/app/javascript/themes/glitch/features/ui/components/bundle.js
+++ b/app/javascript/flavours/glitch/features/ui/components/bundle.js
diff --git a/app/javascript/themes/glitch/features/ui/components/bundle_column_error.js b/app/javascript/flavours/glitch/features/ui/components/bundle_column_error.js
index daedc6299..3e979a250 100644
--- a/app/javascript/themes/glitch/features/ui/components/bundle_column_error.js
+++ b/app/javascript/flavours/glitch/features/ui/components/bundle_column_error.js
@@ -4,8 +4,8 @@ import { defineMessages, injectIntl } from 'react-intl';
 
 import Column from './column';
 import ColumnHeader from './column_header';
-import ColumnBackButtonSlim from 'themes/glitch/components/column_back_button_slim';
-import IconButton from 'themes/glitch/components/icon_button';
+import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim';
+import IconButton from 'flavours/glitch/components/icon_button';
 
 const messages = defineMessages({
   title: { id: 'bundle_column_error.title', defaultMessage: 'Network error' },
diff --git a/app/javascript/themes/glitch/features/ui/components/bundle_modal_error.js b/app/javascript/flavours/glitch/features/ui/components/bundle_modal_error.js
index 8cca32ae9..2c14a1e5c 100644
--- a/app/javascript/themes/glitch/features/ui/components/bundle_modal_error.js
+++ b/app/javascript/flavours/glitch/features/ui/components/bundle_modal_error.js
@@ -2,7 +2,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { defineMessages, injectIntl } from 'react-intl';
 
-import IconButton from 'themes/glitch/components/icon_button';
+import IconButton from 'flavours/glitch/components/icon_button';
 
 const messages = defineMessages({
   error: { id: 'bundle_modal_error.message', defaultMessage: 'Something went wrong while loading this component.' },
diff --git a/app/javascript/themes/glitch/features/ui/components/column.js b/app/javascript/flavours/glitch/features/ui/components/column.js
index 73a5bc15e..ab78414e0 100644
--- a/app/javascript/themes/glitch/features/ui/components/column.js
+++ b/app/javascript/flavours/glitch/features/ui/components/column.js
@@ -2,8 +2,8 @@ import React from 'react';
 import ColumnHeader from './column_header';
 import PropTypes from 'prop-types';
 import { debounce } from 'lodash';
-import { scrollTop } from 'themes/glitch/util/scroll';
-import { isMobile } from 'themes/glitch/util/is_mobile';
+import { scrollTop } from 'flavours/glitch/util/scroll';
+import { isMobile } from 'flavours/glitch/util/is_mobile';
 
 export default class Column extends React.PureComponent {
 
diff --git a/app/javascript/themes/glitch/features/ui/components/column_header.js b/app/javascript/flavours/glitch/features/ui/components/column_header.js
index af195ea9c..af195ea9c 100644
--- a/app/javascript/themes/glitch/features/ui/components/column_header.js
+++ b/app/javascript/flavours/glitch/features/ui/components/column_header.js
diff --git a/app/javascript/themes/glitch/features/ui/components/column_link.js b/app/javascript/flavours/glitch/features/ui/components/column_link.js
index b845d1895..b845d1895 100644
--- a/app/javascript/themes/glitch/features/ui/components/column_link.js
+++ b/app/javascript/flavours/glitch/features/ui/components/column_link.js
diff --git a/app/javascript/themes/glitch/features/ui/components/column_loading.js b/app/javascript/flavours/glitch/features/ui/components/column_loading.js
index 75f26218a..ba2d0824e 100644
--- a/app/javascript/themes/glitch/features/ui/components/column_loading.js
+++ b/app/javascript/flavours/glitch/features/ui/components/column_loading.js
@@ -1,8 +1,8 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
-import Column from 'themes/glitch/components/column';
-import ColumnHeader from 'themes/glitch/components/column_header';
+import Column from 'flavours/glitch/components/column';
+import ColumnHeader from 'flavours/glitch/components/column_header';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 export default class ColumnLoading extends ImmutablePureComponent {
diff --git a/app/javascript/themes/glitch/features/ui/components/column_subheading.js b/app/javascript/flavours/glitch/features/ui/components/column_subheading.js
index 8160c4aa3..8160c4aa3 100644
--- a/app/javascript/themes/glitch/features/ui/components/column_subheading.js
+++ b/app/javascript/flavours/glitch/features/ui/components/column_subheading.js
diff --git a/app/javascript/themes/glitch/features/ui/components/columns_area.js b/app/javascript/flavours/glitch/features/ui/components/columns_area.js
index 452950363..264f60724 100644
--- a/app/javascript/themes/glitch/features/ui/components/columns_area.js
+++ b/app/javascript/flavours/glitch/features/ui/components/columns_area.js
@@ -11,10 +11,10 @@ import BundleContainer from '../containers/bundle_container';
 import ColumnLoading from './column_loading';
 import DrawerLoading from './drawer_loading';
 import BundleColumnError from './bundle_column_error';
-import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses } from 'themes/glitch/util/async-components';
+import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses } from 'flavours/glitch/util/async-components';
 
 import detectPassiveEvents from 'detect-passive-events';
-import { scrollRight } from 'themes/glitch/util/scroll';
+import { scrollRight } from 'flavours/glitch/util/scroll';
 
 const componentMap = {
   'COMPOSE': Compose,
diff --git a/app/javascript/themes/glitch/features/ui/components/confirmation_modal.js b/app/javascript/flavours/glitch/features/ui/components/confirmation_modal.js
index 3d568aec3..d4d1e587e 100644
--- a/app/javascript/themes/glitch/features/ui/components/confirmation_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/confirmation_modal.js
@@ -1,7 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import { injectIntl, FormattedMessage } from 'react-intl';
-import Button from 'themes/glitch/components/button';
+import Button from 'flavours/glitch/components/button';
 
 @injectIntl
 export default class ConfirmationModal extends React.PureComponent {
diff --git a/app/javascript/themes/glitch/features/ui/components/doodle_modal.js b/app/javascript/flavours/glitch/features/ui/components/doodle_modal.js
index 819656dbf..9c74451b3 100644
--- a/app/javascript/themes/glitch/features/ui/components/doodle_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/doodle_modal.js
@@ -1,12 +1,12 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import Button from 'themes/glitch/components/button';
+import Button from 'flavours/glitch/components/button';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import Atrament from 'atrament'; // the doodling library
 import { connect } from 'react-redux';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import { doodleSet, uploadCompose } from 'themes/glitch/actions/compose';
-import IconButton from 'themes/glitch/components/icon_button';
+import { doodleSet, uploadCompose } from 'flavours/glitch/actions/compose';
+import IconButton from 'flavours/glitch/components/icon_button';
 import { debounce, mapValues } from 'lodash';
 import classNames from 'classnames';
 
diff --git a/app/javascript/themes/glitch/features/ui/components/drawer_loading.js b/app/javascript/flavours/glitch/features/ui/components/drawer_loading.js
index 08b0d2347..08b0d2347 100644
--- a/app/javascript/themes/glitch/features/ui/components/drawer_loading.js
+++ b/app/javascript/flavours/glitch/features/ui/components/drawer_loading.js
diff --git a/app/javascript/themes/glitch/features/ui/components/embed_modal.js b/app/javascript/flavours/glitch/features/ui/components/embed_modal.js
index 1afffb51b..1afffb51b 100644
--- a/app/javascript/themes/glitch/features/ui/components/embed_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/embed_modal.js
diff --git a/app/javascript/themes/glitch/features/ui/components/image_loader.js b/app/javascript/flavours/glitch/features/ui/components/image_loader.js
index aad594380..aad594380 100644
--- a/app/javascript/themes/glitch/features/ui/components/image_loader.js
+++ b/app/javascript/flavours/glitch/features/ui/components/image_loader.js
diff --git a/app/javascript/themes/glitch/features/ui/components/media_modal.js b/app/javascript/flavours/glitch/features/ui/components/media_modal.js
index 1dad972b2..e56147c5b 100644
--- a/app/javascript/themes/glitch/features/ui/components/media_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/media_modal.js
@@ -2,9 +2,9 @@ import React from 'react';
 import ReactSwipeableViews from 'react-swipeable-views';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
-import ExtendedVideoPlayer from 'themes/glitch/components/extended_video_player';
+import ExtendedVideoPlayer from 'flavours/glitch/components/extended_video_player';
 import { defineMessages, injectIntl } from 'react-intl';
-import IconButton from 'themes/glitch/components/icon_button';
+import IconButton from 'flavours/glitch/components/icon_button';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import ImageLoader from './image_loader';
 
diff --git a/app/javascript/themes/glitch/features/ui/components/modal_loading.js b/app/javascript/flavours/glitch/features/ui/components/modal_loading.js
index e14d20fbb..b1c322154 100644
--- a/app/javascript/themes/glitch/features/ui/components/modal_loading.js
+++ b/app/javascript/flavours/glitch/features/ui/components/modal_loading.js
@@ -1,6 +1,6 @@
 import React from 'react';
 
-import LoadingIndicator from 'themes/glitch/components/loading_indicator';
+import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
 
 // Keep the markup in sync with <BundleModalError />
 // (make sure they have the same dimensions)
diff --git a/app/javascript/themes/glitch/features/ui/components/modal_root.js b/app/javascript/flavours/glitch/features/ui/components/modal_root.js
index fbe794170..61b239283 100644
--- a/app/javascript/themes/glitch/features/ui/components/modal_root.js
+++ b/app/javascript/flavours/glitch/features/ui/components/modal_root.js
@@ -15,7 +15,7 @@ import {
   ReportModal,
   SettingsModal,
   EmbedModal,
-} from 'themes/glitch/util/async-components';
+} from 'flavours/glitch/util/async-components';
 
 const MODAL_COMPONENTS = {
   'MEDIA': () => Promise.resolve({ default: MediaModal }),
diff --git a/app/javascript/themes/glitch/features/ui/components/mute_modal.js b/app/javascript/flavours/glitch/features/ui/components/mute_modal.js
index ffccdc84d..0202b1ab1 100644
--- a/app/javascript/themes/glitch/features/ui/components/mute_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/mute_modal.js
@@ -3,10 +3,10 @@ import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
 import { injectIntl, FormattedMessage } from 'react-intl';
 import Toggle from 'react-toggle';
-import Button from 'themes/glitch/components/button';
-import { closeModal } from 'themes/glitch/actions/modal';
-import { muteAccount } from 'themes/glitch/actions/accounts';
-import { toggleHideNotifications } from 'themes/glitch/actions/mutes';
+import Button from 'flavours/glitch/components/button';
+import { closeModal } from 'flavours/glitch/actions/modal';
+import { muteAccount } from 'flavours/glitch/actions/accounts';
+import { toggleHideNotifications } from 'flavours/glitch/actions/mutes';
 
 
 const mapStateToProps = state => {
diff --git a/app/javascript/themes/glitch/features/ui/components/onboarding_modal.js b/app/javascript/flavours/glitch/features/ui/components/onboarding_modal.js
index 58875262e..21f1addea 100644
--- a/app/javascript/themes/glitch/features/ui/components/onboarding_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/onboarding_modal.js
@@ -5,16 +5,16 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import ReactSwipeableViews from 'react-swipeable-views';
 import classNames from 'classnames';
-import Permalink from 'themes/glitch/components/permalink';
-import ComposeForm from 'themes/glitch/features/compose/components/compose_form';
-import Search from 'themes/glitch/features/compose/components/search';
-import NavigationBar from 'themes/glitch/features/compose/components/navigation_bar';
+import Permalink from 'flavours/glitch/components/permalink';
+import ComposeForm from 'flavours/glitch/features/compose/components/compose_form';
+import Search from 'flavours/glitch/features/compose/components/search';
+import NavigationBar from 'flavours/glitch/features/compose/components/navigation_bar';
 import ColumnHeader from './column_header';
 import {
   List as ImmutableList,
   Map as ImmutableMap,
 } from 'immutable';
-import { me } from 'themes/glitch/util/initial_state';
+import { me } from 'flavours/glitch/util/initial_state';
 
 const noop = () => { };
 
diff --git a/app/javascript/themes/glitch/features/ui/components/report_modal.js b/app/javascript/flavours/glitch/features/ui/components/report_modal.js
index e6153948e..b4dc1e3d6 100644
--- a/app/javascript/themes/glitch/features/ui/components/report_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/report_modal.js
@@ -1,15 +1,15 @@
 import React from 'react';
 import { connect } from 'react-redux';
-import { changeReportComment, submitReport } from 'themes/glitch/actions/reports';
-import { refreshAccountTimeline } from 'themes/glitch/actions/timelines';
+import { changeReportComment, submitReport } from 'flavours/glitch/actions/reports';
+import { refreshAccountTimeline } from 'flavours/glitch/actions/timelines';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import { makeGetAccount } from 'themes/glitch/selectors';
+import { makeGetAccount } from 'flavours/glitch/selectors';
 import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
-import StatusCheckBox from 'themes/glitch/features/report/containers/status_check_box_container';
+import StatusCheckBox from 'flavours/glitch/features/report/containers/status_check_box_container';
 import { OrderedSet } from 'immutable';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import Button from 'themes/glitch/components/button';
+import Button from 'flavours/glitch/components/button';
 
 const messages = defineMessages({
   placeholder: { id: 'report.placeholder', defaultMessage: 'Additional comments' },
diff --git a/app/javascript/themes/glitch/features/ui/components/tabs_bar.js b/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js
index ef5deae99..89b455dd8 100644
--- a/app/javascript/themes/glitch/features/ui/components/tabs_bar.js
+++ b/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 import { NavLink } from 'react-router-dom';
 import { FormattedMessage, injectIntl } from 'react-intl';
 import { debounce } from 'lodash';
-import { isUserTouching } from 'themes/glitch/util/is_mobile';
+import { isUserTouching } from 'flavours/glitch/util/is_mobile';
 
 export const links = [
   <NavLink className='tabs-bar__link primary' to='/statuses/new' data-preview-title-id='tabs_bar.compose' data-preview-icon='pencil' ><i className='fa fa-fw fa-pencil' /><FormattedMessage id='tabs_bar.compose' defaultMessage='Compose' /></NavLink>,
diff --git a/app/javascript/themes/glitch/features/ui/components/upload_area.js b/app/javascript/flavours/glitch/features/ui/components/upload_area.js
index 72a450215..cc6b6d171 100644
--- a/app/javascript/themes/glitch/features/ui/components/upload_area.js
+++ b/app/javascript/flavours/glitch/features/ui/components/upload_area.js
@@ -1,6 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import Motion from 'themes/glitch/util/optional_motion';
+import Motion from 'flavours/glitch/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import { FormattedMessage } from 'react-intl';
 
diff --git a/app/javascript/themes/glitch/features/ui/components/video_modal.js b/app/javascript/flavours/glitch/features/ui/components/video_modal.js
index 91168c790..22fa998fb 100644
--- a/app/javascript/themes/glitch/features/ui/components/video_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/video_modal.js
@@ -1,7 +1,7 @@
 import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
-import Video from 'themes/glitch/features/video';
+import Video from 'flavours/glitch/features/video';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 export default class VideoModal extends ImmutablePureComponent {
diff --git a/app/javascript/themes/glitch/features/ui/containers/bundle_container.js b/app/javascript/flavours/glitch/features/ui/containers/bundle_container.js
index e6f9afcf7..c9086c9bc 100644
--- a/app/javascript/themes/glitch/features/ui/containers/bundle_container.js
+++ b/app/javascript/flavours/glitch/features/ui/containers/bundle_container.js
@@ -2,7 +2,7 @@ import { connect } from 'react-redux';
 
 import Bundle from '../components/bundle';
 
-import { fetchBundleRequest, fetchBundleSuccess, fetchBundleFail } from 'themes/glitch/actions/bundles';
+import { fetchBundleRequest, fetchBundleSuccess, fetchBundleFail } from 'flavours/glitch/actions/bundles';
 
 const mapDispatchToProps = dispatch => ({
   onFetch () {
diff --git a/app/javascript/themes/glitch/features/ui/containers/columns_area_container.js b/app/javascript/flavours/glitch/features/ui/containers/columns_area_container.js
index 95f95618b..95f95618b 100644
--- a/app/javascript/themes/glitch/features/ui/containers/columns_area_container.js
+++ b/app/javascript/flavours/glitch/features/ui/containers/columns_area_container.js
diff --git a/app/javascript/themes/glitch/features/ui/containers/loading_bar_container.js b/app/javascript/flavours/glitch/features/ui/containers/loading_bar_container.js
index 4bb90fb68..4bb90fb68 100644
--- a/app/javascript/themes/glitch/features/ui/containers/loading_bar_container.js
+++ b/app/javascript/flavours/glitch/features/ui/containers/loading_bar_container.js
diff --git a/app/javascript/themes/glitch/features/ui/containers/modal_container.js b/app/javascript/flavours/glitch/features/ui/containers/modal_container.js
index c26f19886..f074002e4 100644
--- a/app/javascript/themes/glitch/features/ui/containers/modal_container.js
+++ b/app/javascript/flavours/glitch/features/ui/containers/modal_container.js
@@ -1,5 +1,5 @@
 import { connect } from 'react-redux';
-import { closeModal } from 'themes/glitch/actions/modal';
+import { closeModal } from 'flavours/glitch/actions/modal';
 import ModalRoot from '../components/modal_root';
 
 const mapStateToProps = state => ({
diff --git a/app/javascript/themes/glitch/features/ui/containers/notifications_container.js b/app/javascript/flavours/glitch/features/ui/containers/notifications_container.js
index 5bd4017f5..88d482bcf 100644
--- a/app/javascript/themes/glitch/features/ui/containers/notifications_container.js
+++ b/app/javascript/flavours/glitch/features/ui/containers/notifications_container.js
@@ -1,7 +1,7 @@
 import { connect } from 'react-redux';
 import { NotificationStack } from 'react-notification';
-import { dismissAlert } from 'themes/glitch/actions/alerts';
-import { getAlerts } from 'themes/glitch/selectors';
+import { dismissAlert } from 'flavours/glitch/actions/alerts';
+import { getAlerts } from 'flavours/glitch/selectors';
 
 const mapStateToProps = state => ({
   notifications: getAlerts(state),
diff --git a/app/javascript/themes/glitch/features/ui/containers/status_list_container.js b/app/javascript/flavours/glitch/features/ui/containers/status_list_container.js
index 807c82e16..eca85b8e6 100644
--- a/app/javascript/themes/glitch/features/ui/containers/status_list_container.js
+++ b/app/javascript/flavours/glitch/features/ui/containers/status_list_container.js
@@ -1,10 +1,10 @@
 import { connect } from 'react-redux';
-import StatusList from 'themes/glitch/components/status_list';
-import { scrollTopTimeline } from 'themes/glitch/actions/timelines';
+import StatusList from 'flavours/glitch/components/status_list';
+import { scrollTopTimeline } from 'flavours/glitch/actions/timelines';
 import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 import { createSelector } from 'reselect';
 import { debounce } from 'lodash';
-import { me } from 'themes/glitch/util/initial_state';
+import { me } from 'flavours/glitch/util/initial_state';
 
 const makeGetStatusIds = () => createSelector([
   (state, { type }) => state.getIn(['settings', type], ImmutableMap()),
@@ -32,6 +32,10 @@ const makeGetStatusIds = () => createSelector([
       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 && statusForId.get('account') !== me) {
       const searchIndex = statusForId.get('reblog') ? statuses.getIn([statusForId.get('reblog'), 'search_index']) : statusForId.get('search_index');
       showStatus = !regex.test(searchIndex);
diff --git a/app/javascript/themes/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js
index 3eea63189..4a1982916 100644
--- a/app/javascript/themes/glitch/features/ui/index.js
+++ b/app/javascript/flavours/glitch/features/ui/index.js
@@ -6,13 +6,13 @@ import TabsBar from './components/tabs_bar';
 import ModalContainer from './containers/modal_container';
 import { connect } from 'react-redux';
 import { Redirect, withRouter } from 'react-router-dom';
-import { isMobile } from 'themes/glitch/util/is_mobile';
+import { isMobile } from 'flavours/glitch/util/is_mobile';
 import { debounce } from 'lodash';
-import { uploadCompose, resetCompose } from 'themes/glitch/actions/compose';
-import { refreshHomeTimeline } from 'themes/glitch/actions/timelines';
-import { refreshNotifications } from 'themes/glitch/actions/notifications';
-import { clearHeight } from 'themes/glitch/actions/height_cache';
-import { WrappedSwitch, WrappedRoute } from 'themes/glitch/util/react_router_helpers';
+import { uploadCompose, resetCompose } from 'flavours/glitch/actions/compose';
+import { refreshHomeTimeline } from 'flavours/glitch/actions/timelines';
+import { refreshNotifications } from 'flavours/glitch/actions/notifications';
+import { clearHeight } from 'flavours/glitch/actions/height_cache';
+import { WrappedSwitch, WrappedRoute } from 'flavours/glitch/util/react_router_helpers';
 import UploadArea from './components/upload_area';
 import ColumnsAreaContainer from './containers/columns_area_container';
 import classNames from 'classnames';
@@ -38,9 +38,9 @@ import {
   Blocks,
   Mutes,
   PinnedStatuses,
-} from 'themes/glitch/util/async-components';
+} from 'flavours/glitch/util/async-components';
 import { HotKeys } from 'react-hotkeys';
-import { me } from 'themes/glitch/util/initial_state';
+import { me } from 'flavours/glitch/util/initial_state';
 import { defineMessages, injectIntl } from 'react-intl';
 
 // Dummy import, to make sure that <Status /> ends up in the application bundle.
diff --git a/app/javascript/themes/glitch/features/video/index.js b/app/javascript/flavours/glitch/features/video/index.js
index 0ecbe37c9..97c1c27fa 100644
--- a/app/javascript/themes/glitch/features/video/index.js
+++ b/app/javascript/flavours/glitch/features/video/index.js
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import { throttle } from 'lodash';
 import classNames from 'classnames';
-import { isFullscreen, requestFullscreen, exitFullscreen } from 'themes/glitch/util/fullscreen';
+import { isFullscreen, requestFullscreen, exitFullscreen } from 'flavours/glitch/util/fullscreen';
 
 const messages = defineMessages({
   play: { id: 'video.play', defaultMessage: 'Play' },
diff --git a/app/javascript/themes/glitch/middleware/errors.js b/app/javascript/flavours/glitch/middleware/errors.js
index c54e7b0a2..f3dfc8b06 100644
--- a/app/javascript/themes/glitch/middleware/errors.js
+++ b/app/javascript/flavours/glitch/middleware/errors.js
@@ -1,4 +1,4 @@
-import { showAlert } from 'themes/glitch/actions/alerts';
+import { showAlert } from 'flavours/glitch/actions/alerts';
 
 const defaultFailSuffix = 'FAIL';
 
diff --git a/app/javascript/themes/glitch/middleware/loading_bar.js b/app/javascript/flavours/glitch/middleware/loading_bar.js
index a98f1bb2b..a98f1bb2b 100644
--- a/app/javascript/themes/glitch/middleware/loading_bar.js
+++ b/app/javascript/flavours/glitch/middleware/loading_bar.js
diff --git a/app/javascript/themes/glitch/middleware/sounds.js b/app/javascript/flavours/glitch/middleware/sounds.js
index 3d1e3eaba..3d1e3eaba 100644
--- a/app/javascript/themes/glitch/middleware/sounds.js
+++ b/app/javascript/flavours/glitch/middleware/sounds.js
diff --git a/app/javascript/flavours/glitch/packs/about.js b/app/javascript/flavours/glitch/packs/about.js
new file mode 100644
index 000000000..bc0a4887b
--- /dev/null
+++ b/app/javascript/flavours/glitch/packs/about.js
@@ -0,0 +1,22 @@
+import loadPolyfills from 'flavours/glitch/util/load_polyfills';
+
+function loaded() {
+  const TimelineContainer = require('flavours/glitch/containers/timeline_container').default;
+  const React             = require('react');
+  const ReactDOM          = require('react-dom');
+  const mountNode         = document.getElementById('mastodon-timeline');
+
+  if (mountNode !== null) {
+    const props = JSON.parse(mountNode.getAttribute('data-props'));
+    ReactDOM.render(<TimelineContainer {...props} />, mountNode);
+  }
+}
+
+function main() {
+  const ready = require('flavours/glitch/util/ready').default;
+  ready(loaded);
+}
+
+loadPolyfills().then(main).catch(error => {
+  console.error(error);
+});
diff --git a/app/javascript/flavours/glitch/packs/common.js b/app/javascript/flavours/glitch/packs/common.js
new file mode 100644
index 000000000..07445d2b3
--- /dev/null
+++ b/app/javascript/flavours/glitch/packs/common.js
@@ -0,0 +1 @@
+import 'flavours/glitch/styles/index.scss';
diff --git a/app/javascript/flavours/glitch/packs/home.js b/app/javascript/flavours/glitch/packs/home.js
new file mode 100644
index 000000000..b8f7b7d8e
--- /dev/null
+++ b/app/javascript/flavours/glitch/packs/home.js
@@ -0,0 +1,7 @@
+import loadPolyfills from 'flavours/glitch/util/load_polyfills';
+
+loadPolyfills().then(() => {
+  require('flavours/glitch/util/main').default();
+}).catch(e => {
+  console.error(e);
+});
diff --git a/app/javascript/flavours/glitch/packs/public.js b/app/javascript/flavours/glitch/packs/public.js
new file mode 100644
index 000000000..9ea82b53a
--- /dev/null
+++ b/app/javascript/flavours/glitch/packs/public.js
@@ -0,0 +1,75 @@
+import loadPolyfills from 'flavours/glitch/util/load_polyfills';
+import ready from 'flavours/glitch/util/ready';
+
+function main() {
+  const IntlRelativeFormat = require('intl-relativeformat').default;
+  const emojify = require('flavours/glitch/util/emoji').default;
+  const { getLocale } = require('locales');
+  const { localeData } = getLocale();
+  const VideoContainer = require('flavours/glitch/containers/video_container').default;
+  const MediaGalleryContainer = require('flavours/glitch/containers/media_gallery_container').default;
+  const CardContainer = require('flavours/glitch/containers/card_container').default;
+  const React = require('react');
+  const ReactDOM = require('react-dom');
+
+  localeData.forEach(IntlRelativeFormat.__addLocaleData);
+
+  ready(() => {
+    const locale = document.documentElement.lang;
+
+    const dateTimeFormat = new Intl.DateTimeFormat(locale, {
+      year: 'numeric',
+      month: 'long',
+      day: 'numeric',
+      hour: 'numeric',
+      minute: 'numeric',
+    });
+
+    const relativeFormat = new IntlRelativeFormat(locale);
+
+    [].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;
+    });
+
+    [].forEach.call(document.querySelectorAll('time.time-ago'), (content) => {
+      const datetime = new Date(content.getAttribute('datetime'));
+
+      content.title = dateTimeFormat.format(datetime);
+      content.textContent = relativeFormat.format(datetime);
+    });
+
+    [].forEach.call(document.querySelectorAll('.logo-button'), (content) => {
+      content.addEventListener('click', (e) => {
+        e.preventDefault();
+        window.open(e.target.href, 'mastodon-intent', 'width=400,height=400,resizable=no,menubar=no,status=no,scrollbars=yes');
+      });
+    });
+
+    [].forEach.call(document.querySelectorAll('[data-component="Video"]'), (content) => {
+      const props = JSON.parse(content.getAttribute('data-props'));
+      ReactDOM.render(<VideoContainer locale={locale} {...props} />, content);
+    });
+
+    [].forEach.call(document.querySelectorAll('[data-component="MediaGallery"]'), (content) => {
+      const props = JSON.parse(content.getAttribute('data-props'));
+      ReactDOM.render(<MediaGalleryContainer locale={locale} {...props} />, content);
+    });
+
+    [].forEach.call(document.querySelectorAll('[data-component="Card"]'), (content) => {
+      const props = JSON.parse(content.getAttribute('data-props'));
+      ReactDOM.render(<CardContainer locale={locale} {...props} />, content);
+    });
+  });
+}
+
+loadPolyfills().then(main).catch(error => {
+  console.error(error);
+});
diff --git a/app/javascript/flavours/glitch/packs/share.js b/app/javascript/flavours/glitch/packs/share.js
new file mode 100644
index 000000000..9f2aa2553
--- /dev/null
+++ b/app/javascript/flavours/glitch/packs/share.js
@@ -0,0 +1,22 @@
+import loadPolyfills from 'flavours/glitch/util/load_polyfills';
+
+function loaded() {
+  const ComposeContainer = require('flavours/glitch/containers/compose_container').default;
+  const React = require('react');
+  const ReactDOM = require('react-dom');
+  const mountNode = document.getElementById('mastodon-compose');
+
+  if (mountNode !== null) {
+    const props = JSON.parse(mountNode.getAttribute('data-props'));
+    ReactDOM.render(<ComposeContainer {...props} />, mountNode);
+  }
+}
+
+function main() {
+  const ready = require('flavours/glitch/util/ready').default;
+  ready(loaded);
+}
+
+loadPolyfills().then(main).catch(error => {
+  console.error(error);
+});
diff --git a/app/javascript/themes/glitch/reducers/accounts.js b/app/javascript/flavours/glitch/reducers/accounts.js
index 0a65d3723..5113a281a 100644
--- a/app/javascript/themes/glitch/reducers/accounts.js
+++ b/app/javascript/flavours/glitch/reducers/accounts.js
@@ -6,16 +6,16 @@ import {
   FOLLOWING_EXPAND_SUCCESS,
   FOLLOW_REQUESTS_FETCH_SUCCESS,
   FOLLOW_REQUESTS_EXPAND_SUCCESS,
-} from 'themes/glitch/actions/accounts';
+} from 'flavours/glitch/actions/accounts';
 import {
   BLOCKS_FETCH_SUCCESS,
   BLOCKS_EXPAND_SUCCESS,
-} from 'themes/glitch/actions/blocks';
+} from 'flavours/glitch/actions/blocks';
 import {
   MUTES_FETCH_SUCCESS,
   MUTES_EXPAND_SUCCESS,
-} from 'themes/glitch/actions/mutes';
-import { COMPOSE_SUGGESTIONS_READY } from 'themes/glitch/actions/compose';
+} from 'flavours/glitch/actions/mutes';
+import { COMPOSE_SUGGESTIONS_READY } from 'flavours/glitch/actions/compose';
 import {
   REBLOG_SUCCESS,
   UNREBLOG_SUCCESS,
@@ -23,28 +23,28 @@ import {
   UNFAVOURITE_SUCCESS,
   REBLOGS_FETCH_SUCCESS,
   FAVOURITES_FETCH_SUCCESS,
-} from 'themes/glitch/actions/interactions';
+} from 'flavours/glitch/actions/interactions';
 import {
   TIMELINE_REFRESH_SUCCESS,
   TIMELINE_UPDATE,
   TIMELINE_EXPAND_SUCCESS,
-} from 'themes/glitch/actions/timelines';
+} from 'flavours/glitch/actions/timelines';
 import {
   STATUS_FETCH_SUCCESS,
   CONTEXT_FETCH_SUCCESS,
-} from 'themes/glitch/actions/statuses';
-import { SEARCH_FETCH_SUCCESS } from 'themes/glitch/actions/search';
+} from 'flavours/glitch/actions/statuses';
+import { SEARCH_FETCH_SUCCESS } from 'flavours/glitch/actions/search';
 import {
   NOTIFICATIONS_UPDATE,
   NOTIFICATIONS_REFRESH_SUCCESS,
   NOTIFICATIONS_EXPAND_SUCCESS,
-} from 'themes/glitch/actions/notifications';
+} from 'flavours/glitch/actions/notifications';
 import {
   FAVOURITED_STATUSES_FETCH_SUCCESS,
   FAVOURITED_STATUSES_EXPAND_SUCCESS,
-} from 'themes/glitch/actions/favourites';
-import { STORE_HYDRATE } from 'themes/glitch/actions/store';
-import emojify from 'themes/glitch/util/emoji';
+} from 'flavours/glitch/actions/favourites';
+import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
+import emojify from 'flavours/glitch/util/emoji';
 import { Map as ImmutableMap, fromJS } from 'immutable';
 import escapeTextContentForBrowser from 'escape-html';
 
diff --git a/app/javascript/themes/glitch/reducers/accounts_counters.js b/app/javascript/flavours/glitch/reducers/accounts_counters.js
index e3728ecd7..a9aebd26f 100644
--- a/app/javascript/themes/glitch/reducers/accounts_counters.js
+++ b/app/javascript/flavours/glitch/reducers/accounts_counters.js
@@ -8,16 +8,16 @@ import {
   FOLLOW_REQUESTS_EXPAND_SUCCESS,
   ACCOUNT_FOLLOW_SUCCESS,
   ACCOUNT_UNFOLLOW_SUCCESS,
-} from 'themes/glitch/actions/accounts';
+} from 'flavours/glitch/actions/accounts';
 import {
   BLOCKS_FETCH_SUCCESS,
   BLOCKS_EXPAND_SUCCESS,
-} from 'themes/glitch/actions/blocks';
+} from 'flavours/glitch/actions/blocks';
 import {
   MUTES_FETCH_SUCCESS,
   MUTES_EXPAND_SUCCESS,
-} from 'themes/glitch/actions/mutes';
-import { COMPOSE_SUGGESTIONS_READY } from 'themes/glitch/actions/compose';
+} from 'flavours/glitch/actions/mutes';
+import { COMPOSE_SUGGESTIONS_READY } from 'flavours/glitch/actions/compose';
 import {
   REBLOG_SUCCESS,
   UNREBLOG_SUCCESS,
@@ -25,27 +25,27 @@ import {
   UNFAVOURITE_SUCCESS,
   REBLOGS_FETCH_SUCCESS,
   FAVOURITES_FETCH_SUCCESS,
-} from 'themes/glitch/actions/interactions';
+} from 'flavours/glitch/actions/interactions';
 import {
   TIMELINE_REFRESH_SUCCESS,
   TIMELINE_UPDATE,
   TIMELINE_EXPAND_SUCCESS,
-} from 'themes/glitch/actions/timelines';
+} from 'flavours/glitch/actions/timelines';
 import {
   STATUS_FETCH_SUCCESS,
   CONTEXT_FETCH_SUCCESS,
-} from 'themes/glitch/actions/statuses';
-import { SEARCH_FETCH_SUCCESS } from 'themes/glitch/actions/search';
+} from 'flavours/glitch/actions/statuses';
+import { SEARCH_FETCH_SUCCESS } from 'flavours/glitch/actions/search';
 import {
   NOTIFICATIONS_UPDATE,
   NOTIFICATIONS_REFRESH_SUCCESS,
   NOTIFICATIONS_EXPAND_SUCCESS,
-} from 'themes/glitch/actions/notifications';
+} from 'flavours/glitch/actions/notifications';
 import {
   FAVOURITED_STATUSES_FETCH_SUCCESS,
   FAVOURITED_STATUSES_EXPAND_SUCCESS,
-} from 'themes/glitch/actions/favourites';
-import { STORE_HYDRATE } from 'themes/glitch/actions/store';
+} from 'flavours/glitch/actions/favourites';
+import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
 import { Map as ImmutableMap, fromJS } from 'immutable';
 
 const normalizeAccount = (state, account) => state.set(account.id, fromJS({
diff --git a/app/javascript/themes/glitch/reducers/alerts.js b/app/javascript/flavours/glitch/reducers/alerts.js
index ad66b63f6..50f8d30f7 100644
--- a/app/javascript/themes/glitch/reducers/alerts.js
+++ b/app/javascript/flavours/glitch/reducers/alerts.js
@@ -2,7 +2,7 @@ import {
   ALERT_SHOW,
   ALERT_DISMISS,
   ALERT_CLEAR,
-} from 'themes/glitch/actions/alerts';
+} from 'flavours/glitch/actions/alerts';
 import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 
 const initialState = ImmutableList([]);
diff --git a/app/javascript/themes/glitch/reducers/cards.js b/app/javascript/flavours/glitch/reducers/cards.js
index 35be30444..92ecfd086 100644
--- a/app/javascript/themes/glitch/reducers/cards.js
+++ b/app/javascript/flavours/glitch/reducers/cards.js
@@ -1,4 +1,4 @@
-import { STATUS_CARD_FETCH_SUCCESS } from 'themes/glitch/actions/cards';
+import { STATUS_CARD_FETCH_SUCCESS } from 'flavours/glitch/actions/cards';
 
 import { Map as ImmutableMap, fromJS } from 'immutable';
 
diff --git a/app/javascript/themes/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js
index be359fcb4..aaa36b696 100644
--- a/app/javascript/themes/glitch/reducers/compose.js
+++ b/app/javascript/flavours/glitch/reducers/compose.js
@@ -28,12 +28,12 @@ import {
   COMPOSE_UPLOAD_CHANGE_FAIL,
   COMPOSE_DOODLE_SET,
   COMPOSE_RESET,
-} from 'themes/glitch/actions/compose';
-import { TIMELINE_DELETE } from 'themes/glitch/actions/timelines';
-import { STORE_HYDRATE } from 'themes/glitch/actions/store';
+} from 'flavours/glitch/actions/compose';
+import { TIMELINE_DELETE } from 'flavours/glitch/actions/timelines';
+import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
 import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
-import uuid from 'themes/glitch/util/uuid';
-import { me } from 'themes/glitch/util/initial_state';
+import uuid from 'flavours/glitch/util/uuid';
+import { me } from 'flavours/glitch/util/initial_state';
 
 const initialState = ImmutableMap({
   mounted: false,
diff --git a/app/javascript/themes/glitch/reducers/contexts.js b/app/javascript/flavours/glitch/reducers/contexts.js
index 56c930bd5..53e93a589 100644
--- a/app/javascript/themes/glitch/reducers/contexts.js
+++ b/app/javascript/flavours/glitch/reducers/contexts.js
@@ -1,5 +1,5 @@
-import { CONTEXT_FETCH_SUCCESS } from 'themes/glitch/actions/statuses';
-import { TIMELINE_DELETE, TIMELINE_CONTEXT_UPDATE } from 'themes/glitch/actions/timelines';
+import { CONTEXT_FETCH_SUCCESS } from 'flavours/glitch/actions/statuses';
+import { TIMELINE_DELETE, TIMELINE_CONTEXT_UPDATE } from 'flavours/glitch/actions/timelines';
 import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 
 const initialState = ImmutableMap({
diff --git a/app/javascript/themes/glitch/reducers/custom_emojis.js b/app/javascript/flavours/glitch/reducers/custom_emojis.js
index e3f1e0018..592cea8dc 100644
--- a/app/javascript/themes/glitch/reducers/custom_emojis.js
+++ b/app/javascript/flavours/glitch/reducers/custom_emojis.js
@@ -1,7 +1,7 @@
 import { List as ImmutableList } from 'immutable';
-import { STORE_HYDRATE } from 'themes/glitch/actions/store';
-import { search as emojiSearch } from 'themes/glitch/util/emoji/emoji_mart_search_light';
-import { buildCustomEmojis } from 'themes/glitch/util/emoji';
+import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
+import { search as emojiSearch } from 'flavours/glitch/util/emoji/emoji_mart_search_light';
+import { buildCustomEmojis } from 'flavours/glitch/util/emoji';
 
 const initialState = ImmutableList();
 
diff --git a/app/javascript/themes/glitch/reducers/height_cache.js b/app/javascript/flavours/glitch/reducers/height_cache.js
index 93c31b42c..8b05e0b19 100644
--- a/app/javascript/themes/glitch/reducers/height_cache.js
+++ b/app/javascript/flavours/glitch/reducers/height_cache.js
@@ -1,5 +1,5 @@
 import { Map as ImmutableMap } from 'immutable';
-import { HEIGHT_CACHE_SET, HEIGHT_CACHE_CLEAR } from 'themes/glitch/actions/height_cache';
+import { HEIGHT_CACHE_SET, HEIGHT_CACHE_CLEAR } from 'flavours/glitch/actions/height_cache';
 
 const initialState = ImmutableMap();
 
diff --git a/app/javascript/themes/glitch/reducers/index.js b/app/javascript/flavours/glitch/reducers/index.js
index aa748421a..aa748421a 100644
--- a/app/javascript/themes/glitch/reducers/index.js
+++ b/app/javascript/flavours/glitch/reducers/index.js
diff --git a/app/javascript/themes/glitch/reducers/local_settings.js b/app/javascript/flavours/glitch/reducers/local_settings.js
index b1ffa047e..69d98741b 100644
--- a/app/javascript/themes/glitch/reducers/local_settings.js
+++ b/app/javascript/flavours/glitch/reducers/local_settings.js
@@ -2,8 +2,8 @@
 import { Map as ImmutableMap } from 'immutable';
 
 //  Our imports.
-import { STORE_HYDRATE } from 'themes/glitch/actions/store';
-import { LOCAL_SETTING_CHANGE } from 'themes/glitch/actions/local_settings';
+import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
+import { LOCAL_SETTING_CHANGE } from 'flavours/glitch/actions/local_settings';
 
 const initialState = ImmutableMap({
   layout    : 'auto',
diff --git a/app/javascript/themes/glitch/reducers/media_attachments.js b/app/javascript/flavours/glitch/reducers/media_attachments.js
index 69a44639c..6e6058576 100644
--- a/app/javascript/themes/glitch/reducers/media_attachments.js
+++ b/app/javascript/flavours/glitch/reducers/media_attachments.js
@@ -1,4 +1,4 @@
-import { STORE_HYDRATE } from 'themes/glitch/actions/store';
+import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
 import { Map as ImmutableMap } from 'immutable';
 
 const initialState = ImmutableMap({
diff --git a/app/javascript/themes/glitch/reducers/meta.js b/app/javascript/flavours/glitch/reducers/meta.js
index 2249f1d78..a98dc436a 100644
--- a/app/javascript/themes/glitch/reducers/meta.js
+++ b/app/javascript/flavours/glitch/reducers/meta.js
@@ -1,4 +1,4 @@
-import { STORE_HYDRATE } from 'themes/glitch/actions/store';
+import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
 import { Map as ImmutableMap } from 'immutable';
 
 const initialState = ImmutableMap({
diff --git a/app/javascript/themes/glitch/reducers/modal.js b/app/javascript/flavours/glitch/reducers/modal.js
index 97fb31203..80bc11dda 100644
--- a/app/javascript/themes/glitch/reducers/modal.js
+++ b/app/javascript/flavours/glitch/reducers/modal.js
@@ -1,4 +1,4 @@
-import { MODAL_OPEN, MODAL_CLOSE } from 'themes/glitch/actions/modal';
+import { MODAL_OPEN, MODAL_CLOSE } from 'flavours/glitch/actions/modal';
 
 const initialState = {
   modalType: null,
diff --git a/app/javascript/themes/glitch/reducers/mutes.js b/app/javascript/flavours/glitch/reducers/mutes.js
index 8fe4ae0c3..8f52a7704 100644
--- a/app/javascript/themes/glitch/reducers/mutes.js
+++ b/app/javascript/flavours/glitch/reducers/mutes.js
@@ -3,7 +3,7 @@ import Immutable from 'immutable';
 import {
   MUTES_INIT_MODAL,
   MUTES_TOGGLE_HIDE_NOTIFICATIONS,
-} from 'themes/glitch/actions/mutes';
+} from 'flavours/glitch/actions/mutes';
 
 const initialState = Immutable.Map({
   new: Immutable.Map({
diff --git a/app/javascript/themes/glitch/reducers/notifications.js b/app/javascript/flavours/glitch/reducers/notifications.js
index c4f505053..fb2b3f549 100644
--- a/app/javascript/themes/glitch/reducers/notifications.js
+++ b/app/javascript/flavours/glitch/reducers/notifications.js
@@ -14,12 +14,12 @@ import {
   NOTIFICATIONS_DELETE_MARKED_FAIL,
   NOTIFICATIONS_ENTER_CLEARING_MODE,
   NOTIFICATIONS_MARK_ALL_FOR_DELETE,
-} from 'themes/glitch/actions/notifications';
+} from 'flavours/glitch/actions/notifications';
 import {
   ACCOUNT_BLOCK_SUCCESS,
   ACCOUNT_MUTE_SUCCESS,
-} from 'themes/glitch/actions/accounts';
-import { TIMELINE_DELETE } from 'themes/glitch/actions/timelines';
+} from 'flavours/glitch/actions/accounts';
+import { TIMELINE_DELETE } from 'flavours/glitch/actions/timelines';
 import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 
 const initialState = ImmutableMap({
diff --git a/app/javascript/themes/glitch/reducers/push_notifications.js b/app/javascript/flavours/glitch/reducers/push_notifications.js
index 744e4a0eb..f0a800d23 100644
--- a/app/javascript/themes/glitch/reducers/push_notifications.js
+++ b/app/javascript/flavours/glitch/reducers/push_notifications.js
@@ -1,5 +1,5 @@
-import { STORE_HYDRATE } from 'themes/glitch/actions/store';
-import { SET_BROWSER_SUPPORT, SET_SUBSCRIPTION, CLEAR_SUBSCRIPTION, ALERTS_CHANGE } from 'themes/glitch/actions/push_notifications';
+import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
+import { SET_BROWSER_SUPPORT, SET_SUBSCRIPTION, CLEAR_SUBSCRIPTION, ALERTS_CHANGE } from 'flavours/glitch/actions/push_notifications';
 import Immutable from 'immutable';
 
 const initialState = Immutable.Map({
diff --git a/app/javascript/themes/glitch/reducers/relationships.js b/app/javascript/flavours/glitch/reducers/relationships.js
index d9135d6da..6303089ac 100644
--- a/app/javascript/themes/glitch/reducers/relationships.js
+++ b/app/javascript/flavours/glitch/reducers/relationships.js
@@ -6,11 +6,11 @@ import {
   ACCOUNT_MUTE_SUCCESS,
   ACCOUNT_UNMUTE_SUCCESS,
   RELATIONSHIPS_FETCH_SUCCESS,
-} from 'themes/glitch/actions/accounts';
+} from 'flavours/glitch/actions/accounts';
 import {
   DOMAIN_BLOCK_SUCCESS,
   DOMAIN_UNBLOCK_SUCCESS,
-} from 'themes/glitch/actions/domain_blocks';
+} from 'flavours/glitch/actions/domain_blocks';
 import { Map as ImmutableMap, fromJS } from 'immutable';
 
 const normalizeRelationship = (state, relationship) => state.set(relationship.id, fromJS(relationship));
diff --git a/app/javascript/themes/glitch/reducers/reports.js b/app/javascript/flavours/glitch/reducers/reports.js
index b714374ea..c18fbcdc6 100644
--- a/app/javascript/themes/glitch/reducers/reports.js
+++ b/app/javascript/flavours/glitch/reducers/reports.js
@@ -6,7 +6,7 @@ import {
   REPORT_CANCEL,
   REPORT_STATUS_TOGGLE,
   REPORT_COMMENT_CHANGE,
-} from 'themes/glitch/actions/reports';
+} from 'flavours/glitch/actions/reports';
 import { Map as ImmutableMap, Set as ImmutableSet } from 'immutable';
 
 const initialState = ImmutableMap({
diff --git a/app/javascript/themes/glitch/reducers/search.js b/app/javascript/flavours/glitch/reducers/search.js
index aec9e2efb..f9bf92098 100644
--- a/app/javascript/themes/glitch/reducers/search.js
+++ b/app/javascript/flavours/glitch/reducers/search.js
@@ -3,8 +3,8 @@ import {
   SEARCH_CLEAR,
   SEARCH_FETCH_SUCCESS,
   SEARCH_SHOW,
-} from 'themes/glitch/actions/search';
-import { COMPOSE_MENTION, COMPOSE_REPLY } from 'themes/glitch/actions/compose';
+} from 'flavours/glitch/actions/search';
+import { COMPOSE_MENTION, COMPOSE_REPLY } from 'flavours/glitch/actions/compose';
 import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 
 const initialState = ImmutableMap({
diff --git a/app/javascript/themes/glitch/reducers/settings.js b/app/javascript/flavours/glitch/reducers/settings.js
index c22bbbd8d..6d5d71217 100644
--- a/app/javascript/themes/glitch/reducers/settings.js
+++ b/app/javascript/flavours/glitch/reducers/settings.js
@@ -1,9 +1,9 @@
-import { SETTING_CHANGE, SETTING_SAVE } from 'themes/glitch/actions/settings';
-import { COLUMN_ADD, COLUMN_REMOVE, COLUMN_MOVE } from 'themes/glitch/actions/columns';
-import { STORE_HYDRATE } from 'themes/glitch/actions/store';
-import { EMOJI_USE } from 'themes/glitch/actions/emojis';
+import { SETTING_CHANGE, SETTING_SAVE } from 'flavours/glitch/actions/settings';
+import { COLUMN_ADD, COLUMN_REMOVE, COLUMN_MOVE } from 'flavours/glitch/actions/columns';
+import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
+import { EMOJI_USE } from 'flavours/glitch/actions/emojis';
 import { Map as ImmutableMap, fromJS } from 'immutable';
-import uuid from 'themes/glitch/util/uuid';
+import uuid from 'flavours/glitch/util/uuid';
 
 const initialState = ImmutableMap({
   saved: true,
@@ -17,6 +17,7 @@ const initialState = ImmutableMap({
     shows: ImmutableMap({
       reblog: true,
       reply: true,
+      direct: true,
     }),
 
     regex: ImmutableMap({
diff --git a/app/javascript/themes/glitch/reducers/status_lists.js b/app/javascript/flavours/glitch/reducers/status_lists.js
index 8dc7d374e..5a3d0db0c 100644
--- a/app/javascript/themes/glitch/reducers/status_lists.js
+++ b/app/javascript/flavours/glitch/reducers/status_lists.js
@@ -1,17 +1,17 @@
 import {
   FAVOURITED_STATUSES_FETCH_SUCCESS,
   FAVOURITED_STATUSES_EXPAND_SUCCESS,
-} from 'themes/glitch/actions/favourites';
+} from 'flavours/glitch/actions/favourites';
 import {
   PINNED_STATUSES_FETCH_SUCCESS,
-} from 'themes/glitch/actions/pin_statuses';
+} from 'flavours/glitch/actions/pin_statuses';
 import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 import {
   FAVOURITE_SUCCESS,
   UNFAVOURITE_SUCCESS,
   PIN_SUCCESS,
   UNPIN_SUCCESS,
-} from 'themes/glitch/actions/interactions';
+} from 'flavours/glitch/actions/interactions';
 
 const initialState = ImmutableMap({
   favourites: ImmutableMap({
diff --git a/app/javascript/themes/glitch/reducers/statuses.js b/app/javascript/flavours/glitch/reducers/statuses.js
index ef8086865..410bc013b 100644
--- a/app/javascript/themes/glitch/reducers/statuses.js
+++ b/app/javascript/flavours/glitch/reducers/statuses.js
@@ -9,37 +9,37 @@ import {
   UNFAVOURITE_SUCCESS,
   PIN_SUCCESS,
   UNPIN_SUCCESS,
-} from 'themes/glitch/actions/interactions';
+} from 'flavours/glitch/actions/interactions';
 import {
   STATUS_FETCH_SUCCESS,
   CONTEXT_FETCH_SUCCESS,
   STATUS_MUTE_SUCCESS,
   STATUS_UNMUTE_SUCCESS,
-} from 'themes/glitch/actions/statuses';
+} from 'flavours/glitch/actions/statuses';
 import {
   TIMELINE_REFRESH_SUCCESS,
   TIMELINE_UPDATE,
   TIMELINE_DELETE,
   TIMELINE_EXPAND_SUCCESS,
-} from 'themes/glitch/actions/timelines';
+} from 'flavours/glitch/actions/timelines';
 import {
   ACCOUNT_BLOCK_SUCCESS,
   ACCOUNT_MUTE_SUCCESS,
-} from 'themes/glitch/actions/accounts';
+} from 'flavours/glitch/actions/accounts';
 import {
   NOTIFICATIONS_UPDATE,
   NOTIFICATIONS_REFRESH_SUCCESS,
   NOTIFICATIONS_EXPAND_SUCCESS,
-} from 'themes/glitch/actions/notifications';
+} from 'flavours/glitch/actions/notifications';
 import {
   FAVOURITED_STATUSES_FETCH_SUCCESS,
   FAVOURITED_STATUSES_EXPAND_SUCCESS,
-} from 'themes/glitch/actions/favourites';
+} from 'flavours/glitch/actions/favourites';
 import {
   PINNED_STATUSES_FETCH_SUCCESS,
-} from 'themes/glitch/actions/pin_statuses';
-import { SEARCH_FETCH_SUCCESS } from 'themes/glitch/actions/search';
-import emojify from 'themes/glitch/util/emoji';
+} from 'flavours/glitch/actions/pin_statuses';
+import { SEARCH_FETCH_SUCCESS } from 'flavours/glitch/actions/search';
+import emojify from 'flavours/glitch/util/emoji';
 import { Map as ImmutableMap, fromJS } from 'immutable';
 import escapeTextContentForBrowser from 'escape-html';
 
diff --git a/app/javascript/themes/glitch/reducers/timelines.js b/app/javascript/flavours/glitch/reducers/timelines.js
index 7f19a1897..679e1601e 100644
--- a/app/javascript/themes/glitch/reducers/timelines.js
+++ b/app/javascript/flavours/glitch/reducers/timelines.js
@@ -10,12 +10,12 @@ import {
   TIMELINE_SCROLL_TOP,
   TIMELINE_CONNECT,
   TIMELINE_DISCONNECT,
-} from 'themes/glitch/actions/timelines';
+} from 'flavours/glitch/actions/timelines';
 import {
   ACCOUNT_BLOCK_SUCCESS,
   ACCOUNT_MUTE_SUCCESS,
   ACCOUNT_UNFOLLOW_SUCCESS,
-} from 'themes/glitch/actions/accounts';
+} from 'flavours/glitch/actions/accounts';
 import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
 
 const initialState = ImmutableMap();
diff --git a/app/javascript/themes/glitch/reducers/user_lists.js b/app/javascript/flavours/glitch/reducers/user_lists.js
index 8c3a7d748..a4df9ec8d 100644
--- a/app/javascript/themes/glitch/reducers/user_lists.js
+++ b/app/javascript/flavours/glitch/reducers/user_lists.js
@@ -7,19 +7,19 @@ import {
   FOLLOW_REQUESTS_EXPAND_SUCCESS,
   FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
   FOLLOW_REQUEST_REJECT_SUCCESS,
-} from 'themes/glitch/actions/accounts';
+} from 'flavours/glitch/actions/accounts';
 import {
   REBLOGS_FETCH_SUCCESS,
   FAVOURITES_FETCH_SUCCESS,
-} from 'themes/glitch/actions/interactions';
+} from 'flavours/glitch/actions/interactions';
 import {
   BLOCKS_FETCH_SUCCESS,
   BLOCKS_EXPAND_SUCCESS,
-} from 'themes/glitch/actions/blocks';
+} from 'flavours/glitch/actions/blocks';
 import {
   MUTES_FETCH_SUCCESS,
   MUTES_EXPAND_SUCCESS,
-} from 'themes/glitch/actions/mutes';
+} from 'flavours/glitch/actions/mutes';
 import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 
 const initialState = ImmutableMap({
diff --git a/app/javascript/themes/glitch/selectors/index.js b/app/javascript/flavours/glitch/selectors/index.js
index d26d1b727..d26d1b727 100644
--- a/app/javascript/themes/glitch/selectors/index.js
+++ b/app/javascript/flavours/glitch/selectors/index.js
diff --git a/app/javascript/themes/glitch/service_worker/entry.js b/app/javascript/flavours/glitch/service_worker/entry.js
index eea4cfc3c..eea4cfc3c 100644
--- a/app/javascript/themes/glitch/service_worker/entry.js
+++ b/app/javascript/flavours/glitch/service_worker/entry.js
diff --git a/app/javascript/themes/glitch/service_worker/web_push_notifications.js b/app/javascript/flavours/glitch/service_worker/web_push_notifications.js
index f63cff335..f63cff335 100644
--- a/app/javascript/themes/glitch/service_worker/web_push_notifications.js
+++ b/app/javascript/flavours/glitch/service_worker/web_push_notifications.js
diff --git a/app/javascript/themes/glitch/store/configureStore.js b/app/javascript/flavours/glitch/store/configureStore.js
index 1376d4cba..1376d4cba 100644
--- a/app/javascript/themes/glitch/store/configureStore.js
+++ b/app/javascript/flavours/glitch/store/configureStore.js
diff --git a/app/javascript/themes/glitch/styles/_mixins.scss b/app/javascript/flavours/glitch/styles/_mixins.scss
index 102723e39..102723e39 100644
--- a/app/javascript/themes/glitch/styles/_mixins.scss
+++ b/app/javascript/flavours/glitch/styles/_mixins.scss
diff --git a/app/javascript/themes/glitch/styles/about.scss b/app/javascript/flavours/glitch/styles/about.scss
index 4ec689427..4ec689427 100644
--- a/app/javascript/themes/glitch/styles/about.scss
+++ b/app/javascript/flavours/glitch/styles/about.scss
diff --git a/app/javascript/themes/glitch/styles/accounts.scss b/app/javascript/flavours/glitch/styles/accounts.scss
index 2cf98c642..2cf98c642 100644
--- a/app/javascript/themes/glitch/styles/accounts.scss
+++ b/app/javascript/flavours/glitch/styles/accounts.scss
diff --git a/app/javascript/themes/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss
index 87bc710af..87bc710af 100644
--- a/app/javascript/themes/glitch/styles/admin.scss
+++ b/app/javascript/flavours/glitch/styles/admin.scss
diff --git a/app/javascript/themes/glitch/styles/basics.scss b/app/javascript/flavours/glitch/styles/basics.scss
index b5d77ff63..b5d77ff63 100644
--- a/app/javascript/themes/glitch/styles/basics.scss
+++ b/app/javascript/flavours/glitch/styles/basics.scss
diff --git a/app/javascript/themes/glitch/styles/compact_header.scss b/app/javascript/flavours/glitch/styles/compact_header.scss
index 90d98cc8c..90d98cc8c 100644
--- a/app/javascript/themes/glitch/styles/compact_header.scss
+++ b/app/javascript/flavours/glitch/styles/compact_header.scss
diff --git a/app/javascript/themes/glitch/styles/boost.scss b/app/javascript/flavours/glitch/styles/components/boost.scss
index b07b72f8e..b07b72f8e 100644
--- a/app/javascript/themes/glitch/styles/boost.scss
+++ b/app/javascript/flavours/glitch/styles/components/boost.scss
diff --git a/app/javascript/themes/glitch/styles/doodle.scss b/app/javascript/flavours/glitch/styles/components/doodle.scss
index a4a1cfc84..a4a1cfc84 100644
--- a/app/javascript/themes/glitch/styles/doodle.scss
+++ b/app/javascript/flavours/glitch/styles/components/doodle.scss
diff --git a/app/javascript/themes/glitch/styles/emoji_picker.scss b/app/javascript/flavours/glitch/styles/components/emoji_picker.scss
index 2b46d30fc..2b46d30fc 100644
--- a/app/javascript/themes/glitch/styles/emoji_picker.scss
+++ b/app/javascript/flavours/glitch/styles/components/emoji_picker.scss
diff --git a/app/javascript/themes/glitch/styles/components.scss b/app/javascript/flavours/glitch/styles/components/index.scss
index ade8df018..7aeab0a6a 100644
--- a/app/javascript/themes/glitch/styles/components.scss
+++ b/app/javascript/flavours/glitch/styles/components/index.scss
@@ -1,5 +1,3 @@
-@import 'variables';
-
 .app-body {
   -webkit-overflow-scrolling: touch;
   -ms-overflow-style: -ms-autohiding-scrollbar;
@@ -4830,3 +4828,5 @@ noscript {
 }
 
 @import 'doodle';
+@import 'emoji_picker';
+@import 'local_settings';
diff --git a/app/javascript/flavours/glitch/styles/components/local_settings.scss b/app/javascript/flavours/glitch/styles/components/local_settings.scss
new file mode 100644
index 000000000..16c8cf003
--- /dev/null
+++ b/app/javascript/flavours/glitch/styles/components/local_settings.scss
@@ -0,0 +1,81 @@
+.glitch.local-settings {
+  position: relative;
+  display: flex;
+  flex-direction: row;
+  background: $ui-secondary-color;
+  color: $ui-base-color;
+  border-radius: 8px;
+  height: 80vh;
+  width: 80vw;
+  max-width: 740px;
+  max-height: 450px;
+  overflow: hidden;
+
+  label {
+    display: block;
+  }
+
+  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;
+  }
+}
+
+.glitch.local-settings__navigation__item {
+  display: block;
+  padding: 15px 20px;
+  color: inherit;
+  background: $primary-text-color;
+  border-bottom: 1px $ui-primary-color solid;
+  cursor: pointer;
+  text-decoration: none;
+  outline: none;
+  transition: background .3s;
+
+  &: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;
+  }
+}
+
+.glitch.local-settings__navigation {
+  background: $primary-text-color;
+  color: $ui-base-color;
+  width: 200px;
+  font-size: 15px;
+  line-height: 20px;
+  overflow-y: auto;
+}
+
+.glitch.local-settings__page {
+  display: block;
+  flex: auto;
+  padding: 15px 20px 15px 20px;
+  width: 360px;
+  overflow-y: auto;
+}
+
+.glitch.local-settings__page__item {
+  select {
+    margin-bottom: 5px;
+  }
+}
diff --git a/app/javascript/themes/glitch/styles/containers.scss b/app/javascript/flavours/glitch/styles/containers.scss
index af2589e23..af2589e23 100644
--- a/app/javascript/themes/glitch/styles/containers.scss
+++ b/app/javascript/flavours/glitch/styles/containers.scss
diff --git a/app/javascript/themes/glitch/styles/footer.scss b/app/javascript/flavours/glitch/styles/footer.scss
index 2d953b34e..2d953b34e 100644
--- a/app/javascript/themes/glitch/styles/footer.scss
+++ b/app/javascript/flavours/glitch/styles/footer.scss
diff --git a/app/javascript/themes/glitch/styles/forms.scss b/app/javascript/flavours/glitch/styles/forms.scss
index 61fcf286f..61fcf286f 100644
--- a/app/javascript/themes/glitch/styles/forms.scss
+++ b/app/javascript/flavours/glitch/styles/forms.scss
diff --git a/app/javascript/themes/glitch/styles/index.scss b/app/javascript/flavours/glitch/styles/index.scss
index c962a1f62..a5169f881 100644
--- a/app/javascript/themes/glitch/styles/index.scss
+++ b/app/javascript/flavours/glitch/styles/index.scss
@@ -14,8 +14,7 @@
 @import 'forms';
 @import 'accounts';
 @import 'stream_entries';
-@import 'components';
-@import 'emoji_picker';
+@import 'components/index';
 @import 'about';
 @import 'tables';
 @import 'admin';
diff --git a/app/javascript/themes/glitch/styles/landing_strip.scss b/app/javascript/flavours/glitch/styles/landing_strip.scss
index 0bf9daafd..0bf9daafd 100644
--- a/app/javascript/themes/glitch/styles/landing_strip.scss
+++ b/app/javascript/flavours/glitch/styles/landing_strip.scss
diff --git a/app/javascript/themes/glitch/styles/lists.scss b/app/javascript/flavours/glitch/styles/lists.scss
index 6019cd800..6019cd800 100644
--- a/app/javascript/themes/glitch/styles/lists.scss
+++ b/app/javascript/flavours/glitch/styles/lists.scss
diff --git a/app/javascript/themes/glitch/styles/reset.scss b/app/javascript/flavours/glitch/styles/reset.scss
index cc5ba9d7c..cc5ba9d7c 100644
--- a/app/javascript/themes/glitch/styles/reset.scss
+++ b/app/javascript/flavours/glitch/styles/reset.scss
diff --git a/app/javascript/themes/glitch/styles/rtl.scss b/app/javascript/flavours/glitch/styles/rtl.scss
index 67bfa8a38..67bfa8a38 100644
--- a/app/javascript/themes/glitch/styles/rtl.scss
+++ b/app/javascript/flavours/glitch/styles/rtl.scss
diff --git a/app/javascript/themes/glitch/styles/stream_entries.scss b/app/javascript/flavours/glitch/styles/stream_entries.scss
index 453070b7c..453070b7c 100644
--- a/app/javascript/themes/glitch/styles/stream_entries.scss
+++ b/app/javascript/flavours/glitch/styles/stream_entries.scss
diff --git a/app/javascript/themes/glitch/styles/tables.scss b/app/javascript/flavours/glitch/styles/tables.scss
index ad46f5f9f..ad46f5f9f 100644
--- a/app/javascript/themes/glitch/styles/tables.scss
+++ b/app/javascript/flavours/glitch/styles/tables.scss
diff --git a/app/javascript/themes/glitch/styles/variables.scss b/app/javascript/flavours/glitch/styles/variables.scss
index f42d9c8c5..f42d9c8c5 100644
--- a/app/javascript/themes/glitch/styles/variables.scss
+++ b/app/javascript/flavours/glitch/styles/variables.scss
diff --git a/app/javascript/flavours/glitch/theme.yml b/app/javascript/flavours/glitch/theme.yml
new file mode 100644
index 000000000..fe09fa105
--- /dev/null
+++ b/app/javascript/flavours/glitch/theme.yml
@@ -0,0 +1,33 @@
+#  (REQUIRED) The location of the pack files.
+pack:
+  about: packs/about.js
+  admin:
+  auth:
+  common:
+    filename: packs/common.js
+    stylesheet: true
+  embed: packs/public.js
+  error:
+  home:
+    filename: packs/home.js
+    preload:
+    - flavours/glitch/async/getting_started
+    - flavours/glitch/async/compose
+    - flavours/glitch/async/home_timeline
+    - flavours/glitch/async/notifications
+  modal:
+  public: packs/public.js
+  settings:
+  share: packs/share.js
+
+#  (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/themes/glitch/util/api.js b/app/javascript/flavours/glitch/util/api.js
index ecc703c0a..ecc703c0a 100644
--- a/app/javascript/themes/glitch/util/api.js
+++ b/app/javascript/flavours/glitch/util/api.js
diff --git a/app/javascript/flavours/glitch/util/async-components.js b/app/javascript/flavours/glitch/util/async-components.js
new file mode 100644
index 000000000..b7ac4d3ec
--- /dev/null
+++ b/app/javascript/flavours/glitch/util/async-components.js
@@ -0,0 +1,115 @@
+export function EmojiPicker () {
+  return import(/* webpackChunkName: "flavours/glitch/async/emoji_picker" */'flavours/glitch/util/emoji/emoji_picker');
+}
+
+export function Compose () {
+  return import(/* webpackChunkName: "flavours/glitch/async/compose" */'flavours/glitch/features/compose');
+}
+
+export function Notifications () {
+  return import(/* webpackChunkName: "flavours/glitch/async/notifications" */'flavours/glitch/features/notifications');
+}
+
+export function HomeTimeline () {
+  return import(/* webpackChunkName: "flavours/glitch/async/home_timeline" */'flavours/glitch/features/home_timeline');
+}
+
+export function PublicTimeline () {
+  return import(/* webpackChunkName: "flavours/glitch/async/public_timeline" */'flavours/glitch/features/public_timeline');
+}
+
+export function CommunityTimeline () {
+  return import(/* webpackChunkName: "flavours/glitch/async/community_timeline" */'flavours/glitch/features/community_timeline');
+}
+
+export function HashtagTimeline () {
+  return import(/* webpackChunkName: "flavours/glitch/async/hashtag_timeline" */'flavours/glitch/features/hashtag_timeline');
+}
+
+export function DirectTimeline() {
+  return import(/* webpackChunkName: "flavours/glitch/async/direct_timeline" */'flavours/glitch/features/direct_timeline');
+}
+
+export function Status () {
+  return import(/* webpackChunkName: "flavours/glitch/async/status" */'flavours/glitch/features/status');
+}
+
+export function GettingStarted () {
+  return import(/* webpackChunkName: "flavours/glitch/async/getting_started" */'flavours/glitch/features/getting_started');
+}
+
+export function PinnedStatuses () {
+  return import(/* webpackChunkName: "flavours/glitch/async/pinned_statuses" */'flavours/glitch/features/pinned_statuses');
+}
+
+export function AccountTimeline () {
+  return import(/* webpackChunkName: "flavours/glitch/async/account_timeline" */'flavours/glitch/features/account_timeline');
+}
+
+export function AccountGallery () {
+  return import(/* webpackChunkName: "flavours/glitch/async/account_gallery" */'flavours/glitch/features/account_gallery');
+}
+
+export function Followers () {
+  return import(/* webpackChunkName: "flavours/glitch/async/followers" */'flavours/glitch/features/followers');
+}
+
+export function Following () {
+  return import(/* webpackChunkName: "flavours/glitch/async/following" */'flavours/glitch/features/following');
+}
+
+export function Reblogs () {
+  return import(/* webpackChunkName: "flavours/glitch/async/reblogs" */'flavours/glitch/features/reblogs');
+}
+
+export function Favourites () {
+  return import(/* webpackChunkName: "flavours/glitch/async/favourites" */'flavours/glitch/features/favourites');
+}
+
+export function FollowRequests () {
+  return import(/* webpackChunkName: "flavours/glitch/async/follow_requests" */'flavours/glitch/features/follow_requests');
+}
+
+export function GenericNotFound () {
+  return import(/* webpackChunkName: "flavours/glitch/async/generic_not_found" */'flavours/glitch/features/generic_not_found');
+}
+
+export function FavouritedStatuses () {
+  return import(/* webpackChunkName: "flavours/glitch/async/favourited_statuses" */'flavours/glitch/features/favourited_statuses');
+}
+
+export function Blocks () {
+  return import(/* webpackChunkName: "flavours/glitch/async/blocks" */'flavours/glitch/features/blocks');
+}
+
+export function Mutes () {
+  return import(/* webpackChunkName: "flavours/glitch/async/mutes" */'flavours/glitch/features/mutes');
+}
+
+export function OnboardingModal () {
+  return import(/* webpackChunkName: "flavours/glitch/async/onboarding_modal" */'flavours/glitch/features/ui/components/onboarding_modal');
+}
+
+export function MuteModal () {
+  return import(/* webpackChunkName: "flavours/glitch/async/mute_modal" */'flavours/glitch/features/ui/components/mute_modal');
+}
+
+export function ReportModal () {
+  return import(/* webpackChunkName: "flavours/glitch/async/report_modal" */'flavours/glitch/features/ui/components/report_modal');
+}
+
+export function SettingsModal () {
+  return import(/* webpackChunkName: "flavours/glitch/async/settings_modal" */'flavours/glitch/features/local_settings');
+}
+
+export function MediaGallery () {
+  return import(/* webpackChunkName: "flavours/glitch/async/media_gallery" */'flavours/glitch/components/media_gallery');
+}
+
+export function Video () {
+  return import(/* webpackChunkName: "flavours/glitch/async/video" */'flavours/glitch/features/video');
+}
+
+export function EmbedModal () {
+  return import(/* webpackChunkName: "flavours/glitch/async/embed_modal" */'flavours/glitch/features/ui/components/embed_modal');
+}
diff --git a/app/javascript/themes/glitch/util/base_polyfills.js b/app/javascript/flavours/glitch/util/base_polyfills.js
index 7856b26f9..7856b26f9 100644
--- a/app/javascript/themes/glitch/util/base_polyfills.js
+++ b/app/javascript/flavours/glitch/util/base_polyfills.js
diff --git a/app/javascript/themes/glitch/util/bio_metadata.js b/app/javascript/flavours/glitch/util/bio_metadata.js
index 599ec20e2..599ec20e2 100644
--- a/app/javascript/themes/glitch/util/bio_metadata.js
+++ b/app/javascript/flavours/glitch/util/bio_metadata.js
diff --git a/app/javascript/themes/glitch/util/counter.js b/app/javascript/flavours/glitch/util/counter.js
index 700ba2163..700ba2163 100644
--- a/app/javascript/themes/glitch/util/counter.js
+++ b/app/javascript/flavours/glitch/util/counter.js
diff --git a/app/javascript/themes/glitch/util/emoji/emoji_compressed.js b/app/javascript/flavours/glitch/util/emoji/emoji_compressed.js
index e5b834a74..e5b834a74 100644
--- a/app/javascript/themes/glitch/util/emoji/emoji_compressed.js
+++ b/app/javascript/flavours/glitch/util/emoji/emoji_compressed.js
diff --git a/app/javascript/themes/glitch/util/emoji/emoji_map.json b/app/javascript/flavours/glitch/util/emoji/emoji_map.json
index 13753ba84..13753ba84 100644
--- a/app/javascript/themes/glitch/util/emoji/emoji_map.json
+++ b/app/javascript/flavours/glitch/util/emoji/emoji_map.json
diff --git a/app/javascript/themes/glitch/util/emoji/emoji_mart_data_light.js b/app/javascript/flavours/glitch/util/emoji/emoji_mart_data_light.js
index 45086fc4c..45086fc4c 100644
--- a/app/javascript/themes/glitch/util/emoji/emoji_mart_data_light.js
+++ b/app/javascript/flavours/glitch/util/emoji/emoji_mart_data_light.js
diff --git a/app/javascript/themes/glitch/util/emoji/emoji_mart_search_light.js b/app/javascript/flavours/glitch/util/emoji/emoji_mart_search_light.js
index 5755bf1c4..5755bf1c4 100644
--- a/app/javascript/themes/glitch/util/emoji/emoji_mart_search_light.js
+++ b/app/javascript/flavours/glitch/util/emoji/emoji_mart_search_light.js
diff --git a/app/javascript/themes/glitch/util/emoji/emoji_picker.js b/app/javascript/flavours/glitch/util/emoji/emoji_picker.js
index 7e145381e..7e145381e 100644
--- a/app/javascript/themes/glitch/util/emoji/emoji_picker.js
+++ b/app/javascript/flavours/glitch/util/emoji/emoji_picker.js
diff --git a/app/javascript/themes/glitch/util/emoji/emoji_unicode_mapping_light.js b/app/javascript/flavours/glitch/util/emoji/emoji_unicode_mapping_light.js
index 918684c31..918684c31 100644
--- a/app/javascript/themes/glitch/util/emoji/emoji_unicode_mapping_light.js
+++ b/app/javascript/flavours/glitch/util/emoji/emoji_unicode_mapping_light.js
diff --git a/app/javascript/themes/glitch/util/emoji/emoji_utils.js b/app/javascript/flavours/glitch/util/emoji/emoji_utils.js
index dbf725c1f..dbf725c1f 100644
--- a/app/javascript/themes/glitch/util/emoji/emoji_utils.js
+++ b/app/javascript/flavours/glitch/util/emoji/emoji_utils.js
diff --git a/app/javascript/themes/glitch/util/emoji/index.js b/app/javascript/flavours/glitch/util/emoji/index.js
index 8c45a58fe..31c3e14ca 100644
--- a/app/javascript/themes/glitch/util/emoji/index.js
+++ b/app/javascript/flavours/glitch/util/emoji/index.js
@@ -1,4 +1,4 @@
-import { autoPlayGif } from 'themes/glitch/util/initial_state';
+import { autoPlayGif } from 'flavours/glitch/util/initial_state';
 import unicodeMapping from './emoji_unicode_mapping_light';
 import Trie from 'substring-trie';
 
diff --git a/app/javascript/themes/glitch/util/emoji/unicode_to_filename.js b/app/javascript/flavours/glitch/util/emoji/unicode_to_filename.js
index c75c4cd7d..c75c4cd7d 100644
--- a/app/javascript/themes/glitch/util/emoji/unicode_to_filename.js
+++ b/app/javascript/flavours/glitch/util/emoji/unicode_to_filename.js
diff --git a/app/javascript/themes/glitch/util/emoji/unicode_to_unified_name.js b/app/javascript/flavours/glitch/util/emoji/unicode_to_unified_name.js
index 808ac197e..808ac197e 100644
--- a/app/javascript/themes/glitch/util/emoji/unicode_to_unified_name.js
+++ b/app/javascript/flavours/glitch/util/emoji/unicode_to_unified_name.js
diff --git a/app/javascript/themes/glitch/util/extra_polyfills.js b/app/javascript/flavours/glitch/util/extra_polyfills.js
index 3acc55abd..3acc55abd 100644
--- a/app/javascript/themes/glitch/util/extra_polyfills.js
+++ b/app/javascript/flavours/glitch/util/extra_polyfills.js
diff --git a/app/javascript/themes/glitch/util/fullscreen.js b/app/javascript/flavours/glitch/util/fullscreen.js
index cf5d0cf98..cf5d0cf98 100644
--- a/app/javascript/themes/glitch/util/fullscreen.js
+++ b/app/javascript/flavours/glitch/util/fullscreen.js
diff --git a/app/javascript/themes/glitch/util/get_rect_from_entry.js b/app/javascript/flavours/glitch/util/get_rect_from_entry.js
index c266cd7dc..c266cd7dc 100644
--- a/app/javascript/themes/glitch/util/get_rect_from_entry.js
+++ b/app/javascript/flavours/glitch/util/get_rect_from_entry.js
diff --git a/app/javascript/themes/glitch/util/initial_state.js b/app/javascript/flavours/glitch/util/initial_state.js
index ef5d8b0ef..ef5d8b0ef 100644
--- a/app/javascript/themes/glitch/util/initial_state.js
+++ b/app/javascript/flavours/glitch/util/initial_state.js
diff --git a/app/javascript/themes/glitch/util/intersection_observer_wrapper.js b/app/javascript/flavours/glitch/util/intersection_observer_wrapper.js
index 2b24c6583..2b24c6583 100644
--- a/app/javascript/themes/glitch/util/intersection_observer_wrapper.js
+++ b/app/javascript/flavours/glitch/util/intersection_observer_wrapper.js
diff --git a/app/javascript/themes/glitch/util/is_mobile.js b/app/javascript/flavours/glitch/util/is_mobile.js
index 80e8e0a8a..80e8e0a8a 100644
--- a/app/javascript/themes/glitch/util/is_mobile.js
+++ b/app/javascript/flavours/glitch/util/is_mobile.js
diff --git a/app/javascript/themes/glitch/util/link_header.js b/app/javascript/flavours/glitch/util/link_header.js
index a3e7ccf1c..a3e7ccf1c 100644
--- a/app/javascript/themes/glitch/util/link_header.js
+++ b/app/javascript/flavours/glitch/util/link_header.js
diff --git a/app/javascript/themes/glitch/util/load_polyfills.js b/app/javascript/flavours/glitch/util/load_polyfills.js
index 8927b7358..8927b7358 100644
--- a/app/javascript/themes/glitch/util/load_polyfills.js
+++ b/app/javascript/flavours/glitch/util/load_polyfills.js
diff --git a/app/javascript/themes/glitch/util/main.js b/app/javascript/flavours/glitch/util/main.js
index c10a64ded..fe57fa962 100644
--- a/app/javascript/themes/glitch/util/main.js
+++ b/app/javascript/flavours/glitch/util/main.js
@@ -1,5 +1,5 @@
 import * as WebPushSubscription from './web_push_subscription';
-import Mastodon from 'themes/glitch/containers/mastodon';
+import Mastodon from 'flavours/glitch/containers/mastodon';
 import React from 'react';
 import ReactDOM from 'react-dom';
 import ready from './ready';
diff --git a/app/javascript/themes/glitch/util/optional_motion.js b/app/javascript/flavours/glitch/util/optional_motion.js
index b8a57b22f..eecb6634e 100644
--- a/app/javascript/themes/glitch/util/optional_motion.js
+++ b/app/javascript/flavours/glitch/util/optional_motion.js
@@ -1,4 +1,4 @@
-import { reduceMotion } from 'themes/glitch/util/initial_state';
+import { reduceMotion } from 'flavours/glitch/util/initial_state';
 import ReducedMotion from './reduced_motion';
 import Motion from 'react-motion/lib/Motion';
 
diff --git a/app/javascript/themes/glitch/util/performance.js b/app/javascript/flavours/glitch/util/performance.js
index 450a90626..450a90626 100644
--- a/app/javascript/themes/glitch/util/performance.js
+++ b/app/javascript/flavours/glitch/util/performance.js
diff --git a/app/javascript/themes/glitch/util/react_router_helpers.js b/app/javascript/flavours/glitch/util/react_router_helpers.js
index c02fb5247..1dba5e9bb 100644
--- a/app/javascript/themes/glitch/util/react_router_helpers.js
+++ b/app/javascript/flavours/glitch/util/react_router_helpers.js
@@ -2,9 +2,9 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { Switch, Route } from 'react-router-dom';
 
-import ColumnLoading from 'themes/glitch/features/ui/components/column_loading';
-import BundleColumnError from 'themes/glitch/features/ui/components/bundle_column_error';
-import BundleContainer from 'themes/glitch/features/ui/containers/bundle_container';
+import ColumnLoading from 'flavours/glitch/features/ui/components/column_loading';
+import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error';
+import BundleContainer from 'flavours/glitch/features/ui/containers/bundle_container';
 
 // Small wrapper to pass multiColumn to the route components
 export class WrappedSwitch extends React.PureComponent {
diff --git a/app/javascript/themes/glitch/util/ready.js b/app/javascript/flavours/glitch/util/ready.js
index dd543910b..dd543910b 100644
--- a/app/javascript/themes/glitch/util/ready.js
+++ b/app/javascript/flavours/glitch/util/ready.js
diff --git a/app/javascript/themes/glitch/util/reduced_motion.js b/app/javascript/flavours/glitch/util/reduced_motion.js
index 95519042b..95519042b 100644
--- a/app/javascript/themes/glitch/util/reduced_motion.js
+++ b/app/javascript/flavours/glitch/util/reduced_motion.js
diff --git a/app/javascript/themes/glitch/util/rtl.js b/app/javascript/flavours/glitch/util/rtl.js
index 00870a15d..00870a15d 100644
--- a/app/javascript/themes/glitch/util/rtl.js
+++ b/app/javascript/flavours/glitch/util/rtl.js
diff --git a/app/javascript/themes/glitch/util/schedule_idle_task.js b/app/javascript/flavours/glitch/util/schedule_idle_task.js
index b04d4a8ee..b04d4a8ee 100644
--- a/app/javascript/themes/glitch/util/schedule_idle_task.js
+++ b/app/javascript/flavours/glitch/util/schedule_idle_task.js
diff --git a/app/javascript/themes/glitch/util/scroll.js b/app/javascript/flavours/glitch/util/scroll.js
index 2af07e0fb..2af07e0fb 100644
--- a/app/javascript/themes/glitch/util/scroll.js
+++ b/app/javascript/flavours/glitch/util/scroll.js
diff --git a/app/javascript/themes/glitch/util/stream.js b/app/javascript/flavours/glitch/util/stream.js
index 36c68ffc5..36c68ffc5 100644
--- a/app/javascript/themes/glitch/util/stream.js
+++ b/app/javascript/flavours/glitch/util/stream.js
diff --git a/app/javascript/themes/glitch/util/url_regex.js b/app/javascript/flavours/glitch/util/url_regex.js
index e676d1879..e676d1879 100644
--- a/app/javascript/themes/glitch/util/url_regex.js
+++ b/app/javascript/flavours/glitch/util/url_regex.js
diff --git a/app/javascript/themes/glitch/util/uuid.js b/app/javascript/flavours/glitch/util/uuid.js
index be1899305..be1899305 100644
--- a/app/javascript/themes/glitch/util/uuid.js
+++ b/app/javascript/flavours/glitch/util/uuid.js
diff --git a/app/javascript/themes/glitch/util/web_push_subscription.js b/app/javascript/flavours/glitch/util/web_push_subscription.js
index 70b26105b..de185b6d9 100644
--- a/app/javascript/themes/glitch/util/web_push_subscription.js
+++ b/app/javascript/flavours/glitch/util/web_push_subscription.js
@@ -1,6 +1,6 @@
 import axios from 'axios';
-import { store } from 'themes/glitch/containers/mastodon';
-import { setBrowserSupport, setSubscription, clearSubscription } from 'themes/glitch/actions/push_notifications';
+import { store } from 'flavours/glitch/containers/mastodon';
+import { setBrowserSupport, setSubscription, clearSubscription } from 'flavours/glitch/actions/push_notifications';
 
 // Taken from https://www.npmjs.com/package/web-push
 const urlBase64ToUint8Array = (base64String) => {
diff --git a/app/javascript/flavours/vanilla/theme.yml b/app/javascript/flavours/vanilla/theme.yml
new file mode 100644
index 000000000..67fd9723e
--- /dev/null
+++ b/app/javascript/flavours/vanilla/theme.yml
@@ -0,0 +1,33 @@
+#  (REQUIRED) The location of the pack files inside `pack_directory`.
+pack:
+  about: about.js
+  admin:
+  auth:
+  common:
+    filename: common.js
+    stylesheet: true
+  embed: public.js
+  error:
+  home:
+    filename: application.js
+    preload:
+    - features/getting_started
+    - features/compose
+    - features/home_timeline
+    - features/notifications
+  modal:
+  public: public.js
+  settings:
+  share: share.js
+
+#  (OPTIONAL) The directory which contains the pack files.
+#  Defaults to the theme directory (`app/javascript/themes/[theme]`),
+#  but in the case of the vanilla Mastodon theme the pack files are
+#  somewhere else.
+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/fonts/premillenium/MSSansSerif.ttf b/app/javascript/fonts/premillenium/MSSansSerif.ttf
new file mode 100644
index 000000000..3afd76ff2
--- /dev/null
+++ b/app/javascript/fonts/premillenium/MSSansSerif.ttf
Binary files differdiff --git a/app/javascript/glitch/locales/en.json b/app/javascript/glitch/locales/en.json
index 69aa29108..0276cb837 100644
--- a/app/javascript/glitch/locales/en.json
+++ b/app/javascript/glitch/locales/en.json
@@ -32,6 +32,8 @@
   "status.collapse": "Collapse",
   "status.uncollapse": "Uncollapse",
 
+  "home.column_settings.show_direct": "Show DMs",
+
   "notification.markForDeletion": "Mark for deletion",
   "notifications.clear": "Clear all my notifications",
   "notifications.marked_clear_confirmation": "Are you sure you want to permanently clear all selected notifications?",
diff --git a/app/javascript/images/icon_about.png b/app/javascript/images/icon_about.png
new file mode 100644
index 000000000..08b76dcd9
--- /dev/null
+++ b/app/javascript/images/icon_about.png
Binary files differdiff --git a/app/javascript/images/icon_blocks.png b/app/javascript/images/icon_blocks.png
new file mode 100644
index 000000000..8b1490875
--- /dev/null
+++ b/app/javascript/images/icon_blocks.png
Binary files differdiff --git a/app/javascript/images/icon_home.png b/app/javascript/images/icon_home.png
new file mode 100644
index 000000000..66ce779c0
--- /dev/null
+++ b/app/javascript/images/icon_home.png
Binary files differdiff --git a/app/javascript/images/icon_likes.png b/app/javascript/images/icon_likes.png
new file mode 100644
index 000000000..17d7a9c59
--- /dev/null
+++ b/app/javascript/images/icon_likes.png
Binary files differdiff --git a/app/javascript/images/icon_local.png b/app/javascript/images/icon_local.png
new file mode 100644
index 000000000..5f82df395
--- /dev/null
+++ b/app/javascript/images/icon_local.png
Binary files differdiff --git a/app/javascript/images/icon_logout.png b/app/javascript/images/icon_logout.png
new file mode 100644
index 000000000..7ff806f58
--- /dev/null
+++ b/app/javascript/images/icon_logout.png
Binary files differdiff --git a/app/javascript/images/icon_mutes.png b/app/javascript/images/icon_mutes.png
new file mode 100644
index 000000000..c2225e966
--- /dev/null
+++ b/app/javascript/images/icon_mutes.png
Binary files differdiff --git a/app/javascript/images/icon_pin.png b/app/javascript/images/icon_pin.png
new file mode 100644
index 000000000..2329d8c54
--- /dev/null
+++ b/app/javascript/images/icon_pin.png
Binary files differdiff --git a/app/javascript/images/icon_public.png b/app/javascript/images/icon_public.png
new file mode 100644
index 000000000..3c09460db
--- /dev/null
+++ b/app/javascript/images/icon_public.png
Binary files differdiff --git a/app/javascript/images/icon_settings.png b/app/javascript/images/icon_settings.png
new file mode 100644
index 000000000..07f5c4519
--- /dev/null
+++ b/app/javascript/images/icon_settings.png
Binary files differdiff --git a/app/javascript/images/start.png b/app/javascript/images/start.png
new file mode 100644
index 000000000..7843455b6
--- /dev/null
+++ b/app/javascript/images/start.png
Binary files differdiff --git a/app/javascript/locales/index.js b/app/javascript/locales/index.js
new file mode 100644
index 000000000..421cb7fab
--- /dev/null
+++ b/app/javascript/locales/index.js
@@ -0,0 +1,9 @@
+let theLocale;
+
+export function setLocale(locale) {
+  theLocale = locale;
+}
+
+export function getLocale() {
+  return theLocale;
+}
diff --git a/app/javascript/mastodon/actions/lists.js b/app/javascript/mastodon/actions/lists.js
index 332e42166..4c8f9b186 100644
--- a/app/javascript/mastodon/actions/lists.js
+++ b/app/javascript/mastodon/actions/lists.js
@@ -4,12 +4,52 @@ 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 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(err)));
+    .catch(err => dispatch(fetchListFail(id, err)));
 };
 
 export const fetchListRequest = id => ({
@@ -22,7 +62,252 @@ export const fetchListSuccess = list => ({
   list,
 });
 
-export const fetchListFail = error => ({
+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) => (dispatch, getState) => {
+  dispatch(updateListRequest(id));
+
+  api(getState).put(`/api/v1/lists/${id}`, { title }).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(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(fetchListSuggestionsReady(q, data)));
+};
+
+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,
 });
diff --git a/app/javascript/mastodon/components/account.js b/app/javascript/mastodon/components/account.js
index 1f2d7690f..b0479db4f 100644
--- a/app/javascript/mastodon/components/account.js
+++ b/app/javascript/mastodon/components/account.js
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { Fragment } from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
 import Avatar from './avatar';
@@ -81,19 +81,19 @@ export default class Account extends ImmutablePureComponent {
         buttons = <IconButton active icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
       } else if (muting) {
         let hidingNotificationsButton;
-        if (muting.get('notifications')) {
+        if (account.getIn(['relationship', 'muting_notifications'])) {
           hidingNotificationsButton = <IconButton active icon='bell' title={intl.formatMessage(messages.unmute_notifications, { name: account.get('username') })} onClick={this.handleUnmuteNotifications} />;
         } else {
           hidingNotificationsButton = <IconButton active icon='bell-slash' title={intl.formatMessage(messages.mute_notifications, { name: account.get('username')  })} onClick={this.handleMuteNotifications} />;
         }
         buttons = (
-          <div>
+          <Fragment>
             <IconButton active icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />
             {hidingNotificationsButton}
-          </div>
+          </Fragment>
         );
-      } else {
-        buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following ? true : false} />;
+      } else if (!account.get('moved')) {
+        buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
       }
     }
 
diff --git a/app/javascript/mastodon/components/autosuggest_textarea.js b/app/javascript/mastodon/components/autosuggest_textarea.js
index 14a8d4c38..6a16e2fc7 100644
--- a/app/javascript/mastodon/components/autosuggest_textarea.js
+++ b/app/javascript/mastodon/components/autosuggest_textarea.js
@@ -209,6 +209,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
             onBlur={this.onBlur}
             onPaste={this.onPaste}
             style={style}
+            aria-autocomplete='list'
           />
         </label>
 
diff --git a/app/javascript/mastodon/features/account/components/action_bar.js b/app/javascript/mastodon/features/account/components/action_bar.js
index 389296c42..cb849fa5d 100644
--- a/app/javascript/mastodon/features/account/components/action_bar.js
+++ b/app/javascript/mastodon/features/account/components/action_bar.js
@@ -63,9 +63,8 @@ export default class ActionBar extends React.PureComponent {
     if (account.get('id') === me) {
       menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' });
     } else {
-      const following = account.getIn(['relationship', 'following']);
-      if (following) {
-        if (following.get('reblogs')) {
+      if (account.getIn(['relationship', 'following'])) {
+        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 });
diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.js b/app/javascript/mastodon/features/account_timeline/containers/header_container.js
index b41eb19d4..b5e0e9a3f 100644
--- a/app/javascript/mastodon/features/account_timeline/containers/header_container.js
+++ b/app/javascript/mastodon/features/account_timeline/containers/header_container.js
@@ -68,7 +68,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
   },
 
   onReblogToggle (account) {
-    if (account.getIn(['relationship', 'following', 'reblogs'])) {
+    if (account.getIn(['relationship', 'showing_reblogs'])) {
       dispatch(followAccount(account.get('id'), false));
     } else {
       dispatch(followAccount(account.get('id'), true));
diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js
index df4284207..4b4c02bcc 100644
--- a/app/javascript/mastodon/features/getting_started/index.js
+++ b/app/javascript/mastodon/features/getting_started/index.js
@@ -25,6 +25,7 @@ const messages = defineMessages({
   mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
   info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' },
   pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },
+  lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
   keyboard_shortcuts: { id: 'navigation_bar.keyboard_shortcuts', defaultMessage: 'Keyboard shortcuts' },
 });
 
@@ -70,6 +71,7 @@ export default class GettingStarted extends ImmutablePureComponent {
     navItems = navItems.concat([
       <ColumnLink key='4' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
       <ColumnLink key='5' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />,
+      <ColumnLink key='9' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />,
     ]);
 
     if (myAccount.get('locked')) {
@@ -79,7 +81,7 @@ export default class GettingStarted extends ImmutablePureComponent {
     navItems = navItems.concat([
       <ColumnLink key='7' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />,
       <ColumnLink key='8' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />,
-      <ColumnLink key='9' icon='question' text={intl.formatMessage(messages.keyboard_shortcuts)} to='/keyboard-shortcuts' hideOnMobile />,
+      <ColumnLink key='10' icon='question' text={intl.formatMessage(messages.keyboard_shortcuts)} to='/keyboard-shortcuts' hideOnMobile />,
     ]);
 
     return (
diff --git a/app/javascript/mastodon/features/list_editor/components/account.js b/app/javascript/mastodon/features/list_editor/components/account.js
new file mode 100644
index 000000000..c78c58e24
--- /dev/null
+++ b/app/javascript/mastodon/features/list_editor/components/account.js
@@ -0,0 +1,77 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import { makeGetAccount } from '../../../selectors';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import Avatar from '../../../components/avatar';
+import DisplayName from '../../../components/display_name';
+import IconButton from '../../../components/icon_button';
+import { defineMessages, injectIntl } from 'react-intl';
+import { removeFromListEditor, addToListEditor } from '../../../actions/lists';
+
+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)),
+});
+
+@connect(makeMapStateToProps, mapDispatchToProps)
+@injectIntl
+export default class Account extends ImmutablePureComponent {
+
+  static propTypes = {
+    account: ImmutablePropTypes.map.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>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/features/list_editor/components/search.js b/app/javascript/mastodon/features/list_editor/components/search.js
new file mode 100644
index 000000000..45c4d0f2e
--- /dev/null
+++ b/app/javascript/mastodon/features/list_editor/components/search.js
@@ -0,0 +1,75 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import { defineMessages, injectIntl } from 'react-intl';
+import { fetchListSuggestions, clearListSuggestions, changeListSuggestions } from '../../../actions/lists';
+import classNames from 'classnames';
+
+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)),
+});
+
+@connect(mapStateToProps, mapDispatchToProps)
+@injectIntl
+export default class Search extends React.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}>
+          <i className={classNames('fa fa-search', { active: !hasValue })} />
+          <i aria-label={intl.formatMessage(messages.search)} className={classNames('fa fa-times-circle', { active: hasValue })} />
+        </div>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/features/list_editor/index.js b/app/javascript/mastodon/features/list_editor/index.js
new file mode 100644
index 000000000..a3b60e447
--- /dev/null
+++ b/app/javascript/mastodon/features/list_editor/index.js
@@ -0,0 +1,80 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { injectIntl } from 'react-intl';
+import { setupListEditor, clearListSuggestions, resetListEditor } from '../../actions/lists';
+import Account from './components/account';
+import Search from './components/search';
+import Motion from '../ui/util/optional_motion';
+import spring from 'react-motion/lib/spring';
+
+const mapStateToProps = state => ({
+  title: state.getIn(['listEditor', 'title']),
+  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()),
+});
+
+@connect(mapStateToProps, mapDispatchToProps)
+@injectIntl
+export default 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,
+    title: PropTypes.string.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 { title, accountIds, searchAccountIds, onClear } = this.props;
+    const showSearch = searchAccountIds.size > 0;
+
+    return (
+      <div className='modal-root__modal list-editor'>
+        <h4>{title}</h4>
+
+        <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>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/features/list_timeline/index.js b/app/javascript/mastodon/features/list_timeline/index.js
index 71f6e36a8..1dcd4de14 100644
--- a/app/javascript/mastodon/features/list_timeline/index.js
+++ b/app/javascript/mastodon/features/list_timeline/index.js
@@ -6,10 +6,18 @@ import StatusListContainer from '../ui/containers/status_list_container';
 import Column from '../../components/column';
 import ColumnHeader from '../../components/column_header';
 import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
-import { FormattedMessage } from 'react-intl';
+import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
 import { connectListStream } from '../../actions/streaming';
 import { refreshListTimeline, expandListTimeline } from '../../actions/timelines';
-import { fetchList } from '../../actions/lists';
+import { fetchList, deleteList } from '../../actions/lists';
+import { openModal } from '../../actions/modal';
+import MissingIndicator from '../../components/missing_indicator';
+import LoadingIndicator from '../../components/loading_indicator';
+
+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' },
+});
 
 const mapStateToProps = (state, props) => ({
   list: state.getIn(['lists', props.params.id]),
@@ -17,15 +25,21 @@ const mapStateToProps = (state, props) => ({
 });
 
 @connect(mapStateToProps)
+@injectIntl
 export default class ListTimeline extends React.PureComponent {
 
+  static contextTypes = {
+    router: PropTypes.object,
+  };
+
   static propTypes = {
     params: PropTypes.object.isRequired,
     dispatch: PropTypes.func.isRequired,
     columnId: PropTypes.string,
     hasUnread: PropTypes.bool,
     multiColumn: PropTypes.bool,
-    list: ImmutablePropTypes.map,
+    list: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
+    intl: PropTypes.object.isRequired,
   };
 
   handlePin = () => {
@@ -35,6 +49,7 @@ export default class ListTimeline extends React.PureComponent {
       dispatch(removeColumn(columnId));
     } else {
       dispatch(addColumn('LIST', { id: this.props.params.id }));
+      this.context.router.history.push('/');
     }
   }
 
@@ -73,12 +88,49 @@ export default class ListTimeline extends React.PureComponent {
     this.props.dispatch(expandListTimeline(id));
   }
 
+  handleEditClick = () => {
+    this.props.dispatch(openModal('LIST_EDITOR', { listId: this.props.params.id }));
+  }
+
+  handleDeleteClick = () => {
+    const { dispatch, columnId, intl } = this.props;
+    const { id } = this.props.params;
+
+    dispatch(openModal('CONFIRM', {
+      message: intl.formatMessage(messages.deleteMessage),
+      confirm: intl.formatMessage(messages.deleteConfirm),
+      onConfirm: () => {
+        dispatch(deleteList(id));
+
+        if (!!columnId) {
+          dispatch(removeColumn(columnId));
+        } else {
+          this.context.router.history.push('/lists');
+        }
+      },
+    }));
+  }
+
   render () {
     const { hasUnread, columnId, multiColumn, list } = this.props;
     const { id } = this.props.params;
     const pinned = !!columnId;
     const title  = list ? list.get('title') : id;
 
+    if (typeof list === 'undefined') {
+      return (
+        <Column>
+          <LoadingIndicator />
+        </Column>
+      );
+    } else if (list === false) {
+      return (
+        <Column>
+          <MissingIndicator />
+        </Column>
+      );
+    }
+
     return (
       <Column ref={this.setRef}>
         <ColumnHeader
@@ -90,7 +142,19 @@ export default class ListTimeline extends React.PureComponent {
           onClick={this.handleHeaderClick}
           pinned={pinned}
           multiColumn={multiColumn}
-        />
+        >
+          <div className='column-header__links'>
+            <button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.handleEditClick}>
+              <i className='fa fa-pencil' /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' />
+            </button>
+
+            <button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.handleDeleteClick}>
+              <i className='fa fa-trash' /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' />
+            </button>
+          </div>
+
+          <hr />
+        </ColumnHeader>
 
         <StatusListContainer
           trackScroll={!pinned}
diff --git a/app/javascript/mastodon/features/lists/components/new_list_form.js b/app/javascript/mastodon/features/lists/components/new_list_form.js
new file mode 100644
index 000000000..eed6efc25
--- /dev/null
+++ b/app/javascript/mastodon/features/lists/components/new_list_form.js
@@ -0,0 +1,78 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import PropTypes from 'prop-types';
+import { changeListEditorTitle, submitListEditor } from '../../../actions/lists';
+import IconButton from '../../../components/icon_button';
+import { defineMessages, injectIntl } from 'react-intl';
+
+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)),
+});
+
+@connect(mapStateToProps, mapDispatchToProps)
+@injectIntl
+export default class NewListForm extends React.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}
+          icon='plus'
+          title={title}
+          onClick={this.handleClick}
+        />
+      </form>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/features/lists/index.js b/app/javascript/mastodon/features/lists/index.js
new file mode 100644
index 000000000..28026c434
--- /dev/null
+++ b/app/javascript/mastodon/features/lists/index.js
@@ -0,0 +1,76 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import LoadingIndicator from '../../components/loading_indicator';
+import Column from '../ui/components/column';
+import ColumnBackButtonSlim from '../../components/column_back_button_slim';
+import { fetchLists } from '../../actions/lists';
+import { defineMessages, injectIntl } from 'react-intl';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import ColumnLink from '../ui/components/column_link';
+import ColumnSubheading from '../ui/components/column_subheading';
+import NewListForm from './components/new_list_form';
+import { createSelector } from 'reselect';
+
+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),
+});
+
+@connect(mapStateToProps)
+@injectIntl
+export default class Lists extends ImmutablePureComponent {
+
+  static propTypes = {
+    params: PropTypes.object.isRequired,
+    dispatch: PropTypes.func.isRequired,
+    lists: ImmutablePropTypes.list,
+    intl: PropTypes.object.isRequired,
+  };
+
+  componentWillMount () {
+    this.props.dispatch(fetchLists());
+  }
+
+  render () {
+    const { intl, lists } = this.props;
+
+    if (!lists) {
+      return (
+        <Column>
+          <LoadingIndicator />
+        </Column>
+      );
+    }
+
+    return (
+      <Column icon='bars' heading={intl.formatMessage(messages.heading)}>
+        <ColumnBackButtonSlim />
+
+        <NewListForm />
+
+        <div className='scrollable'>
+          <ColumnSubheading text={intl.formatMessage(messages.subheading)} />
+
+          {lists.map(list =>
+            <ColumnLink key={list.get('id')} to={`/timelines/list/${list.get('id')}`} icon='bars' text={list.get('title')} />
+          )}
+        </div>
+      </Column>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/features/status/components/card.js b/app/javascript/mastodon/features/status/components/card.js
index 680bf63ab..d3e322c36 100644
--- a/app/javascript/mastodon/features/status/components/card.js
+++ b/app/javascript/mastodon/features/status/components/card.js
@@ -101,7 +101,7 @@ export default class Card extends React.PureComponent {
         onClick={this.handlePhotoClick}
         role='button'
         tabIndex='0'
-        src={card.get('url')}
+        src={card.get('embed_url')}
         alt={card.get('title')}
         width={card.get('width')}
         height={card.get('height')}
diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js
index 79d86370e..ebbff6b5a 100644
--- a/app/javascript/mastodon/features/ui/components/modal_root.js
+++ b/app/javascript/mastodon/features/ui/components/modal_root.js
@@ -13,6 +13,7 @@ import {
   MuteModal,
   ReportModal,
   EmbedModal,
+  ListEditor,
 } from '../../../features/ui/util/async-components';
 
 const MODAL_COMPONENTS = {
@@ -25,6 +26,7 @@ const MODAL_COMPONENTS = {
   'REPORT': ReportModal,
   'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
   'EMBED': EmbedModal,
+  'LIST_EDITOR': ListEditor,
 };
 
 export default class ModalRoot extends React.PureComponent {
diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js
index 361326961..5b0d7246a 100644
--- a/app/javascript/mastodon/features/ui/index.js
+++ b/app/javascript/mastodon/features/ui/index.js
@@ -38,6 +38,7 @@ import {
   Blocks,
   Mutes,
   PinnedStatuses,
+  Lists,
 } from './util/async-components';
 import { HotKeys } from 'react-hotkeys';
 import { me } from '../../initial_state';
@@ -404,6 +405,7 @@ export default class UI extends React.Component {
               <WrappedRoute path='/follow_requests' component={FollowRequests} content={children} />
               <WrappedRoute path='/blocks' component={Blocks} content={children} />
               <WrappedRoute path='/mutes' component={Mutes} content={children} />
+              <WrappedRoute path='/lists' component={Lists} content={children} />
 
               <WrappedRoute component={GenericNotFound} content={children} />
             </WrappedSwitch>
diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js
index b741f668e..d6586680b 100644
--- a/app/javascript/mastodon/features/ui/util/async-components.js
+++ b/app/javascript/mastodon/features/ui/util/async-components.js
@@ -30,6 +30,10 @@ export function ListTimeline () {
   return import(/* webpackChunkName: "features/list_timeline" */'../../list_timeline');
 }
 
+export function Lists () {
+  return import(/* webpackChunkName: "features/lists" */'../../lists');
+}
+
 export function Status () {
   return import(/* webpackChunkName: "features/status" */'../../status');
 }
@@ -113,3 +117,7 @@ export function Video () {
 export function EmbedModal () {
   return import(/* webpackChunkName: "modals/embed_modal" */'../components/embed_modal');
 }
+
+export function ListEditor () {
+  return import(/* webpackChunkName: "features/list_editor" */'../../list_editor');
+}
diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json
index 596a519bd..a984b38d2 100644
--- a/app/javascript/mastodon/locales/ar.json
+++ b/app/javascript/mastodon/locales/ar.json
@@ -7,22 +7,22 @@
   "account.followers": "المتابعون",
   "account.follows": "يتبع",
   "account.follows_you": "يتابعك",
-  "account.hide_reblogs": "Hide boosts from @{name}",
+  "account.hide_reblogs": "إخفاء ترقيات @{name}",
   "account.media": "وسائط",
   "account.mention": "أُذكُر @{name}",
-  "account.moved_to": "{name} has moved to:",
+  "account.moved_to": "{name} إنتقل إلى :",
   "account.mute": "أكتم @{name}",
-  "account.mute_notifications": "Mute notifications from @{name}",
+  "account.mute_notifications": "كتم إخطارات @{name}",
   "account.posts": "المشاركات",
   "account.report": "أبلغ عن @{name}",
   "account.requested": "في انتظار الموافقة",
   "account.share": "مشاركة @{name}'s profile",
-  "account.show_reblogs": "Show boosts from @{name}",
+  "account.show_reblogs": "عرض ترقيات @{name}",
   "account.unblock": "إلغاء الحظر عن @{name}",
   "account.unblock_domain": "فك حظر {domain}",
   "account.unfollow": "إلغاء المتابعة",
   "account.unmute": "إلغاء الكتم عن @{name}",
-  "account.unmute_notifications": "Unmute notifications from @{name}",
+  "account.unmute_notifications": "إلغاء كتم إخطارات @{name}",
   "account.view_full_profile": "عرض الملف الشخصي كاملا",
   "boost_modal.combo": "يمكنك ضغط {combo} لتخطّي هذه في المرّة القادمة",
   "bundle_column_error.body": "لقد وقع هناك خطأ أثناء عملية تحميل هذا العنصر.",
@@ -36,6 +36,7 @@
   "column.favourites": "المفضلة",
   "column.follow_requests": "طلبات المتابعة",
   "column.home": "الرئيسية",
+  "column.lists": "Lists",
   "column.mutes": "الحسابات المكتومة",
   "column.notifications": "الإشعارات",
   "column.pins": "التبويقات المثبتة",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "هل أنت متأكد أنك تريد حجب {name} ؟",
   "confirmations.delete.confirm": "حذف",
   "confirmations.delete.message": "هل أنت متأكد أنك تريد حذف هذا المنشور ؟",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "إخفاء إسم النطاق كاملا",
   "confirmations.domain_block.message": "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.",
   "confirmations.mute.confirm": "أكتم",
@@ -76,7 +79,7 @@
   "emoji_button.food": "الطعام والشراب",
   "emoji_button.label": "أدرج إيموجي",
   "emoji_button.nature": "الطبيعة",
-  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
+  "emoji_button.not_found": "لا إيموجو !! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "أشياء",
   "emoji_button.people": "الناس",
   "emoji_button.recent": "الشائعة الإستخدام",
@@ -84,13 +87,13 @@
   "emoji_button.search_results": "نتائج البحث",
   "emoji_button.symbols": "رموز",
   "emoji_button.travel": "أماكن و أسفار",
-  "empty_column.community": "الخط الزمني المحلي فارغ. اكتب شيئا ما للعامة كبداية.",
+  "empty_column.community": "الخط الزمني المحلي فارغ. أكتب شيئا ما للعامة كبداية !",
   "empty_column.hashtag": "ليس هناك بعدُ أي محتوى ذو علاقة بهذا الوسم.",
   "empty_column.home": "إنك لا تتبع بعد أي شخص إلى حد الآن. زر {public} أو استخدام حقل البحث لكي تبدأ على التعرف على مستخدمين آخرين.",
   "empty_column.home.public_timeline": "الخيط العام",
-  "empty_column.list": "There is nothing in this list yet.",
+  "empty_column.list": "هذه القائمة فارغة.",
   "empty_column.notifications": "لم تتلق أي إشعار بعدُ. تفاعل مع المستخدمين الآخرين لإنشاء محادثة.",
-  "empty_column.public": "لا يوجد شيء هنا ! قم بتحرير شيء ما بشكل عام، أو اتبع مستخدمين آخرين في الخوادم المثيلة الأخرى لملء خيط المحادثات العام.",
+  "empty_column.public": "لا يوجد أي شيء هنا ! قم بنشر شيء ما للعامة، أو إتبع مستخدمين آخرين في الخوادم المثيلة الأخرى لملء خيط المحادثات العام",
   "follow_request.authorize": "ترخيص",
   "follow_request.reject": "رفض",
   "getting_started.appsshort": "تطبيقات",
@@ -104,37 +107,46 @@
   "home.column_settings.show_reblogs": "عرض الترقيات",
   "home.column_settings.show_replies": "عرض الردود",
   "home.settings": "إعدادات العمود",
-  "keyboard_shortcuts.back": "to navigate back",
-  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.back": "للعودة",
+  "keyboard_shortcuts.boost": "للترقية",
   "keyboard_shortcuts.column": "to focus a status in one of the columns",
   "keyboard_shortcuts.compose": "to focus the compose textarea",
   "keyboard_shortcuts.description": "Description",
-  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.down": "للإنتقال إلى أسفل القائمة",
   "keyboard_shortcuts.enter": "to open status",
   "keyboard_shortcuts.favourite": "to favourite",
   "keyboard_shortcuts.heading": "Keyboard Shortcuts",
   "keyboard_shortcuts.hotkey": "Hotkey",
   "keyboard_shortcuts.legend": "to display this legend",
-  "keyboard_shortcuts.mention": "to mention author",
-  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.mention": "لذِكر الناشر",
+  "keyboard_shortcuts.reply": "للردّ",
   "keyboard_shortcuts.search": "to focus search",
-  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.toot": "لتحرير تبويق جديد",
   "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
-  "keyboard_shortcuts.up": "to move up in the list",
+  "keyboard_shortcuts.up": "للإنتقال إلى أعلى القائمة",
   "lightbox.close": "إغلاق",
   "lightbox.next": "التالي",
   "lightbox.previous": "العودة",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "تحميل ...",
   "media_gallery.toggle_visible": "عرض / إخفاء",
   "missing_indicator.label": "تعذر العثور عليه",
-  "mute_modal.hide_notifications": "Hide notifications from this user?",
+  "mute_modal.hide_notifications": "هل تود إخفاء الإخطارات القادمة من هذا المستخدم ؟",
   "navigation_bar.blocks": "الحسابات المحجوبة",
   "navigation_bar.community_timeline": "الخيط العام المحلي",
   "navigation_bar.edit_profile": "تعديل الملف الشخصي",
   "navigation_bar.favourites": "المفضلة",
   "navigation_bar.follow_requests": "طلبات المتابعة",
   "navigation_bar.info": "معلومات إضافية",
-  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.keyboard_shortcuts": "إختصارات لوحة المفاتيح",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "خروج",
   "navigation_bar.mutes": "الحسابات المكتومة",
   "navigation_bar.pins": "التبويقات المثبتة",
@@ -186,7 +198,7 @@
   "privacy.unlisted.short": "غير مدرج",
   "relative_time.days": "{number}d",
   "relative_time.hours": "{number}h",
-  "relative_time.just_now": "now",
+  "relative_time.just_now": "الآن",
   "relative_time.minutes": "{number}m",
   "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "إلغاء",
@@ -199,7 +211,7 @@
   "search_popout.tips.status": "حالة",
   "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
   "search_popout.tips.user": "مستخدِم",
-  "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
+  "search_results.total": "{count, number} {count, plural, one {result} و {results}}",
   "standalone.public_title": "نظرة على ...",
   "status.cannot_reblog": "تعذرت ترقية هذا المنشور",
   "status.delete": "إحذف",
@@ -208,7 +220,7 @@
   "status.load_more": "حمّل المزيد",
   "status.media_hidden": "الصورة مستترة",
   "status.mention": "أذكُر @{name}",
-  "status.more": "More",
+  "status.more": "المزيد",
   "status.mute_conversation": "كتم المحادثة",
   "status.open": "وسع هذه المشاركة",
   "status.pin": "تدبيس على الملف الشخصي",
@@ -229,7 +241,7 @@
   "tabs_bar.home": "الرئيسية",
   "tabs_bar.local_timeline": "المحلي",
   "tabs_bar.notifications": "الإخطارات",
-  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
+  "ui.beforeunload": "سوف تفقد مسودتك إن تركت ماستدون.",
   "upload_area.title": "إسحب ثم أفلت للرفع",
   "upload_button.label": "إضافة وسائط",
   "upload_form.description": "وصف للمعاقين بصريا",
diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json
index 82f092648..d20120b11 100644
--- a/app/javascript/mastodon/locales/bg.json
+++ b/app/javascript/mastodon/locales/bg.json
@@ -36,6 +36,7 @@
   "column.favourites": "Favourites",
   "column.follow_requests": "Follow requests",
   "column.home": "Начало",
+  "column.lists": "Lists",
   "column.mutes": "Muted users",
   "column.notifications": "Известия",
   "column.pins": "Pinned toot",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Are you sure you want to block {name}?",
   "confirmations.delete.confirm": "Delete",
   "confirmations.delete.message": "Are you sure you want to delete this status?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Hide entire domain",
   "confirmations.domain_block.message": "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.",
   "confirmations.mute.confirm": "Mute",
@@ -124,6 +127,14 @@
   "lightbox.close": "Затвори",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Зареждане...",
   "media_gallery.toggle_visible": "Toggle visibility",
   "missing_indicator.label": "Not found",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "Follow requests",
   "navigation_bar.info": "Extended information",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Излизане",
   "navigation_bar.mutes": "Muted users",
   "navigation_bar.pins": "Pinned toots",
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index baa9432de..bfa931fc0 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -7,7 +7,7 @@
   "account.followers": "Seguidors",
   "account.follows": "Seguint",
   "account.follows_you": "et segueix",
-  "account.hide_reblogs": "Hide boosts from @{name}",
+  "account.hide_reblogs": "Amaga els impulsos de @{name}",
   "account.media": "Media",
   "account.mention": "Esmentar @{name}",
   "account.moved_to": "{name} s'ha mogut a:",
@@ -17,7 +17,7 @@
   "account.report": "Informe @{name}",
   "account.requested": "Esperant aprovació. Clic per a cancel·lar la petició de seguiment",
   "account.share": "Compartir el perfil de @{name}",
-  "account.show_reblogs": "Show boosts from @{name}",
+  "account.show_reblogs": "Mostra els impulsos de @{name}",
   "account.unblock": "Desbloquejar @{name}",
   "account.unblock_domain": "Mostra {domain}",
   "account.unfollow": "Deixar de seguir",
@@ -36,6 +36,7 @@
   "column.favourites": "Favorits",
   "column.follow_requests": "Peticions per seguir-te",
   "column.home": "Inici",
+  "column.lists": "Lists",
   "column.mutes": "Usuaris silenciats",
   "column.notifications": "Notificacions",
   "column.pins": "Toot fixat",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Estàs segur que vols bloquejar {name}?",
   "confirmations.delete.confirm": "Esborrar",
   "confirmations.delete.message": "Estàs segur que vols esborrar aquest estat?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Amagar tot el domini",
   "confirmations.domain_block.message": "Estàs realment, realment segur que vols bloquejar totalment {domain}? En la majoria dels casos bloquejar o silenciar és suficient i preferible.",
   "confirmations.mute.confirm": "Silenciar",
@@ -72,7 +75,7 @@
   "embed.preview": "Aquí tenim quin aspecte tindrá:",
   "emoji_button.activity": "Activitat",
   "emoji_button.custom": "Personalitzat",
-  "emoji_button.flags": "Flags",
+  "emoji_button.flags": "Marques",
   "emoji_button.food": "Menjar i Beure",
   "emoji_button.label": "Inserir emoji",
   "emoji_button.nature": "Natura",
@@ -88,7 +91,7 @@
   "empty_column.hashtag": "Encara no hi ha res amb aquesta etiqueta.",
   "empty_column.home": "Encara no segueixes ningú. Visita {public} o fes cerca per començar i conèixer altres usuaris.",
   "empty_column.home.public_timeline": "la línia de temps pública",
-  "empty_column.list": "There is nothing in this list yet.",
+  "empty_column.list": "Encara no hi ha res en aquesta llista.",
   "empty_column.notifications": "Encara no tens notificacions. Interactua amb altres per iniciar la conversa.",
   "empty_column.public": "No hi ha res aquí! Escriu alguna cosa públicament o segueix manualment usuaris d'altres instàncies per omplir-ho",
   "follow_request.authorize": "Autoritzar",
@@ -104,26 +107,34 @@
   "home.column_settings.show_reblogs": "Mostrar impulsos",
   "home.column_settings.show_replies": "Mostrar respostes",
   "home.settings": "Ajustos de columna",
-  "keyboard_shortcuts.back": "to navigate back",
-  "keyboard_shortcuts.boost": "to boost",
-  "keyboard_shortcuts.column": "to focus a status in one of the columns",
-  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.back": "navegar enrera",
+  "keyboard_shortcuts.boost": "impulsar",
+  "keyboard_shortcuts.column": "per centrar un estat en una de les columnes",
+  "keyboard_shortcuts.compose": "per centrar l'area de composició de text",
   "keyboard_shortcuts.description": "Description",
-  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.down": "per baixar en la llista",
   "keyboard_shortcuts.enter": "to open status",
-  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.favourite": "afavorir",
   "keyboard_shortcuts.heading": "Keyboard Shortcuts",
-  "keyboard_shortcuts.hotkey": "Hotkey",
-  "keyboard_shortcuts.legend": "to display this legend",
-  "keyboard_shortcuts.mention": "to mention author",
-  "keyboard_shortcuts.reply": "to reply",
-  "keyboard_shortcuts.search": "to focus search",
-  "keyboard_shortcuts.toot": "to start a brand new toot",
-  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
-  "keyboard_shortcuts.up": "to move up in the list",
+  "keyboard_shortcuts.hotkey": "Tecla d'accés directe",
+  "keyboard_shortcuts.legend": "per a mostrar aquesta llegenda",
+  "keyboard_shortcuts.mention": "per esmentar l'autor",
+  "keyboard_shortcuts.reply": "respondre",
+  "keyboard_shortcuts.search": "per centrar la cerca",
+  "keyboard_shortcuts.toot": "per a començar un toot nou de trinca",
+  "keyboard_shortcuts.unfocus": "descentrar l'area de composició de text/cerca",
+  "keyboard_shortcuts.up": "moure amunt en la llista",
   "lightbox.close": "Tancar",
   "lightbox.next": "Següent",
   "lightbox.previous": "Anterior",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Carregant...",
   "media_gallery.toggle_visible": "Alternar visibilitat",
   "missing_indicator.label": "No trobat",
@@ -134,7 +145,8 @@
   "navigation_bar.favourites": "Favorits",
   "navigation_bar.follow_requests": "Sol·licituds de seguiment",
   "navigation_bar.info": "Informació addicional",
-  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.keyboard_shortcuts": "Dreceres de teclat",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Tancar sessió",
   "navigation_bar.mutes": "Usuaris silenciats",
   "navigation_bar.pins": "Toots fixats",
@@ -195,7 +207,7 @@
   "report.target": "Informes",
   "search.placeholder": "Cercar",
   "search_popout.search_format": "Format de cerca avançada",
-  "search_popout.tips.hashtag": "hashtag",
+  "search_popout.tips.hashtag": "etiqueta",
   "search_popout.tips.status": "status",
   "search_popout.tips.text": "El text simple retorna coincidències amb els noms de visualització, els noms d'usuari i els hashtags",
   "search_popout.tips.user": "usuari",
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index 343528899..b6d9e27a7 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -7,22 +7,22 @@
   "account.followers": "Folgende",
   "account.follows": "Folgt",
   "account.follows_you": "Folgt dir",
-  "account.hide_reblogs": "Hide boosts from @{name}",
+  "account.hide_reblogs": "Geteilte Beiträge von @{name} verbergen",
   "account.media": "Medien",
   "account.mention": "@{name} erwähnen",
-  "account.moved_to": "{name} has moved to:",
+  "account.moved_to": "{name} ist umgezogen auf:",
   "account.mute": "@{name} stummschalten",
-  "account.mute_notifications": "Mute notifications from @{name}",
+  "account.mute_notifications": "Benachrichtigungen von @{name} verbergen",
   "account.posts": "Beiträge",
   "account.report": "@{name} melden",
   "account.requested": "Warte auf Erlaubnis. Klicke zum Abbrechen",
   "account.share": "Profil von @{name} teilen",
-  "account.show_reblogs": "Show boosts from @{name}",
+  "account.show_reblogs": "Von @{name} geteilte Beiträge anzeigen",
   "account.unblock": "@{name} entblocken",
   "account.unblock_domain": "{domain} wieder anzeigen",
   "account.unfollow": "Entfolgen",
   "account.unmute": "@{name} nicht mehr stummschalten",
-  "account.unmute_notifications": "Unmute notifications from @{name}",
+  "account.unmute_notifications": "Benachrichtigungen von @{name} einschalten",
   "account.view_full_profile": "Vollständiges Profil anzeigen",
   "boost_modal.combo": "Du kannst {combo} drücken, um dies beim nächsten Mal zu überspringen",
   "bundle_column_error.body": "Etwas ist beim Laden schiefgelaufen.",
@@ -36,6 +36,7 @@
   "column.favourites": "Favoriten",
   "column.follow_requests": "Folgeanfragen",
   "column.home": "Startseite",
+  "column.lists": "Lists",
   "column.mutes": "Stummgeschaltete Profile",
   "column.notifications": "Mitteilungen",
   "column.pins": "Angeheftete Beiträge",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Bist du dir sicher, dass du {name} blockieren möchtest?",
   "confirmations.delete.confirm": "Löschen",
   "confirmations.delete.message": "Bist du dir sicher, dass du diesen Beitrag löschen möchtest?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Die ganze Domain verbergen",
   "confirmations.domain_block.message": "Bist du dir wirklich sicher, dass du die ganze Domain {domain} verbergen willst? In den meisten Fällen reichen ein paar gezielte Blocks aus.",
   "confirmations.mute.confirm": "Stummschalten",
@@ -80,7 +83,7 @@
   "emoji_button.objects": "Gegenstände",
   "emoji_button.people": "Personen",
   "emoji_button.recent": "Häufig benutzt",
-  "emoji_button.search": "Suchen",
+  "emoji_button.search": "Suchen…",
   "emoji_button.search_results": "Suchergebnisse",
   "emoji_button.symbols": "Symbole",
   "emoji_button.travel": "Reisen und Orte",
@@ -88,7 +91,7 @@
   "empty_column.hashtag": "Unter diesem Hashtag gibt es noch nichts.",
   "empty_column.home": "Deine Startseite ist leer! Besuche {public} oder nutze die Suche, um loszulegen und andere Leute zu finden.",
   "empty_column.home.public_timeline": "die öffentliche Zeitleiste",
-  "empty_column.list": "There is nothing in this list yet.",
+  "empty_column.list": "Diese Liste ist derzeit leer.",
   "empty_column.notifications": "Du hast noch keine Mitteilungen. Interagiere mit anderen, um ins Gespräch zu kommen.",
   "empty_column.public": "Hier ist nichts zu sehen! Schreibe etwas öffentlich oder folge Profilen von anderen Instanzen, um die Zeitleiste aufzufüllen",
   "follow_request.authorize": "Erlauben",
@@ -104,37 +107,46 @@
   "home.column_settings.show_reblogs": "Geteilte Beiträge anzeigen",
   "home.column_settings.show_replies": "Antworten anzeigen",
   "home.settings": "Spalteneinstellungen",
-  "keyboard_shortcuts.back": "to navigate back",
-  "keyboard_shortcuts.boost": "to boost",
-  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.back": "zurück navigieren",
+  "keyboard_shortcuts.boost": "boosten",
+  "keyboard_shortcuts.column": "einen Status in einer der Spalten fokussieren",
   "keyboard_shortcuts.compose": "to focus the compose textarea",
   "keyboard_shortcuts.description": "Description",
-  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.down": "sich in der Liste hinunter bewegen",
   "keyboard_shortcuts.enter": "to open status",
-  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.favourite": "favorisieren",
   "keyboard_shortcuts.heading": "Keyboard Shortcuts",
   "keyboard_shortcuts.hotkey": "Hotkey",
-  "keyboard_shortcuts.legend": "to display this legend",
-  "keyboard_shortcuts.mention": "to mention author",
-  "keyboard_shortcuts.reply": "to reply",
-  "keyboard_shortcuts.search": "to focus search",
-  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.legend": "diese Übersicht anzeigen",
+  "keyboard_shortcuts.mention": "Autor_in erwähnen",
+  "keyboard_shortcuts.reply": "antworten",
+  "keyboard_shortcuts.search": "die Suche fokussieren",
+  "keyboard_shortcuts.toot": "einen neuen Toot beginnen",
   "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
-  "keyboard_shortcuts.up": "to move up in the list",
+  "keyboard_shortcuts.up": "sich in der Liste hinauf bewegen",
   "lightbox.close": "Schließen",
   "lightbox.next": "Weiter",
   "lightbox.previous": "Zurück",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Wird geladen …",
   "media_gallery.toggle_visible": "Sichtbarkeit umschalten",
   "missing_indicator.label": "Nicht gefunden",
-  "mute_modal.hide_notifications": "Hide notifications from this user?",
+  "mute_modal.hide_notifications": "Benachrichtigungen von diesem Account verbergen?",
   "navigation_bar.blocks": "Blockierte Profile",
   "navigation_bar.community_timeline": "Lokale Zeitleiste",
   "navigation_bar.edit_profile": "Profil bearbeiten",
   "navigation_bar.favourites": "Favoriten",
   "navigation_bar.follow_requests": "Folgeanfragen",
   "navigation_bar.info": "Über diese Instanz",
-  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.keyboard_shortcuts": "Tastenkombinationen",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Abmelden",
   "navigation_bar.mutes": "Stummgeschaltete Profile",
   "navigation_bar.pins": "Angeheftete Beiträge",
@@ -169,7 +181,7 @@
   "onboarding.page_six.apps_available": "Es gibt verschiedene {apps} für iOS, Android und weitere Plattformen.",
   "onboarding.page_six.github": "Mastodon ist freie, quelloffene Software. Du kannst auf {github} dazu beitragen, Probleme melden und Wünsche äußern.",
   "onboarding.page_six.guidelines": "Richtlinien",
-  "onboarding.page_six.read_guidelines": "Bitte mach dich mit den {guidelines} von {domain} vertraut.",
+  "onboarding.page_six.read_guidelines": "Bitte mach dich mit den {guidelines} von {domain} vertraut!",
   "onboarding.page_six.various_app": "Apps",
   "onboarding.page_three.profile": "Bearbeite dein Profil, um dein Bild, deinen Namen und deine Beschreibung anzupassen. Dort findest du auch weitere Einstellungen.",
   "onboarding.page_three.search": "Benutze die Suchfunktion, um Leute zu finden und mit Hashtags wie {illustration} oder {introductions} nach Beiträgen zu suchen. Um eine Person zu finden, die auf einer anderen Instanz ist, benutze den vollständigen Profilnamen.",
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index be751589e..bb82cf5f5 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -858,6 +858,10 @@
         "id": "navigation_bar.pins"
       },
       {
+        "defaultMessage": "Lists",
+        "id": "navigation_bar.lists"
+      },
+      {
         "defaultMessage": "Keyboard shortcuts",
         "id": "navigation_bar.keyboard_shortcuts"
       },
@@ -1011,6 +1015,44 @@
   {
     "descriptors": [
       {
+        "defaultMessage": "Remove from list",
+        "id": "lists.account.remove"
+      },
+      {
+        "defaultMessage": "Add to list",
+        "id": "lists.account.add"
+      }
+    ],
+    "path": "app/javascript/mastodon/features/list_editor/components/account.json"
+  },
+  {
+    "descriptors": [
+      {
+        "defaultMessage": "Search among people you follow",
+        "id": "lists.search"
+      }
+    ],
+    "path": "app/javascript/mastodon/features/list_editor/components/search.json"
+  },
+  {
+    "descriptors": [
+      {
+        "defaultMessage": "Are you sure you want to permanently delete this list?",
+        "id": "confirmations.delete_list.message"
+      },
+      {
+        "defaultMessage": "Delete",
+        "id": "confirmations.delete_list.confirm"
+      },
+      {
+        "defaultMessage": "Edit list",
+        "id": "lists.edit"
+      },
+      {
+        "defaultMessage": "Delete list",
+        "id": "lists.delete"
+      },
+      {
         "defaultMessage": "There is nothing in this list yet.",
         "id": "empty_column.list"
       }
@@ -1020,6 +1062,32 @@
   {
     "descriptors": [
       {
+        "defaultMessage": "New list title",
+        "id": "lists.new.title_placeholder"
+      },
+      {
+        "defaultMessage": "Add list",
+        "id": "lists.new.create"
+      }
+    ],
+    "path": "app/javascript/mastodon/features/lists/components/new_list_form.json"
+  },
+  {
+    "descriptors": [
+      {
+        "defaultMessage": "Lists",
+        "id": "column.lists"
+      },
+      {
+        "defaultMessage": "Your lists",
+        "id": "lists.subheading"
+      }
+    ],
+    "path": "app/javascript/mastodon/features/lists/index.json"
+  },
+  {
+    "descriptors": [
+      {
         "defaultMessage": "Muted users",
         "id": "column.mutes"
       }
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 4e0b838b0..538124904 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -36,6 +36,7 @@
   "column.favourites": "Favourites",
   "column.follow_requests": "Follow requests",
   "column.home": "Home",
+  "column.lists": "Lists",
   "column.mutes": "Muted users",
   "column.notifications": "Notifications",
   "column.pins": "Pinned toots",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Are you sure you want to block {name}?",
   "confirmations.delete.confirm": "Delete",
   "confirmations.delete.message": "Are you sure you want to delete this status?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Hide entire domain",
   "confirmations.domain_block.message": "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.",
   "confirmations.mute.confirm": "Mute",
@@ -124,6 +127,14 @@
   "lightbox.close": "Close",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Loading...",
   "media_gallery.toggle_visible": "Toggle visibility",
   "missing_indicator.label": "Not found",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "Follow requests",
   "navigation_bar.info": "About this instance",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Logout",
   "navigation_bar.mutes": "Muted users",
   "navigation_bar.pins": "Pinned toots",
diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json
index f9e32ba5e..619c7320a 100644
--- a/app/javascript/mastodon/locales/eo.json
+++ b/app/javascript/mastodon/locales/eo.json
@@ -36,6 +36,7 @@
   "column.favourites": "Favoritoj",
   "column.follow_requests": "Abonpetoj",
   "column.home": "Hejmo",
+  "column.lists": "Lists",
   "column.mutes": "Silentigitaj uzantoj",
   "column.notifications": "Sciigoj",
   "column.pins": "Alpinglitaj pepoj",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Ĉu vi konfirmas la blokadon de {name}?",
   "confirmations.delete.confirm": "Malaperigi",
   "confirmations.delete.message": "Ĉu vi konfirmas la malaperigon de tiun pepon?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Kaŝi la tutan reton",
   "confirmations.domain_block.message": "Ĉu vi vere, vere certas, ke vi volas bloki {domain} tute? Plej ofte, kelkaj celitaj blokadoj aŭ silentigoj estas sufiĉaj kaj preferindaj.",
   "confirmations.mute.confirm": "Silentigi",
@@ -124,6 +127,14 @@
   "lightbox.close": "Fermi",
   "lightbox.next": "Malantaŭa",
   "lightbox.previous": "Antaŭa",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Ŝarganta…",
   "media_gallery.toggle_visible": "Baskuli videblecon",
   "missing_indicator.label": "Ne trovita",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "Abonpetoj",
   "navigation_bar.info": "Plia informo",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Elsaluti",
   "navigation_bar.mutes": "Silentigitaj uzantoj",
   "navigation_bar.pins": "Alpinglitaj pepoj",
diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json
index 38ba291e4..411615744 100644
--- a/app/javascript/mastodon/locales/es.json
+++ b/app/javascript/mastodon/locales/es.json
@@ -7,22 +7,22 @@
   "account.followers": "Seguidores",
   "account.follows": "Sigue",
   "account.follows_you": "Te sigue",
-  "account.hide_reblogs": "Hide boosts from @{name}",
+  "account.hide_reblogs": "Ocultar retoots de @{name}",
   "account.media": "Media",
   "account.mention": "Mencionar a @{name}",
-  "account.moved_to": "{name} has moved to:",
+  "account.moved_to": "{name} se ha mudado a:",
   "account.mute": "Silenciar a @{name}",
-  "account.mute_notifications": "Mute notifications from @{name}",
+  "account.mute_notifications": "Silenciar notificaciones de @{name}",
   "account.posts": "Publicaciones",
   "account.report": "Reportar a @{name}",
   "account.requested": "Esperando aprobación",
   "account.share": "Compartir el perfil de @{name}",
-  "account.show_reblogs": "Show boosts from @{name}",
+  "account.show_reblogs": "Mostrar retoots de @{name}",
   "account.unblock": "Desbloquear a @{name}",
   "account.unblock_domain": "Mostrar a {domain}",
   "account.unfollow": "Dejar de seguir",
   "account.unmute": "Dejar de silenciar a @{name}",
-  "account.unmute_notifications": "Unmute notifications from @{name}",
+  "account.unmute_notifications": "Dejar de silenciar las notificaciones de @{name}",
   "account.view_full_profile": "Ver perfil completo",
   "boost_modal.combo": "Puedes presionar {combo} para saltear este aviso la próxima vez",
   "bundle_column_error.body": "Algo salió mal al cargar este componente.",
@@ -36,6 +36,7 @@
   "column.favourites": "Favoritos",
   "column.follow_requests": "Solicitudes de seguimiento",
   "column.home": "Inicio",
+  "column.lists": "Lists",
   "column.mutes": "Usuarios silenciados",
   "column.notifications": "Notificaciones",
   "column.pins": "Toot fijado",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "¿Estás seguro de que quieres bloquear a {name}?",
   "confirmations.delete.confirm": "Eliminar",
   "confirmations.delete.message": "¿Estás seguro de que quieres borrar este toot?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Ocultar dominio entero",
   "confirmations.domain_block.message": "¿Seguro de que quieres bloquear al dominio entero? En algunos casos es preferible bloquear o silenciar objetivos determinados.",
   "confirmations.mute.confirm": "Silenciar",
@@ -71,26 +74,26 @@
   "embed.instructions": "Añade este toot a tu sitio web con el siguiente código.",
   "embed.preview": "Así es como se verá:",
   "emoji_button.activity": "Actividad",
-  "emoji_button.custom": "Custom",
+  "emoji_button.custom": "Personalizado",
   "emoji_button.flags": "Marcas",
   "emoji_button.food": "Comida y bebida",
   "emoji_button.label": "Insertar emoji",
   "emoji_button.nature": "Naturaleza",
-  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
+  "emoji_button.not_found": "No hay emojos!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Objetos",
   "emoji_button.people": "Gente",
-  "emoji_button.recent": "Frequently used",
+  "emoji_button.recent": "Usados frecuentemente",
   "emoji_button.search": "Buscar…",
-  "emoji_button.search_results": "Search results",
+  "emoji_button.search_results": "Resultados de búsqueda",
   "emoji_button.symbols": "Símbolos",
   "emoji_button.travel": "Viajes y lugares",
   "empty_column.community": "La línea de tiempo local está vacía. ¡Escribe algo para empezar la fiesta!",
   "empty_column.hashtag": "No hay nada en este hashtag aún.",
   "empty_column.home": "No estás siguiendo a nadie aún. Visita {public} o haz búsquedas para empezar y conocer gente nueva.",
   "empty_column.home.public_timeline": "la línea de tiempo pública",
-  "empty_column.list": "There is nothing in this list yet.",
+  "empty_column.list": "No hay nada en esta lista aún.",
   "empty_column.notifications": "No tienes ninguna notificación aún. Interactúa con otros para empezar una conversación.",
-  "empty_column.public": "¡No hay nada aquí! Escribe algo públicamente, o sigue usuarios de otras instancias manualmente para llenarlo.",
+  "empty_column.public": "¡No hay nada aquí! Escribe algo públicamente, o sigue usuarios de otras instancias manualmente para llenarlo",
   "follow_request.authorize": "Autorizar",
   "follow_request.reject": "Rechazar",
   "getting_started.appsshort": "Aplicaciones",
@@ -104,37 +107,46 @@
   "home.column_settings.show_reblogs": "Mostrar retoots",
   "home.column_settings.show_replies": "Mostrar respuestas",
   "home.settings": "Ajustes de columna",
-  "keyboard_shortcuts.back": "to navigate back",
-  "keyboard_shortcuts.boost": "to boost",
-  "keyboard_shortcuts.column": "to focus a status in one of the columns",
-  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.back": "volver atrás",
+  "keyboard_shortcuts.boost": "retootear",
+  "keyboard_shortcuts.column": "enfocar un estado en una de las columnas",
+  "keyboard_shortcuts.compose": "enfocar el área de texto de redacción",
   "keyboard_shortcuts.description": "Description",
-  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.down": "mover hacia abajo en la lista",
   "keyboard_shortcuts.enter": "to open status",
-  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.favourite": "añadir a favoritos",
   "keyboard_shortcuts.heading": "Keyboard Shortcuts",
-  "keyboard_shortcuts.hotkey": "Hotkey",
-  "keyboard_shortcuts.legend": "to display this legend",
-  "keyboard_shortcuts.mention": "to mention author",
-  "keyboard_shortcuts.reply": "to reply",
-  "keyboard_shortcuts.search": "to focus search",
-  "keyboard_shortcuts.toot": "to start a brand new toot",
-  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
-  "keyboard_shortcuts.up": "to move up in the list",
+  "keyboard_shortcuts.hotkey": "Tecla caliente",
+  "keyboard_shortcuts.legend": "para mostrar esta leyenda",
+  "keyboard_shortcuts.mention": "para mencionar al autor",
+  "keyboard_shortcuts.reply": "para responder",
+  "keyboard_shortcuts.search": "para poner el foco en la búsqueda",
+  "keyboard_shortcuts.toot": "para comenzar un nuevo toot",
+  "keyboard_shortcuts.unfocus": "para retirar el foco de la caja de redacción/búsqueda",
+  "keyboard_shortcuts.up": "para ir hacia arriba en la lista",
   "lightbox.close": "Cerrar",
   "lightbox.next": "Siguiente",
   "lightbox.previous": "Anterior",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Cargando…",
   "media_gallery.toggle_visible": "Cambiar visibilidad",
   "missing_indicator.label": "No encontrado",
-  "mute_modal.hide_notifications": "Hide notifications from this user?",
+  "mute_modal.hide_notifications": "Ocultar notificaciones de este usuario?",
   "navigation_bar.blocks": "Usuarios bloqueados",
   "navigation_bar.community_timeline": "Historia local",
   "navigation_bar.edit_profile": "Editar perfil",
   "navigation_bar.favourites": "Favoritos",
   "navigation_bar.follow_requests": "Solicitudes para seguirte",
   "navigation_bar.info": "Información adicional",
-  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.keyboard_shortcuts": "Atajos de teclado",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Cerrar sesión",
   "navigation_bar.mutes": "Usuarios silenciados",
   "navigation_bar.pins": "Toots fijados",
@@ -150,8 +162,8 @@
   "notifications.column_settings.favourite": "Favoritos:",
   "notifications.column_settings.follow": "Nuevos seguidores:",
   "notifications.column_settings.mention": "Menciones:",
-  "notifications.column_settings.push": "Notificaciones push:",
-  "notifications.column_settings.push_meta": "Este dispositivo:",
+  "notifications.column_settings.push": "Notificaciones push",
+  "notifications.column_settings.push_meta": "Este dispositivo",
   "notifications.column_settings.reblog": "Retoots:",
   "notifications.column_settings.show": "Mostrar en columna",
   "notifications.column_settings.sound": "Reproducir sonido",
@@ -194,11 +206,11 @@
   "report.submit": "Publicar",
   "report.target": "Reportando",
   "search.placeholder": "Buscar",
-  "search_popout.search_format": "Advanced search format",
-  "search_popout.tips.hashtag": "hashtag",
+  "search_popout.search_format": "Formato de búsqueda avanzada",
+  "search_popout.tips.hashtag": "etiqueta",
   "search_popout.tips.status": "status",
-  "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
-  "search_popout.tips.user": "user",
+  "search_popout.tips.text": "El texto simple devuelve correspondencias de nombre, usuario y hashtag",
+  "search_popout.tips.user": "usuario",
   "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}",
   "standalone.public_title": "Un pequeño vistazo...",
   "status.cannot_reblog": "Este toot no puede retootearse",
@@ -229,10 +241,10 @@
   "tabs_bar.home": "Inicio",
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notificaciones",
-  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
+  "ui.beforeunload": "Tu borrador se perderá si sales de Mastodon.",
   "upload_area.title": "Arrastra y suelta para subir",
   "upload_button.label": "Subir multimedia",
-  "upload_form.description": "Describe for the visually impaired",
+  "upload_form.description": "Describir para los usuarios con dificultad visual",
   "upload_form.undo": "Deshacer",
   "upload_progress.label": "Subiendo…",
   "video.close": "Cerrar video",
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index dafb52bb5..aa5c21feb 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -36,6 +36,7 @@
   "column.favourites": "پسندیده‌ها",
   "column.follow_requests": "درخواست‌های پیگیری",
   "column.home": "خانه",
+  "column.lists": "Lists",
   "column.mutes": "کاربران بی‌صداشده",
   "column.notifications": "اعلان‌ها",
   "column.pins": "نوشته‌های ثابت",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "آیا واقعاً می‌خواهید {name} را مسدود کنید؟",
   "confirmations.delete.confirm": "پاک کن",
   "confirmations.delete.message": "آیا واقعاً می‌خواهید این نوشته را پاک کنید؟",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "پنهان‌سازی کل دامین",
   "confirmations.domain_block.message": "آیا جدی جدی می‌خواهید کل دامین {domain} را مسدود کنید؟ بیشتر وقت‌ها مسدودکردن یا بی‌صداکردن چند حساب کاربری خاص کافی است و توصیه می‌شود.",
   "confirmations.mute.confirm": "بی‌صدا کن",
@@ -124,6 +127,14 @@
   "lightbox.close": "بستن",
   "lightbox.next": "بعدی",
   "lightbox.previous": "قبلی",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "بارگیری...",
   "media_gallery.toggle_visible": "تغییر پیدایی",
   "missing_indicator.label": "پیدا نشد",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "درخواست‌های پیگیری",
   "navigation_bar.info": "اطلاعات تکمیلی",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "خروج",
   "navigation_bar.mutes": "کاربران بی‌صداشده",
   "navigation_bar.pins": "نوشته‌های ثابت",
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index e3739eb68..db9319e2e 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -36,6 +36,7 @@
   "column.favourites": "Favourites",
   "column.follow_requests": "Follow requests",
   "column.home": "Koti",
+  "column.lists": "Lists",
   "column.mutes": "Muted users",
   "column.notifications": "Ilmoitukset",
   "column.pins": "Pinned toot",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Are you sure you want to block {name}?",
   "confirmations.delete.confirm": "Delete",
   "confirmations.delete.message": "Are you sure you want to delete this status?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Hide entire domain",
   "confirmations.domain_block.message": "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.",
   "confirmations.mute.confirm": "Mute",
@@ -124,6 +127,14 @@
   "lightbox.close": "Sulje",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Ladataan...",
   "media_gallery.toggle_visible": "Toggle visibility",
   "missing_indicator.label": "Not found",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "Follow requests",
   "navigation_bar.info": "Extended information",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Kirjaudu ulos",
   "navigation_bar.mutes": "Muted users",
   "navigation_bar.pins": "Pinned toots",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index e4cabf52c..9b9469bc2 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -7,22 +7,22 @@
   "account.followers": "Abonné⋅e⋅s",
   "account.follows": "Abonnements",
   "account.follows_you": "Vous suit",
-  "account.hide_reblogs": "Hide boosts from @{name}",
+  "account.hide_reblogs": "Masquer les partages de @{name}",
   "account.media": "Média",
   "account.mention": "Mentionner",
-  "account.moved_to": "{name} has moved to:",
+  "account.moved_to": "{name} a déménagé vers :",
   "account.mute": "Masquer",
-  "account.mute_notifications": "Mute notifications from @{name}",
+  "account.mute_notifications": "Ignorer les notifications de @{name}",
   "account.posts": "Statuts",
   "account.report": "Signaler",
   "account.requested": "Invitation envoyée",
   "account.share": "Partager le profil de @{name}",
-  "account.show_reblogs": "Show boosts from @{name}",
+  "account.show_reblogs": "Afficher les partages de @{name}",
   "account.unblock": "Débloquer",
   "account.unblock_domain": "Ne plus masquer {domain}",
   "account.unfollow": "Ne plus suivre",
   "account.unmute": "Ne plus masquer",
-  "account.unmute_notifications": "Unmute notifications from @{name}",
+  "account.unmute_notifications": "Réactiver les notifications de @{name}",
   "account.view_full_profile": "Afficher le profil complet",
   "boost_modal.combo": "Vous pouvez appuyer sur {combo} pour pouvoir passer ceci, la prochaine fois",
   "bundle_column_error.body": "Une erreur s’est produite lors du chargement de ce composant.",
@@ -36,6 +36,7 @@
   "column.favourites": "Favoris",
   "column.follow_requests": "Demandes de suivi",
   "column.home": "Accueil",
+  "column.lists": "Lists",
   "column.mutes": "Comptes masqués",
   "column.notifications": "Notifications",
   "column.pins": "Pouets épinglés",
@@ -52,7 +53,7 @@
   "compose_form.lock_disclaimer": "Votre compte n’est pas {locked}. Tout le monde peut vous suivre et voir vos pouets privés.",
   "compose_form.lock_disclaimer.lock": "verrouillé",
   "compose_form.placeholder": "Qu’avez-vous en tête ?",
-  "compose_form.publish": "Pouet ",
+  "compose_form.publish": "Pouet",
   "compose_form.publish_loud": "{publish} !",
   "compose_form.sensitive": "Marquer le média comme sensible",
   "compose_form.spoiler": "Masquer le texte derrière un avertissement",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Confirmez-vous le blocage de {name} ?",
   "confirmations.delete.confirm": "Supprimer",
   "confirmations.delete.message": "Confirmez-vous la suppression de ce pouet ?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Masquer le domaine entier",
   "confirmations.domain_block.message": "Êtes-vous vraiment, vraiment sûr⋅e de vouloir bloquer {domain} en entier ? Dans la plupart des cas, quelques blocages ou masquages ciblés sont suffisants et préférables.",
   "confirmations.mute.confirm": "Masquer",
@@ -69,14 +72,14 @@
   "confirmations.unfollow.confirm": "Ne plus suivre",
   "confirmations.unfollow.message": "Voulez-vous arrêter de suivre {name} ?",
   "embed.instructions": "Intégrez ce statut à votre site en copiant le code ci-dessous.",
-  "embed.preview": "Il apparaîtra comme cela : ",
+  "embed.preview": "Il apparaîtra comme cela :",
   "emoji_button.activity": "Activités",
   "emoji_button.custom": "Personnalisés",
   "emoji_button.flags": "Drapeaux",
   "emoji_button.food": "Boire et manger",
   "emoji_button.label": "Insérer un émoji",
   "emoji_button.nature": "Nature",
-  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
+  "emoji_button.not_found": "Pas d'emojis !! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Objets",
   "emoji_button.people": "Personnages",
   "emoji_button.recent": "Fréquemment utilisés",
@@ -88,7 +91,7 @@
   "empty_column.hashtag": "Il n’y a encore aucun contenu associé à ce hashtag",
   "empty_column.home": "Vous ne suivez encore personne. Visitez {public} ou bien utilisez la recherche pour vous connecter à d’autres utilisateur⋅ice⋅s.",
   "empty_column.home.public_timeline": "le fil public",
-  "empty_column.list": "There is nothing in this list yet.",
+  "empty_column.list": "Il n'y a rien dans cette liste pour l'instant.",
   "empty_column.notifications": "Vous n’avez pas encore de notification. Interagissez avec d’autres utilisateur⋅ice⋅s pour débuter la conversation.",
   "empty_column.public": "Il n’y a rien ici ! Écrivez quelque chose publiquement, ou bien suivez manuellement des utilisateur⋅ice⋅s d’autres instances pour remplir le fil public.",
   "follow_request.authorize": "Accepter",
@@ -104,12 +107,12 @@
   "home.column_settings.show_reblogs": "Afficher les partages",
   "home.column_settings.show_replies": "Afficher les réponses",
   "home.settings": "Paramètres de la colonne",
-  "keyboard_shortcuts.back": "to navigate back",
-  "keyboard_shortcuts.boost": "to boost",
-  "keyboard_shortcuts.column": "to focus a status in one of the columns",
-  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.back": "revenir en arrière",
+  "keyboard_shortcuts.boost": "partager",
+  "keyboard_shortcuts.column": "focaliser un statut dans l'une des colonnes",
+  "keyboard_shortcuts.compose": "pour centrer la zone de redaction",
   "keyboard_shortcuts.description": "Description",
-  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.down": "descendre dans la liste",
   "keyboard_shortcuts.enter": "to open status",
   "keyboard_shortcuts.favourite": "to favourite",
   "keyboard_shortcuts.heading": "Keyboard Shortcuts",
@@ -124,10 +127,18 @@
   "lightbox.close": "Fermer",
   "lightbox.next": "Suivant",
   "lightbox.previous": "Précédent",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Chargement…",
   "media_gallery.toggle_visible": "Modifier la visibilité",
   "missing_indicator.label": "Non trouvé",
-  "mute_modal.hide_notifications": "Hide notifications from this user?",
+  "mute_modal.hide_notifications": "Masquer les notifications de cet utilisateur ?",
   "navigation_bar.blocks": "Comptes bloqués",
   "navigation_bar.community_timeline": "Fil public local",
   "navigation_bar.edit_profile": "Modifier le profil",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "Demandes de suivi",
   "navigation_bar.info": "Plus d’informations",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Déconnexion",
   "navigation_bar.mutes": "Comptes masqués",
   "navigation_bar.pins": "Pouets épinglés",
@@ -147,7 +159,7 @@
   "notifications.clear": "Nettoyer",
   "notifications.clear_confirmation": "Voulez-vous vraiment supprimer toutes vos notifications ?",
   "notifications.column_settings.alert": "Notifications locales",
-  "notifications.column_settings.favourite": "Favoris :",
+  "notifications.column_settings.favourite": "Favoris :",
   "notifications.column_settings.follow": "Nouveaux⋅elles abonné⋅e⋅s :",
   "notifications.column_settings.mention": "Mentions :",
   "notifications.column_settings.push": "Notifications push",
diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json
index fdebdf92a..ec1e30dd5 100644
--- a/app/javascript/mastodon/locales/he.json
+++ b/app/javascript/mastodon/locales/he.json
@@ -1,52 +1,53 @@
 {
   "account.block": "חסימת @{name}",
   "account.block_domain": "להסתיר הכל מהקהילה {domain}",
-  "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
+  "account.disclaimer_full": "המידע להלן עשוי להיות לא עדכני או לא שלם.",
   "account.edit_profile": "עריכת פרופיל",
   "account.follow": "מעקב",
   "account.followers": "עוקבים",
   "account.follows": "נעקבים",
   "account.follows_you": "במעקב אחריך",
-  "account.hide_reblogs": "Hide boosts from @{name}",
+  "account.hide_reblogs": "להסתיר הידהודים מאת @{name}",
   "account.media": "מדיה",
   "account.mention": "אזכור של @{name}",
-  "account.moved_to": "{name} has moved to:",
+  "account.moved_to": "החשבון {name} הועבר אל:",
   "account.mute": "להשתיק את @{name}",
-  "account.mute_notifications": "Mute notifications from @{name}",
+  "account.mute_notifications": "להסתיר התראות מאת @{name}",
   "account.posts": "הודעות",
   "account.report": "לדווח על @{name}",
   "account.requested": "בהמתנה לאישור",
-  "account.share": "Share @{name}'s profile",
-  "account.show_reblogs": "Show boosts from @{name}",
+  "account.share": "לשתף את אודות @{name}",
+  "account.show_reblogs": "להראות הדהודים מאת @{name}",
   "account.unblock": "הסרת חסימה מעל @{name}",
   "account.unblock_domain": "הסר חסימה מקהילת {domain}",
   "account.unfollow": "הפסקת מעקב",
   "account.unmute": "הפסקת השתקת @{name}",
-  "account.unmute_notifications": "Unmute notifications from @{name}",
-  "account.view_full_profile": "View full profile",
+  "account.unmute_notifications": "להפסיק הסתרת הודעות מעם @{name}",
+  "account.view_full_profile": "הראה אודות מלאות",
   "boost_modal.combo": "ניתן להקיש {combo} כדי לדלג בפעם הבאה",
-  "bundle_column_error.body": "Something went wrong while loading this component.",
-  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.body": "משהו השתבש בעת הצגת הרכיב הזה.",
+  "bundle_column_error.retry": "לנסות שוב",
   "bundle_column_error.title": "Network error",
-  "bundle_modal_error.close": "Close",
-  "bundle_modal_error.message": "Something went wrong while loading this component.",
-  "bundle_modal_error.retry": "Try again",
+  "bundle_modal_error.close": "לסגור",
+  "bundle_modal_error.message": "משהו השתבש בעת טעינת הרכיב הזה.",
+  "bundle_modal_error.retry": "לנסות שוב",
   "column.blocks": "חסימות",
   "column.community": "ציר זמן מקומי",
   "column.favourites": "חיבובים",
   "column.follow_requests": "בקשות מעקב",
   "column.home": "בבית",
+  "column.lists": "Lists",
   "column.mutes": "השתקות",
   "column.notifications": "התראות",
   "column.pins": "Pinned toot",
   "column.public": "בפרהסיה",
   "column_back_button.label": "חזרה",
-  "column_header.hide_settings": "Hide settings",
-  "column_header.moveLeft_settings": "Move column to the left",
-  "column_header.moveRight_settings": "Move column to the right",
-  "column_header.pin": "Pin",
-  "column_header.show_settings": "Show settings",
-  "column_header.unpin": "Unpin",
+  "column_header.hide_settings": "הסתרת העדפות",
+  "column_header.moveLeft_settings": "הזחת טור לשמאל",
+  "column_header.moveRight_settings": "הזחת טור לימין",
+  "column_header.pin": "קיבוע",
+  "column_header.show_settings": "הצגת העדפות",
+  "column_header.unpin": "שחרור קיבוע",
   "column_subheading.navigation": "ניווט",
   "column_subheading.settings": "אפשרויות",
   "compose_form.lock_disclaimer": "חשבונך אינו {locked}. כל אחד יוכל לעקוב אחריך כדי לקרוא את הודעותיך המיועדות לעוקבים בלבד.",
@@ -62,35 +63,37 @@
   "confirmations.block.message": "לחסום את {name}?",
   "confirmations.delete.confirm": "למחוק",
   "confirmations.delete.message": "למחוק את ההודעה?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "הסתר קהילה שלמה",
   "confirmations.domain_block.message": "באמת באמת לחסום את כל קהילת {domain}? ברב המקרים השתקות נבחרות של מספר משתמשים מסויימים צריכה להספיק.",
   "confirmations.mute.confirm": "להשתיק",
   "confirmations.mute.message": "להשתיק את {name}?",
-  "confirmations.unfollow.confirm": "Unfollow",
-  "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
-  "embed.instructions": "Embed this status on your website by copying the code below.",
-  "embed.preview": "Here is what it will look like:",
+  "confirmations.unfollow.confirm": "להפסיק מעקב",
+  "confirmations.unfollow.message": "להפסיק מעקב אחרי {name}?",
+  "embed.instructions": "ניתן להטמיע את ההודעה באתרך ע\"י העתקת הקוד שלהלן.",
+  "embed.preview": "דוגמא כיצד זה יראה:",
   "emoji_button.activity": "פעילות",
-  "emoji_button.custom": "Custom",
+  "emoji_button.custom": "מיוחדים",
   "emoji_button.flags": "דגלים",
   "emoji_button.food": "אוכל ושתיה",
   "emoji_button.label": "הוספת אמוג'י",
   "emoji_button.nature": "טבע",
-  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
+  "emoji_button.not_found": "רגישון לא נמצא!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "חפצים",
   "emoji_button.people": "אנשים",
-  "emoji_button.recent": "Frequently used",
+  "emoji_button.recent": "בשימוש תדיר",
   "emoji_button.search": "חיפוש...",
-  "emoji_button.search_results": "Search results",
+  "emoji_button.search_results": "תוצאות חיפוש",
   "emoji_button.symbols": "סמלים",
   "emoji_button.travel": "טיולים ואתרים",
   "empty_column.community": "טור הסביבה ריק. יש לפרסם משהו כדי שדברים יתרחילו להתגלגל!",
   "empty_column.hashtag": "אין כלום בהאשתג הזה עדיין.",
   "empty_column.home": "אף אחד לא במעקב עדיין. אפשר לבקר ב{public} או להשתמש בחיפוש כדי להתחיל ולהכיר חצוצרנים אחרים.",
   "empty_column.home.public_timeline": "ציר זמן בין-קהילתי",
-  "empty_column.list": "There is nothing in this list yet.",
-  "empty_column.notifications": "אין התראות עדיין. יאללה, הגיע הזמן להתחיל להתערבב!",
-  "empty_column.public": "אין פה כלום! כדי למלא את הטור הזה אפשר לכתוב משהו, או להתחיל לעקוב אחרי אנשים מקהילות אחרות.",
+  "empty_column.list": "אין עדיין מאום ברשימה.",
+  "empty_column.notifications": "אין התראות עדיין. יאללה, הגיע הזמן להתחיל להתערבב.",
+  "empty_column.public": "אין פה כלום! כדי למלא את הטור הזה אפשר לכתוב משהו, או להתחיל לעקוב אחרי אנשים מקהילות אחרות",
   "follow_request.authorize": "קבלה",
   "follow_request.reject": "דחיה",
   "getting_started.appsshort": "יישומונים לניידים",
@@ -104,40 +107,49 @@
   "home.column_settings.show_reblogs": "הצגת הדהודים",
   "home.column_settings.show_replies": "הצגת תגובות",
   "home.settings": "הגדרות טור",
-  "keyboard_shortcuts.back": "to navigate back",
-  "keyboard_shortcuts.boost": "to boost",
-  "keyboard_shortcuts.column": "to focus a status in one of the columns",
-  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.back": "ניווט חזרה",
+  "keyboard_shortcuts.boost": "להדהד",
+  "keyboard_shortcuts.column": "להתמקד בהודעה באחד מהטורים",
+  "keyboard_shortcuts.compose": "להתמקד בתיבת חיבור ההודעות",
   "keyboard_shortcuts.description": "Description",
-  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.down": "לנוע במורד הרשימה",
   "keyboard_shortcuts.enter": "to open status",
-  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.favourite": "לחבב",
   "keyboard_shortcuts.heading": "Keyboard Shortcuts",
-  "keyboard_shortcuts.hotkey": "Hotkey",
-  "keyboard_shortcuts.legend": "to display this legend",
-  "keyboard_shortcuts.mention": "to mention author",
-  "keyboard_shortcuts.reply": "to reply",
-  "keyboard_shortcuts.search": "to focus search",
-  "keyboard_shortcuts.toot": "to start a brand new toot",
-  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
-  "keyboard_shortcuts.up": "to move up in the list",
+  "keyboard_shortcuts.hotkey": "מקש קיצור",
+  "keyboard_shortcuts.legend": "להציג את הפירוש",
+  "keyboard_shortcuts.mention": "לאזכר את המחבר(ת)",
+  "keyboard_shortcuts.reply": "לענות",
+  "keyboard_shortcuts.search": "להתמקד בחלון החיפוש",
+  "keyboard_shortcuts.toot": "להתחיל חיצרוץ חדש",
+  "keyboard_shortcuts.unfocus": "לצאת מתיבת חיבור/חיפוש",
+  "keyboard_shortcuts.up": "לנוע במעלה הרשימה",
   "lightbox.close": "סגירה",
-  "lightbox.next": "Next",
-  "lightbox.previous": "Previous",
+  "lightbox.next": "הלאה",
+  "lightbox.previous": "הקודם",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "טוען...",
   "media_gallery.toggle_visible": "נראה\\בלתי נראה",
   "missing_indicator.label": "לא נמצא",
-  "mute_modal.hide_notifications": "Hide notifications from this user?",
+  "mute_modal.hide_notifications": "להסתיר הודעות מחשבון זה?",
   "navigation_bar.blocks": "חסימות",
   "navigation_bar.community_timeline": "ציר זמן מקומי",
   "navigation_bar.edit_profile": "עריכת פרופיל",
   "navigation_bar.favourites": "חיבובים",
   "navigation_bar.follow_requests": "בקשות מעקב",
   "navigation_bar.info": "מידע נוסף",
-  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.keyboard_shortcuts": "קיצורי מקלדת",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "יציאה",
   "navigation_bar.mutes": "השתקות",
-  "navigation_bar.pins": "Pinned toots",
+  "navigation_bar.pins": "חיצרוצים מקובעים",
   "navigation_bar.preferences": "העדפות",
   "navigation_bar.public_timeline": "ציר זמן בין-קהילתי",
   "notification.favourite": "חצרוצך חובב על ידי {name}",
@@ -150,8 +162,8 @@
   "notifications.column_settings.favourite": "מחובבים:",
   "notifications.column_settings.follow": "עוקבים חדשים:",
   "notifications.column_settings.mention": "פניות:",
-  "notifications.column_settings.push": "Push notifications",
-  "notifications.column_settings.push_meta": "This device",
+  "notifications.column_settings.push": "הודעות בדחיפה",
+  "notifications.column_settings.push_meta": "מכשיר זה",
   "notifications.column_settings.reblog": "הדהודים:",
   "notifications.column_settings.show": "הצגה בטור",
   "notifications.column_settings.sound": "שמע מופעל",
@@ -160,7 +172,7 @@
   "onboarding.page_five.public_timelines": "ציר הזמן המקומי מראה הודעות פומביות מכל באי קהילת {domain}. ציר הזמן העולמי מראה הודעות פומביות מאת כי מי שבאי קהילת {domain} עוקבים אחריו. אלו צירי הזמן הפומביים, דרך נהדרת לגלות אנשים חדשים.",
   "onboarding.page_four.home": "ציר זמן הבית מראה הודעות מהנעקבים שלך.",
   "onboarding.page_four.notifications": "טור ההתראות מראה כשמישהו מתייחס להודעות שלך.",
-  "onboarding.page_one.federation": "מסטודון היא רשת של שרתים עצמאיים מצורפים ביחד לכדי רשת חברתית אחת גדולה. אנחנו מכנים את השרתים האלו: קהילות",
+  "onboarding.page_one.federation": "מסטודון היא רשת של שרתים עצמאיים מצורפים ביחד לכדי רשת חברתית אחת גדולה. אנחנו מכנים את השרתים האלו קהילות.",
   "onboarding.page_one.handle": "אתם בקהילה {domain}, ולכן מזהה המשתמש המלא שלכם הוא {handle}",
   "onboarding.page_one.welcome": "ברוכים הבאים למסטודון!",
   "onboarding.page_six.admin": "הקהילה מנוהלת בידי {admin}.",
@@ -186,7 +198,7 @@
   "privacy.unlisted.short": "לא לפיד הכללי",
   "relative_time.days": "{number}d",
   "relative_time.hours": "{number}h",
-  "relative_time.just_now": "now",
+  "relative_time.just_now": "כרגע",
   "relative_time.minutes": "{number}m",
   "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "ביטול",
@@ -194,24 +206,24 @@
   "report.submit": "שליחה",
   "report.target": "דיווח",
   "search.placeholder": "חיפוש",
-  "search_popout.search_format": "Advanced search format",
-  "search_popout.tips.hashtag": "hashtag",
+  "search_popout.search_format": "מבנה חיפוש מתקדם",
+  "search_popout.tips.hashtag": "האשתג",
   "search_popout.tips.status": "status",
-  "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
-  "search_popout.tips.user": "user",
+  "search_popout.tips.text": "טקסט פשוט מחזיר כינויים, שמות משתמש והאשתגים",
+  "search_popout.tips.user": "משתמש(ת)",
   "search_results.total": "{count, number} {count, plural, one {תוצאה} other {תוצאות}}",
-  "standalone.public_title": "A look inside...",
+  "standalone.public_title": "הצצה פנימה...",
   "status.cannot_reblog": "לא ניתן להדהד הודעה זו",
   "status.delete": "מחיקה",
-  "status.embed": "Embed",
+  "status.embed": "הטמעה",
   "status.favourite": "חיבוב",
   "status.load_more": "עוד",
   "status.media_hidden": "מדיה מוסתרת",
   "status.mention": "פניה אל @{name}",
-  "status.more": "More",
+  "status.more": "עוד",
   "status.mute_conversation": "השתקת שיחה",
   "status.open": "הרחבת הודעה",
-  "status.pin": "Pin on profile",
+  "status.pin": "לקבע באודות",
   "status.reblog": "הדהוד",
   "status.reblogged_by": "הודהד על ידי {name}",
   "status.reply": "תגובה",
@@ -219,29 +231,29 @@
   "status.report": "דיווח על @{name}",
   "status.sensitive_toggle": "לחצו כדי לראות",
   "status.sensitive_warning": "תוכן רגיש",
-  "status.share": "Share",
+  "status.share": "שיתוף",
   "status.show_less": "הראה פחות",
   "status.show_more": "הראה יותר",
   "status.unmute_conversation": "הסרת השתקת שיחה",
-  "status.unpin": "Unpin from profile",
+  "status.unpin": "לשחרר מקיבוע באודות",
   "tabs_bar.compose": "חיבור",
   "tabs_bar.federated_timeline": "ציר זמן בין-קהילתי",
   "tabs_bar.home": "בבית",
   "tabs_bar.local_timeline": "ציר זמן מקומי",
   "tabs_bar.notifications": "התראות",
-  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
+  "ui.beforeunload": "הטיוטא תאבד אם תעזבו את מסטודון.",
   "upload_area.title": "ניתן להעלות על ידי Drag & drop",
   "upload_button.label": "הוספת מדיה",
-  "upload_form.description": "Describe for the visually impaired",
+  "upload_form.description": "תיאור לכבדי ראיה",
   "upload_form.undo": "ביטול",
   "upload_progress.label": "עולה...",
-  "video.close": "Close video",
-  "video.exit_fullscreen": "Exit full screen",
-  "video.expand": "Expand video",
+  "video.close": "סגירת וידאו",
+  "video.exit_fullscreen": "יציאה ממסך מלא",
+  "video.expand": "להרחיב וידאו",
   "video.fullscreen": "Full screen",
-  "video.hide": "Hide video",
-  "video.mute": "Mute sound",
+  "video.hide": "להסתיר וידאו",
+  "video.mute": "השתקת צליל",
   "video.pause": "Pause",
-  "video.play": "Play",
-  "video.unmute": "Unmute sound"
+  "video.play": "ניגון",
+  "video.unmute": "החזרת צליל"
 }
diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json
index 497917b9c..c21482670 100644
--- a/app/javascript/mastodon/locales/hr.json
+++ b/app/javascript/mastodon/locales/hr.json
@@ -36,6 +36,7 @@
   "column.favourites": "Favoriti",
   "column.follow_requests": "Zahtjevi za slijeđenje",
   "column.home": "Dom",
+  "column.lists": "Lists",
   "column.mutes": "Utišani korisnici",
   "column.notifications": "Notifikacije",
   "column.pins": "Pinned toot",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Želiš li sigurno blokirati {name}?",
   "confirmations.delete.confirm": "Obriši",
   "confirmations.delete.message": "Želiš li stvarno obrisati ovaj status?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Sakrij cijelu domenu",
   "confirmations.domain_block.message": "Jesi li zaista, zaista siguran da želiš potpuno blokirati {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
   "confirmations.mute.confirm": "Utišaj",
@@ -124,6 +127,14 @@
   "lightbox.close": "Zatvori",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Učitavam...",
   "media_gallery.toggle_visible": "Preklopi vidljivost",
   "missing_indicator.label": "Nije nađen",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "Zahtjevi za slijeđenje",
   "navigation_bar.info": "Više informacija",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Odjavi se",
   "navigation_bar.mutes": "Utišani korisnici",
   "navigation_bar.pins": "Pinned toots",
diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
index 6df09aea9..71dd810b6 100644
--- a/app/javascript/mastodon/locales/hu.json
+++ b/app/javascript/mastodon/locales/hu.json
@@ -36,6 +36,7 @@
   "column.favourites": "Favourites",
   "column.follow_requests": "Follow requests",
   "column.home": "Kezdőlap",
+  "column.lists": "Lists",
   "column.mutes": "Muted users",
   "column.notifications": "Értesítések",
   "column.pins": "Pinned toot",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Are you sure you want to block {name}?",
   "confirmations.delete.confirm": "Delete",
   "confirmations.delete.message": "Are you sure you want to delete this status?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Hide entire domain",
   "confirmations.domain_block.message": "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.",
   "confirmations.mute.confirm": "Mute",
@@ -124,6 +127,14 @@
   "lightbox.close": "Bezárás",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Betöltés...",
   "media_gallery.toggle_visible": "Toggle visibility",
   "missing_indicator.label": "Not found",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "Follow requests",
   "navigation_bar.info": "Extended information",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Kijelentkezés",
   "navigation_bar.mutes": "Muted users",
   "navigation_bar.pins": "Pinned toots",
diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json
index 66aff71f0..744423e78 100644
--- a/app/javascript/mastodon/locales/id.json
+++ b/app/javascript/mastodon/locales/id.json
@@ -36,6 +36,7 @@
   "column.favourites": "Favorit",
   "column.follow_requests": "Permintaan mengikuti",
   "column.home": "Beranda",
+  "column.lists": "Lists",
   "column.mutes": "Pengguna dibisukan",
   "column.notifications": "Notifikasi",
   "column.pins": "Pinned toot",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Apa anda yakin ingin memblokir {name}?",
   "confirmations.delete.confirm": "Hapus",
   "confirmations.delete.message": "Apa anda yakin akan menghapus status ini?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Hide entire domain",
   "confirmations.domain_block.message": "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.",
   "confirmations.mute.confirm": "Bisukan",
@@ -124,6 +127,14 @@
   "lightbox.close": "Tutup",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Tunggu sebentar...",
   "media_gallery.toggle_visible": "Tampil/Sembunyikan",
   "missing_indicator.label": "Tidak ditemukan",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "Permintaan mengikuti",
   "navigation_bar.info": "Informasi selengkapnya",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Keluar",
   "navigation_bar.mutes": "Pengguna dibisukan",
   "navigation_bar.pins": "Pinned toots",
diff --git a/app/javascript/mastodon/locales/index.js b/app/javascript/mastodon/locales/index.js
index 421cb7fab..7e7297561 100644
--- a/app/javascript/mastodon/locales/index.js
+++ b/app/javascript/mastodon/locales/index.js
@@ -1,9 +1 @@
-let theLocale;
-
-export function setLocale(locale) {
-  theLocale = locale;
-}
-
-export function getLocale() {
-  return theLocale;
-}
+export * from 'locales';
diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json
index 673f895fe..b1523e626 100644
--- a/app/javascript/mastodon/locales/io.json
+++ b/app/javascript/mastodon/locales/io.json
@@ -36,6 +36,7 @@
   "column.favourites": "Favorati",
   "column.follow_requests": "Demandi di sequado",
   "column.home": "Hemo",
+  "column.lists": "Lists",
   "column.mutes": "Celita uzeri",
   "column.notifications": "Savigi",
   "column.pins": "Pinned toot",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Are you sure you want to block {name}?",
   "confirmations.delete.confirm": "Delete",
   "confirmations.delete.message": "Are you sure you want to delete this status?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Hide entire domain",
   "confirmations.domain_block.message": "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.",
   "confirmations.mute.confirm": "Mute",
@@ -124,6 +127,14 @@
   "lightbox.close": "Klozar",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Kargante...",
   "media_gallery.toggle_visible": "Chanjar videbleso",
   "missing_indicator.label": "Ne trovita",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "Demandi di sequado",
   "navigation_bar.info": "Detaloza informi",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Ekirar",
   "navigation_bar.mutes": "Celita uzeri",
   "navigation_bar.pins": "Pinned toots",
diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json
index 3d2d47471..9a2d320fd 100644
--- a/app/javascript/mastodon/locales/it.json
+++ b/app/javascript/mastodon/locales/it.json
@@ -36,6 +36,7 @@
   "column.favourites": "Apprezzati",
   "column.follow_requests": "Richieste di amicizia",
   "column.home": "Home",
+  "column.lists": "Lists",
   "column.mutes": "Utenti silenziati",
   "column.notifications": "Notifiche",
   "column.pins": "Pinned toot",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Are you sure you want to block {name}?",
   "confirmations.delete.confirm": "Delete",
   "confirmations.delete.message": "Are you sure you want to delete this status?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Hide entire domain",
   "confirmations.domain_block.message": "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.",
   "confirmations.mute.confirm": "Mute",
@@ -124,6 +127,14 @@
   "lightbox.close": "Chiudi",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Carico...",
   "media_gallery.toggle_visible": "Imposta visibilità",
   "missing_indicator.label": "Non trovato",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "Richieste di amicizia",
   "navigation_bar.info": "Informazioni estese",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Logout",
   "navigation_bar.mutes": "Utenti silenziati",
   "navigation_bar.pins": "Pinned toots",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index 388dd57e8..e015c41c2 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -7,22 +7,22 @@
   "account.followers": "フォロワー",
   "account.follows": "フォロー",
   "account.follows_you": "フォローされています",
-  "account.hide_reblogs": "Hide boosts from @{name}",
+  "account.hide_reblogs": "@{name}さんからのブーストを非表示",
   "account.media": "メディア",
   "account.mention": "返信",
   "account.moved_to": "{name}さんは引っ越しました:",
   "account.mute": "ミュート",
-  "account.mute_notifications": "@{name}からの通知を受け取る",
+  "account.mute_notifications": "@{name}さんからの通知を受け取る",
   "account.posts": "投稿",
   "account.report": "通報",
   "account.requested": "承認待ち",
-  "account.share": "@{name} のプロフィールを共有する",
-  "account.show_reblogs": "Show boosts from @{name}",
+  "account.share": "@{name}さんのプロフィールを共有する",
+  "account.show_reblogs": "@{name}さんからのブーストを表示",
   "account.unblock": "ブロック解除",
   "account.unblock_domain": "{domain}を表示",
   "account.unfollow": "フォロー解除",
   "account.unmute": "ミュート解除",
-  "account.unmute_notifications": "@{name}からの通知を受け取らない",
+  "account.unmute_notifications": "@{name}さんからの通知を受け取らない",
   "account.view_full_profile": "全ての情報を見る",
   "boost_modal.combo": "次からは{combo}を押せば、これをスキップできます。",
   "bundle_column_error.body": "コンポーネントの読み込み中に問題が発生しました。",
@@ -36,6 +36,7 @@
   "column.favourites": "お気に入り",
   "column.follow_requests": "フォローリクエスト",
   "column.home": "ホーム",
+  "column.lists": "リスト",
   "column.mutes": "ミュートしたユーザー",
   "column.notifications": "通知",
   "column.pins": "固定されたトゥート",
@@ -59,15 +60,17 @@
   "compose_form.spoiler_placeholder": "ここに警告を書いてください",
   "confirmation_modal.cancel": "キャンセル",
   "confirmations.block.confirm": "ブロック",
-  "confirmations.block.message": "本当に{name}をブロックしますか?",
+  "confirmations.block.message": "本当に{name}さんをブロックしますか?",
   "confirmations.delete.confirm": "削除",
   "confirmations.delete.message": "本当に削除しますか?",
+  "confirmations.delete_list.confirm": "削除",
+  "confirmations.delete_list.message": "本当に削除しますか?",
   "confirmations.domain_block.confirm": "ドメイン全体を非表示",
   "confirmations.domain_block.message": "本当に{domain}全体を非表示にしますか? 多くの場合は個別にブロックやミュートするだけで充分であり、また好ましいです。",
   "confirmations.mute.confirm": "ミュート",
-  "confirmations.mute.message": "本当に{name}をミュートしますか?",
+  "confirmations.mute.message": "本当に{name}さんをミュートしますか?",
   "confirmations.unfollow.confirm": "フォロー解除",
-  "confirmations.unfollow.message": "本当に{name}をフォロー解除しますか?",
+  "confirmations.unfollow.message": "本当に{name}さんをフォロー解除しますか?",
   "embed.instructions": "下記のコードをコピーしてウェブサイトに埋め込みます。",
   "embed.preview": "表示例:",
   "emoji_button.activity": "活動",
@@ -112,7 +115,7 @@
   "keyboard_shortcuts.down": "カラム内一つ下に移動",
   "keyboard_shortcuts.enter": "トゥートの詳細を表示",
   "keyboard_shortcuts.favourite": "お気に入り",
-  "keyboard_shortcuts.heading": "キーボードショートカット一覧",
+  "keyboard_shortcuts.heading": "キーボードショートカット",
   "keyboard_shortcuts.hotkey": "ホットキー",
   "keyboard_shortcuts.legend": "この一覧を表示",
   "keyboard_shortcuts.mention": "メンション",
@@ -124,6 +127,14 @@
   "lightbox.close": "閉じる",
   "lightbox.next": "次",
   "lightbox.previous": "前",
+  "lists.account.add": "リストに追加",
+  "lists.account.remove": "リストから外す",
+  "lists.delete": "リストを削除",
+  "lists.edit": "リストを編集",
+  "lists.new.create": "リストを作成",
+  "lists.new.title_placeholder": "新規リスト名",
+  "lists.search": "フォローしている人の中から検索",
+  "lists.subheading": "あなたのリスト",
   "loading_indicator.label": "読み込み中...",
   "media_gallery.toggle_visible": "表示切り替え",
   "missing_indicator.label": "見つかりません",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "フォローリクエスト",
   "navigation_bar.info": "このインスタンスについて",
   "navigation_bar.keyboard_shortcuts": "キーボードショートカット",
+  "navigation_bar.lists": "リスト",
   "navigation_bar.logout": "ログアウト",
   "navigation_bar.mutes": "ミュートしたユーザー",
   "navigation_bar.pins": "固定されたトゥート",
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index 03028fc31..3f47baa76 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -36,6 +36,7 @@
   "column.favourites": "즐겨찾기",
   "column.follow_requests": "팔로우 요청",
   "column.home": "홈",
+  "column.lists": "Lists",
   "column.mutes": "뮤트 중인 사용자",
   "column.notifications": "알림",
   "column.pins": "고정된 툿",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "정말로 {name}를 차단하시겠습니까?",
   "confirmations.delete.confirm": "삭제",
   "confirmations.delete.message": "정말로 삭제하시겠습니까?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "도메인 전체를 숨김",
   "confirmations.domain_block.message": "정말로 {domain} 전체를 숨기시겠습니까? 대부분의 경우 개별 차단이나 뮤트로 충분합니다.",
   "confirmations.mute.confirm": "뮤트",
@@ -124,6 +127,14 @@
   "lightbox.close": "닫기",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "불러오는 중...",
   "media_gallery.toggle_visible": "표시 전환",
   "missing_indicator.label": "찾을 수 없습니다",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "팔로우 요청",
   "navigation_bar.info": "이 인스턴스에 대해서",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "로그아웃",
   "navigation_bar.mutes": "뮤트 중인 사용자",
   "navigation_bar.pins": "고정된 툿",
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
index 218709899..26e86308d 100644
--- a/app/javascript/mastodon/locales/nl.json
+++ b/app/javascript/mastodon/locales/nl.json
@@ -7,22 +7,22 @@
   "account.followers": "Volgers",
   "account.follows": "Volgt",
   "account.follows_you": "Volgt jou",
-  "account.hide_reblogs": "Hide boosts from @{name}",
+  "account.hide_reblogs": "Verberg boosts van @{name}",
   "account.media": "Media",
   "account.mention": "Vermeld @{name}",
-  "account.moved_to": "{name} has moved to:",
+  "account.moved_to": "{name} is verhuisd naar:",
   "account.mute": "Negeer @{name}",
-  "account.mute_notifications": "Mute notifications from @{name}",
+  "account.mute_notifications": "Negeer meldingen van @{name}",
   "account.posts": "Toots",
   "account.report": "Rapporteer @{name}",
-  "account.requested": "Wacht op goedkeuring. Klik om volgverzoek te annuleren.",
+  "account.requested": "Wacht op goedkeuring. Klik om het volgverzoek te annuleren",
   "account.share": "Profiel van @{name} delen",
-  "account.show_reblogs": "Show boosts from @{name}",
+  "account.show_reblogs": "Toon boosts van @{name}",
   "account.unblock": "Deblokkeer @{name}",
   "account.unblock_domain": "{domain} niet meer negeren",
   "account.unfollow": "Ontvolgen",
   "account.unmute": "@{name} niet meer negeren",
-  "account.unmute_notifications": "Unmute notifications from @{name}",
+  "account.unmute_notifications": "@{name} meldingen niet meer negeren",
   "account.view_full_profile": "Volledig profiel tonen",
   "boost_modal.combo": "Je kunt {combo} klikken om dit de volgende keer over te slaan",
   "bundle_column_error.body": "Tijdens het laden van dit onderdeel is er iets fout gegaan.",
@@ -36,6 +36,7 @@
   "column.favourites": "Favorieten",
   "column.follow_requests": "Volgverzoeken",
   "column.home": "Start",
+  "column.lists": "Lists",
   "column.mutes": "Genegeerde gebruikers",
   "column.notifications": "Meldingen",
   "column.pins": "Vastgezette toots",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Weet je het zeker dat je {name} wilt blokkeren?",
   "confirmations.delete.confirm": "Verwijderen",
   "confirmations.delete.message": "Weet je het zeker dat je deze toot wilt verwijderen?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Negeer alles van deze server",
   "confirmations.domain_block.message": "Weet je het echt, echt zeker dat je alles van {domain} wil negeren? In de meeste gevallen is het blokkeren of negeren van een paar specifieke personen voldoende en gewenst.",
   "confirmations.mute.confirm": "Negeren",
@@ -88,9 +91,9 @@
   "empty_column.hashtag": "Er is nog niks te vinden onder deze hashtag.",
   "empty_column.home": "Jij volgt nog niemand. Bezoek {public} of gebruik het zoekvenster om andere mensen te ontmoeten.",
   "empty_column.home.public_timeline": "de globale tijdlijn",
-  "empty_column.list": "There is nothing in this list yet.",
+  "empty_column.list": "Er is nog niks in deze lijst.",
   "empty_column.notifications": "Je hebt nog geen meldingen. Heb interactie met andere mensen om het gesprek aan te gaan.",
-  "empty_column.public": "Er is hier helemaal niks! Toot iets in het openbaar of volg mensen van andere Mastodon-servers om het te vullen.",
+  "empty_column.public": "Er is hier helemaal niks! Toot iets in het openbaar of volg mensen van andere servers om het te vullen",
   "follow_request.authorize": "Goedkeuren",
   "follow_request.reject": "Afkeuren",
   "getting_started.appsshort": "Apps",
@@ -99,42 +102,51 @@
   "getting_started.open_source_notice": "Mastodon is open-sourcesoftware. Je kunt bijdragen of problemen melden op GitHub via {github}.",
   "getting_started.userguide": "Gebruikersgids",
   "home.column_settings.advanced": "Geavanceerd",
-  "home.column_settings.basic": "Basic",
+  "home.column_settings.basic": "Basis",
   "home.column_settings.filter_regex": "Wegfilteren met reguliere expressies",
   "home.column_settings.show_reblogs": "Boosts tonen",
   "home.column_settings.show_replies": "Reacties tonen",
   "home.settings": "Kolom-instellingen",
-  "keyboard_shortcuts.back": "to navigate back",
-  "keyboard_shortcuts.boost": "to boost",
-  "keyboard_shortcuts.column": "to focus a status in one of the columns",
-  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.back": "om terug te navigeren",
+  "keyboard_shortcuts.boost": "om te boosten",
+  "keyboard_shortcuts.column": "om te focussen op een status in één van de kolommen",
+  "keyboard_shortcuts.compose": "om te focussen op het toot tekstvak",
   "keyboard_shortcuts.description": "Description",
-  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.down": "om naar beneden door de lijst te bewegen",
   "keyboard_shortcuts.enter": "to open status",
-  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.favourite": "om het te markeren als favoriet",
   "keyboard_shortcuts.heading": "Keyboard Shortcuts",
-  "keyboard_shortcuts.hotkey": "Hotkey",
-  "keyboard_shortcuts.legend": "to display this legend",
-  "keyboard_shortcuts.mention": "to mention author",
-  "keyboard_shortcuts.reply": "to reply",
-  "keyboard_shortcuts.search": "to focus search",
-  "keyboard_shortcuts.toot": "to start a brand new toot",
-  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
-  "keyboard_shortcuts.up": "to move up in the list",
+  "keyboard_shortcuts.hotkey": "Sneltoets",
+  "keyboard_shortcuts.legend": "om deze legenda weer te geven",
+  "keyboard_shortcuts.mention": "om de auteur te vermelden",
+  "keyboard_shortcuts.reply": "om te antwoorden",
+  "keyboard_shortcuts.search": "om te focussen op zoeken",
+  "keyboard_shortcuts.toot": "om een nieuwe toot te starten",
+  "keyboard_shortcuts.unfocus": "om te ontfocussen van het toot tekstvak/zoeken",
+  "keyboard_shortcuts.up": "om omhoog te bewegen in de lijst",
   "lightbox.close": "Sluiten",
   "lightbox.next": "Volgende",
   "lightbox.previous": "Vorige",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Laden…",
   "media_gallery.toggle_visible": "Media wel/niet tonen",
   "missing_indicator.label": "Niet gevonden",
-  "mute_modal.hide_notifications": "Hide notifications from this user?",
+  "mute_modal.hide_notifications": "Verberg meldingen van deze persoon?",
   "navigation_bar.blocks": "Geblokkeerde gebruikers",
   "navigation_bar.community_timeline": "Lokale tijdlijn",
   "navigation_bar.edit_profile": "Profiel bewerken",
   "navigation_bar.favourites": "Favorieten",
   "navigation_bar.follow_requests": "Volgverzoeken",
   "navigation_bar.info": "Uitgebreide informatie",
-  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.keyboard_shortcuts": "Toetsenbord sneltoetsen",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Afmelden",
   "navigation_bar.mutes": "Genegeerde gebruikers",
   "navigation_bar.pins": "Vastgezette toots",
@@ -186,7 +198,7 @@
   "privacy.unlisted.short": "Minder openbaar",
   "relative_time.days": "{number}d",
   "relative_time.hours": "{number}h",
-  "relative_time.just_now": "now",
+  "relative_time.just_now": "nu",
   "relative_time.minutes": "{number}m",
   "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "Annuleren",
@@ -197,7 +209,7 @@
   "search_popout.search_format": "Geavanceerd zoeken",
   "search_popout.tips.hashtag": "hashtag",
   "search_popout.tips.status": "toot",
-  "search_popout.tips.text": "Gebruik gewone tekst om te zoeken op weergavenamen, gebruikersnamen en hashtags.",
+  "search_popout.tips.text": "Gebruik gewone tekst om te zoeken op weergavenamen, gebruikersnamen en hashtags",
   "search_popout.tips.user": "gebruiker",
   "search_results.total": "{count, number} {count, plural, one {resultaat} other {resultaten}}",
   "standalone.public_title": "Een kijkje binnenin...",
@@ -229,7 +241,7 @@
   "tabs_bar.home": "Start",
   "tabs_bar.local_timeline": "Lokaal",
   "tabs_bar.notifications": "Meldingen",
-  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
+  "ui.beforeunload": "Je concept zal verloren gaan als je Mastodon verlaat.",
   "upload_area.title": "Hierin slepen om te uploaden",
   "upload_button.label": "Media toevoegen",
   "upload_form.description": "Omschrijf dit voor mensen met een visuele beperking",
diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json
index 234589d34..233b6c946 100644
--- a/app/javascript/mastodon/locales/no.json
+++ b/app/javascript/mastodon/locales/no.json
@@ -1,7 +1,7 @@
 {
   "account.block": "Blokkér @{name}",
   "account.block_domain": "Skjul alt fra {domain}",
-  "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
+  "account.disclaimer_full": "Informasjonen nedenfor kan gi et ufullstendig bilde av brukerens profil.",
   "account.edit_profile": "Rediger profil",
   "account.follow": "Følg",
   "account.followers": "Følgere",
@@ -10,43 +10,44 @@
   "account.hide_reblogs": "Hide boosts from @{name}",
   "account.media": "Media",
   "account.mention": "Nevn @{name}",
-  "account.moved_to": "{name} has moved to:",
+  "account.moved_to": "{name} har flyttet til:",
   "account.mute": "Demp @{name}",
-  "account.mute_notifications": "Mute notifications from @{name}",
+  "account.mute_notifications": "Ignorer varsler fra @{name}",
   "account.posts": "Innlegg",
   "account.report": "Rapportér @{name}",
   "account.requested": "Venter på godkjennelse",
-  "account.share": "Share @{name}'s profile",
-  "account.show_reblogs": "Show boosts from @{name}",
+  "account.share": "Del @{name}s profil",
+  "account.show_reblogs": "Vis boosts fra @{name}",
   "account.unblock": "Avblokker @{name}",
   "account.unblock_domain": "Vis {domain}",
   "account.unfollow": "Avfølg",
   "account.unmute": "Avdemp @{name}",
-  "account.unmute_notifications": "Unmute notifications from @{name}",
-  "account.view_full_profile": "View full profile",
+  "account.unmute_notifications": "Vis varsler fra @{name}",
+  "account.view_full_profile": "Vis full profil",
   "boost_modal.combo": "You kan trykke {combo} for å hoppe over dette neste gang",
-  "bundle_column_error.body": "Something went wrong while loading this component.",
-  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.body": "Noe gikk galt mens denne komponenten lastet.",
+  "bundle_column_error.retry": "Prøv igjen",
   "bundle_column_error.title": "Network error",
-  "bundle_modal_error.close": "Close",
-  "bundle_modal_error.message": "Something went wrong while loading this component.",
-  "bundle_modal_error.retry": "Try again",
+  "bundle_modal_error.close": "Lukk",
+  "bundle_modal_error.message": "Noe gikk galt da denne komponenten lastet.",
+  "bundle_modal_error.retry": "Prøv igjen",
   "column.blocks": "Blokkerte brukere",
   "column.community": "Lokal tidslinje",
   "column.favourites": "Likt",
   "column.follow_requests": "Følgeforespørsler",
   "column.home": "Hjem",
+  "column.lists": "Lists",
   "column.mutes": "Dempede brukere",
   "column.notifications": "Varsler",
   "column.pins": "Pinned toot",
   "column.public": "Felles tidslinje",
   "column_back_button.label": "Tilbake",
-  "column_header.hide_settings": "Hide settings",
-  "column_header.moveLeft_settings": "Move column to the left",
-  "column_header.moveRight_settings": "Move column to the right",
-  "column_header.pin": "Pin",
-  "column_header.show_settings": "Show settings",
-  "column_header.unpin": "Unpin",
+  "column_header.hide_settings": "Gjem  innstillinger",
+  "column_header.moveLeft_settings": "Flytt feltet til venstre",
+  "column_header.moveRight_settings": "Flytt feltet til høyre",
+  "column_header.pin": "Fest",
+  "column_header.show_settings": "Vis innstillinger",
+  "column_header.unpin": "Løsne",
   "column_subheading.navigation": "Navigasjon",
   "column_subheading.settings": "Innstillinger",
   "compose_form.lock_disclaimer": "Din konto er ikke {locked}. Hvem som helst kan følge deg og se dine private poster.",
@@ -62,33 +63,35 @@
   "confirmations.block.message": "Er du sikker på at du vil blokkere {name}?",
   "confirmations.delete.confirm": "Slett",
   "confirmations.delete.message": "Er du sikker på at du vil slette denne statusen?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Skjul alt fra domenet",
   "confirmations.domain_block.message": "Er du sikker på at du vil skjule hele domenet {domain}? I de fleste tilfeller er det bedre med målrettet blokkering eller demping.",
   "confirmations.mute.confirm": "Demp",
   "confirmations.mute.message": "Er du sikker på at du vil dempe {name}?",
-  "confirmations.unfollow.confirm": "Unfollow",
-  "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
-  "embed.instructions": "Embed this status on your website by copying the code below.",
-  "embed.preview": "Here is what it will look like:",
+  "confirmations.unfollow.confirm": "Slutt å følge",
+  "confirmations.unfollow.message": "Er du sikker på at du vil slutte å følge {name}?",
+  "embed.instructions": "Kopier koden under for å bygge inn denne statusen på hjemmesiden din.",
+  "embed.preview": "Slik kommer det til å se ut:",
   "emoji_button.activity": "Aktivitet",
   "emoji_button.custom": "Custom",
   "emoji_button.flags": "Flagg",
   "emoji_button.food": "Mat og drikke",
   "emoji_button.label": "Sett inn emoji",
   "emoji_button.nature": "Natur",
-  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
+  "emoji_button.not_found": "Ingen emojojoer!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Objekter",
   "emoji_button.people": "Mennesker",
-  "emoji_button.recent": "Frequently used",
+  "emoji_button.recent": "Hyppig brukt",
   "emoji_button.search": "Søk...",
-  "emoji_button.search_results": "Search results",
+  "emoji_button.search_results": "Søkeresultat",
   "emoji_button.symbols": "Symboler",
   "emoji_button.travel": "Reise & steder",
   "empty_column.community": "Den lokale tidslinjen er tom. Skriv noe offentlig for å få snøballen til å rulle!",
   "empty_column.hashtag": "Det er ingenting i denne hashtagen ennå.",
   "empty_column.home": "Du har ikke fulgt noen ennå. Besøk {publlic} eller bruk søk for å komme i gang og møte andre brukere.",
   "empty_column.home.public_timeline": "en offentlig tidslinje",
-  "empty_column.list": "There is nothing in this list yet.",
+  "empty_column.list": "Det er ikke noe i denne listen ennå.",
   "empty_column.notifications": "Du har ingen varsler ennå. Kommuniser med andre for å begynne samtalen.",
   "empty_column.public": "Det er ingenting her! Skriv noe offentlig, eller følg brukere manuelt fra andre instanser for å fylle den opp",
   "follow_request.authorize": "Autorisér",
@@ -104,19 +107,19 @@
   "home.column_settings.show_reblogs": "Vis fremhevinger",
   "home.column_settings.show_replies": "Vis svar",
   "home.settings": "Kolonneinnstillinger",
-  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.back": "for å navigere tilbake",
   "keyboard_shortcuts.boost": "to boost",
   "keyboard_shortcuts.column": "to focus a status in one of the columns",
   "keyboard_shortcuts.compose": "to focus the compose textarea",
   "keyboard_shortcuts.description": "Description",
-  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.down": "for å flytte ned i listen",
   "keyboard_shortcuts.enter": "to open status",
-  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.favourite": "for å favorittmarkere",
   "keyboard_shortcuts.heading": "Keyboard Shortcuts",
-  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.hotkey": "Lyntast",
   "keyboard_shortcuts.legend": "to display this legend",
   "keyboard_shortcuts.mention": "to mention author",
-  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.reply": "for å svare",
   "keyboard_shortcuts.search": "to focus search",
   "keyboard_shortcuts.toot": "to start a brand new toot",
   "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
@@ -124,6 +127,14 @@
   "lightbox.close": "Lukk",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Laster...",
   "media_gallery.toggle_visible": "Veksle synlighet",
   "missing_indicator.label": "Ikke funnet",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "Følgeforespørsler",
   "navigation_bar.info": "Utvidet informasjon",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Logg ut",
   "navigation_bar.mutes": "Dempede brukere",
   "navigation_bar.pins": "Pinned toots",
@@ -237,11 +249,11 @@
   "upload_progress.label": "Laster opp...",
   "video.close": "Close video",
   "video.exit_fullscreen": "Exit full screen",
-  "video.expand": "Expand video",
+  "video.expand": "Utvid video",
   "video.fullscreen": "Full screen",
-  "video.hide": "Hide video",
-  "video.mute": "Mute sound",
+  "video.hide": "Skjul video",
+  "video.mute": "Skru av lyd",
   "video.pause": "Pause",
-  "video.play": "Play",
-  "video.unmute": "Unmute sound"
+  "video.play": "Spill av",
+  "video.unmute": "Skru på lyd"
 }
diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json
index 6e68d6810..ec7202ff6 100644
--- a/app/javascript/mastodon/locales/oc.json
+++ b/app/javascript/mastodon/locales/oc.json
@@ -7,17 +7,17 @@
   "account.followers": "Seguidors",
   "account.follows": "Abonaments",
   "account.follows_you": "Vos sèc",
-  "account.hide_reblogs": "Hide boosts from @{name}",
+  "account.hide_reblogs": "Rescondre los partages de @{name}",
   "account.media": "Mèdias",
   "account.mention": "Mencionar @{name}",
-  "account.moved_to": "{name} has moved to:",
+  "account.moved_to": "{name} a mudat los catons a : ",
   "account.mute": "Rescondre @{name}",
   "account.mute_notifications": "Mute notifications from @{name}",
   "account.posts": "Estatuts",
   "account.report": "Senhalar @{name}",
   "account.requested": "Invitacion mandada. Clicatz per anullar.",
   "account.share": "Partejar lo perfil a @{name}",
-  "account.show_reblogs": "Show boosts from @{name}",
+  "account.show_reblogs": "Mostrar los partages de @{name}",
   "account.unblock": "Desblocar @{name}",
   "account.unblock_domain": "Desblocar {domain}",
   "account.unfollow": "Quitar de sègre",
@@ -36,6 +36,7 @@
   "column.favourites": "Favorits",
   "column.follow_requests": "Demandas d’abonament",
   "column.home": "Acuèlh",
+  "column.lists": "Lists",
   "column.mutes": "Personas rescondudas",
   "column.notifications": "Notificacions",
   "column.pins": "Tuts penjats",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Sètz segur de voler blocar {name} ?",
   "confirmations.delete.confirm": "Escafar",
   "confirmations.delete.message": "Sètz segur de voler escafar l’estatut ?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Amagar tot lo domeni",
   "confirmations.domain_block.message": "Sètz segur segur de voler blocar completament {domain} ? De còps cal pas que blocar o rescondre unas personas solament.",
   "confirmations.mute.confirm": "Rescondre",
@@ -88,7 +91,7 @@
   "empty_column.hashtag": "I a pas encara de contengut ligat a aqueste hashtag",
   "empty_column.home": "Vòstre flux d’acuèlh es void. Visitatz {public} o utilizatz la recèrca per vos connectar a d’autras personas.",
   "empty_column.home.public_timeline": "lo flux public",
-  "empty_column.list": "There is nothing in this list yet.",
+  "empty_column.list": "I a pas res dins la lista pel moment.",
   "empty_column.notifications": "Avètz pas encara de notificacions. Respondètz a qualqu’un per començar una conversacion.",
   "empty_column.public": "I a pas res aquí ! Escrivètz quicòm de public, o seguètz de personas d’autras instàncias per garnir lo flux public.",
   "follow_request.authorize": "Autorizar",
@@ -104,26 +107,34 @@
   "home.column_settings.show_reblogs": "Mostrar los partatges",
   "home.column_settings.show_replies": "Mostrar las responsas",
   "home.settings": "Paramètres de la colomna",
-  "keyboard_shortcuts.back": "to navigate back",
-  "keyboard_shortcuts.boost": "to boost",
-  "keyboard_shortcuts.column": "to focus a status in one of the columns",
-  "keyboard_shortcuts.compose": "to focus the compose textarea",
-  "keyboard_shortcuts.description": "Description",
-  "keyboard_shortcuts.down": "to move down in the list",
-  "keyboard_shortcuts.enter": "to open status",
-  "keyboard_shortcuts.favourite": "to favourite",
-  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
-  "keyboard_shortcuts.hotkey": "Hotkey",
-  "keyboard_shortcuts.legend": "to display this legend",
-  "keyboard_shortcuts.mention": "to mention author",
-  "keyboard_shortcuts.reply": "to reply",
-  "keyboard_shortcuts.search": "to focus search",
-  "keyboard_shortcuts.toot": "to start a brand new toot",
-  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
-  "keyboard_shortcuts.up": "to move up in the list",
+  "keyboard_shortcuts.back": "per anar enrèire",
+  "keyboard_shortcuts.boost": "per partejar",
+  "keyboard_shortcuts.column": "per centrar un estatut a una colomna",
+  "keyboard_shortcuts.compose": "per anar al camp tèxte",
+  "keyboard_shortcuts.description": "Descripcion",
+  "keyboard_shortcuts.down": "per far davalar dins la lista",
+  "keyboard_shortcuts.enter": "per dobrir los estatuts",
+  "keyboard_shortcuts.favourite": "per apondre als favorits",
+  "keyboard_shortcuts.heading": "Acorchis clavièr",
+  "keyboard_shortcuts.hotkey": "Clau",
+  "keyboard_shortcuts.legend": "per mostrar aquesta legenda",
+  "keyboard_shortcuts.mention": "per mencionar l’autor",
+  "keyboard_shortcuts.reply": "per respondre",
+  "keyboard_shortcuts.search": "per anar a la recèrca",
+  "keyboard_shortcuts.toot": "per començar un estatut tot novèl",
+  "keyboard_shortcuts.unfocus": "per quitar lo camp tèxte/de recèrca",
+  "keyboard_shortcuts.up": "per far montar dins la lista",
   "lightbox.close": "Tampar",
   "lightbox.next": "Seguent",
   "lightbox.previous": "Precedent",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Cargament…",
   "media_gallery.toggle_visible": "Modificar la visibilitat",
   "missing_indicator.label": "Pas trobat",
@@ -134,7 +145,8 @@
   "navigation_bar.favourites": "Favorits",
   "navigation_bar.follow_requests": "Demandas d'abonament",
   "navigation_bar.info": "Mai informacions",
-  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.keyboard_shortcuts": "Acorchis clavièr",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Desconnexion",
   "navigation_bar.mutes": "Personas rescondudas",
   "navigation_bar.pins": "Tuts penjats",
@@ -229,7 +241,7 @@
   "tabs_bar.home": "Acuèlh",
   "tabs_bar.local_timeline": "Flux public local",
   "tabs_bar.notifications": "Notificacions",
-  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
+  "ui.beforeunload": "Vòstre brolhon serà perdut se quitatz Mastodon.",
   "upload_area.title": "Lisatz e depausatz per mandar",
   "upload_button.label": "Ajustar un mèdia",
   "upload_form.description": "Descripcion pels mal vesents",
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
index 907999940..9295ab937 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -36,6 +36,7 @@
   "column.favourites": "Ulubione",
   "column.follow_requests": "Prośby o śledzenie",
   "column.home": "Strona główna",
+  "column.lists": "Listy",
   "column.mutes": "Wyciszeni użytkownicy",
   "column.notifications": "Powiadomienia",
   "column.pins": "Przypięte wpisy",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Czy na pewno chcesz zablokować {name}?",
   "confirmations.delete.confirm": "Usuń",
   "confirmations.delete.message": "Czy na pewno chcesz usunąć ten wpis?",
+  "confirmations.delete_list.confirm": "Usuń",
+  "confirmations.delete_list.message": "Czy na pewno chcesz bezpowrotnie usunąć tą listę?",
   "confirmations.domain_block.confirm": "Ukryj wszysyko z domeny",
   "confirmations.domain_block.message": "Czy na pewno chcesz zablokować całą domenę {domain}? Zwykle lepszym rozwiązaniem jest blokada lub wyciszenie kilku użytkowników.",
   "confirmations.mute.confirm": "Wycisz",
@@ -124,6 +127,14 @@
   "lightbox.close": "Zamknij",
   "lightbox.next": "Następne",
   "lightbox.previous": "Poprzednie",
+  "lists.account.add": "Dodaj do listy",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Usuń listę",
+  "lists.edit": "Edytuj listę",
+  "lists.new.create": "Utwórz listę",
+  "lists.new.title_placeholder": "Wprowadź tytuł listy…",
+  "lists.search": "Szukaj wśród osób które śledzisz",
+  "lists.subheading": "Twoje listy",
   "loading_indicator.label": "Ładowanie…",
   "media_gallery.toggle_visible": "Przełącz widoczność",
   "missing_indicator.label": "Nie znaleziono",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "Prośby o śledzenie",
   "navigation_bar.info": "Szczegółowe informacje",
   "navigation_bar.keyboard_shortcuts": "Skróty klawiszowe",
+  "navigation_bar.lists": "Listy",
   "navigation_bar.logout": "Wyloguj",
   "navigation_bar.mutes": "Wyciszeni użytkownicy",
   "navigation_bar.pins": "Przypięte wpisy",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index ba0d2093e..1df27d536 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -7,22 +7,22 @@
   "account.followers": "Seguidores",
   "account.follows": "Segue",
   "account.follows_you": "Segue você",
-  "account.hide_reblogs": "Hide boosts from @{name}",
+  "account.hide_reblogs": "Esconder compartilhamentos de @{name}",
   "account.media": "Mídia",
   "account.mention": "Mencionar @{name}",
-  "account.moved_to": "{name} has moved to:",
+  "account.moved_to": "{name} se mudou para:",
   "account.mute": "Silenciar @{name}",
-  "account.mute_notifications": "Mute notifications from @{name}",
+  "account.mute_notifications": "Silenciar notificações de @{name}",
   "account.posts": "Posts",
   "account.report": "Denunciar @{name}",
-  "account.requested": "Aguardando aprovação. Clique para cancelar a solicitação.",
+  "account.requested": "Aguardando aprovação. Clique para cancelar a solicitação",
   "account.share": "Compartilhar perfil de @{name}",
-  "account.show_reblogs": "Show boosts from @{name}",
+  "account.show_reblogs": "Mostra compartilhamentos de @{name}",
   "account.unblock": "Desbloquear @{name}",
   "account.unblock_domain": "Desbloquear {domain}",
   "account.unfollow": "Deixar de seguir",
   "account.unmute": "Não silenciar @{name}",
-  "account.unmute_notifications": "Unmute notifications from @{name}",
+  "account.unmute_notifications": "Retirar silêncio das notificações vindas de @{name}",
   "account.view_full_profile": "Ver perfil completo",
   "boost_modal.combo": "Você pode pressionar {combo} para ignorar este diálogo na próxima vez",
   "bundle_column_error.body": "Algo de errado aconteceu enquanto este componente era carregado.",
@@ -36,6 +36,7 @@
   "column.favourites": "Favoritos",
   "column.follow_requests": "Seguidores pendentes",
   "column.home": "Página inicial",
+  "column.lists": "Lists",
   "column.mutes": "Usuários silenciados",
   "column.notifications": "Notificações",
   "column.pins": "Postagens fixadas",
@@ -62,13 +63,15 @@
   "confirmations.block.message": "Você tem certeza de que quer bloquear {name}?",
   "confirmations.delete.confirm": "Excluir",
   "confirmations.delete.message": "Você tem certeza de que quer excluir esta postagem?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Esconder o domínio inteiro",
   "confirmations.domain_block.message": "Você quer mesmo bloquear {domain} inteiro? Na maioria dos casos, silenciar ou bloquear alguns usuários é o suficiente e o recomendado.",
   "confirmations.mute.confirm": "Silenciar",
   "confirmations.mute.message": "Você tem certeza de que quer silenciar {name}?",
   "confirmations.unfollow.confirm": "Deixar de seguir",
   "confirmations.unfollow.message": "Você tem certeza de que quer deixar de seguir {name}?",
-  "embed.instructions": "Incorpore esta postagem em seu site copiando o código abaixo:",
+  "embed.instructions": "Incorpore esta postagem em seu site copiando o código abaixo.",
   "embed.preview": "Aqui está uma previsão de como ficará:",
   "emoji_button.activity": "Atividades",
   "emoji_button.custom": "Customizados",
@@ -85,12 +88,12 @@
   "emoji_button.symbols": "Símbolos",
   "emoji_button.travel": "Viagens & Lugares",
   "empty_column.community": "A timeline local está vazia. Escreva algo publicamente para começar!",
-  "empty_column.hashtag": "Ainda não há qualquer conteúdo com essa hashtag",
+  "empty_column.hashtag": "Ainda não há qualquer conteúdo com essa hashtag.",
   "empty_column.home": "Você ainda não segue usuário algo. Visite a timeline {public} ou use o buscador para procurar e conhecer outros usuários.",
   "empty_column.home.public_timeline": "global",
-  "empty_column.list": "There is nothing in this list yet.",
-  "empty_column.notifications": "Você ainda não possui notificações. Interaja com outros usuários para começar a conversar!",
-  "empty_column.public": "Não há nada aqui! Escreva algo publicamente ou siga manualmente usuários de outras instâncias.",
+  "empty_column.list": "Ainda não há nada nesta lista.",
+  "empty_column.notifications": "Você ainda não possui notificações. Interaja com outros usuários para começar a conversar.",
+  "empty_column.public": "Não há nada aqui! Escreva algo publicamente ou siga manualmente usuários de outras instâncias",
   "follow_request.authorize": "Autorizar",
   "follow_request.reject": "Rejeitar",
   "getting_started.appsshort": "Apps",
@@ -104,8 +107,8 @@
   "home.column_settings.show_reblogs": "Mostrar compartilhamentos",
   "home.column_settings.show_replies": "Mostrar as respostas",
   "home.settings": "Configurações de colunas",
-  "keyboard_shortcuts.back": "to navigate back",
-  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.back": "para navegar de volta",
+  "keyboard_shortcuts.boost": "para compartilhar",
   "keyboard_shortcuts.column": "to focus a status in one of the columns",
   "keyboard_shortcuts.compose": "to focus the compose textarea",
   "keyboard_shortcuts.description": "Description",
@@ -124,6 +127,14 @@
   "lightbox.close": "Fechar",
   "lightbox.next": "Próximo",
   "lightbox.previous": "Anterior",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Carregando...",
   "media_gallery.toggle_visible": "Esconder/Mostrar",
   "missing_indicator.label": "Não encontrado",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "Seguidores pendentes",
   "navigation_bar.info": "Mais informações",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Sair",
   "navigation_bar.mutes": "Usuários silenciados",
   "navigation_bar.pins": "Postagens fixadas",
diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json
index f349bd191..3d3e19571 100644
--- a/app/javascript/mastodon/locales/pt.json
+++ b/app/javascript/mastodon/locales/pt.json
@@ -1,143 +1,155 @@
 {
   "account.block": "Bloquear @{name}",
-  "account.block_domain": "Hide everything from {domain}",
+  "account.block_domain": "Esconder tudo do domínio {domain}",
   "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
   "account.edit_profile": "Editar perfil",
   "account.follow": "Seguir",
   "account.followers": "Seguidores",
   "account.follows": "Segue",
   "account.follows_you": "É teu seguidor",
-  "account.hide_reblogs": "Hide boosts from @{name}",
+  "account.hide_reblogs": "Esconder partilhas de @{name}",
   "account.media": "Media",
   "account.mention": "Mencionar @{name}",
-  "account.moved_to": "{name} has moved to:",
+  "account.moved_to": "{name} mudou a sua conta para:",
   "account.mute": "Silenciar @{name}",
-  "account.mute_notifications": "Mute notifications from @{name}",
+  "account.mute_notifications": "Silenciar notificações de @{name}",
   "account.posts": "Posts",
   "account.report": "Denunciar @{name}",
   "account.requested": "A aguardar aprovação",
-  "account.share": "Share @{name}'s profile",
-  "account.show_reblogs": "Show boosts from @{name}",
+  "account.share": "Partilhar o perfil @{name}",
+  "account.show_reblogs": "Mostrar partilhas de @{name}",
   "account.unblock": "Não bloquear @{name}",
   "account.unblock_domain": "Unhide {domain}",
   "account.unfollow": "Deixar de seguir",
   "account.unmute": "Não silenciar @{name}",
-  "account.unmute_notifications": "Unmute notifications from @{name}",
-  "account.view_full_profile": "View full profile",
+  "account.unmute_notifications": "Deixar de silenciar @{name}",
+  "account.view_full_profile": "Ver perfil completo",
   "boost_modal.combo": "Pode clicar {combo} para não voltar a ver",
-  "bundle_column_error.body": "Something went wrong while loading this component.",
-  "bundle_column_error.retry": "Try again",
-  "bundle_column_error.title": "Network error",
-  "bundle_modal_error.close": "Close",
-  "bundle_modal_error.message": "Something went wrong while loading this component.",
-  "bundle_modal_error.retry": "Try again",
+  "bundle_column_error.body": "Algo de errado aconteceu enquanto este componente era carregado.",
+  "bundle_column_error.retry": "Tente de novo",
+  "bundle_column_error.title": "Erro de rede",
+  "bundle_modal_error.close": "Fechar",
+  "bundle_modal_error.message": "Algo de errado aconteceu enquanto este componente era carregado.",
+  "bundle_modal_error.retry": "Tente de novo",
   "column.blocks": "Utilizadores Bloqueados",
   "column.community": "Local",
   "column.favourites": "Favoritos",
   "column.follow_requests": "Seguidores Pendentes",
   "column.home": "Home",
+  "column.lists": "Lists",
   "column.mutes": "Utilizadores silenciados",
   "column.notifications": "Notificações",
   "column.pins": "Pinned toot",
   "column.public": "Global",
   "column_back_button.label": "Voltar",
-  "column_header.hide_settings": "Hide settings",
-  "column_header.moveLeft_settings": "Move column to the left",
-  "column_header.moveRight_settings": "Move column to the right",
-  "column_header.pin": "Pin",
-  "column_header.show_settings": "Show settings",
-  "column_header.unpin": "Unpin",
-  "column_subheading.navigation": "Navigation",
-  "column_subheading.settings": "Settings",
-  "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
-  "compose_form.lock_disclaimer.lock": "locked",
+  "column_header.hide_settings": "Esconder preferências",
+  "column_header.moveLeft_settings": "Mover coluna para a esquerda",
+  "column_header.moveRight_settings": "Mover coluna para a direita",
+  "column_header.pin": "Fixar",
+  "column_header.show_settings": "Mostrar preferências",
+  "column_header.unpin": "Remover fixar",
+  "column_subheading.navigation": "Navegação",
+  "column_subheading.settings": "Preferências",
+  "compose_form.lock_disclaimer": "A tua conta não está {locked}. Qualquer pessoa pode seguir-te e ver as publicações direcionadas apenas a seguidores.",
+  "compose_form.lock_disclaimer.lock": "bloqueada",
   "compose_form.placeholder": "Em que estás a pensar?",
   "compose_form.publish": "Publicar",
   "compose_form.publish_loud": "{publish}!",
   "compose_form.sensitive": "Marcar media como conteúdo sensível",
   "compose_form.spoiler": "Esconder texto com aviso",
   "compose_form.spoiler_placeholder": "Aviso de conteúdo",
-  "confirmation_modal.cancel": "Cancel",
+  "confirmation_modal.cancel": "Cancelar",
   "confirmations.block.confirm": "Block",
-  "confirmations.block.message": "Are you sure you want to block {name}?",
-  "confirmations.delete.confirm": "Delete",
-  "confirmations.delete.message": "Are you sure you want to delete this status?",
-  "confirmations.domain_block.confirm": "Hide entire domain",
-  "confirmations.domain_block.message": "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.",
-  "confirmations.mute.confirm": "Mute",
-  "confirmations.mute.message": "Are you sure you want to mute {name}?",
-  "confirmations.unfollow.confirm": "Unfollow",
-  "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
-  "embed.instructions": "Embed this status on your website by copying the code below.",
-  "embed.preview": "Here is what it will look like:",
-  "emoji_button.activity": "Activity",
-  "emoji_button.custom": "Custom",
-  "emoji_button.flags": "Flags",
-  "emoji_button.food": "Food & Drink",
+  "confirmations.block.message": "De certeza que queres bloquear {name}?",
+  "confirmations.delete.confirm": "Eliminar",
+  "confirmations.delete.message": "De certeza que queres eliminar esta publicação?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
+  "confirmations.domain_block.confirm": "Esconder tudo deste domínio",
+  "confirmations.domain_block.message": "De certeza que queres bloquear por completo o domínio {domain}? Na maioria dos casos, silenciar ou bloquear alguns utilizadores é o suficiente e o recomendado.",
+  "confirmations.mute.confirm": "Silenciar",
+  "confirmations.mute.message": "De certeza que queres silenciar {name}?",
+  "confirmations.unfollow.confirm": "Deixar de seguir",
+  "confirmations.unfollow.message": "De certeza que queres deixar de seguir {name}?",
+  "embed.instructions": "Publicar este post num outro site copiando o código abaixo.",
+  "embed.preview": "Podes ver aqui como irá ficar:",
+  "emoji_button.activity": "Actividade",
+  "emoji_button.custom": "Especiais",
+  "emoji_button.flags": "Bandeiras",
+  "emoji_button.food": "Comida & Bebida",
   "emoji_button.label": "Inserir Emoji",
-  "emoji_button.nature": "Nature",
-  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
-  "emoji_button.objects": "Objects",
-  "emoji_button.people": "People",
-  "emoji_button.recent": "Frequently used",
-  "emoji_button.search": "Search...",
-  "emoji_button.search_results": "Search results",
-  "emoji_button.symbols": "Symbols",
-  "emoji_button.travel": "Travel & Places",
-  "empty_column.community": "Ainda não existem conteúdo local para mostrar!",
-  "empty_column.hashtag": "Ainda não existe qualquer conteúdo com essa hashtag",
+  "emoji_button.nature": "Natureza",
+  "emoji_button.not_found": "Não tem emojos!! (╯°□°)╯︵ ┻━┻",
+  "emoji_button.objects": "Objectos",
+  "emoji_button.people": "Pessoas",
+  "emoji_button.recent": "Regularmente utilizados",
+  "emoji_button.search": "Procurar...",
+  "emoji_button.search_results": "Resultados da pesquisa",
+  "emoji_button.symbols": "Símbolos",
+  "emoji_button.travel": "Viagens & Lugares",
+  "empty_column.community": "Ainda não existe conteúdo local para mostrar!",
+  "empty_column.hashtag": "Não foram encontradas publicações com essa hashtag",
   "empty_column.home": "Ainda não segues qualquer utilizador. Visita {public} ou utiliza a pesquisa para procurar outros utilizadores.",
   "empty_column.home.public_timeline": "global",
-  "empty_column.list": "There is nothing in this list yet.",
+  "empty_column.list": "Ainda não existem publicações nesta lista.",
   "empty_column.notifications": "Não tens notificações. Interage com outros utilizadores para iniciar uma conversa.",
   "empty_column.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para ver aqui os conteúdos públicos.",
   "follow_request.authorize": "Autorizar",
   "follow_request.reject": "Rejeitar",
-  "getting_started.appsshort": "Apps",
+  "getting_started.appsshort": "Aplicações",
   "getting_started.faq": "FAQ",
   "getting_started.heading": "Primeiros passos",
   "getting_started.open_source_notice": "Mastodon é software de fonte aberta. Podes contribuir ou repostar problemas no GitHub do projecto: {github}.",
-  "getting_started.userguide": "User Guide",
+  "getting_started.userguide": "Guia do utilizador",
   "home.column_settings.advanced": "Avançado",
   "home.column_settings.basic": "Básico",
   "home.column_settings.filter_regex": "Filtrar com uma expressão regular",
   "home.column_settings.show_reblogs": "Mostrar as partilhas",
   "home.column_settings.show_replies": "Mostrar as respostas",
   "home.settings": "Parâmetros da listagem",
-  "keyboard_shortcuts.back": "to navigate back",
-  "keyboard_shortcuts.boost": "to boost",
-  "keyboard_shortcuts.column": "to focus a status in one of the columns",
-  "keyboard_shortcuts.compose": "to focus the compose textarea",
-  "keyboard_shortcuts.description": "Description",
-  "keyboard_shortcuts.down": "to move down in the list",
-  "keyboard_shortcuts.enter": "to open status",
-  "keyboard_shortcuts.favourite": "to favourite",
-  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.back": "para navegar de volta",
+  "keyboard_shortcuts.boost": "para partilhar",
+  "keyboard_shortcuts.column": "para focar uma publicação numa das colunas",
+  "keyboard_shortcuts.compose": "para focar na área de publicação",
+  "keyboard_shortcuts.description": "Descrição",
+  "keyboard_shortcuts.down": "para mover para baixo na lista",
+  "keyboard_shortcuts.enter": "para expandir uma publicação",
+  "keyboard_shortcuts.favourite": "para adicionar aos favoritos",
+  "keyboard_shortcuts.heading": "Atalhos do teclado",
   "keyboard_shortcuts.hotkey": "Hotkey",
-  "keyboard_shortcuts.legend": "to display this legend",
-  "keyboard_shortcuts.mention": "to mention author",
-  "keyboard_shortcuts.reply": "to reply",
-  "keyboard_shortcuts.search": "to focus search",
-  "keyboard_shortcuts.toot": "to start a brand new toot",
-  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
-  "keyboard_shortcuts.up": "to move up in the list",
+  "keyboard_shortcuts.legend": "para mostrar esta legenda",
+  "keyboard_shortcuts.mention": "para mencionar o autor",
+  "keyboard_shortcuts.reply": "para responder",
+  "keyboard_shortcuts.search": "para focar na pesquisa",
+  "keyboard_shortcuts.toot": "para compor um novo post",
+  "keyboard_shortcuts.unfocus": "para remover o foco da área de publicação/pesquisa",
+  "keyboard_shortcuts.up": "para mover para cima na lista",
   "lightbox.close": "Fechar",
-  "lightbox.next": "Next",
-  "lightbox.previous": "Previous",
-  "loading_indicator.label": "Carregando...",
+  "lightbox.next": "Próximo",
+  "lightbox.previous": "Anterior",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
+  "loading_indicator.label": "A carregar...",
   "media_gallery.toggle_visible": "Esconder/Mostrar",
   "missing_indicator.label": "Não encontrado",
-  "mute_modal.hide_notifications": "Hide notifications from this user?",
+  "mute_modal.hide_notifications": "Esconder notificações deste utilizador?",
   "navigation_bar.blocks": "Utilizadores bloqueados",
   "navigation_bar.community_timeline": "Local",
   "navigation_bar.edit_profile": "Editar perfil",
   "navigation_bar.favourites": "Favoritos",
   "navigation_bar.follow_requests": "Seguidores pendentes",
   "navigation_bar.info": "Mais informações",
-  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.keyboard_shortcuts": "Atalhos de teclado",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Sair",
   "navigation_bar.mutes": "Utilizadores silenciados",
-  "navigation_bar.pins": "Pinned toots",
+  "navigation_bar.pins": "Posts fixos",
   "navigation_bar.preferences": "Preferências",
   "navigation_bar.public_timeline": "Global",
   "notification.favourite": "{name} adicionou o teu post aos favoritos",
@@ -150,31 +162,31 @@
   "notifications.column_settings.favourite": "Favoritos:",
   "notifications.column_settings.follow": "Novos seguidores:",
   "notifications.column_settings.mention": "Menções:",
-  "notifications.column_settings.push": "Push notifications",
-  "notifications.column_settings.push_meta": "This device",
+  "notifications.column_settings.push": "Notificações Push",
+  "notifications.column_settings.push_meta": "Este dispositivo",
   "notifications.column_settings.reblog": "Partilhas:",
   "notifications.column_settings.show": "Mostrar nas colunas",
   "notifications.column_settings.sound": "Reproduzir som",
-  "onboarding.done": "Done",
-  "onboarding.next": "Next",
-  "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
-  "onboarding.page_four.home": "The home timeline shows posts from people you follow.",
-  "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.",
-  "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
-  "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}",
-  "onboarding.page_one.welcome": "Welcome to Mastodon!",
-  "onboarding.page_six.admin": "Your instance's admin is {admin}.",
-  "onboarding.page_six.almost_done": "Almost done...",
+  "onboarding.done": "Pronto",
+  "onboarding.next": "Próximo",
+  "onboarding.page_five.public_timelines": "A timeline local mostra as publicações de todos os utilizadores em {domain}. A timeline global mostra as publicações de todas as pessoas que pessoas em {domain} seguem. Estas são as timelines públicas, uma óptima forma de conhecer novas pessoas.",
+  "onboarding.page_four.home": "A timeline home mostra as publicações de pessoas que tu segues.",
+  "onboarding.page_four.notifications": "A coluna de notificações mostra-te quando alguém interage contigo.",
+  "onboarding.page_one.federation": "Mastodon é uma rede de servidores independentes ligados entre si para fazer uma grande rede social. Nós chamamos instâncias a estes servidores.",
+  "onboarding.page_one.handle": "Tu estás em {domain}, por isso o teu endereço completo de utilizador é {handle}",
+  "onboarding.page_one.welcome": "Bem-vindo(a) ao Mastodon!",
+  "onboarding.page_six.admin": "O administrador da tua instância é {admin}.",
+  "onboarding.page_six.almost_done": "Quase pronto...",
   "onboarding.page_six.appetoot": "Bon Appetoot!",
-  "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.",
-  "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
-  "onboarding.page_six.guidelines": "community guidelines",
-  "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!",
-  "onboarding.page_six.various_app": "mobile apps",
-  "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.",
-  "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.",
-  "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.",
-  "onboarding.skip": "Skip",
+  "onboarding.page_six.apps_available": "Existem {apps} disponíveis para iOS, Android e outras plataformas.",
+  "onboarding.page_six.github": "Mastodon é um software gratuito e de código aberto. Podes reportar bugs, solicitar novas funcionalidades e contribuir para o código em {github}.",
+  "onboarding.page_six.guidelines": "termos de utilização da comunidade",
+  "onboarding.page_six.read_guidelines": "Por favor, lê os {guidelines} de {domain}!",
+  "onboarding.page_six.various_app": "aplicações de telemóvel",
+  "onboarding.page_three.profile": "Edita o teu perfil para mudar a tua imagem, biografia e nome. Lá encontrarás também outras preferências que podes personalizar.",
+  "onboarding.page_three.search": "Utiliza a caixa de pesquisa para procurar pessoas ou hashtags, exemplo {illustration} / {introductions}. Para procurar uma pessoa que não está nesta instância, utiliza o endereço completo.",
+  "onboarding.page_two.compose": "Escreve posts na coluna de publicações. Podes publicar imagens, alterar a privacidade e adicionar alertas de conteúdo usando os ícones abaixo da caixa de composição.",
+  "onboarding.skip": "Saltar",
   "privacy.change": "Ajustar a privacidade da mensagem",
   "privacy.direct.long": "Apenas para utilizadores mencionados",
   "privacy.direct.short": "Directo",
@@ -186,7 +198,7 @@
   "privacy.unlisted.short": "Não listar",
   "relative_time.days": "{number}d",
   "relative_time.hours": "{number}h",
-  "relative_time.just_now": "now",
+  "relative_time.just_now": "agora",
   "relative_time.minutes": "{number}m",
   "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "Cancelar",
@@ -194,54 +206,54 @@
   "report.submit": "Enviar",
   "report.target": "Denunciar",
   "search.placeholder": "Pesquisar",
-  "search_popout.search_format": "Advanced search format",
+  "search_popout.search_format": "Formato avançado de pesquisa",
   "search_popout.tips.hashtag": "hashtag",
   "search_popout.tips.status": "status",
   "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
-  "search_popout.tips.user": "user",
+  "search_popout.tips.user": "utilizador",
   "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}",
-  "standalone.public_title": "A look inside...",
-  "status.cannot_reblog": "This post cannot be boosted",
+  "standalone.public_title": "Espreitar lá dentro...",
+  "status.cannot_reblog": "Este post não pode ser partilhado",
   "status.delete": "Eliminar",
   "status.embed": "Embed",
   "status.favourite": "Adicionar aos favoritos",
   "status.load_more": "Carregar mais",
   "status.media_hidden": "Media escondida",
   "status.mention": "Mencionar @{name}",
-  "status.more": "More",
-  "status.mute_conversation": "Mute conversation",
+  "status.more": "Mais",
+  "status.mute_conversation": "Silenciar conversa",
   "status.open": "Expandir",
   "status.pin": "Pin on profile",
   "status.reblog": "Partilhar",
   "status.reblogged_by": "{name} partilhou",
   "status.reply": "Responder",
-  "status.replyAll": "Reply to thread",
-  "status.report": "Denúnciar @{name}",
+  "status.replyAll": "Responder à conversa",
+  "status.report": "Denunciar @{name}",
   "status.sensitive_toggle": "Clique para ver",
   "status.sensitive_warning": "Conteúdo sensível",
   "status.share": "Share",
   "status.show_less": "Mostrar menos",
   "status.show_more": "Mostrar mais",
-  "status.unmute_conversation": "Unmute conversation",
-  "status.unpin": "Unpin from profile",
+  "status.unmute_conversation": "Deixar de silenciar esta conversa",
+  "status.unpin": "Não fixar no perfil",
   "tabs_bar.compose": "Criar",
   "tabs_bar.federated_timeline": "Global",
   "tabs_bar.home": "Home",
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notificações",
-  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
+  "ui.beforeunload": "O teu rascunho vai ser perdido se abandonares o Mastodon.",
   "upload_area.title": "Arraste e solte para enviar",
   "upload_button.label": "Adicionar media",
-  "upload_form.description": "Describe for the visually impaired",
+  "upload_form.description": "Descrição da imagem para pessoas com dificuldades visuais",
   "upload_form.undo": "Anular",
   "upload_progress.label": "A gravar...",
-  "video.close": "Close video",
-  "video.exit_fullscreen": "Exit full screen",
-  "video.expand": "Expand video",
+  "video.close": "Fechar vídeo",
+  "video.exit_fullscreen": "Sair de full screen",
+  "video.expand": "Expandir vídeo",
   "video.fullscreen": "Full screen",
-  "video.hide": "Hide video",
-  "video.mute": "Mute sound",
-  "video.pause": "Pause",
-  "video.play": "Play",
-  "video.unmute": "Unmute sound"
+  "video.hide": "Esconder vídeo",
+  "video.mute": "Silenciar",
+  "video.pause": "Pausar",
+  "video.play": "Reproduzir",
+  "video.unmute": "Remover de silêncio"
 }
diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json
index df52eca14..0aef2d9df 100644
--- a/app/javascript/mastodon/locales/ru.json
+++ b/app/javascript/mastodon/locales/ru.json
@@ -36,6 +36,7 @@
   "column.favourites": "Понравившееся",
   "column.follow_requests": "Запросы на подписку",
   "column.home": "Главная",
+  "column.lists": "Lists",
   "column.mutes": "Список глушения",
   "column.notifications": "Уведомления",
   "column.pins": "Pinned toot",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Вы уверены, что хотите заблокировать {name}?",
   "confirmations.delete.confirm": "Удалить",
   "confirmations.delete.message": "Вы уверены, что хотите удалить этот статус?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Блокировать весь домен",
   "confirmations.domain_block.message": "Вы на самом деле уверены, что хотите блокировать весь {domain}? В большинстве случаев нескольких отдельных блокировок или глушений достаточно.",
   "confirmations.mute.confirm": "Заглушить",
@@ -124,6 +127,14 @@
   "lightbox.close": "Закрыть",
   "lightbox.next": "Далее",
   "lightbox.previous": "Назад",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Загрузка...",
   "media_gallery.toggle_visible": "Показать/скрыть",
   "missing_indicator.label": "Не найдено",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "Запросы на подписку",
   "navigation_bar.info": "Об узле",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Выйти",
   "navigation_bar.mutes": "Список глушения",
   "navigation_bar.pins": "Pinned toots",
diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json
index 5f887ef34..53090452f 100644
--- a/app/javascript/mastodon/locales/sv.json
+++ b/app/javascript/mastodon/locales/sv.json
@@ -36,6 +36,7 @@
   "column.favourites": "Favoriter",
   "column.follow_requests": "Följ förfrågningar",
   "column.home": "Hem",
+  "column.lists": "Lists",
   "column.mutes": "Tystade användare",
   "column.notifications": "Meddelanden",
   "column.pins": "Nålade toots",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Är du säker att du vill blockera {name}?",
   "confirmations.delete.confirm": "Ta bort",
   "confirmations.delete.message": "Är du säker att du vill ta bort denna status?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Blockera hela domänen",
   "confirmations.domain_block.message": "Är du verkligen, verkligen säker på att du vill blockera hela {domain}? I de flesta fall är några riktade blockeringar eller nedtystade tillräckligt och föredras.",
   "confirmations.mute.confirm": "Tysta",
@@ -124,6 +127,14 @@
   "lightbox.close": "Stäng",
   "lightbox.next": "Nästa",
   "lightbox.previous": "Tidigare",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Laddar...",
   "media_gallery.toggle_visible": "Växla synlighet",
   "missing_indicator.label": "Hittades inte",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "Följförfrågningar",
   "navigation_bar.info": "Om denna instans",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Logga ut",
   "navigation_bar.mutes": "Tystade användare",
   "navigation_bar.pins": "Nålade inlägg (toots)",
diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json
index 7b2b13202..2f064a193 100644
--- a/app/javascript/mastodon/locales/th.json
+++ b/app/javascript/mastodon/locales/th.json
@@ -36,6 +36,7 @@
   "column.favourites": "Favourites",
   "column.follow_requests": "Follow requests",
   "column.home": "Home",
+  "column.lists": "Lists",
   "column.mutes": "Muted users",
   "column.notifications": "Notifications",
   "column.pins": "Pinned toot",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Are you sure you want to block {name}?",
   "confirmations.delete.confirm": "Delete",
   "confirmations.delete.message": "Are you sure you want to delete this status?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Hide entire domain",
   "confirmations.domain_block.message": "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.",
   "confirmations.mute.confirm": "Mute",
@@ -124,6 +127,14 @@
   "lightbox.close": "Close",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Loading...",
   "media_gallery.toggle_visible": "Toggle visibility",
   "missing_indicator.label": "Not found",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "Follow requests",
   "navigation_bar.info": "About this instance",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Logout",
   "navigation_bar.mutes": "Muted users",
   "navigation_bar.pins": "Pinned toots",
diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json
index dd5a96bdb..be8103d1c 100644
--- a/app/javascript/mastodon/locales/tr.json
+++ b/app/javascript/mastodon/locales/tr.json
@@ -36,6 +36,7 @@
   "column.favourites": "Favoriler",
   "column.follow_requests": "Takip istekleri",
   "column.home": "Anasayfa",
+  "column.lists": "Lists",
   "column.mutes": "Susturulmuş kullanıcılar",
   "column.notifications": "Bildirimler",
   "column.pins": "Pinned toot",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "{name} kullanıcısını engellemek istiyor musunuz?",
   "confirmations.delete.confirm": "Sil",
   "confirmations.delete.message": "Bu gönderiyi silmek istiyor musunuz?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Hide entire domain",
   "confirmations.domain_block.message": "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.",
   "confirmations.mute.confirm": "Sessize al",
@@ -124,6 +127,14 @@
   "lightbox.close": "Kapat",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Yükleniyor...",
   "media_gallery.toggle_visible": "Görünürlüğü değiştir",
   "missing_indicator.label": "Bulunamadı",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "Takip istekleri",
   "navigation_bar.info": "Genişletilmiş bilgi",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Çıkış",
   "navigation_bar.mutes": "Sessize alınmış kullanıcılar",
   "navigation_bar.pins": "Pinned toots",
diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json
index 6206cd65b..273661462 100644
--- a/app/javascript/mastodon/locales/uk.json
+++ b/app/javascript/mastodon/locales/uk.json
@@ -36,6 +36,7 @@
   "column.favourites": "Вподобане",
   "column.follow_requests": "Запити на підписку",
   "column.home": "Головна",
+  "column.lists": "Lists",
   "column.mutes": "Заглушені користувачі",
   "column.notifications": "Сповіщення",
   "column.pins": "Pinned toot",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Ви впевнені, що хочете заблокувати {name}?",
   "confirmations.delete.confirm": "Видалити",
   "confirmations.delete.message": "Ви впевнені, що хочете видалити цей допис?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Сховати весь домен",
   "confirmations.domain_block.message": "Ви точно, точно впевнені, що хочете заблокувати весь домен {domain}? У більшості випадків для нормальної роботи краще заблокувати/заглушити лише деяких користувачів.",
   "confirmations.mute.confirm": "Заглушити",
@@ -124,6 +127,14 @@
   "lightbox.close": "Закрити",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Завантаження...",
   "media_gallery.toggle_visible": "Показати/приховати",
   "missing_indicator.label": "Не знайдено",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "Запити на підписку",
   "navigation_bar.info": "Про інстанцію",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Вийти",
   "navigation_bar.mutes": "Заглушені користувачі",
   "navigation_bar.pins": "Pinned toots",
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index c23868cda..cb5607cc5 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -7,7 +7,7 @@
   "account.followers": "关注者",
   "account.follows": "正在关注",
   "account.follows_you": "关注了你",
-  "account.hide_reblogs": "Hide boosts from @{name}",
+  "account.hide_reblogs": "隐藏来自 @{name} 的转嘟",
   "account.media": "媒体",
   "account.mention": "提及 @{name}",
   "account.moved_to": "{name} 已经迁移到:",
@@ -17,7 +17,7 @@
   "account.report": "举报 @{name}",
   "account.requested": "正在等待对方同意。点击以取消发送关注请求",
   "account.share": "分享 @{name} 的个人资料",
-  "account.show_reblogs": "Show boosts from @{name}",
+  "account.show_reblogs": "显示来自 @{name} 的转嘟",
   "account.unblock": "不再屏蔽 @{name}",
   "account.unblock_domain": "不再隐藏来自 {domain} 的内容",
   "account.unfollow": "取消关注",
@@ -36,6 +36,7 @@
   "column.favourites": "收藏过的嘟文",
   "column.follow_requests": "关注请求",
   "column.home": "主页",
+  "column.lists": "列表",
   "column.mutes": "被隐藏的用户",
   "column.notifications": "通知",
   "column.pins": "置顶嘟文",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "想好了,真的要屏蔽 {name}?",
   "confirmations.delete.confirm": "删除",
   "confirmations.delete.message": "想好了,真的要删除这条嘟文?",
+  "confirmations.delete_list.confirm": "删除",
+  "confirmations.delete_list.message": "你确定要永久删除这个列表吗?",
   "confirmations.domain_block.confirm": "隐藏整个网站的内容",
   "confirmations.domain_block.message": "你真的真的确定要隐藏所有来自 {domain} 的内容吗?多数情况下,屏蔽或隐藏几个特定的用户就应该能满足你的需要了。",
   "confirmations.mute.confirm": "隐藏",
@@ -104,26 +107,34 @@
   "home.column_settings.show_reblogs": "显示转嘟",
   "home.column_settings.show_replies": "显示回复",
   "home.settings": "栏目设置",
-  "keyboard_shortcuts.back": "to navigate back",
-  "keyboard_shortcuts.boost": "to boost",
-  "keyboard_shortcuts.column": "to focus a status in one of the columns",
-  "keyboard_shortcuts.compose": "to focus the compose textarea",
-  "keyboard_shortcuts.description": "Description",
-  "keyboard_shortcuts.down": "to move down in the list",
-  "keyboard_shortcuts.enter": "to open status",
-  "keyboard_shortcuts.favourite": "to favourite",
-  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
-  "keyboard_shortcuts.hotkey": "Hotkey",
-  "keyboard_shortcuts.legend": "to display this legend",
-  "keyboard_shortcuts.mention": "to mention author",
-  "keyboard_shortcuts.reply": "to reply",
-  "keyboard_shortcuts.search": "to focus search",
-  "keyboard_shortcuts.toot": "to start a brand new toot",
-  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
-  "keyboard_shortcuts.up": "to move up in the list",
+  "keyboard_shortcuts.back": "返回上一页",
+  "keyboard_shortcuts.boost": "转嘟",
+  "keyboard_shortcuts.column": "选择第 X 栏中的嘟文",
+  "keyboard_shortcuts.compose": "选择嘟文撰写框",
+  "keyboard_shortcuts.description": "说明",
+  "keyboard_shortcuts.down": "在列表中让光标下移",
+  "keyboard_shortcuts.enter": "展开嘟文",
+  "keyboard_shortcuts.favourite": "收藏嘟文",
+  "keyboard_shortcuts.heading": "快捷键列表",
+  "keyboard_shortcuts.hotkey": "快捷键",
+  "keyboard_shortcuts.legend": "显示此列表",
+  "keyboard_shortcuts.mention": "提及嘟文作者",
+  "keyboard_shortcuts.reply": "回复嘟文",
+  "keyboard_shortcuts.search": "选择搜索框",
+  "keyboard_shortcuts.toot": "发送新嘟文",
+  "keyboard_shortcuts.unfocus": "取消输入",
+  "keyboard_shortcuts.up": "在列表中让光标上移",
   "lightbox.close": "关闭",
   "lightbox.next": "下一步",
   "lightbox.previous": "上一步",
+  "lists.account.add": "添加到列表",
+  "lists.account.remove": "从列表中删除",
+  "lists.delete": "删除列表",
+  "lists.edit": "编辑列表",
+  "lists.new.create": "新建列表",
+  "lists.new.title_placeholder": "新列表的标题",
+  "lists.search": "搜索你关注的人",
+  "lists.subheading": "你的列表",
   "loading_indicator.label": "加载中……",
   "media_gallery.toggle_visible": "切换显示/隐藏",
   "missing_indicator.label": "找不到内容",
@@ -134,7 +145,8 @@
   "navigation_bar.favourites": "收藏的内容",
   "navigation_bar.follow_requests": "关注请求",
   "navigation_bar.info": "关于本站",
-  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.keyboard_shortcuts": "快捷键列表",
+  "navigation_bar.lists": "列表",
   "navigation_bar.logout": "注销",
   "navigation_bar.mutes": "被隐藏的用户",
   "navigation_bar.pins": "置顶嘟文",
diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json
index a1c7aa875..dbb9584c6 100644
--- a/app/javascript/mastodon/locales/zh-HK.json
+++ b/app/javascript/mastodon/locales/zh-HK.json
@@ -36,6 +36,7 @@
   "column.favourites": "最愛的文章",
   "column.follow_requests": "關注請求",
   "column.home": "主頁",
+  "column.lists": "Lists",
   "column.mutes": "靜音名單",
   "column.notifications": "通知",
   "column.pins": "置頂文章",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "你確定要封鎖{name}嗎?",
   "confirmations.delete.confirm": "刪除",
   "confirmations.delete.message": "你確定要刪除{name}嗎?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "隱藏整個網站",
   "confirmations.domain_block.message": "你真的真的確定要隱藏整個 {domain} ?多數情況下,比較推薦封鎖或靜音幾個特定目標就好。",
   "confirmations.mute.confirm": "靜音",
@@ -124,6 +127,14 @@
   "lightbox.close": "關閉",
   "lightbox.next": "繼續",
   "lightbox.previous": "回退",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "載入中...",
   "media_gallery.toggle_visible": "打開或關上",
   "missing_indicator.label": "找不到內容",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "關注請求",
   "navigation_bar.info": "關於本服務站",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "登出",
   "navigation_bar.mutes": "被你靜音的用戶",
   "navigation_bar.pins": "置頂文章",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index b52c5a810..0b05a83cd 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -36,6 +36,7 @@
   "column.favourites": "最愛",
   "column.follow_requests": "關注請求",
   "column.home": "家",
+  "column.lists": "Lists",
   "column.mutes": "消音的使用者",
   "column.notifications": "通知",
   "column.pins": "置頂貼文",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "你確定要封鎖 {name} ?",
   "confirmations.delete.confirm": "刪除",
   "confirmations.delete.message": "你確定要刪除這個狀態?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "隱藏整個網域",
   "confirmations.domain_block.message": "你真的真的確定要隱藏整個 {domain} ?多數情況下,比較推薦封鎖或消音幾個特定目標就好。",
   "confirmations.mute.confirm": "消音",
@@ -124,6 +127,14 @@
   "lightbox.close": "關閉",
   "lightbox.next": "繼續",
   "lightbox.previous": "回退",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "讀取中...",
   "media_gallery.toggle_visible": "切換可見性",
   "missing_indicator.label": "找不到",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "關注請求",
   "navigation_bar.info": "關於本站",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "登出",
   "navigation_bar.mutes": "消音的使用者",
   "navigation_bar.pins": "置頂貼文",
diff --git a/app/javascript/mastodon/reducers/accounts.js b/app/javascript/mastodon/reducers/accounts.js
index 5891d0fdd..f77061dfa 100644
--- a/app/javascript/mastodon/reducers/accounts.js
+++ b/app/javascript/mastodon/reducers/accounts.js
@@ -43,6 +43,10 @@ import {
   FAVOURITED_STATUSES_FETCH_SUCCESS,
   FAVOURITED_STATUSES_EXPAND_SUCCESS,
 } from '../actions/favourites';
+import {
+  LIST_ACCOUNTS_FETCH_SUCCESS,
+  LIST_EDITOR_SUGGESTIONS_READY,
+} from '../actions/lists';
 import { STORE_HYDRATE } from '../actions/store';
 import emojify from '../features/emoji/emoji';
 import { Map as ImmutableMap, fromJS } from 'immutable';
@@ -115,6 +119,8 @@ export default function accounts(state = initialState, action) {
   case BLOCKS_EXPAND_SUCCESS:
   case MUTES_FETCH_SUCCESS:
   case MUTES_EXPAND_SUCCESS:
+  case LIST_ACCOUNTS_FETCH_SUCCESS:
+  case LIST_EDITOR_SUGGESTIONS_READY:
     return action.accounts ? normalizeAccounts(state, action.accounts) : state;
   case NOTIFICATIONS_REFRESH_SUCCESS:
   case NOTIFICATIONS_EXPAND_SUCCESS:
diff --git a/app/javascript/mastodon/reducers/accounts_counters.js b/app/javascript/mastodon/reducers/accounts_counters.js
index 2a78a9f34..a93fa4245 100644
--- a/app/javascript/mastodon/reducers/accounts_counters.js
+++ b/app/javascript/mastodon/reducers/accounts_counters.js
@@ -45,6 +45,10 @@ import {
   FAVOURITED_STATUSES_FETCH_SUCCESS,
   FAVOURITED_STATUSES_EXPAND_SUCCESS,
 } from '../actions/favourites';
+import {
+  LIST_ACCOUNTS_FETCH_SUCCESS,
+  LIST_EDITOR_SUGGESTIONS_READY,
+} from '../actions/lists';
 import { STORE_HYDRATE } from '../actions/store';
 import { Map as ImmutableMap, fromJS } from 'immutable';
 
@@ -106,6 +110,8 @@ export default function accountsCounters(state = initialState, action) {
   case BLOCKS_EXPAND_SUCCESS:
   case MUTES_FETCH_SUCCESS:
   case MUTES_EXPAND_SUCCESS:
+  case LIST_ACCOUNTS_FETCH_SUCCESS:
+  case LIST_EDITOR_SUGGESTIONS_READY:
     return action.accounts ? normalizeAccounts(state, action.accounts) : state;
   case NOTIFICATIONS_REFRESH_SUCCESS:
   case NOTIFICATIONS_EXPAND_SUCCESS:
diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js
index 425a2acdd..a028e989c 100644
--- a/app/javascript/mastodon/reducers/index.js
+++ b/app/javascript/mastodon/reducers/index.js
@@ -23,6 +23,7 @@ import notifications from './notifications';
 import height_cache from './height_cache';
 import custom_emojis from './custom_emojis';
 import lists from './lists';
+import listEditor from './list_editor';
 
 const reducers = {
   timelines,
@@ -49,6 +50,7 @@ const reducers = {
   height_cache,
   custom_emojis,
   lists,
+  listEditor,
 };
 
 export default combineReducers(reducers);
diff --git a/app/javascript/mastodon/reducers/list_editor.js b/app/javascript/mastodon/reducers/list_editor.js
new file mode 100644
index 000000000..02a0dabb1
--- /dev/null
+++ b/app/javascript/mastodon/reducers/list_editor.js
@@ -0,0 +1,89 @@
+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,
+  title: '',
+
+  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('isSubmitting', false);
+    });
+  case LIST_EDITOR_TITLE_CHANGE:
+    return state.set('title', action.value);
+  case LIST_CREATE_REQUEST:
+  case LIST_UPDATE_REQUEST:
+    return state.set('isSubmitting', true);
+  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/mastodon/reducers/lists.js b/app/javascript/mastodon/reducers/lists.js
index 3e3908869..f30ffbcbd 100644
--- a/app/javascript/mastodon/reducers/lists.js
+++ b/app/javascript/mastodon/reducers/lists.js
@@ -1,14 +1,36 @@
-import { LIST_FETCH_SUCCESS } from '../actions/lists';
+import {
+  LIST_FETCH_SUCCESS,
+  LIST_FETCH_FAIL,
+  LISTS_FETCH_SUCCESS,
+  LIST_CREATE_SUCCESS,
+  LIST_UPDATE_SUCCESS,
+  LIST_DELETE_SUCCESS,
+} from '../actions/lists';
 import { Map as ImmutableMap, fromJS } from 'immutable';
 
 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/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js
index a9f3f9529..5817cf49b 100644
--- a/app/javascript/mastodon/reducers/settings.js
+++ b/app/javascript/mastodon/reducers/settings.js
@@ -2,6 +2,7 @@ import { SETTING_CHANGE, SETTING_SAVE } from '../actions/settings';
 import { COLUMN_ADD, COLUMN_REMOVE, COLUMN_MOVE } from '../actions/columns';
 import { STORE_HYDRATE } from '../actions/store';
 import { EMOJI_USE } from '../actions/emojis';
+import { LIST_DELETE_SUCCESS, LIST_FETCH_FAIL } from '../actions/lists';
 import { Map as ImmutableMap, fromJS } from 'immutable';
 import uuid from '../uuid';
 
@@ -84,6 +85,8 @@ const moveColumn = (state, uuid, direction) => {
 
 const updateFrequentEmojis = (state, emoji) => state.update('frequentlyUsedEmojis', ImmutableMap(), map => map.update(emoji.id, 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:
@@ -106,6 +109,10 @@ export default function settings(state = initialState, action) {
     return updateFrequentEmojis(state, action.emoji);
   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/packs/about.js b/app/javascript/packs/about.js
index 6ce8757dc..63e12da42 100644
--- a/app/javascript/packs/about.js
+++ b/app/javascript/packs/about.js
@@ -1,9 +1,7 @@
-import loadPolyfills from 'themes/glitch/util/load_polyfills';
-
-require.context('../images/', true);
+import loadPolyfills from '../mastodon/load_polyfills';
 
 function loaded() {
-  const TimelineContainer = require('themes/glitch/containers/timeline_container').default;
+  const TimelineContainer = require('../mastodon/containers/timeline_container').default;
   const React             = require('react');
   const ReactDOM          = require('react-dom');
   const mountNode         = document.getElementById('mastodon-timeline');
@@ -15,7 +13,7 @@ function loaded() {
 }
 
 function main() {
-  const ready = require('themes/glitch/util/ready').default;
+  const ready = require('../mastodon/ready').default;
   ready(loaded);
 }
 
diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js
index 21dc78986..116632dea 100644
--- a/app/javascript/packs/application.js
+++ b/app/javascript/packs/application.js
@@ -1,16 +1,5 @@
-//  THIS IS THE `vanilla` THEME PACK FILE!!
-//  IT'S HERE FOR UPSTREAM COMPATIBILITY!!
-//  THE `glitch` PACK FILE IS IN `themes/glitch/index.js`!!
-
 import loadPolyfills from '../mastodon/load_polyfills';
 
-// import default stylesheet with variables
-require('font-awesome/css/font-awesome.css');
-
-import '../styles/application.scss';
-
-require.context('../images/', true);
-
 loadPolyfills().then(() => {
   require('../mastodon/main').default();
 }).catch(e => {
diff --git a/app/javascript/packs/common.js b/app/javascript/packs/common.js
index 96e6f4b16..5d42509c5 100644
--- a/app/javascript/packs/common.js
+++ b/app/javascript/packs/common.js
@@ -1,6 +1 @@
-import { start } from 'rails-ujs';
-import 'font-awesome/css/font-awesome.css';
-
-require.context('../images/', true);
-
-start();
+import 'styles/application.scss';
diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js
index 6adacad98..3472af6c1 100644
--- a/app/javascript/packs/public.js
+++ b/app/javascript/packs/public.js
@@ -1,33 +1,14 @@
-import loadPolyfills from 'themes/glitch/util/load_polyfills';
-import { processBio } from 'themes/glitch/util/bio_metadata';
-import ready from 'themes/glitch/util/ready';
-
-window.addEventListener('message', e => {
-  const data = e.data || {};
-
-  if (!window.parent || data.type !== 'setHeight') {
-    return;
-  }
-
-  ready(() => {
-    window.parent.postMessage({
-      type: 'setHeight',
-      id: data.id,
-      height: document.getElementsByTagName('html')[0].scrollHeight,
-    }, '*');
-  });
-});
+import loadPolyfills from '../mastodon/load_polyfills';
+import ready from '../mastodon/ready';
 
 function main() {
-  const { length } = require('stringz');
   const IntlRelativeFormat = require('intl-relativeformat').default;
-  const { delegate } = require('rails-ujs');
-  const emojify = require('../themes/glitch/util/emoji').default;
-  const { getLocale } = require('mastodon/locales');
+  const emojify = require('../mastodon/features/emoji/emoji').default;
+  const { getLocale } = require('../mastodon/locales');
   const { localeData } = getLocale();
-  const VideoContainer = require('../themes/glitch/containers/video_container').default;
-  const MediaGalleryContainer = require('../themes/glitch/containers/media_gallery_container').default;
-  const CardContainer = require('../themes/glitch/containers/card_container').default;
+  const VideoContainer = require('../mastodon/containers/video_container').default;
+  const MediaGalleryContainer = require('../mastodon/containers/media_gallery_container').default;
+  const CardContainer = require('../mastodon/containers/card_container').default;
   const React = require('react');
   const ReactDOM = require('react-dom');
 
@@ -87,61 +68,6 @@ function main() {
       ReactDOM.render(<CardContainer locale={locale} {...props} />, content);
     });
   });
-
-  delegate(document, '.webapp-btn', 'click', ({ target, button }) => {
-    if (button !== 0) {
-      return true;
-    }
-    window.location.href = target.href;
-    return false;
-  });
-
-  delegate(document, '.status__content__spoiler-link', 'click', ({ target }) => {
-    const contentEl = target.parentNode.parentNode.querySelector('.e-content');
-
-    if (contentEl.style.display === 'block') {
-      contentEl.style.display = 'none';
-      target.parentNode.style.marginBottom = 0;
-    } else {
-      contentEl.style.display = 'block';
-      target.parentNode.style.marginBottom = null;
-    }
-
-    return false;
-  });
-
-  delegate(document, '.account_display_name', 'input', ({ target }) => {
-    const nameCounter = document.querySelector('.name-counter');
-
-    if (nameCounter) {
-      nameCounter.textContent = 30 - length(target.value);
-    }
-  });
-
-  delegate(document, '.account_note', 'input', ({ target }) => {
-    const noteCounter = document.querySelector('.note-counter');
-
-    if (noteCounter) {
-      const noteWithoutMetadata = processBio(target.value).text;
-      noteCounter.textContent = 500 - length(noteWithoutMetadata);
-    }
-  });
-
-  delegate(document, '#account_avatar', 'change', ({ target }) => {
-    const avatar = document.querySelector('.card.compact .avatar img');
-    const [file] = target.files || [];
-    const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc;
-
-    avatar.src = url;
-  });
-
-  delegate(document, '#account_header', 'change', ({ target }) => {
-    const header = document.querySelector('.card.compact');
-    const [file] = target.files || [];
-    const url = file ? URL.createObjectURL(file) : header.dataset.originalSrc;
-
-    header.style.backgroundImage = `url(${url})`;
-  });
 }
 
 loadPolyfills().then(main).catch(error => {
diff --git a/app/javascript/packs/share.js b/app/javascript/packs/share.js
index 9cd95bcee..e9580f648 100644
--- a/app/javascript/packs/share.js
+++ b/app/javascript/packs/share.js
@@ -1,9 +1,7 @@
-import loadPolyfills from 'themes/glitch/util/load_polyfills';
-
-require.context('../images/', true);
+import loadPolyfills from '../mastodon/load_polyfills';
 
 function loaded() {
-  const ComposeContainer = require('themes/glitch/containers/compose_container').default;
+  const ComposeContainer = require('../mastodon/containers/compose_container').default;
   const React = require('react');
   const ReactDOM = require('react-dom');
   const mountNode = document.getElementById('mastodon-compose');
@@ -15,7 +13,7 @@ function loaded() {
 }
 
 function main() {
-  const ready = require('themes/glitch/util/ready').default;
+  const ready = require('../mastodon/ready').default;
   ready(loaded);
 }
 
diff --git a/app/javascript/skins/vanilla/win95.scss b/app/javascript/skins/vanilla/win95.scss
new file mode 100644
index 000000000..298f6ee9d
--- /dev/null
+++ b/app/javascript/skins/vanilla/win95.scss
@@ -0,0 +1 @@
+@import 'styles/win95';
diff --git a/app/javascript/styles/common.scss b/app/javascript/styles/common.scss
deleted file mode 100644
index c1772e7ae..000000000
--- a/app/javascript/styles/common.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-// This makes our fonts available everywhere.
-
-@import 'fonts/roboto';
-@import 'fonts/roboto-mono';
-@import 'fonts/montserrat';
diff --git a/app/javascript/styles/mastodon/about.scss b/app/javascript/styles/mastodon/about.scss
index 358d86eec..e45fc03d3 100644
--- a/app/javascript/styles/mastodon/about.scss
+++ b/app/javascript/styles/mastodon/about.scss
@@ -19,7 +19,7 @@
     display: inline;
     margin: 0;
     padding: 0;
-    font-weight: 500;
+    font-weight: 700;
     background: transparent;
     font-family: inherit;
     font-size: inherit;
diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss
index 23e20a366..9015d04cb 100644
--- a/app/javascript/styles/mastodon/accounts.scss
+++ b/app/javascript/styles/mastodon/accounts.scss
@@ -473,6 +473,12 @@
       strong {
         font-weight: 500;
         color: $ui-base-color;
+
+        @each $lang in $cjk-langs {
+          &:lang(#{$lang}) {
+            font-weight: 700;
+          }
+        }
       }
 
       span {
diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss
index 7f078470e..bddea557b 100644
--- a/app/javascript/styles/mastodon/admin.scss
+++ b/app/javascript/styles/mastodon/admin.scss
@@ -121,6 +121,12 @@
       strong {
         color: $primary-text-color;
         font-weight: 500;
+
+        @each $lang in $cjk-langs {
+          &:lang(#{$lang}) {
+            font-weight: 700;
+          }
+        }
       }
     }
 
@@ -222,6 +228,12 @@
       font-weight: 500;
       text-transform: uppercase;
       font-size: 12px;
+
+      @each $lang in $cjk-langs {
+        &:lang(#{$lang}) {
+          font-weight: 700;
+        }
+      }
     }
 
     a {
@@ -265,6 +277,12 @@
     font-size: 14px;
     line-height: 18px;
     color: $ui-secondary-color;
+
+    @each $lang in $cjk-langs {
+      &:lang(#{$lang}) {
+        font-weight: 700;
+      }
+    }
   }
 
   .account-card {
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 26ff8bf00..17322264e 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -280,6 +280,12 @@
   strong {
     color: darken($ui-secondary-color, 65%);
     font-weight: 500;
+
+    @each $lang in $cjk-langs {
+      &:lang(#{$lang}) {
+        font-weight: 700;
+      }
+    }
   }
 
   a {
@@ -440,11 +446,6 @@
     position: absolute;
     right: 5px;
     top: 5px;
-
-    ::-webkit-scrollbar-track:hover,
-    ::-webkit-scrollbar-track:active {
-      background-color: rgba($base-overlay-background, 0.3);
-    }
   }
 }
 
@@ -517,7 +518,10 @@
   font-weight: 400;
   overflow: hidden;
   white-space: pre-wrap;
-  padding-top: 5px;
+
+  &:focus {
+    outline: 0;
+  }
 
   &.status__content--with-spoiler {
     white-space: normal;
@@ -530,7 +534,7 @@
   .emojione {
     width: 20px;
     height: 20px;
-    margin: -5px 0 0;
+    margin: -3px 0 0;
   }
 
   p {
@@ -766,7 +770,7 @@
 .status__action-bar {
   align-items: center;
   display: flex;
-  margin-top: 5px;
+  margin-top: 10px;
 }
 
 .status__action-bar-button {
@@ -799,7 +803,7 @@
     .emojione {
       width: 24px;
       height: 24px;
-      margin: -5px 0 0;
+      margin: -3px 0 0;
     }
   }
 
@@ -965,6 +969,12 @@
 
   strong {
     font-weight: 500;
+
+    @each $lang in $cjk-langs {
+      &:lang(#{$lang}) {
+        font-weight: 700;
+      }
+    }
   }
 
   a {
@@ -1066,6 +1076,12 @@
     font-size: 15px;
     font-weight: 500;
     color: $primary-text-color;
+
+    @each $lang in $cjk-langs {
+      &:lang(#{$lang}) {
+        font-weight: 700;
+      }
+    }
   }
 
   abbr {
@@ -2085,7 +2101,7 @@
 .getting-started {
   box-sizing: border-box;
   padding-bottom: 235px;
-  background: url('../images/mastodon-getting-started.png') no-repeat 0 100%;
+  background: url('~images/mastodon-getting-started.png') no-repeat 0 100%;
   flex: 1 0 auto;
 
   p {
@@ -2298,7 +2314,7 @@ button.icon-button.active i.fa-retweet {
   justify-content: center;
 
   & > div {
-    background: url('../images/mastodon-not-found.png') no-repeat center -50px;
+    background: url('~images/mastodon-not-found.png') no-repeat center -50px;
     padding-top: 210px;
     width: 100%;
   }
@@ -2359,6 +2375,10 @@ button.icon-button.active i.fa-retweet {
   margin-left: 0;
 }
 
+.column-header__links .text-btn {
+  margin-right: 10px;
+}
+
 .column-header__button {
   background: lighten($ui-base-color, 4%);
   border: 0;
@@ -2398,6 +2418,14 @@ button.icon-button.active i.fa-retweet {
   &.animating {
     overflow-y: hidden;
   }
+
+  hr {
+    height: 0;
+    background: transparent;
+    border: 0;
+    border-top: 1px solid lighten($ui-base-color, 12%);
+    margin: 10px 0;
+  }
 }
 
 .column-header__collapsible-inner {
@@ -2974,6 +3002,12 @@ button.icon-button.active i.fa-retweet {
     font-weight: 500;
     display: block;
     color: $ui-base-color;
+
+    @each $lang in $cjk-langs {
+      &:lang(#{$lang}) {
+        font-weight: 700;
+      }
+    }
   }
 }
 
@@ -3171,7 +3205,7 @@ button.icon-button.active i.fa-retweet {
   img,
   canvas {
     display: block;
-    background: url('../images/void.png') repeat;
+    background: url('~images/void.png') repeat;
     object-fit: contain;
   }
 
@@ -3408,6 +3442,12 @@ button.icon-button.active i.fa-retweet {
       border-radius: 4px;
       font-size: 14px;
       padding: 3px 6px;
+
+      @each $lang in $cjk-langs {
+        &:lang(#{$lang}) {
+          font-weight: 700;
+        }
+      }
     }
   }
 }
@@ -3418,7 +3458,7 @@ button.icon-button.active i.fa-retweet {
 }
 
 .onboarding-modal__page-one__elephant-friend {
-  background: url('../images/elephant-friend-1.png') no-repeat center center / contain;
+  background: url('~images/elephant-friend-1.png') no-repeat center center / contain;
   width: 155px;
   height: 193px;
   margin-right: 15px;
@@ -3738,6 +3778,12 @@ button.icon-button.active i.fa-retweet {
 
   strong {
     font-weight: 500;
+
+    @each $lang in $cjk-langs {
+      &:lang(#{$lang}) {
+        font-weight: 700;
+      }
+    }
   }
 }
 
@@ -4440,3 +4486,94 @@ noscript {
     margin-bottom: 0;
   }
 }
+
+.column-inline-form {
+  padding: 7px 15px;
+  padding-right: 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-left: 5px;
+  }
+}
+
+.drawer__backdrop {
+  cursor: pointer;
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background: rgba($base-overlay-background, 0.5);
+}
+
+.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 (max-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;
+  }
+}
diff --git a/app/javascript/styles/mastodon/emoji_picker.scss b/app/javascript/styles/mastodon/emoji_picker.scss
index 2b46d30fc..4161cc045 100644
--- a/app/javascript/styles/mastodon/emoji_picker.scss
+++ b/app/javascript/styles/mastodon/emoji_picker.scss
@@ -95,6 +95,11 @@
   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 {
diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss
index 61fcf286f..2bef53cff 100644
--- a/app/javascript/styles/mastodon/forms.scss
+++ b/app/javascript/styles/mastodon/forms.scss
@@ -56,6 +56,12 @@ code {
 
   strong {
     font-weight: 500;
+
+    @each $lang in $cjk-langs {
+      &:lang(#{$lang}) {
+        font-weight: 700;
+      }
+    }
   }
 
   .label_input {
@@ -395,6 +401,12 @@ code {
 
   strong {
     font-weight: 500;
+
+    @each $lang in $cjk-langs {
+      &:lang(#{$lang}) {
+        font-weight: 700;
+      }
+    }
   }
 
   @media screen and (max-width: 740px) and (min-width: 441px) {
@@ -430,6 +442,12 @@ code {
   strong {
     color: $ui-secondary-color;
     font-weight: 500;
+
+    @each $lang in $cjk-langs {
+      &:lang(#{$lang}) {
+        font-weight: 700;
+      }
+    }
   }
 
   @media screen and (max-width: 740px) and (min-width: 441px) {
@@ -474,6 +492,12 @@ code {
 
     strong {
       font-weight: 500;
+
+      @each $lang in $cjk-langs {
+        &:lang(#{$lang}) {
+          font-weight: 700;
+        }
+      }
     }
   }
 }
@@ -506,6 +530,12 @@ code {
       display: block;
       margin-bottom: 5px;
 
+      @each $lang in $cjk-langs {
+        &:lang(#{$lang}) {
+          font-weight: 700;
+        }
+      }
+
       .fa {
         font-weight: 400;
       }
diff --git a/app/javascript/styles/mastodon/landing_strip.scss b/app/javascript/styles/mastodon/landing_strip.scss
index 2f7c98d54..ffa1e149d 100644
--- a/app/javascript/styles/mastodon/landing_strip.scss
+++ b/app/javascript/styles/mastodon/landing_strip.scss
@@ -12,6 +12,12 @@
   strong,
   a {
     font-weight: 500;
+
+    @each $lang in $cjk-langs {
+      &:lang(#{$lang}) {
+        font-weight: 700;
+      }
+    }
   }
 
   a {
@@ -46,6 +52,12 @@
   strong,
   a {
     font-weight: 500;
+
+    @each $lang in $cjk-langs {
+      &:lang(#{$lang}) {
+        font-weight: 700;
+      }
+    }
   }
 
   a {
diff --git a/app/javascript/styles/mastodon/stream_entries.scss b/app/javascript/styles/mastodon/stream_entries.scss
index 4f323a378..a35bbee79 100644
--- a/app/javascript/styles/mastodon/stream_entries.scss
+++ b/app/javascript/styles/mastodon/stream_entries.scss
@@ -119,6 +119,12 @@
       strong {
         font-weight: 500;
         color: $ui-base-color;
+
+        @each $lang in $cjk-langs {
+          &:lang(#{$lang}) {
+            font-weight: 700;
+          }
+        }
       }
 
       span {
@@ -170,6 +176,12 @@
         strong {
           font-weight: 500;
           color: $ui-base-color;
+
+          @each $lang in $cjk-langs {
+            &:lang(#{$lang}) {
+              font-weight: 700;
+            }
+          }
         }
 
         span {
diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss
index ad46f5f9f..92870e679 100644
--- a/app/javascript/styles/mastodon/tables.scss
+++ b/app/javascript/styles/mastodon/tables.scss
@@ -40,6 +40,12 @@
 
   strong {
     font-weight: 500;
+
+    @each $lang in $cjk-langs {
+      &:lang(#{$lang}) {
+        font-weight: 700;
+      }
+    }
   }
 
   &.inline-table > tbody > tr:nth-child(odd) > td,
diff --git a/app/javascript/styles/mastodon/variables.scss b/app/javascript/styles/mastodon/variables.scss
index 52c8cd1cf..dcc2857ff 100644
--- a/app/javascript/styles/mastodon/variables.scss
+++ b/app/javascript/styles/mastodon/variables.scss
@@ -27,3 +27,6 @@ $ui-base-lighter-color: lighten($ui-base-color, 26%) !default; // Lighter darkes
 $ui-primary-color: $classic-primary-color !default;            // Lighter
 $ui-secondary-color: $classic-secondary-color !default;        // Lightest
 $ui-highlight-color: $classic-highlight-color !default;        // Vibrant
+
+// Language codes that uses CJK fonts
+$cjk-langs: ja, ko, zh-CN, zh-HK, zh-TW;
diff --git a/app/javascript/styles/win95.scss b/app/javascript/styles/win95.scss
new file mode 100644
index 000000000..d683218b0
--- /dev/null
+++ b/app/javascript/styles/win95.scss
@@ -0,0 +1,1655 @@
+//  win95 theme from cybrespace.
+
+//  Modified by kibi! to use webpack package syntax for urls (eg,
+//  `url(~images/…)`) for easy importing into skins.
+
+$win95-bg: #bfbfbf;
+$win95-dark-grey: #404040;
+$win95-mid-grey: #808080;
+$win95-window-header: #00007f;
+$win95-tooltip-yellow: #ffffcc;
+$win95-blue: blue;
+
+$ui-base-lighter-color: $win95-dark-grey;
+$ui-highlight-color: $win95-window-header;
+
+@mixin win95-border-outset() {
+  border-left: 2px solid #efefef;
+  border-top: 2px solid #efefef;
+  border-right: 2px solid #404040;
+  border-bottom: 2px solid #404040;
+  border-radius:0px;
+}
+
+@mixin win95-outset() {
+  box-shadow: inset -1px -1px 0px #000000,
+              inset 1px 1px 0px #ffffff,
+              inset -2px -2px 0px #808080,
+              inset 2px 2px 0px #dfdfdf;
+  border-radius:0px;
+}
+
+@mixin win95-border-inset() {
+  border-left: 2px solid #404040;
+  border-top: 2px solid #404040;
+  border-right: 2px solid #efefef;
+  border-bottom: 2px solid #efefef;
+  border-radius:0px;
+}
+
+@mixin win95-border-slight-inset() {
+  border-left: 1px solid #404040;
+  border-top: 1px solid #404040;
+  border-right: 1px solid #efefef;
+  border-bottom: 1px solid #efefef;
+  border-radius:0px;
+}
+
+@mixin win95-inset() {
+  box-shadow: inset 1px 1px 0px #000000,
+              inset -1px -1px 0px #ffffff,
+              inset 2px 2px 0px #808080,
+              inset -2px -2px 0px #dfdfdf;
+  border-width:0px;
+  border-radius:0px;
+}
+
+
+@mixin win95-tab() {
+  box-shadow: inset -1px 0px 0px #000000,
+              inset 1px 0px 0px #ffffff,
+              inset 0px 1px 0px #ffffff,
+              inset 0px 2px 0px #dfdfdf,
+              inset -2px 0px 0px #808080,
+              inset 2px 0px 0px #dfdfdf;
+  border-radius:0px;
+  border-top-left-radius: 1px;
+  border-top-right-radius: 1px;
+}
+
+@mixin win95-reset() {
+  box-shadow: unset;
+}
+
+@font-face {
+  font-family:"premillenium";
+  src: url('~fonts/premillenium/MSSansSerif.ttf') format('truetype');
+}
+
+@import 'application';
+
+/* borrowed from cybrespace style: wider columns and full column width images */
+
+@media screen and (min-width: 1300px) {
+  .column {
+    flex-grow: 1 !important;
+    max-width: 400px;
+  }
+
+  .drawer {
+    width: 17%;
+    max-width: 400px;
+    min-width: 330px;
+  }
+}
+
+.media-gallery,
+.video-player {
+  max-height:30vh;
+  height:30vh !important;
+  position:relative;
+  margin-top:20px;
+  margin-left:-68px;
+  width: calc(100% + 80px) !important;
+  max-width: calc(100% + 80px);
+}
+
+.detailed-status .media-gallery,
+.detailed-status .video-player {
+  margin-left:-5px;
+  width: calc(100% + 9px);
+  max-width: calc(100% + 9px);
+}
+
+.video-player video {
+  transform: unset;
+  top: unset;
+}
+
+.detailed-status .media-spoiler,
+.status .media-spoiler {
+  height: 100%!important;
+  vertical-align: middle;
+}
+
+
+/* main win95 style */
+
+body {
+  font-size:13px;
+  font-family: "MS Sans Serif", "premillenium", sans-serif;
+  color:black;
+}
+
+.ui,
+.ui .columns-area,
+body.admin {
+  background: #008080;
+}
+
+.loading-bar {
+  height:5px;
+  background-color: #000080;
+}
+
+.tabs-bar {
+  background: $win95-bg;
+  @include win95-outset()
+  height: 30px;
+}
+
+.tabs-bar__link {
+  color:black;
+  border:2px outset $win95-bg;
+  border-top-width: 1px;
+  border-left-width: 1px;
+  margin:2px;
+  padding:3px;
+}
+
+.tabs-bar__link.active {
+  @include win95-inset();
+  color:black;
+}
+
+.tabs-bar__link:last-child::before {
+  content:"Start";
+  color:black;
+  font-weight:bold;
+  font-size:15px;
+  width:80%;
+  display:block;
+  position:absolute;
+  right:0px;
+}
+
+.tabs-bar__link:last-child {
+  position:relative;
+  flex-basis:60px !important;
+  font-size:0px;
+  color:$win95-bg;
+
+  background-image: url("~images/start.png");
+  background-repeat:no-repeat;
+  background-position:8%;
+  background-clip:padding-box;
+  background-size:auto 50%;
+}
+
+.drawer .drawer__inner {
+  overflow: visible;
+  height:inherit;
+}
+
+.drawer__pager {
+  overflow-y:auto;
+}
+
+.column {
+  max-height:100vh;
+}
+
+.column > .scrollable {
+  background: $win95-bg;
+  @include win95-border-outset()
+  border-top-width:0px;
+}
+
+.column-header__wrapper {
+  color:white;
+  font-weight:bold;
+  background:#7f7f7f;
+}
+
+.column-header {
+  padding:2px;
+  font-size:13px;
+  background:#7f7f7f;
+  @include win95-border-outset()
+  border-bottom-width:0px;
+  color:white;
+  font-weight:bold;
+}
+
+.column-header__wrapper.active {
+  background:$win95-window-header;
+}
+
+.column-header__wrapper.active::before {
+  display:none;
+}
+.column-header.active {
+  box-shadow:unset;
+  background:$win95-window-header;
+}
+
+.column-header.active .column-header__icon {
+  color:white;
+}
+
+.column-header__button {
+  background: $win95-bg;
+  color: black;
+  line-height:0px;
+  font-size:14px;
+  max-height:20px;
+  padding:0px 2px;
+  margin-top:2px;
+  @include win95-outset()
+}
+
+.column-header__button.active, .column-header__button.active:hover {
+  @include win95-inset();
+  background-color:#7f7f7f;
+}
+
+.column-header__back-button {
+  background: $win95-bg;
+  color: black;
+  padding:2px;
+  max-height:20px;
+  margin-top:2px;
+  @include win95-outset()
+  font-size:13px;
+  font-weight:bold;
+}
+
+.column-back-button {
+  background:$win95-bg;
+  color:black;
+  @include win95-outset()
+  padding:2px;
+  font-size:13px;
+  font-weight:bold;
+}
+
+.column-back-button--slim-button {
+  position:absolute;
+  top:-22px;
+  right:4px;
+  max-height:20px;
+  max-width:60px;
+  padding:0px 2px;
+}
+
+.column-back-button__icon {
+  font-size:11px;
+  margin-top:-3px;
+}
+
+.column-header__collapsible {
+  border-left:2px outset $win95-bg;
+  border-right:2px outset $win95-bg;
+}
+
+.column-header__collapsible-inner {
+  background:$win95-bg;
+  color:black;
+}
+
+.column-header__collapsible__extra {
+  color:black;
+}
+
+.column-header__collapsible__extra div[role="group"] {
+  border: 2px groove $win95-bg;
+  border-radius:4px;
+  margin-bottom:8px;
+  padding:4px;
+}
+
+.column-settings__section {
+  color:black;
+  font-weight:bold;
+  font-size:11px;
+  position:relative;
+  top: -12px;
+  left:4px;
+  background-color:$win95-bg;
+  display:inline-block;
+  padding:0px 4px;
+  margin-bottom:0px;
+}
+
+.setting-meta__label, .setting-toggle__label {
+  color:black;
+  font-weight:normal;
+}
+
+.setting-meta__label span:before {
+  content:"(";
+}
+.setting-meta__label span:after {
+  content:")";
+}
+
+.setting-toggle {
+  line-height:13px;
+}
+
+.react-toggle .react-toggle-track {
+  border-radius:0px;
+  background-color:white;
+  @include win95-border-inset();
+
+  width:12px;
+  height:12px;
+}
+
+.react-toggle:hover:not(.react-toggle--disabled) .react-toggle-track {
+  background-color:white;
+}
+
+.react-toggle .react-toggle-track-check {
+  left:2px;
+  transition:unset;
+}
+
+.react-toggle .react-toggle-track-check svg path {
+  fill: black;
+}
+
+.react-toggle .react-toggle-track-x {
+  display:none;
+}
+
+.react-toggle .react-toggle-thumb {
+  border-radius:0px;
+  display:none;
+}
+
+.text-btn {
+  background-color:$win95-bg;
+  @include win95-outset()
+  padding:4px;
+}
+
+.text-btn:hover {
+  text-decoration:none;
+  color:black;
+}
+
+.text-btn:active {
+  @include win95-inset();
+}
+
+.setting-text {
+  color:black;
+  background-color:white;
+  @include win95-inset();
+  font-size:13px;
+  padding:2px;
+}
+
+.setting-text:active, .setting-text:focus,
+.setting-text.light:active, .setting-text.light:focus {
+  color:black;
+  border-bottom:2px inset $win95-bg;
+}
+
+.column-header__setting-arrows .column-header__setting-btn {
+  padding:3px 10px;
+}
+
+.column-header__setting-arrows .column-header__setting-btn:last-child {
+  padding:3px 10px;
+}
+
+.missing-indicator {
+  background-color:$win95-bg;
+  color:black;
+  @include win95-outset()
+}
+
+.missing-indicator > div {
+  background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAARCAYAAAA7bUf6AAAACXBIWXMAAC4jAAAuIwF4pT92AAAAF3pUWHRUaXRsZQAACJnLyy9Jyy/NSwEAD5IDblIFOhoAAAAXelRYdEF1dGhvcgAACJlLzijKz0vMAQALmgLoDsFj8gAAAQpJREFUOMuVlD0OwjAMhd2oQl04Axfo0IGBgYELcAY6cqQuSO0ZOEAZGBg6VKg74gwsEaoESRVHjusI8aQqzY8/PbtOEz1qkFSn2YevlaNOpLMJh2DwvixhuXtOa6/LCh51DUMEFkAsgAZD207Doin8mQ562JpRE5CHBAAhmIqD1L8AqzUUUJkxc6kr3AgAJ+NuvIWRdk7WcrKl0AUqcIBBHOiEbpS4m27mIL5Onfg3k0rgggeQuS2sDOGSahKR+glgqaGLgUJs951NN1q9D72cQqQWR9cr3sm9YcEssEuz6eEuZh2bu0aSOhQ1MBezu2O/+TVSvEFII3qLsZWrSA2AAUQIh1HpyP/kC++zjVSMj6ntAAAAAElFTkSuQmCC')
+    no-repeat;
+  background-position:center center;
+}
+
+.empty-column-indicator,
+.error-column {
+  background: $win95-bg;
+  color: black;
+}
+
+.status__wrapper {
+  border: 2px groove $win95-bg;
+  margin:4px;
+}
+
+.status {
+  @include win95-border-slight-inset();
+  background-color:white;
+  margin:4px;
+  padding-bottom:40px;
+  margin-bottom:8px;
+}
+
+.status.status-direct {
+  background-color:$win95-bg;
+}
+
+.status__content {
+  font-size:13px;
+}
+
+.status.light .status__relative-time,
+.status.light .display-name span {
+  color: #7f7f7f;
+}
+
+.status__action-bar {
+  box-sizing:border-box;
+  position:absolute;
+  bottom:-1px;
+  left:-1px;
+  background:$win95-bg;
+  width:calc(100% + 2px);
+  padding-left:10px;
+  padding: 4px 2px;
+  padding-bottom:4px;
+  border-bottom:2px groove $win95-bg;
+  border-top:1px outset $win95-bg;
+  text-align: right;
+}
+
+.status__wrapper .status__action-bar {
+  border-bottom-width:0px;
+}
+
+.status__action-bar-button {
+  float:right;
+}
+
+.status__action-bar-dropdown {
+  margin-left:auto;
+  margin-right:10px;
+
+  .icon-button {
+    min-width:28px;
+  }
+}
+.status.light .status__content a {
+  color:blue;
+}
+
+.focusable:focus {
+  background: $win95-bg;
+  .detailed-status__action-bar {
+    background: $win95-bg;
+  }
+
+  .status, .detailed-status {
+    background: white;
+    outline:2px dotted $win95-mid-grey;
+  }
+}
+
+.dropdown__trigger.icon-button {
+  padding-right:6px;
+}
+
+.detailed-status__action-bar-dropdown .icon-button {
+  min-width:28px;
+}
+
+.detailed-status {
+  background:white;
+  background-clip:padding-box;
+  margin:4px;
+  border: 2px groove $win95-bg;
+  padding:4px;
+}
+
+.detailed-status__display-name {
+  color:#7f7f7f;
+}
+
+.detailed-status__display-name strong {
+  color:black;
+  font-weight:bold;
+}
+.account__avatar,
+.account__avatar-overlay-base,
+.account__header__avatar,
+.account__avatar-overlay-overlay {
+  @include win95-border-slight-inset();
+  clip-path:none;
+  filter: saturate(1.8) brightness(1.1);
+}
+
+.detailed-status__action-bar {
+  background-color:$win95-bg;
+  border:0px;
+  border-bottom:2px groove $win95-bg;
+  margin-bottom:8px;
+  justify-items:left;
+  padding-left:4px;
+}
+.icon-button {
+  background:$win95-bg;
+  @include win95-border-outset()
+  padding:0px 0px 0px 0px;
+  margin-right:4px;
+
+  color:#3f3f3f;
+  &.inverted, &:hover, &.inverted:hover, &:active, &:focus {
+    color:#3f3f3f;
+  }
+}
+
+.icon-button:active {
+  @include win95-border-inset();
+}
+
+.status__action-bar > .icon-button {
+  padding:0px 15px 0px 0px;
+  min-width:25px;
+}
+
+.icon-button.star-icon,
+.icon-button.star-icon:active {
+  background:transparent;
+  border:none;
+}
+
+.icon-button.star-icon.active {
+  color: $gold-star;
+  &:active,  &:hover, &:focus {
+    color: $gold-star;
+  }
+}
+
+.icon-button.star-icon > i {
+  background:$win95-bg;
+  @include win95-border-outset()
+  padding-bottom:3px;
+}
+
+.icon-button.star-icon:active > i {
+  @include win95-border-inset();
+}
+
+.text-icon-button {
+  color:$win95-dark-grey;
+}
+
+.detailed-status__action-bar-dropdown {
+  margin-left:auto;
+  justify-content:right;
+  padding-right:16px;
+}
+
+.detailed-status__button {
+  flex:0 0 auto;
+}
+
+.detailed-status__button .icon-button {
+  padding-left:2px;
+  padding-right:25px;
+}
+
+.status-card {
+  border-radius:0px;
+  background:white;
+  border: 1px solid black;
+  color:black;
+}
+
+.status-card:hover {
+  background-color:white;
+}
+
+.status-card__title {
+  color:blue;
+  text-decoration:underline;
+  font-weight:bold;
+}
+
+.load-more {
+  width:auto;
+  margin:5px auto;
+  background: $win95-bg;
+  @include win95-outset();
+  color:black;
+  padding: 2px 5px;
+
+  &:hover {
+    background: $win95-bg;
+    color:black;
+  }
+}
+
+.status-card__description {
+ color:black;
+}
+
+.account__display-name strong, .status__display-name strong {
+  color:black;
+  font-weight:bold;
+}
+
+.account .account__display-name {
+  color:black;
+}
+
+.account {
+  border-bottom: 2px groove $win95-bg;
+}
+
+.reply-indicator__content .status__content__spoiler-link, .status__content .status__content__spoiler-link {
+  background:$win95-bg;
+  @include win95-outset()
+}
+
+.reply-indicator__content .status__content__spoiler-link:hover, .status__content .status__content__spoiler-link:hover {
+  background:$win95-bg;
+}
+
+.reply-indicator__content .status__content__spoiler-link:active, .status__content .status__content__spoiler-link:active {
+  @include win95-inset();
+}
+
+.reply-indicator__content a, .status__content a {
+  color:blue;
+}
+
+.notification {
+  border: 2px groove $win95-bg;
+  margin:4px;
+}
+
+.notification__message {
+  color:black;
+  font-size:13px;
+}
+
+.notification__display-name {
+  font-weight:bold;
+}
+
+
+.drawer__header {
+  background: $win95-bg;
+  @include win95-border-outset()
+  justify-content:left;
+  margin-bottom:0px;
+  padding-bottom:2px;
+  border-bottom:2px groove $win95-bg;
+}
+
+.drawer__tab {
+  color:black;
+  @include win95-outset()
+  padding:5px;
+  margin:2px;
+  flex: 0 0 auto;
+}
+
+.drawer__tab:first-child::before {
+  content:"Start";
+  color:black;
+  font-weight:bold;
+  font-size:15px;
+  width:80%;
+  display:block;
+  position:absolute;
+  right:0px;
+
+}
+
+.drawer__tab:first-child {
+  position:relative;
+  padding:5px 15px;
+  width:40px;
+  font-size:0px;
+  color:$win95-bg;
+
+  background-image: url("~images/start.png");
+  background-repeat:no-repeat;
+  background-position:8%;
+  background-clip:padding-box;
+  background-size:auto 50%;
+}
+
+.drawer__header a:hover {
+  background-color:transparent;
+}
+
+.drawer__header a:first-child:hover {
+  background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAOCAIAAACpTQvdAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAF3pUWHRBdXRob3IAAAiZS84oys9LzAEAC5oC6A7BY/IAAACWSURBVCiRhVJJDsQgDEuqOfRZ7a1P5gbP4uaJaEjTADMWQhHYjlk4p0wLnNdptdF4KvBUDyGzVwc2xO+uKtH+1o0ytEEmqFpuxlvFCGCxKbNIT56QCi2MzaA/2Mz+mERSOeqzJG2RUxkjdTabgPtFoZ1bZxcKvgPcLZVufAyR9Ni8v5dWDzfFx0giC1RvZFv6l35QQ/Mvv39XXgGzQpoAAAAASUVORK5CYII=");
+  background-repeat:no-repeat;
+  background-position:8%;
+  background-clip:padding-box;
+  background-size:auto 50%;
+  transition:unset;
+}
+
+.drawer__tab:first-child {
+
+}
+
+.search {
+  background:$win95-bg;
+  padding-top:2px;
+  padding:2px;
+  border:2px outset $win95-bg;
+  border-top-width:0px;
+  border-bottom: 2px groove $win95-bg;
+  margin-bottom:0px;
+}
+
+.search input {
+  background-color:white;
+  color:black;
+  @include win95-border-slight-inset();
+}
+
+.search__input:focus {
+  background-color:white;
+}
+
+.search-popout {
+  box-shadow: unset;
+  color:black;
+  border-radius:0px;
+  background-color:$win95-tooltip-yellow;
+  border:1px solid black;
+
+  h4 {
+    color:black;
+    text-transform: none;
+    font-weight:bold;
+  }
+}
+
+.search-results__header {
+  background-color: $win95-bg;
+  color:black;
+  border-bottom:2px groove $win95-bg;
+}
+
+.search-results__hashtag {
+  color:blue;
+}
+
+.search-results__section .account:hover,
+.search-results__section .account:hover .account__display-name,
+.search-results__section .account:hover .account__display-name strong,
+.search-results__section .search-results__hashtag:hover {
+  background-color:$win95-window-header;
+  color:white;
+}
+
+.search__icon .fa {
+  color:#808080;
+
+  &.active {
+    opacity:1.0;
+  }
+
+  &:hover {
+    color: #808080;
+  }
+}
+
+.drawer__inner,
+.drawer__inner.darker {
+  background-color:$win95-bg;
+  border: 2px outset $win95-bg;
+  border-top-width:0px;
+}
+
+.navigation-bar {
+  color:black;
+}
+
+.navigation-bar strong {
+  color:black;
+  font-weight:bold;
+}
+
+.autosuggest-textarea__textarea, .spoiler-input__input {
+  border-radius:0px;
+  @include win95-border-slight-inset();
+}
+
+.autosuggest-textarea__textarea {
+  border-bottom:0px;
+}
+
+.compose-form__uploads-wrapper {
+  border-radius:0px;
+  border-bottom:1px inset $win95-bg;
+  border-top-width:0px;
+}
+
+.compose-form__upload-wrapper {
+  border-left:1px inset $win95-bg;
+  border-right:1px inset $win95-bg;
+}
+
+.compose-form__buttons {
+  background-color:$win95-bg;
+  border-radius:0px;
+  box-shadow:unset;
+  border:2px groove $win95-bg;
+  margin-top:4px;
+  padding:4px 8px;
+}
+
+.privacy-dropdown.active
+.privacy-dropdown__value {
+  background: $win95-bg;
+  box-shadow:unset;
+}
+
+.privacy-dropdown__option.active, .privacy-dropdown__option:hover,
+.privacy-dropdown__option.active:hover {
+  background:$win95-window-header;
+}
+
+.privacy-dropdown.active .privacy-dropdown__dropdown {
+  box-shadow:unset;
+  color:black;
+  @include win95-outset()
+  background: $win95-bg;
+}
+
+.privacy-dropdown__option__content {
+  color:black;
+}
+
+.privacy-dropdown__option__content strong {
+  font-weight:bold;
+}
+
+.compose-form__warning::before {
+  content:"Tip:";
+  font-weight:bold;
+  display:block;
+  position:absolute;
+  top:-10px;
+  background-color:$win95-bg;
+  font-size:11px;
+  padding: 0px 5px;
+}
+
+.compose-form__warning {
+  position:relative;
+  box-shadow:unset;
+  border:2px groove $win95-bg;
+  background-color:$win95-bg;
+  color:black;
+}
+
+.compose-form__warning a {
+  color:blue;
+}
+
+.compose-form__warning strong {
+  color:black;
+  text-decoration:underline;
+}
+
+.compose-form__buttons button.active:last-child {
+  @include win95-border-inset();
+  background: #dfdfdf;
+  color:#7f7f7f;
+}
+
+.compose-form__upload-thumbnail {
+  border-radius:0px;
+  border:2px groove $win95-bg;
+  background-color:$win95-bg;
+  padding:2px;
+  box-sizing:border-box;
+}
+
+.compose-form__upload-thumbnail .icon-button {
+  max-width:20px;
+  max-height:20px;
+  line-height:10px !important;
+}
+
+.compose-form__upload-thumbnail .icon-button::before {
+  content:"X";
+  font-size:13px;
+  font-weight:bold;
+  color:black;
+}
+
+.compose-form__upload-thumbnail .icon-button i {
+  display:none;
+}
+
+.emoji-dialog.with-search {
+  box-shadow:unset;
+  border-radius:0px;
+  background-color:$win95-bg;
+  border:1px solid black;
+  box-sizing:content-box;
+
+}
+
+.emoji-dialog .emoji-search {
+  color:black;
+  background-color:white;
+  border-radius:0px;
+  @include win95-inset();
+}
+
+.emoji-dialog .emoji-search-wrapper {
+  border-bottom:2px groove $win95-bg;
+}
+
+.emoji-dialog .emoji-category-title {
+  color:black;
+  font-weight:bold;
+}
+
+.reply-indicator {
+  background-color:$win95-bg;
+  border-radius:3px;
+  border:2px groove $win95-bg;
+}
+
+.button {
+  background-color:$win95-bg;
+  @include win95-outset()
+  border-radius:0px;
+  color:black;
+  font-weight:bold;
+
+  &:hover, &:focus, &:disabled {
+    background-color:$win95-bg;
+  }
+
+  &:active {
+    @include win95-inset();
+  }
+
+  &:disabled {
+    color: #808080;
+    text-shadow: 1px 1px 0px #efefef;
+
+    &:active {
+      @include win95-outset();
+    }
+  }
+
+}
+
+#Getting-started {
+  background-color:$win95-bg;
+  @include win95-inset();
+  border-bottom-width:0px;
+}
+
+#Getting-started::before {
+  content:"Start";
+  color:black;
+  font-weight:bold;
+  font-size:15px;
+  width:80%;
+  text-align:center;
+  display:block;
+  position:absolute;
+  right:2px;
+}
+
+#Getting-started {
+  position:relative;
+  padding:5px 15px;
+  width:60px;
+  font-size:0px;
+  color:$win95-bg;
+
+  background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAOCAIAAACpTQvdAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAF3pUWHRBdXRob3IAAAiZS84oys9LzAEAC5oC6A7BY/IAAACWSURBVCiRhVJJDsQgDEuqOfRZ7a1P5gbP4uaJaEjTADMWQhHYjlk4p0wLnNdptdF4KvBUDyGzVwc2xO+uKtH+1o0ytEEmqFpuxlvFCGCxKbNIT56QCi2MzaA/2Mz+mERSOeqzJG2RUxkjdTabgPtFoZ1bZxcKvgPcLZVufAyR9Ni8v5dWDzfFx0giC1RvZFv6l35QQ/Mvv39XXgGzQpoAAAAASUVORK5CYII=");
+  background-repeat:no-repeat;
+  background-position:8%;
+  background-clip:padding-box;
+  background-size:auto 50%;
+}
+
+.column-subheading {
+  background-color:$win95-bg;
+  color:black;
+  font-size:0px;
+  border-bottom: 2px groove $win95-bg;
+  padding:0px;
+  margin:0px;
+}
+
+.column-link {
+  background-color:transparent;
+  background-size:32px 32px;
+  background-repeat:no-repeat;
+  background-position: 36px 50%;
+  color:black;
+  padding-left:40px;
+
+  &:hover {
+    background-color: $win95-window-header;
+    background-size:32px 32px;
+    background-repeat:no-repeat;
+    background-position: 36px 50%;
+    color:white;
+  }
+
+  i {
+    font-size: 0px;
+    width:32px;
+  }
+}
+
+.column-link[href="/web/timelines/public"] {
+  background-image: url("~images/icon_public.png");
+  &:hover { background-image: url("~images/icon_public.png"); }
+}
+.column-link[href="/web/timelines/public/local"] {
+  background-image: url("~images/icon_local.png");
+  &:hover { background-image: url("~images/icon_local.png"); }
+}
+.column-link[href="/web/pinned"] {
+  background-image: url("~images/icon_pin.png");
+  &:hover { background-image: url("~images/icon_pin.png"); }
+}
+.column-link[href="/web/favourites"] {
+  background-image: url("~images/icon_likes.png");
+  &:hover { background-image: url("~images/icon_likes.png"); }
+}
+.column-link[href="/web/blocks"] {
+  background-image: url("~images/icon_blocks.png");
+  &:hover { background-image: url("~images/icon_blocks.png"); }
+}
+.column-link[href="/web/mutes"] {
+  background-image: url("~images/icon_mutes.png");
+  &:hover { background-image: url("~images/icon_mutes.png"); }
+}
+.column-link[href="/settings/preferences"] {
+  background-image: url("~images/icon_settings.png");
+  &:hover { background-image: url("~images/icon_settings.png"); }
+}
+.column-link[href="/about/more"] {
+  background-image: url("~images/icon_about.png");
+  &:hover { background-image: url("~images/icon_about.png"); }
+}
+.column-link[href="/auth/sign_out"] {
+  background-image: url("~images/icon_logout.png");
+  &:hover { background-image: url("~images/icon_logout.png"); }
+}
+
+.getting-started__footer {
+  display:none;
+}
+
+.getting-started__wrapper::before {
+  content:"Mastodon 95";
+  font-weight:bold;
+  font-size:23px;
+  color:white;
+  line-height:30px;
+  padding-left:20px;
+  padding-right:40px;
+
+  left:0px;
+  bottom:-30px;
+  display:block;
+  position:absolute;
+  background-color:#7f7f7f;
+  width:200%;
+  height:30px;
+
+  -ms-transform: rotate(-90deg);
+
+  -webkit-transform: rotate(-90deg);
+  transform: rotate(-90deg);
+  transform-origin:top left;
+}
+
+.getting-started__wrapper {
+  @include win95-border-outset()
+  background-color:$win95-bg;
+}
+
+.account__header {
+  background-color:#7f7f7f;
+}
+
+.account__header .account__header__content {
+  color:white;
+}
+
+.account__action-bar__tab > span {
+  color:black;
+  font-weight:bold;
+}
+
+.account__action-bar__tab strong {
+  color:black;
+}
+
+.account__action-bar {
+  border: unset;
+}
+
+.account__action-bar__tab {
+  border: 1px outset $win95-bg;
+}
+
+.account__action-bar__tab:active {
+  @include win95-inset();
+}
+
+.dropdown--active .dropdown__content > ul,
+.dropdown-menu {
+  background:$win95-tooltip-yellow;
+  border-radius:0px;
+  border:1px solid black;
+  box-shadow:unset;
+}
+
+.dropdown-menu a {
+  background-color:transparent;
+}
+
+.dropdown--active::after {
+  display:none;
+}
+
+.dropdown--active .icon-button {
+  color:black;
+  @include win95-inset();
+}
+
+.dropdown--active .dropdown__content > ul > li > a {
+  background:transparent;
+}
+
+.dropdown--active .dropdown__content > ul > li > a:hover {
+  background:transparent;
+  color:black;
+  text-decoration:underline;
+}
+
+.dropdown__sep,
+.dropdown-menu__separator
+{
+  border-color:#7f7f7f;
+}
+
+.detailed-status__action-bar-dropdown .dropdown--active .dropdown__content.dropdown__left {
+  left:unset;
+}
+
+.dropdown > .icon-button, .detailed-status__button > .icon-button,
+.status__action-bar > .icon-button, .star-icon i {
+    /* i don't know what's going on with the inline
+       styles someone should look at the react code */
+    height: 25px !important;
+    width: 28px !important;
+    box-sizing: border-box;
+}
+
+.status__action-bar-button .fa-floppy-o {
+    padding-top: 2px;
+}
+
+.status__action-bar-dropdown {
+    position: relative;
+    top: -3px;
+}
+
+.detailed-status__action-bar-dropdown .dropdown {
+    position: relative;
+    top: -4px;
+}
+
+.notification .status__action-bar {
+    border-bottom: none;
+}
+
+.notification .status {
+    margin-bottom: 4px;
+}
+
+.status__wrapper .status {
+    margin-bottom: 3px;
+}
+
+.status__wrapper {
+    margin-bottom: 8px;
+}
+
+.icon-button .fa-retweet {
+    position: relative;
+    top: -1px;
+}
+
+.embed-modal, .error-modal, .onboarding-modal,
+.actions-modal, .boost-modal, .confirmation-modal, .report-modal {
+  @include win95-outset()
+  background:$win95-bg;
+}
+
+.actions-modal::before,
+.boost-modal::before,
+.confirmation-modal::before,
+.report-modal::before {
+  content: "Confirmation";
+  display:block;
+  background:$win95-window-header;
+  color:white;
+  font-weight:bold;
+  padding-left:2px;
+}
+
+.boost-modal::before {
+  content: "Boost confirmation";
+}
+
+.boost-modal__action-bar > div > span:before {
+  content: "Tip: ";
+  font-weight:bold;
+}
+
+.boost-modal__action-bar, .confirmation-modal__action-bar, .report-modal__action-bar {
+  background:$win95-bg;
+  margin-top:-15px;
+}
+
+.embed-modal h4, .error-modal h4, .onboarding-modal h4 {
+  background:$win95-window-header;
+  color:white;
+  font-weight:bold;
+  padding:2px;
+  font-size:13px;
+  text-align:left;
+}
+
+.confirmation-modal__action-bar {
+  .confirmation-modal__cancel-button {
+    color:black;
+
+    &:active,
+    &:focus,
+    &:hover {
+      color:black;
+    }
+
+    &:active {
+      @include win95-inset();
+    }
+  }
+}
+
+.embed-modal .embed-modal__container .embed-modal__html,
+.embed-modal .embed-modal__container .embed-modal__html:focus {
+  background:white;
+  color:black;
+  @include win95-inset();
+}
+
+.modal-root__overlay,
+.account__header > div {
+  background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAFnpUWHRUaXRsZQAACJnLzU9JzElKBwALgwLXaCRlPwAAABd6VFh0QXV0aG9yAAAImUvOKMrPS8wBAAuaAugOwWPyAAAAEUlEQVQImWNgYGD4z4AE/gMADwMB/414xEUAAAAASUVORK5CYII=');
+}
+
+
+.admin-wrapper::before {
+  position:absolute;
+  top:0px;
+  content:"Control Panel";
+  color:white;
+  background-color:$win95-window-header;
+  font-size:13px;
+  font-weight:bold;
+  width:calc(100%);
+  margin: 2px;
+  display:block;
+  padding:2px;
+  padding-left:22px;
+  box-sizing:border-box;
+}
+
+.admin-wrapper {
+  position:relative;
+  background: $win95-bg;
+  @include win95-outset()
+  width:70vw;
+  height:80vh;
+  margin:10vh auto;
+  color: black;
+  padding-top:24px;
+  flex-direction:column;
+  overflow:hidden;
+}
+
+@media screen and (max-width: 1120px) {
+  .admin-wrapper {
+    width:90vw;
+    height:95vh;
+    margin:2.5vh auto;
+  }
+}
+
+@media screen and (max-width: 740px) {
+  .admin-wrapper {
+    width:100vw;
+    height:95vh;
+    height:calc(100vh - 24px);
+    margin:0px 0px 0px 0px;
+  }
+}
+
+.admin-wrapper .sidebar-wrapper {
+  position:static;
+  height:auto;
+  flex: 0 0 auto;
+  margin:2px;
+}
+
+.admin-wrapper .content-wrapper {
+  flex: 1 1 auto;
+  width:calc(100% - 20px);
+  @include win95-border-outset()
+  position:relative;
+  margin-left:10px;
+  margin-right:10px;
+  margin-bottom:40px;
+  box-sizing:border-box;
+}
+
+.admin-wrapper .content {
+  background-color: $win95-bg;
+  width: 100%;
+  max-width:100%;
+  min-height:100%;
+  box-sizing:border-box;
+  position:relative;
+}
+
+.admin-wrapper .sidebar {
+  position:static;
+  background: $win95-bg;
+  color:black;
+  width: 100%;
+  height:auto;
+  padding-bottom: 20px;
+}
+
+.admin-wrapper .sidebar .logo {
+  position:absolute;
+  top:2px;
+  left:4px;
+  width:18px;
+  height:18px;
+  margin:0px;
+}
+
+.admin-wrapper .sidebar > ul {
+  background: $win95-bg;
+  margin:0px;
+  margin-left:8px;
+  color:black;
+
+  & > li {
+    display:inline-block;
+
+    &#settings,
+    &#admin {
+      padding:2px;
+      border: 0px solid transparent;
+    }
+
+    &#logout {
+      position:absolute;
+      @include win95-outset();
+      right:12px;
+      bottom:10px;
+    }
+
+    &#web {
+      display:inline-block;
+      @include win95-outset();
+      position:absolute;
+      left: 12px;
+      bottom: 10px;
+    }
+
+    & > a {
+      display:inline-block;
+      @include win95-tab();
+      padding:2px 5px;
+      margin:0px;
+      color:black;
+      vertical-align:baseline;
+
+      &.selected {
+        background: $win95-bg;
+        color:black;
+        padding-top: 4px;
+        padding-bottom:4px;
+      }
+
+      &:hover {
+        background: $win95-bg;
+        color:black;
+      }
+    }
+
+    & > ul {
+      width:calc(100% - 20px);
+      background: transparent;
+      position:absolute;
+      left: 10px;
+      top:54px;
+      z-index:3;
+
+      & > li {
+        background: $win95-bg;
+        display: inline-block;
+        vertical-align:baseline;
+
+        & > a {
+          background: $win95-bg;
+          @include win95-tab();
+          color:black;
+          padding:2px 5px;
+          position:relative;
+          z-index:3;
+
+          &.selected {
+            background: $win95-bg;
+            color:black;
+            padding-bottom:4px;
+            padding-top: 4px;
+            padding-right:7px;
+            margin-left:-2px;
+            margin-right:-2px;
+            position:relative;
+            z-index:4;
+
+            &:first-child {
+              margin-left:0px;
+            }
+
+            &:hover {
+              background: transparent;
+              color:black;
+            }
+          }
+
+          &:hover {
+            background: $win95-bg;
+            color:black;
+          }
+        }
+      }
+    }
+  }
+}
+
+@media screen and (max-width: 1520px) {
+  .admin-wrapper .sidebar > ul > li > ul {
+    max-width:1000px;
+  }
+
+  .admin-wrapper .sidebar {
+    padding-bottom: 45px;
+  }
+}
+
+@media screen and (max-width: 600px) {
+  .admin-wrapper .sidebar > ul > li > ul {
+    max-width:500px;
+  }
+
+  .admin-wrapper {
+    .sidebar {
+      padding:0px;
+      padding-bottom: 70px;
+      width: 100%;
+      height: auto;
+    }
+    .content-wrapper {
+      overflow:auto;
+      height:80%;
+      height:calc(100% - 150px);
+    }
+  }
+}
+
+.flash-message {
+  background-color:$win95-tooltip-yellow;
+  color:black;
+  border:1px solid black;
+  border-radius:0px;
+  position:absolute;
+  top:0px;
+  left:0px;
+  width:100%;
+}
+
+.admin-wrapper table {
+  background-color: white;
+  @include win95-border-slight-inset();
+}
+
+.admin-wrapper .content h2,
+.simple_form .input.with_label .label_input > label,
+.admin-wrapper .content h6,
+.admin-wrapper .content > p,
+.admin-wrapper .content .muted-hint,
+.simple_form span.hint,
+.simple_form h4,
+.simple_form .check_boxes .checkbox label,
+.simple_form .input.with_label.boolean .label_input > label,
+.filters .filter-subset a,
+.simple_form .input.radio_buttons .radio label,
+a.table-action-link,
+a.table-action-link:hover,
+.simple_form .input.with_block_label > label,
+.simple_form p.hint {
+  color:black;
+}
+
+.table > tbody > tr:nth-child(2n+1) > td,
+.table > tbody > tr:nth-child(2n+1) > th {
+  background-color:white;
+}
+
+.simple_form input[type=text],
+.simple_form input[type=number],
+.simple_form input[type=email],
+.simple_form input[type=password],
+.simple_form textarea {
+  color:black;
+  background-color:white;
+  @include win95-border-slight-inset();
+
+  &:active, &:focus {
+    background-color:white;
+  }
+}
+
+.simple_form button,
+.simple_form .button,
+.simple_form .block-button
+{
+  background: $win95-bg;
+  @include win95-outset()
+  color:black;
+  font-weight: normal;
+
+  &:hover {
+    background: $win95-bg;
+  }
+}
+
+.simple_form .warning, .table-form .warning
+{
+  background: $win95-tooltip-yellow;
+  color:black;
+  box-shadow: unset;
+  text-shadow:unset;
+  border:1px solid black;
+
+  a {
+    color: blue;
+    text-decoration:underline;
+  }
+}
+
+.simple_form button.negative,
+.simple_form .button.negative,
+.simple_form .block-button.negative
+{
+  background: $win95-bg;
+}
+
+.filters .filter-subset  {
+  border: 2px groove $win95-bg;
+  padding:2px;
+}
+
+.filters .filter-subset a::before {
+  content: "";
+  background-color:white;
+  border-radius:50%;
+  border:2px solid black;
+  border-top-color:#7f7f7f;
+  border-left-color:#7f7f7f;
+  border-bottom-color:#f5f5f5;
+  border-right-color:#f5f5f5;
+  width:12px;
+  height:12px;
+  display:inline-block;
+  vertical-align:middle;
+  margin-right:2px;
+}
+
+.filters .filter-subset a.selected::before {
+  background-color:black;
+  box-shadow: inset 0 0 0 3px white;
+}
+
+.filters .filter-subset a,
+.filters .filter-subset a:hover,
+.filters .filter-subset a.selected {
+  color:black;
+  border-bottom: 0px solid transparent;
+}
+
diff --git a/app/javascript/themes/glitch/features/generic_not_found/index.js b/app/javascript/themes/glitch/features/generic_not_found/index.js
deleted file mode 100644
index ccd2b87b2..000000000
--- a/app/javascript/themes/glitch/features/generic_not_found/index.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import React from 'react';
-import Column from 'themes/glitch/features/ui/components/column';
-import MissingIndicator from 'themes/glitch/components/missing_indicator';
-
-const GenericNotFound = () => (
-  <Column>
-    <MissingIndicator />
-  </Column>
-);
-
-export default GenericNotFound;
diff --git a/app/javascript/themes/glitch/features/local_settings/navigation/item/style.scss b/app/javascript/themes/glitch/features/local_settings/navigation/item/style.scss
deleted file mode 100644
index 7f7371993..000000000
--- a/app/javascript/themes/glitch/features/local_settings/navigation/item/style.scss
+++ /dev/null
@@ -1,27 +0,0 @@
-@import 'styles/mastodon/variables';
-
-.glitch.local-settings__navigation__item {
-  display: block;
-  padding: 15px 20px;
-  color: inherit;
-  background: $primary-text-color;
-  border-bottom: 1px $ui-primary-color solid;
-  cursor: pointer;
-  text-decoration: none;
-  outline: none;
-  transition: background .3s;
-
-  &: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;
-  }
-}
diff --git a/app/javascript/themes/glitch/features/local_settings/navigation/style.scss b/app/javascript/themes/glitch/features/local_settings/navigation/style.scss
deleted file mode 100644
index 0336f943b..000000000
--- a/app/javascript/themes/glitch/features/local_settings/navigation/style.scss
+++ /dev/null
@@ -1,10 +0,0 @@
-@import 'styles/mastodon/variables';
-
-.glitch.local-settings__navigation {
-  background: $primary-text-color;
-  color: $ui-base-color;
-  width: 200px;
-  font-size: 15px;
-  line-height: 20px;
-  overflow-y: auto;
-}
diff --git a/app/javascript/themes/glitch/features/local_settings/page/item/style.scss b/app/javascript/themes/glitch/features/local_settings/page/item/style.scss
deleted file mode 100644
index b2d8f7185..000000000
--- a/app/javascript/themes/glitch/features/local_settings/page/item/style.scss
+++ /dev/null
@@ -1,7 +0,0 @@
-@import 'styles/mastodon/variables';
-
-.glitch.local-settings__page__item {
-  select {
-    margin-bottom: 5px;
-  }
-}
diff --git a/app/javascript/themes/glitch/features/local_settings/page/style.scss b/app/javascript/themes/glitch/features/local_settings/page/style.scss
deleted file mode 100644
index e9eedcad0..000000000
--- a/app/javascript/themes/glitch/features/local_settings/page/style.scss
+++ /dev/null
@@ -1,9 +0,0 @@
-@import 'styles/mastodon/variables';
-
-.glitch.local-settings__page {
-  display: block;
-  flex: auto;
-  padding: 15px 20px 15px 20px;
-  width: 360px;
-  overflow-y: auto;
-}
diff --git a/app/javascript/themes/glitch/features/local_settings/style.scss b/app/javascript/themes/glitch/features/local_settings/style.scss
deleted file mode 100644
index 765294607..000000000
--- a/app/javascript/themes/glitch/features/local_settings/style.scss
+++ /dev/null
@@ -1,34 +0,0 @@
-@import 'styles/mastodon/variables';
-
-.glitch.local-settings {
-  position: relative;
-  display: flex;
-  flex-direction: row;
-  background: $ui-secondary-color;
-  color: $ui-base-color;
-  border-radius: 8px;
-  height: 80vh;
-  width: 80vw;
-  max-width: 740px;
-  max-height: 450px;
-  overflow: hidden;
-
-  label {
-    display: block;
-  }
-
-  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;
-  }
-}
diff --git a/app/javascript/themes/glitch/features/standalone/compose/index.js b/app/javascript/themes/glitch/features/standalone/compose/index.js
deleted file mode 100644
index 8a8118178..000000000
--- a/app/javascript/themes/glitch/features/standalone/compose/index.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import React from 'react';
-import ComposeFormContainer from 'themes/glitch/features/compose/containers/compose_form_container';
-import NotificationsContainer from 'themes/glitch/features/ui/containers/notifications_container';
-import LoadingBarContainer from 'themes/glitch/features/ui/containers/loading_bar_container';
-import ModalContainer from 'themes/glitch/features/ui/containers/modal_container';
-
-export default class Compose extends React.PureComponent {
-
-  render () {
-    return (
-      <div>
-        <ComposeFormContainer />
-        <NotificationsContainer />
-        <ModalContainer />
-        <LoadingBarContainer className='loading-bar' />
-      </div>
-    );
-  }
-
-}
diff --git a/app/javascript/themes/glitch/index.js b/app/javascript/themes/glitch/index.js
deleted file mode 100644
index 407e1f767..000000000
--- a/app/javascript/themes/glitch/index.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import loadPolyfills from './util/load_polyfills';
-
-// import default stylesheet with variables
-require('font-awesome/css/font-awesome.css');
-
-import './styles/index.scss';
-
-require.context('../../images/', true);
-
-loadPolyfills().then(() => {
-  require('./util/main').default();
-}).catch(e => {
-  console.error(e);
-});
diff --git a/app/javascript/themes/glitch/styles/reset copy.scss b/app/javascript/themes/glitch/styles/reset copy.scss
deleted file mode 100644
index cc5ba9d7c..000000000
--- a/app/javascript/themes/glitch/styles/reset copy.scss
+++ /dev/null
@@ -1,91 +0,0 @@
-/* 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;
-}
-
-::-webkit-scrollbar {
-  width: 8px;
-  height: 8px;
-}
-
-::-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/themes/glitch/theme.yml b/app/javascript/themes/glitch/theme.yml
deleted file mode 100644
index 49fba8f40..000000000
--- a/app/javascript/themes/glitch/theme.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-#  (REQUIRED) The location of the pack file inside `pack_directory`.
-pack: index.js
-
-#  (OPTIONAL) The directory which contains the pack file.
-#  Defaults to the theme directory (`app/javascript/themes/[theme]`),
-#  but in the case of the vanilla Mastodon theme the pack file is
-#  somewhere else.
-#    pack_directory: app/javascript/packs
-
-#  (OPTIONAL) Additional javascript resources to preload, for use with
-#  lazy-loaded components. It is **STRONGLY RECOMMENDED** that you
-#  derive these pathnames from `themes/[your-theme]` to ensure that
-#  they stay unique. (Of course, vanilla doesn't do this ^^;;)
-preload:
-- themes/glitch/async/getting_started
-- themes/glitch/async/compose
-- themes/glitch/async/home_timeline
-- themes/glitch/async/notifications
diff --git a/app/javascript/themes/glitch/util/async-components.js b/app/javascript/themes/glitch/util/async-components.js
deleted file mode 100644
index 91e85fed5..000000000
--- a/app/javascript/themes/glitch/util/async-components.js
+++ /dev/null
@@ -1,115 +0,0 @@
-export function EmojiPicker () {
-  return import(/* webpackChunkName: "themes/glitch/async/emoji_picker" */'themes/glitch/util/emoji/emoji_picker');
-}
-
-export function Compose () {
-  return import(/* webpackChunkName: "themes/glitch/async/compose" */'themes/glitch/features/compose');
-}
-
-export function Notifications () {
-  return import(/* webpackChunkName: "themes/glitch/async/notifications" */'themes/glitch/features/notifications');
-}
-
-export function HomeTimeline () {
-  return import(/* webpackChunkName: "themes/glitch/async/home_timeline" */'themes/glitch/features/home_timeline');
-}
-
-export function PublicTimeline () {
-  return import(/* webpackChunkName: "themes/glitch/async/public_timeline" */'themes/glitch/features/public_timeline');
-}
-
-export function CommunityTimeline () {
-  return import(/* webpackChunkName: "themes/glitch/async/community_timeline" */'themes/glitch/features/community_timeline');
-}
-
-export function HashtagTimeline () {
-  return import(/* webpackChunkName: "themes/glitch/async/hashtag_timeline" */'themes/glitch/features/hashtag_timeline');
-}
-
-export function DirectTimeline() {
-  return import(/* webpackChunkName: "themes/glitch/async/direct_timeline" */'themes/glitch/features/direct_timeline');
-}
-
-export function Status () {
-  return import(/* webpackChunkName: "themes/glitch/async/status" */'themes/glitch/features/status');
-}
-
-export function GettingStarted () {
-  return import(/* webpackChunkName: "themes/glitch/async/getting_started" */'themes/glitch/features/getting_started');
-}
-
-export function PinnedStatuses () {
-  return import(/* webpackChunkName: "themes/glitch/async/pinned_statuses" */'themes/glitch/features/pinned_statuses');
-}
-
-export function AccountTimeline () {
-  return import(/* webpackChunkName: "themes/glitch/async/account_timeline" */'themes/glitch/features/account_timeline');
-}
-
-export function AccountGallery () {
-  return import(/* webpackChunkName: "themes/glitch/async/account_gallery" */'themes/glitch/features/account_gallery');
-}
-
-export function Followers () {
-  return import(/* webpackChunkName: "themes/glitch/async/followers" */'themes/glitch/features/followers');
-}
-
-export function Following () {
-  return import(/* webpackChunkName: "themes/glitch/async/following" */'themes/glitch/features/following');
-}
-
-export function Reblogs () {
-  return import(/* webpackChunkName: "themes/glitch/async/reblogs" */'themes/glitch/features/reblogs');
-}
-
-export function Favourites () {
-  return import(/* webpackChunkName: "themes/glitch/async/favourites" */'themes/glitch/features/favourites');
-}
-
-export function FollowRequests () {
-  return import(/* webpackChunkName: "themes/glitch/async/follow_requests" */'themes/glitch/features/follow_requests');
-}
-
-export function GenericNotFound () {
-  return import(/* webpackChunkName: "themes/glitch/async/generic_not_found" */'themes/glitch/features/generic_not_found');
-}
-
-export function FavouritedStatuses () {
-  return import(/* webpackChunkName: "themes/glitch/async/favourited_statuses" */'themes/glitch/features/favourited_statuses');
-}
-
-export function Blocks () {
-  return import(/* webpackChunkName: "themes/glitch/async/blocks" */'themes/glitch/features/blocks');
-}
-
-export function Mutes () {
-  return import(/* webpackChunkName: "themes/glitch/async/mutes" */'themes/glitch/features/mutes');
-}
-
-export function OnboardingModal () {
-  return import(/* webpackChunkName: "themes/glitch/async/onboarding_modal" */'themes/glitch/features/ui/components/onboarding_modal');
-}
-
-export function MuteModal () {
-  return import(/* webpackChunkName: "themes/glitch/async/mute_modal" */'themes/glitch/features/ui/components/mute_modal');
-}
-
-export function ReportModal () {
-  return import(/* webpackChunkName: "themes/glitch/async/report_modal" */'themes/glitch/features/ui/components/report_modal');
-}
-
-export function SettingsModal () {
-  return import(/* webpackChunkName: "themes/glitch/async/settings_modal" */'themes/glitch/features/local_settings');
-}
-
-export function MediaGallery () {
-  return import(/* webpackChunkName: "themes/glitch/async/media_gallery" */'themes/glitch/components/media_gallery');
-}
-
-export function Video () {
-  return import(/* webpackChunkName: "themes/glitch/async/video" */'themes/glitch/features/video');
-}
-
-export function EmbedModal () {
-  return import(/* webpackChunkName: "themes/glitch/async/embed_modal" */'themes/glitch/features/ui/components/embed_modal');
-}
diff --git a/app/javascript/themes/mastodon-go b/app/javascript/themes/mastodon-go
deleted file mode 160000
-Subproject 74c0293e83dbb49ea4f27eea108526df6216d2a
diff --git a/app/javascript/themes/vanilla/theme.yml b/app/javascript/themes/vanilla/theme.yml
deleted file mode 100644
index 0b262cc82..000000000
--- a/app/javascript/themes/vanilla/theme.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-#  (REQUIRED) The location of the pack file inside `pack_directory`.
-pack: application.js
-
-#  (OPTIONAL) The directory which contains the pack file.
-#  Defaults to the theme directory (`app/javascript/themes/[theme]`),
-#  but in the case of the vanilla Mastodon theme the pack file is
-#  somewhere else.
-pack_directory: app/javascript/packs
-
-#  (OPTIONAL) Additional javascript resources to preload, for use with
-#  lazy-loaded components. It is **STRONGLY RECOMMENDED** that you
-#  derive these pathnames from `themes/[your-theme]` to ensure that
-#  they stay unique. (Of course, vanilla doesn't do this ^^;;)
-preload:
-- features/getting_started
-- features/compose
-- features/home_timeline
-- features/notifications
diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb
index 01144f595..820189d29 100644
--- a/app/lib/activitypub/activity.rb
+++ b/app/lib/activitypub/activity.rb
@@ -3,7 +3,7 @@
 class ActivityPub::Activity
   include JsonLdHelper
 
-  def initialize(json, account, options = {})
+  def initialize(json, account, **options)
     @json    = json
     @account = account
     @object  = @json['object']
@@ -15,7 +15,7 @@ class ActivityPub::Activity
   end
 
   class << self
-    def factory(json, account, options = {})
+    def factory(json, account, **options)
       @json = json
       klass&.new(json, account, options)
     end
diff --git a/app/lib/extractor.rb b/app/lib/extractor.rb
index 738ec89a0..479689d60 100644
--- a/app/lib/extractor.rb
+++ b/app/lib/extractor.rb
@@ -32,7 +32,7 @@ module Extractor
     possible_entries
   end
 
-  def extract_hashtags_with_indices(text, _options = {})
+  def extract_hashtags_with_indices(text, **)
     return [] unless text =~ /#/
 
     tags = []
diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb
index 9d8bc52db..f5bf64cc7 100644
--- a/app/lib/formatter.rb
+++ b/app/lib/formatter.rb
@@ -9,7 +9,7 @@ class Formatter
 
   include ActionView::Helpers::TextHelper
 
-  def format(status, options = {})
+  def format(status, **options)
     if status.reblog?
       prepend_reblog = status.reblog.account.acct
       status         = status.proper
diff --git a/app/lib/ostatus/activity/base.rb b/app/lib/ostatus/activity/base.rb
index 8b27b124f..c5933f3ad 100644
--- a/app/lib/ostatus/activity/base.rb
+++ b/app/lib/ostatus/activity/base.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class OStatus::Activity::Base
-  def initialize(xml, account = nil, options = {})
+  def initialize(xml, account = nil, **options)
     @xml     = xml
     @account = account
     @options = options
diff --git a/app/lib/ostatus/atom_serializer.rb b/app/lib/ostatus/atom_serializer.rb
index 3ca6c5943..656e45822 100644
--- a/app/lib/ostatus/atom_serializer.rb
+++ b/app/lib/ostatus/atom_serializer.rb
@@ -319,7 +319,7 @@ class OStatus::AtomSerializer
 
   private
 
-  def append_element(parent, name, content = nil, attributes = {})
+  def append_element(parent, name, content = nil, **attributes)
     element = Ox::Element.new(name)
     attributes.each { |k, v| element[k] = sanitize_str(v) }
     element << sanitize_str(content) unless content.nil?
diff --git a/app/lib/provider_discovery.rb b/app/lib/provider_discovery.rb
index 5e02e6806..bcc4ed500 100644
--- a/app/lib/provider_discovery.rb
+++ b/app/lib/provider_discovery.rb
@@ -2,7 +2,7 @@
 
 class ProviderDiscovery < OEmbed::ProviderDiscovery
   class << self
-    def discover_provider(url, options = {})
+    def discover_provider(url, **options)
       res    = Request.new(:get, url).perform
       format = options[:format]
 
diff --git a/app/lib/request.rb b/app/lib/request.rb
index 30ea0e7ee..7671f4ffc 100644
--- a/app/lib/request.rb
+++ b/app/lib/request.rb
@@ -5,7 +5,7 @@ class Request
 
   include RoutingHelper
 
-  def initialize(verb, url, options = {})
+  def initialize(verb, url, **options)
     @verb    = verb
     @url     = Addressable::URI.parse(url).normalize
     @options = options
diff --git a/app/lib/themes.rb b/app/lib/themes.rb
index f7ec22fd2..863326e2d 100644
--- a/app/lib/themes.rb
+++ b/app/lib/themes.rb
@@ -7,22 +7,59 @@ class Themes
   include Singleton
 
   def initialize
+
+    core = YAML.load_file(Rails.root.join('app', 'javascript', 'core', 'theme.yml'))
+    core['pack'] = Hash.new unless core['pack']
+
     result = Hash.new
-    Dir.glob(Rails.root.join('app', 'javascript', 'themes', '*', 'theme.yml')) do |path|
+    Dir.glob(Rails.root.join('app', 'javascript', 'flavours', '*', 'theme.yml')) do |path|
       data = YAML.load_file(path)
       name = File.basename(File.dirname(path))
       if data['pack']
+        data['name'] = name
+        data['skin'] = { 'default' => [] }
         result[name] = data
       end
     end
+
+    Dir.glob(Rails.root.join('app', 'javascript', 'skins', '*', '*')) do |path|
+      ext = File.extname(path)
+      skin = File.basename(path)
+      name = File.basename(File.dirname(path))
+      if result[name]
+        if File.directory?(path)
+          pack = []
+          Dir.glob(File.join(path, '*.{css,scss}')) do |sheet|
+            pack.push(File.basename(sheet, File.extname(sheet)))
+          end
+        elsif ext.match(/^\.s?css$/i)
+          skin = File.basename(path, ext)
+          pack = ['common']
+        end
+        if skin != 'default'
+          result[name]['skin'][skin] = pack
+        end
+      end
+    end
+
+    @core = core
     @conf = result
+
+  end
+
+  def core
+    @core
   end
 
-  def get(name)
+  def flavour(name)
     @conf[name]
   end
 
-  def names
+  def flavours
     @conf.keys
   end
+
+  def skins_for(name)
+    @conf[name]['skin'].keys
+  end
 end
diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb
index d86959c0b..8af384a2d 100644
--- a/app/lib/user_settings_decorator.rb
+++ b/app/lib/user_settings_decorator.rb
@@ -26,7 +26,8 @@ class UserSettingsDecorator
     user.settings['reduce_motion']       = reduce_motion_preference if change?('setting_reduce_motion')
     user.settings['system_font_ui']      = system_font_ui_preference if change?('setting_system_font_ui')
     user.settings['noindex']             = noindex_preference if change?('setting_noindex')
-    user.settings['theme']               = theme_preference if change?('setting_theme')
+    user.settings['flavour']             = flavour_preference if change?('setting_flavour')
+    user.settings['skin']                = skin_preference if change?('setting_skin')
   end
 
   def merged_notification_emails
@@ -73,10 +74,14 @@ class UserSettingsDecorator
     boolean_cast_setting 'setting_noindex'
   end
 
-  def theme_preference
-    settings['setting_theme']
+  def flavour_preference
+    settings['setting_flavour']
   end
-  
+
+  def skin_preference
+    settings['setting_skin']
+  end
+
   def boolean_cast_setting(key)
     settings[key] == '1'
   end
diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb
index d79f26366..fd2b0649a 100644
--- a/app/mailers/notification_mailer.rb
+++ b/app/mailers/notification_mailer.rb
@@ -63,7 +63,7 @@ class NotificationMailer < ApplicationMailer
     end
   end
 
-  def digest(recipient, opts = {})
+  def digest(recipient, **opts)
     @me            = recipient
     @since         = opts[:since] || @me.user.last_emailed_at || @me.user.current_sign_in_at
     @notifications = Notification.where(account: @me, activity_type: 'Mention').where('created_at > ?', @since)
diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb
index bdb29ebad..5a062dc25 100644
--- a/app/mailers/user_mailer.rb
+++ b/app/mailers/user_mailer.rb
@@ -5,7 +5,7 @@ class UserMailer < Devise::Mailer
 
   helper :instance
 
-  def confirmation_instructions(user, token, _opts = {})
+  def confirmation_instructions(user, token, **)
     @resource = user
     @token    = token
     @instance = Rails.configuration.x.local_domain
@@ -17,7 +17,7 @@ class UserMailer < Devise::Mailer
     end
   end
 
-  def reset_password_instructions(user, token, _opts = {})
+  def reset_password_instructions(user, token, **)
     @resource = user
     @token    = token
     @instance = Rails.configuration.x.local_domain
@@ -29,7 +29,7 @@ class UserMailer < Devise::Mailer
     end
   end
 
-  def password_change(user, _opts = {})
+  def password_change(user, **)
     @resource = user
     @instance = Rails.configuration.x.local_domain
 
diff --git a/app/models/account.rb b/app/models/account.rb
index ffd19fa52..48b17bbb8 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -186,6 +186,21 @@ class Account < ApplicationRecord
     @keypair ||= OpenSSL::PKey::RSA.new(private_key || public_key)
   end
 
+  def magic_key
+    modulus, exponent = [keypair.public_key.n, keypair.public_key.e].map do |component|
+      result = []
+
+      until component.zero?
+        result << [component % 256].pack('C')
+        component >>= 8
+      end
+
+      result.reverse.join
+    end
+
+    (['RSA'] + [modulus, exponent].map { |n| Base64.urlsafe_encode64(n) }).join('.')
+  end
+
   def subscription(webhook_url)
     @subscription ||= OStatus2::Subscription.new(remote_url, secret: secret, webhook: webhook_url, hub: hub_url)
   end
@@ -279,23 +294,46 @@ class Account < ApplicationRecord
       find_by_sql([sql, limit])
     end
 
-    def advanced_search_for(terms, account, limit = 10)
+    def advanced_search_for(terms, account, limit = 10, following = false)
       textsearch, query = generate_query_for_search(terms)
 
-      sql = <<-SQL.squish
-        SELECT
-          accounts.*,
-          (count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
-        FROM accounts
-        LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?) OR (accounts.id = f.target_account_id AND f.account_id = ?)
-        WHERE #{query} @@ #{textsearch}
-          AND accounts.suspended = false
-        GROUP BY accounts.id
-        ORDER BY rank DESC
-        LIMIT ?
-      SQL
-
-      find_by_sql([sql, account.id, account.id, limit])
+      if following
+        sql = <<-SQL.squish
+          WITH first_degree AS (
+            SELECT target_account_id
+            FROM follows
+            WHERE account_id = ?
+          )
+          SELECT
+            accounts.*,
+            (count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
+          FROM accounts
+          LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?) OR (accounts.id = f.target_account_id AND f.account_id = ?)
+          WHERE accounts.id IN (SELECT * FROM first_degree)
+            AND #{query} @@ #{textsearch}
+            AND accounts.suspended = false
+          GROUP BY accounts.id
+          ORDER BY rank DESC
+          LIMIT ?
+        SQL
+
+        find_by_sql([sql, account.id, account.id, account.id, limit])
+      else
+        sql = <<-SQL.squish
+          SELECT
+            accounts.*,
+            (count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
+          FROM accounts
+          LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?) OR (accounts.id = f.target_account_id AND f.account_id = ?)
+          WHERE #{query} @@ #{textsearch}
+            AND accounts.suspended = false
+          GROUP BY accounts.id
+          ORDER BY rank DESC
+          LIMIT ?
+        SQL
+
+        find_by_sql([sql, account.id, account.id, limit])
+      end
     end
 
     private
diff --git a/app/models/list.rb b/app/models/list.rb
index 5d7ba0065..910864b26 100644
--- a/app/models/list.rb
+++ b/app/models/list.rb
@@ -19,4 +19,23 @@ class List < ApplicationRecord
   has_many :accounts, through: :list_accounts
 
   validates :title, presence: true
+
+  before_destroy :clean_feed_manager
+
+  private
+
+  def clean_feed_manager
+    reblog_key       = FeedManager.instance.key(:list, id, 'reblogs')
+    reblogged_id_set = Redis.current.zrange(reblog_key, 0, -1)
+
+    Redis.current.pipelined do
+      Redis.current.del(FeedManager.instance.key(:list, id))
+      Redis.current.del(reblog_key)
+
+      reblogged_id_set.each do |reblogged_id|
+        reblog_set_key = FeedManager.instance.key(:list, id, "reblogs:#{reblogged_id}")
+        Redis.current.del(reblog_set_key)
+      end
+    end
+  end
 end
diff --git a/app/models/list_account.rb b/app/models/list_account.rb
index c08239aa0..253932590 100644
--- a/app/models/list_account.rb
+++ b/app/models/list_account.rb
@@ -14,6 +14,8 @@ class ListAccount < ApplicationRecord
   belongs_to :account, required: true
   belongs_to :follow, required: true
 
+  validates :account_id, uniqueness: { scope: :list_id }
+
   before_validation :set_follow
 
   private
diff --git a/app/models/preview_card.rb b/app/models/preview_card.rb
index e2bf65d94..5baddba8a 100644
--- a/app/models/preview_card.rb
+++ b/app/models/preview_card.rb
@@ -21,6 +21,7 @@
 #  height             :integer          default(0), not null
 #  created_at         :datetime         not null
 #  updated_at         :datetime         not null
+#  embed_url          :string           default(""), not null
 #
 
 class PreviewCard < ApplicationRecord
diff --git a/app/models/remote_follow.rb b/app/models/remote_follow.rb
index c3f867743..070144e2d 100644
--- a/app/models/remote_follow.rb
+++ b/app/models/remote_follow.rb
@@ -7,8 +7,8 @@ class RemoteFollow
 
   validates :acct, presence: true
 
-  def initialize(attrs = {})
-    @acct = attrs[:acct].gsub(/\A@/, '').strip unless attrs[:acct].nil?
+  def initialize(attrs = nil)
+    @acct = attrs[:acct].gsub(/\A@/, '').strip if !attrs.nil? && !attrs[:acct].nil?
   end
 
   def valid?
diff --git a/app/models/session_activation.rb b/app/models/session_activation.rb
index d19489b36..1d4ebca02 100644
--- a/app/models/session_activation.rb
+++ b/app/models/session_activation.rb
@@ -14,16 +14,6 @@
 #  web_push_subscription_id :integer
 #
 
-#  id              :bigint           not null, primary key
-#  user_id         :bigint           not null
-#  session_id      :string           not null
-#  created_at      :datetime         not null
-#  updated_at      :datetime         not null
-#  user_agent      :string           default(""), not null
-#  ip              :inet
-#  access_token_id :bigint
-#
-
 class SessionActivation < ApplicationRecord
   belongs_to :user, inverse_of: :session_activations, required: true
   belongs_to :access_token, class_name: 'Doorkeeper::AccessToken', dependent: :destroy
@@ -53,7 +43,7 @@ class SessionActivation < ApplicationRecord
       id && where(session_id: id).exists?
     end
 
-    def activate(options = {})
+    def activate(**options)
       activation = create!(options)
       purge_old
       activation
diff --git a/app/models/user.rb b/app/models/user.rb
index 578622fdf..29bdcbd67 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -76,7 +76,7 @@ class User < ApplicationRecord
   has_many :session_activations, dependent: :destroy
 
   delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :delete_modal,
-           :reduce_motion, :system_font_ui, :noindex, :theme,
+           :reduce_motion, :system_font_ui, :noindex, :flavour, :skin,
            to: :settings, prefix: :setting, allow_nil: false
 
   attr_accessor :invite_code
diff --git a/app/presenters/account_relationships_presenter.rb b/app/presenters/account_relationships_presenter.rb
index a30558bac..bf1ba3716 100644
--- a/app/presenters/account_relationships_presenter.rb
+++ b/app/presenters/account_relationships_presenter.rb
@@ -4,7 +4,7 @@ class AccountRelationshipsPresenter
   attr_reader :following, :followed_by, :blocking,
               :muting, :requested, :domain_blocking
 
-  def initialize(account_ids, current_account_id, options = {})
+  def initialize(account_ids, current_account_id, **options)
     @following       = Account.following_map(account_ids, current_account_id).merge(options[:following_map] || {})
     @followed_by     = Account.followed_by_map(account_ids, current_account_id).merge(options[:followed_by_map] || {})
     @blocking        = Account.blocking_map(account_ids, current_account_id).merge(options[:blocking_map] || {})
diff --git a/app/presenters/status_relationships_presenter.rb b/app/presenters/status_relationships_presenter.rb
index bc3887a44..b04e10e2f 100644
--- a/app/presenters/status_relationships_presenter.rb
+++ b/app/presenters/status_relationships_presenter.rb
@@ -3,7 +3,7 @@
 class StatusRelationshipsPresenter
   attr_reader :reblogs_map, :favourites_map, :mutes_map, :pins_map
 
-  def initialize(statuses, current_account_id = nil, options = {})
+  def initialize(statuses, current_account_id = nil, **options)
     if current_account_id.nil?
       @reblogs_map    = {}
       @favourites_map = {}
diff --git a/app/serializers/rest/preview_card_serializer.rb b/app/serializers/rest/preview_card_serializer.rb
index 9c460332c..2df9d07a7 100644
--- a/app/serializers/rest/preview_card_serializer.rb
+++ b/app/serializers/rest/preview_card_serializer.rb
@@ -6,7 +6,7 @@ class REST::PreviewCardSerializer < ActiveModel::Serializer
   attributes :url, :title, :description, :type,
              :author_name, :author_url, :provider_name,
              :provider_url, :html, :width, :height,
-             :image
+             :image, :embed_url
 
   def image
     object.image? ? full_asset_url(object.image.url(:original)) : nil
diff --git a/app/serializers/rest/relationship_serializer.rb b/app/serializers/rest/relationship_serializer.rb
index 998727e37..45bfd4d6e 100644
--- a/app/serializers/rest/relationship_serializer.rb
+++ b/app/serializers/rest/relationship_serializer.rb
@@ -1,15 +1,21 @@
 # frozen_string_literal: true
 
 class REST::RelationshipSerializer < ActiveModel::Serializer
-  attributes :id, :following, :followed_by, :blocking,
-             :muting, :requested, :domain_blocking
+  attributes :id, :following, :showing_reblogs, :followed_by, :blocking,
+             :muting, :muting_notifications, :requested, :domain_blocking
 
   def id
     object.id.to_s
   end
 
   def following
-    instance_options[:relationships].following[object.id] || false
+    instance_options[:relationships].following[object.id] ? true : false
+  end
+
+  def showing_reblogs
+    (instance_options[:relationships].following[object.id] || {})[:reblogs] ||
+      (instance_options[:relationships].requested[object.id] || {})[:reblogs] ||
+      false
   end
 
   def followed_by
@@ -21,11 +27,15 @@ class REST::RelationshipSerializer < ActiveModel::Serializer
   end
 
   def muting
-    instance_options[:relationships].muting[object.id] || false
+    instance_options[:relationships].muting[object.id] ? true : false
+  end
+
+  def muting_notifications
+    (instance_options[:relationships].muting[object.id] || {})[:notifications] || false
   end
 
   def requested
-    instance_options[:relationships].requested[object.id] || false
+    instance_options[:relationships].requested[object.id] ? true : false
   end
 
   def domain_blocking
diff --git a/app/serializers/webfinger_serializer.rb b/app/serializers/webfinger_serializer.rb
new file mode 100644
index 000000000..f80d12c02
--- /dev/null
+++ b/app/serializers/webfinger_serializer.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class WebfingerSerializer < ActiveModel::Serializer
+  include RoutingHelper
+
+  attributes :subject, :aliases, :links
+
+  def subject
+    object.to_webfinger_s
+  end
+
+  def aliases
+    [short_account_url(object), account_url(object)]
+  end
+
+  def links
+    [
+      { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: short_account_url(object) },
+      { rel: 'http://schemas.google.com/g/2010#updates-from', type: 'application/atom+xml', href: account_url(object, format: 'atom') },
+      { rel: 'self', type: 'application/activity+json', href: account_url(object) },
+      { rel: 'salmon', href: api_salmon_url(object.id) },
+      { rel: 'magic-public-key', href: "data:application/magic-public-key,#{object.magic_key}" },
+      { rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_follow_url}?acct={uri}" },
+    ]
+  end
+end
diff --git a/app/services/account_search_service.rb b/app/services/account_search_service.rb
index b0c663d02..a289ceac4 100644
--- a/app/services/account_search_service.rb
+++ b/app/services/account_search_service.rb
@@ -1,12 +1,12 @@
 # frozen_string_literal: true
 
 class AccountSearchService < BaseService
-  attr_reader :query, :limit, :resolve, :account
+  attr_reader :query, :limit, :options, :account
 
-  def call(query, limit, resolve = false, account = nil)
-    @query = query
-    @limit = limit
-    @resolve = resolve
+  def call(query, limit, account = nil, options = {})
+    @query   = query
+    @limit   = limit
+    @options = options
     @account = account
 
     search_service_results
@@ -25,7 +25,7 @@ class AccountSearchService < BaseService
   end
 
   def resolving_non_matching_remote_account?
-    resolve && !exact_match && !domain_is_local?
+    options[:resolve] && !exact_match && !domain_is_local?
   end
 
   def search_results_and_exact_match
@@ -58,12 +58,16 @@ class AccountSearchService < BaseService
     @_domain_is_local ||= TagManager.instance.local_domain?(query_domain)
   end
 
+  def search_from
+    options[:following] && account ? account.following : Account
+  end
+
   def exact_match
     @_exact_match ||= begin
       if domain_is_local?
-        Account.find_local(query_username)
+        search_from.find_local(query_username)
       else
-        Account.find_remote(query_username, query_domain)
+        search_from.find_remote(query_username, query_domain)
       end
     end
   end
@@ -79,7 +83,7 @@ class AccountSearchService < BaseService
   end
 
   def advanced_search_results
-    Account.advanced_search_for(terms_for_query, account, limit)
+    Account.advanced_search_for(terms_for_query, account, limit, options[:following])
   end
 
   def simple_search_results
diff --git a/app/services/activitypub/process_collection_service.rb b/app/services/activitypub/process_collection_service.rb
index db4d1b4bc..eb93329e9 100644
--- a/app/services/activitypub/process_collection_service.rb
+++ b/app/services/activitypub/process_collection_service.rb
@@ -3,7 +3,7 @@
 class ActivityPub::ProcessCollectionService < BaseService
   include JsonLdHelper
 
-  def call(body, account, options = {})
+  def call(body, account, **options)
     @account = account
     @json    = Oj.load(body, mode: :strict)
     @options = options
diff --git a/app/services/authorize_follow_service.rb b/app/services/authorize_follow_service.rb
index b1bff8962..f47d488f1 100644
--- a/app/services/authorize_follow_service.rb
+++ b/app/services/authorize_follow_service.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class AuthorizeFollowService < BaseService
-  def call(source_account, target_account, options = {})
+  def call(source_account, target_account, **options)
     if options[:skip_follow_request]
       follow_request = FollowRequest.new(account: source_account, target_account: target_account)
     else
diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb
index 37cf75379..cec96d927 100644
--- a/app/services/fetch_link_card_service.rb
+++ b/app/services/fetch_link_card_service.rb
@@ -74,9 +74,6 @@ class FetchLinkCardService < BaseService
 
     return false unless response.respond_to?(:type)
 
-    # The photo will change the URL. So, to avoid duplication of URLs, PreviewCard needs to be checked again.
-    @card = PreviewCard.find_by(url: response.url) || @card if response.type == 'photo'
-
     @card.type          = response.type
     @card.title         = response.respond_to?(:title)         ? response.title         : ''
     @card.author_name   = response.respond_to?(:author_name)   ? response.author_name   : ''
@@ -90,9 +87,9 @@ class FetchLinkCardService < BaseService
     when 'link'
       @card.image = URI.parse(response.thumbnail_url) if response.respond_to?(:thumbnail_url)
     when 'photo'
-      @card.url    = response.url
-      @card.width  = response.width.presence  || 0
-      @card.height = response.height.presence || 0
+      @card.embed_url = response.url
+      @card.width     = response.width.presence  || 0
+      @card.height    = response.height.presence || 0
     when 'video'
       @card.width  = response.width.presence  || 0
       @card.height = response.height.presence || 0
diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb
index 974c586f2..59531a76c 100644
--- a/app/services/post_status_service.rb
+++ b/app/services/post_status_service.rb
@@ -13,7 +13,7 @@ class PostStatusService < BaseService
   # @option [Doorkeeper::Application] :application
   # @option [String] :idempotency Optional idempotency key
   # @return [Status]
-  def call(account, text, in_reply_to = nil, options = {})
+  def call(account, text, in_reply_to = nil, **options)
     if options[:idempotency].present?
       existing_id = redis.get("idempotency:status:#{account.id}:#{options[:idempotency]}")
       return Status.find(existing_id) if existing_id
diff --git a/app/services/process_feed_service.rb b/app/services/process_feed_service.rb
index 60eff135e..30a9dd85e 100644
--- a/app/services/process_feed_service.rb
+++ b/app/services/process_feed_service.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class ProcessFeedService < BaseService
-  def call(body, account, options = {})
+  def call(body, account, **options)
     @options = options
 
     xml = Nokogiri::XML(body)
diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb
index 7789bd441..e164c03ab 100644
--- a/app/services/remove_status_service.rb
+++ b/app/services/remove_status_service.rb
@@ -3,7 +3,7 @@
 class RemoveStatusService < BaseService
   include StreamEntryRenderer
 
-  def call(status, options = {})
+  def call(status, **options)
     @payload      = Oj.dump(event: :delete, payload: status.id.to_s)
     @status       = status
     @account      = status.account
diff --git a/app/services/search_service.rb b/app/services/search_service.rb
index 1ed3f0032..85ad94463 100644
--- a/app/services/search_service.rb
+++ b/app/services/search_service.rb
@@ -10,7 +10,7 @@ class SearchService < BaseService
       if url_query?
         results.merge!(remote_resource_results) unless remote_resource.nil?
       elsif query.present?
-        results[:accounts] = AccountSearchService.new.call(query, limit, resolve, account)
+        results[:accounts] = AccountSearchService.new.call(query, limit, account, resolve: resolve)
         results[:hashtags] = Tag.search_for(query.gsub(/\A#/, ''), limit) unless query.start_with?('@')
       end
     end
diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb
index 5b37ba9ba..958b28cdc 100644
--- a/app/services/suspend_account_service.rb
+++ b/app/services/suspend_account_service.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class SuspendAccountService < BaseService
-  def call(account, options = {})
+  def call(account, **options)
     @account = account
     @options = options
 
diff --git a/app/views/about/more.html.haml b/app/views/about/more.html.haml
index 7ffa5ecc3..d92362bd7 100644
--- a/app/views/about/more.html.haml
+++ b/app/views/about/more.html.haml
@@ -2,7 +2,6 @@
   = site_hostname
 
 - content_for :header_tags do
-  = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous'
   = render partial: 'shared/og'
 
 .landing-page
diff --git a/app/views/about/show.html.haml b/app/views/about/show.html.haml
index 385b0b1dc..4f5b53470 100644
--- a/app/views/about/show.html.haml
+++ b/app/views/about/show.html.haml
@@ -3,7 +3,6 @@
 
 - content_for :header_tags do
   %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json)
-  = javascript_pack_tag 'about', integrity: true, crossorigin: 'anonymous'
   = render partial: 'shared/og'
 
 .landing-page
diff --git a/app/views/admin/reports/show.html.haml b/app/views/admin/reports/show.html.haml
index 5747cc274..7dd962bf2 100644
--- a/app/views/admin/reports/show.html.haml
+++ b/app/views/admin/reports/show.html.haml
@@ -1,6 +1,3 @@
-- content_for :header_tags do
-  = javascript_pack_tag 'admin', integrity: true, async: true, crossorigin: 'anonymous'
-
 - content_for :page_title do
   = t('admin.reports.report', id: @report.id)
 
diff --git a/app/views/admin/statuses/index.html.haml b/app/views/admin/statuses/index.html.haml
index fe2581527..9747a92cf 100644
--- a/app/views/admin/statuses/index.html.haml
+++ b/app/views/admin/statuses/index.html.haml
@@ -1,6 +1,3 @@
-- content_for :header_tags do
-  = javascript_pack_tag 'admin', integrity: true, async: true, crossorigin: 'anonymous'
-
 - content_for :page_title do
   = t('admin.statuses.title')
 
diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml
index 63b3a0c26..e8a81656c 100644
--- a/app/views/home/index.html.haml
+++ b/app/views/home/index.html.haml
@@ -1,13 +1,7 @@
 - content_for :header_tags do
-  - if theme_data['preload']
-    - theme_data['preload'].each do |link|
-      %link{ href: asset_pack_path("#{link}.js"), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
   %meta{name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key}
   %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json)
 
-  = javascript_pack_tag "themes/#{current_theme}", integrity: true, crossorigin: 'anonymous'
-  = stylesheet_pack_tag "themes/#{current_theme}", integrity: true, media: 'all'
-
 .app-holder#mastodon{ data: { props: Oj.dump(default_props) } }
   %noscript
     = image_tag asset_pack_path('logo.svg'), alt: 'Mastodon'
diff --git a/app/views/layouts/_theme.html.haml b/app/views/layouts/_theme.html.haml
new file mode 100644
index 000000000..066d9de42
--- /dev/null
+++ b/app/views/layouts/_theme.html.haml
@@ -0,0 +1,13 @@
+- if theme
+  - if theme[:pack] != 'common' && theme[:common]
+    = render partial: 'layouts/theme', object: theme[:common]
+  - if theme[:pack]
+    = javascript_pack_tag theme[:flavour] ? "flavours/#{theme[:flavour]}/#{theme[:pack]}" : "core/#{theme[:pack]}", integrity: true, crossorigin: 'anonymous'
+    - if theme[:skin]
+      - if !theme[:flavour] || theme[:skin] == 'default'
+        = stylesheet_pack_tag theme[:flavour] ? "flavours/#{theme[:flavour]}/#{theme[:pack]}" : "core/#{theme[:pack]}", integrity: true, media: 'all'
+      - else
+        = stylesheet_pack_tag "skins/#{theme[:flavour]}/#{theme[:skin]}/#{theme[:pack]}"
+    - if theme[:preload]
+      - theme[:preload].each do |link|
+        %link{ href: asset_pack_path("#{link}.js"), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml
index c98d85f7b..66382db50 100644
--- a/app/views/layouts/admin.html.haml
+++ b/app/views/layouts/admin.html.haml
@@ -1,6 +1,3 @@
-- content_for :header_tags do
-  = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous'
-
 - content_for :content do
   .admin-wrapper
     .sidebar-wrapper
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 24b74c787..99ae7d90d 100755
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -18,16 +18,16 @@
         = ' - '
       = title
 
-    = stylesheet_pack_tag 'common', media: 'all'
-    = javascript_pack_tag 'common', integrity: true, crossorigin: 'anonymous'
+    = javascript_pack_tag "locales", integrity: true, crossorigin: 'anonymous'
     = javascript_pack_tag "locale_#{I18n.locale}", integrity: true, crossorigin: 'anonymous'
     = csrf_meta_tags
 
-    - if controller_name != 'home'
-      = stylesheet_pack_tag 'application', integrity: true, media: 'all'
-
     = yield :header_tags
 
+    -#  These must come after :header_tags to ensure our initial state has been defined.
+    = render partial: 'layouts/theme', object: @core
+    = render partial: 'layouts/theme', object: @theme
+
   - body_classes ||= @body_classes || ''
   - body_classes += ' system-font' if current_account&.user&.setting_system_font_ui
 
diff --git a/app/views/layouts/auth.html.haml b/app/views/layouts/auth.html.haml
index d8ac733f9..f4812ac6a 100644
--- a/app/views/layouts/auth.html.haml
+++ b/app/views/layouts/auth.html.haml
@@ -1,6 +1,3 @@
-- content_for :header_tags do
-  = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous'
-
 - content_for :content do
   .container
     .logo-container
diff --git a/app/views/layouts/embedded.html.haml b/app/views/layouts/embedded.html.haml
index 5fc60be17..935670514 100644
--- a/app/views/layouts/embedded.html.haml
+++ b/app/views/layouts/embedded.html.haml
@@ -4,10 +4,8 @@
     %meta{ charset: 'utf-8' }/
     %meta{ name: 'robots', content: 'noindex' }/
 
-    = stylesheet_pack_tag 'common', media: 'all'
-    = javascript_pack_tag 'common', integrity: true, crossorigin: 'anonymous'
-    = stylesheet_pack_tag 'application', integrity: true, media: 'all'
+    = javascript_pack_tag "locales", integrity: true, crossorigin: 'anonymous'
     = javascript_pack_tag "locale_#{I18n.locale}", integrity: true, crossorigin: 'anonymous'
-    = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous'
-  %body.embed
+    = render partial: 'layouts/theme', object: @core
+    = render partial: 'layouts/theme', object: @theme
     = yield
diff --git a/app/views/layouts/error.html.haml b/app/views/layouts/error.html.haml
index d0eae4434..9904b8fdd 100644
--- a/app/views/layouts/error.html.haml
+++ b/app/views/layouts/error.html.haml
@@ -5,8 +5,8 @@
     %meta{ charset: 'utf-8' }/
     %title= safe_join([yield(:page_title), Setting.default_settings['site_title']], ' - ')
     %meta{ content: 'width=device-width,initial-scale=1', name: 'viewport' }/
-    = stylesheet_pack_tag 'common', media: 'all'
-    = stylesheet_pack_tag 'application', integrity: true, media: 'all'
+    = render partial: 'layouts/theme', object: @core
+    = render partial: 'layouts/theme', object: @theme
   %body.error
     .dialog
       %img{ alt: Setting.default_settings['site_title'], src: '/oops.gif' }/
diff --git a/app/views/layouts/modal.html.haml b/app/views/layouts/modal.html.haml
index a819e098d..d3519f032 100644
--- a/app/views/layouts/modal.html.haml
+++ b/app/views/layouts/modal.html.haml
@@ -1,6 +1,3 @@
-- content_for :header_tags do
-  = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous'
-
 - content_for :content do
   - if user_signed_in?
     .account-header
diff --git a/app/views/layouts/public.html.haml b/app/views/layouts/public.html.haml
index 83e92b938..b3795eaad 100644
--- a/app/views/layouts/public.html.haml
+++ b/app/views/layouts/public.html.haml
@@ -1,6 +1,3 @@
-- content_for :header_tags do
-  = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous'
-
 - content_for :content do
   .container= yield
   .footer
diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml
index 69e26a7be..9564c0399 100644
--- a/app/views/settings/preferences/show.html.haml
+++ b/app/views/settings/preferences/show.html.haml
@@ -26,8 +26,9 @@
   %h4= t 'preferences.web'
 
   .fields-group
-    - if Themes.instance.names.size > 1
-      = f.input :setting_theme, collection: Themes.instance.names, label_method: lambda { |theme| I18n.t("themes.#{theme}", default: theme) }, wrapper: :with_label, include_blank: false
+    - if Themes.instance.flavours.size > 1
+      = f.input :setting_flavour, collection: Themes.instance.flavours, label_method: lambda { |flavour| I18n.t("themes.#{flavour}", default: flavour) }, wrapper: :with_label, include_blank: false
+      = f.input :setting_skin, collection: Themes.instance.skins_for(current_flavour), label_method: lambda { |skin| I18n.t("themes.#{current_flavour}.skins.#{skin}", default: skin) }, wrapper: :with_label, include_blank: false
 
     = f.input :setting_unfollow_modal, as: :boolean, wrapper: :with_label
     = f.input :setting_boost_modal, as: :boolean, wrapper: :with_label
diff --git a/app/views/shares/show.html.haml b/app/views/shares/show.html.haml
index 44b6f145f..4c0390c42 100644
--- a/app/views/shares/show.html.haml
+++ b/app/views/shares/show.html.haml
@@ -1,5 +1,4 @@
 - content_for :header_tags do
   %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json)
-  = javascript_pack_tag 'share', integrity: true, crossorigin: 'anonymous'
 
 #mastodon-compose{ data: { props: Oj.dump(default_props) } }
diff --git a/app/views/tags/show.html.haml b/app/views/tags/show.html.haml
index ea8b0faa3..e05fe1c39 100644
--- a/app/views/tags/show.html.haml
+++ b/app/views/tags/show.html.haml
@@ -3,7 +3,6 @@
 
 - content_for :header_tags do
   %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json)
-  = javascript_pack_tag 'about', integrity: true, crossorigin: 'anonymous'
   = render 'og'
 
 .landing-page.tag-page
diff --git a/app/views/well_known/webfinger/show.json.rabl b/app/views/well_known/webfinger/show.json.rabl
deleted file mode 100644
index 762d1860d..000000000
--- a/app/views/well_known/webfinger/show.json.rabl
+++ /dev/null
@@ -1,18 +0,0 @@
-object @account
-
-node(:subject) { @canonical_account_uri }
-
-node(:aliases) do
-  [short_account_url(@account), account_url(@account)]
-end
-
-node(:links) do
-  [
-    { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: short_account_url(@account) },
-    { rel: 'http://schemas.google.com/g/2010#updates-from', type: 'application/atom+xml', href: account_url(@account, format: 'atom') },
-    { rel: 'self', type: 'application/activity+json', href: account_url(@account) },
-    { rel: 'salmon', href: api_salmon_url(@account.id) },
-    { rel: 'magic-public-key', href: "data:application/magic-public-key,#{@magic_key}" },
-    { rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_follow_url}?acct={uri}" },
-  ]
-end
diff --git a/app/views/well_known/webfinger/show.xml.ruby b/app/views/well_known/webfinger/show.xml.ruby
index b0158b8bd..0c7289d6a 100644
--- a/app/views/well_known/webfinger/show.xml.ruby
+++ b/app/views/well_known/webfinger/show.xml.ruby
@@ -1,13 +1,13 @@
 Nokogiri::XML::Builder.new do |xml|
   xml.XRD(xmlns: 'http://docs.oasis-open.org/ns/xri/xrd-1.0') do
-    xml.Subject @canonical_account_uri
+    xml.Subject @account.to_webfinger_s
     xml.Alias short_account_url(@account)
     xml.Alias account_url(@account)
     xml.Link(rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: short_account_url(@account))
     xml.Link(rel: 'http://schemas.google.com/g/2010#updates-from', type: 'application/atom+xml', href: account_url(@account, format: 'atom'))
     xml.Link(rel: 'self', type: 'application/activity+json', href: account_url(@account))
     xml.Link(rel: 'salmon', href: api_salmon_url(@account.id))
-    xml.Link(rel: 'magic-public-key', href: "data:application/magic-public-key,#{@magic_key}")
+    xml.Link(rel: 'magic-public-key', href: "data:application/magic-public-key,#{@account.magic_key}")
     xml.Link(rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_follow_url}?acct={uri}")
   end
 end.to_xml
diff --git a/app/workers/scheduler/feed_cleanup_scheduler.rb b/app/workers/scheduler/feed_cleanup_scheduler.rb
index cfa2d31a4..23fa7672b 100644
--- a/app/workers/scheduler/feed_cleanup_scheduler.rb
+++ b/app/workers/scheduler/feed_cleanup_scheduler.rb
@@ -5,16 +5,30 @@ class Scheduler::FeedCleanupScheduler
   include Sidekiq::Worker
 
   def perform
+    clean_home_feeds!
+    clean_list_feeds!
+  end
+
+  private
+
+  def clean_home_feeds!
+    clean_feeds!(inactive_account_ids, :home)
+  end
+
+  def clean_list_feeds!
+    clean_feeds!(inactive_list_ids, :list)
+  end
+
+  def clean_feeds!(ids, type)
     reblogged_id_sets = {}
-    feedmanager = FeedManager.instance
 
     redis.pipelined do
-      inactive_user_ids.each do |account_id|
-        redis.del(feedmanager.key(:home, account_id))
-        reblog_key = feedmanager.key(:home, account_id, 'reblogs')
+      ids.each do |feed_id|
+        redis.del(feed_manager.key(type, feed_id))
+        reblog_key = feed_manager.key(type, feed_id, 'reblogs')
         # We collect a future for this: we don't block while getting
         # it, but we can iterate over it later.
-        reblogged_id_sets[account_id] = redis.zrange(reblog_key, 0, -1)
+        reblogged_id_sets[feed_id] = redis.zrange(reblog_key, 0, -1)
         redis.del(reblog_key)
       end
     end
@@ -22,19 +36,25 @@ class Scheduler::FeedCleanupScheduler
     # Remove all of the reblog tracking keys we just removed the
     # references to.
     redis.pipelined do
-      reblogged_id_sets.each do |account_id, future|
+      reblogged_id_sets.each do |feed_id, future|
         future.value.each do |reblogged_id|
-          reblog_set_key = feedmanager.key(:home, account_id, "reblogs:#{reblogged_id}")
+          reblog_set_key = feed_manager.key(type, feed_id, "reblogs:#{reblogged_id}")
           redis.del(reblog_set_key)
         end
       end
     end
   end
 
-  private
+  def inactive_account_ids
+    @inactive_account_ids ||= User.confirmed.inactive.pluck(:account_id)
+  end
+
+  def inactive_list_ids
+    List.where(account_id: inactive_account_ids).pluck(:id)
+  end
 
-  def inactive_user_ids
-    @inactive_user_ids ||= User.confirmed.inactive.pluck(:account_id)
+  def feed_manager
+    FeedManager.instance
   end
 
   def redis
diff --git a/config/initializers/oj.rb b/config/initializers/oj.rb
new file mode 100644
index 000000000..de3e17f2e
--- /dev/null
+++ b/config/initializers/oj.rb
@@ -0,0 +1 @@
+Oj.default_options = { mode: :compat, time_format: :ruby, use_to_json: true }
diff --git a/config/initializers/rabl_init.rb b/config/initializers/rabl_init.rb
deleted file mode 100644
index 132a42144..000000000
--- a/config/initializers/rabl_init.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-Rabl.configure do |config|
-  config.json_engine       = Oj
-  config.cache_all_output  = false
-  config.cache_sources     = Rails.env.production?
-  config.include_json_root = false
-  config.view_paths        = [Rails.root.join('app/views')]
-end
diff --git a/config/locales/activerecord.de.yml b/config/locales/activerecord.de.yml
index 668abe2a3..7d09856d7 100644
--- a/config/locales/activerecord.de.yml
+++ b/config/locales/activerecord.de.yml
@@ -10,4 +10,4 @@ de:
         status:
           attributes:
             reblog:
-              taken: of status already exists
+              taken: des Status existiert schon
diff --git a/config/locales/activerecord.nl.yml b/config/locales/activerecord.nl.yml
index eeabab34a..797209665 100644
--- a/config/locales/activerecord.nl.yml
+++ b/config/locales/activerecord.nl.yml
@@ -6,7 +6,7 @@ nl:
         account:
           attributes:
             username:
-              invalid: alleen letters, nummers en underscores
+              invalid: alleen letters, nummers en laag streepje
         status:
           attributes:
             reblog:
diff --git a/config/locales/ar.yml b/config/locales/ar.yml
index 666b8cb97..a96b353c1 100644
--- a/config/locales/ar.yml
+++ b/config/locales/ar.yml
@@ -5,25 +5,58 @@ ar:
     about_this: عن مثيل الخادوم هذا
     closed_registrations: التسجيلات في مثيل الخادوم هذا مُغلقة حاليًا.
     contact: للتواصل معنا
+    contact_unavailable: غير متوفر
     description_headline: ما هو %{domain}?
     domain_count_after: خوادم أخرى
     domain_count_before: متصل بـ
+    features:
+      not_a_product_title: إنك إنسان و لست سلعة
+    find_another_instance: إبحث عن مثيل خادوم آخر
+    hosted_on: ماستدون مُستضاف على %{domain}
+    learn_more: تعلم المزيد
     other_instances: خوادم أخرى
     source_code: الشفرة المصدرية
     status_count_after: منشورا
     status_count_before: نشروا
     user_count_after: مستخدم
     user_count_before: يستضيف
+    what_is_mastodon: ما هو ماستدون ؟
   accounts:
     follow: إتبع
     followers: متابِعون
     following: يتابعون
+    media: الوسائط
     nothing_here: لا يوجد أي شيء هنا !
     people_followed_by: الأشخاص الذين يتبعهم %{name}
     people_who_follow: الأشخاص الذين يتبعون %{name}
     posts: منشورات
+    posts_with_replies: التبويقات و الردود
     remote_follow: إتبع عن بعد
+    reserved_username: إسم المستخدم محجوز
+    roles:
+      admin: المدير
     unfollow: إلغاء المتابعة
+  admin:
+    account_moderation_notes:
+      created_at: التاريخ
+      delete: حذف
+    accounts:
+      are_you_sure: متأكد ؟
+      by_domain: النطاق
+      confirm: تأكيد
+      display_name: عرض الإسم
+      domain: النطاق
+      edit: تعديل
+      email: البريد الإلكتروني
+      enable: تفعيل
+      enabled: مفعَّل
+      followers: المتابِعون
+      follows: يتابع
+      ip: عنوان الإيبي
+      media_attachments: الوسائط المرفقة
+      order:
+        title: الترتيب
+      profile_url: رابط الملف الشخصي
   application_mailer:
     settings: 'تغيير تفضيلات البريد الإلكتروني : %{link}'
     signature: إشعارات ماستدون من %{instance}
@@ -156,9 +189,13 @@ ar:
     description_html: If you enable <strong>two-factor authentication</strong>, logging in will require you to be in possession of your phone, which will generate tokens for you to enter.
     disable: تعطيل
     enable: تفعيل
+    enabled: نظام المصادقة بخطوتين مُفعَّل
     enabled_success: تم تفعيل إثبات الهوية المزدوج بنجاح
+    generate_recovery_codes: توليد رموز الإسترجاع
     instructions_html: "<strong>Scan this QR code into Google Authenticator or a similiar TOTP app on your phone</strong>. From now on, that app will generate tokens that you will have to enter when logging in."
     manual_instructions: 'If you can''t scan the QR code and need to enter it manually, here is the plain-text secret:'
+    recovery_codes: النسخ الإحتياطي لرموز الإسترجاع
+    recovery_codes_regenerated: تم إعادة توليد رموز الإسترجاع الإحتياطية بنجاح
     setup: تنشيط
     wrong_code: الرمز الذي أدخلته غير صالح. تحقق من صحة الوقت على الخادم و الجهاز.
   users:
diff --git a/config/locales/ca.yml b/config/locales/ca.yml
index fa8cf49f0..357e39e31 100644
--- a/config/locales/ca.yml
+++ b/config/locales/ca.yml
@@ -49,16 +49,16 @@ ca:
     reserved_username: El nom d'usuari està reservat
     roles:
       admin: Admin
-      moderator: Mod
+      moderator: Moderador
     unfollow: Deixar de seguir
   admin:
     account_moderation_notes:
       account: Moderador
       create: Crear
       created_at: Data
-      created_msg: La nota de moderació s'ha creat correctament.
+      created_msg: La nota de moderació s'ha creat correctament!
       delete: Suprimeix
-      destroyed_msg: S'ha destruït la nota de moderació.
+      destroyed_msg: S'ha destruït la nota de moderació!
     accounts:
       are_you_sure: Estàs segur?
       by_domain: Domini
@@ -133,6 +133,32 @@ ca:
       unsubscribe: Donar-se de baixa
       username: Nom d'usuari
       web: Web
+    action_logs:
+      actions:
+        confirm_user: "%{name} ha confirmat l'adreça de correu electrònic de l'usuari %{target}"
+        create_custom_emoji: "%{name} ha penjat un nou emoji %{target}"
+        create_domain_block: "%{name} ha bloquejat el domini %{target}"
+        create_email_domain_block: "%{name} ha afegit a la llista negra el domini del correu electrònic %{target}"
+        demote_user: "%{name} ha degradat l'usuari %{target}"
+        destroy_domain_block: "%{name} ha desbloquejat el domini %{target}"
+        destroy_email_domain_block: "%{name} ha afegit a la llista negra el domini de correu electrònic %{target}"
+        destroy_status: "%{name} estat eliminat per %{target}"
+        disable_2fa_user: "%{name} ha desactivat el requisit de dos factors per a l'usuari %{target}"
+        disable_custom_emoji: "%{name} ha desactivat l'emoji %{target}"
+        disable_user: "%{name} ha desactivat l'accés per a l'usuari %{target}"
+        enable_custom_emoji: "%{name} ha activat l'emoji %{target}"
+        enable_user: "%{name} ha activat l'accés per a l'usuari %{target}"
+        memorialize_account: "%{name} ha convertit el compte %{target} en una pàgina de memòria"
+        promote_user: "%{name} ha promogut l'usuari %{target}"
+        reset_password_user: "%{name} restablirà la contrasenya de l'usuari %{target}"
+        resolve_report: "%{name} ha descartat l'informe %{target}"
+        silence_account: "%{name} ha silenciat el compte de %{target}"
+        suspend_account: "%{name} ha suspès el compte de %{target}"
+        unsilence_account: "%{name} ha silenciat el compte de %{target}"
+        unsuspend_account: "%{name} ha llevat la suspensió del compte de %{target}"
+        update_custom_emoji: "%{name} ha actualizat l'emoji %{target}"
+        update_status: "%{name} estat actualizat per %{target}"
+      title: Registre d'auditoria
     custom_emojis:
       copied_msg: S'ha creat correctament la còpia local del emoji
       copy: Copia
@@ -155,7 +181,7 @@ ca:
       title: Emojis personatlitzats
       unlisted: Sense classificar
       update_failed_msg: No s'ha pogut actualitzar aquest emoji
-      updated_msg: Emoji s'ha actualitzat correctament.
+      updated_msg: Emoji s'ha actualitzat correctament!
       upload: Carrega
     domain_blocks:
       add_new: Afegeix
@@ -164,9 +190,9 @@ ca:
       domain: Domini
       new:
         create: Crea un bloqueig
-        hint: El bloqueig de domini no impedirà la creació de nous comptes en la base de dades, però s´aplicaran mètodes de moderació específics sobre aquests comptes
+        hint: El bloqueig de domini no impedirà la creació de nous comptes en la base de dades, però s´aplicaran de manera retroactiva mètodes de moderació específics sobre aquests comptes.
         severity:
-          desc_html: "<strong>Silenci</strong> farà les publicacions del compte invisibles a tothom que no l'estigui seguint. La suspencsió eliminarà tots els continguts, multimèdia i les dades del perfil del compte."
+          desc_html: "<strong>Silenci</strong> farà les publicacions del compte invisibles a tothom que no l'estigui seguint. <strong>La suspensió</strong> eliminarà tots els continguts, multimèdia i les dades del perfil del compte. Usa <strong>Cap</strong> si només vols rebutjar el fitxers multimèdia."
           noop: Cap
           silence: Silenci
           suspend: Suspensió
@@ -205,6 +231,13 @@ ca:
       reset: Restablir
       search: Cerca
       title: Instàncies conegudes
+    invites:
+      filter:
+        all: Tot
+        available: Disponible
+        expired: Caducat
+        title: Filtre
+      title: Convida
     reports:
       action_taken_by: Mesures adoptades per
       are_you_sure: Estàs segur?
@@ -243,6 +276,9 @@ ca:
         deletion:
           desc_html: Permet a qualsevol esborrar el seu compte
           title: Obre la supressió del compte
+        min_invite_role:
+          disabled: Ningú
+          title: Permet les invitacions de
         open:
           desc_html: Permet que qualsevol pugui crear un compte
           title: Registre obert
@@ -250,7 +286,7 @@ ca:
         desc_html: Mostra una insígnia de personal en una pàgina d'usuari
         title: Mostra insígnia de personal
       site_description:
-        desc_html: Es mostra com un paràgraf a la pàgina principal i s'utilitza com una etiqueta meta.<br>Pots utilitzar etiquetes HTML, en particular <code>&lt;a&gt;</code> i <code>&lt;em&gt;</code>.
+        desc_html: Paràgraf introductori a la pàgina principal i en etiquetes meta.<br>Pots utilitzar etiquetes HTML, en particular <code>&lt;a&gt;</code> i <code>&lt;em&gt;</code>.
         title: Descripció del lloc
       site_description_extended:
         desc_html: Un bon lloc per al codi de conducta, regles, directrius i altres coses que distingeixen la vostra instància. Pots utilitzar etiquetes HTML
@@ -282,7 +318,7 @@ ca:
       title: Estats del compte
       with_media: Amb contingut multimèdia
     subscriptions:
-      callback_url: Callback URL
+      callback_url: URL de retorn
       confirmed: Confirmat
       expires_in: Expira en
       last_delivery: Últim lliurament
@@ -316,6 +352,8 @@ ca:
     invalid_reset_password_token: L'enllaç de restabliment de la contrasenya no és vàlid o ha caducat. Torna-ho a provar..
     login: Inicia sessió
     logout: Tanca sessió
+    migrate_account: Mou a un compte diferent
+    migrate_account_html: Si vols redirigir aquest compte a un altre diferent, el pots  <a href="%{path}">configurar aquí</a>.
     register: Registra't
     resend_confirmation: Torna a enviar el correu de confirmació
     reset_password: Restableix la contrasenya
@@ -361,8 +399,8 @@ ca:
       title: La verificació de seguretat ha fallat
     '429': Estrangulat
     '500':
-      content: We're sorry, but something went wrong on our end.
-      title: This page is not correct
+      content: Ho sentim, però alguna cosa ha fallat a la nostra banda.
+      title: Aquesta pàgina no es correcte
     noscript_html: Per utilitzar Mastodon si us plau activa JavaScript. També podeu provar una de les <a href="https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md"> aplicacions natives</a> per Mastodon per a la vostra plataforma.
   exports:
     blocks: Persones que has bloquejat
@@ -372,7 +410,7 @@ ca:
     storage: Emmagatzematge
   followers:
     domain: Domini
-    explanation_html: Si desitges garantir la privacitat de les teves publicacions, has de ser conscient de qui t'està seguint. <strong> Les publicacions privades es lliuren a totes les instàncies on tens seguidors </ strong>. És possible que vulguis revisar-los i eliminar seguidors si no confies en que la teva privacitat sigui respectada pel personal o el programari d'aquests casos
+    explanation_html: Si desitges garantir la privacitat de les teves publicacions, has de ser conscient de qui t'està seguint. <strong> Les publicacions privades es lliuren a totes les instàncies on tens seguidors </ strong>. És possible que vulguis revisar-los i eliminar seguidors si no confies en que la teva privacitat sigui respectada pel personal o el programari d'aquestes instàncies.
     followers_count: Nombre de seguidors
     lock_link: Bloca el teu compte
     purge: Elimina dels seguidors
@@ -398,19 +436,46 @@ ca:
       muting: Llista d'apagats
     upload: Carregar
   in_memoriam_html: En Memòria.
+  invites:
+    delete: Desactivar
+    expired: Caducat
+    expires_in:
+      '1800': 30 minuts
+      '21600': 6 hores
+      '3600': 1 hora
+      '43200': 12 hores
+      '86400': 1 dia
+    expires_in_prompt: Mai
+    generate: Genera
+    max_uses:
+      one: 1 ús
+      other: "%{count} usos"
+    max_uses_prompt: Sense limit
+    prompt: Genera i comparteix enllaços amb altres persones per donar accés a aquesta instància
+    table:
+      expires_at: Caduca
+      uses: Usos
+    title: Convida persones
   landing_strip_html: "<strong>%{name}</strong> és un usuari/a de %{link_to_root_path}. Pots seguir-lo/la o interactuar amb ell/a si tens un compte a qualsevol node del fediverse."
   landing_strip_signup_html: Si no en tens, pots <a href="%{sign_up_path}">registrar-te aquí</a>.
   media_attachments:
     validations:
       images_and_video: No es pot adjuntar un vídeo a una publicació que ja contingui imatges
       too_many: No es poden adjuntar més de 4 fitxers
+  migrations:
+    acct: usuari@domini del nou compte
+    currently_redirecting: 'El teu perfil està configurat com a redirecció a:'
+    proceed: Desa
+    updated_msg: La configuració de la migració del compte s'ha actualitzat correctament!
+  moderation:
+    title: Moderació
   notification_mailer:
     digest:
       body: 'Un resum del que et vas perdre en %{instance} desde la darrera visita el %{since}:'
       mention: "%{name} t'ha mencionat en:"
       new_followers_summary:
-        one: Visca!. Algú més t´ha començat a seguir
-        other: Genial!. Et segueixen %{count} persones més
+        one: Visca!. Algú més t´ha començat a seguir!
+        other: Genial!. Et segueixen %{count} persones més!
       subject:
         one: "1 notificació nova des de la darrera visita \U0001F418"
         other: "%{count} notificacions noves des de la darrera visita \U0001F418"
@@ -427,7 +492,7 @@ ca:
       body: "%{name} t'ha mencionat en:"
       subject: "%{name} t'ha mencionat"
     reblog:
-      body: "%{name} ha retootejat el teu estat"
+      body: "%{name} ha impulsat el teu estat:"
       subject: "%{name} ha retootejat el teu estat"
   number:
     human:
@@ -465,7 +530,7 @@ ca:
       title: "%{name} t'ha retootejat"
   remote_follow:
     acct: Escriu l'usuari@domini de la persona que vols seguir
-    missing_resource: No s'ha pogut trobar la URL de redirecció necessaria per al compte.
+    missing_resource: No s'ha pogut trobar la URL de redirecció necessaria per al compte
     proceed: Comença a seguir
     prompt: 'Seguiràs a:'
   sessions:
@@ -516,6 +581,7 @@ ca:
     export: Exportar informació
     followers: Seguidors autoritzats
     import: Importar
+    migrate: Migració del compte
     notifications: Notificacions
     preferences: Preferències
     settings: Configuració
@@ -612,6 +678,8 @@ ca:
 
       <p>Originalment adaptat a la <a href="https://github.com/discourse/discourse">política de privadesa del Discurs</a>.</p>
     title: "%{instance} Condicions del servei i política de privadesa"
+  themes:
+    default: Mastodont
   time:
     formats:
       default: "%b %d, %Y, %H:%M"
@@ -628,7 +696,7 @@ ca:
     manual_instructions: 'Si no pots escanejar el codi QR code i necessites introduir-lo manualment, aquí tens el secret en text pla:'
     recovery_codes: Codis de recuperació de còpia de seguretat
     recovery_codes_regenerated: Codis de recuperació regenerats amb èxit
-    recovery_instructions_html: Si mai perds l'accéss al telèfon pots utilitzar un dels codis de recuperació a continuació per a recuperar l'accés al teu compte. Cal mantenir els codis de recuperació en lloc segur, per exemple imprimint-los i guardar-los amb altres documents importants.
+    recovery_instructions_html: Si mai perds l'accéss al teu telèfon pots utilitzar un dels codis de recuperació a continuació per a recuperar l'accés al teu compte. <strong>Cal mantenir els codis de recuperació en lloc segur</strong>. Per exemple, imprimint-los i guardar-los amb altres documents importants.
     setup: Establir
     wrong_code: El codi introduït no és vàlid! És correcta l'hora del servidor i del dispositiu?
   users:
diff --git a/config/locales/de.yml b/config/locales/de.yml
index db96f7de7..470395767 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -39,6 +39,7 @@ de:
     followers: Folgende
     following: Folgt
     media: Medien
+    moved_html: "%{name} ist auf %{new_profile_link} umgezogen:"
     nothing_here: Hier gibt es nichts!
     people_followed_by: Profile, denen %{name} folgt
     people_who_follow: Profile, die %{name} folgen
@@ -48,6 +49,7 @@ de:
     reserved_username: Dieser Profilname ist belegt
     roles:
       admin: Admin
+      moderator: Mod
     unfollow: Entfolgen
   admin:
     account_moderation_notes:
@@ -59,15 +61,21 @@ de:
       destroyed_msg: Moderationsnotiz erfolgreich gelöscht!
     accounts:
       are_you_sure: Bist du sicher?
+      by_domain: Domain
       confirm: Bestätigen
       confirmed: Bestätigt
+      demote: Degradieren
+      disable: Ausschalten
       disable_two_factor_authentication: 2FA abschalten
+      disabled: Ausgeschaltet
       display_name: Anzeigename
       domain: Domain
       edit: Bearbeiten
       email: E-Mail
+      enable: Freischalten
+      enabled: Freigegeben
       feed_url: Feed-URL
-      followers: Folgende
+      followers: Folger
       followers_url: Followers URL
       follows: Folgt
       inbox_url: Inbox URL
@@ -77,7 +85,9 @@ de:
         local: Lokal
         remote: Entfernt
         title: Ort
+      login_status: Loginstatus
       media_attachments: Medienanhänge
+      memorialize: In Gedenkmal verwandeln
       moderation:
         all: Alle
         silenced: Stummgeschaltet
@@ -94,6 +104,7 @@ de:
       outbox_url: Outbox URL
       perform_full_suspension: Vollständige Sperre durchführen
       profile_url: Profil-URL
+      promote: Befördern
       protocol: Protokoll
       public: Öffentlich
       push_subscription_expires: PuSH-Abonnement läuft aus
@@ -101,6 +112,11 @@ de:
       reset: Zurücksetzen
       reset_password: Passwort zurücksetzen
       resubscribe: Wieder abonnieren
+      role: Berechtigungen
+      roles:
+        admin: Administrator
+        moderator: Moderator
+        user: Nutzer
       salmon_url: Salmon-URL
       search: Suche
       shared_inbox_url: Shared Inbox URL
@@ -117,6 +133,29 @@ de:
       unsubscribe: Abbestellen
       username: Profilname
       web: Web
+    action_logs:
+      actions:
+        confirm_user: "%{name} hat die E-Mail-Adresse von %{target} bestätigt"
+        create_custom_emoji: "%{name} hat neues Emoji %{target} hochgeladen"
+        create_domain_block: "%{name} hat die Domain %{target} blockiert"
+        create_email_domain_block: "%{name} hat die E-Mail-Domain %{target} geblacklistet"
+        demote_user: "%{name} stufte Benutzer %{target} herunter"
+        destroy_domain_block: "%{name} hat die Domain %{target} entblockt"
+        destroy_email_domain_block: "%{name} hat die E-Mail-Domain %{target} gewhitelistet"
+        destroy_status: "%{name} hat Status von %{target} entfernt"
+        disable_custom_emoji: "%{name} hat Emoji %{target} deaktiviert"
+        enable_custom_emoji: "%{name} hat das %{target} Emoji aktiviert"
+        enable_user: "%{name} hat die Anmeldung für den Benutzer %{target} aktiviert"
+        memorialize_account: "%{name} hat %{target}s Profil in eine Gedenkseite umgewandelt"
+        reset_password_user: "%{name} hat das Passwort für den Benutzer %{target} zurückgesetzt"
+        resolve_report: "%{name} hat die Meldung %{target} abgelehnt"
+        silence_account: "%{name} hat %{target}s Account stummgeschaltet"
+        suspend_account: "%{name} hat %{target}s Account gesperrt"
+        unsilence_account: "%{name} hat die Stummschaltung von %{target}s Account aufgehoben"
+        unsuspend_account: "%{name} hat die Sperrung von %{target}s Account aufgehoben"
+        update_custom_emoji: "%{name} hat das %{target} Emoji aktualisiert"
+        update_status: "%{name} hat den Status von %{target} aktualisiert"
+      title: Überprüfungsprotokoll
     custom_emojis:
       copied_msg: Eine lokale Kopie des Emojis wurde erstellt
       copy: Kopieren
diff --git a/config/locales/devise.ar.yml b/config/locales/devise.ar.yml
index 169221352..4fd19244d 100644
--- a/config/locales/devise.ar.yml
+++ b/config/locales/devise.ar.yml
@@ -1,9 +1,14 @@
 ---
 ar:
   devise:
+    confirmations:
+      confirmed: تم التحقق من عنوان بريدك الإلكتروني بنجاح.
     failure:
+      already_authenticated: لقد تم تسجيل دخولك من قبل.
       inactive: لم يتم تنشيط حسابك بعد.
+      last_attempt: بإمكانك إعادة المحاولة مرة واحدة قبل أن يتم قفل حسابك.
       locked: إن حسابك مقفل.
+      unconfirmed: يجب عليك تأكيد عنوان بريدك الإلكتروني قبل المواصلة.
     mailer:
       password_change:
         subject: 'ماستدون : تم تغيير كلمة المرور'
@@ -15,6 +20,7 @@ ar:
       updated: تم تغيير كلمة المرور بنجاح. أنت مسجل الآن.
       updated_not_active: تم تغيير كلمة المرور بنجاح.
     registrations:
+      destroyed: إلى اللقاء ! لقد تم إلغاء حسابك. نتمنى أن نراك مجددا.
       signed_up: أهلا وسهلا ! تم تسجيل دخولك بنجاح.
       updated: تم تحديث حسابك بنجاح.
     sessions:
diff --git a/config/locales/devise.he.yml b/config/locales/devise.he.yml
index ddb688625..4a2811b1f 100644
--- a/config/locales/devise.he.yml
+++ b/config/locales/devise.he.yml
@@ -51,9 +51,9 @@ he:
       unlocked: נעילת חשבונך בוטלה בהצלחה. נא להכנס לחשבון על מנת להמשיך.
   errors:
     messages:
-      already_confirmed: כבר אושר. נא לנסות להכנס לחשבון.
-      confirmation_period_expired: האישור צריך להתקבל תוך %{period}, נא לבקש חדש.
-      expired: פג תוקפו. נא לבקש חדש.
+      already_confirmed: כבר אושר. נא לנסות להכנס לחשבון
+      confirmation_period_expired: האישור צריך להתקבל תוך %{period}, נא לבקש חדש
+      expired: פג תוקפו. נא לבקש חדש
       not_found: לא נמצא
       not_locked: לא היה נעול
       not_saved:
diff --git a/config/locales/devise.nl.yml b/config/locales/devise.nl.yml
index ff79d036e..8c2c7b6a1 100644
--- a/config/locales/devise.nl.yml
+++ b/config/locales/devise.nl.yml
@@ -35,7 +35,7 @@ nl:
       updated: Jouw wachtwoord is gewijzigd. Je bent nu ingelogd.
       updated_not_active: Jouw wachtwoord is gewijzigd.
     registrations:
-      destroyed: Jouw account is verwijderd. Wellicht tot ziens!
+      destroyed: Jouw account is succesvol verwijderd. Wellicht tot ziens.
       signed_up: Je bent geregistreerd.
       signed_up_but_inactive: Je bent geregistreerd. Je kon alleen niet automatisch ingelogd worden omdat jouw account nog niet geactiveerd is.
       signed_up_but_locked: Je bent ingeschreven. Je kon alleen niet automatisch ingelogd worden omdat jouw account is opgeschort.
@@ -43,6 +43,7 @@ nl:
       update_needs_confirmation: Je hebt je e-mailadres succesvol gewijzigd, maar we moeten je nieuwe mailadres nog bevestigen. Controleer jouw e-mail en klik op de link in de mail om jouw e-mailadres te bevestigen. Kijk tussen je spam wanneer niks werd ontvangen.
       updated: Jouw accountgegevens zijn opgeslagen.
     sessions:
+      already_signed_out: Succesvol uitgelogd.
       signed_in: Je bent succesvol ingelogd.
       signed_out: Je bent succesvol uitgelogd.
     unlocks:
diff --git a/config/locales/doorkeeper.ar.yml b/config/locales/doorkeeper.ar.yml
index 7980e107a..1925d5a65 100644
--- a/config/locales/doorkeeper.ar.yml
+++ b/config/locales/doorkeeper.ar.yml
@@ -30,7 +30,7 @@ ar:
         error: عفوا ! تحقق من خُلوّ الاستمارة من الأخطاء من فضلك
       help:
         native_redirect_uri: إستخدم %{native_redirect_uri} للاختبار و التجريب محليا
-        redirect_uri: إستخدم خطا واحدا لكل رابط.
+        redirect_uri: إستخدم خطا واحدا لكل رابط
         scopes: Separate scopes with spaces. Leave blank to use the default scopes.
       index:
         callback_url: رابط رد النداء
@@ -54,10 +54,10 @@ ar:
         title: حدث هناك خطأ
       new:
         able_to: يُخوَّل لهذا التطبيق القيام بـ
-        prompt: طلبَ تطبيق %{client_name} تصريحا لاستعمال حسابك.
+        prompt: طلبَ تطبيق %{client_name} تصريحا لاستعمال حسابك
         title: إذن بالتصريح
       show:
-        title: Copy this authorization code and paste it to the application.
+        title: قم بنسخ رمز المصادقة و إلصاقه على التطبيق.
     authorized_applications:
       buttons:
         revoke: إبطال التصريح
diff --git a/config/locales/doorkeeper.ca.yml b/config/locales/doorkeeper.ca.yml
index c2d2b79b9..c1748d05b 100644
--- a/config/locales/doorkeeper.ca.yml
+++ b/config/locales/doorkeeper.ca.yml
@@ -3,8 +3,10 @@ ca:
   activerecord:
     attributes:
       doorkeeper/application:
-        name: Nom
+        name: Nom de l'aplicació
         redirect_uri: URI per a redirecció
+        scopes: Àmbits
+        website: Lloc web de l'aplicació
     errors:
       models:
         doorkeeper/application:
@@ -33,9 +35,13 @@ ca:
         redirect_uri: Utilitza una línia per URI
         scopes: Separa els àmbits amb espais. Deixa-ho en blanc per a utilitzar els àmbits per defecte.
       index:
-        callback_url: Callback URL
+        application: Aplicació
+        callback_url: URL de retorn
+        delete: Suprimeix
         name: Nom
         new: Aplicació nova
+        scopes: Àmbits
+        show: Mostra
         title: Les teves aplicacions
       new:
         title: Aplicació nova
@@ -67,6 +73,7 @@ ca:
         application: Aplicació
         created_at: Creat el
         date_format: "%A-%m-%d %H:%M:%S"
+        scopes: Àmbits
         title: Les teves aplicacions autoritzades
     errors:
       messages:
@@ -76,12 +83,12 @@ ca:
         invalid_grant: La concessió d'autorizació oferida és invàlida, ha vençut, s'ha revocat, no coincideix amb l'URI de redirecció utilizada en la petició d'autorizació, o fou emesa per a un altre client.
         invalid_redirect_uri: L'URI de redirecció inclòs no és vàlid.
         invalid_request: En la petició manca un paràmetre necessari o inclou un valor de paràmetre no suportat o te un altre tipus de format incorrecte.
-        invalid_resource_owner: Les credencials del propietari del recurso proporcionat no son vàlides, o el propietari del recurs no pot ser trobat.
+        invalid_resource_owner: Les credencials del propietari del recurs proporcionat no son vàlides, o el propietari del recurs no pot ser trobat
         invalid_scope: L'àmbit demanat és invàlid, desconegut o erroni.
         invalid_token:
-          expired: L'identificador d'accés ha caducat.
-          revoked: L'identificador d'accés fou revocat.
-          unknown: L'identificador d'accés és invàlid.
+          expired: L'identificador d'accés ha caducat
+          revoked: L'identificador d'accés fou revocat
+          unknown: L'identificador d'accés és invàlid
         resource_owner_authenticator_not_configured: El propietari del recurs ha fallat perquè Doorkeeper.configure.resource_owner_authenticator està sense configurar.
         server_error: El servidor de l'autorizació ha trobat unca condició inesperada que ha impedit complir la sol·licitud.
         temporarily_unavailable: El servidor de l'autorizació és actualment incapaç de gestionar la petició degut a una sobrecàrrega temporal o una tasca de manteniment del servidor.
diff --git a/config/locales/doorkeeper.es.yml b/config/locales/doorkeeper.es.yml
index ca6e620d8..976fae82c 100644
--- a/config/locales/doorkeeper.es.yml
+++ b/config/locales/doorkeeper.es.yml
@@ -36,7 +36,7 @@ es:
         scopes: Separe los ámbitos con espacios. Déjelo en blanco para utilizar los ámbitos por defecto.
       index:
         application: Aplicación
-        callback_url: Callback URL
+        callback_url: URL de callback
         delete: Eliminar
         name: Nombre
         new: Nueva aplicación
@@ -48,7 +48,7 @@ es:
       show:
         actions: Acciones
         application_id: Id de la aplicación
-        callback_urls: Callback URLs
+        callback_urls: URLs de callback
         scopes: Ámbitos
         secret: Secreto
         title: 'Aplicación: %{name}'
@@ -63,7 +63,7 @@ es:
         prompt: La aplicación %{client_name} solicita tener acceso a su cuenta
         title: Se requiere autorización
       show:
-        title: Copy this authorization code and paste it to the application.
+        title: Copia este código de autorización y pégalo en la aplicación.
     authorized_applications:
       buttons:
         revoke: Revocar
@@ -83,12 +83,12 @@ es:
         invalid_grant: La concesión de autorización ofrecida es inválida, venció, se revocó, no coincide con la URI de redirección utilizada en la petición de autorización, o fue emitida para otro cliente.
         invalid_redirect_uri: La URI de redirección incluida no es válida.
         invalid_request: En la petición falta un parámetro necesario o incluye un valor de parámetro no soportado o tiene otro tipo de formato incorrecto.
-        invalid_resource_owner: Las credenciales del propietario del recurso proporcionado no son válidas, o el propietario del recurso no puede ser encontrado.
+        invalid_resource_owner: Las credenciales proporcionadas del propietario del recurso no son válidas, o el propietario del recurso no puede ser encontrado
         invalid_scope: El ámbito pedido es inválido, desconocido o erróneo.
         invalid_token:
-          expired: El identificador de acceso finalizó.
-          revoked: El identificador de acceso fue revocado.
-          unknown: El identificador de acceso es inválido.
+          expired: El autentificador de acceso expiró
+          revoked: El autentificador de acceso fue revocado
+          unknown: El autentificador de acceso es inválido
         resource_owner_authenticator_not_configured: El propietario del recurso falló debido a que Doorkeeper.configure.resource_owner_authenticator está sin configurar.
         server_error: El servidor de la autorización entontró una condición inesperada que le impidió cumplir con la solicitud.
         temporarily_unavailable: El servidor de la autorización es actualmente incapaz de manejar la petición debido a una sobrecarga temporal o un trabajo de mantenimiento del servidor.
diff --git a/config/locales/doorkeeper.fr.yml b/config/locales/doorkeeper.fr.yml
index 1dcc0da18..a6b58156a 100644
--- a/config/locales/doorkeeper.fr.yml
+++ b/config/locales/doorkeeper.fr.yml
@@ -6,6 +6,7 @@ fr:
         name: Nom
         redirect_uri: L’URL de redirection
         scope: Portée
+        scopes: Étendue
         website: Site web de l'application
     errors:
       models:
@@ -35,9 +36,13 @@ fr:
         redirect_uri: Utiliser une ligne par URL
         scopes: Séparer les portées avec des espaces. Laisser vide pour utiliser les portées par défaut.
       index:
+        application: Application
         callback_url: URL de retour d’appel
+        delete: Effacer
         name: Nom
         new: Nouvelle application
+        scopes: Scopes
+        show: Voir
         title: Vos applications
       new:
         title: Nouvelle application
diff --git a/config/locales/doorkeeper.he.yml b/config/locales/doorkeeper.he.yml
index 78e72a56f..d797b0ac9 100644
--- a/config/locales/doorkeeper.he.yml
+++ b/config/locales/doorkeeper.he.yml
@@ -5,6 +5,8 @@ he:
       doorkeeper/application:
         name: שם
         redirect_uri: קישורית הפניה
+        scopes: תחומים
+        website: אתר יישום
     errors:
       models:
         doorkeeper/application:
@@ -33,16 +35,20 @@ he:
         redirect_uri: שימוש בשורה אחת לכל קישורית
         scopes: יש להפריד תחומים בעזרת רווחים. נה להשאיר ריק על מנת להשתמש בתחום ברירת המחדל.
       index:
-        callback_url: Callback URL
+        application: יישום
+        callback_url: כתובת גישה חוזרת (Callback URL)
+        delete: למחוק
         name: שם
         new: ישום חדש
+        scopes: תחומים
+        show: להציג
         title: ישומך
       new:
         title: ישום חדש
       show:
         actions: פעולות
         application_id: זהות ישום
-        callback_urls: Callback URLs
+        callback_urls: כתובות לקריאה חוזרת (Callback URLs)
         scopes: תחומים
         secret: סוד
         title: 'ישום: %{name}'
@@ -57,7 +63,7 @@ he:
         prompt: ישום %{client_name} מבקש גישה לחשבונך
         title: נדרשת הרשאה
       show:
-        title: Copy this authorization code and paste it to the application.
+        title: יש להעתיק את קוד ההרשאה הזה ולהדביקו ביישום שביקש אותו.
     authorized_applications:
       buttons:
         revoke: ביטול
@@ -77,7 +83,7 @@ he:
         invalid_grant: חוזה ההרשאה המצורף אינו חוקי, אינו תקף, מבוטל, או שאינו מתאים לקישורית ההפניה שבשימוש על ידי בקשת ההרשאה, או שהופק על ידי לקוח אחר.
         invalid_redirect_uri: קישורית ההפניה המצורפת אינה חוקית.
         invalid_request: הבקשה חסרה פרמטר נדרש, מכילה פרמטר עם ערך שאיננו נתמך, או שתצורתה שגויה.
-        invalid_resource_owner: הרשאות בעלי המשאב שהוזנו אינן חוקיות, או שלא ניתן למצוא את בעלי המשאב.
+        invalid_resource_owner: הרשאות בעלי המשאב שהוזנו אינן חוקיות, או שלא ניתן למצוא את בעלי המשאב
         invalid_scope: התחום המבוקש אינו חוקי, אינו ידוע, או שתצורותו שגויה.
         invalid_token:
           expired: פג תוקף אסימון הגישה
@@ -104,7 +110,7 @@ he:
       admin:
         nav:
           applications: ישומים
-          oauth2_provider: OAuth2 Provider
+          oauth2_provider: ספק OAuth2
       application:
         title: נדרשת הרשאת OAuth
     scopes:
diff --git a/config/locales/doorkeeper.nl.yml b/config/locales/doorkeeper.nl.yml
index f97aa8111..3dd0a7d26 100644
--- a/config/locales/doorkeeper.nl.yml
+++ b/config/locales/doorkeeper.nl.yml
@@ -32,7 +32,7 @@ nl:
         error: Oops! Controleer het formulier op fouten
       help:
         native_redirect_uri: Gebruik %{native_redirect_uri} voor lokale tests
-        redirect_uri: 'Gebruik één regel per URI. '
+        redirect_uri: Gebruik één regel per URI
         scopes: Toestemmingen met spaties van elkaar scheiden. Laat leeg om de standaardtoestemmingen te gebruiken.
       index:
         application: Toepassing
@@ -60,10 +60,10 @@ nl:
         title: Er is een fout opgetreden
       new:
         able_to: Deze toepassing zal in staat zijn om
-        prompt: "%{client_name} autoriseren om uw account te gebruiken?"
+        prompt: "%{client_name} autoriseren om je account te gebruiken"
         title: Autorisatie vereist
       show:
-        title: Copy this authorization code and paste it to the application.
+        title: Kopieer deze autorisatiecode en plak het in de applicatie.
     authorized_applications:
       buttons:
         revoke: Intrekken
@@ -73,6 +73,7 @@ nl:
         application: Toepassing
         created_at: Aangemaakt op
         date_format: "%d-%m-%Y %H:%M:%S"
+        scopes: Toestemmingen
         title: Jouw geautoriseerde toepassingen
     errors:
       messages:
@@ -82,7 +83,7 @@ nl:
         invalid_grant: De verstrekte autorisatie is ongeldig, verlopen, ingetrokken, komt niet overeen met de redirect-URI die is opgegeven of werd uitgegeven aan een andere client.
         invalid_redirect_uri: De opgegeven redirect-URI is ongeldig.
         invalid_request: Het verzoek mist een vereiste parameter, bevat een niet ondersteunde parameterwaarde of is anderszins onjuist.
-        invalid_resource_owner: De verstrekte resource-eigenaargegevens zijn ogeldig of de resource-eigenaar kan niet worden gevonden.
+        invalid_resource_owner: De verstrekte resource-eigenaargegevens zijn ongeldig of de resource-eigenaar kan niet worden gevonden
         invalid_scope: De opgevraagde scope is ongeldig, onbekend of onjuist.
         invalid_token:
           expired: Het toegangssleutel is verlopen
@@ -92,8 +93,8 @@ nl:
         server_error: De autorisatieserver is is een onverwachte situatie tegengekomen die het verzoek verhinderde.
         temporarily_unavailable: De autorisatieserver is momenteel niet in staat het verzoek te behandelen als gevolg van een tijdelijke overbelasting of onderhoud aan de server.
         unauthorized_client: De client is niet bevoegd om dit verzoek op deze manier uit te voeren.
-        unsupported_grant_type: Het type autorisatie wordt niet door de autorisatieserver ondersteund
-        unsupported_response_type: De autorisatieserver ondersteund dit antwoordtype niet
+        unsupported_grant_type: Het type autorisatie wordt niet door de autorisatieserver ondersteund.
+        unsupported_response_type: De autorisatieserver ondersteund dit antwoordtype niet.
     flash:
       applications:
         create:
diff --git a/config/locales/doorkeeper.pt-BR.yml b/config/locales/doorkeeper.pt-BR.yml
index 53cadec38..f3da6fcd1 100644
--- a/config/locales/doorkeeper.pt-BR.yml
+++ b/config/locales/doorkeeper.pt-BR.yml
@@ -51,7 +51,7 @@ pt-BR:
         callback_urls: URLs de retorno
         scopes: Autorizações
         secret: Segredo do cliente
-        title: 'Application: %{name}'
+        title: 'Aplicativo: %{name}'
     authorizations:
       buttons:
         authorize: Autorizar
@@ -63,7 +63,7 @@ pt-BR:
         prompt: O aplicativo %{client_name} solicita acesso à sua conta
         title: Autorização necessária
       show:
-        title: Copy this authorization code and paste it to the application.
+        title: Copie este código de autorização e cole no aplicativo.
     authorized_applications:
       buttons:
         revoke: Revogar
@@ -83,7 +83,7 @@ pt-BR:
         invalid_grant: A garantia de autorização é inválida, expirou, foi revogada, não é equivalente à URI de redirecionamento usada da solicitação de autorização ou foi emitida por outro cliente.
         invalid_redirect_uri: A URI de redirecionamento incluída não é válida.
         invalid_request: A solicitação não possui um parâmetro obrigatório, inclui um valor não suportado ou está mal formatada.
-        invalid_resource_owner: As credenciais do proprietário não são válidas ou o proprietário não pôde ser encontrado.
+        invalid_resource_owner: As credenciais do proprietário informadas não são válidas ou o proprietário não pôde ser encontrado
         invalid_scope: A autorização requirida é inválida, desconhecida ou está mal formatada.
         invalid_token:
           expired: O token de acesso expirou
diff --git a/config/locales/doorkeeper.pt.yml b/config/locales/doorkeeper.pt.yml
index 30d9f7f5a..039b90ffa 100644
--- a/config/locales/doorkeeper.pt.yml
+++ b/config/locales/doorkeeper.pt.yml
@@ -3,8 +3,10 @@ pt:
   activerecord:
     attributes:
       doorkeeper/application:
-        name: Nome
-        redirect_uri: Redirect URI
+        name: Nome da Aplicação
+        redirect_uri: URL de redirecionamento
+        scopes: Scopes
+        website: Site da Aplicação
     errors:
       models:
         doorkeeper/application:
@@ -33,9 +35,12 @@ pt:
         redirect_uri: Utiliza uma linha por URI
         scopes: Separate scopes with spaces. Leave blank to use the default scopes.
       index:
+        application: Aplicações
         callback_url: Callback URL
+        delete: Eliminar
         name: Nome
         new: Nova Aplicação
+        show: Mostrar
         title: As tuas aplicações
       new:
         title: Nova aplicação
@@ -57,7 +62,7 @@ pt:
         prompt: Aplicação %{client_name} pede acesso à tua conta
         title: Autorização é necessária
       show:
-        title: Copy this authorization code and paste it to the application.
+        title: Copiar o código desta autorização e colar na aplicação.
     authorized_applications:
       buttons:
         revoke: Revogar
diff --git a/config/locales/es.yml b/config/locales/es.yml
index ca3bdd983..18b93b08e 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -1,6 +1,7 @@
 ---
 es:
   about:
+    about_hashtag_html: Estos son toots públicos etiquetados con <strong>#%{hashtag}</strong>. Puedes interactuar con ellos si tienes una cuenta en cualquier parte del fediverso.
     about_mastodon_html: Mastodon es un servidor de red social <em>libre y de código abierto</em>. Una alternativa <em>descentralizada</em> a plataformas comerciales, que evita el riesgo de que una única compañía monopolice tu comunicación. Cualquiera puede ejecutar Mastodon y participar sin problemas en la <em>red social</em>.
     about_this: Acerca de esta instancia
     closed_registrations: Los registros están actualmente cerrados en esta instancia.
@@ -21,7 +22,7 @@ es:
       real_conversation_body: Con 500 caracteres a tu disposición y soporte para contenido granular y advertencias de contenido, puedes expresarte como quieras.
       real_conversation_title: Hecho para verdaderas conversaciones
       within_reach_body: Aplicaciones múltiples para iOS, Android, y otras plataformas gracias a un ecosistema de APIs amigable al desarrollador para permitirte estar con tus amigos donde sea.
-      within_reach_title: Always within reach
+      within_reach_title: Siempre al alcance
     find_another_instance: Busca otra instancia
     generic_description: "%{domain} es un servidor en la red"
     hosted_on: Mastodon hosteado en %{domain}
@@ -38,6 +39,7 @@ es:
     followers: Seguidores
     following: Siguiendo
     media: Media
+    moved_html: "%{name} se ha trasladado a %{new_profile_link}:"
     nothing_here: "¡No hay nada aquí!"
     people_followed_by: Usuarios a quien %{name} sigue
     people_who_follow: Usuarios que siguen a %{name}
@@ -47,17 +49,31 @@ es:
     reserved_username: El nombre de usuario está reservado
     roles:
       admin: Administrador
+      moderator: Moderador
     unfollow: Dejar de seguir
   admin:
+    account_moderation_notes:
+      account: Moderador
+      create: Crear
+      created_at: Fecha
+      created_msg: "¡Nota de moderación creada con éxito!"
+      delete: Borrar
+      destroyed_msg: "¡Nota de moderación destruida con éxito!"
     accounts:
       are_you_sure: "¿Estás seguro?"
+      by_domain: Dominio
       confirm: Confirmar
       confirmed: Confirmado
+      demote: Degradar
+      disable: Deshabilitar
       disable_two_factor_authentication: Desactivar autenticación de dos factores
+      disabled: Deshabilitada
       display_name: Nombre
       domain: Dominio
       edit: Editar
       email: E-mail
+      enable: Habilitar
+      enabled: Habilitada
       feed_url: URL de notificaciones
       followers: Seguidores
       followers_url: URL de los seguidores
@@ -69,12 +85,15 @@ es:
         local: Local
         remote: Remoto
         title: Localización
+      login_status: Estado del login
       media_attachments: Multimedia
+      memorialize: Convertir en memorial
       moderation:
         all: Todos
         silenced: Silenciados
         suspended: Suspendidos
         title: Moderación
+      moderation_notes: Notas de moderación
       most_recent_activity: Actividad más reciente
       most_recent_ip: IP más reciente
       not_subscribed: No se está suscrito
@@ -85,6 +104,7 @@ es:
       outbox_url: URL de bandeja de salida
       perform_full_suspension: Performar suspensión completa
       profile_url: URL del perfil
+      promote: Promocionar
       protocol: Protocolo
       public: Público
       push_subscription_expires: Expiración de la suscripción PuSH
@@ -92,6 +112,11 @@ es:
       reset: Reiniciar
       reset_password: Reiniciar contraseña
       resubscribe: Re-suscribir
+      role: Permisos
+      roles:
+        admin: Administrador
+        moderator: Moderador
+        user: Usuario
       salmon_url: URL de salmón
       search: Buscar
       shared_inbox_url: URL de bandeja compartida
@@ -108,6 +133,56 @@ es:
       unsubscribe: Desuscribir
       username: Nombre de usuario
       web: Web
+    action_logs:
+      actions:
+        confirm_user: "%{name} confirmó la dirección de correo del usuario %{target}"
+        create_custom_emoji: "%{name} subió un nuevo emoji %{target}"
+        create_domain_block: "%{name} bloqueó el dominio %{target}"
+        create_email_domain_block: "%{name} puso en lista negra el dominio de correos %{target}"
+        demote_user: "%{name} degradó al usuario %{target}"
+        destroy_domain_block: "%{name} desbloqueó el dominio %{target}"
+        destroy_email_domain_block: "%{name} puso en lista blanca el dominio de correos %{target}"
+        destroy_status: "%{name} eliminó el estado de %{target}"
+        disable_2fa_user: "%{name} deshabilitó el requerimiento de dos factores para el usuario %{target}"
+        disable_custom_emoji: "%{name} deshabilitó el emoji %{target}"
+        disable_user: "%{name} deshabilitó el acceso del usuario %{target}"
+        enable_custom_emoji: "%{name} habilitó el emoji %{target}"
+        enable_user: "%{name} habilitó el acceso del usuario %{target}"
+        memorialize_account: "%{name} convirtió la cuenta de %{target} en una página de memorial"
+        promote_user: "%{name} promoción al usuario %{target}"
+        reset_password_user: "%{name} restauró la contraseña del usuario %{target}"
+        resolve_report: "%{name} desestimó el reporte %{target}"
+        silence_account: "%{name} silenció la cuenta de %{target}"
+        suspend_account: "%{name} suspendió la cuenta de %{target}"
+        unsilence_account: "%{name} desactivó el silenciado de la cuenta de %{target}"
+        unsuspend_account: "%{name} desactivó la suspensión de la cuenta de %{target}"
+        update_custom_emoji: "%{name} actualizó el emoji %{target}"
+        update_status: "%{name} actualizó el estado de %{target}"
+      title: Log de auditoría
+    custom_emojis:
+      copied_msg: Copia local del emoji creada con éxito
+      copy: Copiar
+      copy_failed_msg: No se pudo realizar una copia local de ese emoji
+      created_msg: "¡Emoji creado con éxito!"
+      delete: Borrar
+      destroyed_msg: "¡Emojo destruido con éxito!"
+      disable: Deshabilitar
+      disabled_msg: Se deshabilitó con éxito ese emoji
+      emoji: Emoji
+      enable: Habilitar
+      enabled_msg: Se habilitó con éxito ese emoji
+      image_hint: PNG de hasta 50KB
+      listed: Listados
+      new:
+        title: Añadir nuevo emoji personalizado
+      overwrite: Sobrescribir
+      shortcode: Código de atajo
+      shortcode_hint: Al menos 2 caracteres, solo caracteres alfanuméricos y guiones bajos
+      title: Emojis personalizados
+      unlisted: Sin listar
+      update_failed_msg: No se pudo actualizar ese emoji
+      updated_msg: "¡Emoji actualizado con éxito!"
+      upload: Subir
     domain_blocks:
       add_new: Añadir nuevo
       created_msg: El bloque de dominio está siendo procesado
@@ -140,12 +215,29 @@ es:
         undo: Deshacer
       title: Bloques de Dominio
       undo: Deshacer
+    email_domain_blocks:
+      add_new: Añadir nuevo
+      created_msg: Dominio de correo añadido a la lista negra con éxito
+      delete: Borrar
+      destroyed_msg: Dominio de correo borrado de la lista negra con éxito
+      domain: Dominio
+      new:
+        create: Añadir dominio
+        title: Nueva entrada en la lista negra de correo
+      title: Lista negra de correo
     instances:
       account_count: Cuentas conocidas
       domain_name: Dominio
       reset: Reiniciar
       search: Buscar
       title: Instancias conocidas
+    invites:
+      filter:
+        all: Todas
+        available: Disponibles
+        expired: Expiradas
+        title: Filtrar
+      title: Invitaciones
     reports:
       action_taken_by: Acción tomada por
       are_you_sure: "¿Estás seguro?"
@@ -172,7 +264,7 @@ es:
       view: Ver
     settings:
       bootstrap_timeline_accounts:
-        desc_html: Separa nombres de usuarios múltiples con coma. Solo funcionará con cuentas desbloqueadas. Si está vacío, el predeterminado son todos los administradores locales
+        desc_html: Separa con comas los nombres de usuario. Solo funcionará para cuentas locales desbloqueadas. Si se deja vacío, se tomará como valor por defecto a todos los administradores locales.
         title: Seguimientos predeterminados para usuarios nuevos
       contact_information:
         email: Correo de trabajo
@@ -184,9 +276,15 @@ es:
         deletion:
           desc_html: Permite a cualquiera a eliminar su cuenta
           title: Eliminación de cuenta abierta
+        min_invite_role:
+          disabled: Nadie
+          title: Permitir invitaciones de
         open:
           desc_html: Permite a cualquiera a registrar una cuenta
           title: Registro abierto
+      show_staff_badge:
+        desc_html: Mostrar un parche de staff en la página de un usuario
+        title: Mostrar parche de staff
       site_description:
         desc_html: Párrafo introductorio en la portada y en meta tags. Puedes usar tags HTML, en particular <code>&lt;a&gt;</code> y <code>&lt;em&gt;</code>.
         title: Descripción de instancia
@@ -232,6 +330,7 @@ es:
       body: "%{reporter} ha reportado a %{target}"
       subject: Nuevo reporte para la %{instance} (#%{id})
   application_mailer:
+    salutation: "%{name},"
     settings: 'Cambiar preferencias de correo: %{link}'
     signature: Notificaciones de Mastodon desde %{instance}
     view: 'Vista:'
@@ -246,12 +345,15 @@ es:
   auth:
     agreement_html: Al registrarte aceptas <a href="%{rules_path}">nuestros y términos y condiciones del servicio</a> y <a href="%{terms_path}">nuestras políticas de privacidda</a>.
     change_password: Cambiar contraseña
+    delete_account: Borrar cuenta
     delete_account_html: Si desea eliminar su cuenta, puede <a href="%{path}">proceder aquí</a>. Será pedido de una confirmación.
     didnt_get_confirmation: "¿No recibió el correo de confirmación?"
     forgot_password: "¿Olvidaste tu contraseña?"
     invalid_reset_password_token: El token de reinicio de contraseña es inválido o expiró. Por favor pide uno nuevo.
     login: Iniciar sesión
     logout: Cerrar sesión
+    migrate_account: Mudarse a otra cuenta
+    migrate_account_html: Si deseas redireccionar esta cuenta a otra distinta, puedes <a href="%{path}">configurarlo aquí</a>.
     register: Registrarse
     resend_confirmation: Volver a enviar el correo de confirmación
     reset_password: Restablecer contraseña
@@ -296,11 +398,15 @@ es:
       content: Verificación de seguridad fallida. ¿Estás bloqueando algunas cookies?
       title: Verificación de seguridad fallida
     '429': Asfixiado
+    '500':
+      content: Lo sentimos, algo ha funcionado mal por nuestra parte.
+      title: Esta página no es correcta
     noscript_html: Para usar la aplicación web de Mastodon, por favor activa Javascript. Alternativamente, prueba alguna de las <a href="https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md">aplicaciones nativas</a> para Mastodon para tu plataforma.
   exports:
     blocks: Personas que has bloqueado
     csv: CSV
     follows: Personas que sigues
+    mutes: Tienes en silencio
     storage: Almacenamiento
   followers:
     domain: Dominio
@@ -329,19 +435,47 @@ es:
       following: Lista de seguidos
       muting: Lista de silenciados
     upload: Cargar
+  in_memoriam_html: In Memoriam.
+  invites:
+    delete: Desactivar
+    expired: Expiradas
+    expires_in:
+      '1800': 30 minutos
+      '21600': 6 horas
+      '3600': 1 hora
+      '43200': 12 horas
+      '86400': 1 día
+    expires_in_prompt: Nunca
+    generate: Generar
+    max_uses:
+      one: 1 uso
+      other: "%{count} usos"
+    max_uses_prompt: Sin límite
+    prompt: Generar y compartir enlaces con otros para conceder acceso a este nodo
+    table:
+      expires_at: Expira
+      uses: Usos
+    title: Invitar a gente
   landing_strip_html: "<strong>%{name}</strong> es un usuario en %{link_to_root_path}. Puedes seguirlo(a) o interactuar con el o ella si tienes una cuenta en cualquier parte del fediverse."
   landing_strip_signup_html: Si no tienes una, puedes <a href="%{sign_up_path}">registrarte aquí</a>.
   media_attachments:
     validations:
       images_and_video: No se puede adjuntar un video a un estado que ya contenga imágenes
       too_many: No se pueden adjuntar más de 4 archivos
+  migrations:
+    acct: username@domain de la nueva cuenta
+    currently_redirecting: 'Tu perfil está redireccionado a:'
+    proceed: Guardar
+    updated_msg: "¡La configuración de migración de tu cuenta  ha sido actualizada con éxito!"
+  moderation:
+    title: Moderación
   notification_mailer:
     digest:
       body: 'Un resumen de lo que te perdiste en %{instance} desde tu última visita el %{since}:'
       mention: "%{name} te ha mencionado en:"
       new_followers_summary:
-        one: "¡Hurra! Alguien más te ha comenzado a seguir"
-        other: "¡Genial! Te han seguido %{count} nuevas personas"
+        one: "¡Hurra! ¡Tienes un nuevo seguidor!"
+        other: "¡Genial! ¡Tienes %{count} nuevos seguidores!"
       subject:
         one: "1 nueva notificación desde tu última visita \U0001F418"
         other: "%{count} nuevas notificaciones desde tu última visita \U0001F418"
@@ -358,7 +492,7 @@ es:
       body: 'Fuiste mencionado por %{name} en:'
       subject: Fuiste mencionado por %{name}
     reblog:
-      body: "%{name} ha retooteado tu estado"
+      body: "%{name} ha retooteado tu estado:"
       subject: "%{name} ha retooteado tu estado"
   number:
     human:
@@ -375,6 +509,11 @@ es:
     next: Próximo
     prev: Anterior
     truncate: "&hellip;"
+  preferences:
+    languages: Idiomas
+    other: Otros
+    publishing: Publicación
+    web: Web
   push_notifications:
     favourite:
       title: "%A {name} le gustó tu estado"
@@ -383,7 +522,7 @@ es:
     group:
       title: "%{count} notificaciones"
     mention:
-      action_boost: Boost
+      action_boost: Retoot
       action_expand: Mostrar más
       action_favourite: Me Gusta
       title: "%{name} te mencionó"
@@ -391,7 +530,7 @@ es:
       title: "%{name} boosteó tu estado"
   remote_follow:
     acct: Ingesa el usuario@dominio de la persona que quieres seguir
-    missing_resource: No se pudo encontrar la URL de redirección necesaria para su cuenta.
+    missing_resource: No se pudo encontrar la URL de redirección requerida para tu cuenta
     proceed: Proceder a seguir
     prompt: 'Vas a seguir a:'
   sessions:
@@ -430,15 +569,20 @@ es:
       windows: Windows
       windows_mobile: Windows Mobile
       windows_phone: Windows Phone
-    revoke: Revoke
+    revoke: Revocar
     revoke_success: Sesión revocada exitosamente
     title: Sesiones
   settings:
     authorized_apps: Aplicaciones autorizadas
     back: Volver al inicio
+    delete: Borrar cuenta
+    development: Desarrollo
     edit_profile: Editar perfil
     export: Exportar información
+    followers: Seguidores autorizados
     import: Importar
+    migrate: Migración de cuenta
+    notifications: Notificaciones
     preferences: Preferencias
     settings: Ajustes
     two_factor_authentication: Autenticación de dos factores
@@ -534,6 +678,8 @@ es:
 
       <p>Adaptado originalmente del <a href="https://github.com/discourse/discourse">discurso de las políticas de privacidad</a>.</p>
     title: Términos del Servicio y Políticas de Privacidad de %{instance}
+  themes:
+    default: Mastodon
   time:
     formats:
       default: "%d de %b del %Y, %H:%M"
@@ -542,11 +688,14 @@ es:
     description_html: Si habilitas la <strong>autenticación de dos factores</strong>, se requerirá estar en posesión de su teléfono, lo que generará tokens para que usted pueda iniciar sesión.
     disable: Deshabilitar
     enable: Habilitar
+    enabled: La autenticación de dos factores está activada
     enabled_success: Verificación de dos factores activada exitosamente
     generate_recovery_codes: generar códigos de recuperación
     instructions_html: "<strong>Escanea este código QR desde Google Authenticator o una aplicación similar en su teléfono</strong>. Desde ahora, esta aplicación va a generar tokens que tienes que ingresar cuando quieras iniciar sesión."
     lost_recovery_codes: Los códigos de recuperación te permiten obtener acceso a tu cuenta si pierdes tu teléfono. Si has perdido tus códigos de recuperación, puedes regenerarlos aquí. Tus viejos códigos de recuperación se harán inválidos.
+    manual_instructions: 'Si no puedes escanear el código QR y necesitas introducirlo manualmente, este es el secreto en texto plano:'
     recovery_codes: Hacer copias de seguridad de tus códigos de recuperación
+    recovery_codes_regenerated: Códigos de recuperación regenerados con éxito
     recovery_instructions_html: Si pierdes acceso a tu teléfono, puedes usar uno de los siguientes códigos de recuperación para obtener acceso a tu cuenta. <strong>Mantenlos a salvo</strong>. Por ejemplo, puedes imprimirlos y guardarlos con otros documentos importantes.
     setup: Configurar
     wrong_code: "¡El código ingresado es inválido! ¿El dispositivo y tiempo del servidor están correctos?"
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 2fd875b2c..cd97f5967 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -39,6 +39,7 @@ fr:
     followers: Abonné⋅e⋅s
     following: Abonnements
     media: Médias
+    moved_html: "%{name} a déménagé vers %{new_profile_link} :"
     nothing_here: Rien à voir ici !
     people_followed_by: Personnes suivies par %{name}
     people_who_follow: Personnes qui suivent %{name}
@@ -48,6 +49,7 @@ fr:
     reserved_username: Ce nom d’utilisateur⋅ice est réservé
     roles:
       admin: Admin
+      moderator: Modérateur·trice
     unfollow: Ne plus suivre
   admin:
     account_moderation_notes:
@@ -59,13 +61,19 @@ fr:
       destroyed_msg: Note de modération supprimée avec succès !
     accounts:
       are_you_sure: Êtes-vous certain⋅e ?
+      by_domain: Domaine
       confirm: Confirmer
       confirmed: Confirmé
+      demote: Rétrograder
+      disable: Désactiver
       disable_two_factor_authentication: Désactiver l’authentification à deux facteurs
+      disabled: Désactivé
       display_name: Nom affiché
       domain: Domaine
       edit: Éditer
       email: Courriel
+      enable: Activer
+      enabled: Activé
       feed_url: URL du flux
       followers: Abonné⋅e⋅s
       followers_url: URL des abonné·e·s
@@ -77,7 +85,9 @@ fr:
         local: Local
         remote: Distant
         title: Situation
+      login_status: Statut de connexion
       media_attachments: Fichiers médias
+      memorialize: Convertir en mémorial
       moderation:
         all: Tous
         silenced: Masqués
@@ -94,13 +104,19 @@ fr:
       outbox_url: URL de sortie
       perform_full_suspension: Effectuer une suspension complète
       profile_url: URL du profil
+      promote: Promouvoir
       protocol: Protocole
-      public: Public
+      public: Publique
       push_subscription_expires: Expiration de l’abonnement PuSH
       redownload: Rafraîchir les avatars
       reset: Réinitialiser
       reset_password: Réinitialiser le mot de passe
       resubscribe: Se réabonner
+      role: Permissions
+      roles:
+        admin: Administrateur
+        moderator: Modérateur
+        user: Utilisateur
       salmon_url: URL Salmon
       search: Rechercher
       shared_inbox_url: URL de la boite de réception partagée
@@ -117,6 +133,31 @@ fr:
       unsubscribe: Se désabonner
       username: Nom d’utilisateur⋅ice
       web: Web
+    action_logs:
+      actions:
+        confirm_user: "%{name} adresse e-mail confirmée de l'utilisateur %{target}"
+        create_custom_emoji: "%{name} a importé de nouveaux emoji %{target}"
+        create_domain_block: "%{name} a bloqué le domaine %{target}"
+        create_email_domain_block: "%{name} a blacklisté le domaine de l'e-mail %{target}"
+        destroy_domain_block: "%{name} a débloqué le domaine %{target}"
+        destroy_email_domain_block: "%{name} a mis le domaine de l'e-mail %{target} sur liste blanche"
+        destroy_status: "%{name} a enlevé le statut de %{target}"
+        disable_2fa_user: "%{name} a désactivé l'authentification à deux facteurs pour l'utilisateur %{target}"
+        disable_custom_emoji: "%{name} a désactivé l'emoji %{target}"
+        disable_user: "%{name} a désactivé le login pour l'utilisateur %{target}"
+        enable_custom_emoji: "%{name} a activé l'emoji %{target}"
+        enable_user: "%{name} a activé le login pour l'utilisateur %{target}"
+        memorialize_account: "%{name} a transformé le compte de %{target} en une page de mémorial"
+        promote_user: "%{name} a promu l'utilisateur %{target}"
+        reset_password_user: "%{name} a réinitialisé le mot de passe de %{target}"
+        resolve_report: "%{name} n'a pas pris en compte la dénonciation de %{target}"
+        silence_account: "%{name} a mis le compte %{target} en mode silence"
+        suspend_account: "%{name} a suspendu le compte %{target}"
+        unsilence_account: "%{name} a mis fin au mode silence de %{target}"
+        unsuspend_account: "%{name} a réactivé le compte de %{target}"
+        update_custom_emoji: "%{name} a mis à jour l'emoji %{target}"
+        update_status: "%{name} a mis à jour le statut de %{target}"
+      title: Journal d'audit
     custom_emojis:
       copied_msg: Copie locale de l’émoji créée avec succès !
       copy: Copier
@@ -130,11 +171,16 @@ fr:
       enable: Activer
       enabled_msg: Émoji activé avec succès
       image_hint: PNG de moins de 50 Ko
+      listed: Listé
       new:
         title: Ajouter un nouvel émoji personnalisé
+      overwrite: Réécrire
       shortcode: Raccourci
       shortcode_hint: Au moins deux caractères, seulement des caractères alphanumériques ou des tirets bas
       title: Émoji personnalisés
+      unlisted: Délisté
+      update_failed_msg: N'a pas pu mettre à jour cet emoji
+      updated_msg: Emoji mis à jour avec succès !
       upload: Téléverser
     domain_blocks:
       add_new: Ajouter
@@ -145,13 +191,13 @@ fr:
         create: Créer le blocage
         hint: Le blocage de domaine n’empêchera pas la création de comptes dans la base de données, mais il appliquera automatiquement et rétrospectivement des méthodes de modération spécifiques sur ces comptes.
         severity:
-          desc_html: "<strong>Silence</strong> rendra les messages des comptes concernés invisibles à ceux qui ne les suivent pas. <strong>Suspend</strong> supprimera tout le contenu des comptes concernés, les médias, et les données du profil."
+          desc_html: "<strong>Silence</strong> rendra les messages des comptes concernés invisibles à ceux qui ne les suivent pas. <strong>Suspendre</strong> supprimera tout le contenu des comptes concernés, les médias, et les données du profil."
           noop: Aucune
           silence: Masqué
           suspend: Suspendre
         title: Nouveau blocage de domaine
       reject_media: Fichiers média rejetés
-      reject_media_hint: Supprime localement les fichiers média stockés et refuse d’en télécharger ultérieurement. Ne concerne pas les suspensions.
+      reject_media_hint: Supprime localement les fichiers média stockés et refuse d’en télécharger ultérieurement. Ne concerne pas les suspensions
       severities:
         noop: Aucune
         silence: Masquer
@@ -184,6 +230,13 @@ fr:
       reset: Réinitialiser
       search: Rechercher
       title: Instances connues
+    invites:
+      filter:
+        all: Tout
+        available: Disponible
+        expired: Expiré
+        title: Filtre
+      title: Invitations
     reports:
       action_taken_by: Intervention de
       are_you_sure: Êtes vous certain⋅e ?
@@ -222,11 +275,17 @@ fr:
         deletion:
           desc_html: Permettre à tou·te·s les utilisateur·ice·s de supprimer leur compte
           title: Autoriser les suppressions de compte
+        min_invite_role:
+          disabled: Personne
+          title: Autoriser les invitations par
         open:
           desc_html: Autoriser tout le monde à créer un compte
           title: Ouvrir les inscriptions
+      show_staff_badge:
+        desc_html: Montrer un badge de responsable sur une page utilisateur
+        title: Montrer un badge de responsable
       site_description:
-        desc_html: Affichée sous la forme d’un paragraphe sur la page d’accueil et utilisée comme balise meta.<br>Vous pouvez utiliser des balises HTML, en particulier <code>&lt;a&gt;</code> et <code>&lt;em&gt;</code>.
+        desc_html: Affichée sous la forme d’un paragraphe sur la page d’accueil et utilisée comme balise meta.<br/>Vous pouvez utiliser des balises HTML, en particulier <code>&lt;a&gt;</code> et <code>&lt;em&gt;</code>.
         title: Description du site
       site_description_extended:
         desc_html: Affichée sur la page d’informations complémentaires du site<br>Vous pouvez utiliser des balises HTML
@@ -273,7 +332,7 @@ fr:
     salutation: "%{name},"
     settings: 'Changer les préférences courriel : %{link}'
     signature: Notifications de Mastodon depuis %{instance}
-    view: 'Voir :'
+    view: 'Voir :'
   applications:
     created: Application créée avec succès
     destroyed: Application supprimée avec succès
@@ -292,6 +351,8 @@ fr:
     invalid_reset_password_token: Le lien de réinitialisation du mot de passe est invalide ou a expiré. Merci de réessayer.
     login: Se connecter
     logout: Se déconnecter
+    migrate_account: Déplacer vers un compte différent
+    migrate_account_html: Si vous voulez rediriger ce compte vers un autre, vous pouvez le <a href="%{path}">configurer ici</a>.
     register: S’inscrire
     resend_confirmation: Envoyer à nouveau les consignes de confirmation
     reset_password: Réinitialiser le mot de passe
@@ -299,8 +360,8 @@ fr:
   authorize_follow:
     error: Malheureusement, il y a eu une erreur en cherchant les détails du compte distant
     follow: Suivre
-    follow_request: 'Vous avez demandé à suivre:'
-    following: 'Youpi ! Vous suivez :'
+    follow_request: 'Vous avez demandé à suivre :'
+    following: 'Youpi ! Vous suivez  :'
     post_follow:
       close: Ou bien, vous pouvez fermer cette fenêtre.
       return: Retour au profil de l’utilisateur⋅ice
@@ -323,7 +384,7 @@ fr:
   deletes:
     bad_password_msg: Bien essayé ! Mot de passe incorrect
     confirm_password: Entrez votre mot de passe pour vérifier votre identité
-    description_html: Cela va supprimer votre compte et le désactiver de manière <strong>permanente et irréversible</strong>. Votre nom d’utilisateur⋅ice restera réservé afin d’éviter la confusion
+    description_html: Cela va supprimer votre compte et le désactiver de manière <strong>permanente et irréversible</strong>. Votre nom d’utilisateur⋅ice restera réservé afin d’éviter la confusion.
     proceed: Supprimer compte
     success_msg: Votre compte a été supprimé avec succès
     warning_html: Seule la suppression du contenu depuis cette instance est garantie. Le contenu qui a été partagé est susceptible de laisser des traces. Les serveurs hors-lignes ainsi que ceux n’étant plus abonnés à vos publications ne mettront pas leur base de données à jour.
@@ -335,11 +396,11 @@ fr:
     '422':
       content: Vérification de sécurité échouée. Bloquez-vous les cookies ?
       title: Vérification de sécurité échouée
-    '429': Trop de requêtes émises dans un délai donné.
+    '429': Trop de requêtes émises dans un délai donné
     '500':
       content: Nous sommes désolé·e·s, mais quelque chose s’est mal passé de notre côté.
       title: Cette page n’est pas correcte
-    noscript_html: Pour utiliser Mastodon, veuillez activer JavaScript
+    noscript_html: Pour utiliser Mastodon, veuillez activer JavaScript. Sinon, essayez l'une des <a href="https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md">applications natives</a> pour Mastodon pour votre plate-forme.
   exports:
     blocks: Vous bloquez
     csv: CSV
@@ -363,8 +424,8 @@ fr:
     powered_by: propulsé par %{link}
     save_changes: Enregistrer les modifications
     validation_errors:
-      one: Quelque chose ne va pas ! Vérifiez l’erreur ci-dessous.
-      other: Certaines choses ne vont pas ! Vérifiez les erreurs ci-dessous.
+      one: Quelque chose ne va pas ! Vérifiez l’erreur ci-dessous
+      other: Certaines choses ne vont pas ! Vérifiez les %{count} erreurs ci-dessous
   imports:
     preface: Vous pouvez importer certaines données comme les personnes que vous suivez ou bloquez sur votre compte sur cette instance à partir de fichiers créés sur une autre instance.
     success: Vos données ont été importées avec succès et seront traitées en temps et en heure
@@ -373,16 +434,44 @@ fr:
       following: Liste d’utilisateur⋅ice⋅s suivi⋅e⋅s
       muting: Liste d’utilisateur⋅ice⋅s que vous masquez
     upload: Importer
+  in_memoriam_html: In Memoriam.
+  invites:
+    delete: Désactiver
+    expired: Expiré
+    expires_in:
+      '1800': 30 minutes
+      '21600': 6 heures
+      '3600': 1 heure
+      '43200': 12 heures
+      '86400': 1 jour
+    expires_in_prompt: Jamais
+    generate: Générer
+    max_uses:
+      one: 1 usage
+      other: "%{count} usages"
+    max_uses_prompt: Pas de limite
+    prompt: Générer et partager des liens avec les autres pour donner accès à cette instance
+    table:
+      expires_at: Expire
+      uses: Utilise
+    title: Personnes invitées
   landing_strip_html: <strong>%{name}</strong> utilise %{link_to_root_path}. Vous pouvez læ suivre et interagir si vous possédez un compte quelque part dans le "fediverse".
   landing_strip_signup_html: Si ce n’est pas le cas, vous pouvez <a href="%{sign_up_path}">en créer un ici</a>.
   media_attachments:
     validations:
       images_and_video: Impossible de joindre une vidéo à un statut contenant déjà des images
       too_many: Impossible de joindre plus de 4 fichiers
+  migrations:
+    acct: utilisateur@domaine du nouveau compte
+    currently_redirecting: 'Votre profile va être redirigé vers :'
+    proceed: Enregistrer
+    updated_msg: Les paramètres de votre migration de compte ont été mis à jour avec succès !
+  moderation:
+    title: Modération
   notification_mailer:
     digest:
-      body: 'Voici ce que vous avez raté sur ${instance} depuis votre dernière visite (%{}) :'
-      mention: "%{name} vous a mentionné⋅e"
+      body: 'Voici ce que vous avez raté sur ${instance} depuis votre dernière visite le %{since} :'
+      mention: "%{name} vous a mentionné⋅e dans :"
       new_followers_summary:
         one: Vous avez un⋅e nouvel⋅le abonné⋅e ! Youpi !
         other: Vous avez %{count} nouveaux⋅elles abonné⋅e⋅s ! Incroyable !
@@ -390,7 +479,7 @@ fr:
         one: "Une nouvelle notification depuis votre dernière visite \U0001F418"
         other: "%{count} nouvelles notifications depuis votre dernière visite \U0001F418"
     favourite:
-      body: "%{name} a ajouté votre post à ses favoris :"
+      body: "%{name} a ajouté votre post à ses favoris :"
       subject: "%{name} a ajouté votre post à ses favoris"
     follow:
       body: "%{name} vous suit !"
@@ -399,10 +488,10 @@ fr:
       body: "%{name} a demandé à vous suivre"
       subject: 'Abonné⋅es en attente : %{name}'
     mention:
-      body: "%{name} vous a mentionné⋅e dans :"
+      body: "%{name} vous a mentionné⋅e dans :"
       subject: "%{name} vous a mentionné·e"
     reblog:
-      body: "%{name} a partagé votre statut :"
+      body: "%{name} a partagé votre statut :"
       subject: "%{name} a partagé votre statut"
   number:
     human:
@@ -442,7 +531,7 @@ fr:
     acct: Entrez votre pseudo@instance depuis lequel vous voulez suivre ce⋅tte utilisateur⋅rice
     missing_resource: L’URL de redirection n’a pas pu être trouvée
     proceed: Continuez pour suivre
-    prompt: 'Vous allez suivre :'
+    prompt: 'Vous allez suivre :'
   sessions:
     activity: Dernière activité
     browser: Navigateur
@@ -491,6 +580,7 @@ fr:
     export: Export de données
     followers: Abonné⋅es autorisé⋅es
     import: Import de données
+    migrate: Migration de compte
     notifications: Notifications
     preferences: Préférences
     settings: Réglages
@@ -587,6 +677,8 @@ fr:
 
       <p>Originellement adapté à partir de la politique de confidentialité de <a href="https://github.com/discourse/discourse">Discourse</a>.</p>
     title: "%{instance} Conditions d’utilisations et politique de confidentialité"
+  themes:
+    default: Mastodon
   time:
     formats:
       default: "%d %b %Y, %H:%M"
@@ -600,7 +692,7 @@ fr:
     generate_recovery_codes: Générer les codes de récupération
     instructions_html: "<strong>Scannez ce QR code grâce à Google Authenticator, Authy ou une application similaire sur votre téléphone</strong>. Désormais, cette application génèrera des jetons que vous devrez saisir à chaque connexion."
     lost_recovery_codes: Les codes de récupération vous permettent de retrouver les accès à votre comptre si vous perdez votre téléphone. Si vous perdez vos codes de récupération, vous pouvez les générer à nouveau ici. Vos anciens codes de récupération seront invalidés.
-    manual_instructions: 'Si vous ne pouvez pas scanner ce QR code et devez l’entrer manuellement, voici le secret en clair :'
+    manual_instructions: 'Si vous ne pouvez pas scanner ce QR code et devez l’entrer manuellement, voici le secret en clair :'
     recovery_codes: Codes de récupération
     recovery_codes_regenerated: Codes de récupération régénérés avec succès
     recovery_instructions_html: Si vous perdez l’accès à votre téléphone, vous pouvez utiliser un des codes de récupération ci-dessous pour récupérer l’accès à votre compte. Conservez les codes de récupération en toute sécurité, par exemple, en les imprimant et en les stockant avec vos autres documents importants.
@@ -609,4 +701,4 @@ fr:
   users:
     invalid_email: L’adresse courriel est invalide
     invalid_otp_token: Le code d’authentification à deux facteurs est invalide
-    signed_in_as: 'Connecté·e en tant que :'
+    signed_in_as: 'Connecté·e en tant que :'
diff --git a/config/locales/he.yml b/config/locales/he.yml
index 84d6d8468..4b977ce1b 100644
--- a/config/locales/he.yml
+++ b/config/locales/he.yml
@@ -1,54 +1,99 @@
 ---
 he:
   about:
+    about_hashtag_html: אלו סטטוסים פומביים המתוייגים בתור<strong>#%{hashtag}</strong>. ניתן להגיב, להדהד או לחבב אותם אם יש לך חשבון בכל מקום בפדרציה.
     about_mastodon_html: מסטודון היא רשת חברתית <em>חופשית, מבוססת תוכנה חופשית ("קוד פתוח")</em>. כאלטרנטיבה <em>בלתי ריכוזית</em> לפלטפרומות המסחריות, מסטודון מאפשרת להמנע מהסיכונים הנלווים להפקדת התקשורת שלך בידי חברה יחידה. שמת את מבטחך בשרת אחד &mdash; לא משנה במי בחרת, תמיד אפשר לדבר עם כל שאר המשתמשים. לכל מי שרוצה יש את האפשרות להקים שרת מסטודון עצמאי, ולהשתתף ב<em>רשת החברתית</em> באופן חלק.
     about_this: אודות שרת זה
     closed_registrations: הרשמות סגורות לשרת זה לעת עתה.
     contact: צור קשר
+    contact_missing: אין
+    contact_unavailable: לא רלוונטי/חסר
     description_headline: מהו %{domain}?
     domain_count_after: שרתים אחרים
     domain_count_before: מחובר אל
+    extended_description_html: |
+      <h3>מקום טוב לכללים</h3>
+      <p>התיאור המורחב טרם הוגדר.</p>
+    features:
+      humane_approach_body: מתוך למידה מכשלים של רשתות אחרות, מסטודון מכוונת להחלטות תכנוניות אתיות שנאבקות בשימוש לרעה של מדיה חברתית.
+      humane_approach_title: גישה הומאנית יותר
+      not_a_product_body: מסטודון איננה רשת מסחרית. אין פרסום, אין כריית מידע, אין גנים סגורים. אין סמכות מרכזית.
+      not_a_product_title: את(ה) אדם, לא מוצר
+      real_conversation_body: עם 500 תווים לרשותך, ואפשרויות פרטניות לאזהרות תוכן והסתרת מדיה, יש לך את החופש להתבטא כרצונך.
+      real_conversation_title: בנוי לשיחות אמתיות
+      within_reach_body: שלל אפליקציות עבור iOS, אנדרואיד ופלטפורמות אחרות שיאפשרו לך לשמור על קשר עם חברים בכל מקום, תודות למערכת מנשקי תוכנה ידידותיים למפתחים.
+      within_reach_title: תמיד במרחק נגיעה
+    find_another_instance: לאיתור שרת אחר
+    generic_description: "%{domain} הוא שרת אחד בתוך הרשת"
+    hosted_on: מסטודון שיושב בכתובת %{domain}
+    learn_more: מידע נוסף
     other_instances: שרתים אחרים
     source_code: קוד מקור
     status_count_after: הודעות
     status_count_before: שכתבו
     user_count_after: משתמשים
     user_count_before: ביתם של
+    what_is_mastodon: מה זה מסטודון?
   accounts:
     follow: לעקוב
     followers: עוקבים
     following: נעקבים
+    media: מדיה
+    moved_html: "%{name} עבר(ה) אל %{new_profile_link}:"
     nothing_here: אין פה שום דבר!
     people_followed_by: הנעקבים של %{name}
     people_who_follow: העוקבים של %{name}
     posts: הודעות
+    posts_with_replies: חצרוצים ותגובות
     remote_follow: מעקב מרחוק
+    reserved_username: שם המשתמש שמור
+    roles:
+      admin: מנהל
+      moderator: מנחה
     unfollow: הפסקת מעקב
   admin:
+    account_moderation_notes:
+      account: מנחה דיון
+      create: ליצור
+      created_at: תאריך
+      created_msg: הודעת מנחה נוצרה בהצלחה!
+      delete: למחוק
+      destroyed_msg: הודעת מנחה נמחקה בהצלחה!
     accounts:
       are_you_sure: בטוח?
-      confirm: אשר
+      by_domain: שם מתחם
+      confirm: אישור
       confirmed: אושר
+      demote: הורדה בדרגה
+      disable: לחסום
       disable_two_factor_authentication: ביטול הזדהות דו-שלבית
+      disabled: נחסם
       display_name: שם לתצוגה
       domain: תחום
       edit: עריכה
       email: דוא"ל
+      enable: לאפשר
+      enabled: מאופשר
       feed_url: כתובת פיד
       followers: עוקבים
+      followers_url: כתובת עוקבים
       follows: נעקבים
+      inbox_url: כתובת תיבה נכנסת
       ip: כתובת IP
       location:
         all: הכל
         local: מקומי
         remote: מרחוק
         title: מיקום
+      login_status: מצב חיבור
       media_attachments: תוספות מדיה
+      memorialize: הפוך לדף זכרון
       moderation:
         all: הכל
         silenced: מושתקים
         suspended: מושהים
         title: ניהול קהילה
+      moderation_notes: הודעות מנחה
       most_recent_activity: פעילות עדכנית
       most_recent_ip: כתובות אחרונות
       not_subscribed: לא רשום
@@ -56,25 +101,49 @@ he:
         alphabetic: אלפביתי
         most_recent: עדכני
         title: סידור
+      outbox_url: כתובת תיבת דואר יוצא
       perform_full_suspension: ביצוע השעייה מלאה
       profile_url: כתובת פרופיל
+      promote: להעלות בדרגה
+      protocol: פרטיכל
       public: פומבי
       push_subscription_expires: הרשמה להודעות בדחיפה פגה
+      redownload: לקריאה מחדש של האווטאר
       reset: איפוס
       reset_password: אתחול סיסמא
+      resubscribe: להרשם מחדש
+      role: הרשאות
+      roles:
+        admin: מנהל מערכת
+        moderator: מנחה דיונים
+        user: משתמש(ת)
       salmon_url: כתובת סלמון
       search: חיפוש
+      shared_inbox_url: תיבה משותפת לדואר נכנס
       show:
         created_reports: דיווחים מאת חשבון זה
         report: דו"ח
         targeted_reports: דיווחים נגד חשבון זה
       silence: השתקה
       statuses: הודעות
+      subscribe: הרשמה
       title: חשבונות
       undo_silenced: ביטול השתקה
       undo_suspension: ביטול השעייה
+      unsubscribe: הפסקת הרשמה
       username: שם משתמש
       web: רשת
+    action_logs:
+      actions:
+        confirm_user: יש אישור מאת %{name} על כתובת הדוא"ל של %{target}
+        create_custom_emoji: "%{name} תרמה/תרם אמוג'י חדש %{target}"
+        create_domain_block: "%{name} חסמה/חסם את שם המתחם %{target}"
+        create_email_domain_block: מתחם דוא"ל %{target} הוסף לרשימה השחורה ע"י %{name}
+        demote_user: '%{name} הורד(ה) בדרגה ע"י %{target}'
+        destroy_domain_block: החסימה על מתחם %{target} הוסרה ע"י %{name}
+        destroy_email_domain_block: מתחם דוא"ל %{target} הוכנס לרשימה הלבנה ע"י %{name}
+        destroy_status: ההודעה של  %{target} הוסרה ע"י %{name}
+        disable_2fa_user: אימות דו שלבי של  %{target} הוסר ע"י %{name}
     domain_blocks:
       add_new: הוספת חדש
       created_msg: חסימת שרת בתהליך
@@ -84,12 +153,12 @@ he:
         create: יצירת חסימה
         hint: חסימת השרת לא תמנע יצירת רישומי חשבון במסד הנתונים, אבל תבצע פעולות ניהול קהילה מסוימות על חשבונות אלו אוטומטית ורטרואקטיבית.
         severity:
-          desc_html: "<strong>השתקה</strong> תחביא הודעות מחשבון זה לכל מי שלא עוקב אחריו. <strong>השעייה</strong> תסיר מהשרת את כל התוכן, מדיה ותכונות הפרופיל שמקושרות לחשבון זה."
+          desc_html: "<strong>השתקה</strong> תחביא הודעות מחשבון זה לכל מי שלא עוקב אחריו. <strong>השעייה</strong> תסיר מהשרת את כל התוכן, מדיה ותכונות הפרופיל שמקושרות לחשבון זה. <strong>כלום</strong> כדי לחסום קבצי מדיה בלבד."
           silence: השתקה
           suspend: השעייה
         title: חסימת שרת חדשה
       reject_media: חסימת קבצי מדיה
-      reject_media_hint: מסירה קבצי מדיה השמורים מקומית ומונעת מהורדת קבצים נוספים בעתיד. לא רלוונטי להשעיות.
+      reject_media_hint: מסירה קבצי מדיה השמורים מקומית ומונעת מהורדת קבצים נוספים בעתיד. לא רלוונטי להשעיות
       severities:
         silence: השתקה
         suspend: השעייה
@@ -143,7 +212,7 @@ he:
         open:
           title: הרשמה פתוחה
       site_description:
-        desc_html: מוצג כפסקה על הדף הראשי ומשמש כתגית מטא.<br>ניתן להשתמש בתגיות HTML, ובמיוחד ב־<code>&lt;a&gt;</code> ו־<code>&lt;em&gt;</code>.
+        desc_html: מוצג כפסקה על הדף הראשי ומשמש כתגית מטא. ניתן להשתמש בתגיות HTML, ובמיוחד ב־<code> &lt; a&gt; </code> ו־<code> &lt; em&gt; </code> .
         title: תיאור האתר
       site_description_extended:
         desc_html: מוצג על עמוד המידע הנוסף<br>ניתן להשתמש בתגיות HTML
@@ -155,7 +224,7 @@ he:
       confirmed: מאושר
       expires_in: פג תוקף ב-
       last_delivery: משלוח אחרון
-      title: WebSub
+      title: מנוי WebSub
       topic: נושא
     title: ניהול
   application_mailer:
@@ -323,7 +392,7 @@ he:
     lost_recovery_codes: קודי האחזור מאפשרים אחזור גישה לחשבון במידה ומכשירך אבד. במידה וקודי האחזור אבדו, ניתן לייצרם מחדש כאן. תוקף קודי האחזור הישנים יפוג.
     manual_instructions: 'במידה ולא ניתן לסרוק את קוד ה-QR אלא יש צורך להקליד אותו ידנית, להלן סוד כמוס בלתי מוצפן:'
     recovery_codes_regenerated: קודי האחזור יוצרו בהצלחה
-    recovery_instructions_html: במידה והגישה למכשירך תאבד, ניתן לייצר קודי אחזור למטה על מנת לאחזר גישה לחשבונך בכל עת. נא לשמור על קודי הגישה במקום בטוח )לדוגמא על ידי הדפסתם ושמירתם עם מסמכים חשובים אחרים, או שימוש בתוכנה ייעודית לניהול סיסמאות וסודות(
+    recovery_instructions_html: במידה והגישה למכשירך תאבד, ניתן לייצר קודי אחזור למטה על מנת לאחזר גישה לחשבונך בכל עת. <strong>נא לשמור על קודי הגישה במקום בטוח</strong>. לדוגמא על ידי הדפסתם ושמירתם עם מסמכים חשובים אחרים, או שימוש בתוכנה ייעודית לניהול סיסמאות וסודות.
     setup: הכנה
     wrong_code: הקוד שהוזן שגוי! האם הזמן בשרת והזמן במכשירך נכונים?
   users:
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index a008d9cc4..3097fb22b 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -448,7 +448,7 @@ ja:
     expires_in_prompt: 無期限
     generate: 作成
     max_uses:
-      one: 1
+      one: '1'
       other: "%{count}"
     max_uses_prompt: 無制限
     prompt: リンクを生成・共有してこのインスタンスへの新規登録を受け付けることができます。
@@ -456,6 +456,14 @@ ja:
       expires_at: 有効期限
       uses: 使用
     title: 新規ユーザーの招待
+  keyword_mutes:
+    add_keyword: キーワードを追加
+    edit: 編集
+    edit_keyword: キーワードを編集
+    keyword: キーワード
+    match_whole_word: 単語全体が一致
+    remove: 削除
+    remove_all: すべて削除
   landing_strip_html: "<strong>%{name}</strong> さんはインスタンス %{link_to_root_path} のユーザーです。アカウントさえ持っていればフォローしたり会話したりできます。"
   landing_strip_signup_html: もしお持ちでないなら <a href="%{sign_up_path}">こちら</a> からサインアップできます。
   media_attachments:
diff --git a/config/locales/ko.yml b/config/locales/ko.yml
index fd6351486..333bcc689 100644
--- a/config/locales/ko.yml
+++ b/config/locales/ko.yml
@@ -39,6 +39,7 @@ ko:
     followers: 팔로워
     following: 팔로잉
     media: 미디어
+    moved_html: "%{name}은 %{new_profile_link}으로 이동되었습니다:"
     nothing_here: 아무 것도 없습니다.
     people_followed_by: "%{name} 님이 팔로우 중인 계정"
     people_who_follow: "%{name} 님을 팔로우 중인 계정"
@@ -48,6 +49,7 @@ ko:
     reserved_username: 이 아이디는 예약되어 있습니다.
     roles:
       admin: Admin
+      moderator: 모드
     unfollow: 팔로우 해제
   admin:
     account_moderation_notes:
@@ -59,13 +61,18 @@ ko:
       destroyed_msg: 모더레이션 기록이 성공적으로 삭제되었습니다.
     accounts:
       are_you_sure: 정말로 실행하시겠습니까?
+      by_domain: 도메인
       confirm: 확인
       confirmed: 확인됨
+      disable: 비활성화
       disable_two_factor_authentication: 2단계 인증을 비활성화
+      disabled: 비활성화된
       display_name: 이름
       domain: 도메인
       edit: 편집
       email: E-mail
+      enable: 활성화
+      enabled: 활성화된
       feed_url: 피드 URL
       followers: 팔로워 수
       followers_url: 팔로워 URL
@@ -77,6 +84,7 @@ ko:
         local: 로컬
         remote: 리모트
         title: 위치
+      login_status: 로그인 상태
       media_attachments: 첨부된 미디어
       moderation:
         all: 전체
@@ -101,6 +109,10 @@ ko:
       reset: 초기화
       reset_password: 비밀번호 초기화
       resubscribe: 다시 구독
+      role: 권한
+      roles:
+        admin: 관리자
+        user: 사용자
       salmon_url: Salmon URL
       search: 검색
       shared_inbox_url: 공유된 inbox URL
diff --git a/config/locales/nl.yml b/config/locales/nl.yml
index cda771ce2..2410c1112 100644
--- a/config/locales/nl.yml
+++ b/config/locales/nl.yml
@@ -39,6 +39,7 @@ nl:
     followers: Volgers
     following: Volgend
     media: Media
+    moved_html: "%{name} is verhuisd naar %{new_profile_link}:"
     nothing_here: Hier is niets!
     people_followed_by: Mensen die %{name} volgt
     people_who_follow: Mensen die %{name} volgen
@@ -48,6 +49,7 @@ nl:
     reserved_username: Deze gebruikersnaam is gereserveerd
     roles:
       admin: Beheerder
+      moderator: Mod
     unfollow: Ontvolgen
   admin:
     account_moderation_notes:
@@ -59,13 +61,19 @@ nl:
       destroyed_msg: Verwijderen van opmerking voor moderatoren geslaagd!
     accounts:
       are_you_sure: Weet je het zeker?
+      by_domain: Domein
       confirm: Bevestigen
       confirmed: Bevestigd
+      demote: Degraderen
+      disable: Uitschakelen
       disable_two_factor_authentication: 2FA uitschakelen
+      disabled: Uitgeschakeld
       display_name: Weergavenaam
       domain: Domein
       edit: Bewerken
       email: E-mail
+      enable: Inschakelen
+      enabled: Ingeschakeld
       feed_url: Feed-URL
       followers: Volgers
       followers_url: Volgers-URL
@@ -77,7 +85,9 @@ nl:
         local: Lokaal
         remote: Extern
         title: Locatie
+      login_status: Aanmeld status
       media_attachments: Media-bijlagen
+      memorialize: Verander in memoriam
       moderation:
         all: Alles
         silenced: Genegeerd
@@ -94,6 +104,7 @@ nl:
       outbox_url: Outbox-URL
       perform_full_suspension: Volledig opschorten
       profile_url: Profiel-URL
+      promote: Promoten
       protocol: Protocol
       public: Openbaar
       push_subscription_expires: PuSH-abonnement verloopt op
@@ -101,6 +112,11 @@ nl:
       reset: Opnieuw
       reset_password: Wachtwoord opnieuw instellen
       resubscribe: Opnieuw abonneren
+      role: Permissies
+      roles:
+        admin: Beheerder
+        moderator: Moderateur
+        user: Persoon
       salmon_url: Salmon-URL
       search: Zoeken
       shared_inbox_url: Gedeelde inbox-URL
@@ -117,6 +133,24 @@ nl:
       unsubscribe: Opzeggen
       username: Gebruikersnaam
       web: Webapp
+    action_logs:
+      actions:
+        confirm_user: "%{name} bevestigd e-mailadres van persoon %{target}"
+        create_custom_emoji: "%{name} heeft de nieuwe emoji %{target} geupload"
+        create_domain_block: "%{name} heeft domein %{target} geblokkeerd"
+        create_email_domain_block: "%{name} heeft e-maildomein %{target} geblacklist"
+        demote_user: "%{name} heeft persoon %{target} gedegradeerd"
+        destroy_domain_block: "%{name} heeft domein %{target} vrijgegeven"
+        destroy_email_domain_block: "%{name} heeft e-maildomein %{target} gewhitelist"
+        destroy_status: "%{name} heeft status van %{target} verwijderd"
+        disable_2fa_user: "%{name} heeft tweefactor voorwaarden van persoon %{target} uitgeschakeld"
+        disable_custom_emoji: "%{name} heeft emoji %{target} uitgeschakeld"
+        disable_user: "%{name} heeft de login van persoon %{target} uitgeschakeld"
+        enable_custom_emoji: "%{name} heeft emoji %{target} ingeschakeld"
+        enable_user: "%{name} heeft de login voor persoon %{target} ingeschakeld"
+        memorialize_account: "%{name} heeft %{target}'s account gewijzigd in een memoriam pagina"
+        promote_user: "%{name} heeft persoon %{target} gepromoveerd"
+        reset_password_user: "%{name} heeft het wachtwoord van gebruiker %{target} opnieuw ingesteld"
     custom_emojis:
       copied_msg: Lokale kopie van emoji maken geslaagd
       copy: Kopiëren
@@ -156,7 +190,7 @@ nl:
         noop: Geen
         silence: Negeren
         suspend: Opschorten
-      severity: Zwaarte
+      severity: Strengheid
       show:
         affected_accounts:
           one: Eén account in de database aangepast
diff --git a/config/locales/oc.yml b/config/locales/oc.yml
index e5d036303..a9bad2d34 100644
--- a/config/locales/oc.yml
+++ b/config/locales/oc.yml
@@ -39,11 +39,12 @@ oc:
     followers: Seguidors
     following: Abonaments
     media: Mèdias
+    moved_html: "%{name} a mudat a %{new_profile_link} :"
     nothing_here: I a pas res aquí !
     people_followed_by: Lo mond que %{name} sèc
     people_who_follow: Lo mond que sègon %{name}
     posts: Tuts
-    posts_with_replies: Tuts amb responsas
+    posts_with_replies: Tuts e responsas
     remote_follow: Sègre a distància
     reserved_username: Aqueste nom d’utilizaire es reservat
     roles:
@@ -62,6 +63,7 @@ oc:
       by_domain: Domeni
       confirm: Confirmar
       confirmed: Confirmat
+      demote: Retrogradar
       disable: Desactivar
       disable_two_factor_authentication: Desactivar 2FA
       disabled: Desactivat
@@ -101,6 +103,7 @@ oc:
       outbox_url: URL Outbox
       perform_full_suspension: Botar en tren la suspension complèta
       profile_url: URL del perfil
+      promote: Promòure
       protocol: Protocòl
       public: Public
       push_subscription_expires: Fin de l’abonament PuSH
@@ -108,6 +111,11 @@ oc:
       reset: Reïnicializar
       reset_password: Reïnicializar lo senhal
       resubscribe: Se tornar abonar
+      role: Permissions
+      roles:
+        admin: Admin
+        moderator: Mod
+        user: Uitlizaire
       salmon_url: URL Salmon
       search: Cercar
       shared_inbox_url: URL de recepcion partejada
@@ -124,6 +132,32 @@ oc:
       unsubscribe: Se desabonar
       username: Nom d’utilizaire
       web: Web
+    action_logs:
+      actions:
+        confirm_user: "%{name} confirmèt l’adreça a %{target}"
+        create_custom_emoji: "%{name} mandèt un nòu emoji %{target}"
+        create_domain_block: "%{name} bloquèt lo domeni %{target}"
+        create_email_domain_block: "%{name} botèt a la lista nègra lo domeni de corrièl %{target}"
+        demote_user: "%{name} retragradèt l‘utilizaire %{target}"
+        destroy_domain_block: "%{name} desbloquèt lo domeni %{target}"
+        destroy_email_domain_block: "%{name} botèt a la lista blanca lo domeni de corrièl %{target}"
+        destroy_status: "%{name} levèt l‘estatut a %{target}"
+        disable_2fa_user: "%{name} desactivèt l’autentificacion en dos temps per %{target}"
+        disable_custom_emoji: "%{name} desactivèt l‘emoji %{target}"
+        disable_user: "%{name} desactivèt la connexion per %{target}"
+        enable_custom_emoji: "%{name} activèt l‘emoji %{target}"
+        enable_user: "%{name} activèt la connexion per %{target}"
+        memorialize_account: "%{name} transformèt en memorial la pagina de perfil a %{target}"
+        promote_user: "%{name} promoguèt %{target}"
+        reset_password_user: "%{name} reïnicializèt lo senhal a %{target}"
+        resolve_report: "%{name} anullèt lo rapòrt de %{target}"
+        silence_account: "%{name} metèt en silenci lo compte a %{target}"
+        suspend_account: "%{name} susprenguèt lo compte a %{target}"
+        unsilence_account: "%{name} levèt lo silenci del compte a %{target}"
+        unsuspend_account: "%{name} restabliguèt lo compte a %{target}"
+        update_custom_emoji: "%{name} metèt a jorn l’emoji %{target}"
+        update_status: "%{name} metèt a jorn l’estatut a %{target}"
+      title: Audit log
     custom_emojis:
       copied_msg: Còpia locala de l’emoji ben creada
       copy: Copiar
@@ -196,6 +230,8 @@ oc:
       reset: Reïnicializar
       search: Cercar
       title: Instàncias conegudas
+    invites:
+      title: Covits
     reports:
       action_taken_by: Mesura menada per
       are_you_sure: Es segur ?
@@ -237,6 +273,9 @@ oc:
         open:
           desc_html: Autorizar lo monde a se marcar
           title: Inscripcions
+      show_staff_badge:
+        desc_html: Mostrar lo badge Personal sus la pagina de perfil
+        title: Mostrar lo badge personal
       site_description:
         desc_html: Afichada jos la forma de paragraf sus la pagina d’acuèlh e utilizada coma balisa meta.<br> Podètz utilizar de balisas HTML, coma <code>&lt;a&gt;</code> e <code>&lt;em&gt;</code>.
         title: Descripcion del site
diff --git a/config/locales/pl.yml b/config/locales/pl.yml
index 19ee154ab..baa26f1dc 100644
--- a/config/locales/pl.yml
+++ b/config/locales/pl.yml
@@ -277,6 +277,9 @@ pl:
         deletion:
           desc_html: Pozwól każdemu na usunięcie konta
           title: Możliwość usunięcia
+        min_invite_role:
+          disabled: Nikt
+          title: Kto może zapraszać użytkowników
         open:
           desc_html: Pozwól każdemu na założenie konta
           title: Otwarta rejestracja
@@ -474,6 +477,7 @@ pl:
     acct: nazwa@domena nowego konta
     currently_redirecting: 'Obecnie Twoje konto przekierowuje do:'
     proceed: Zapisz
+    updated_msg: Pomyślnie zaktualizowano ustawienia i migracji Twojego konta!
   moderation:
     title: Moderacja
   notification_mailer:
@@ -689,6 +693,8 @@ pl:
 
       <p>Tekst bazuje na <a href="https://github.com/discourse/discourse">polityce prywatności Discourse</a>.</p>
     title: Zasady korzystania i polityka prywatności %{instance}
+  themes:
+    default: Mastodon
   time:
     formats:
       default: "%b %d, %Y, %H:%M"
diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml
index de2b9c778..760bb69a2 100644
--- a/config/locales/pt-BR.yml
+++ b/config/locales/pt-BR.yml
@@ -39,6 +39,7 @@ pt-BR:
     followers: Seguidores
     following: Seguindo
     media: Mídia
+    moved_html: "%{name} se mudou para %{new_profile_link}:"
     nothing_here: Não há nada aqui!
     people_followed_by: Pessoas que %{name} segue
     people_who_follow: Pessoas que seguem %{name}
@@ -48,6 +49,7 @@ pt-BR:
     reserved_username: Este usuário está reservado
     roles:
       admin: Administrador
+      moderator: Moderador
     unfollow: Deixar de seguir
   admin:
     account_moderation_notes:
@@ -76,7 +78,7 @@ pt-BR:
       followers: Seguidores
       followers_url: URL de seguidores
       follows: Segue
-      inbox_url: Inbox URL
+      inbox_url: URL da caixa de entrada
       ip: IP
       location:
         all: Todos
@@ -115,7 +117,7 @@ pt-BR:
         admin: Administrador
         moderator: Moderador
         user: Usuário
-      salmon_url: Salmon URL
+      salmon_url: URL Salmon
       search: Pesquisar
       shared_inbox_url: URL da Inbox Compartilhada
       show:
@@ -131,18 +133,39 @@ pt-BR:
       unsubscribe: Desinscrever-se
       username: Nome de usuário
       web: Web
+    action_logs:
+      actions:
+        confirm_user: "%{name} confirmou o endereço de e-mail do usuário %{target}"
+        create_custom_emoji: "%{name} enviou o emoji novo %{target}"
+        create_domain_block: "%{name} bloqueou o domínio %{target}"
+        create_email_domain_block: "%{name} colocou o domínio de e-mail %{target} na lista negra"
+        demote_user: "%{name} rebaixou o usuário %{target}"
+        destroy_domain_block: "%{name} desbloqueou o domínio %{target}"
+        destroy_email_domain_block: "%{name} retirou o domínio de e-mail %{target} da lista negra"
+        destroy_status: "%{name} removeu postagem feita por %{target}"
+        disable_2fa_user: "%{name} desabilitou a exigência de autenticação em dois passos para o usuário %{target}"
+        disable_custom_emoji: "%{name} desabilitou o emoji %{target}"
+        disable_user: "%{name} desabilitou o acesso para o usuário %{target}"
+        enable_custom_emoji: "%{name} habilitou o emoji %{target}"
+        enable_user: "%{name} habilitou o acesso para o usuário %{target}"
+        memorialize_account: "%{name} transformou a conta de %{target} em um memorial"
+        promote_user: "%{name} promoveu o usuário %{target}"
+        reset_password_user: "%{name} redefiniu a senha do usuário %{target}"
+        resolve_report: "%{name} dispensou o relatório %{target}"
+        silence_account: "%{name} silenciou a conta de %{target}"
+        suspend_account: "%{name} suspendeu a conta de %{target}"
     custom_emojis:
-      copied_msg: Cópia local do emoji criada com sucesso!
+      copied_msg: Cópia local do emoji criada com sucesso
       copy: Copiar
       copy_failed_msg: Não foi possível criar uma cópia local deste emoji
       created_msg: Emoji criado com sucesso!
       delete: Excluir
       destroyed_msg: Emoji deletado com sucesso!
       disable: Desabilitar
-      disabled_msg: Emoji desabilitado com sucesso!
+      disabled_msg: Emoji desabilitado com sucesso
       emoji: Emoji
       enable: Habilitar
-      enabled_msg: Emoji habilitado com sucesso!
+      enabled_msg: Emoji habilitado com sucesso
       image_hint: PNG de até 50KB
       listed: Listado
       new:
@@ -170,7 +193,7 @@ pt-BR:
           suspend: Suspensão
         title: Novo bloqueio de domínio
       reject_media: Rejeitar arquivos de mídia
-      reject_media_hint: Remove arquivos de mídia armazenados localmente e recusa quaisquer outros no futuro. Irrelevante para suspensões.
+      reject_media_hint: Remove arquivos de mídia armazenados localmente e recusa quaisquer outros no futuro. Irrelevante para suspensões
       severities:
         noop: Nenhum
         silence: Silêncio
@@ -236,7 +259,7 @@ pt-BR:
         username: Contate usuário
       registrations:
         closed_message:
-          desc_html: Exibido na página inicial quando cadastros estão fechados. Você pode usar tags HTML.
+          desc_html: Exibido na página inicial quando cadastros estão fechados. Você pode usar tags HTML
           title: Mensagem de cadastros fechados
         deletion:
           desc_html: Permitir que qualquer um delete a sua conta
@@ -248,10 +271,10 @@ pt-BR:
         desc_html: Parágrafo introdutório na página inicial e em meta tags. Você pode usar tags HTML, em especial <code>&lt;a&gt;</code> e <code>&lt;em&gt;</code>.
         title: Descrição da instância
       site_description_extended:
-        desc_html: Um ótimo lugar para seu código de conduta, regras, diretrizes e outras coisas para diferenciar a sua instância. Você pode usar tags HTML.
+        desc_html: Um ótimo lugar para seu código de conduta, regras, diretrizes e outras coisas para diferenciar a sua instância. Você pode usar tags HTML
         title: Informação estendida customizada
       site_terms:
-        desc_html: Você pode escrever a sua própria política de privacidade, termos de serviço, entre outras coisas.Você pode usar tags HTML.
+        desc_html: Você pode escrever a sua própria política de privacidade, termos de serviço, entre outras coisas. Você pode usar tags HTML
         title: Termos de serviço customizados
       site_title: Nome da instância
       thumbnail:
@@ -277,7 +300,7 @@ pt-BR:
       title: Postagens da conta
       with_media: Com mídia
     subscriptions:
-      callback_url: Callback URL
+      callback_url: URL de Callback
       confirmed: Confirmado
       expires_in: Expira em
       last_delivery: Última entrega
@@ -316,7 +339,7 @@ pt-BR:
     reset_password: Modificar senha
     set_new_password: Definir uma nova senha
   authorize_follow:
-    error: Infelizmente, ocorreu um erro quando visualizando a conta remota.
+    error: Infelizmente, ocorreu um erro ao buscar a conta remota
     follow: Seguir
     follow_request: 'Você mandou uma solicitação de seguidor para:'
     following: 'Sucesso! Você agora está seguindo:'
@@ -340,7 +363,7 @@ pt-BR:
       x_months: "%{count} meses"
       x_seconds: "%{count} segundos"
   deletes:
-    bad_password_msg: Boa tentativa, hackers! Senha incorreta.
+    bad_password_msg: Boa tentativa, hackers! Senha incorreta
     confirm_password: Insira a sua senha atual para verificar a sua identidade
     description_html: Isto vai <strong>permanente e irreversivelmente</strong> remover conteúdo de sua conta e desativá-la. O seu nome de usuário permanecerá reservado para previnir futuros roubos de identidade.
     proceed: Excluir conta
@@ -379,7 +402,7 @@ pt-BR:
     unlocked_warning_title: A sua conta não está trancada
   generic:
     changes_saved_msg: Mudanças salvas com sucesso!
-    powered_by: powered by %{link}
+    powered_by: graças a tecnologia de %{link}
     save_changes: Salvar mudanças
     validation_errors:
       one: Algo não está certo! Por favor, reveja o erro abaixo
@@ -392,13 +415,13 @@ pt-BR:
       following: Pessoas que você segue
       muting: Lista de silêncio
     upload: Enviar
-  in_memoriam_html: Em memória de
+  in_memoriam_html: Em memória.
   landing_strip_html: "<strong>%{name}</strong> é um usuário no %{link_to_root_path}. Você pode segui-lo ou interagir com ele se você tiver uma conta em qualquer lugar no fediverso."
   landing_strip_signup_html: Se não, você pode <a href="%{sign_up_path}">se cadastrar aqui</a>.
   media_attachments:
     validations:
-      images_and_video: Não é possível anexar um vídeo a uma postagem que já contém imagens.
-      too_many: Não é possível anexar mais de quatro imagens.
+      images_and_video: Não é possível anexar um vídeo a uma postagem que já contém imagens
+      too_many: Não é possível anexar mais de 4 imagens
   notification_mailer:
     digest:
       body: 'Aqui está um resumo do que você perdeu no %{instance} desde o seu último acesso em %{since}:'
@@ -417,7 +440,7 @@ pt-BR:
       subject: "%{name} está te seguindo"
     follow_request:
       body: "%{name} requisitou autorização para te seguir"
-      subject: 'Pending follower: %{name}'
+      subject: 'Seguidor pendente: %{name}'
     mention:
       body: 'Você foi mencionado por %{name} em:'
       subject: Você foi mencionado por %{name}
@@ -472,7 +495,7 @@ pt-BR:
       chrome: Chrome
       edge: Microsoft Edge
       firefox: Firefox
-      generic: Unknown browser
+      generic: Navegador desconhecido
       ie: Internet Explorer
       micro_messenger: MicroMessenger
       nokia: Nokia S40 Ovi Browser
diff --git a/config/locales/pt.yml b/config/locales/pt.yml
index 140f6b71b..c476bac59 100644
--- a/config/locales/pt.yml
+++ b/config/locales/pt.yml
@@ -1,52 +1,98 @@
 ---
 pt:
   about:
-    about_mastodon_html: Mastodon é uma rede social <em>grátis e em código aberto</em>. Uma alternativa <em>descentralizada</em> às plataformas comerciais, que evita o risco de uma única empresa monopolizar a tua comunicação. Escolhe um servidor que confies, não importa qual, pois vais poder comunicar com todos os outros. Qualquer um pode criar uma instância Mastodon e participar nesta <em>rede social</em>.
+    about_hashtag_html: Estes são toots públicos marcados com <strong>#%{hashtag}</strong>. Podes interagir com eles se tiveres uma conta Mastodon.
+    about_mastodon_html: Mastodon é uma rede social baseada em protocolos abertos da web e software livre e gratuito. É descentralizado como e-mail.
     about_this: Sobre esta instância
     closed_registrations: Novos registos estão fechados nesta instância.
     contact: Contacto
+    contact_missing: Não definido
+    contact_unavailable: N/A
     description_headline: O que é o %{domain}?
     domain_count_after: outras instâncias
     domain_count_before: Ligado a
+    extended_description_html: |
+      <h3>Um bom lugar para regras</h3>
+      <p>A descrição da instância ainda não foi feita.</p>
+    features:
+      humane_approach_body: Aprendendo com erros de outras redes sociais, Mastodon tem como objetivo fazer decisões éticas de design para combater o utilização errada de redes sociais.
+      humane_approach_title: Uma abordagem mais humana
+      not_a_product_body: Mastodon não é uma rede comercial. Sem publicidade, sem recolha de dados ou portas fechadas. Não existe uma autoridade central.
+      not_a_product_title: Tu és uma pessoa, não um produto
+      real_conversation_body: Com 500 caracteres à sua disposição e suporte para conteúdo granular e avisos de conteúdo, podes te expressar da forma que desejares.
+      real_conversation_title: Feito para conversas reais
+      within_reach_body: Várias aplicações para iOS, Android e outras plataformas graças a um ecossistema de API amigável para desenvolvedores, permitem-te que te mantenhas em contacto com os teus amigos em qualquer lugar.
+      within_reach_title: Sempre ao teu alcance
+    find_another_instance: Encontra outra instância
+    generic_description: "%{domain} é um servidor na rede"
+    hosted_on: Mastodon em %{domain}
+    learn_more: Saber mais
     other_instances: Outras instâncias
     source_code: Código fonte
     status_count_after: publicações
     status_count_before: Que fizeram
     user_count_after: utilizadores
     user_count_before: Casa para
+    what_is_mastodon: O que é o Mastodon?
   accounts:
     follow: Seguir
     followers: Seguidores
     following: A seguir
+    media: Media
+    moved_html: "%{name} mudou-se para %{new_profile_link}:"
     nothing_here: Não há nada aqui!
     people_followed_by: Pessoas seguidas por %{name}
     people_who_follow: Pessoas que seguem %{name}
     posts: Posts
+    posts_with_replies: Posts e Respostas
     remote_follow: Seguir remotamente
+    reserved_username: Este nome de utilizadores é reservado
+    roles:
+      admin: Administrador
+      moderator: Moderador
     unfollow: Deixar de seguir
   admin:
+    account_moderation_notes:
+      account: Moderador
+      create: Criar
+      created_at: Data
+      created_msg: Nota de moderação criada com sucesso!
+      delete: Eliminar
+      destroyed_msg: Nota de moderação excluída com sucesso!
     accounts:
       are_you_sure: Tens a certeza?
+      by_domain: Domínio
       confirm: Confirme
       confirmed: Confirmado
+      demote: Rebaixar
+      disable: Desativar
+      disable_two_factor_authentication: Desativar 2FA
+      disabled: Desativado
       display_name: Nome a mostrar
       domain: Domínio
       edit: Editar
       email: E-mail
+      enable: Ativar
+      enabled: Ativado
       feed_url: URL do Feed
       followers: Seguidores
+      followers_url: URL dos seguidores
       follows: A seguir
+      inbox_url: URL da caixa de entrada
+      ip: IP
       location:
         all: Todos
         local: Local
         remote: Remoto
         title: Local
+      login_status: Estado de início de sessão
       media_attachments: Media anexa
       moderation:
         all: Todos
         silenced: Silenciados
         suspended: Supensos
         title: Moderação
+      moderation_notes: Notas de moderação
       most_recent_activity: Actividade mais recente
       most_recent_ip: IP mais recente
       not_subscribed: Não inscrito
@@ -56,10 +102,26 @@ pt:
         title: Ordem
       perform_full_suspension: Fazer suspensão completa
       profile_url: URL do perfil
+      promote: Promover
+      protocol: Protocolo
       public: Público
       push_subscription_expires: PuSH subscription expires
+      redownload: Atualizar avatar
+      reset: Restaurar
       reset_password: Reset palavra-passe
+      resubscribe: Reinscrever
+      role: Permissões
+      roles:
+        admin: Administrador
+        moderator: Moderador
+        user: Utilizador
       salmon_url: Salmon URL
+      search: Pesquisar
+      shared_inbox_url: URL da caixa de entrada compartilhada
+      show:
+        created_reports: Relatórios gerados por esta conta
+        report: relatórios
+        targeted_reports: Relatórios feitos sobre esta conta
       silence: Silêncio
       statuses: Status
       title: Contas
@@ -67,6 +129,14 @@ pt:
       undo_suspension: Desfazer supensão
       username: Usuário
       web: Web
+    action_logs:
+      actions:
+        confirm_user: "%{name} confirmou o endereço de e-mail do utilizador %{target}"
+        create_custom_emoji: "%{name} enviado emoji novo %{target}"
+        create_domain_block: "%{name} bloqueou o domínio %{target}"
+        create_email_domain_block: "%{name} adicionou na lista negra o domínio de correio electrónico %{target}"
+        demote_user: "%{name} rebaixou o utilizador %{target}"
+        destroy_domain_block: "%{name} desbloqueou o domínio %{target}"
     domain_blocks:
       add_new: Adicionar novo
       created_msg: Bloqueio do domínio está a ser processado
diff --git a/config/locales/simple_form.ca.yml b/config/locales/simple_form.ca.yml
index f8402af21..2ff3348f3 100644
--- a/config/locales/simple_form.ca.yml
+++ b/config/locales/simple_form.ca.yml
@@ -20,7 +20,7 @@ ca:
       sessions:
         otp: Introdueix el codi de dos factors des del teu telèfon o utilitza un dels teus codis de recuperació.
       user:
-        filtered_languages: Els idiomes seleccionats seran eliminats de les línies de temps públiques.
+        filtered_languages: Els idiomes seleccionats seran eliminats de les línies de temps públiques
     labels:
       defaults:
         avatar: Avatar
@@ -30,10 +30,12 @@ ca:
         data: Informació
         display_name: Mostrar nom
         email: Direcció de correu electrònic
+        expires_in: Caduca després
         filtered_languages: Idiomes filtrats
         header: Img. capçalera
         locale: Idioma
         locked: Fer privat aquest compte
+        max_uses: Nombre màxim d'usos
         new_password: Nova contrasenya
         note: Biografia
         otp_attempt: Codi de dos factors
diff --git a/config/locales/simple_form.de.yml b/config/locales/simple_form.de.yml
index 9933093ca..3c5e467a2 100644
--- a/config/locales/simple_form.de.yml
+++ b/config/locales/simple_form.de.yml
@@ -30,10 +30,12 @@ de:
         data: Daten
         display_name: Anzeigename
         email: E-Mail-Adresse
+        expires_in: Gültig bis
         filtered_languages: Gefilterte Sprachen
         header: Kopfbild
         locale: Sprache
         locked: Gesperrtes Profil
+        max_uses: Max Verwendungen
         new_password: Neues Passwort
         note: Über mich
         otp_attempt: Zwei-Faktor-Authentisierungs-Code
@@ -54,6 +56,7 @@ de:
       interactions:
         must_be_follower: Benachrichtigungen von Nicht-Folgenden blockieren
         must_be_following: Benachrichtigungen von Profilen blockieren, denen ich nicht folge
+        must_be_following_dm: Private Nachrichten von Profilen denen ich nicht folge blockieren
       notification_emails:
         digest: Schicke Übersichts-E-Mails
         favourite: E-Mail senden, wenn jemand meinen Beitrag favorisiert
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index ff1a40ccd..756f6b119 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -13,8 +13,9 @@ en:
         note:
           one: <span class="note-counter">1</span> character left
           other: <span class="note-counter">%{count}</span> characters left
+        setting_flavour: Affects how Mastodon looks when you're logged in from any device
         setting_noindex: Affects your public profile and status pages
-        setting_theme: Affects how Mastodon looks when you're logged in from any device.
+        setting_skin: Reskins the selected Mastodon flavour
       imports:
         data: CSV file exported from another Mastodon instance
       sessions:
@@ -45,10 +46,11 @@ en:
         setting_default_privacy: Post privacy
         setting_default_sensitive: Always mark media as sensitive
         setting_delete_modal: Show confirmation dialog before deleting a toot
+        setting_flavour: Flavour
         setting_noindex: Opt-out of search engine indexing
         setting_reduce_motion: Reduce motion in animations
+        setting_skin: Skin
         setting_system_font_ui: Use system's default font
-        setting_theme: Site theme
         setting_unfollow_modal: Show confirmation dialog before unfollowing someone
         severity: Severity
         type: Import type
diff --git a/config/locales/simple_form.es.yml b/config/locales/simple_form.es.yml
index 63a0710d1..01fe2af4c 100644
--- a/config/locales/simple_form.es.yml
+++ b/config/locales/simple_form.es.yml
@@ -4,6 +4,7 @@ es:
     hints:
       defaults:
         avatar: PNG, GIF o JPG. Máximo 2MB. Será escalado a 120x120px
+        digest: Enviado tras un largo periodo de inactividad, con un resumen de las menciones que has recibido durante tu ausencia
         display_name:
           one: <span class="name-counter">1</span> caracter restante
           other: <span class="name-counter">%{count}</span> caracteres restantes
@@ -13,12 +14,13 @@ es:
           one: <span class="name-counter">1</span> caracter restante
           other: <span class="name-counter">%{count}</span> caracteres restantes
         setting_noindex: Afecta a tu perfil público y páginas de estado
+        setting_theme: Afecta al aspecto de Mastodon cuando te identificas desde cualquier dispositivo.
       imports:
         data: Archivo CSV exportado desde otra instancia de Mastodon
       sessions:
         otp: Ingresa el código de autenticación de dos factores de tu teléfono o usa uno de tus códigos de recuperación.
       user:
-        filtered_languages: Los lenguajes seleccionados serán removidos de tus líneas de tiempo públicas.
+        filtered_languages: Los idiomas seleccionados dejarán de mostrarse para ti en las líneas de tiempo públicas
     labels:
       defaults:
         avatar: Avatar
@@ -28,21 +30,33 @@ es:
         data: Información
         display_name: Mostrar nombre
         email: Dirección de correo electrónico
+        expires_in: Expirar tras
+        filtered_languages: Idiomas filtrados
         header: Img. cabecera
         locale: Idioma
         locked: Hacer privada esta cuenta
+        max_uses: Máx. número de usos
         new_password: Nueva contraseña
         note: Biografía
         otp_attempt: Código de dos factores
         password: Contraseña
+        setting_auto_play_gif: Reproducir automáticamente los GIFs animados
         setting_boost_modal: Mostrar ventana de confirmación antes de un Retoot
         setting_default_privacy: Privacidad de publicaciones
+        setting_default_sensitive: Marcar siempre imágenes como sensibles
+        setting_delete_modal: Mostrar diálogo de confirmación antes de borrar un toot
+        setting_noindex: Excluirse del indexado de motores de búsqueda
+        setting_reduce_motion: Reducir el movimiento de las animaciones
+        setting_system_font_ui: Utilizar la tipografía por defecto del sistema
+        setting_theme: Tema del sitio
+        setting_unfollow_modal: Mostrar diálogo de confirmación antes de dejar de seguir a alguien
         severity: Severidad
         type: Importar tipo
         username: Nombre de usuario
       interactions:
         must_be_follower: Bloquear notificaciones de personas que no te siguen
         must_be_following: Bloquear notificaciones de personas que no sigues
+        must_be_following_dm: Bloquear mensajes directos de la gente que no sigues
       notification_emails:
         digest: Enviar resumen de correos electrónicos
         favourite: Enviar correo electrónico cuando alguien de a favorito en su publicación
diff --git a/config/locales/simple_form.fr.yml b/config/locales/simple_form.fr.yml
index 06ab018a7..2397e5161 100644
--- a/config/locales/simple_form.fr.yml
+++ b/config/locales/simple_form.fr.yml
@@ -30,10 +30,12 @@ fr:
         data: Données
         display_name: Nom public
         email: Adresse courriel
+        expires_in: Expire après
         filtered_languages: Langues filtrées
         header: Image d’en-tête
         locale: Langue
         locked: Verrouiller le compte
+        max_uses: Nombre maximum d'utilisations
         new_password: Nouveau mot de passe
         note: Présentation
         otp_attempt: Code d’identification à deux facteurs
@@ -54,6 +56,7 @@ fr:
       interactions:
         must_be_follower: Masquer les notifications des personnes qui ne vous suivent pas
         must_be_following: Masquer les notifications des personnes que vous ne suivez pas
+        must_be_following_dm: Bloquer les messages directs des personnes que vous ne suivez pas
       notification_emails:
         digest: Envoyer des courriels récapitulatifs
         favourite: Envoyer un courriel lorsque quelqu’un ajoute mes statuts à ses favoris
diff --git a/config/locales/simple_form.he.yml b/config/locales/simple_form.he.yml
index d6af5e7ac..604236a73 100644
--- a/config/locales/simple_form.he.yml
+++ b/config/locales/simple_form.he.yml
@@ -4,6 +4,7 @@ he:
     hints:
       defaults:
         avatar: PNG, GIF או JPG. מקסימום 2MB. גודל התמונה יוקטן ל-120x120px
+        digest: נשלח לאחר תקופה ארוכה של אי-פעילות עם סיכום איזכורים שקיבלת בהעדרך
         display_name:
           one: נותרה אות<span class="name-counter">אחת</span>
           other: נותרו<span class="name-counter">%{count}</span> אותיות
@@ -12,12 +13,14 @@ he:
         note:
           one: נותרה אות<span class="note-counter">אחת</span>
           other: נותרו <span class="note-counter">%{count}</span> אותיות
+        setting_noindex: משפיע על הפרופיל הציבורי שלך ועמודי ההודעות
+        setting_theme: משפיע על המראה של מסטודון בעת החיבור המזוהה מכל מכשיר שהוא.
       imports:
         data: קובץ CSV שיוצא משרת מסטודון אחר
       sessions:
         otp: נא להקליד קוד אימות דו-שלבי ממכשירך או קוד אחזור גישה.
       user:
-        filtered_languages: בחירת שפות להסתרה מציר הזמן הציבורי שלך.
+        filtered_languages: שפות שנבחרו יוסתרו מציר הזמן הציבורי בשבילך
     labels:
       defaults:
         avatar: תמונת פרופיל
@@ -27,9 +30,12 @@ he:
         data: מידע
         display_name: שם להצגה
         email: כתובת דוא"ל
+        expires_in: תפוגה לאחר
+        filtered_languages: שפות מסוננות
         header: ראשה
         locale: שפה
         locked: הפוך חשבון לפרטי
+        max_uses: מספר מרבי של שימושים
         new_password: סיסמא חדשה
         note: אודות
         otp_attempt: קוד אימות דו-שלבי
@@ -37,12 +43,20 @@ he:
         setting_auto_play_gif: ניגון אוטומטי של גיפים
         setting_boost_modal: הצגת דיאלוג אישור לפני הדהוד
         setting_default_privacy: פרטיות ההודעות
+        setting_default_sensitive: תמיד לתת סימון "רגיש" למדיה
+        setting_delete_modal: להראות תיבת אישור לפני מחיקת חיצרוץ
+        setting_noindex: לבקש הסתרה ממנועי חיפוש
+        setting_reduce_motion: הפחתת תנועה בהנפשות
+        setting_system_font_ui: להשתמש בגופן ברירת המחדל של המערכת
+        setting_theme: ערכת העיצוב של האתר
+        setting_unfollow_modal: להראות תיבת אישור לפני הפסקת מעקב אחרי אחרים
         severity: חומרה
         type: סוג יבוא
         username: שם משתמש
       interactions:
         must_be_follower: חסימת התראות משאינם עוקבים
         must_be_following: חסימת התראות משאינם נעקבים
+        must_be_following_dm: חסימת הודעות ישירות מכותבים שאינם במעקב
       notification_emails:
         digest: שליחת הודעות דוא"ל מסכמות
         favourite: שליחת דוא"ל כשמחבבים חצרוץ
diff --git a/config/locales/simple_form.nl.yml b/config/locales/simple_form.nl.yml
index 5637bd848..f2847e7ca 100644
--- a/config/locales/simple_form.nl.yml
+++ b/config/locales/simple_form.nl.yml
@@ -4,23 +4,23 @@ nl:
     hints:
       defaults:
         avatar: PNG, GIF of JPG. Maximaal 2MB. Wordt teruggeschaald naar 120x120px
-        digest: Wordt na een lange periode van inactiviteit verzonden, met een samenvatting van vermeldingen tijdens je afwezigheid.
+        digest: Wordt na een lange periode van inactiviteit verzonden met een samenvatting van vermeldingen tijdens je afwezigheid
         display_name:
           one: <span class="name-counter">1</span> teken over
           other: <span class="name-counter">%{count}</span> tekens over
         header: PNG, GIF of JPG. Maximaal 2MB. Wordt teruggeschaald naar 700x335px
-        locked: Vereist dat je handmatig volgers moet accepteren en stelt de privacy van toots standaard in op alleen volgers
+        locked: Vereist dat je handmatig volgers moet accepteren
         note:
           one: <span class="note-counter">1</span> teken over
           other: <span class="note-counter">%{count}</span> tekens over
         setting_noindex: Heeft invloed op jouw openbare profiel en toots
-        setting_theme: Heeft invloed op hoe de webapp van Mastodon er uit ziet, op elk apparaat waarmee je inlogt.
+        setting_theme: Heeft invloed op hoe Mastodon eruitziet op elk apparaat waarmee je inlogt.
       imports:
         data: CSV-bestand dat op een andere Mastodon-server werd geëxporteerd
       sessions:
         otp: Voer de tweestaps-aanmeldcode vanaf jouw mobiele telefoon in of gebruik een van jouw herstelcode's.
       user:
-        filtered_languages: De geselecteerde talen worden uit de lokale en globale tijdlijn verwijderd.
+        filtered_languages: De geselecteerde talen worden uit de lokale en globale tijdlijn verwijderd
     labels:
       defaults:
         avatar: Avatar
@@ -30,10 +30,12 @@ nl:
         data: Gegevens
         display_name: Weergavenaam
         email: E-mailadres
+        expires_in: Vervalt na
         filtered_languages: Talen filteren
         header: Omslagfoto
         locale: Taal
         locked: Maak account besloten
+        max_uses: Max aantal keer te gebruiken
         new_password: Nieuwe wachtwoord
         note: Bio
         otp_attempt: Tweestaps-aanmeldcode
@@ -46,12 +48,15 @@ nl:
         setting_noindex: Jouw toots niet door zoekmachines laten indexeren
         setting_reduce_motion: Langzamere animaties
         setting_system_font_ui: Standaardlettertype van jouw systeem gebruiken
+        setting_theme: Site thema
         setting_unfollow_modal: Vraag voor het ontvolgen van iemand een bevestiging
+        severity: Strengheid
         type: Importtype
         username: gebruikersnaam
       interactions:
         must_be_follower: Blokkeer meldingen van mensen die jou niet volgen
         must_be_following: Blokkeer meldingen van mensen die jij niet volgt
+        must_be_following_dm: Blokkeer directe berichten van mensen die jij niet volgt
       notification_emails:
         digest: Verstuur periodiek e-mails met een samenvatting
         favourite: Verstuur een e-mail wanneer iemand jouw toot als favoriet markeert
diff --git a/config/locales/simple_form.pt-BR.yml b/config/locales/simple_form.pt-BR.yml
index 9d60e0171..2ed0e3329 100644
--- a/config/locales/simple_form.pt-BR.yml
+++ b/config/locales/simple_form.pt-BR.yml
@@ -4,7 +4,7 @@ pt-BR:
     hints:
       defaults:
         avatar: PNG, GIF or JPG. Arquivos de até 2MB. Eles serão diminuídos para 120x120px
-        digest: Enviado após um longo período de inatividade com um resumo das menções que você recebeu em sua ausência.
+        digest: Enviado após um longo período de inatividade com um resumo das menções que você recebeu em sua ausência
         display_name:
           one: <span class="name-counter">1</span> caracter restante
           other: <span class="name-counter">%{count}</span> caracteres restantes
@@ -20,7 +20,7 @@ pt-BR:
       sessions:
         otp: Insira o código de autenticação do seu celular ou use um dos códigos de recuperação.
       user:
-        filtered_languages: Selecione os idiomas que devem ser removidos de suas timelines públicas.
+        filtered_languages: Selecione os idiomas que devem ser removidos de suas timelines públicas
     labels:
       defaults:
         avatar: Avatar
@@ -30,10 +30,12 @@ pt-BR:
         data: Dados
         display_name: Nome de exibição
         email: Endereço de e-mail
+        expires_in: Expira em
         filtered_languages: Idiomas filtrados
         header: Cabeçalho
         locale: Idioma
         locked: Trancar conta
+        max_uses: Número máximo de usos
         new_password: Nova senha
         note: Bio
         otp_attempt: Código de autenticação em dois passos
@@ -54,6 +56,7 @@ pt-BR:
       interactions:
         must_be_follower: Bloquear notificações de não-seguidores
         must_be_following: Bloquear notificações de pessoas que você não segue
+        must_be_following_dm: Bloquear mensagens diretas de pessoas que você não segue
       notification_emails:
         digest: Mandar e-mails com relatórios
         favourite: Mandar um e-mail quando alguém favoritar suas postagens
diff --git a/config/locales/simple_form.pt.yml b/config/locales/simple_form.pt.yml
index 0df7a9bb9..a5afd02d3 100644
--- a/config/locales/simple_form.pt.yml
+++ b/config/locales/simple_form.pt.yml
@@ -30,6 +30,7 @@ pt:
         password: Palavra-passe
         setting_boost_modal: Pedir confirmação antes de partilhar um post
         setting_default_privacy: Privacidade padrão de posts
+        setting_reduce_motion: Reduzir movimento em animações
         severity: Severity
         type: Import type
         username: Utilizador
diff --git a/config/locales/simple_form.zh-CN.yml b/config/locales/simple_form.zh-CN.yml
index 23a1f59da..b1888f4e3 100644
--- a/config/locales/simple_form.zh-CN.yml
+++ b/config/locales/simple_form.zh-CN.yml
@@ -26,10 +26,12 @@ zh-CN:
         data: 数据文件
         display_name: 昵称
         email: 电子邮件地址
+        expires_in: 失效时间
         filtered_languages: 语言过滤
         header: 个人资料页横幅图片
         locale: 语言
         locked: 保护你的帐户(锁嘟)
+        max_uses: 最大使用次数
         new_password: 新密码
         note: 简介
         otp_attempt: 双重认证代码
diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml
index ec913113f..cd087c3e8 100644
--- a/config/locales/zh-CN.yml
+++ b/config/locales/zh-CN.yml
@@ -2,10 +2,10 @@
 zh-CN:
   about:
     about_hashtag_html: 这里展示的是带有话题标签 <strong>#%{hashtag}</strong> 的公开嘟文。如果你想与他们互动,你需要在任意一个 Mastodon 实例或与其兼容的网站上拥有一个帐户。
-    about_mastodon_html: Mastodon(长毛象)是一个基于开放式网络协议和自由、开源软件建立的社交网络,有着类似于电子邮件的分布式设计。
+    about_mastodon_html: Mastodon(长毛象)是一个建立在开放式网络协议和自由、开源软件之上的社交网络,有着类似于电子邮件的分布式设计。
     about_this: 关于本实例
     closed_registrations: 这个实例目前没有开放注册。不过,你可以前往其他实例注册一个帐户,同样可以加入到这个网络中哦!
-    contact: 联络
+    contact: 联系方式
     contact_missing: 未设定
     contact_unavailable: 未公开
     description_headline: 关于 %{domain}
@@ -46,7 +46,7 @@ zh-CN:
     posts: 嘟文
     posts_with_replies: 嘟文和回复
     remote_follow: 跨站关注
-    reserved_username: 此用户名已保留
+    reserved_username: 此用户名已被保留
     roles:
       admin: 管理员
       moderator: 协管
@@ -56,9 +56,9 @@ zh-CN:
       account: 管理员
       create: 新建
       created_at: 日期
-      created_msg: 管理记录建立成功!
+      created_msg: 管理备忘建立成功!
       delete: 删除
-      destroyed_msg: 管理记录删除成功!
+      destroyed_msg: 管理备忘删除成功!
     accounts:
       are_you_sure: 你确定吗?
       by_domain: 域名
@@ -93,7 +93,7 @@ zh-CN:
         silenced: 已隐藏
         suspended: 已封禁
         title: 帐户状态
-      moderation_notes: 管理记录
+      moderation_notes: 管理备忘
       most_recent_activity: 最后一次活跃的时间
       most_recent_ip: 最后一次活跃的 IP 地址
       not_subscribed: 未订阅
@@ -116,7 +116,7 @@ zh-CN:
       roles:
         admin: 管理员
         moderator: 协管
-        user: 用户
+        user: 普通用户
       salmon_url: Salmon URL
       search: 搜索
       shared_inbox_url: 公用收件箱(Shared Inbox)URL
@@ -133,6 +133,32 @@ zh-CN:
       unsubscribe: 取消订阅
       username: 用户名
       web: 站内页面
+    action_logs:
+      actions:
+        confirm_user: "%{name} 确认了用户 %{target} 的电子邮件地址"
+        create_custom_emoji: "%{name} 添加了新的自定义表情 %{target}"
+        create_domain_block: "%{name} 屏蔽了域名 %{target}"
+        create_email_domain_block: "%{name} 屏蔽了电子邮件域名 %{target}"
+        demote_user: "%{name} 对用户 %{target} 进行了降任操作"
+        destroy_domain_block: "%{name} 解除了对域名 %{target} 的屏蔽"
+        destroy_email_domain_block: "%{name} 解除了对电子邮件域名 %{target} 的屏蔽"
+        destroy_status: "%{name} 删除了 %{target} 的嘟文"
+        disable_2fa_user: "%{name} 停用了用户 %{target} 的双重认证"
+        disable_custom_emoji: "%{name} 停用了自定义表情 %{target}"
+        disable_user: "%{name} 将用户 %{target} 设置为禁止登录"
+        enable_custom_emoji: "%{name} 启用了自定义表情 %{target}"
+        enable_user: "%{name} 将用户 %{target} 设置为允许登录"
+        memorialize_account: "%{name} 将 %{target} 的帐户设置为追悼帐户"
+        promote_user: "%{name} 对用户 %{target} 进行了升任操作"
+        reset_password_user: "%{name} 重置了用户 %{target} 的密码"
+        resolve_report: "%{name} 处理了举报 %{target}"
+        silence_account: "%{name} 隐藏了用户 %{target}"
+        suspend_account: "%{name} 封禁了用户 %{target}"
+        unsilence_account: "%{name} 解除了用户 %{target} 的隐藏状态"
+        unsuspend_account: "%{name} 解除了用户 %{target} 的封禁状态"
+        update_custom_emoji: "%{name} 更新了自定义表情 %{target}"
+        update_status: "%{name} 刷新了 %{target} 的嘟文"
+      title: 运营日志
     custom_emojis:
       copied_msg: 成功将表情复制到本地
       copy: 复制
@@ -156,14 +182,14 @@ zh-CN:
       unlisted: 已隐藏
       update_failed_msg: 表情更新失败!
       updated_msg: 表情更新成功!
-      upload: 上传
+      upload: 上传新表情
     domain_blocks:
       add_new: 添加新条目
       created_msg: 正在进行域名屏蔽
       destroyed_msg: 域名屏蔽已撤销
       domain: 域名
       new:
-        create: 添加域名屏蔽
+        create: 添加屏蔽
         hint: 域名屏蔽不会阻止该域名下的帐户进入本站的数据库,但是会对来自这个域名的帐户自动进行预先设置的管理操作。
         severity:
           desc_html: 选择<strong>自动隐藏</strong>会将该域名下帐户发送的嘟文设置为仅关注者可见;选择<strong>自动封禁</strong>会将该域名下帐户发送的嘟文、媒体文件以及个人资料数据从本实例上删除;如果你只是想拒绝接收来自该域名的任何媒体文件,请选择<strong>无</strong>。
@@ -194,7 +220,7 @@ zh-CN:
       destroyed_msg: 电子邮件域名屏蔽删除成功
       domain: 域名
       new:
-        create: 添加屏蔽
+        create: 添加域名
         title: 添加电子邮件域名屏蔽
       title: 电子邮件域名屏蔽
     instances:
@@ -203,6 +229,13 @@ zh-CN:
       reset: 重置
       search: 搜索
       title: 已知实例
+    invites:
+      filter:
+        all: 全部
+        available: 可用
+        expired: 已失效
+        title: 筛选
+      title: 邀请用户
     reports:
       action_taken_by: 操作执行者
       are_you_sure: 你确定吗?
@@ -241,6 +274,9 @@ zh-CN:
         deletion:
           desc_html: 允许所有人删除自己的帐户
           title: 开放删除帐户权限
+        min_invite_role:
+          disabled: 没有人
+          title: 允许发送邀请的用户组
         open:
           desc_html: 允许任何人建立一个帐户
           title: 开放注册
@@ -314,6 +350,8 @@ zh-CN:
     invalid_reset_password_token: 密码重置令牌无效或已过期。请重新发起重置密码请求。
     login: 登录
     logout: 登出
+    migrate_account: 迁移到另一个帐户
+    migrate_account_html: 如果你希望引导其他人关注另一个帐户,请<a href="%{path}">点击这里设置</a>。
     register: 注册
     resend_confirmation: 重新发送确认邮件
     reset_password: 重置密码
@@ -348,7 +386,7 @@ zh-CN:
     description_html: 继续操作将会<strong>永久地、不可撤销地</strong>删除你帐户中的内容,并冻结你的帐户。你的用户名将会被保留,以防有人冒用你的身份。
     proceed: 删除帐户
     success_msg: 你的帐户已经成功删除
-    warning_html: 我们只能保证本实例上的内容已经被彻底删除。对于已经被广泛传播的内容,它们在本实例以外的某些地方可能仍然可见。此外,失去连接的服务器以及停止接收订阅的服务器上的数据亦无法删除。
+    warning_html: 我们只能保证本实例上的内容将会被彻底删除。对于已经被广泛传播的内容,它们在本实例以外的某些地方可能仍然可见。此外,失去连接的服务器以及停止接收订阅的服务器上的数据亦无法删除。
     warning_title: 关于已传播的内容的警告
   errors:
     '403': 无权查看
@@ -394,12 +432,37 @@ zh-CN:
       muting: 隐藏列表
     upload: 上传
   in_memoriam_html: 谨此悼念。
+  invites:
+    delete: 停用
+    expired: 已失效
+    expires_in:
+      '1800': 30 分钟后
+      '21600': 6 小时后
+      '3600': 1 小时后
+      '43200': 12 小时后
+      '86400': 1 天后
+    expires_in_prompt: 永不过期
+    generate: 生成邀请链接
+    max_uses: "%{count} 次"
+    max_uses_prompt: 无限制
+    prompt: 生成可供分享的链接以便邀请他人在本实例注册
+    table:
+      expires_at: 失效时间
+      uses: 已使用次数
+    title: 邀请用户
   landing_strip_html: "<strong>%{name}</strong> 是一位来自 %{link_to_root_path} 的用户。如果你想关注他们或者与他们互动,你需要在任意一个 Mastodon 实例或与其兼容的网站上拥有一个帐户。"
   landing_strip_signup_html: 还没有这种帐户?你可以<a href="%{sign_up_path}">在本站注册一个</a>。
   media_attachments:
     validations:
       images_and_video: 无法在嘟文中同时插入视频和图片
       too_many: 最多只能添加 4 张图片
+  migrations:
+    acct: 新帐户的 用户名@域名
+    currently_redirecting: 目前你的个人资料页显示的新帐户是:
+    proceed: 保存
+    updated_msg: 帐户迁移设置更新成功!
+  moderation:
+    title: 运营
   notification_mailer:
     digest:
       body: 自从你最后一次(时间是%{since})登录 %{instance} 以来,你错过了这些嘟嘟滴滴:
@@ -508,8 +571,9 @@ zh-CN:
     development: 开发
     edit_profile: 更改个人信息
     export: 导出
-    followers: 授权的关注者
+    followers: 已授权的关注者
     import: 导入
+    migrate: 帐户迁移
     notifications: 通知
     preferences: 首选项
     settings: 设置
diff --git a/config/settings.yml b/config/settings.yml
index 01478972c..5aad45da2 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -27,7 +27,8 @@ defaults: &defaults
   reduce_motion: false
   system_font_ui: false
   noindex: false
-  theme: 'glitch'
+  flavour: 'glitch'
+  skin: 'default'
   notification_emails:
     follow: false
     reblog: false
diff --git a/config/webpack/configuration.js b/config/webpack/configuration.js
index 74f75d89b..9cdd6f934 100644
--- a/config/webpack/configuration.js
+++ b/config/webpack/configuration.js
@@ -1,25 +1,57 @@
 // Common configuration for webpacker loaded from config/webpacker.yml
 
-const { basename, dirname, join, resolve } = require('path');
+const { basename, dirname, extname, join, resolve } = require('path');
 const { env } = require('process');
 const { safeLoad } = require('js-yaml');
-const { readFileSync } = require('fs');
+const { lstatSync, readFileSync } = require('fs');
 const glob = require('glob');
 
 const configPath = resolve('config', 'webpacker.yml');
 const loadersDir = join(__dirname, 'loaders');
 const settings = safeLoad(readFileSync(configPath), 'utf8')[env.NODE_ENV];
-const themeFiles = glob.sync('app/javascript/themes/*/theme.yml');
-const themes = {};
+const flavourFiles = glob.sync('app/javascript/flavours/*/theme.yml');
+const skinFiles = glob.sync('app/javascript/skins/*/*');
+const flavours = {};
 
-for (let i = 0; i < themeFiles.length; i++) {
-  const themeFile = themeFiles[i];
-  const data = safeLoad(readFileSync(themeFile), 'utf8');
+const core = function () {
+  const coreFile = resolve('app', 'javascript', 'core', 'theme.yml');
+  const data = safeLoad(readFileSync(coreFile), 'utf8');
   if (!data.pack_directory) {
-    data.pack_directory = dirname(themeFile);
+    data.pack_directory = dirname(coreFile);
   }
-  if (data.pack) {
-    themes[basename(dirname(themeFile))] = data;
+  return data.pack ? data : {};
+}();
+
+for (let i = 0; i < flavourFiles.length; i++) {
+  const flavourFile = flavourFiles[i];
+  const data = safeLoad(readFileSync(flavourFile), 'utf8');
+  data.name = basename(dirname(flavourFile));
+  data.skin = {};
+  if (!data.pack_directory) {
+    data.pack_directory = dirname(flavourFile);
+  }
+  if (data.pack && typeof data.pack === 'object') {
+    flavours[data.name] = data;
+  }
+}
+
+for (let i = 0; i < skinFiles.length; i++) {
+  const skinFile = skinFiles[i];
+  let skin = basename(skinFile);
+  const name = basename(dirname(skinFile));
+  if (!flavours[name]) {
+    continue;
+  }
+  const data = flavours[name].skin;
+  if (lstatSync(skinFile).isDirectory()) {
+    data[skin] = {};
+    const skinPacks = glob.sync(resolve(skinFile, '*.{css,scss}'));
+    for (let j = 0; j < skinPacks.length; j++) {
+      const pack = skinPacks[i];
+      data[skin][basename(pack, extname(pack))] = pack;
+    }
+  } else if ((skin = skin.match(/^(.*)\.s?css$/i))) {
+    data[skin[1]] = { common: skinFile };
   }
 }
 
@@ -43,7 +75,8 @@ const output = {
 
 module.exports = {
   settings,
-  themes,
+  core,
+  flavours,
   env,
   loadersDir,
   output,
diff --git a/config/webpack/generateLocalePacks.js b/config/webpack/generateLocalePacks.js
index cd3bed50c..a943589f7 100644
--- a/config/webpack/generateLocalePacks.js
+++ b/config/webpack/generateLocalePacks.js
@@ -57,7 +57,7 @@ Object.keys(glitchMessages).forEach(function (key) {
 //
 import messages from '../../app/javascript/mastodon/locales/${locale}.json';
 import localeData from ${JSON.stringify(localeDataPath)};
-import { setLocale } from '../../app/javascript/mastodon/locales';
+import { setLocale } from 'locales';
 ${glitchInject}
 setLocale({messages: mergedMessages, localeData: localeData});
 `;
diff --git a/config/webpack/shared.js b/config/webpack/shared.js
index 5d176db4e..e4b057ffb 100644
--- a/config/webpack/shared.js
+++ b/config/webpack/shared.js
@@ -1,38 +1,60 @@
 // Note: You must restart bin/webpack-dev-server for changes to take effect
 
 const webpack = require('webpack');
-const { basename, dirname, join, relative, resolve } = require('path');
+const { basename, join, resolve } = require('path');
 const { sync } = require('glob');
 const ExtractTextPlugin = require('extract-text-webpack-plugin');
 const ManifestPlugin = require('webpack-manifest-plugin');
 const extname = require('path-complete-extname');
-const { env, settings, themes, output, loadersDir } = require('./configuration.js');
+const { env, settings, core, flavours, output, loadersDir } = require('./configuration.js');
 const localePackPaths = require('./generateLocalePacks');
 
-const extensionGlob = `**/*{${settings.extensions.join(',')}}*`;
-const entryPath = join(settings.source_path, settings.source_entry_path);
-const packPaths = sync(join(entryPath, extensionGlob));
+function reducePacks (data, into = {}) {
+  if (!data.pack) {
+    return into;
+  }
+  Object.keys(data.pack).reduce((map, entry) => {
+    const pack = data.pack[entry];
+    if (!pack) {
+      return map;
+    }
+    const packFile = typeof pack === 'string' ? pack : pack.filename;
+    if (packFile) {
+      map[data.name ? `flavours/${data.name}/${entry}` : `core/${entry}`] = resolve(data.pack_directory, packFile);
+    }
+    return map;
+  }, into);
+  if (data.name) {
+    Object.keys(data.skin).reduce((map, entry) => {
+      const skin = data.skin[entry];
+      const skinName = entry;
+      if (!skin) {
+        return map;
+      }
+      Object.keys(skin).reduce((map, entry) => {
+        const packFile = skin[entry];
+        if (!packFile) {
+          return map;
+        }
+        map[`skins/${data.name}/${skinName}/${entry}`] = resolve(packFile);
+        return map;
+      }, into);
+      return map;
+    }, into);
+  }
+  return into;
+}
 
 module.exports = {
   entry: Object.assign(
-    packPaths.reduce((map, entry) => {
-      const localMap = map;
-      const namespace = relative(join(entryPath), dirname(entry));
-      localMap[join(namespace, basename(entry, extname(entry)))] = resolve(entry);
-      return localMap;
-    }, {}),
+    { locales: resolve('app', 'javascript', 'locales') },
     localePackPaths.reduce((map, entry) => {
       const localMap = map;
       localMap[basename(entry, extname(entry, extname(entry)))] = resolve(entry);
       return localMap;
     }, {}),
-    Object.keys(themes).reduce(
-      (themePaths, name) => {
-        const themeData = themes[name];
-        themePaths[`themes/${name}`] = resolve(themeData.pack_directory, themeData.pack);
-        return themePaths;
-      }, {}
-    )
+    reducePacks(core),
+    Object.keys(flavours).reduce((map, entry) => reducePacks(flavours[entry], map), {})
   ),
 
   output: {
@@ -64,7 +86,7 @@ module.exports = {
       writeToFileEmit: true,
     }),
     new webpack.optimize.CommonsChunkPlugin({
-      name: 'common',
+      name: 'locales',
       minChunks: Infinity, // It doesn't make sense to use common chunks with multiple frontend support.
     }),
   ],
diff --git a/config/webpacker.yml b/config/webpacker.yml
index 8d8470651..50d95813a 100644
--- a/config/webpacker.yml
+++ b/config/webpacker.yml
@@ -2,7 +2,6 @@
 
 default: &default
   source_path: app/javascript
-  source_entry_path: packs
   public_output_path: packs
   cache_path: tmp/cache/webpacker
 
@@ -13,17 +12,6 @@ default: &default
   # Reload manifest.json on all requests so we reload latest compiled packs
   cache_manifest: false
 
-  extensions:
-    - .js
-    - .sass
-    - .scss
-    - .css
-    - .png
-    - .svg
-    - .gif
-    - .jpeg
-    - .jpg
-
 development:
   <<: *default
   compile: true
diff --git a/db/migrate/20171130000000_add_embed_url_to_preview_cards.rb b/db/migrate/20171130000000_add_embed_url_to_preview_cards.rb
new file mode 100644
index 000000000..d19c0091b
--- /dev/null
+++ b/db/migrate/20171130000000_add_embed_url_to_preview_cards.rb
@@ -0,0 +1,18 @@
+require Rails.root.join('lib', 'mastodon', 'migration_helpers')
+
+class AddEmbedUrlToPreviewCards < ActiveRecord::Migration[5.1]
+  include Mastodon::MigrationHelpers
+
+  disable_ddl_transaction!
+
+  def up
+    safety_assured do
+      add_column_with_default :preview_cards, :embed_url, :string, default: '', allow_null: false
+    end
+  end
+
+  def down
+    execute "UPDATE preview_cards SET url=embed_url WHERE embed_url!=''"
+    remove_column :preview_cards, :embed_url
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index c87c9b393..4cf886a00 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 20171129172043) do
+ActiveRecord::Schema.define(version: 20171130000000) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -334,6 +334,7 @@ ActiveRecord::Schema.define(version: 20171129172043) do
     t.integer "height", default: 0, null: false
     t.datetime "created_at", null: false
     t.datetime "updated_at", null: false
+    t.string "embed_url", default: "", null: false
     t.index ["url"], name: "index_preview_cards_on_url", unique: true
   end
 
diff --git a/lib/mastodon/migration_helpers.rb b/lib/mastodon/migration_helpers.rb
index 2b5a6cd42..6f6f99f63 100644
--- a/lib/mastodon/migration_helpers.rb
+++ b/lib/mastodon/migration_helpers.rb
@@ -99,7 +99,7 @@ module Mastodon
     # default - The default value for the column.
     # null - When set to `true` the column will allow NULL values.
     #        The default is to not allow NULL values.
-    def add_timestamps_with_timezone(table_name, options = {})
+    def add_timestamps_with_timezone(table_name, **options)
       options[:null] = false if options[:null].nil?
 
       [:created_at, :updated_at].each do |column_name|
@@ -134,7 +134,7 @@ module Mastodon
     #     add_concurrent_index :users, :some_column
     #
     # See Rails' `add_index` for more info on the available arguments.
-    def add_concurrent_index(table_name, column_name, options = {})
+    def add_concurrent_index(table_name, column_name, **options)
       if transaction_open?
         raise 'add_concurrent_index can not be run inside a transaction, ' \
           'you can disable transactions by calling disable_ddl_transaction! ' \
@@ -158,7 +158,7 @@ module Mastodon
     #     remove_concurrent_index :users, :some_column
     #
     # See Rails' `remove_index` for more info on the available arguments.
-    def remove_concurrent_index(table_name, column_name, options = {})
+    def remove_concurrent_index(table_name, column_name, **options)
       if transaction_open?
         raise 'remove_concurrent_index can not be run inside a transaction, ' \
           'you can disable transactions by calling disable_ddl_transaction! ' \
@@ -182,7 +182,7 @@ module Mastodon
     #     remove_concurrent_index :users, "index_X_by_Y"
     #
     # See Rails' `remove_index` for more info on the available arguments.
-    def remove_concurrent_index_by_name(table_name, index_name, options = {})
+    def remove_concurrent_index_by_name(table_name, index_name, **options)
       if transaction_open?
         raise 'remove_concurrent_index_by_name can not be run inside a transaction, ' \
           'you can disable transactions by calling disable_ddl_transaction! ' \
diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb
index f10ace13e..ac04913e6 100644
--- a/lib/mastodon/version.rb
+++ b/lib/mastodon/version.rb
@@ -9,7 +9,7 @@ module Mastodon
     end
 
     def minor
-      0
+      1
     end
 
     def patch
@@ -17,7 +17,7 @@ module Mastodon
     end
 
     def pre
-      nil
+      'rc2'
     end
 
     def flags
diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake
index 995cf0d6f..0f2cc536a 100644
--- a/lib/tasks/mastodon.rake
+++ b/lib/tasks/mastodon.rake
@@ -326,5 +326,17 @@ namespace :mastodon do
         end
       end
     end
+
+    desc 'Migrate photo preview cards made before 2.1'
+    task migrate_photo_preview_cards: :environment do
+      status_ids = Status.joins(:preview_cards)
+                         .where(preview_cards: { embed_url: '', type: :photo })
+                         .reorder(nil)
+                         .group(:id)
+                         .pluck(:id)
+
+      PreviewCard.where(embed_url: '', type: :photo).delete_all
+      LinkCrawlWorker.push_bulk status_ids
+    end
   end
 end
diff --git a/spec/controllers/api/v1/accounts/relationships_controller_spec.rb b/spec/controllers/api/v1/accounts/relationships_controller_spec.rb
index f25b86ac1..508415fc8 100644
--- a/spec/controllers/api/v1/accounts/relationships_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/relationships_controller_spec.rb
@@ -32,7 +32,7 @@ describe Api::V1::Accounts::RelationshipsController do
         json = body_as_json
 
         expect(json).to be_a Enumerable
-        expect(json.first[:following]).to be_truthy
+        expect(json.first[:following]).to be true
         expect(json.first[:followed_by]).to be false
       end
     end
@@ -51,7 +51,8 @@ describe Api::V1::Accounts::RelationshipsController do
 
         expect(json).to be_a Enumerable
         expect(json.first[:id]).to eq simon.id.to_s
-        expect(json.first[:following]).to be_truthy
+        expect(json.first[:following]).to be true
+        expect(json.first[:showing_reblogs]).to be true
         expect(json.first[:followed_by]).to be false
         expect(json.first[:muting]).to be false
         expect(json.first[:requested]).to be false
@@ -59,6 +60,7 @@ describe Api::V1::Accounts::RelationshipsController do
 
         expect(json.second[:id]).to eq lewis.id.to_s
         expect(json.second[:following]).to be false
+        expect(json.second[:showing_reblogs]).to be false
         expect(json.second[:followed_by]).to be true
         expect(json.second[:muting]).to be false
         expect(json.second[:requested]).to be false
diff --git a/spec/controllers/api/v1/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts_controller_spec.rb
index f3b879421..053c53e5a 100644
--- a/spec/controllers/api/v1/accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts_controller_spec.rb
@@ -31,10 +31,10 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
         expect(response).to have_http_status(:success)
       end
 
-      it 'returns JSON with following=truthy and requested=false' do
+      it 'returns JSON with following=true and requested=false' do
         json = body_as_json
 
-        expect(json[:following]).to be_truthy
+        expect(json[:following]).to be true
         expect(json[:requested]).to be false
       end
 
@@ -50,11 +50,11 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
         expect(response).to have_http_status(:success)
       end
 
-      it 'returns JSON with following=false and requested=truthy' do
+      it 'returns JSON with following=false and requested=true' do
         json = body_as_json
 
         expect(json[:following]).to be false
-        expect(json[:requested]).to be_truthy
+        expect(json[:requested]).to be true
       end
 
       it 'creates a follow request relation between user and target user' do
diff --git a/spec/services/account_search_service_spec.rb b/spec/services/account_search_service_spec.rb
index 25bb56bca..4685e619f 100644
--- a/spec/services/account_search_service_spec.rb
+++ b/spec/services/account_search_service_spec.rb
@@ -72,7 +72,7 @@ describe AccountSearchService do
         describe 'and there is no account provided' do
           it 'uses search_for to find matches' do
             allow(Account).to receive(:search_for)
-            subject.call('two@example.com', 10, false, nil)
+            subject.call('two@example.com', 10, nil, resolve: false)
 
             expect(Account).to have_received(:search_for).with('two example.com', 10)
           end
@@ -82,9 +82,9 @@ describe AccountSearchService do
           it 'uses advanced_search_for to find matches' do
             account = Fabricate(:account)
             allow(Account).to receive(:advanced_search_for)
-            subject.call('two@example.com', 10, false, account)
+            subject.call('two@example.com', 10, account, resolve: false)
 
-            expect(Account).to have_received(:advanced_search_for).with('two example.com', account, 10)
+            expect(Account).to have_received(:advanced_search_for).with('two example.com', account, 10, nil)
           end
         end
       end
@@ -125,7 +125,7 @@ describe AccountSearchService do
         service = double(call: nil)
         allow(ResolveRemoteAccountService).to receive(:new).and_return(service)
 
-        results = subject.call('newuser@remote.com', 10, true)
+        results = subject.call('newuser@remote.com', 10, nil, resolve: true)
         expect(service).to have_received(:call).with('newuser@remote.com')
       end
 
@@ -133,7 +133,7 @@ describe AccountSearchService do
         service = double(call: nil)
         allow(ResolveRemoteAccountService).to receive(:new).and_return(service)
 
-        results = subject.call('newuser@remote.com', 10, false)
+        results = subject.call('newuser@remote.com', 10, nil, resolve: false)
         expect(service).not_to have_received(:call)
       end
     end
diff --git a/spec/services/post_status_service_spec.rb b/spec/services/post_status_service_spec.rb
index 91902ff69..92fbc73cd 100644
--- a/spec/services/post_status_service_spec.rb
+++ b/spec/services/post_status_service_spec.rb
@@ -182,7 +182,7 @@ RSpec.describe PostStatusService do
     expect(status2.id).to eq status1.id
   end
 
-  def create_status_with_options(options = {})
+  def create_status_with_options(**options)
     subject.call(Fabricate(:account), 'test', nil, options)
   end
 end
diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb
index 00475c699..3ffcc389b 100644
--- a/spec/services/search_service_spec.rb
+++ b/spec/services/search_service_spec.rb
@@ -68,7 +68,7 @@ describe SearchService do
           allow(AccountSearchService).to receive(:new).and_return(service)
 
           results = subject.call(query, 10)
-          expect(service).to have_received(:call).with(query, 10, false, nil)
+          expect(service).to have_received(:call).with(query, 10, nil, resolve: false)
           expect(results).to eq empty_results.merge(accounts: [account])
         end
       end