diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx
index 726ff547ff..9d9d57d8a6 100644
--- a/src/components/structures/ContextMenu.tsx
+++ b/src/components/structures/ContextMenu.tsx
@@ -22,6 +22,7 @@ import classNames from "classnames";
 
 import {Key} from "../../Keyboard";
 import {Writeable} from "../../@types/common";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 // Shamelessly ripped off Modal.js.  There's probably a better way
 // of doing reusable widgets like dialog boxes & menus where we go and
@@ -91,6 +92,7 @@ interface IState {
 // Generic ContextMenu Portal wrapper
 // all options inside the menu should be of role=menuitem/menuitemcheckbox/menuitemradiobutton and have tabIndex={-1}
 // this will allow the ContextMenu to manage its own focus using arrow keys as per the ARIA guidelines.
+@replaceableComponent("structures.ContextMenu")
 export class ContextMenu extends React.PureComponent<IProps, IState> {
     private initialFocus: HTMLElement;
 
@@ -467,6 +469,7 @@ export const useContextMenu = <T extends any = HTMLElement>(): ContextMenuTuple<
     return [isOpen, button, open, close, setIsOpen];
 };
 
+@replaceableComponent("structures.LegacyContextMenu")
 export default class LegacyContextMenu extends ContextMenu {
     render() {
         return this.renderMenu(false);
diff --git a/src/components/structures/CustomRoomTagPanel.js b/src/components/structures/CustomRoomTagPanel.js
index a79bdafeb5..73359f17a5 100644
--- a/src/components/structures/CustomRoomTagPanel.js
+++ b/src/components/structures/CustomRoomTagPanel.js
@@ -21,7 +21,9 @@ import * as sdk from '../../index';
 import dis from '../../dispatcher/dispatcher';
 import classNames from 'classnames';
 import * as FormattingUtils from '../../utils/FormattingUtils';
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
+@replaceableComponent("structures.CustomRoomTagPanel")
 class CustomRoomTagPanel extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js
index 0e4df4621d..9f5a0b6211 100644
--- a/src/components/structures/FilePanel.js
+++ b/src/components/structures/FilePanel.js
@@ -26,10 +26,12 @@ import { _t } from '../../languageHandler';
 import BaseCard from "../views/right_panel/BaseCard";
 import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
 import DesktopBuildsNotice, {WarningKind} from "../views/elements/DesktopBuildsNotice";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 /*
  * Component which shows the filtered file using a TimelinePanel
  */
+@replaceableComponent("structures.FilePanel")
 class FilePanel extends React.Component {
     static propTypes = {
         roomId: PropTypes.string.isRequired,
diff --git a/src/components/structures/GenericErrorPage.js b/src/components/structures/GenericErrorPage.js
index ab7d4f9311..cfd2016d47 100644
--- a/src/components/structures/GenericErrorPage.js
+++ b/src/components/structures/GenericErrorPage.js
@@ -16,7 +16,9 @@ limitations under the License.
 
 import React from 'react';
 import PropTypes from 'prop-types';
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
+@replaceableComponent("structures.GenericErrorPage")
 export default class GenericErrorPage extends React.PureComponent {
     static propTypes = {
         title: PropTypes.object.isRequired, // jsx for title
diff --git a/src/components/structures/GroupFilterPanel.js b/src/components/structures/GroupFilterPanel.js
index 96aa1ba728..976b2d81a5 100644
--- a/src/components/structures/GroupFilterPanel.js
+++ b/src/components/structures/GroupFilterPanel.js
@@ -30,7 +30,9 @@ import MatrixClientContext from "../../contexts/MatrixClientContext";
 import AutoHideScrollbar from "./AutoHideScrollbar";
 import SettingsStore from "../../settings/SettingsStore";
 import UserTagTile from "../views/elements/UserTagTile";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
+@replaceableComponent("structures.GroupFilterPanel")
 class GroupFilterPanel extends React.Component {
     static contextType = MatrixClientContext;
 
diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js
index bbc4187298..b4b871a0b4 100644
--- a/src/components/structures/GroupView.js
+++ b/src/components/structures/GroupView.js
@@ -39,6 +39,7 @@ import {Group} from "matrix-js-sdk";
 import {allSettled, sleep} from "../../utils/promise";
 import RightPanelStore from "../../stores/RightPanelStore";
 import AutoHideScrollbar from "./AutoHideScrollbar";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 const LONG_DESC_PLACEHOLDER = _td(
 `<h1>HTML for your community's page</h1>
@@ -391,6 +392,7 @@ class FeaturedUser extends React.Component {
 const GROUP_JOINPOLICY_OPEN = "open";
 const GROUP_JOINPOLICY_INVITE = "invite";
 
+@replaceableComponent("structures.GroupView")
 export default class GroupView extends React.Component {
     static propTypes = {
         groupId: PropTypes.string.isRequired,
diff --git a/src/components/structures/HostSignupAction.tsx b/src/components/structures/HostSignupAction.tsx
index 9cf84a9379..769775d549 100644
--- a/src/components/structures/HostSignupAction.tsx
+++ b/src/components/structures/HostSignupAction.tsx
@@ -22,11 +22,13 @@ import {
 import { _t } from "../../languageHandler";
 import { HostSignupStore } from "../../stores/HostSignupStore";
 import SdkConfig from "../../SdkConfig";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 interface IProps {}
 
 interface IState {}
 
+@replaceableComponent("structures.HostSignupAction")
 export default class HostSignupAction extends React.PureComponent<IProps, IState> {
     private openDialog = async () => {
         await HostSignupStore.instance.setHostSignupActive(true);
diff --git a/src/components/structures/IndicatorScrollbar.js b/src/components/structures/IndicatorScrollbar.js
index cd5510de9d..341ab2df71 100644
--- a/src/components/structures/IndicatorScrollbar.js
+++ b/src/components/structures/IndicatorScrollbar.js
@@ -17,7 +17,9 @@ limitations under the License.
 import React from "react";
 import PropTypes from "prop-types";
 import AutoHideScrollbar from "./AutoHideScrollbar";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
+@replaceableComponent("structures.IndicatorScrollbar")
 export default class IndicatorScrollbar extends React.Component {
     static propTypes = {
         // If true, the scrollbar will append mx_IndicatorScrollbar_leftOverflowIndicator
diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js
index ac7049ed88..9b61f71fd7 100644
--- a/src/components/structures/InteractiveAuth.js
+++ b/src/components/structures/InteractiveAuth.js
@@ -22,9 +22,11 @@ import PropTypes from 'prop-types';
 import getEntryComponentForLoginType from '../views/auth/InteractiveAuthEntryComponents';
 
 import * as sdk from '../../index';
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 export const ERROR_USER_CANCELLED = new Error("User cancelled auth session");
 
+@replaceableComponent("structures.InteractiveAuthComponent")
 export default class InteractiveAuthComponent extends React.Component {
     static propTypes = {
         // matrix client to use for UI auth requests
diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx
index 82dd9443cc..88c7a71b35 100644
--- a/src/components/structures/LeftPanel.tsx
+++ b/src/components/structures/LeftPanel.tsx
@@ -40,6 +40,7 @@ import { MatrixClientPeg } from "../../MatrixClientPeg";
 import RoomListNumResults from "../views/rooms/RoomListNumResults";
 import LeftPanelWidget from "./LeftPanelWidget";
 import SpacePanel from "../views/spaces/SpacePanel";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 interface IProps {
     isMinimized: boolean;
@@ -60,6 +61,7 @@ const cssClasses = [
     "mx_RoomSublist_showNButton",
 ];
 
+@replaceableComponent("structures.LeftPanel")
 export default class LeftPanel extends React.Component<IProps, IState> {
     private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
     private groupFilterPanelWatcherRef: string;
diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx
index 3e6d56fd54..15e90a383a 100644
--- a/src/components/structures/LoggedInView.tsx
+++ b/src/components/structures/LoggedInView.tsx
@@ -56,6 +56,7 @@ import Modal from "../../Modal";
 import { ICollapseConfig } from "../../resizer/distributors/collapse";
 import HostSignupContainer from '../views/host_signup/HostSignupContainer';
 import { IOpts } from "../../createRoom";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 // We need to fetch each pinned message individually (if we don't already have it)
 // so each pinned message may trigger a request. Limit the number per room for sanity.
@@ -128,6 +129,7 @@ interface IState {
  *
  * Components mounted below us can access the matrix client via the react context.
  */
+@replaceableComponent("structures.LoggedInView")
 class LoggedInView extends React.Component<IProps, IState> {
     static displayName = 'LoggedInView';
 
diff --git a/src/components/structures/MainSplit.js b/src/components/structures/MainSplit.js
index 47dfe83ad6..5818d303fc 100644
--- a/src/components/structures/MainSplit.js
+++ b/src/components/structures/MainSplit.js
@@ -17,7 +17,9 @@ limitations under the License.
 
 import React from 'react';
 import { Resizable } from 're-resizable';
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
+@replaceableComponent("structures.MainSplit")
 export default class MainSplit extends React.Component {
     _onResizeStart = () => {
         this.props.resizeNotifier.startResizing();
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index 1700b627db..0272633e8f 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -84,6 +84,7 @@ import DialPadModal from "../views/voip/DialPadModal";
 import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast';
 import SpaceStore from "../../stores/SpaceStore";
 import SpaceRoomDirectory from "./SpaceRoomDirectory";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 /** constants for MatrixChat.state.view */
 export enum Views {
@@ -208,6 +209,7 @@ interface IState {
     roomJustCreatedOpts?: IOpts;
 }
 
+@replaceableComponent("structures.MatrixChat")
 export default class MatrixChat extends React.PureComponent<IProps, IState> {
     static displayName = "MatrixChat";
 
diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index 9deda54bee..0f9ef70ec1 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -34,6 +34,7 @@ import {textForEvent} from "../../TextForEvent";
 import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer";
 import DMRoomMap from "../../utils/DMRoomMap";
 import NewRoomIntro from "../views/rooms/NewRoomIntro";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
 const continuedTypes = ['m.sticker', 'm.room.message'];
@@ -66,6 +67,7 @@ const isMembershipChange = (e) => e.getType() === 'm.room.member' || e.getType()
 
 /* (almost) stateless UI component which builds the event tiles in the room timeline.
  */
+@replaceableComponent("structures.MessagePanel")
 export default class MessagePanel extends React.Component {
     static propTypes = {
         // true to give the component a 'display: none' style.
diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js
index e0551eecdb..2ab11dad25 100644
--- a/src/components/structures/MyGroups.js
+++ b/src/components/structures/MyGroups.js
@@ -24,7 +24,9 @@ import dis from '../../dispatcher/dispatcher';
 import AccessibleButton from '../views/elements/AccessibleButton';
 import MatrixClientContext from "../../contexts/MatrixClientContext";
 import AutoHideScrollbar from "./AutoHideScrollbar";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
+@replaceableComponent("structures.MyGroups")
 export default class MyGroups extends React.Component {
     static contextType = MatrixClientContext;
 
diff --git a/src/components/structures/NonUrgentToastContainer.tsx b/src/components/structures/NonUrgentToastContainer.tsx
index 8d415df4dd..7c193ec9d7 100644
--- a/src/components/structures/NonUrgentToastContainer.tsx
+++ b/src/components/structures/NonUrgentToastContainer.tsx
@@ -18,6 +18,7 @@ import * as React from "react";
 import { ComponentClass } from "../../@types/common";
 import NonUrgentToastStore from "../../stores/NonUrgentToastStore";
 import { UPDATE_EVENT } from "../../stores/AsyncStore";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 interface IProps {
 }
@@ -26,6 +27,7 @@ interface IState {
     toasts: ComponentClass[],
 }
 
+@replaceableComponent("structures.NonUrgentToastContainer")
 export default class NonUrgentToastContainer extends React.PureComponent<IProps, IState> {
     public constructor(props, context) {
         super(props, context);
diff --git a/src/components/structures/NotificationPanel.js b/src/components/structures/NotificationPanel.js
index b4eb6c187b..41aafc8b13 100644
--- a/src/components/structures/NotificationPanel.js
+++ b/src/components/structures/NotificationPanel.js
@@ -23,10 +23,12 @@ import { _t } from '../../languageHandler';
 import {MatrixClientPeg} from "../../MatrixClientPeg";
 import * as sdk from "../../index";
 import BaseCard from "../views/right_panel/BaseCard";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 /*
  * Component which shows the global notification list using a TimelinePanel
  */
+@replaceableComponent("structures.NotificationPanel")
 class NotificationPanel extends React.Component {
     static propTypes = {
         onClose: PropTypes.func.isRequired,
diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js
index 3d9df2e927..5bcb3b2450 100644
--- a/src/components/structures/RightPanel.js
+++ b/src/components/structures/RightPanel.js
@@ -34,7 +34,9 @@ import MatrixClientContext from "../../contexts/MatrixClientContext";
 import {Action} from "../../dispatcher/actions";
 import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
 import WidgetCard from "../views/right_panel/WidgetCard";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
+@replaceableComponent("structures.RightPanel")
 export default class RightPanel extends React.Component {
     static get propTypes() {
         return {
diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js
index 7387e1aac0..363c67262b 100644
--- a/src/components/structures/RoomDirectory.js
+++ b/src/components/structures/RoomDirectory.js
@@ -34,6 +34,7 @@ import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore";
 import GroupStore from "../../stores/GroupStore";
 import FlairStore from "../../stores/FlairStore";
 import CountlyAnalytics from "../../CountlyAnalytics";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 const MAX_NAME_LENGTH = 80;
 const MAX_TOPIC_LENGTH = 800;
@@ -42,6 +43,7 @@ function track(action) {
     Analytics.trackEvent('RoomDirectory', action);
 }
 
+@replaceableComponent("structures.RoomDirectory")
 export default class RoomDirectory extends React.Component {
     static propTypes = {
         initialText: PropTypes.string,
diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx
index a64e40bc65..fda09f9774 100644
--- a/src/components/structures/RoomSearch.tsx
+++ b/src/components/structures/RoomSearch.tsx
@@ -25,6 +25,7 @@ import AccessibleButton from "../views/elements/AccessibleButton";
 import { Action } from "../../dispatcher/actions";
 import RoomListStore from "../../stores/room-list/RoomListStore";
 import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 interface IProps {
     isMinimized: boolean;
@@ -37,6 +38,7 @@ interface IState {
     focused: boolean;
 }
 
+@replaceableComponent("structures.RoomSearch")
 export default class RoomSearch extends React.PureComponent<IProps, IState> {
     private dispatcherRef: string;
     private inputRef: React.RefObject<HTMLInputElement> = createRef();
diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js
index aa4bceba74..8b70998be0 100644
--- a/src/components/structures/RoomStatusBar.js
+++ b/src/components/structures/RoomStatusBar.js
@@ -23,6 +23,7 @@ import Resend from '../../Resend';
 import dis from '../../dispatcher/dispatcher';
 import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils';
 import {Action} from "../../dispatcher/actions";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 const STATUS_BAR_HIDDEN = 0;
 const STATUS_BAR_EXPANDED = 1;
@@ -35,6 +36,7 @@ function getUnsentMessages(room) {
     });
 }
 
+@replaceableComponent("structures.RoomStatusBar")
 export default class RoomStatusBar extends React.Component {
     static propTypes = {
         // the room this statusbar is representing.
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 90f6daf6cb..b57638413b 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -82,6 +82,7 @@ import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutS
 import { objectHasDiff } from "../../utils/objects";
 import SpaceRoomView from "./SpaceRoomView";
 import { IOpts } from "../../createRoom";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 const DEBUG = false;
 let debuglog = function(msg: string) {};
@@ -195,6 +196,7 @@ export interface IState {
     dragCounter: number;
 }
 
+@replaceableComponent("structures.RoomView")
 export default class RoomView extends React.Component<IProps, IState> {
     private readonly dispatcherRef: string;
     private readonly roomStoreToken: EventSubscription;
diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js
index 744400df3c..3a9b2b8a77 100644
--- a/src/components/structures/ScrollPanel.js
+++ b/src/components/structures/ScrollPanel.js
@@ -19,6 +19,7 @@ import PropTypes from 'prop-types';
 import { Key } from '../../Keyboard';
 import Timer from '../../utils/Timer';
 import AutoHideScrollbar from "./AutoHideScrollbar";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 const DEBUG_SCROLL = false;
 
@@ -83,6 +84,7 @@ if (DEBUG_SCROLL) {
  * offset as normal.
  */
 
+@replaceableComponent("structures.ScrollPanel")
 export default class ScrollPanel extends React.Component {
     static propTypes = {
         /* stickyBottom: if set to true, then once the user hits the bottom of
diff --git a/src/components/structures/SearchBox.js b/src/components/structures/SearchBox.js
index c1e3ad0cf2..6daa8526bc 100644
--- a/src/components/structures/SearchBox.js
+++ b/src/components/structures/SearchBox.js
@@ -22,7 +22,9 @@ import dis from '../../dispatcher/dispatcher';
 import {throttle} from 'lodash';
 import AccessibleButton from '../../components/views/elements/AccessibleButton';
 import classNames from 'classnames';
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
+@replaceableComponent("structures.SearchBox")
 export default class SearchBox extends React.Component {
     static propTypes = {
         onSearch: PropTypes.func,
diff --git a/src/components/structures/TabbedView.tsx b/src/components/structures/TabbedView.tsx
index 21f9f3f5d6..0097d55cf5 100644
--- a/src/components/structures/TabbedView.tsx
+++ b/src/components/structures/TabbedView.tsx
@@ -20,6 +20,7 @@ import * as React from "react";
 import {_t} from '../../languageHandler';
 import * as sdk from "../../index";
 import AutoHideScrollbar from './AutoHideScrollbar';
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 /**
  * Represents a tab for the TabbedView.
@@ -45,6 +46,7 @@ interface IState {
     activeTabIndex: number;
 }
 
+@replaceableComponent("structures.TabbedView")
 export default class TabbedView extends React.Component<IProps, IState> {
     constructor(props: IProps) {
         super(props);
diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js
index 6bc1f70ba1..f32b8ed0a9 100644
--- a/src/components/structures/TimelinePanel.js
+++ b/src/components/structures/TimelinePanel.js
@@ -37,6 +37,7 @@ import EditorStateTransfer from '../../utils/EditorStateTransfer';
 import {haveTileForEvent} from "../views/rooms/EventTile";
 import {UIFeature} from "../../settings/UIFeature";
 import {objectHasDiff} from "../../utils/objects";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 const PAGINATE_SIZE = 20;
 const INITIAL_SIZE = 20;
@@ -55,6 +56,7 @@ if (DEBUG) {
  *
  * Also responsible for handling and sending read receipts.
  */
+@replaceableComponent("structures.TimelinePanel")
 class TimelinePanel extends React.Component {
     static propTypes = {
         // The js-sdk EventTimelineSet object for the timeline sequence we are
diff --git a/src/components/structures/ToastContainer.tsx b/src/components/structures/ToastContainer.tsx
index 513cca82c3..1fd3e3419f 100644
--- a/src/components/structures/ToastContainer.tsx
+++ b/src/components/structures/ToastContainer.tsx
@@ -17,12 +17,14 @@ limitations under the License.
 import * as React from "react";
 import ToastStore, {IToast} from "../../stores/ToastStore";
 import classNames from "classnames";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 interface IState {
     toasts: IToast<any>[];
     countSeen: number;
 }
 
+@replaceableComponent("structures.ToastContainer")
 export default class ToastContainer extends React.Component<{}, IState> {
     constructor(props, context) {
         super(props, context);
diff --git a/src/components/structures/UploadBar.tsx b/src/components/structures/UploadBar.tsx
index b9d157ee00..4a1fd4313d 100644
--- a/src/components/structures/UploadBar.tsx
+++ b/src/components/structures/UploadBar.tsx
@@ -25,6 +25,7 @@ import { Action } from "../../dispatcher/actions";
 import ProgressBar from "../views/elements/ProgressBar";
 import AccessibleButton from "../views/elements/AccessibleButton";
 import { IUpload } from "../../models/IUpload";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 interface IProps {
     room: Room;
@@ -35,6 +36,7 @@ interface IState {
     uploadsHere: IUpload[];
 }
 
+@replaceableComponent("structures.UploadBar")
 export default class UploadBar extends React.Component<IProps, IState> {
     private dispatcherRef: string;
     private mounted: boolean;
diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx
index b31a5f4b8e..0543cc4d07 100644
--- a/src/components/structures/UserMenu.tsx
+++ b/src/components/structures/UserMenu.tsx
@@ -56,6 +56,7 @@ import HostSignupAction from "./HostSignupAction";
 import { IHostSignupConfig } from "../views/dialogs/HostSignupDialogTypes";
 import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore";
 import RoomName from "../views/elements/RoomName";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 interface IProps {
     isMinimized: boolean;
@@ -69,6 +70,7 @@ interface IState {
     selectedSpace?: Room;
 }
 
+@replaceableComponent("structures.UserMenu")
 export default class UserMenu extends React.Component<IProps, IState> {
     private dispatcherRef: string;
     private themeWatcherRef: string;
diff --git a/src/components/structures/UserView.js b/src/components/structures/UserView.js
index 8e21771bb9..dc05193ece 100644
--- a/src/components/structures/UserView.js
+++ b/src/components/structures/UserView.js
@@ -23,7 +23,9 @@ import * as sdk from "../../index";
 import Modal from '../../Modal';
 import { _t } from '../../languageHandler';
 import HomePage from "./HomePage";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
+@replaceableComponent("structures.UserView")
 export default class UserView extends React.Component {
     static get propTypes() {
         return {
diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js
index ca6c0d4226..b762ad3fca 100644
--- a/src/components/structures/ViewSource.js
+++ b/src/components/structures/ViewSource.js
@@ -21,8 +21,10 @@ import PropTypes from 'prop-types';
 import SyntaxHighlight from '../views/elements/SyntaxHighlight';
 import {_t} from "../../languageHandler";
 import * as sdk from "../../index";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 
+@replaceableComponent("structures.ViewSource")
 export default class ViewSource extends React.Component {
     static propTypes = {
         content: PropTypes.object.isRequired,
diff --git a/src/components/structures/auth/CompleteSecurity.js b/src/components/structures/auth/CompleteSecurity.js
index c73691611d..eee5667052 100644
--- a/src/components/structures/auth/CompleteSecurity.js
+++ b/src/components/structures/auth/CompleteSecurity.js
@@ -26,7 +26,9 @@ import {
     PHASE_CONFIRM_SKIP,
 } from '../../../stores/SetupEncryptionStore';
 import SetupEncryptionBody from "./SetupEncryptionBody";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("structures.auth.CompleteSecurity")
 export default class CompleteSecurity extends React.Component {
     static propTypes = {
         onFinished: PropTypes.func.isRequired,
diff --git a/src/components/structures/auth/E2eSetup.js b/src/components/structures/auth/E2eSetup.js
index d97a972718..4e51ae828c 100644
--- a/src/components/structures/auth/E2eSetup.js
+++ b/src/components/structures/auth/E2eSetup.js
@@ -19,7 +19,9 @@ import PropTypes from 'prop-types';
 import AuthPage from '../../views/auth/AuthPage';
 import CompleteSecurityBody from '../../views/auth/CompleteSecurityBody';
 import CreateCrossSigningDialog from '../../views/dialogs/security/CreateCrossSigningDialog';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("structures.auth.E2eSetup")
 export default class E2eSetup extends React.Component {
     static propTypes = {
         onFinished: PropTypes.func.isRequired,
diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js
index 5a39fe9fd9..31a5de0222 100644
--- a/src/components/structures/auth/ForgotPassword.js
+++ b/src/components/structures/auth/ForgotPassword.js
@@ -27,6 +27,7 @@ import classNames from 'classnames';
 import AuthPage from "../../views/auth/AuthPage";
 import CountlyAnalytics from "../../../CountlyAnalytics";
 import ServerPicker from "../../views/elements/ServerPicker";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // Phases
 // Show the forgot password inputs
@@ -38,6 +39,7 @@ const PHASE_EMAIL_SENT = 3;
 // User has clicked the link in email and completed reset
 const PHASE_DONE = 4;
 
+@replaceableComponent("structures.auth.ForgotPassword")
 export default class ForgotPassword extends React.Component {
     static propTypes = {
         serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx
index 96fc39a437..3ab73fb9ac 100644
--- a/src/components/structures/auth/Login.tsx
+++ b/src/components/structures/auth/Login.tsx
@@ -35,6 +35,7 @@ import InlineSpinner from "../../views/elements/InlineSpinner";
 import Spinner from "../../views/elements/Spinner";
 import SSOButtons from "../../views/elements/SSOButtons";
 import ServerPicker from "../../views/elements/ServerPicker";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // These are used in several places, and come from the js-sdk's autodiscovery
 // stuff. We define them here so that they'll be picked up by i18n.
@@ -99,6 +100,7 @@ interface IState {
 /*
  * A wire component which glues together login UI components and Login logic
  */
+@replaceableComponent("structures.auth.LoginComponent")
 export default class LoginComponent extends React.PureComponent<IProps, IState> {
     private unmounted = false;
     private loginLogic: Login;
diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx
index f9d338902c..32bdddb82a 100644
--- a/src/components/structures/auth/Registration.tsx
+++ b/src/components/structures/auth/Registration.tsx
@@ -30,6 +30,7 @@ import Login, {ISSOFlow} from "../../../Login";
 import dis from "../../../dispatcher/dispatcher";
 import SSOButtons from "../../views/elements/SSOButtons";
 import ServerPicker from '../../views/elements/ServerPicker';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     serverConfig: ValidatedServerConfig;
@@ -109,6 +110,7 @@ interface IState {
     ssoFlow?: ISSOFlow;
 }
 
+@replaceableComponent("structures.auth.Registration")
 export default class Registration extends React.Component<IProps, IState> {
     loginLogic: Login;
 
diff --git a/src/components/structures/auth/SetupEncryptionBody.js b/src/components/structures/auth/SetupEncryptionBody.js
index 3e7264dfec..f66c434edd 100644
--- a/src/components/structures/auth/SetupEncryptionBody.js
+++ b/src/components/structures/auth/SetupEncryptionBody.js
@@ -28,6 +28,7 @@ import {
     PHASE_CONFIRM_SKIP,
     PHASE_FINISHED,
 } from '../../../stores/SetupEncryptionStore';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 function keyHasPassphrase(keyInfo) {
     return (
@@ -37,6 +38,7 @@ function keyHasPassphrase(keyInfo) {
     );
 }
 
+@replaceableComponent("structures.auth.SetupEncryptionBody")
 export default class SetupEncryptionBody extends React.Component {
     static propTypes = {
         onFinished: PropTypes.func.isRequired,
diff --git a/src/components/structures/auth/SoftLogout.js b/src/components/structures/auth/SoftLogout.js
index a7fe340457..08db3b2efe 100644
--- a/src/components/structures/auth/SoftLogout.js
+++ b/src/components/structures/auth/SoftLogout.js
@@ -26,6 +26,7 @@ import {sendLoginRequest} from "../../../Login";
 import AuthPage from "../../views/auth/AuthPage";
 import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "../../../BasePlatform";
 import SSOButtons from "../../views/elements/SSOButtons";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const LOGIN_VIEW = {
     LOADING: 1,
@@ -41,6 +42,7 @@ const FLOWS_TO_VIEWS = {
     "m.login.sso": LOGIN_VIEW.SSO,
 };
 
+@replaceableComponent("structures.auth.SoftLogout")
 export default class SoftLogout extends React.Component {
     static propTypes = {
         // Query parameters from MatrixChat
diff --git a/src/components/views/auth/AuthBody.js b/src/components/views/auth/AuthBody.js
index 7881486a5f..2cb72b5e1d 100644
--- a/src/components/views/auth/AuthBody.js
+++ b/src/components/views/auth/AuthBody.js
@@ -15,7 +15,9 @@ limitations under the License.
 */
 
 import React from 'react';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.auth.AuthBody")
 export default class AuthBody extends React.PureComponent {
     render() {
         return <div className="mx_AuthBody">
diff --git a/src/components/views/auth/AuthFooter.js b/src/components/views/auth/AuthFooter.js
index 3de5a19350..f167e16283 100644
--- a/src/components/views/auth/AuthFooter.js
+++ b/src/components/views/auth/AuthFooter.js
@@ -18,7 +18,9 @@ limitations under the License.
 
 import { _t } from '../../../languageHandler';
 import React from 'react';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.auth.AuthFooter")
 export default class AuthFooter extends React.Component {
     render() {
         return (
diff --git a/src/components/views/auth/AuthHeader.js b/src/components/views/auth/AuthHeader.js
index 57499e397c..323299b3a8 100644
--- a/src/components/views/auth/AuthHeader.js
+++ b/src/components/views/auth/AuthHeader.js
@@ -18,7 +18,9 @@ limitations under the License.
 import React from 'react';
 import PropTypes from 'prop-types';
 import * as sdk from '../../../index';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.auth.AuthHeader")
 export default class AuthHeader extends React.Component {
     static propTypes = {
         disableLanguageSelector: PropTypes.bool,
diff --git a/src/components/views/auth/AuthHeaderLogo.js b/src/components/views/auth/AuthHeaderLogo.js
index 2f27885322..b4e04799bb 100644
--- a/src/components/views/auth/AuthHeaderLogo.js
+++ b/src/components/views/auth/AuthHeaderLogo.js
@@ -15,7 +15,9 @@ limitations under the License.
 */
 
 import React from 'react';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.auth.AuthHeaderLogo")
 export default class AuthHeaderLogo extends React.PureComponent {
     render() {
         return <div className="mx_AuthHeaderLogo">
diff --git a/src/components/views/auth/CaptchaForm.js b/src/components/views/auth/CaptchaForm.js
index e2d7d594fa..50de24d403 100644
--- a/src/components/views/auth/CaptchaForm.js
+++ b/src/components/views/auth/CaptchaForm.js
@@ -18,12 +18,14 @@ import React, {createRef} from 'react';
 import PropTypes from 'prop-types';
 import { _t } from '../../../languageHandler';
 import CountlyAnalytics from "../../../CountlyAnalytics";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const DIV_ID = 'mx_recaptcha';
 
 /**
  * A pure UI component which displays a captcha form.
  */
+@replaceableComponent("views.auth.CaptchaForm")
 export default class CaptchaForm extends React.Component {
     static propTypes = {
         sitePublicKey: PropTypes.string,
diff --git a/src/components/views/auth/CompleteSecurityBody.js b/src/components/views/auth/CompleteSecurityBody.js
index 734af3192c..6647bb1200 100644
--- a/src/components/views/auth/CompleteSecurityBody.js
+++ b/src/components/views/auth/CompleteSecurityBody.js
@@ -15,7 +15,9 @@ limitations under the License.
 */
 
 import React from 'react';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.auth.CompleteSecurityBody")
 export default class CompleteSecurityBody extends React.PureComponent {
     render() {
         return <div className="mx_CompleteSecurityBody">
diff --git a/src/components/views/auth/CountryDropdown.js b/src/components/views/auth/CountryDropdown.js
index 3296b574a4..e21f112865 100644
--- a/src/components/views/auth/CountryDropdown.js
+++ b/src/components/views/auth/CountryDropdown.js
@@ -22,6 +22,7 @@ import * as sdk from '../../../index';
 import {COUNTRIES, getEmojiFlag} from '../../../phonenumber';
 import SdkConfig from "../../../SdkConfig";
 import { _t } from "../../../languageHandler";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const COUNTRIES_BY_ISO2 = {};
 for (const c of COUNTRIES) {
@@ -40,6 +41,7 @@ function countryMatchesSearchQuery(query, country) {
     return false;
 }
 
+@replaceableComponent("views.auth.CountryDropdown")
 export default class CountryDropdown extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.js
index 7dc1976641..6cbecd22ee 100644
--- a/src/components/views/auth/InteractiveAuthEntryComponents.js
+++ b/src/components/views/auth/InteractiveAuthEntryComponents.js
@@ -26,6 +26,7 @@ import SettingsStore from "../../../settings/SettingsStore";
 import AccessibleButton from "../elements/AccessibleButton";
 import Spinner from "../elements/Spinner";
 import CountlyAnalytics from "../../../CountlyAnalytics";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 /* This file contains a collection of components which are used by the
  * InteractiveAuth to prompt the user to enter the information needed
@@ -75,6 +76,7 @@ import CountlyAnalytics from "../../../CountlyAnalytics";
 
 export const DEFAULT_PHASE = 0;
 
+@replaceableComponent("views.auth.PasswordAuthEntry")
 export class PasswordAuthEntry extends React.Component {
     static LOGIN_TYPE = "m.login.password";
 
@@ -173,6 +175,7 @@ export class PasswordAuthEntry extends React.Component {
     }
 }
 
+@replaceableComponent("views.auth.RecaptchaAuthEntry")
 export class RecaptchaAuthEntry extends React.Component {
     static LOGIN_TYPE = "m.login.recaptcha";
 
@@ -235,6 +238,7 @@ export class RecaptchaAuthEntry extends React.Component {
     }
 }
 
+@replaceableComponent("views.auth.TermsAuthEntry")
 export class TermsAuthEntry extends React.Component {
     static LOGIN_TYPE = "m.login.terms";
 
@@ -385,6 +389,7 @@ export class TermsAuthEntry extends React.Component {
     }
 }
 
+@replaceableComponent("views.auth.EmailIdentityAuthEntry")
 export class EmailIdentityAuthEntry extends React.Component {
     static LOGIN_TYPE = "m.login.email.identity";
 
@@ -432,6 +437,7 @@ export class EmailIdentityAuthEntry extends React.Component {
     }
 }
 
+@replaceableComponent("views.auth.MsisdnAuthEntry")
 export class MsisdnAuthEntry extends React.Component {
     static LOGIN_TYPE = "m.login.msisdn";
 
@@ -578,6 +584,7 @@ export class MsisdnAuthEntry extends React.Component {
     }
 }
 
+@replaceableComponent("views.auth.SSOAuthEntry")
 export class SSOAuthEntry extends React.Component {
     static propTypes = {
         matrixClient: PropTypes.object.isRequired,
@@ -708,6 +715,7 @@ export class SSOAuthEntry extends React.Component {
     }
 }
 
+@replaceableComponent("views.auth.FallbackAuthEntry")
 export class FallbackAuthEntry extends React.Component {
     static propTypes = {
         matrixClient: PropTypes.object.isRequired,
diff --git a/src/components/views/auth/PassphraseField.tsx b/src/components/views/auth/PassphraseField.tsx
index e240ad61ca..274c244b2a 100644
--- a/src/components/views/auth/PassphraseField.tsx
+++ b/src/components/views/auth/PassphraseField.tsx
@@ -22,6 +22,7 @@ import SdkConfig from "../../../SdkConfig";
 import withValidation, {IFieldState, IValidationResult} from "../elements/Validation";
 import {_t, _td} from "../../../languageHandler";
 import Field, {IInputProps} from "../elements/Field";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps extends Omit<IInputProps, "onValidate"> {
     autoFocus?: boolean;
@@ -40,6 +41,7 @@ interface IProps extends Omit<IInputProps, "onValidate"> {
     onValidate(result: IValidationResult);
 }
 
+@replaceableComponent("views.auth.PassphraseField")
 class PassphraseField extends PureComponent<IProps> {
     static defaultProps = {
         label: _td("Password"),
diff --git a/src/components/views/auth/PasswordLogin.tsx b/src/components/views/auth/PasswordLogin.tsx
index b2a3d62f55..2a42804a61 100644
--- a/src/components/views/auth/PasswordLogin.tsx
+++ b/src/components/views/auth/PasswordLogin.tsx
@@ -26,6 +26,7 @@ import withValidation from "../elements/Validation";
 import * as Email from "../../../email";
 import Field from "../elements/Field";
 import CountryDropdown from "./CountryDropdown";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // For validating phone numbers without country codes
 const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
@@ -66,6 +67,7 @@ enum LoginField {
  * A pure UI component which displays a username/password form.
  * The email/username/phone fields are fully-controlled, the password field is not.
  */
+@replaceableComponent("views.auth.PasswordLogin")
 export default class PasswordLogin extends React.PureComponent<IProps, IState> {
     static defaultProps = {
         onUsernameChanged: function() {},
diff --git a/src/components/views/auth/RegistrationForm.tsx b/src/components/views/auth/RegistrationForm.tsx
index e42ed88f99..85e0933be9 100644
--- a/src/components/views/auth/RegistrationForm.tsx
+++ b/src/components/views/auth/RegistrationForm.tsx
@@ -30,6 +30,7 @@ import PassphraseField from "./PassphraseField";
 import CountlyAnalytics from "../../../CountlyAnalytics";
 import Field from '../elements/Field';
 import RegistrationEmailPromptDialog from '../dialogs/RegistrationEmailPromptDialog';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 enum RegistrationField {
     Email = "field_email",
@@ -80,6 +81,7 @@ interface IState {
 /*
  * A pure UI component which displays a registration form.
  */
+@replaceableComponent("views.auth.RegistrationForm")
 export default class RegistrationForm extends React.PureComponent<IProps, IState> {
     static defaultProps = {
         onValidationChange: console.error,
diff --git a/src/components/views/auth/Welcome.js b/src/components/views/auth/Welcome.js
index 0205f4e0b9..fca66fcf9b 100644
--- a/src/components/views/auth/Welcome.js
+++ b/src/components/views/auth/Welcome.js
@@ -24,10 +24,12 @@ import {_td} from "../../../languageHandler";
 import SettingsStore from "../../../settings/SettingsStore";
 import {UIFeature} from "../../../settings/UIFeature";
 import CountlyAnalytics from "../../../CountlyAnalytics";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // translatable strings for Welcome pages
 _td("Sign in with SSO");
 
+@replaceableComponent("views.auth.Welcome")
 export default class Welcome extends React.PureComponent {
     constructor(props) {
         super(props);
diff --git a/src/components/views/avatars/DecoratedRoomAvatar.tsx b/src/components/views/avatars/DecoratedRoomAvatar.tsx
index d7e012467b..e95022687a 100644
--- a/src/components/views/avatars/DecoratedRoomAvatar.tsx
+++ b/src/components/views/avatars/DecoratedRoomAvatar.tsx
@@ -30,6 +30,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import {_t} from "../../../languageHandler";
 import TextWithTooltip from "../elements/TextWithTooltip";
 import DMRoomMap from "../../../utils/DMRoomMap";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     room: Room;
@@ -68,6 +69,7 @@ function tooltipText(variant: Icon) {
     }
 }
 
+@replaceableComponent("views.avatars.DecoratedRoomAvatar")
 export default class DecoratedRoomAvatar extends React.PureComponent<IProps, IState> {
     private _dmUser: User;
     private isUnmounted = false;
diff --git a/src/components/views/avatars/GroupAvatar.tsx b/src/components/views/avatars/GroupAvatar.tsx
index 51327605c0..a033257871 100644
--- a/src/components/views/avatars/GroupAvatar.tsx
+++ b/src/components/views/avatars/GroupAvatar.tsx
@@ -17,6 +17,7 @@ limitations under the License.
 import React from 'react';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import BaseAvatar from './BaseAvatar';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 export interface IProps {
         groupId?: string;
@@ -28,6 +29,7 @@ export interface IProps {
         onClick?: React.MouseEventHandler;
 }
 
+@replaceableComponent("views.avatars.GroupAvatar")
 export default class GroupAvatar extends React.Component<IProps> {
     public static defaultProps = {
         width: 36,
diff --git a/src/components/views/avatars/MemberAvatar.tsx b/src/components/views/avatars/MemberAvatar.tsx
index 60b043016b..641046aa55 100644
--- a/src/components/views/avatars/MemberAvatar.tsx
+++ b/src/components/views/avatars/MemberAvatar.tsx
@@ -22,6 +22,7 @@ import dis from "../../../dispatcher/dispatcher";
 import {Action} from "../../../dispatcher/actions";
 import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import BaseAvatar from "./BaseAvatar";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps extends Omit<React.ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url"> {
     member: RoomMember;
@@ -42,6 +43,7 @@ interface IState {
     imageUrl?: string;
 }
 
+@replaceableComponent("views.avatars.MemberAvatar")
 export default class MemberAvatar extends React.Component<IProps, IState> {
     public static defaultProps = {
         width: 40,
diff --git a/src/components/views/avatars/MemberStatusMessageAvatar.js b/src/components/views/avatars/MemberStatusMessageAvatar.js
index d5d927106c..acf190f17f 100644
--- a/src/components/views/avatars/MemberStatusMessageAvatar.js
+++ b/src/components/views/avatars/MemberStatusMessageAvatar.js
@@ -23,7 +23,9 @@ import classNames from 'classnames';
 import StatusMessageContextMenu from "../context_menus/StatusMessageContextMenu";
 import SettingsStore from "../../../settings/SettingsStore";
 import {ContextMenu, ContextMenuButton} from "../../structures/ContextMenu";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.avatars.MemberStatusMessageAvatar")
 export default class MemberStatusMessageAvatar extends React.Component {
     static propTypes = {
         member: PropTypes.object.isRequired,
diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx
index 952b9d4cb6..0a59f6e36a 100644
--- a/src/components/views/avatars/RoomAvatar.tsx
+++ b/src/components/views/avatars/RoomAvatar.tsx
@@ -23,6 +23,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import Modal from '../../../Modal';
 import * as Avatar from '../../../Avatar';
 import {ResizeMethod} from "../../../Avatar";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick"> {
     // Room may be left unset here, but if it is,
@@ -42,6 +43,7 @@ interface IState {
     urls: string[];
 }
 
+@replaceableComponent("views.avatars.RoomAvatar")
 export default class RoomAvatar extends React.Component<IProps, IState> {
     public static defaultProps = {
         width: 36,
diff --git a/src/components/views/context_menus/CallContextMenu.tsx b/src/components/views/context_menus/CallContextMenu.tsx
index 3557976326..97473059a6 100644
--- a/src/components/views/context_menus/CallContextMenu.tsx
+++ b/src/components/views/context_menus/CallContextMenu.tsx
@@ -22,11 +22,13 @@ import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
 import CallHandler from '../../../CallHandler';
 import InviteDialog, { KIND_CALL_TRANSFER } from '../dialogs/InviteDialog';
 import Modal from '../../../Modal';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps extends IContextMenuProps {
     call: MatrixCall;
 }
 
+@replaceableComponent("views.context_menus.CallContextMenu")
 export default class CallContextMenu extends React.Component<IProps> {
     static propTypes = {
         // js-sdk User object. Not required because it might not exist.
diff --git a/src/components/views/context_menus/DialpadContextMenu.tsx b/src/components/views/context_menus/DialpadContextMenu.tsx
index e3aed0179b..17abce0c61 100644
--- a/src/components/views/context_menus/DialpadContextMenu.tsx
+++ b/src/components/views/context_menus/DialpadContextMenu.tsx
@@ -19,6 +19,7 @@ import { _t } from '../../../languageHandler';
 import { ContextMenu, IProps as IContextMenuProps } from '../../structures/ContextMenu';
 import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
 import Dialpad from '../voip/DialPad';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps extends IContextMenuProps {
     call: MatrixCall;
@@ -28,6 +29,7 @@ interface IState {
     value: string;
 }
 
+@replaceableComponent("views.context_menus.DialpadContextMenu")
 export default class DialpadContextMenu extends React.Component<IProps, IState> {
     constructor(props) {
         super(props);
diff --git a/src/components/views/context_menus/GenericElementContextMenu.js b/src/components/views/context_menus/GenericElementContextMenu.js
index cea684b663..e04e3f7695 100644
--- a/src/components/views/context_menus/GenericElementContextMenu.js
+++ b/src/components/views/context_menus/GenericElementContextMenu.js
@@ -16,6 +16,7 @@ limitations under the License.
 
 import React from 'react';
 import PropTypes from 'prop-types';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 /*
  * This component can be used to display generic HTML content in a contextual
@@ -23,6 +24,7 @@ import PropTypes from 'prop-types';
  */
 
 
+@replaceableComponent("views.context_menus.GenericElementContextMenu")
 export default class GenericElementContextMenu extends React.Component {
     static propTypes = {
         element: PropTypes.element.isRequired,
diff --git a/src/components/views/context_menus/GenericTextContextMenu.js b/src/components/views/context_menus/GenericTextContextMenu.js
index 068f83be5f..3d3add006f 100644
--- a/src/components/views/context_menus/GenericTextContextMenu.js
+++ b/src/components/views/context_menus/GenericTextContextMenu.js
@@ -16,7 +16,9 @@ limitations under the License.
 
 import React from 'react';
 import PropTypes from 'prop-types';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.context_menus.GenericTextContextMenu")
 export default class GenericTextContextMenu extends React.Component {
     static propTypes = {
         message: PropTypes.string.isRequired,
diff --git a/src/components/views/context_menus/GroupInviteTileContextMenu.js b/src/components/views/context_menus/GroupInviteTileContextMenu.js
index 27ef76452f..11a9d90ac2 100644
--- a/src/components/views/context_menus/GroupInviteTileContextMenu.js
+++ b/src/components/views/context_menus/GroupInviteTileContextMenu.js
@@ -23,7 +23,9 @@ import Modal from '../../../Modal';
 import {Group} from 'matrix-js-sdk';
 import GroupStore from "../../../stores/GroupStore";
 import {MenuItem} from "../../structures/ContextMenu";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.context_menus.GroupInviteTileContextMenu")
 export default class GroupInviteTileContextMenu extends React.Component {
     static propTypes = {
         group: PropTypes.instanceOf(Group).isRequired,
diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js
index 98d0aad578..f97b429abc 100644
--- a/src/components/views/context_menus/MessageContextMenu.js
+++ b/src/components/views/context_menus/MessageContextMenu.js
@@ -32,11 +32,13 @@ import { isUrlPermitted } from '../../../HtmlUtils';
 import { isContentActionable } from '../../../utils/EventUtils';
 import {MenuItem} from "../../structures/ContextMenu";
 import {EventType} from "matrix-js-sdk/src/@types/event";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 function canCancel(eventStatus) {
     return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
 }
 
+@replaceableComponent("views.context_menus.MessageContextMenu")
 export default class MessageContextMenu extends React.Component {
     static propTypes = {
         /* the MatrixEvent associated with the context menu */
diff --git a/src/components/views/context_menus/StatusMessageContextMenu.js b/src/components/views/context_menus/StatusMessageContextMenu.js
index 5e6f06dd5d..41f0e0ba61 100644
--- a/src/components/views/context_menus/StatusMessageContextMenu.js
+++ b/src/components/views/context_menus/StatusMessageContextMenu.js
@@ -20,7 +20,9 @@ import { _t } from '../../../languageHandler';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import * as sdk from '../../../index';
 import AccessibleButton from '../elements/AccessibleButton';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.context_menus.StatusMessageContextMenu")
 export default class StatusMessageContextMenu extends React.Component {
     static propTypes = {
         // js-sdk User object. Not required because it might not exist.
diff --git a/src/components/views/context_menus/TagTileContextMenu.js b/src/components/views/context_menus/TagTileContextMenu.js
index 8d690483a8..8dea62690c 100644
--- a/src/components/views/context_menus/TagTileContextMenu.js
+++ b/src/components/views/context_menus/TagTileContextMenu.js
@@ -22,7 +22,9 @@ import dis from '../../../dispatcher/dispatcher';
 import TagOrderActions from '../../../actions/TagOrderActions';
 import {MenuItem} from "../../structures/ContextMenu";
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.context_menus.TagTileContextMenu")
 export default class TagTileContextMenu extends React.Component {
     static propTypes = {
         tag: PropTypes.string.isRequired,
diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js
index 2cd09874b2..929d688e47 100644
--- a/src/components/views/dialogs/AddressPickerDialog.js
+++ b/src/components/views/dialogs/AddressPickerDialog.js
@@ -33,6 +33,7 @@ import { abbreviateUrl } from '../../../utils/UrlUtils';
 import {sleep} from "../../../utils/promise";
 import {Key} from "../../../Keyboard";
 import {Action} from "../../../dispatcher/actions";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const TRUNCATE_QUERY_LIST = 40;
 const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
@@ -43,7 +44,7 @@ const addressTypeName = {
     'email': _td("email address"),
 };
 
-
+@replaceableComponent("views.dialogs.AddressPickerDialog")
 export default class AddressPickerDialog extends React.Component {
     static propTypes = {
         title: PropTypes.string.isRequired,
diff --git a/src/components/views/dialogs/AskInviteAnywayDialog.js b/src/components/views/dialogs/AskInviteAnywayDialog.js
index c69400977a..e6cd45ba6b 100644
--- a/src/components/views/dialogs/AskInviteAnywayDialog.js
+++ b/src/components/views/dialogs/AskInviteAnywayDialog.js
@@ -20,7 +20,9 @@ import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
 import SettingsStore from "../../../settings/SettingsStore";
 import {SettingLevel} from "../../../settings/SettingLevel";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.AskInviteAnywayDialog")
 export default class AskInviteAnywayDialog extends React.Component {
     static propTypes = {
         unknownProfileUsers: PropTypes.array.isRequired, // [ {userId, errorText}... ]
diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js
index 9ba5368ee5..0858e53e50 100644
--- a/src/components/views/dialogs/BaseDialog.js
+++ b/src/components/views/dialogs/BaseDialog.js
@@ -26,6 +26,7 @@ import AccessibleButton from '../elements/AccessibleButton';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import { _t } from "../../../languageHandler";
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 /*
  * Basic container for modal dialogs.
@@ -33,6 +34,7 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
  * Includes a div for the title, and a keypress handler which cancels the
  * dialog on escape.
  */
+@replaceableComponent("views.dialogs.BaseDialog")
 export default class BaseDialog extends React.Component {
     static propTypes = {
         // onFinished callback to call when Escape is pressed
diff --git a/src/components/views/dialogs/BugReportDialog.js b/src/components/views/dialogs/BugReportDialog.js
index c4dd0a1430..8948c14c7c 100644
--- a/src/components/views/dialogs/BugReportDialog.js
+++ b/src/components/views/dialogs/BugReportDialog.js
@@ -25,7 +25,9 @@ import Modal from '../../../Modal';
 import { _t } from '../../../languageHandler';
 import sendBugReport, {downloadBugReport} from '../../../rageshake/submit-rageshake';
 import AccessibleButton from "../elements/AccessibleButton";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.BugReportDialog")
 export default class BugReportDialog extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx b/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx
index 1c8a4ad6f6..d1080566ac 100644
--- a/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx
+++ b/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx
@@ -31,6 +31,7 @@ import {inviteMultipleToRoom, showAnyInviteErrors} from "../../../RoomInvite";
 import StyledCheckbox from "../elements/StyledCheckbox";
 import Modal from "../../../Modal";
 import ErrorDialog from "./ErrorDialog";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps extends IDialogProps {
     roomId: string;
@@ -52,6 +53,7 @@ interface IState {
     busy: boolean;
 }
 
+@replaceableComponent("views.dialogs.CommunityPrototypeInviteDialog")
 export default class CommunityPrototypeInviteDialog extends React.PureComponent<IProps, IState> {
     constructor(props: IProps) {
         super(props);
diff --git a/src/components/views/dialogs/ConfirmAndWaitRedactDialog.js b/src/components/views/dialogs/ConfirmAndWaitRedactDialog.js
index 0622dd7dfb..37d5510756 100644
--- a/src/components/views/dialogs/ConfirmAndWaitRedactDialog.js
+++ b/src/components/views/dialogs/ConfirmAndWaitRedactDialog.js
@@ -17,6 +17,7 @@ limitations under the License.
 import React from 'react';
 import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 /*
  * A dialog for confirming a redaction.
@@ -30,6 +31,7 @@ import { _t } from '../../../languageHandler';
  *
  * To avoid this, we keep the dialog open as long as /redact is in progress.
  */
+@replaceableComponent("views.dialogs.ConfirmAndWaitRedactDialog")
 export default class ConfirmAndWaitRedactDialog extends React.PureComponent {
     constructor(props) {
         super(props);
diff --git a/src/components/views/dialogs/ConfirmRedactDialog.js b/src/components/views/dialogs/ConfirmRedactDialog.js
index 2216f9a93a..bd63d3acc1 100644
--- a/src/components/views/dialogs/ConfirmRedactDialog.js
+++ b/src/components/views/dialogs/ConfirmRedactDialog.js
@@ -17,10 +17,12 @@ limitations under the License.
 import React from 'react';
 import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 /*
  * A dialog for confirming a redaction.
  */
+@replaceableComponent("views.dialogs.ConfirmRedactDialog")
 export default class ConfirmRedactDialog extends React.Component {
     render() {
         const TextInputDialog = sdk.getComponent('views.dialogs.TextInputDialog');
diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.js b/src/components/views/dialogs/ConfirmUserActionDialog.js
index 44f57f047e..8827f161f1 100644
--- a/src/components/views/dialogs/ConfirmUserActionDialog.js
+++ b/src/components/views/dialogs/ConfirmUserActionDialog.js
@@ -20,6 +20,7 @@ import { MatrixClient } from 'matrix-js-sdk';
 import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
 import { GroupMemberType } from '../../../groups';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 /*
  * A dialog for confirming an operation on another user.
@@ -29,6 +30,7 @@ import { GroupMemberType } from '../../../groups';
  * to make it obvious what is going to happen.
  * Also tweaks the style for 'dangerous' actions (albeit only with colour)
  */
+@replaceableComponent("views.dialogs.ConfirmUserActionDialog")
 export default class ConfirmUserActionDialog extends React.Component {
     static propTypes = {
         // matrix-js-sdk (room) member object. Supply either this or 'groupMember'
diff --git a/src/components/views/dialogs/ConfirmWipeDeviceDialog.js b/src/components/views/dialogs/ConfirmWipeDeviceDialog.js
index 41ef9131fa..4faaad0f7e 100644
--- a/src/components/views/dialogs/ConfirmWipeDeviceDialog.js
+++ b/src/components/views/dialogs/ConfirmWipeDeviceDialog.js
@@ -18,7 +18,9 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import {_t} from "../../../languageHandler";
 import * as sdk from "../../../index";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.ConfirmWipeDeviceDialog")
 export default class ConfirmWipeDeviceDialog extends React.Component {
     static propTypes = {
         onFinished: PropTypes.func.isRequired,
diff --git a/src/components/views/dialogs/CreateCommunityPrototypeDialog.tsx b/src/components/views/dialogs/CreateCommunityPrototypeDialog.tsx
index 1d9d92b9c9..9b4484d661 100644
--- a/src/components/views/dialogs/CreateCommunityPrototypeDialog.tsx
+++ b/src/components/views/dialogs/CreateCommunityPrototypeDialog.tsx
@@ -25,6 +25,7 @@ import InfoTooltip from "../elements/InfoTooltip";
 import dis from "../../../dispatcher/dispatcher";
 import {showCommunityRoomInviteDialog} from "../../../RoomInvite";
 import GroupStore from "../../../stores/GroupStore";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps extends IDialogProps {
 }
@@ -38,6 +39,7 @@ interface IState {
     avatarPreview: string;
 }
 
+@replaceableComponent("views.dialogs.CreateCommunityPrototypeDialog")
 export default class CreateCommunityPrototypeDialog extends React.PureComponent<IProps, IState> {
     private avatarUploadRef: React.RefObject<HTMLInputElement> = React.createRef();
 
diff --git a/src/components/views/dialogs/CreateGroupDialog.js b/src/components/views/dialogs/CreateGroupDialog.js
index 6636153c98..e6c7a67aca 100644
--- a/src/components/views/dialogs/CreateGroupDialog.js
+++ b/src/components/views/dialogs/CreateGroupDialog.js
@@ -20,7 +20,9 @@ import * as sdk from '../../../index';
 import dis from '../../../dispatcher/dispatcher';
 import { _t } from '../../../languageHandler';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.CreateGroupDialog")
 export default class CreateGroupDialog extends React.Component {
     static propTypes = {
         onFinished: PropTypes.func.isRequired,
diff --git a/src/components/views/dialogs/CreateRoomDialog.js b/src/components/views/dialogs/CreateRoomDialog.js
index 0771b0ec45..e9dc6e2be0 100644
--- a/src/components/views/dialogs/CreateRoomDialog.js
+++ b/src/components/views/dialogs/CreateRoomDialog.js
@@ -27,7 +27,9 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import {Key} from "../../../Keyboard";
 import {privateShouldBeEncrypted} from "../../../createRoom";
 import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.CreateRoomDialog")
 export default class CreateRoomDialog extends React.Component {
     static propTypes = {
         onFinished: PropTypes.func.isRequired,
diff --git a/src/components/views/dialogs/DeactivateAccountDialog.js b/src/components/views/dialogs/DeactivateAccountDialog.js
index fca8c42546..4e52549d51 100644
--- a/src/components/views/dialogs/DeactivateAccountDialog.js
+++ b/src/components/views/dialogs/DeactivateAccountDialog.js
@@ -26,7 +26,9 @@ import { _t } from '../../../languageHandler';
 import InteractiveAuth, {ERROR_USER_CANCELLED} from "../../structures/InteractiveAuth";
 import {DEFAULT_PHASE, PasswordAuthEntry, SSOAuthEntry} from "../auth/InteractiveAuthEntryComponents";
 import StyledCheckbox from "../elements/StyledCheckbox";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.DeactivateAccountDialog")
 export default class DeactivateAccountDialog extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/dialogs/DevtoolsDialog.js b/src/components/views/dialogs/DevtoolsDialog.js
index 814378bb51..9b24aaa571 100644
--- a/src/components/views/dialogs/DevtoolsDialog.js
+++ b/src/components/views/dialogs/DevtoolsDialog.js
@@ -38,6 +38,7 @@ import {SETTINGS} from "../../../settings/Settings";
 import SettingsStore, {LEVEL_ORDER} from "../../../settings/SettingsStore";
 import Modal from "../../../Modal";
 import ErrorDialog from "./ErrorDialog";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 class GenericEditor extends React.PureComponent {
     // static propTypes = {onBack: PropTypes.func.isRequired};
@@ -1089,6 +1090,7 @@ const Entries = [
     SettingsExplorer,
 ];
 
+@replaceableComponent("views.dialogs.DevtoolsDialog")
 export default class DevtoolsDialog extends React.PureComponent {
     static propTypes = {
         roomId: PropTypes.string.isRequired,
diff --git a/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx b/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx
index 3071854b3e..504d563bd9 100644
--- a/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx
+++ b/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx
@@ -23,6 +23,7 @@ import AccessibleButton from "../elements/AccessibleButton";
 import { MatrixClientPeg } from "../../../MatrixClientPeg";
 import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
 import FlairStore from "../../../stores/FlairStore";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps extends IDialogProps {
     communityId: string;
@@ -38,6 +39,7 @@ interface IState {
 }
 
 // XXX: This is a lot of duplication from the create dialog, just in a different shape
+@replaceableComponent("views.dialogs.EditCommunityPrototypeDialog")
 export default class EditCommunityPrototypeDialog extends React.PureComponent<IProps, IState> {
     private avatarUploadRef: React.RefObject<HTMLInputElement> = React.createRef();
 
diff --git a/src/components/views/dialogs/ErrorDialog.js b/src/components/views/dialogs/ErrorDialog.js
index 3bfa635adf..5197c68b5a 100644
--- a/src/components/views/dialogs/ErrorDialog.js
+++ b/src/components/views/dialogs/ErrorDialog.js
@@ -29,7 +29,9 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.ErrorDialog")
 export default class ErrorDialog extends React.Component {
     static propTypes = {
         title: PropTypes.string,
diff --git a/src/components/views/dialogs/HostSignupDialog.tsx b/src/components/views/dialogs/HostSignupDialog.tsx
index 45a03b7cf0..c8bc907136 100644
--- a/src/components/views/dialogs/HostSignupDialog.tsx
+++ b/src/components/views/dialogs/HostSignupDialog.tsx
@@ -31,6 +31,7 @@ import {
     IPostmessageResponseData,
     PostmessageAction,
 } from "./HostSignupDialogTypes";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const HOST_SIGNUP_KEY = "host_signup";
 
@@ -42,6 +43,7 @@ interface IState {
     minimized: boolean;
 }
 
+@replaceableComponent("views.dialogs.HostSignupDialog")
 export default class HostSignupDialog extends React.PureComponent<IProps, IState> {
     private iframeRef: React.RefObject<HTMLIFrameElement> = React.createRef();
     private readonly config: IHostSignupConfig;
diff --git a/src/components/views/dialogs/IncomingSasDialog.js b/src/components/views/dialogs/IncomingSasDialog.js
index 2a4ff9cec3..d65ec7563f 100644
--- a/src/components/views/dialogs/IncomingSasDialog.js
+++ b/src/components/views/dialogs/IncomingSasDialog.js
@@ -19,6 +19,7 @@ import PropTypes from 'prop-types';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const PHASE_START = 0;
 const PHASE_SHOW_SAS = 1;
@@ -26,6 +27,7 @@ const PHASE_WAIT_FOR_PARTNER_TO_CONFIRM = 2;
 const PHASE_VERIFIED = 3;
 const PHASE_CANCELLED = 4;
 
+@replaceableComponent("views.dialogs.IncomingSasDialog")
 export default class IncomingSasDialog extends React.Component {
     static propTypes = {
         verifier: PropTypes.object.isRequired,
diff --git a/src/components/views/dialogs/IntegrationsDisabledDialog.js b/src/components/views/dialogs/IntegrationsDisabledDialog.js
index 7c996fbeab..0e9878f4bc 100644
--- a/src/components/views/dialogs/IntegrationsDisabledDialog.js
+++ b/src/components/views/dialogs/IntegrationsDisabledDialog.js
@@ -20,7 +20,9 @@ import {_t} from "../../../languageHandler";
 import * as sdk from "../../../index";
 import dis from '../../../dispatcher/dispatcher';
 import {Action} from "../../../dispatcher/actions";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.IntegrationsDisabledDialog")
 export default class IntegrationsDisabledDialog extends React.Component {
     static propTypes = {
         onFinished: PropTypes.func.isRequired,
diff --git a/src/components/views/dialogs/IntegrationsImpossibleDialog.js b/src/components/views/dialogs/IntegrationsImpossibleDialog.js
index 68bedc711d..9bc9d02ba6 100644
--- a/src/components/views/dialogs/IntegrationsImpossibleDialog.js
+++ b/src/components/views/dialogs/IntegrationsImpossibleDialog.js
@@ -19,7 +19,9 @@ import PropTypes from 'prop-types';
 import {_t} from "../../../languageHandler";
 import SdkConfig from "../../../SdkConfig";
 import * as sdk from "../../../index";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.IntegrationsImpossibleDialog")
 export default class IntegrationsImpossibleDialog extends React.Component {
     static propTypes = {
         onFinished: PropTypes.func.isRequired,
diff --git a/src/components/views/dialogs/InteractiveAuthDialog.js b/src/components/views/dialogs/InteractiveAuthDialog.js
index 22291225ad..28a9bf673a 100644
--- a/src/components/views/dialogs/InteractiveAuthDialog.js
+++ b/src/components/views/dialogs/InteractiveAuthDialog.js
@@ -25,7 +25,9 @@ import { _t } from '../../../languageHandler';
 import AccessibleButton from '../elements/AccessibleButton';
 import {ERROR_USER_CANCELLED} from "../../structures/InteractiveAuth";
 import {SSOAuthEntry} from "../auth/InteractiveAuthEntryComponents";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.InteractiveAuthDialog")
 export default class InteractiveAuthDialog extends React.Component {
     static propTypes = {
         // matrix client to use for UI auth requests
diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx
index 9bc5b6476f..db9077291e 100644
--- a/src/components/views/dialogs/InviteDialog.tsx
+++ b/src/components/views/dialogs/InviteDialog.tsx
@@ -42,6 +42,7 @@ import {UIFeature} from "../../../settings/UIFeature";
 import CountlyAnalytics from "../../../CountlyAnalytics";
 import {Room} from "matrix-js-sdk/src/models/room";
 import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // we have a number of types defined from the Matrix spec which can't reasonably be altered here.
 /* eslint-disable camelcase */
@@ -337,6 +338,7 @@ interface IInviteDialogState {
     errorText: string,
 }
 
+@replaceableComponent("views.dialogs.InviteDialog")
 export default class InviteDialog extends React.PureComponent<IInviteDialogProps, IInviteDialogState> {
     static defaultProps = {
         kind: KIND_DM,
diff --git a/src/components/views/dialogs/LogoutDialog.js b/src/components/views/dialogs/LogoutDialog.js
index af36dba2b6..7bced46d43 100644
--- a/src/components/views/dialogs/LogoutDialog.js
+++ b/src/components/views/dialogs/LogoutDialog.js
@@ -22,7 +22,9 @@ import dis from '../../../dispatcher/dispatcher';
 import { _t } from '../../../languageHandler';
 import { MatrixClientPeg } from '../../../MatrixClientPeg';
 import RestoreKeyBackupDialog from './security/RestoreKeyBackupDialog';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.LogoutDialog")
 export default class LogoutDialog extends React.Component {
     defaultProps = {
         onFinished: function() {},
diff --git a/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.js b/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.js
index 4b9d7239e6..3151edd796 100644
--- a/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.js
+++ b/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.js
@@ -24,7 +24,9 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import * as sdk from '../../../index';
 import * as FormattingUtils from '../../../utils/FormattingUtils';
 import { _t } from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.ManualDeviceKeyVerificationDialog")
 export default class ManualDeviceKeyVerificationDialog extends React.Component {
     static propTypes = {
         userId: PropTypes.string.isRequired,
diff --git a/src/components/views/dialogs/MessageEditHistoryDialog.js b/src/components/views/dialogs/MessageEditHistoryDialog.js
index 2bdf2be35c..7585561c0c 100644
--- a/src/components/views/dialogs/MessageEditHistoryDialog.js
+++ b/src/components/views/dialogs/MessageEditHistoryDialog.js
@@ -21,7 +21,9 @@ import { _t } from '../../../languageHandler';
 import * as sdk from "../../../index";
 import {wantsDateSeparator} from '../../../DateUtils';
 import SettingsStore from '../../../settings/SettingsStore';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.MessageEditHistoryDialog")
 export default class MessageEditHistoryDialog extends React.PureComponent {
     static propTypes = {
         mxEvent: PropTypes.object.isRequired,
diff --git a/src/components/views/dialogs/ModalWidgetDialog.tsx b/src/components/views/dialogs/ModalWidgetDialog.tsx
index 92fb406965..59eaab7b81 100644
--- a/src/components/views/dialogs/ModalWidgetDialog.tsx
+++ b/src/components/views/dialogs/ModalWidgetDialog.tsx
@@ -38,6 +38,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import {OwnProfileStore} from "../../../stores/OwnProfileStore";
 import { arrayFastClone } from "../../../utils/arrays";
 import { ElementWidget } from "../../../stores/widgets/StopGapWidget";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     widgetDefinition: IModalWidgetOpenRequestData;
@@ -53,6 +54,7 @@ interface IState {
 
 const MAX_BUTTONS = 3;
 
+@replaceableComponent("views.dialogs.ModalWidgetDialog")
 export default class ModalWidgetDialog extends React.PureComponent<IProps, IState> {
     private readonly widget: Widget;
     private readonly possibleButtons: ModalButtonID[];
diff --git a/src/components/views/dialogs/ReportEventDialog.js b/src/components/views/dialogs/ReportEventDialog.js
index f5509dec4d..67ed0f8f53 100644
--- a/src/components/views/dialogs/ReportEventDialog.js
+++ b/src/components/views/dialogs/ReportEventDialog.js
@@ -22,10 +22,12 @@ import {MatrixEvent} from "matrix-js-sdk";
 import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import SdkConfig from '../../../SdkConfig';
 import Markdown from '../../../Markdown';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 /*
  * A dialog for reporting an event.
  */
+@replaceableComponent("views.dialogs.ReportEventDialog")
 export default class ReportEventDialog extends PureComponent {
     static propTypes = {
         mxEvent: PropTypes.instanceOf(MatrixEvent).isRequired,
diff --git a/src/components/views/dialogs/RoomSettingsDialog.js b/src/components/views/dialogs/RoomSettingsDialog.js
index 368f2aeccd..9c2f23ef22 100644
--- a/src/components/views/dialogs/RoomSettingsDialog.js
+++ b/src/components/views/dialogs/RoomSettingsDialog.js
@@ -30,6 +30,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import dis from "../../../dispatcher/dispatcher";
 import SettingsStore from "../../../settings/SettingsStore";
 import {UIFeature} from "../../../settings/UIFeature";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 export const ROOM_GENERAL_TAB = "ROOM_GENERAL_TAB";
 export const ROOM_SECURITY_TAB = "ROOM_SECURITY_TAB";
@@ -38,6 +39,7 @@ export const ROOM_NOTIFICATIONS_TAB = "ROOM_NOTIFICATIONS_TAB";
 export const ROOM_BRIDGES_TAB = "ROOM_BRIDGES_TAB";
 export const ROOM_ADVANCED_TAB = "ROOM_ADVANCED_TAB";
 
+@replaceableComponent("views.dialogs.RoomSettingsDialog")
 export default class RoomSettingsDialog extends React.Component {
     static propTypes = {
         roomId: PropTypes.string.isRequired,
diff --git a/src/components/views/dialogs/RoomUpgradeDialog.js b/src/components/views/dialogs/RoomUpgradeDialog.js
index 85e97444ed..8f9ed42ada 100644
--- a/src/components/views/dialogs/RoomUpgradeDialog.js
+++ b/src/components/views/dialogs/RoomUpgradeDialog.js
@@ -20,7 +20,9 @@ import * as sdk from '../../../index';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import Modal from '../../../Modal';
 import { _t } from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.RoomUpgradeDialog")
 export default class RoomUpgradeDialog extends React.Component {
     static propTypes = {
         room: PropTypes.object.isRequired,
diff --git a/src/components/views/dialogs/RoomUpgradeWarningDialog.js b/src/components/views/dialogs/RoomUpgradeWarningDialog.js
index c83528c5ba..452ac56dff 100644
--- a/src/components/views/dialogs/RoomUpgradeWarningDialog.js
+++ b/src/components/views/dialogs/RoomUpgradeWarningDialog.js
@@ -22,7 +22,9 @@ import * as sdk from "../../../index";
 import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
 import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import Modal from "../../../Modal";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.RoomUpgradeWarningDialog")
 export default class RoomUpgradeWarningDialog extends React.Component {
     static propTypes = {
         onFinished: PropTypes.func.isRequired,
diff --git a/src/components/views/dialogs/ServerOfflineDialog.tsx b/src/components/views/dialogs/ServerOfflineDialog.tsx
index 81f628343b..52ff056907 100644
--- a/src/components/views/dialogs/ServerOfflineDialog.tsx
+++ b/src/components/views/dialogs/ServerOfflineDialog.tsx
@@ -28,10 +28,12 @@ import AccessibleButton from "../elements/AccessibleButton";
 import { UPDATE_EVENT } from "../../../stores/AsyncStore";
 import { MatrixClientPeg } from "../../../MatrixClientPeg";
 import { IDialogProps } from "./IDialogProps";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps extends IDialogProps {
 }
 
+@replaceableComponent("views.dialogs.ServerOfflineDialog")
 export default class ServerOfflineDialog extends React.PureComponent<IProps> {
     public componentDidMount() {
         EchoStore.instance.on(UPDATE_EVENT, this.onEchosUpdated);
diff --git a/src/components/views/dialogs/ServerPickerDialog.tsx b/src/components/views/dialogs/ServerPickerDialog.tsx
index 7ca115760e..4abc0a88b1 100644
--- a/src/components/views/dialogs/ServerPickerDialog.tsx
+++ b/src/components/views/dialogs/ServerPickerDialog.tsx
@@ -26,6 +26,7 @@ import Field from "../elements/Field";
 import StyledRadioButton from "../elements/StyledRadioButton";
 import TextWithTooltip from "../elements/TextWithTooltip";
 import withValidation, {IFieldState} from "../elements/Validation";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     title?: string;
@@ -38,6 +39,7 @@ interface IState {
     otherHomeserver: string;
 }
 
+@replaceableComponent("views.dialogs.ServerPickerDialog")
 export default class ServerPickerDialog extends React.PureComponent<IProps, IState> {
     private readonly defaultServer: ValidatedServerConfig;
     private readonly fieldRef = createRef<Field>();
diff --git a/src/components/views/dialogs/SessionRestoreErrorDialog.js b/src/components/views/dialogs/SessionRestoreErrorDialog.js
index bae6b19fbe..50d7fbea09 100644
--- a/src/components/views/dialogs/SessionRestoreErrorDialog.js
+++ b/src/components/views/dialogs/SessionRestoreErrorDialog.js
@@ -22,8 +22,9 @@ import * as sdk from '../../../index';
 import SdkConfig from '../../../SdkConfig';
 import Modal from '../../../Modal';
 import { _t } from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
-
+@replaceableComponent("views.dialogs.SessionRestoreErrorDialog")
 export default class SessionRestoreErrorDialog extends React.Component {
     static propTypes = {
         error: PropTypes.string.isRequired,
diff --git a/src/components/views/dialogs/SetEmailDialog.js b/src/components/views/dialogs/SetEmailDialog.js
index 6514d94dc9..0f8f410a6a 100644
--- a/src/components/views/dialogs/SetEmailDialog.js
+++ b/src/components/views/dialogs/SetEmailDialog.js
@@ -22,6 +22,7 @@ import * as Email from '../../../email';
 import AddThreepid from '../../../AddThreepid';
 import { _t } from '../../../languageHandler';
 import Modal from '../../../Modal';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 
 /*
@@ -29,6 +30,7 @@ import Modal from '../../../Modal';
  *
  * On success, `onFinished(true)` is called.
  */
+@replaceableComponent("views.dialogs.SetEmailDialog")
 export default class SetEmailDialog extends React.Component {
     static propTypes = {
         onFinished: PropTypes.func.isRequired,
diff --git a/src/components/views/dialogs/ShareDialog.tsx b/src/components/views/dialogs/ShareDialog.tsx
index 5264031cc6..df1206a4f0 100644
--- a/src/components/views/dialogs/ShareDialog.tsx
+++ b/src/components/views/dialogs/ShareDialog.tsx
@@ -34,6 +34,7 @@ import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
 import { IDialogProps } from "./IDialogProps";
 import SettingsStore from "../../../settings/SettingsStore";
 import {UIFeature} from "../../../settings/UIFeature";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const socials = [
     {
@@ -73,6 +74,7 @@ interface IState {
     permalinkCreator: RoomPermalinkCreator;
 }
 
+@replaceableComponent("views.dialogs.ShareDialog")
 export default class ShareDialog extends React.PureComponent<IProps, IState> {
     static propTypes = {
         onFinished: PropTypes.func.isRequired,
diff --git a/src/components/views/dialogs/StorageEvictedDialog.js b/src/components/views/dialogs/StorageEvictedDialog.js
index a22f302807..15c5347644 100644
--- a/src/components/views/dialogs/StorageEvictedDialog.js
+++ b/src/components/views/dialogs/StorageEvictedDialog.js
@@ -20,7 +20,9 @@ import * as sdk from '../../../index';
 import SdkConfig from '../../../SdkConfig';
 import Modal from '../../../Modal';
 import { _t } from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.StorageEvictedDialog")
 export default class StorageEvictedDialog extends React.Component {
     static propTypes = {
         onFinished: PropTypes.func.isRequired,
diff --git a/src/components/views/dialogs/TabbedIntegrationManagerDialog.js b/src/components/views/dialogs/TabbedIntegrationManagerDialog.js
index 9f5c9f6a11..07e29adcff 100644
--- a/src/components/views/dialogs/TabbedIntegrationManagerDialog.js
+++ b/src/components/views/dialogs/TabbedIntegrationManagerDialog.js
@@ -22,7 +22,9 @@ import * as sdk from '../../../index';
 import {dialogTermsInteractionCallback, TermsNotSignedError} from "../../../Terms";
 import classNames from 'classnames';
 import * as ScalarMessaging from "../../../ScalarMessaging";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.TabbedIntegrationManagerDialog")
 export default class TabbedIntegrationManagerDialog extends React.Component {
     static propTypes = {
         /**
diff --git a/src/components/views/dialogs/TermsDialog.js b/src/components/views/dialogs/TermsDialog.js
index 402605c545..72e6c3f3a0 100644
--- a/src/components/views/dialogs/TermsDialog.js
+++ b/src/components/views/dialogs/TermsDialog.js
@@ -21,6 +21,7 @@ import * as sdk from '../../../index';
 import { _t, pickBestLanguage } from '../../../languageHandler';
 
 import Matrix from 'matrix-js-sdk';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 class TermsCheckbox extends React.PureComponent {
     static propTypes = {
@@ -41,6 +42,7 @@ class TermsCheckbox extends React.PureComponent {
     }
 }
 
+@replaceableComponent("views.dialogs.TermsDialog")
 export default class TermsDialog extends React.PureComponent {
     static propTypes = {
         /**
diff --git a/src/components/views/dialogs/TextInputDialog.js b/src/components/views/dialogs/TextInputDialog.js
index 69cc4390be..97abd209c0 100644
--- a/src/components/views/dialogs/TextInputDialog.js
+++ b/src/components/views/dialogs/TextInputDialog.js
@@ -19,7 +19,9 @@ import PropTypes from 'prop-types';
 import * as sdk from '../../../index';
 import Field from "../elements/Field";
 import { _t, _td } from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.TextInputDialog")
 export default class TextInputDialog extends React.Component {
     static propTypes = {
         title: PropTypes.string,
diff --git a/src/components/views/dialogs/UploadConfirmDialog.js b/src/components/views/dialogs/UploadConfirmDialog.js
index e3521eb282..2ff16b9440 100644
--- a/src/components/views/dialogs/UploadConfirmDialog.js
+++ b/src/components/views/dialogs/UploadConfirmDialog.js
@@ -20,7 +20,9 @@ import PropTypes from 'prop-types';
 import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
 import filesize from "filesize";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.UploadConfirmDialog")
 export default class UploadConfirmDialog extends React.Component {
     static propTypes = {
         file: PropTypes.object.isRequired,
diff --git a/src/components/views/dialogs/UploadFailureDialog.js b/src/components/views/dialogs/UploadFailureDialog.js
index 4be1656f66..d220d6c684 100644
--- a/src/components/views/dialogs/UploadFailureDialog.js
+++ b/src/components/views/dialogs/UploadFailureDialog.js
@@ -21,12 +21,14 @@ import PropTypes from 'prop-types';
 import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
 import ContentMessages from '../../../ContentMessages';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 /*
  * Tells the user about files we know cannot be uploaded before we even try uploading
  * them. This is named fairly generically but the only thing we check right now is
  * the size of the file.
  */
+@replaceableComponent("views.dialogs.UploadFailureDialog")
 export default class UploadFailureDialog extends React.Component {
     static propTypes = {
         badFiles: PropTypes.arrayOf(PropTypes.object).isRequired,
diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.js
index 3291fa2387..eb9eaeb5dd 100644
--- a/src/components/views/dialogs/UserSettingsDialog.js
+++ b/src/components/views/dialogs/UserSettingsDialog.js
@@ -33,6 +33,7 @@ import * as sdk from "../../../index";
 import SdkConfig from "../../../SdkConfig";
 import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab";
 import {UIFeature} from "../../../settings/UIFeature";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 export const USER_GENERAL_TAB = "USER_GENERAL_TAB";
 export const USER_APPEARANCE_TAB = "USER_APPEARANCE_TAB";
@@ -45,6 +46,7 @@ export const USER_LABS_TAB = "USER_LABS_TAB";
 export const USER_MJOLNIR_TAB = "USER_MJOLNIR_TAB";
 export const USER_HELP_TAB = "USER_HELP_TAB";
 
+@replaceableComponent("views.dialogs.UserSettingsDialog")
 export default class UserSettingsDialog extends React.Component {
     static propTypes = {
         onFinished: PropTypes.func.isRequired,
diff --git a/src/components/views/dialogs/VerificationRequestDialog.js b/src/components/views/dialogs/VerificationRequestDialog.js
index 3a6e9a2d10..574beebbc6 100644
--- a/src/components/views/dialogs/VerificationRequestDialog.js
+++ b/src/components/views/dialogs/VerificationRequestDialog.js
@@ -19,7 +19,9 @@ import PropTypes from 'prop-types';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.VerificationRequestDialog")
 export default class VerificationRequestDialog extends React.Component {
     static propTypes = {
         verificationRequest: PropTypes.object,
diff --git a/src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx b/src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx
index 535e0b7b8e..70fe7fe5e3 100644
--- a/src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx
+++ b/src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx
@@ -29,6 +29,7 @@ import StyledCheckbox from "../elements/StyledCheckbox";
 import DialogButtons from "../elements/DialogButtons";
 import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
 import { CapabilityText } from "../../../widgets/CapabilityText";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 export function getRememberedCapabilitiesForWidget(widget: Widget): Capability[] {
     return JSON.parse(localStorage.getItem(`widget_${widget.id}_approved_caps`) || "[]");
@@ -54,6 +55,7 @@ interface IState {
     rememberSelection: boolean;
 }
 
+@replaceableComponent("views.dialogs.WidgetCapabilitiesPromptDialog")
 export default class WidgetCapabilitiesPromptDialog extends React.PureComponent<IProps, IState> {
     private eventPermissionsMap = new Map<Capability, WidgetEventCapability>();
 
diff --git a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js
index c01d3d39b8..f45adf9738 100644
--- a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js
+++ b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js
@@ -21,7 +21,9 @@ import * as sdk from "../../../index";
 import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
 import {Widget} from "matrix-widget-api";
 import {OIDCState, WidgetPermissionStore} from "../../../stores/widgets/WidgetPermissionStore";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.WidgetOpenIDPermissionsDialog")
 export default class WidgetOpenIDPermissionsDialog extends React.Component {
     static propTypes = {
         onFinished: PropTypes.func.isRequired,
diff --git a/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js b/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js
index abc1586205..43fb25f152 100644
--- a/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js
+++ b/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js
@@ -18,7 +18,9 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import {_t} from "../../../../languageHandler";
 import * as sdk from "../../../../index";
+import {replaceableComponent} from "../../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.security.ConfirmDestroyCrossSigningDialog")
 export default class ConfirmDestroyCrossSigningDialog extends React.Component {
     static propTypes = {
         onFinished: PropTypes.func.isRequired,
diff --git a/src/components/views/dialogs/security/CreateCrossSigningDialog.js b/src/components/views/dialogs/security/CreateCrossSigningDialog.js
index be546d2616..fedcc02f89 100644
--- a/src/components/views/dialogs/security/CreateCrossSigningDialog.js
+++ b/src/components/views/dialogs/security/CreateCrossSigningDialog.js
@@ -25,12 +25,14 @@ import DialogButtons from '../../elements/DialogButtons';
 import BaseDialog from '../BaseDialog';
 import Spinner from '../../elements/Spinner';
 import InteractiveAuthDialog from '../InteractiveAuthDialog';
+import {replaceableComponent} from "../../../../utils/replaceableComponent";
 
 /*
  * Walks the user through the process of creating a cross-signing keys. In most
  * cases, only a spinner is shown, but for more complex auth like SSO, the user
  * may need to complete some steps to proceed.
  */
+@replaceableComponent("views.dialogs.security.CreateCrossSigningDialog")
 export default class CreateCrossSigningDialog extends React.PureComponent {
     static propTypes = {
         accountPassword: PropTypes.string,
diff --git a/src/components/views/dialogs/security/SetupEncryptionDialog.js b/src/components/views/dialogs/security/SetupEncryptionDialog.js
index 9ce3144534..3c15ea9f1d 100644
--- a/src/components/views/dialogs/security/SetupEncryptionDialog.js
+++ b/src/components/views/dialogs/security/SetupEncryptionDialog.js
@@ -20,6 +20,7 @@ import SetupEncryptionBody from '../../../structures/auth/SetupEncryptionBody';
 import BaseDialog from '../BaseDialog';
 import { _t } from '../../../../languageHandler';
 import { SetupEncryptionStore, PHASE_DONE } from '../../../../stores/SetupEncryptionStore';
+import {replaceableComponent} from "../../../../utils/replaceableComponent";
 
 function iconFromPhase(phase) {
     if (phase === PHASE_DONE) {
@@ -29,6 +30,7 @@ function iconFromPhase(phase) {
     }
 }
 
+@replaceableComponent("views.dialogs.security.SetupEncryptionDialog")
 export default class SetupEncryptionDialog extends React.Component {
     static propTypes = {
         onFinished: PropTypes.func.isRequired,
diff --git a/src/components/views/elements/AccessibleTooltipButton.tsx b/src/components/views/elements/AccessibleTooltipButton.tsx
index b7c7b78e63..3bb264fb3e 100644
--- a/src/components/views/elements/AccessibleTooltipButton.tsx
+++ b/src/components/views/elements/AccessibleTooltipButton.tsx
@@ -20,6 +20,7 @@ import classNames from 'classnames';
 
 import AccessibleButton from "./AccessibleButton";
 import Tooltip from './Tooltip';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
     title: string;
@@ -33,6 +34,7 @@ interface IState {
     hover: boolean;
 }
 
+@replaceableComponent("views.elements.AccessibleTooltipButton")
 export default class AccessibleTooltipButton extends React.PureComponent<ITooltipProps, IState> {
     constructor(props: ITooltipProps) {
         super(props);
diff --git a/src/components/views/elements/ActionButton.js b/src/components/views/elements/ActionButton.js
index bec016bce0..1714891cb5 100644
--- a/src/components/views/elements/ActionButton.js
+++ b/src/components/views/elements/ActionButton.js
@@ -20,7 +20,9 @@ import AccessibleButton from './AccessibleButton';
 import dis from '../../../dispatcher/dispatcher';
 import * as sdk from '../../../index';
 import Analytics from '../../../Analytics';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.elements.ActionButton")
 export default class ActionButton extends React.Component {
     static propTypes = {
         size: PropTypes.string,
diff --git a/src/components/views/elements/AddressSelector.js b/src/components/views/elements/AddressSelector.js
index 2a71622bb8..33b2906870 100644
--- a/src/components/views/elements/AddressSelector.js
+++ b/src/components/views/elements/AddressSelector.js
@@ -20,7 +20,9 @@ import PropTypes from 'prop-types';
 import * as sdk from '../../../index';
 import classNames from 'classnames';
 import { UserAddressType } from '../../../UserAddress';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.elements.AddressSelector")
 export default class AddressSelector extends React.Component {
     static propTypes = {
         onSelected: PropTypes.func.isRequired,
diff --git a/src/components/views/elements/AddressTile.js b/src/components/views/elements/AddressTile.js
index dc6c6b2914..4a216dbae4 100644
--- a/src/components/views/elements/AddressTile.js
+++ b/src/components/views/elements/AddressTile.js
@@ -22,8 +22,9 @@ import * as sdk from "../../../index";
 import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import { _t } from '../../../languageHandler';
 import { UserAddressType } from '../../../UserAddress.js';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
-
+@replaceableComponent("views.elements.AddressTile")
 export default class AddressTile extends React.Component {
     static propTypes = {
         address: UserAddressType.isRequired,
diff --git a/src/components/views/elements/AppPermission.js b/src/components/views/elements/AppPermission.js
index ec8bffc32f..65e40ef19a 100644
--- a/src/components/views/elements/AppPermission.js
+++ b/src/components/views/elements/AppPermission.js
@@ -24,7 +24,9 @@ import { _t } from '../../../languageHandler';
 import SdkConfig from '../../../SdkConfig';
 import WidgetUtils from "../../../utils/WidgetUtils";
 import {MatrixClientPeg} from "../../../MatrixClientPeg";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.elements.AppPermission")
 export default class AppPermission extends React.Component {
     static propTypes = {
         url: PropTypes.string.isRequired,
diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index 2a72621ccc..e206fda797 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -38,7 +38,9 @@ import {ElementWidgetActions} from "../../../stores/widgets/ElementWidgetActions
 import {MatrixCapabilities} from "matrix-widget-api";
 import RoomWidgetContextMenu from "../context_menus/WidgetContextMenu";
 import WidgetAvatar from "../avatars/WidgetAvatar";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.elements.AppTile")
 export default class AppTile extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/elements/DesktopCapturerSourcePicker.tsx b/src/components/views/elements/DesktopCapturerSourcePicker.tsx
index 6ae465c362..2d066a7ed7 100644
--- a/src/components/views/elements/DesktopCapturerSourcePicker.tsx
+++ b/src/components/views/elements/DesktopCapturerSourcePicker.tsx
@@ -19,6 +19,7 @@ import { _t } from '../../../languageHandler';
 import BaseDialog from "..//dialogs/BaseDialog"
 import AccessibleButton from './AccessibleButton';
 import {getDesktopCapturerSources} from "matrix-js-sdk/src/webrtc/call";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 export interface DesktopCapturerSource {
     id: string;
@@ -69,6 +70,7 @@ export interface DesktopCapturerSourcePickerIProps {
     onFinished(source: DesktopCapturerSource): void;
 }
 
+@replaceableComponent("views.elements.DesktopCapturerSourcePicker")
 export default class DesktopCapturerSourcePicker extends React.Component<
     DesktopCapturerSourcePickerIProps,
     DesktopCapturerSourcePickerIState
diff --git a/src/components/views/elements/DialogButtons.js b/src/components/views/elements/DialogButtons.js
index 3417485eb8..dcb1cee077 100644
--- a/src/components/views/elements/DialogButtons.js
+++ b/src/components/views/elements/DialogButtons.js
@@ -19,10 +19,12 @@ limitations under the License.
 import React from "react";
 import PropTypes from "prop-types";
 import { _t } from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 /**
  * Basic container for buttons in modal dialogs.
  */
+@replaceableComponent("views.elements.DialogButtons")
 export default class DialogButtons extends React.Component {
     static propTypes = {
         // The primary button which is styled differently and has default focus.
diff --git a/src/components/views/elements/DirectorySearchBox.js b/src/components/views/elements/DirectorySearchBox.js
index 644b69417b..6447bb3cd8 100644
--- a/src/components/views/elements/DirectorySearchBox.js
+++ b/src/components/views/elements/DirectorySearchBox.js
@@ -18,7 +18,9 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.elements.DirectorySearchBox")
 export default class DirectorySearchBox extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/elements/Draggable.tsx b/src/components/views/elements/Draggable.tsx
index a6eb8323f3..6032721a48 100644
--- a/src/components/views/elements/Draggable.tsx
+++ b/src/components/views/elements/Draggable.tsx
@@ -15,6 +15,7 @@ limitations under the License.
 */
 
 import React from 'react';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     className: string;
@@ -33,6 +34,7 @@ export interface ILocationState {
     currentY: number;
 }
 
+@replaceableComponent("views.elements.Draggable")
 export default class Draggable extends React.Component<IProps, IState> {
     constructor(props: IProps) {
         super(props);
diff --git a/src/components/views/elements/Dropdown.js b/src/components/views/elements/Dropdown.js
index 6b3efb5ee1..981c0becc0 100644
--- a/src/components/views/elements/Dropdown.js
+++ b/src/components/views/elements/Dropdown.js
@@ -22,6 +22,7 @@ import classnames from 'classnames';
 import AccessibleButton from './AccessibleButton';
 import { _t } from '../../../languageHandler';
 import {Key} from "../../../Keyboard";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 class MenuOption extends React.Component {
     constructor(props) {
@@ -83,6 +84,7 @@ MenuOption.propTypes = {
  *
  * TODO: Port NetworkDropdown to use this.
  */
+@replaceableComponent("views.elements.Dropdown")
 export default class Dropdown extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/elements/EditableItemList.js b/src/components/views/elements/EditableItemList.js
index 5a07a400d7..ff62f169fa 100644
--- a/src/components/views/elements/EditableItemList.js
+++ b/src/components/views/elements/EditableItemList.js
@@ -19,6 +19,7 @@ import PropTypes from 'prop-types';
 import {_t} from '../../../languageHandler';
 import Field from "./Field";
 import AccessibleButton from "./AccessibleButton";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 export class EditableItem extends React.Component {
     static propTypes = {
@@ -85,6 +86,7 @@ export class EditableItem extends React.Component {
     }
 }
 
+@replaceableComponent("views.elements.EditableItemList")
 export default class EditableItemList extends React.Component {
     static propTypes = {
         id: PropTypes.string.isRequired,
diff --git a/src/components/views/elements/EditableText.js b/src/components/views/elements/EditableText.js
index 49eb331aef..638fd02553 100644
--- a/src/components/views/elements/EditableText.js
+++ b/src/components/views/elements/EditableText.js
@@ -18,7 +18,9 @@ limitations under the License.
 import React, {createRef} from 'react';
 import PropTypes from 'prop-types';
 import {Key} from "../../../Keyboard";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.elements.EditableText")
 export default class EditableText extends React.Component {
     static propTypes = {
         onValueChanged: PropTypes.func,
diff --git a/src/components/views/elements/EditableTextContainer.js b/src/components/views/elements/EditableTextContainer.js
index bbc5560557..e925220089 100644
--- a/src/components/views/elements/EditableTextContainer.js
+++ b/src/components/views/elements/EditableTextContainer.js
@@ -17,6 +17,7 @@ limitations under the License.
 import React from 'react';
 import PropTypes from 'prop-types';
 import * as sdk from '../../../index';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 /**
  * A component which wraps an EditableText, with a spinner while updates take
@@ -29,6 +30,7 @@ import * as sdk from '../../../index';
  * similarly asynchronous way. If this is not provided, the initial value is
  * taken from the 'initialValue' property.
  */
+@replaceableComponent("views.elements.EditableTextContainer")
 export default class EditableTextContainer extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/elements/ErrorBoundary.js b/src/components/views/elements/ErrorBoundary.js
index 9fe6861250..9037287f49 100644
--- a/src/components/views/elements/ErrorBoundary.js
+++ b/src/components/views/elements/ErrorBoundary.js
@@ -21,11 +21,13 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import PlatformPeg from '../../../PlatformPeg';
 import Modal from '../../../Modal';
 import SdkConfig from "../../../SdkConfig";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 /**
  * This error boundary component can be used to wrap large content areas and
  * catch exceptions during rendering in the component tree below them.
  */
+@replaceableComponent("views.elements.ErrorBoundary")
 export default class ErrorBoundary extends React.PureComponent {
     constructor(props) {
         super(props);
diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx
index 49c97831bc..c539f2be1c 100644
--- a/src/components/views/elements/EventTilePreview.tsx
+++ b/src/components/views/elements/EventTilePreview.tsx
@@ -24,6 +24,7 @@ import EventTile from '../rooms/EventTile';
 import SettingsStore from "../../../settings/SettingsStore";
 import {Layout} from "../../../settings/Layout";
 import {UIFeature} from "../../../settings/UIFeature";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     /**
@@ -52,6 +53,7 @@ interface IState {
 
 const AVATAR_SIZE = 32;
 
+@replaceableComponent("views.elements.EventTilePreview")
 export default class EventTilePreview extends React.Component<IProps, IState> {
     constructor(props: IProps) {
         super(props);
diff --git a/src/components/views/elements/Flair.js b/src/components/views/elements/Flair.js
index 645444b300..75998cb721 100644
--- a/src/components/views/elements/Flair.js
+++ b/src/components/views/elements/Flair.js
@@ -19,6 +19,7 @@ import PropTypes from 'prop-types';
 import FlairStore from '../../../stores/FlairStore';
 import dis from '../../../dispatcher/dispatcher';
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 
 class FlairAvatar extends React.Component {
@@ -62,6 +63,7 @@ FlairAvatar.propTypes = {
 
 FlairAvatar.contextType = MatrixClientContext;
 
+@replaceableComponent("views.elements.Flair")
 export default class Flair extends React.Component {
     constructor() {
         super();
diff --git a/src/components/views/elements/IRCTimelineProfileResizer.tsx b/src/components/views/elements/IRCTimelineProfileResizer.tsx
index ecd63816de..cd1ccf2fc4 100644
--- a/src/components/views/elements/IRCTimelineProfileResizer.tsx
+++ b/src/components/views/elements/IRCTimelineProfileResizer.tsx
@@ -18,6 +18,7 @@ import React from 'react';
 import SettingsStore from "../../../settings/SettingsStore";
 import Draggable, {ILocationState} from './Draggable';
 import { SettingLevel } from "../../../settings/SettingLevel";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     // Current room
@@ -31,6 +32,7 @@ interface IState {
     IRCLayoutRoot: HTMLElement;
 }
 
+@replaceableComponent("views.elements.IRCTimelineProfileResizer")
 export default class IRCTimelineProfileResizer extends React.Component<IProps, IState> {
     constructor(props: IProps) {
         super(props);
diff --git a/src/components/views/elements/ImageView.js b/src/components/views/elements/ImageView.js
index e39075cedc..96b6de832d 100644
--- a/src/components/views/elements/ImageView.js
+++ b/src/components/views/elements/ImageView.js
@@ -26,7 +26,9 @@ import Modal from "../../../Modal";
 import * as sdk from "../../../index";
 import {Key} from "../../../Keyboard";
 import FocusLock from "react-focus-lock";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.elements.ImageView")
 export default class ImageView extends React.Component {
     static propTypes = {
         src: PropTypes.string.isRequired, // the source of the image being displayed
diff --git a/src/components/views/elements/InfoTooltip.tsx b/src/components/views/elements/InfoTooltip.tsx
index dd21c95b74..8f7f1ea53f 100644
--- a/src/components/views/elements/InfoTooltip.tsx
+++ b/src/components/views/elements/InfoTooltip.tsx
@@ -20,6 +20,7 @@ import classNames from 'classnames';
 
 import Tooltip from './Tooltip';
 import { _t } from "../../../languageHandler";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface ITooltipProps {
     tooltip?: React.ReactNode;
@@ -30,6 +31,7 @@ interface IState {
     hover: boolean;
 }
 
+@replaceableComponent("views.elements.InfoTooltip")
 export default class InfoTooltip extends React.PureComponent<ITooltipProps, IState> {
     constructor(props: ITooltipProps) {
         super(props);
diff --git a/src/components/views/elements/InlineSpinner.js b/src/components/views/elements/InlineSpinner.js
index 73316157f4..3654a1f34c 100644
--- a/src/components/views/elements/InlineSpinner.js
+++ b/src/components/views/elements/InlineSpinner.js
@@ -17,7 +17,9 @@ limitations under the License.
 import React from "react";
 import {_t} from "../../../languageHandler";
 import SettingsStore from "../../../settings/SettingsStore";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.elements.InlineSpinner")
 export default class InlineSpinner extends React.Component {
     render() {
         const w = this.props.w || 16;
diff --git a/src/components/views/elements/LabelledToggleSwitch.js b/src/components/views/elements/LabelledToggleSwitch.js
index 78beb2aa91..e6378f0e6a 100644
--- a/src/components/views/elements/LabelledToggleSwitch.js
+++ b/src/components/views/elements/LabelledToggleSwitch.js
@@ -17,7 +17,9 @@ limitations under the License.
 import React from 'react';
 import PropTypes from "prop-types";
 import ToggleSwitch from "./ToggleSwitch";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.elements.LabelledToggleSwitch")
 export default class LabelledToggleSwitch extends React.Component {
     static propTypes = {
         // The value for the toggle switch
diff --git a/src/components/views/elements/LanguageDropdown.js b/src/components/views/elements/LanguageDropdown.js
index 03ec456af5..2e961be700 100644
--- a/src/components/views/elements/LanguageDropdown.js
+++ b/src/components/views/elements/LanguageDropdown.js
@@ -22,6 +22,7 @@ import * as sdk from '../../../index';
 import * as languageHandler from '../../../languageHandler';
 import SettingsStore from "../../../settings/SettingsStore";
 import { _t } from "../../../languageHandler";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 function languageMatchesSearchQuery(query, language) {
     if (language.label.toUpperCase().includes(query.toUpperCase())) return true;
@@ -29,6 +30,7 @@ function languageMatchesSearchQuery(query, language) {
     return false;
 }
 
+@replaceableComponent("views.elements.LanguageDropdown")
 export default class LanguageDropdown extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/elements/LazyRenderList.js b/src/components/views/elements/LazyRenderList.js
index 7572dced0b..f2c8148cd2 100644
--- a/src/components/views/elements/LazyRenderList.js
+++ b/src/components/views/elements/LazyRenderList.js
@@ -16,6 +16,7 @@ limitations under the License.
 
 import React from "react";
 import PropTypes from 'prop-types';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 class ItemRange {
     constructor(topCount, renderCount, bottomCount) {
@@ -55,6 +56,7 @@ class ItemRange {
     }
 }
 
+@replaceableComponent("views.elements.LazyRenderList")
 export default class LazyRenderList extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/elements/MemberEventListSummary.tsx b/src/components/views/elements/MemberEventListSummary.tsx
index 073bedf207..0290ef6d83 100644
--- a/src/components/views/elements/MemberEventListSummary.tsx
+++ b/src/components/views/elements/MemberEventListSummary.tsx
@@ -24,6 +24,7 @@ import { _t } from '../../../languageHandler';
 import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
 import { isValid3pidInvite } from "../../../RoomInvite";
 import EventListSummary from "./EventListSummary";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     // An array of member events to summarise
@@ -69,6 +70,7 @@ enum TransitionType {
 
 const SEP = ",";
 
+@replaceableComponent("views.elements.MemberEventListSummary")
 export default class MemberEventListSummary extends React.Component<IProps> {
     static defaultProps = {
         summaryLength: 1,
diff --git a/src/components/views/elements/PersistedElement.js b/src/components/views/elements/PersistedElement.js
index 3732f644b8..f504b3e97f 100644
--- a/src/components/views/elements/PersistedElement.js
+++ b/src/components/views/elements/PersistedElement.js
@@ -24,6 +24,7 @@ import dis from '../../../dispatcher/dispatcher';
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import {isNullOrUndefined} from "matrix-js-sdk/src/utils";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // Shamelessly ripped off Modal.js.  There's probably a better way
 // of doing reusable widgets like dialog boxes & menus where we go and
@@ -56,6 +57,7 @@ function getOrCreateContainer(containerId) {
  * children are made visible and are positioned into a div that is given the same
  * bounding rect as the parent of PE.
  */
+@replaceableComponent("views.elements.PersistedElement")
 export default class PersistedElement extends React.Component {
     static propTypes = {
         // Unique identifier for this PersistedElement instance
diff --git a/src/components/views/elements/PersistentApp.js b/src/components/views/elements/PersistentApp.js
index 7801076c66..5df373e4fe 100644
--- a/src/components/views/elements/PersistentApp.js
+++ b/src/components/views/elements/PersistentApp.js
@@ -21,7 +21,9 @@ import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
 import WidgetUtils from '../../../utils/WidgetUtils';
 import * as sdk from '../../../index';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.elements.PersistentApp")
 export default class PersistentApp extends React.Component {
     state = {
         roomId: RoomViewStore.getRoomId(),
diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js
index c6806c289e..b0d4fc7fa2 100644
--- a/src/components/views/elements/Pill.js
+++ b/src/components/views/elements/Pill.js
@@ -27,7 +27,9 @@ import {getPrimaryPermalinkEntity, parseAppLocalLink} from "../../../utils/perma
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import {Action} from "../../../dispatcher/actions";
 import Tooltip from './Tooltip';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.elements.Pill")
 class Pill extends React.Component {
     static roomNotifPos(text) {
         return text.indexOf("@room");
diff --git a/src/components/views/elements/PowerSelector.js b/src/components/views/elements/PowerSelector.js
index 66922df0f8..622bed9890 100644
--- a/src/components/views/elements/PowerSelector.js
+++ b/src/components/views/elements/PowerSelector.js
@@ -20,7 +20,9 @@ import * as Roles from '../../../Roles';
 import { _t } from '../../../languageHandler';
 import Field from "./Field";
 import {Key} from "../../../Keyboard";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.elements.PowerSelector")
 export default class PowerSelector extends React.Component {
     static propTypes = {
         value: PropTypes.number.isRequired,
diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js
index 27d773b099..2e0cc50435 100644
--- a/src/components/views/elements/ReplyThread.js
+++ b/src/components/views/elements/ReplyThread.js
@@ -31,10 +31,12 @@ import {Action} from "../../../dispatcher/actions";
 import sanitizeHtml from "sanitize-html";
 import {UIFeature} from "../../../settings/UIFeature";
 import {PERMITTED_URL_SCHEMES} from "../../../HtmlUtils";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // This component does no cycle detection, simply because the only way to make such a cycle would be to
 // craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would
 // be low as each event being loaded (after the first) is triggered by an explicit user action.
+@replaceableComponent("views.elements.ReplyThread")
 export default class ReplyThread extends React.Component {
     static propTypes = {
         // the latest event in this chain of replies
diff --git a/src/components/views/elements/RoomAliasField.js b/src/components/views/elements/RoomAliasField.js
index 04bbe1c3de..1db154c2cd 100644
--- a/src/components/views/elements/RoomAliasField.js
+++ b/src/components/views/elements/RoomAliasField.js
@@ -19,8 +19,10 @@ import PropTypes from 'prop-types';
 import * as sdk from '../../../index';
 import withValidation from './Validation';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // Controlled form component wrapping Field for inputting a room alias scoped to a given domain
+@replaceableComponent("views.elements.RoomAliasField")
 export default class RoomAliasField extends React.PureComponent {
     static propTypes = {
         domain: PropTypes.string.isRequired,
diff --git a/src/components/views/elements/SettingsFlag.tsx b/src/components/views/elements/SettingsFlag.tsx
index 03e91fac62..4f885ab47d 100644
--- a/src/components/views/elements/SettingsFlag.tsx
+++ b/src/components/views/elements/SettingsFlag.tsx
@@ -21,6 +21,7 @@ import { _t } from '../../../languageHandler';
 import ToggleSwitch from "./ToggleSwitch";
 import StyledCheckbox from "./StyledCheckbox";
 import { SettingLevel } from "../../../settings/SettingLevel";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     // The setting must be a boolean
@@ -39,6 +40,7 @@ interface IState {
     value: boolean;
 }
 
+@replaceableComponent("views.elements.SettingsFlag")
 export default class SettingsFlag extends React.Component<IProps, IState> {
     constructor(props: IProps) {
         super(props);
diff --git a/src/components/views/elements/Slider.tsx b/src/components/views/elements/Slider.tsx
index b7c8e1b533..b513f90460 100644
--- a/src/components/views/elements/Slider.tsx
+++ b/src/components/views/elements/Slider.tsx
@@ -15,6 +15,7 @@ limitations under the License.
 */
 
 import * as React from 'react';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     // A callback for the selected value
@@ -34,6 +35,7 @@ interface IProps {
     disabled: boolean;
 }
 
+@replaceableComponent("views.elements.Slider")
 export default class Slider extends React.Component<IProps> {
     // offset is a terrible inverse approximation.
     // if the values represents some function f(x) = y where x is the
diff --git a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx
index c647f6e410..06e1efe415 100644
--- a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx
+++ b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx
@@ -21,6 +21,7 @@ import * as sdk from '../../../index';
 import PlatformPeg from "../../../PlatformPeg";
 import SettingsStore from "../../../settings/SettingsStore";
 import { _t } from "../../../languageHandler";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 function languageMatchesSearchQuery(query, language) {
     if (language.label.toUpperCase().includes(query.toUpperCase())) return true;
@@ -39,6 +40,7 @@ interface SpellCheckLanguagesDropdownIState {
     languages: any,
 }
 
+@replaceableComponent("views.elements.SpellCheckLanguagesDropdown")
 export default class SpellCheckLanguagesDropdown extends React.Component<SpellCheckLanguagesDropdownIProps,
                                                                          SpellCheckLanguagesDropdownIState> {
     constructor(props) {
diff --git a/src/components/views/elements/Spoiler.js b/src/components/views/elements/Spoiler.js
index b75967b225..33b4382a2c 100644
--- a/src/components/views/elements/Spoiler.js
+++ b/src/components/views/elements/Spoiler.js
@@ -15,7 +15,9 @@
  */
 
 import React from 'react';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.elements.Spoiler")
 export default class Spoiler extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/elements/StyledCheckbox.tsx b/src/components/views/elements/StyledCheckbox.tsx
index f8d2665d07..2454d1336b 100644
--- a/src/components/views/elements/StyledCheckbox.tsx
+++ b/src/components/views/elements/StyledCheckbox.tsx
@@ -16,6 +16,7 @@ limitations under the License.
 
 import React from "react";
 import { randomString } from "matrix-js-sdk/src/randomstring";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
 }
@@ -23,6 +24,7 @@ interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
 interface IState {
 }
 
+@replaceableComponent("views.elements.StyledCheckbox")
 export default class StyledCheckbox extends React.PureComponent<IProps, IState> {
     private id: string;
 
diff --git a/src/components/views/elements/StyledRadioButton.tsx b/src/components/views/elements/StyledRadioButton.tsx
index 2efd084861..835394e055 100644
--- a/src/components/views/elements/StyledRadioButton.tsx
+++ b/src/components/views/elements/StyledRadioButton.tsx
@@ -16,6 +16,7 @@ limitations under the License.
 
 import React from 'react';
 import classnames from 'classnames';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
     outlined?: boolean;
@@ -24,6 +25,7 @@ interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
 interface IState {
 }
 
+@replaceableComponent("views.elements.StyledRadioButton")
 export default class StyledRadioButton extends React.PureComponent<IProps, IState> {
     public static readonly defaultProps = {
         className: '',
diff --git a/src/components/views/elements/SyntaxHighlight.js b/src/components/views/elements/SyntaxHighlight.js
index a4dc97d46e..f9874c5367 100644
--- a/src/components/views/elements/SyntaxHighlight.js
+++ b/src/components/views/elements/SyntaxHighlight.js
@@ -17,7 +17,9 @@ limitations under the License.
 import React from 'react';
 import PropTypes from 'prop-types';
 import {highlightBlock} from 'highlight.js';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.elements.SyntaxHighlight")
 export default class SyntaxHighlight extends React.Component {
     static propTypes = {
         className: PropTypes.string,
diff --git a/src/components/views/elements/TagTile.js b/src/components/views/elements/TagTile.js
index 6c9a01a840..663acd6329 100644
--- a/src/components/views/elements/TagTile.js
+++ b/src/components/views/elements/TagTile.js
@@ -30,12 +30,14 @@ import GroupFilterOrderStore from '../../../stores/GroupFilterOrderStore';
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import AccessibleButton from "./AccessibleButton";
 import SettingsStore from "../../../settings/SettingsStore";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // A class for a child of GroupFilterPanel (possibly wrapped in a DNDTagTile) that represents
 // a thing to click on for the user to filter the visible rooms in the RoomList to:
 //  - Rooms that are part of the group
 //  - Direct messages with members of the group
 // with the intention that this could be expanded to arbitrary tags in future.
+@replaceableComponent("views.elements.TagTile")
 export default class TagTile extends React.Component {
     static propTypes = {
         // A string tag such as "m.favourite" or a group ID such as "+groupid:domain.bla"
diff --git a/src/components/views/elements/TextWithTooltip.js b/src/components/views/elements/TextWithTooltip.js
index b0405dc4c9..e4ad234ae2 100644
--- a/src/components/views/elements/TextWithTooltip.js
+++ b/src/components/views/elements/TextWithTooltip.js
@@ -17,7 +17,9 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import * as sdk from '../../../index';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.elements.TextWithTooltip")
 export default class TextWithTooltip extends React.Component {
     static propTypes = {
         class: PropTypes.string,
diff --git a/src/components/views/elements/TintableSvg.js b/src/components/views/elements/TintableSvg.js
index df55b0a854..690deeedcd 100644
--- a/src/components/views/elements/TintableSvg.js
+++ b/src/components/views/elements/TintableSvg.js
@@ -18,7 +18,9 @@ limitations under the License.
 import React from 'react';
 import PropTypes from 'prop-types';
 import Tinter from "../../../Tinter";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.elements.TintableSvg")
 class TintableSvg extends React.Component {
     static propTypes = {
         src: PropTypes.string.isRequired,
diff --git a/src/components/views/elements/Tooltip.tsx b/src/components/views/elements/Tooltip.tsx
index 03b9eb08d0..b2dd00de18 100644
--- a/src/components/views/elements/Tooltip.tsx
+++ b/src/components/views/elements/Tooltip.tsx
@@ -21,6 +21,7 @@ limitations under the License.
 import React, {Component, CSSProperties} from 'react';
 import ReactDOM from 'react-dom';
 import classNames from 'classnames';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const MIN_TOOLTIP_HEIGHT = 25;
 
@@ -39,6 +40,7 @@ interface IProps {
         yOffset?: number;
 }
 
+@replaceableComponent("views.elements.Tooltip")
 export default class Tooltip extends React.Component<IProps> {
     private tooltipContainer: HTMLElement;
     private tooltip: void | Element | Component<Element, any, any>;
diff --git a/src/components/views/elements/TooltipButton.js b/src/components/views/elements/TooltipButton.js
index 240d763bdc..c5ebb3b1aa 100644
--- a/src/components/views/elements/TooltipButton.js
+++ b/src/components/views/elements/TooltipButton.js
@@ -17,7 +17,9 @@ limitations under the License.
 
 import React from 'react';
 import * as sdk from '../../../index';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.elements.TooltipButton")
 export default class TooltipButton extends React.Component {
     state = {
         hover: false,
diff --git a/src/components/views/elements/TruncatedList.js b/src/components/views/elements/TruncatedList.js
index 81eb057e36..0509775545 100644
--- a/src/components/views/elements/TruncatedList.js
+++ b/src/components/views/elements/TruncatedList.js
@@ -18,7 +18,9 @@ limitations under the License.
 import React from 'react';
 import PropTypes from 'prop-types';
 import { _t } from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.elements.TruncatedList")
 export default class TruncatedList extends React.Component {
     static propTypes = {
         // The number of elements to show before truncating. If negative, no truncation is done.
diff --git a/src/components/views/elements/UserTagTile.tsx b/src/components/views/elements/UserTagTile.tsx
index e7c74bb10e..d3e07a0a34 100644
--- a/src/components/views/elements/UserTagTile.tsx
+++ b/src/components/views/elements/UserTagTile.tsx
@@ -21,6 +21,7 @@ import GroupFilterOrderStore from "../../../stores/GroupFilterOrderStore";
 import AccessibleTooltipButton from "./AccessibleTooltipButton";
 import classNames from "classnames";
 import { _t } from "../../../languageHandler";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
 }
@@ -29,6 +30,7 @@ interface IState {
     selected: boolean;
 }
 
+@replaceableComponent("views.elements.UserTagTile")
 export default class UserTagTile extends React.PureComponent<IProps, IState> {
     private tagStoreRef: fbEmitter.EventSubscription;
 
diff --git a/src/components/views/emojipicker/Category.tsx b/src/components/views/emojipicker/Category.tsx
index c4feaac8ae..4c7852def3 100644
--- a/src/components/views/emojipicker/Category.tsx
+++ b/src/components/views/emojipicker/Category.tsx
@@ -21,6 +21,7 @@ import { CATEGORY_HEADER_HEIGHT, EMOJI_HEIGHT, EMOJIS_PER_ROW } from "./EmojiPic
 import LazyRenderList from "../elements/LazyRenderList";
 import {DATA_BY_CATEGORY, IEmoji} from "../../../emoji";
 import Emoji from './Emoji';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const OVERFLOW_ROWS = 3;
 
@@ -47,6 +48,7 @@ interface IProps {
     onMouseLeave(emoji: IEmoji): void;
 }
 
+@replaceableComponent("views.emojipicker.Category")
 class Category extends React.PureComponent<IProps> {
     private renderEmojiRow = (rowIndex: number) => {
         const { onClick, onMouseEnter, onMouseLeave, selectedEmojis, emojis } = this.props;
diff --git a/src/components/views/emojipicker/Emoji.tsx b/src/components/views/emojipicker/Emoji.tsx
index 5d715fb935..5d7665ce98 100644
--- a/src/components/views/emojipicker/Emoji.tsx
+++ b/src/components/views/emojipicker/Emoji.tsx
@@ -19,6 +19,7 @@ import React from 'react';
 
 import {MenuItem} from "../../structures/ContextMenu";
 import {IEmoji} from "../../../emoji";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     emoji: IEmoji;
@@ -28,6 +29,7 @@ interface IProps {
     onMouseLeave(emoji: IEmoji): void;
 }
 
+@replaceableComponent("views.emojipicker.Emoji")
 class Emoji extends React.PureComponent<IProps> {
     render() {
         const { onClick, onMouseEnter, onMouseLeave, emoji, selectedEmojis } = this.props;
diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx
index bf0481c51c..6d7b90c8a6 100644
--- a/src/components/views/emojipicker/EmojiPicker.tsx
+++ b/src/components/views/emojipicker/EmojiPicker.tsx
@@ -26,6 +26,7 @@ import Search from "./Search";
 import Preview from "./Preview";
 import QuickReactions from "./QuickReactions";
 import Category, {ICategory, CategoryKey} from "./Category";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 export const CATEGORY_HEADER_HEIGHT = 22;
 export const EMOJI_HEIGHT = 37;
@@ -47,6 +48,7 @@ interface IState {
     viewportHeight: number;
 }
 
+@replaceableComponent("views.emojipicker.EmojiPicker")
 class EmojiPicker extends React.Component<IProps, IState> {
     private readonly recentlyUsed: IEmoji[];
     private readonly memoizedDataByCategory: Record<CategoryKey, IEmoji[]>;
diff --git a/src/components/views/emojipicker/Header.tsx b/src/components/views/emojipicker/Header.tsx
index 9a93722483..693f86ad73 100644
--- a/src/components/views/emojipicker/Header.tsx
+++ b/src/components/views/emojipicker/Header.tsx
@@ -21,12 +21,14 @@ import classNames from "classnames";
 import {_t} from "../../../languageHandler";
 import {Key} from "../../../Keyboard";
 import {CategoryKey, ICategory} from "./Category";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     categories: ICategory[];
     onAnchorClick(id: CategoryKey): void
 }
 
+@replaceableComponent("views.emojipicker.Header")
 class Header extends React.PureComponent<IProps> {
     private findNearestEnabled(index: number, delta: number) {
         index += this.props.categories.length;
diff --git a/src/components/views/emojipicker/Preview.tsx b/src/components/views/emojipicker/Preview.tsx
index 69bfdf4d1c..e0952ec73e 100644
--- a/src/components/views/emojipicker/Preview.tsx
+++ b/src/components/views/emojipicker/Preview.tsx
@@ -18,11 +18,13 @@ limitations under the License.
 import React from 'react';
 
 import {IEmoji} from "../../../emoji";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     emoji: IEmoji;
 }
 
+@replaceableComponent("views.emojipicker.Preview")
 class Preview extends React.PureComponent<IProps> {
     render() {
         const {
diff --git a/src/components/views/emojipicker/QuickReactions.tsx b/src/components/views/emojipicker/QuickReactions.tsx
index 0477ecfb93..a250aca458 100644
--- a/src/components/views/emojipicker/QuickReactions.tsx
+++ b/src/components/views/emojipicker/QuickReactions.tsx
@@ -20,6 +20,7 @@ import React from 'react';
 import { _t } from '../../../languageHandler';
 import {getEmojiFromUnicode, IEmoji} from "../../../emoji";
 import Emoji from "./Emoji";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // We use the variation-selector Heart in Quick Reactions for some reason
 const QUICK_REACTIONS = ["👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀"].map(emoji => {
@@ -39,6 +40,7 @@ interface IState {
     hover?: IEmoji;
 }
 
+@replaceableComponent("views.emojipicker.QuickReactions")
 class QuickReactions extends React.Component<IProps, IState> {
     constructor(props) {
         super(props);
diff --git a/src/components/views/emojipicker/ReactionPicker.tsx b/src/components/views/emojipicker/ReactionPicker.tsx
index dbef0eadbe..e86d183aba 100644
--- a/src/components/views/emojipicker/ReactionPicker.tsx
+++ b/src/components/views/emojipicker/ReactionPicker.tsx
@@ -21,6 +21,7 @@ import {MatrixEvent} from "matrix-js-sdk/src/models/event";
 import EmojiPicker from "./EmojiPicker";
 import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import dis from "../../../dispatcher/dispatcher";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     mxEvent: MatrixEvent;
@@ -32,6 +33,7 @@ interface IState {
     selectedEmojis: Set<string>;
 }
 
+@replaceableComponent("views.emojipicker.ReactionPicker")
 class ReactionPicker extends React.Component<IProps, IState> {
     constructor(props) {
         super(props);
diff --git a/src/components/views/emojipicker/Search.tsx b/src/components/views/emojipicker/Search.tsx
index fe1fecec7b..abe3e026be 100644
--- a/src/components/views/emojipicker/Search.tsx
+++ b/src/components/views/emojipicker/Search.tsx
@@ -19,6 +19,7 @@ import React from 'react';
 
 import { _t } from '../../../languageHandler';
 import {Key} from "../../../Keyboard";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     query: string;
@@ -26,6 +27,7 @@ interface IProps {
     onEnter(): void;
 }
 
+@replaceableComponent("views.emojipicker.Search")
 class Search extends React.PureComponent<IProps> {
     private inputRef = React.createRef<HTMLInputElement>();
 
diff --git a/src/components/views/groups/GroupInviteTile.js b/src/components/views/groups/GroupInviteTile.js
index 0c09b6ed05..dc48c01acb 100644
--- a/src/components/views/groups/GroupInviteTile.js
+++ b/src/components/views/groups/GroupInviteTile.js
@@ -26,8 +26,10 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import {ContextMenu, ContextMenuButton, toRightOf} from "../../structures/ContextMenu";
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import {RovingTabIndexWrapper} from "../../../accessibility/RovingTabIndex";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // XXX this class copies a lot from RoomTile.js
+@replaceableComponent("views.groups.GroupInviteTile")
 export default class GroupInviteTile extends React.Component {
     static propTypes: {
         group: PropTypes.object.isRequired,
diff --git a/src/components/views/groups/GroupMemberList.js b/src/components/views/groups/GroupMemberList.js
index 600a466601..d5b3f9aec7 100644
--- a/src/components/views/groups/GroupMemberList.js
+++ b/src/components/views/groups/GroupMemberList.js
@@ -26,9 +26,11 @@ import AccessibleButton from '../elements/AccessibleButton';
 import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
 import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
 import {Action} from "../../../dispatcher/actions";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const INITIAL_LOAD_NUM_MEMBERS = 30;
 
+@replaceableComponent("views.groups.GroupMemberList")
 export default class GroupMemberList extends React.Component {
     static propTypes = {
         groupId: PropTypes.string.isRequired,
diff --git a/src/components/views/groups/GroupMemberTile.js b/src/components/views/groups/GroupMemberTile.js
index 13617cf681..e8285803b0 100644
--- a/src/components/views/groups/GroupMemberTile.js
+++ b/src/components/views/groups/GroupMemberTile.js
@@ -22,7 +22,9 @@ import * as sdk from '../../../index';
 import dis from '../../../dispatcher/dispatcher';
 import { GroupMemberType } from '../../../groups';
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.groups.GroupMemberTile")
 export default class GroupMemberTile extends React.Component {
     static propTypes = {
         groupId: PropTypes.string.isRequired,
diff --git a/src/components/views/groups/GroupPublicityToggle.js b/src/components/views/groups/GroupPublicityToggle.js
index d42059551e..5399125d9f 100644
--- a/src/components/views/groups/GroupPublicityToggle.js
+++ b/src/components/views/groups/GroupPublicityToggle.js
@@ -19,7 +19,9 @@ import PropTypes from 'prop-types';
 import * as sdk from '../../../index';
 import GroupStore from '../../../stores/GroupStore';
 import ToggleSwitch from "../elements/ToggleSwitch";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.groups.GroupPublicityTile")
 export default class GroupPublicityToggle extends React.Component {
     static propTypes = {
         groupId: PropTypes.string.isRequired,
diff --git a/src/components/views/groups/GroupRoomInfo.js b/src/components/views/groups/GroupRoomInfo.js
index 50bbd26029..227a17e995 100644
--- a/src/components/views/groups/GroupRoomInfo.js
+++ b/src/components/views/groups/GroupRoomInfo.js
@@ -24,7 +24,9 @@ import { _t } from '../../../languageHandler';
 import GroupStore from '../../../stores/GroupStore';
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.groups.GroupRoomInfo")
 export default class GroupRoomInfo extends React.Component {
     static contextType = MatrixClientContext;
 
diff --git a/src/components/views/groups/GroupRoomList.js b/src/components/views/groups/GroupRoomList.js
index 9bb46db47c..f8a90f9222 100644
--- a/src/components/views/groups/GroupRoomList.js
+++ b/src/components/views/groups/GroupRoomList.js
@@ -21,9 +21,11 @@ import PropTypes from 'prop-types';
 import { showGroupAddRoomDialog } from '../../../GroupAddressPicker';
 import AccessibleButton from '../elements/AccessibleButton';
 import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const INITIAL_LOAD_NUM_ROOMS = 30;
 
+@replaceableComponent("views.groups.GroupRoomList")
 export default class GroupRoomList extends React.Component {
     static propTypes = {
         groupId: PropTypes.string.isRequired,
diff --git a/src/components/views/groups/GroupRoomTile.js b/src/components/views/groups/GroupRoomTile.js
index 85aa56d055..8b25437f71 100644
--- a/src/components/views/groups/GroupRoomTile.js
+++ b/src/components/views/groups/GroupRoomTile.js
@@ -20,7 +20,9 @@ import * as sdk from '../../../index';
 import dis from '../../../dispatcher/dispatcher';
 import { GroupRoomType } from '../../../groups';
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.groups.GroupRoomTile")
 class GroupRoomTile extends React.Component {
     static propTypes = {
         groupId: PropTypes.string.isRequired,
diff --git a/src/components/views/groups/GroupTile.js b/src/components/views/groups/GroupTile.js
index dcc749b01f..bb1714c9f2 100644
--- a/src/components/views/groups/GroupTile.js
+++ b/src/components/views/groups/GroupTile.js
@@ -21,9 +21,11 @@ import * as sdk from '../../../index';
 import dis from '../../../dispatcher/dispatcher';
 import FlairStore from '../../../stores/FlairStore';
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 function nop() {}
 
+@replaceableComponent("views.groups.GroupTile")
 class GroupTile extends React.Component {
     static propTypes = {
         groupId: PropTypes.string.isRequired,
diff --git a/src/components/views/groups/GroupUserSettings.js b/src/components/views/groups/GroupUserSettings.js
index 9209106c8f..5b537d7377 100644
--- a/src/components/views/groups/GroupUserSettings.js
+++ b/src/components/views/groups/GroupUserSettings.js
@@ -18,7 +18,9 @@ import React from 'react';
 import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.groups.GroupUserSettings")
 export default class GroupUserSettings extends React.Component {
     static contextType = MatrixClientContext;
 
diff --git a/src/components/views/messages/DateSeparator.js b/src/components/views/messages/DateSeparator.js
index ef4b5d16d1..82ce8dc4ae 100644
--- a/src/components/views/messages/DateSeparator.js
+++ b/src/components/views/messages/DateSeparator.js
@@ -19,6 +19,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { _t } from '../../../languageHandler';
 import {formatFullDateNoTime} from '../../../DateUtils';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 function getdaysArray() {
     return [
@@ -32,6 +33,7 @@ function getdaysArray() {
     ];
 }
 
+@replaceableComponent("views.messages.DateSeparator")
 export default class DateSeparator extends React.Component {
     static propTypes = {
         ts: PropTypes.number.isRequired,
diff --git a/src/components/views/messages/EditHistoryMessage.js b/src/components/views/messages/EditHistoryMessage.js
index cc098d04cd..c5002b3308 100644
--- a/src/components/views/messages/EditHistoryMessage.js
+++ b/src/components/views/messages/EditHistoryMessage.js
@@ -27,12 +27,14 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import Modal from '../../../Modal';
 import classNames from 'classnames';
 import RedactedBody from "./RedactedBody";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 function getReplacedContent(event) {
     const originalContent = event.getOriginalContent();
     return originalContent["m.new_content"] || originalContent;
 }
 
+@replaceableComponent("views.messages.EditHistoryMessage")
 export default class EditHistoryMessage extends React.PureComponent {
     static propTypes = {
         // the message event being edited
diff --git a/src/components/views/messages/MAudioBody.js b/src/components/views/messages/MAudioBody.js
index ac42b485d7..498e2db12a 100644
--- a/src/components/views/messages/MAudioBody.js
+++ b/src/components/views/messages/MAudioBody.js
@@ -21,7 +21,9 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import { decryptFile } from '../../../utils/DecryptFile';
 import { _t } from '../../../languageHandler';
 import InlineSpinner from '../elements/InlineSpinner';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.messages.MAudioBody")
 export default class MAudioBody extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js
index 676f0b7986..e9893f99b6 100644
--- a/src/components/views/messages/MFileBody.js
+++ b/src/components/views/messages/MFileBody.js
@@ -26,6 +26,7 @@ import Tinter from '../../../Tinter';
 import request from 'browser-request';
 import Modal from '../../../Modal';
 import AccessibleButton from "../elements/AccessibleButton";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 
 // A cached tinted copy of require("../../../../res/img/download.svg")
@@ -116,6 +117,7 @@ function computedStyle(element) {
     return cssText;
 }
 
+@replaceableComponent("views.messages.MFileBody")
 export default class MFileBody extends React.Component {
     static propTypes = {
         /* the MatrixEvent to show */
diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js
index 771d12accd..59c5b4e66b 100644
--- a/src/components/views/messages/MImageBody.js
+++ b/src/components/views/messages/MImageBody.js
@@ -27,7 +27,9 @@ import { _t } from '../../../languageHandler';
 import SettingsStore from "../../../settings/SettingsStore";
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import InlineSpinner from '../elements/InlineSpinner';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.messages.MImageBody")
 export default class MImageBody extends React.Component {
     static propTypes = {
         /* the MatrixEvent to show */
diff --git a/src/components/views/messages/MJitsiWidgetEvent.tsx b/src/components/views/messages/MJitsiWidgetEvent.tsx
index 6031ede8fa..626efe1f36 100644
--- a/src/components/views/messages/MJitsiWidgetEvent.tsx
+++ b/src/components/views/messages/MJitsiWidgetEvent.tsx
@@ -21,11 +21,13 @@ import WidgetStore from "../../../stores/WidgetStore";
 import EventTileBubble from "./EventTileBubble";
 import { MatrixClientPeg } from "../../../MatrixClientPeg";
 import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     mxEvent: MatrixEvent;
 }
 
+@replaceableComponent("views.messages.MJitsiWidgetEvent")
 export default class MJitsiWidgetEvent extends React.PureComponent<IProps> {
     constructor(props) {
         super(props);
diff --git a/src/components/views/messages/MKeyVerificationConclusion.js b/src/components/views/messages/MKeyVerificationConclusion.js
index 880299d29d..75d20131c0 100644
--- a/src/components/views/messages/MKeyVerificationConclusion.js
+++ b/src/components/views/messages/MKeyVerificationConclusion.js
@@ -22,7 +22,9 @@ import { _t } from '../../../languageHandler';
 import {getNameForEventRoom, userLabelForEventRoom}
     from '../../../utils/KeyVerificationStateObserver';
 import EventTileBubble from "./EventTileBubble";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.messages.MKeyVerificationConclusion")
 export default class MKeyVerificationConclusion extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/messages/MKeyVerificationRequest.js b/src/components/views/messages/MKeyVerificationRequest.js
index d9594091c5..988606a766 100644
--- a/src/components/views/messages/MKeyVerificationRequest.js
+++ b/src/components/views/messages/MKeyVerificationRequest.js
@@ -25,7 +25,9 @@ import dis from "../../../dispatcher/dispatcher";
 import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
 import {Action} from "../../../dispatcher/actions";
 import EventTileBubble from "./EventTileBubble";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.messages.MKeyVerificationRequest")
 export default class MKeyVerificationRequest extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/messages/MStickerBody.js b/src/components/views/messages/MStickerBody.js
index 9839080661..54eb7649b4 100644
--- a/src/components/views/messages/MStickerBody.js
+++ b/src/components/views/messages/MStickerBody.js
@@ -17,7 +17,9 @@ limitations under the License.
 import React from 'react';
 import MImageBody from './MImageBody';
 import * as sdk from '../../../index';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.messages.MStickerBody")
 export default class MStickerBody extends MImageBody {
     // Mostly empty to prevent default behaviour of MImageBody
     onClick(ev) {
diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx
index ce4a4eda76..89985dee7d 100644
--- a/src/components/views/messages/MVideoBody.tsx
+++ b/src/components/views/messages/MVideoBody.tsx
@@ -22,6 +22,7 @@ import { decryptFile } from '../../../utils/DecryptFile';
 import { _t } from '../../../languageHandler';
 import SettingsStore from "../../../settings/SettingsStore";
 import InlineSpinner from '../elements/InlineSpinner';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     /* the MatrixEvent to show */
@@ -38,6 +39,7 @@ interface IState {
     fetchingData: boolean,
 }
 
+@replaceableComponent("views.messages.MVideoBody")
 export default class MVideoBody extends React.PureComponent<IProps, IState> {
     private videoRef = React.createRef<HTMLVideoElement>();
 
diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js
index c94f296eac..c33debe3f5 100644
--- a/src/components/views/messages/MessageActionBar.js
+++ b/src/components/views/messages/MessageActionBar.js
@@ -28,6 +28,7 @@ import { isContentActionable, canEditContent } from '../../../utils/EventUtils';
 import RoomContext from "../../../contexts/RoomContext";
 import Toolbar from "../../../accessibility/Toolbar";
 import {RovingAccessibleTooltipButton, useRovingTabIndex} from "../../../accessibility/RovingTabIndex";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => {
     const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
@@ -101,6 +102,7 @@ const ReactButton = ({mxEvent, reactions, onFocusChange}) => {
     </React.Fragment>;
 };
 
+@replaceableComponent("views.messages.MessageActionBar")
 export default class MessageActionBar extends React.PureComponent {
     static propTypes = {
         mxEvent: PropTypes.object.isRequired,
diff --git a/src/components/views/messages/MessageEvent.js b/src/components/views/messages/MessageEvent.js
index f93813fe79..866e0f521d 100644
--- a/src/components/views/messages/MessageEvent.js
+++ b/src/components/views/messages/MessageEvent.js
@@ -21,7 +21,9 @@ import SettingsStore from "../../../settings/SettingsStore";
 import {Mjolnir} from "../../../mjolnir/Mjolnir";
 import RedactedBody from "./RedactedBody";
 import UnknownBody from "./UnknownBody";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.messages.MessageEvent")
 export default class MessageEvent extends React.Component {
     static propTypes = {
         /* the MatrixEvent to show */
diff --git a/src/components/views/messages/MessageTimestamp.js b/src/components/views/messages/MessageTimestamp.js
index 199a6f47ce..c9bdb8937e 100644
--- a/src/components/views/messages/MessageTimestamp.js
+++ b/src/components/views/messages/MessageTimestamp.js
@@ -18,7 +18,9 @@ limitations under the License.
 import React from 'react';
 import PropTypes from 'prop-types';
 import {formatFullDate, formatTime} from '../../../DateUtils';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.messages.MessageTimestamp")
 export default class MessageTimestamp extends React.Component {
     static propTypes = {
         ts: PropTypes.number.isRequired,
diff --git a/src/components/views/messages/MjolnirBody.js b/src/components/views/messages/MjolnirBody.js
index baaee91657..4368fd936c 100644
--- a/src/components/views/messages/MjolnirBody.js
+++ b/src/components/views/messages/MjolnirBody.js
@@ -17,7 +17,9 @@ limitations under the License.
 import React from 'react';
 import PropTypes from 'prop-types';
 import {_t} from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.messages.MjolnirBody")
 export default class MjolnirBody extends React.Component {
     static propTypes = {
         mxEvent: PropTypes.object.isRequired,
diff --git a/src/components/views/messages/ReactionsRow.js b/src/components/views/messages/ReactionsRow.js
index 3451cdbb2d..d5c8ea2ac9 100644
--- a/src/components/views/messages/ReactionsRow.js
+++ b/src/components/views/messages/ReactionsRow.js
@@ -21,10 +21,12 @@ import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
 import { isContentActionable } from '../../../utils/EventUtils';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // The maximum number of reactions to initially show on a message.
 const MAX_ITEMS_WHEN_LIMITED = 8;
 
+@replaceableComponent("views.messages.ReactionsRow")
 export default class ReactionsRow extends React.PureComponent {
     static propTypes = {
         // The event we're displaying reactions for
diff --git a/src/components/views/messages/ReactionsRowButton.js b/src/components/views/messages/ReactionsRowButton.js
index bb8d9a3b6e..06421c02a2 100644
--- a/src/components/views/messages/ReactionsRowButton.js
+++ b/src/components/views/messages/ReactionsRowButton.js
@@ -23,7 +23,9 @@ import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
 import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
 import dis from "../../../dispatcher/dispatcher";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.messages.ReactionsRowButton")
 export default class ReactionsRowButton extends React.PureComponent {
     static propTypes = {
         // The event we're displaying reactions for
diff --git a/src/components/views/messages/ReactionsRowButtonTooltip.js b/src/components/views/messages/ReactionsRowButtonTooltip.js
index 2b90175722..5ecdfe311d 100644
--- a/src/components/views/messages/ReactionsRowButtonTooltip.js
+++ b/src/components/views/messages/ReactionsRowButtonTooltip.js
@@ -22,7 +22,9 @@ import * as sdk from '../../../index';
 import { unicodeToShortcode } from '../../../HtmlUtils';
 import { _t } from '../../../languageHandler';
 import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.messages.ReactionsRowButtonTooltip")
 export default class ReactionsRowButtonTooltip extends React.PureComponent {
     static propTypes = {
         // The event we're displaying reactions for
diff --git a/src/components/views/messages/RoomAvatarEvent.js b/src/components/views/messages/RoomAvatarEvent.js
index f526d080cc..ba860216f0 100644
--- a/src/components/views/messages/RoomAvatarEvent.js
+++ b/src/components/views/messages/RoomAvatarEvent.js
@@ -23,7 +23,9 @@ import { _t } from '../../../languageHandler';
 import * as sdk from '../../../index';
 import Modal from '../../../Modal';
 import AccessibleButton from '../elements/AccessibleButton';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.messages.RoomAvatarEvent")
 export default class RoomAvatarEvent extends React.Component {
     static propTypes = {
         /* the MatrixEvent to show */
diff --git a/src/components/views/messages/RoomCreate.js b/src/components/views/messages/RoomCreate.js
index 479592aa42..3e02884c02 100644
--- a/src/components/views/messages/RoomCreate.js
+++ b/src/components/views/messages/RoomCreate.js
@@ -23,7 +23,9 @@ import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
 import { _t } from '../../../languageHandler';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import EventTileBubble from "./EventTileBubble";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.messages.RoomCreate")
 export default class RoomCreate extends React.Component {
     static propTypes = {
         /* the MatrixEvent to show */
diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js
index d2db05252c..bd10526799 100644
--- a/src/components/views/messages/SenderProfile.js
+++ b/src/components/views/messages/SenderProfile.js
@@ -20,7 +20,9 @@ import Flair from '../elements/Flair.js';
 import FlairStore from '../../../stores/FlairStore';
 import {getUserNameColorClass} from '../../../utils/FormattingUtils';
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.messages.SenderProfile")
 export default class SenderProfile extends React.Component {
     static propTypes = {
         mxEvent: PropTypes.object.isRequired, // event whose sender we're showing
diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js
index 04db7bd725..b0eb6f2f35 100644
--- a/src/components/views/messages/TextualBody.js
+++ b/src/components/views/messages/TextualBody.js
@@ -35,7 +35,9 @@ import {isPermalinkHost} from "../../../utils/permalinks/Permalinks";
 import {toRightOf} from "../../structures/ContextMenu";
 import {copyPlaintext} from "../../../utils/strings";
 import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.messages.TextualBody")
 export default class TextualBody extends React.Component {
     static propTypes = {
         /* the MatrixEvent to show */
diff --git a/src/components/views/messages/TextualEvent.js b/src/components/views/messages/TextualEvent.js
index 99e94147f7..a020cc6c52 100644
--- a/src/components/views/messages/TextualEvent.js
+++ b/src/components/views/messages/TextualEvent.js
@@ -18,7 +18,9 @@ limitations under the License.
 import React from 'react';
 import PropTypes from 'prop-types';
 import * as TextForEvent from "../../../TextForEvent";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.messages.TextualEvent")
 export default class TextualEvent extends React.Component {
     static propTypes = {
         /* the MatrixEvent to show */
diff --git a/src/components/views/messages/TileErrorBoundary.js b/src/components/views/messages/TileErrorBoundary.js
index 9b67e32548..0e9a7b6128 100644
--- a/src/components/views/messages/TileErrorBoundary.js
+++ b/src/components/views/messages/TileErrorBoundary.js
@@ -20,7 +20,9 @@ import { _t } from '../../../languageHandler';
 import * as sdk from '../../../index';
 import Modal from '../../../Modal';
 import SdkConfig from "../../../SdkConfig";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.messages.TileErrorBoundary")
 export default class TileErrorBoundary extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/messages/ViewSourceEvent.js b/src/components/views/messages/ViewSourceEvent.js
index 9064fc3b68..adc7a248cd 100644
--- a/src/components/views/messages/ViewSourceEvent.js
+++ b/src/components/views/messages/ViewSourceEvent.js
@@ -17,7 +17,9 @@ limitations under the License.
 import React from 'react';
 import PropTypes from 'prop-types';
 import classNames from 'classnames';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.messages.ViewSourceEvent")
 export default class ViewSourceEvent extends React.PureComponent {
     static propTypes = {
         /* the MatrixEvent to show */
diff --git a/src/components/views/right_panel/GroupHeaderButtons.tsx b/src/components/views/right_panel/GroupHeaderButtons.tsx
index dd4a82e645..f006975b08 100644
--- a/src/components/views/right_panel/GroupHeaderButtons.tsx
+++ b/src/components/views/right_panel/GroupHeaderButtons.tsx
@@ -26,6 +26,7 @@ import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
 import {Action} from "../../../dispatcher/actions";
 import {ActionPayload} from "../../../dispatcher/payloads";
 import {ViewUserPayload} from "../../../dispatcher/payloads/ViewUserPayload";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const GROUP_PHASES = [
     RightPanelPhases.GroupMemberInfo,
@@ -38,6 +39,7 @@ const ROOM_PHASES = [
 
 interface IProps {}
 
+@replaceableComponent("views.right_panel.GroupHeaderButtons")
 export default class GroupHeaderButtons extends HeaderButtons {
     constructor(props: IProps) {
         super(props, HeaderKind.Group);
diff --git a/src/components/views/right_panel/HeaderButton.tsx b/src/components/views/right_panel/HeaderButton.tsx
index 7f682e2d89..2bc360e380 100644
--- a/src/components/views/right_panel/HeaderButton.tsx
+++ b/src/components/views/right_panel/HeaderButton.tsx
@@ -22,6 +22,7 @@ import React from 'react';
 import classNames from 'classnames';
 import Analytics from '../../../Analytics';
 import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     // Whether this button is highlighted
@@ -41,6 +42,7 @@ interface IProps {
 
 // TODO: replace this, the composer buttons and the right panel buttons with a unified
 // representation
+@replaceableComponent("views.right_panel.HeaderButton")
 export default class HeaderButton extends React.Component<IProps> {
     constructor(props: IProps) {
         super(props);
diff --git a/src/components/views/right_panel/HeaderButtons.tsx b/src/components/views/right_panel/HeaderButtons.tsx
index 543c7c067f..2144292679 100644
--- a/src/components/views/right_panel/HeaderButtons.tsx
+++ b/src/components/views/right_panel/HeaderButtons.tsx
@@ -28,6 +28,7 @@ import {
     SetRightPanelPhaseRefireParams,
 } from '../../../dispatcher/payloads/SetRightPanelPhasePayload';
 import {EventSubscription} from "fbemitter";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 export enum HeaderKind {
   Room = "room",
@@ -41,6 +42,7 @@ interface IState {
 
 interface IProps {}
 
+@replaceableComponent("views.right_panel.HeaderButtons")
 export default abstract class HeaderButtons extends React.Component<IProps, IState> {
     private storeToken: EventSubscription;
     private dispatcherRef: string;
diff --git a/src/components/views/right_panel/RoomHeaderButtons.tsx b/src/components/views/right_panel/RoomHeaderButtons.tsx
index c2364546fd..0571622e64 100644
--- a/src/components/views/right_panel/RoomHeaderButtons.tsx
+++ b/src/components/views/right_panel/RoomHeaderButtons.tsx
@@ -26,6 +26,7 @@ import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
 import {Action} from "../../../dispatcher/actions";
 import {ActionPayload} from "../../../dispatcher/payloads";
 import RightPanelStore from "../../../stores/RightPanelStore";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const ROOM_INFO_PHASES = [
     RightPanelPhases.RoomSummary,
@@ -37,6 +38,7 @@ const ROOM_INFO_PHASES = [
     RightPanelPhases.Room3pidMemberInfo,
 ];
 
+@replaceableComponent("views.right_panel.RoomHeaderButtons")
 export default class RoomHeaderButtons extends HeaderButtons {
     constructor(props) {
         super(props, HeaderKind.Room);
diff --git a/src/components/views/right_panel/VerificationPanel.tsx b/src/components/views/right_panel/VerificationPanel.tsx
index f584a63209..ac01c953b9 100644
--- a/src/components/views/right_panel/VerificationPanel.tsx
+++ b/src/components/views/right_panel/VerificationPanel.tsx
@@ -36,6 +36,7 @@ import {
     PHASE_CANCELLED,
 } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
 import Spinner from "../elements/Spinner";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // XXX: Should be defined in matrix-js-sdk
 enum VerificationPhase {
@@ -65,6 +66,7 @@ interface IState {
     reciprocateQREvent?: ReciprocateQRCode;
 }
 
+@replaceableComponent("views.right_panel.VerificationPanel")
 export default class VerificationPanel extends React.PureComponent<IProps, IState> {
     private hasVerifier: boolean;
 
diff --git a/src/components/views/room_settings/AliasSettings.js b/src/components/views/room_settings/AliasSettings.js
index eb9276b729..ee8232ebd7 100644
--- a/src/components/views/room_settings/AliasSettings.js
+++ b/src/components/views/room_settings/AliasSettings.js
@@ -26,6 +26,7 @@ import ErrorDialog from "../dialogs/ErrorDialog";
 import AccessibleButton from "../elements/AccessibleButton";
 import Modal from "../../../Modal";
 import RoomPublishSetting from "./RoomPublishSetting";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 class EditableAliasesList extends EditableItemList {
     constructor(props) {
@@ -74,6 +75,7 @@ class EditableAliasesList extends EditableItemList {
     }
 }
 
+@replaceableComponent("views.room_settings.AliasSettings")
 export default class AliasSettings extends React.Component {
     static propTypes = {
         roomId: PropTypes.string.isRequired,
diff --git a/src/components/views/room_settings/RelatedGroupSettings.js b/src/components/views/room_settings/RelatedGroupSettings.js
index af3f58f9db..f82e238722 100644
--- a/src/components/views/room_settings/RelatedGroupSettings.js
+++ b/src/components/views/room_settings/RelatedGroupSettings.js
@@ -22,9 +22,11 @@ import { _t } from '../../../languageHandler';
 import Modal from '../../../Modal';
 import ErrorDialog from "../dialogs/ErrorDialog";
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const GROUP_ID_REGEX = /\+\S+:\S+/;
 
+@replaceableComponent("views.room_settings.RelatedGroupSettings")
 export default class RelatedGroupSettings extends React.Component {
     static propTypes = {
         roomId: PropTypes.string.isRequired,
diff --git a/src/components/views/room_settings/RoomProfileSettings.js b/src/components/views/room_settings/RoomProfileSettings.js
index 65acc802dc..563368384b 100644
--- a/src/components/views/room_settings/RoomProfileSettings.js
+++ b/src/components/views/room_settings/RoomProfileSettings.js
@@ -20,8 +20,10 @@ import {_t} from "../../../languageHandler";
 import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import Field from "../elements/Field";
 import * as sdk from "../../../index";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // TODO: Merge with ProfileSettings?
+@replaceableComponent("views.room_settings.RoomProfileSettings")
 export default class RoomProfileSettings extends React.Component {
     static propTypes = {
         roomId: PropTypes.string.isRequired,
diff --git a/src/components/views/room_settings/UrlPreviewSettings.js b/src/components/views/room_settings/UrlPreviewSettings.js
index 114e9b2894..7b04e296e5 100644
--- a/src/components/views/room_settings/UrlPreviewSettings.js
+++ b/src/components/views/room_settings/UrlPreviewSettings.js
@@ -26,8 +26,9 @@ import dis from "../../../dispatcher/dispatcher";
 import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import {Action} from "../../../dispatcher/actions";
 import {SettingLevel} from "../../../settings/SettingLevel";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
-
+@replaceableComponent("views.room_settings.UrlPreviewSettings")
 export default class UrlPreviewSettings extends React.Component {
     static propTypes = {
         room: PropTypes.object,
diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js
index aa7120bbe6..3ef8d71682 100644
--- a/src/components/views/rooms/AppsDrawer.js
+++ b/src/components/views/rooms/AppsDrawer.js
@@ -35,7 +35,9 @@ import PercentageDistributor from "../../../resizer/distributors/percentage";
 import {Container, WidgetLayoutStore} from "../../../stores/widgets/WidgetLayoutStore";
 import {clamp, percentageOf, percentageWithin} from "../../../utils/numbers";
 import {useStateCallback} from "../../../hooks/useStateCallback";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.rooms.AppsDrawer")
 export default class AppsDrawer extends React.Component {
     static propTypes = {
         userId: PropTypes.string.isRequired,
diff --git a/src/components/views/rooms/Autocomplete.tsx b/src/components/views/rooms/Autocomplete.tsx
index 15af75084a..a4dcba11a3 100644
--- a/src/components/views/rooms/Autocomplete.tsx
+++ b/src/components/views/rooms/Autocomplete.tsx
@@ -23,6 +23,7 @@ import {Room} from 'matrix-js-sdk/src/models/room';
 
 import SettingsStore from "../../../settings/SettingsStore";
 import Autocompleter from '../../../autocomplete/Autocompleter';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const COMPOSER_SELECTED = 0;
 
@@ -49,6 +50,7 @@ interface IState {
     forceComplete: boolean;
 }
 
+@replaceableComponent("views.rooms.Autocomplete")
 export default class Autocomplete extends React.PureComponent<IProps, IState> {
     autocompleter: Autocompleter;
     queryRequested: string;
diff --git a/src/components/views/rooms/AuxPanel.tsx b/src/components/views/rooms/AuxPanel.tsx
index d193b98ec1..3d431f7c67 100644
--- a/src/components/views/rooms/AuxPanel.tsx
+++ b/src/components/views/rooms/AuxPanel.tsx
@@ -27,6 +27,7 @@ import {UIFeature} from "../../../settings/UIFeature";
 import { ResizeNotifier } from "../../../utils/ResizeNotifier";
 import CallViewForRoom from '../voip/CallViewForRoom';
 import {objectHasDiff} from "../../../utils/objects";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     // js-sdk room object
@@ -58,6 +59,7 @@ interface IState {
     counters: Counter[],
 }
 
+@replaceableComponent("views.rooms.AuxPanel")
 export default class AuxPanel extends React.Component<IProps, IState> {
     static defaultProps = {
         showApps: true,
diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx
index 017ce77166..5ab2b82a32 100644
--- a/src/components/views/rooms/BasicMessageComposer.tsx
+++ b/src/components/views/rooms/BasicMessageComposer.tsx
@@ -46,6 +46,7 @@ import {IDiff} from "../../../editor/diff";
 import AutocompleteWrapperModel from "../../../editor/autocomplete";
 import DocumentPosition from "../../../editor/position";
 import {ICompletion} from "../../../autocomplete/Autocompleter";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // matches emoticons which follow the start of a line or whitespace
 const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');
@@ -105,6 +106,7 @@ interface IState {
     completionIndex?: number;
 }
 
+@replaceableComponent("views.rooms.BasicMessageEditor")
 export default class BasicMessageEditor extends React.Component<IProps, IState> {
     private editorRef = createRef<HTMLDivElement>();
     private autocompleteRef = createRef<Autocomplete>();
diff --git a/src/components/views/rooms/EditMessageComposer.js b/src/components/views/rooms/EditMessageComposer.js
index c59b3555b9..6ecb2bd549 100644
--- a/src/components/views/rooms/EditMessageComposer.js
+++ b/src/components/views/rooms/EditMessageComposer.js
@@ -34,6 +34,7 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import {Action} from "../../../dispatcher/actions";
 import SettingsStore from "../../../settings/SettingsStore";
 import CountlyAnalytics from "../../../CountlyAnalytics";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 function _isReply(mxEvent) {
     const relatesTo = mxEvent.getContent()["m.relates_to"];
@@ -102,6 +103,7 @@ function createEditContent(model, editedEvent) {
     }, contentBody);
 }
 
+@replaceableComponent("views.rooms.EditMessageComposer")
 export default class EditMessageComposer extends React.Component {
     static propTypes = {
         // the message event being edited
diff --git a/src/components/views/rooms/EntityTile.js b/src/components/views/rooms/EntityTile.js
index 9017e4aa3e..75b03739b9 100644
--- a/src/components/views/rooms/EntityTile.js
+++ b/src/components/views/rooms/EntityTile.js
@@ -23,6 +23,7 @@ import AccessibleButton from '../elements/AccessibleButton';
 import { _t } from '../../../languageHandler';
 import classNames from "classnames";
 import E2EIcon from './E2EIcon';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const PRESENCE_CLASS = {
     "offline": "mx_EntityTile_offline",
@@ -50,6 +51,7 @@ function presenceClassForMember(presenceState, lastActiveAgo, showPresence) {
     }
 }
 
+@replaceableComponent("views.rooms.EntityTile")
 class EntityTile extends React.Component {
     static propTypes = {
         name: PropTypes.string,
diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js
index a705e92d9c..1366d9b603 100644
--- a/src/components/views/rooms/EventTile.js
+++ b/src/components/views/rooms/EventTile.js
@@ -39,6 +39,7 @@ import {WidgetType} from "../../../widgets/WidgetType";
 import RoomAvatar from "../avatars/RoomAvatar";
 import {WIDGET_LAYOUT_EVENT_TYPE} from "../../../stores/widgets/WidgetLayoutStore";
 import {objectHasDiff} from "../../../utils/objects";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const eventTileTypes = {
     'm.room.message': 'messages.MessageEvent',
@@ -146,6 +147,7 @@ const MAX_READ_AVATARS = 5;
 // |    '--------------------------------------'              |
 // '----------------------------------------------------------'
 
+@replaceableComponent("views.rooms.EventTile")
 export default class EventTile extends React.Component {
     static propTypes = {
         /* the MatrixEvent to show */
diff --git a/src/components/views/rooms/ForwardMessage.js b/src/components/views/rooms/ForwardMessage.js
index b85dd2c8df..dd894c0dcf 100644
--- a/src/components/views/rooms/ForwardMessage.js
+++ b/src/components/views/rooms/ForwardMessage.js
@@ -19,8 +19,9 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { _t } from '../../../languageHandler';
 import {Key} from '../../../Keyboard';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
-
+@replaceableComponent("views.rooms.ForwardMessage")
 export default class ForwardMessage extends React.Component {
     static propTypes = {
         onCancelClick: PropTypes.func.isRequired,
diff --git a/src/components/views/rooms/LinkPreviewWidget.js b/src/components/views/rooms/LinkPreviewWidget.js
index 2a053bf467..39c9f0bcf7 100644
--- a/src/components/views/rooms/LinkPreviewWidget.js
+++ b/src/components/views/rooms/LinkPreviewWidget.js
@@ -25,7 +25,9 @@ import * as sdk from "../../../index";
 import Modal from "../../../Modal";
 import * as ImageUtils from "../../../ImageUtils";
 import { _t } from "../../../languageHandler";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.rooms.LinkPreviewWidget")
 export default class LinkPreviewWidget extends React.Component {
     static propTypes = {
         link: PropTypes.string.isRequired, // the URL being previewed
diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js
index d4d618c821..593132a283 100644
--- a/src/components/views/rooms/MemberList.js
+++ b/src/components/views/rooms/MemberList.js
@@ -29,6 +29,7 @@ import BaseCard from "../right_panel/BaseCard";
 import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
 import RoomAvatar from "../avatars/RoomAvatar";
 import RoomName from "../elements/RoomName";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const INITIAL_LOAD_NUM_MEMBERS = 30;
 const INITIAL_LOAD_NUM_INVITED = 5;
@@ -38,6 +39,7 @@ const SHOW_MORE_INCREMENT = 100;
 // matches all ASCII punctuation: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
 const SORT_REGEX = /[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]+/g;
 
+@replaceableComponent("views.rooms.MemberList")
 export default class MemberList extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/rooms/MemberTile.js b/src/components/views/rooms/MemberTile.js
index a43b42b6d3..f8df7ed78f 100644
--- a/src/components/views/rooms/MemberTile.js
+++ b/src/components/views/rooms/MemberTile.js
@@ -23,7 +23,9 @@ import dis from "../../../dispatcher/dispatcher";
 import { _t } from '../../../languageHandler';
 import { MatrixClientPeg } from "../../../MatrixClientPeg";
 import {Action} from "../../../dispatcher/actions";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.rooms.MemberTile")
 export default class MemberTile extends React.Component {
     static propTypes = {
         member: PropTypes.any.isRequired, // RoomMember
diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index c03178cdf7..ccf097c4fd 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -32,6 +32,7 @@ import {UIFeature} from "../../../settings/UIFeature";
 import WidgetStore from "../../../stores/WidgetStore";
 import {UPDATE_EVENT} from "../../../stores/AsyncStore";
 import ActiveWidgetStore from "../../../stores/ActiveWidgetStore";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 function ComposerAvatar(props) {
     const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
@@ -168,6 +169,7 @@ class UploadButton extends React.Component {
     }
 }
 
+@replaceableComponent("views.rooms.MessageComposer")
 export default class MessageComposer extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/rooms/MessageComposerFormatBar.js b/src/components/views/rooms/MessageComposerFormatBar.js
index 71aef1e833..d2539b1ef4 100644
--- a/src/components/views/rooms/MessageComposerFormatBar.js
+++ b/src/components/views/rooms/MessageComposerFormatBar.js
@@ -19,7 +19,9 @@ import PropTypes from 'prop-types';
 import { _t } from '../../../languageHandler';
 import classNames from 'classnames';
 import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.rooms.MessageComposerFormatBar")
 export default class MessageComposerFormatBar extends React.PureComponent {
     static propTypes = {
         onAction: PropTypes.func.isRequired,
diff --git a/src/components/views/rooms/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge.tsx
index 8b996d3238..36a52e260d 100644
--- a/src/components/views/rooms/NotificationBadge.tsx
+++ b/src/components/views/rooms/NotificationBadge.tsx
@@ -21,6 +21,7 @@ import SettingsStore from "../../../settings/SettingsStore";
 import AccessibleButton from "../elements/AccessibleButton";
 import { XOR } from "../../../@types/common";
 import { NOTIFICATION_STATE_UPDATE, NotificationState } from "../../../stores/notifications/NotificationState";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     notification: NotificationState;
@@ -48,6 +49,7 @@ interface IState {
     showCounts: boolean; // whether or not to show counts. Independent of props.forceCount
 }
 
+@replaceableComponent("views.rooms.NotificationBadge")
 export default class NotificationBadge extends React.PureComponent<XOR<IProps, IClickableProps>, IState> {
     private countWatcherRef: string;
 
diff --git a/src/components/views/rooms/PinnedEventTile.js b/src/components/views/rooms/PinnedEventTile.js
index 9fad0c2391..2259cad7fb 100644
--- a/src/components/views/rooms/PinnedEventTile.js
+++ b/src/components/views/rooms/PinnedEventTile.js
@@ -23,7 +23,9 @@ import MessageEvent from "../messages/MessageEvent";
 import MemberAvatar from "../avatars/MemberAvatar";
 import { _t } from '../../../languageHandler';
 import {formatFullDate} from '../../../DateUtils';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.rooms.PinnedEventTile")
 export default class PinnedEventTile extends React.Component {
     static propTypes = {
         mxRoom: PropTypes.object.isRequired,
diff --git a/src/components/views/rooms/PinnedEventsPanel.js b/src/components/views/rooms/PinnedEventsPanel.js
index 3ea0299976..285829bf63 100644
--- a/src/components/views/rooms/PinnedEventsPanel.js
+++ b/src/components/views/rooms/PinnedEventsPanel.js
@@ -22,7 +22,9 @@ import AccessibleButton from "../elements/AccessibleButton";
 import PinnedEventTile from "./PinnedEventTile";
 import { _t } from '../../../languageHandler';
 import PinningUtils from "../../../utils/PinningUtils";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.rooms.PinnedEventsPanel")
 export default class PinnedEventsPanel extends React.Component {
     static propTypes = {
         // The Room from the js-sdk we're going to show pinned events for
diff --git a/src/components/views/rooms/PresenceLabel.js b/src/components/views/rooms/PresenceLabel.js
index ff1460ca21..ca21afe63d 100644
--- a/src/components/views/rooms/PresenceLabel.js
+++ b/src/components/views/rooms/PresenceLabel.js
@@ -18,8 +18,9 @@ import React from 'react';
 import PropTypes from 'prop-types';
 
 import { _t } from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
-
+@replaceableComponent("views.rooms.PresenceLabel")
 export default class PresenceLabel extends React.Component {
     static propTypes = {
         // number of milliseconds ago this user was last active.
diff --git a/src/components/views/rooms/ReadReceiptMarker.js b/src/components/views/rooms/ReadReceiptMarker.js
index ba2b3064fd..ade84cbef3 100644
--- a/src/components/views/rooms/ReadReceiptMarker.js
+++ b/src/components/views/rooms/ReadReceiptMarker.js
@@ -23,6 +23,7 @@ import {formatDate} from '../../../DateUtils';
 import Velociraptor from "../../../Velociraptor";
 import * as sdk from "../../../index";
 import {toPx} from "../../../utils/units";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 let bounce = false;
 try {
@@ -32,6 +33,7 @@ try {
 } catch (e) {
 }
 
+@replaceableComponent("views.rooms.ReadReceiptMarker")
 export default class ReadReceiptMarker extends React.PureComponent {
     static propTypes = {
         // the RoomMember to show the RR for
diff --git a/src/components/views/rooms/ReplyPreview.js b/src/components/views/rooms/ReplyPreview.js
index c7872d95ed..0d99be4f53 100644
--- a/src/components/views/rooms/ReplyPreview.js
+++ b/src/components/views/rooms/ReplyPreview.js
@@ -23,6 +23,7 @@ import SettingsStore from "../../../settings/SettingsStore";
 import PropTypes from "prop-types";
 import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
 import {UIFeature} from "../../../settings/UIFeature";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 function cancelQuoting() {
     dis.dispatch({
@@ -31,6 +32,7 @@ function cancelQuoting() {
     });
 }
 
+@replaceableComponent("views.rooms.ReplyPreview")
 export default class ReplyPreview extends React.Component {
     static propTypes = {
         permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired,
diff --git a/src/components/views/rooms/RoomBreadcrumbs.tsx b/src/components/views/rooms/RoomBreadcrumbs.tsx
index ff60ab7779..ea0ff233da 100644
--- a/src/components/views/rooms/RoomBreadcrumbs.tsx
+++ b/src/components/views/rooms/RoomBreadcrumbs.tsx
@@ -27,6 +27,7 @@ import RoomListStore from "../../../stores/room-list/RoomListStore";
 import { DefaultTagID } from "../../../stores/room-list/models";
 import { RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex";
 import Toolbar from "../../../accessibility/Toolbar";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
 }
@@ -42,6 +43,7 @@ interface IState {
     skipFirst: boolean;
 }
 
+@replaceableComponent("views.rooms.RoomBreadcrumbs")
 export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState> {
     private isMounted = true;
 
diff --git a/src/components/views/rooms/RoomDetailList.js b/src/components/views/rooms/RoomDetailList.js
index d8205aeb21..be22cda199 100644
--- a/src/components/views/rooms/RoomDetailList.js
+++ b/src/components/views/rooms/RoomDetailList.js
@@ -22,7 +22,9 @@ import PropTypes from 'prop-types';
 import classNames from 'classnames';
 
 import {roomShape} from './RoomDetailRow';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.rooms.RoomDetailList")
 export default class RoomDetailList extends React.Component {
     static propTypes = {
         rooms: PropTypes.arrayOf(roomShape),
diff --git a/src/components/views/rooms/RoomDetailRow.js b/src/components/views/rooms/RoomDetailRow.js
index 667f821922..e7c259cd98 100644
--- a/src/components/views/rooms/RoomDetailRow.js
+++ b/src/components/views/rooms/RoomDetailRow.js
@@ -21,6 +21,7 @@ import { linkifyElement } from '../../../HtmlUtils';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import PropTypes from 'prop-types';
 import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 export function getDisplayAliasForRoom(room) {
     return room.canonicalAlias || (room.aliases ? room.aliases[0] : "");
@@ -39,6 +40,7 @@ export const roomShape = PropTypes.shape({
     guestCanJoin: PropTypes.bool,
 });
 
+@replaceableComponent("views.rooms.RoomDetailRow")
 export default class RoomDetailRow extends React.Component {
     static propTypes = {
         room: roomShape,
diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js
index 6736600bc8..f856f7f6ef 100644
--- a/src/components/views/rooms/RoomHeader.js
+++ b/src/components/views/rooms/RoomHeader.js
@@ -32,7 +32,9 @@ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
 import RoomTopic from "../elements/RoomTopic";
 import RoomName from "../elements/RoomName";
 import {PlaceCallType} from "../../../CallHandler";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.rooms.RoomHeader")
 export default class RoomHeader extends React.Component {
     static propTypes = {
         room: PropTypes.object,
diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx
index f7da6571da..ff6e3793bf 100644
--- a/src/components/views/rooms/RoomList.tsx
+++ b/src/components/views/rooms/RoomList.tsx
@@ -50,6 +50,7 @@ import CallHandler from "../../../CallHandler";
 import SpaceStore from "../../../stores/SpaceStore";
 import { showAddExistingRooms, showCreateNewRoom } from "../../../utils/space";
 import { EventType } from "matrix-js-sdk/src/@types/event";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     onKeyDown: (ev: React.KeyboardEvent) => void;
@@ -256,6 +257,7 @@ function customTagAesthetics(tagId: TagID): ITagAesthetics {
     };
 }
 
+@replaceableComponent("views.rooms.RoomList")
 export default class RoomList extends React.PureComponent<IProps, IState> {
     private dispatcherRef;
     private customTagStoreRef;
diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js
index dc68068157..36038da61c 100644
--- a/src/components/views/rooms/RoomPreviewBar.js
+++ b/src/components/views/rooms/RoomPreviewBar.js
@@ -27,6 +27,7 @@ import SdkConfig from "../../../SdkConfig";
 import IdentityAuthClient from '../../../IdentityAuthClient';
 import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
 import {UPDATE_EVENT} from "../../../stores/AsyncStore";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const MessageCase = Object.freeze({
     NotLoggedIn: "NotLoggedIn",
@@ -45,6 +46,7 @@ const MessageCase = Object.freeze({
     OtherError: "OtherError",
 });
 
+@replaceableComponent("views.rooms.RoomPreviewBar")
 export default class RoomPreviewBar extends React.Component {
     static propTypes = {
         onJoinClick: PropTypes.func,
diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx
index a2574bf60c..cb98ba85e4 100644
--- a/src/components/views/rooms/RoomSublist.tsx
+++ b/src/components/views/rooms/RoomSublist.tsx
@@ -51,6 +51,7 @@ import { objectExcluding, objectHasDiff } from "../../../utils/objects";
 import TemporaryTile from "./TemporaryTile";
 import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
 import IconizedContextMenu from "../context_menus/IconizedContextMenu";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const SHOW_N_BUTTON_HEIGHT = 28; // As defined by CSS
 const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS
@@ -98,6 +99,7 @@ interface IState {
     filteredExtraTiles?: TemporaryTile[];
 }
 
+@replaceableComponent("views.rooms.RoomSublist")
 export default class RoomSublist extends React.Component<IProps, IState> {
     private headerButton = createRef<HTMLDivElement>();
     private sublistRef = createRef<HTMLDivElement>();
diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx
index 835447dc18..07de70fe45 100644
--- a/src/components/views/rooms/RoomTile.tsx
+++ b/src/components/views/rooms/RoomTile.tsx
@@ -51,6 +51,7 @@ import IconizedContextMenu, {
     IconizedContextMenuRadio,
 } from "../context_menus/IconizedContextMenu";
 import { CommunityPrototypeStore, IRoomProfile } from "../../../stores/CommunityPrototypeStore";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     room: Room;
@@ -78,6 +79,7 @@ const contextMenuBelow = (elementRect: PartialDOMRect) => {
     return {left, top, chevronFace};
 };
 
+@replaceableComponent("views.rooms.RoomTile")
 export default class RoomTile extends React.PureComponent<IProps, IState> {
     private dispatcherRef: string;
     private roomTileRef = createRef<HTMLDivElement>();
diff --git a/src/components/views/rooms/RoomUpgradeWarningBar.js b/src/components/views/rooms/RoomUpgradeWarningBar.js
index 877cfb39d7..a2d4f92d35 100644
--- a/src/components/views/rooms/RoomUpgradeWarningBar.js
+++ b/src/components/views/rooms/RoomUpgradeWarningBar.js
@@ -21,7 +21,9 @@ import Modal from '../../../Modal';
 
 import { _t } from '../../../languageHandler';
 import {MatrixClientPeg} from "../../../MatrixClientPeg";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.rooms.RoomUpgradeWarningBar")
 export default class RoomUpgradeWarningBar extends React.Component {
     static propTypes = {
         room: PropTypes.object.isRequired,
diff --git a/src/components/views/rooms/SearchBar.js b/src/components/views/rooms/SearchBar.js
index ac637673e4..029516c932 100644
--- a/src/components/views/rooms/SearchBar.js
+++ b/src/components/views/rooms/SearchBar.js
@@ -21,7 +21,9 @@ import classNames from "classnames";
 import { _t } from '../../../languageHandler';
 import {Key} from "../../../Keyboard";
 import DesktopBuildsNotice, {WarningKind} from "../elements/DesktopBuildsNotice";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.rooms.SearchBar")
 export default class SearchBar extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/rooms/SearchResultTile.js b/src/components/views/rooms/SearchResultTile.js
index 29def9e368..dcfd633e76 100644
--- a/src/components/views/rooms/SearchResultTile.js
+++ b/src/components/views/rooms/SearchResultTile.js
@@ -21,7 +21,9 @@ import * as sdk from '../../../index';
 import {haveTileForEvent} from "./EventTile";
 import SettingsStore from "../../../settings/SettingsStore";
 import {UIFeature} from "../../../settings/UIFeature";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.rooms.SearchResultTile")
 export default class SearchResultTile extends React.Component {
     static propTypes = {
         // a matrix-js-sdk SearchResult containing the details of this result
diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js
index 673df949f7..ba3076c07d 100644
--- a/src/components/views/rooms/SendMessageComposer.js
+++ b/src/components/views/rooms/SendMessageComposer.js
@@ -48,6 +48,7 @@ import SettingsStore from "../../../settings/SettingsStore";
 import CountlyAnalytics from "../../../CountlyAnalytics";
 import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import EMOJI_REGEX from 'emojibase-regex';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
     const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent);
@@ -111,6 +112,7 @@ export function isQuickReaction(model) {
     return false;
 }
 
+@replaceableComponent("views.rooms.SendMessageComposer")
 export default class SendMessageComposer extends React.Component {
     static propTypes = {
         room: PropTypes.object.isRequired,
diff --git a/src/components/views/rooms/SimpleRoomHeader.js b/src/components/views/rooms/SimpleRoomHeader.js
index 1c78253eff..b2a66f6670 100644
--- a/src/components/views/rooms/SimpleRoomHeader.js
+++ b/src/components/views/rooms/SimpleRoomHeader.js
@@ -19,6 +19,7 @@ import PropTypes from 'prop-types';
 import AccessibleButton from '../elements/AccessibleButton';
 import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // cancel button which is shared between room header and simple room header
 export function CancelButton(props) {
@@ -36,6 +37,7 @@ export function CancelButton(props) {
  * A stripped-down room header used for things like the user settings
  * and room directory.
  */
+@replaceableComponent("views.rooms.SimpleRoomHeader")
 export default class SimpleRoomHeader extends React.Component {
     static propTypes = {
         title: PropTypes.string,
diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js
index 5446d15671..44d31d7146 100644
--- a/src/components/views/rooms/Stickerpicker.js
+++ b/src/components/views/rooms/Stickerpicker.js
@@ -30,6 +30,7 @@ import {WidgetType} from "../../../widgets/WidgetType";
 import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
 import {Action} from "../../../dispatcher/actions";
 import {WidgetMessagingStore} from "../../../stores/widgets/WidgetMessagingStore";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // This should be below the dialog level (4000), but above the rest of the UI (1000-2000).
 // We sit in a context menu, so this should be given to the context menu.
@@ -38,6 +39,7 @@ const STICKERPICKER_Z_INDEX = 3500;
 // Key to store the widget's AppTile under in PersistedElement
 const PERSISTED_ELEMENT_KEY = "stickerPicker";
 
+@replaceableComponent("views.rooms.Stickerpicker")
 export default class Stickerpicker extends React.Component {
     static currentWidget;
 
diff --git a/src/components/views/rooms/TemporaryTile.tsx b/src/components/views/rooms/TemporaryTile.tsx
index eec3105880..a9765faa5d 100644
--- a/src/components/views/rooms/TemporaryTile.tsx
+++ b/src/components/views/rooms/TemporaryTile.tsx
@@ -22,6 +22,7 @@ import {
 } from "../../../accessibility/RovingTabIndex";
 import NotificationBadge from "./NotificationBadge";
 import { NotificationState } from "../../../stores/notifications/NotificationState";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     isMinimized: boolean;
@@ -37,6 +38,7 @@ interface IState {
 }
 
 // TODO: Remove with community invites in the room list: https://github.com/vector-im/element-web/issues/14456
+@replaceableComponent("views.rooms.TemporaryTile")
 export default class TemporaryTile extends React.Component<IProps, IState> {
     constructor(props: IProps) {
         super(props);
diff --git a/src/components/views/rooms/ThirdPartyMemberInfo.js b/src/components/views/rooms/ThirdPartyMemberInfo.js
index 73510c2b4f..5e2d82a1b2 100644
--- a/src/components/views/rooms/ThirdPartyMemberInfo.js
+++ b/src/components/views/rooms/ThirdPartyMemberInfo.js
@@ -25,7 +25,9 @@ import Modal from "../../../Modal";
 import {isValid3pidInvite} from "../../../RoomInvite";
 import RoomAvatar from "../avatars/RoomAvatar";
 import RoomName from "../elements/RoomName";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.rooms.ThirdPartyMemberInfo")
 export default class ThirdPartyMemberInfo extends React.Component {
     static propTypes = {
         event: PropTypes.instanceOf(MatrixEvent).isRequired,
diff --git a/src/components/views/rooms/TopUnreadMessagesBar.js b/src/components/views/rooms/TopUnreadMessagesBar.js
index 9ac3c49ef4..cba99ac913 100644
--- a/src/components/views/rooms/TopUnreadMessagesBar.js
+++ b/src/components/views/rooms/TopUnreadMessagesBar.js
@@ -20,7 +20,9 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { _t } from '../../../languageHandler';
 import AccessibleButton from '../elements/AccessibleButton';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.rooms.TopUnreadMessagesBar")
 export default class TopUnreadMessagesBar extends React.Component {
     static propTypes = {
         onScrollUpClick: PropTypes.func,
diff --git a/src/components/views/rooms/WhoIsTypingTile.js b/src/components/views/rooms/WhoIsTypingTile.js
index 905cbe6d09..a25b43fc3a 100644
--- a/src/components/views/rooms/WhoIsTypingTile.js
+++ b/src/components/views/rooms/WhoIsTypingTile.js
@@ -21,7 +21,9 @@ import * as WhoIsTyping from '../../../WhoIsTyping';
 import Timer from '../../../utils/Timer';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import MemberAvatar from '../avatars/MemberAvatar';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.rooms.WhoIsTypingTile")
 export default class WhoIsTypingTile extends React.Component {
     static propTypes = {
         // the room this statusbar is representing.
diff --git a/src/components/views/settings/BridgeTile.tsx b/src/components/views/settings/BridgeTile.tsx
index 58499ebd25..b33219ad4a 100644
--- a/src/components/views/settings/BridgeTile.tsx
+++ b/src/components/views/settings/BridgeTile.tsx
@@ -26,6 +26,7 @@ import SettingsStore from "../../../settings/SettingsStore";
 import {MatrixEvent} from "matrix-js-sdk/src/models/event";
 import { Room } from "matrix-js-sdk/src/models/room";
 import { isUrlPermitted } from '../../../HtmlUtils';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     ev: MatrixEvent;
@@ -64,6 +65,7 @@ interface IBridgeStateEvent {
     };
 }
 
+@replaceableComponent("views.settings.BridgeTile")
 export default class BridgeTile extends React.PureComponent<IProps> {
     static propTypes = {
         ev: PropTypes.object.isRequired,
diff --git a/src/components/views/settings/ChangeAvatar.js b/src/components/views/settings/ChangeAvatar.js
index 7ab2936584..8067046ffd 100644
--- a/src/components/views/settings/ChangeAvatar.js
+++ b/src/components/views/settings/ChangeAvatar.js
@@ -20,7 +20,9 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
 import Spinner from '../elements/Spinner';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.ChangeAvatar")
 export default class ChangeAvatar extends React.Component {
     static propTypes = {
         initialAvatarUrl: PropTypes.string,
diff --git a/src/components/views/settings/ChangeDisplayName.js b/src/components/views/settings/ChangeDisplayName.js
index 538e52d0ca..cae4a22be9 100644
--- a/src/components/views/settings/ChangeDisplayName.js
+++ b/src/components/views/settings/ChangeDisplayName.js
@@ -20,7 +20,9 @@ import React from 'react';
 import * as sdk from '../../../index';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import { _t } from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.ChangeDisplayName")
 export default class ChangeDisplayName extends React.Component {
     _getDisplayName = async () => {
         const cli = MatrixClientPeg.get();
diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js
index 22b758b1ca..aa635ef974 100644
--- a/src/components/views/settings/ChangePassword.js
+++ b/src/components/views/settings/ChangePassword.js
@@ -27,6 +27,7 @@ import * as sdk from "../../../index";
 import Modal from "../../../Modal";
 import PassphraseField from "../auth/PassphraseField";
 import CountlyAnalytics from "../../../CountlyAnalytics";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const FIELD_OLD_PASSWORD = 'field_old_password';
 const FIELD_NEW_PASSWORD = 'field_new_password';
@@ -34,6 +35,7 @@ const FIELD_NEW_PASSWORD_CONFIRM = 'field_new_password_confirm';
 
 const PASSWORD_MIN_SCORE = 3; // safely unguessable: moderate protection from offline slow-hash scenario.
 
+@replaceableComponent("views.settings.ChangePassword")
 export default class ChangePassword extends React.Component {
     static propTypes = {
         onFinished: PropTypes.func,
diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js
index 1c548bd9d8..e5f57d1af2 100644
--- a/src/components/views/settings/CrossSigningPanel.js
+++ b/src/components/views/settings/CrossSigningPanel.js
@@ -23,7 +23,9 @@ import Modal from '../../../Modal';
 import Spinner from '../elements/Spinner';
 import InteractiveAuthDialog from '../dialogs/InteractiveAuthDialog';
 import ConfirmDestroyCrossSigningDialog from '../dialogs/security/ConfirmDestroyCrossSigningDialog';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.CrossSigningPanel")
 export default class CrossSigningPanel extends React.PureComponent {
     constructor(props) {
         super(props);
diff --git a/src/components/views/settings/DevicesPanel.js b/src/components/views/settings/DevicesPanel.js
index dc3ce9e03d..e7d300b0f8 100644
--- a/src/components/views/settings/DevicesPanel.js
+++ b/src/components/views/settings/DevicesPanel.js
@@ -24,7 +24,9 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import { _t } from '../../../languageHandler';
 import Modal from '../../../Modal';
 import {SSOAuthEntry} from "../auth/InteractiveAuthEntryComponents";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.DevicesPanel")
 export default class DevicesPanel extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/settings/DevicesPanelEntry.js b/src/components/views/settings/DevicesPanelEntry.js
index 567b144a92..93d4c78476 100644
--- a/src/components/views/settings/DevicesPanelEntry.js
+++ b/src/components/views/settings/DevicesPanelEntry.js
@@ -22,7 +22,9 @@ import { _t } from '../../../languageHandler';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import {formatDate} from '../../../DateUtils';
 import StyledCheckbox from '../elements/StyledCheckbox';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.DevicesPanelEntry")
 export default class DevicesPanelEntry extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/settings/EventIndexPanel.js b/src/components/views/settings/EventIndexPanel.js
index ec6ccacc9a..d78b99fc5d 100644
--- a/src/components/views/settings/EventIndexPanel.js
+++ b/src/components/views/settings/EventIndexPanel.js
@@ -25,7 +25,9 @@ import AccessibleButton from "../elements/AccessibleButton";
 import {formatBytes, formatCountLong} from "../../../utils/FormattingUtils";
 import EventIndexPeg from "../../../indexing/EventIndexPeg";
 import {SettingLevel} from "../../../settings/SettingLevel";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.EventIndexPanel")
 export default class EventIndexPanel extends React.Component {
     constructor() {
         super();
diff --git a/src/components/views/settings/IntegrationManager.js b/src/components/views/settings/IntegrationManager.js
index da11832cf5..b058625139 100644
--- a/src/components/views/settings/IntegrationManager.js
+++ b/src/components/views/settings/IntegrationManager.js
@@ -21,7 +21,9 @@ import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
 import dis from '../../../dispatcher/dispatcher';
 import {Key} from "../../../Keyboard";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.IntegrationManager")
 export default class IntegrationManager extends React.Component {
     static propTypes = {
         // false to display an error saying that we couldn't connect to the integration manager
diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js
index 1337991dc3..25fe434994 100644
--- a/src/components/views/settings/Notifications.js
+++ b/src/components/views/settings/Notifications.js
@@ -32,6 +32,7 @@ import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
 import AccessibleButton from "../elements/AccessibleButton";
 import {SettingLevel} from "../../../settings/SettingLevel";
 import {UIFeature} from "../../../settings/UIFeature";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // TODO: this "view" component still has far too much application logic in it,
 // which should be factored out to other files.
@@ -65,6 +66,7 @@ function portLegacyActions(actions) {
     }
 }
 
+@replaceableComponent("views.settings.Notifications")
 export default class Notifications extends React.Component {
     static phases = {
         LOADING: "LOADING", // The component is loading or sending data to the hs
diff --git a/src/components/views/settings/ProfileSettings.js b/src/components/views/settings/ProfileSettings.js
index 89d7cf6c2b..30dcdc3c47 100644
--- a/src/components/views/settings/ProfileSettings.js
+++ b/src/components/views/settings/ProfileSettings.js
@@ -23,7 +23,9 @@ import * as sdk from "../../../index";
 import {OwnProfileStore} from "../../../stores/OwnProfileStore";
 import Modal from "../../../Modal";
 import ErrorDialog from "../dialogs/ErrorDialog";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.ProfileSettings")
 export default class ProfileSettings extends React.Component {
     constructor() {
         super();
diff --git a/src/components/views/settings/SecureBackupPanel.js b/src/components/views/settings/SecureBackupPanel.js
index 080d83b2cf..310114c8af 100644
--- a/src/components/views/settings/SecureBackupPanel.js
+++ b/src/components/views/settings/SecureBackupPanel.js
@@ -26,7 +26,9 @@ import AccessibleButton from '../elements/AccessibleButton';
 import QuestionDialog from '../dialogs/QuestionDialog';
 import RestoreKeyBackupDialog from '../dialogs/security/RestoreKeyBackupDialog';
 import { accessSecretStorage } from '../../../SecurityManager';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.SecureBackupPanel")
 export default class SecureBackupPanel extends React.PureComponent {
     constructor(props) {
         super(props);
diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js
index e05fe4f1c3..fa2a36476d 100644
--- a/src/components/views/settings/SetIdServer.js
+++ b/src/components/views/settings/SetIdServer.js
@@ -27,6 +27,7 @@ import IdentityAuthClient from "../../../IdentityAuthClient";
 import {abbreviateUrl, unabbreviateUrl} from "../../../utils/UrlUtils";
 import { getDefaultIdentityServerUrl, doesIdentityServerHaveTerms } from '../../../utils/IdentityServerUtils';
 import {timeout} from "../../../utils/promise";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // We'll wait up to this long when checking for 3PID bindings on the IS.
 const REACHABILITY_TIMEOUT = 10000; // ms
@@ -58,6 +59,7 @@ async function checkIdentityServerUrl(u) {
     }
 }
 
+@replaceableComponent("views.settings.SetIdServer")
 export default class SetIdServer extends React.Component {
     static propTypes = {
         // Whether or not the ID server is missing terms. This affects the text
diff --git a/src/components/views/settings/SetIntegrationManager.js b/src/components/views/settings/SetIntegrationManager.js
index e6fb3f6e1c..29cc5d7131 100644
--- a/src/components/views/settings/SetIntegrationManager.js
+++ b/src/components/views/settings/SetIntegrationManager.js
@@ -20,7 +20,9 @@ import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
 import * as sdk from '../../../index';
 import SettingsStore from "../../../settings/SettingsStore";
 import {SettingLevel} from "../../../settings/SettingLevel";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.SetIntegrationManager")
 export default class SetIntegrationManager extends React.Component {
     constructor() {
         super();
diff --git a/src/components/views/settings/SpellCheckSettings.tsx b/src/components/views/settings/SpellCheckSettings.tsx
index d08f263b5f..e5455c8c68 100644
--- a/src/components/views/settings/SpellCheckSettings.tsx
+++ b/src/components/views/settings/SpellCheckSettings.tsx
@@ -18,6 +18,7 @@ import React from 'react';
 import SpellCheckLanguagesDropdown from "../../../components/views/elements/SpellCheckLanguagesDropdown";
 import AccessibleButton from "../../../components/views/elements/AccessibleButton";
 import {_t} from "../../../languageHandler";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface ExistingSpellCheckLanguageIProps {
     language: string,
@@ -53,6 +54,7 @@ export class ExistingSpellCheckLanguage extends React.Component<ExistingSpellChe
     }
 }
 
+@replaceableComponent("views.settings.SpellCheckLanguages")
 export default class SpellCheckLanguages extends React.Component<SpellCheckLanguagesIProps, SpellCheckLanguagesIState> {
     constructor(props) {
         super(props);
diff --git a/src/components/views/settings/account/EmailAddresses.js b/src/components/views/settings/account/EmailAddresses.js
index a8de7693a9..1ebd374173 100644
--- a/src/components/views/settings/account/EmailAddresses.js
+++ b/src/components/views/settings/account/EmailAddresses.js
@@ -25,6 +25,7 @@ import * as Email from "../../../../email";
 import AddThreepid from "../../../../AddThreepid";
 import * as sdk from '../../../../index';
 import Modal from '../../../../Modal';
+import {replaceableComponent} from "../../../../utils/replaceableComponent";
 
 /*
 TODO: Improve the UX for everything in here.
@@ -112,6 +113,7 @@ export class ExistingEmailAddress extends React.Component {
     }
 }
 
+@replaceableComponent("views.settings.account.EmailAddresses")
 export default class EmailAddresses extends React.Component {
     static propTypes = {
         emails: PropTypes.array.isRequired,
diff --git a/src/components/views/settings/account/PhoneNumbers.js b/src/components/views/settings/account/PhoneNumbers.js
index df54b5ca1f..5725fdb909 100644
--- a/src/components/views/settings/account/PhoneNumbers.js
+++ b/src/components/views/settings/account/PhoneNumbers.js
@@ -25,6 +25,7 @@ import AddThreepid from "../../../../AddThreepid";
 import CountryDropdown from "../../auth/CountryDropdown";
 import * as sdk from '../../../../index';
 import Modal from '../../../../Modal';
+import {replaceableComponent} from "../../../../utils/replaceableComponent";
 
 /*
 TODO: Improve the UX for everything in here.
@@ -107,6 +108,7 @@ export class ExistingPhoneNumber extends React.Component {
     }
 }
 
+@replaceableComponent("views.settings.account.PhoneNumbers")
 export default class PhoneNumbers extends React.Component {
     static propTypes = {
         msisdns: PropTypes.array.isRequired,
diff --git a/src/components/views/settings/discovery/EmailAddresses.js b/src/components/views/settings/discovery/EmailAddresses.js
index f9a1ba1818..0493597537 100644
--- a/src/components/views/settings/discovery/EmailAddresses.js
+++ b/src/components/views/settings/discovery/EmailAddresses.js
@@ -23,6 +23,7 @@ import {MatrixClientPeg} from "../../../../MatrixClientPeg";
 import * as sdk from '../../../../index';
 import Modal from '../../../../Modal';
 import AddThreepid from '../../../../AddThreepid';
+import {replaceableComponent} from "../../../../utils/replaceableComponent";
 
 /*
 TODO: Improve the UX for everything in here.
@@ -233,6 +234,7 @@ export class EmailAddress extends React.Component {
     }
 }
 
+@replaceableComponent("views.settings.discovery.EmailAddresses")
 export default class EmailAddresses extends React.Component {
     static propTypes = {
         emails: PropTypes.array.isRequired,
diff --git a/src/components/views/settings/discovery/PhoneNumbers.js b/src/components/views/settings/discovery/PhoneNumbers.js
index 03f459ee15..5cbcdfe47e 100644
--- a/src/components/views/settings/discovery/PhoneNumbers.js
+++ b/src/components/views/settings/discovery/PhoneNumbers.js
@@ -23,6 +23,7 @@ import {MatrixClientPeg} from "../../../../MatrixClientPeg";
 import * as sdk from '../../../../index';
 import Modal from '../../../../Modal';
 import AddThreepid from '../../../../AddThreepid';
+import {replaceableComponent} from "../../../../utils/replaceableComponent";
 
 /*
 TODO: Improve the UX for everything in here.
@@ -246,6 +247,7 @@ export class PhoneNumber extends React.Component {
     }
 }
 
+@replaceableComponent("views.settings.discovery.PhoneNumbers")
 export default class PhoneNumbers extends React.Component {
     static propTypes = {
         msisdns: PropTypes.array.isRequired,
diff --git a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js
index 2fa61a0ee6..28aad65129 100644
--- a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js
+++ b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js
@@ -22,7 +22,9 @@ import * as sdk from "../../../../..";
 import AccessibleButton from "../../../elements/AccessibleButton";
 import Modal from "../../../../../Modal";
 import dis from "../../../../../dispatcher/dispatcher";
+import {replaceableComponent} from "../../../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.tabs.room.AdvancedRoomSettingsTab")
 export default class AdvancedRoomSettingsTab extends React.Component {
     static propTypes = {
         roomId: PropTypes.string.isRequired,
diff --git a/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx b/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx
index 3c74bd4c1a..8d886a191e 100644
--- a/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx
+++ b/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx
@@ -21,6 +21,7 @@ import {MatrixEvent} from "matrix-js-sdk/src/models/event";
 import {_t} from "../../../../../languageHandler";
 import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
 import BridgeTile from "../../BridgeTile";
+import {replaceableComponent} from "../../../../../utils/replaceableComponent";
 
 const BRIDGE_EVENT_TYPES = [
     "uk.half-shot.bridge",
@@ -33,6 +34,7 @@ interface IProps {
     roomId: string;
 }
 
+@replaceableComponent("views.settings.tabs.room.BridgeSettingsTab")
 export default class BridgeSettingsTab extends React.Component<IProps> {
     private renderBridgeCard(event: MatrixEvent, room: Room) {
         const content = event.getContent();
diff --git a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js
index 9b8004d9d6..cd4a043622 100644
--- a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js
+++ b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js
@@ -24,7 +24,9 @@ import dis from "../../../../../dispatcher/dispatcher";
 import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
 import SettingsStore from "../../../../../settings/SettingsStore";
 import {UIFeature} from "../../../../../settings/UIFeature";
+import {replaceableComponent} from "../../../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.tabs.room.GeneralRoomSettingsTab")
 export default class GeneralRoomSettingsTab extends React.Component {
     static propTypes = {
         roomId: PropTypes.string.isRequired,
diff --git a/src/components/views/settings/tabs/room/NotificationSettingsTab.js b/src/components/views/settings/tabs/room/NotificationSettingsTab.js
index dd88b5018f..baefb5ae20 100644
--- a/src/components/views/settings/tabs/room/NotificationSettingsTab.js
+++ b/src/components/views/settings/tabs/room/NotificationSettingsTab.js
@@ -22,7 +22,9 @@ import AccessibleButton from "../../../elements/AccessibleButton";
 import Notifier from "../../../../../Notifier";
 import SettingsStore from '../../../../../settings/SettingsStore';
 import {SettingLevel} from "../../../../../settings/SettingLevel";
+import {replaceableComponent} from "../../../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.tabs.room.NotificationsSettingsTab")
 export default class NotificationsSettingsTab extends React.Component {
     static propTypes = {
         roomId: PropTypes.string.isRequired,
diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js
index 49d683c42a..09498e0d4a 100644
--- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js
+++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js
@@ -21,6 +21,7 @@ import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
 import * as sdk from "../../../../..";
 import AccessibleButton from "../../../elements/AccessibleButton";
 import Modal from "../../../../../Modal";
+import {replaceableComponent} from "../../../../../utils/replaceableComponent";
 
 const plEventsToLabels = {
     // These will be translated for us later.
@@ -103,6 +104,7 @@ export class BannedUser extends React.Component {
     }
 }
 
+@replaceableComponent("views.settings.tabs.room.RolesRoomSettingsTab")
 export default class RolesRoomSettingsTab extends React.Component {
     static propTypes = {
         roomId: PropTypes.string.isRequired,
diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js
index f72e78fa3f..ce883c6d23 100644
--- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js
+++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js
@@ -26,7 +26,9 @@ import StyledRadioGroup from '../../../elements/StyledRadioGroup';
 import {SettingLevel} from "../../../../../settings/SettingLevel";
 import SettingsStore from "../../../../../settings/SettingsStore";
 import {UIFeature} from "../../../../../settings/UIFeature";
+import {replaceableComponent} from "../../../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.tabs.room.SecurityRoomSettingsTab")
 export default class SecurityRoomSettingsTab extends React.Component {
     static propTypes = {
         roomId: PropTypes.string.isRequired,
diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx
index 80a20d8afa..d6e01d194c 100644
--- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx
+++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx
@@ -36,6 +36,7 @@ import StyledRadioGroup from "../../../elements/StyledRadioGroup";
 import { SettingLevel } from "../../../../../settings/SettingLevel";
 import {UIFeature} from "../../../../../settings/UIFeature";
 import {Layout} from "../../../../../settings/Layout";
+import {replaceableComponent} from "../../../../../utils/replaceableComponent";
 
 interface IProps {
 }
@@ -64,7 +65,7 @@ interface IState extends IThemeState {
     layout: Layout;
 }
 
-
+@replaceableComponent("views.settings.tabs.user.AppearanceUserSettingsTab")
 export default class AppearanceUserSettingsTab extends React.Component<IProps, IState> {
     private readonly MESSAGE_PREVIEW_TEXT = _t("Hey you. You're the best!");
 
diff --git a/src/components/views/settings/tabs/user/FlairUserSettingsTab.js b/src/components/views/settings/tabs/user/FlairUserSettingsTab.js
index 26e0033233..28e80f3030 100644
--- a/src/components/views/settings/tabs/user/FlairUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/FlairUserSettingsTab.js
@@ -17,7 +17,9 @@ limitations under the License.
 import React from 'react';
 import {_t} from "../../../../../languageHandler";
 import GroupUserSettings from "../../../groups/GroupUserSettings";
+import {replaceableComponent} from "../../../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.tabs.user.FlairUserSettingsTab")
 export default class FlairUserSettingsTab extends React.Component {
     render() {
         return (
diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
index b17ab18c39..314acf5d65 100644
--- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
@@ -39,7 +39,9 @@ import { getThreepidsWithBindStatus } from '../../../../../boundThreepids';
 import Spinner from "../../../elements/Spinner";
 import {SettingLevel} from "../../../../../settings/SettingLevel";
 import {UIFeature} from "../../../../../settings/UIFeature";
+import {replaceableComponent} from "../../../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.tabs.user.GeneralUserSettingsTab")
 export default class GeneralUserSettingsTab extends React.Component {
     static propTypes = {
         closeSettingsFn: PropTypes.func.isRequired,
diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js
index 85ba22a353..e16ee686f5 100644
--- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js
@@ -27,7 +27,9 @@ import * as sdk from "../../../../../";
 import PlatformPeg from "../../../../../PlatformPeg";
 import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts";
 import UpdateCheckButton from "../../UpdateCheckButton";
+import {replaceableComponent} from "../../../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.tabs.user.HelpUserSettingsTab")
 export default class HelpUserSettingsTab extends React.Component {
     static propTypes = {
         closeSettingsFn: PropTypes.func.isRequired,
diff --git a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js
index 91bc9abcad..f515f1862b 100644
--- a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js
@@ -21,6 +21,7 @@ import SettingsStore from "../../../../../settings/SettingsStore";
 import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
 import * as sdk from "../../../../../index";
 import {SettingLevel} from "../../../../../settings/SettingLevel";
+import {replaceableComponent} from "../../../../../utils/replaceableComponent";
 
 export class LabsSettingToggle extends React.Component {
     static propTypes = {
@@ -40,6 +41,7 @@ export class LabsSettingToggle extends React.Component {
     }
 }
 
+@replaceableComponent("views.settings.tabs.user.LabsUserSettingsTab")
 export default class LabsUserSettingsTab extends React.Component {
     constructor() {
         super();
diff --git a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js
index 510d6076a0..91f6728a7a 100644
--- a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js
@@ -23,7 +23,9 @@ import {BanList, RULE_SERVER, RULE_USER} from "../../../../../mjolnir/BanList";
 import Modal from "../../../../../Modal";
 import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
 import * as sdk from "../../../../../index";
+import {replaceableComponent} from "../../../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.tabs.user.MjolnirUserSettingsTab")
 export default class MjolnirUserSettingsTab extends React.Component {
     constructor() {
         super();
diff --git a/src/components/views/settings/tabs/user/NotificationUserSettingsTab.js b/src/components/views/settings/tabs/user/NotificationUserSettingsTab.js
index 2e649cb7f8..8a71d1bf15 100644
--- a/src/components/views/settings/tabs/user/NotificationUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/NotificationUserSettingsTab.js
@@ -17,7 +17,9 @@ limitations under the License.
 import React from 'react';
 import {_t} from "../../../../../languageHandler";
 import * as sdk from "../../../../../index";
+import {replaceableComponent} from "../../../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.tabs.user.NotificationUserSettingsTab")
 export default class NotificationUserSettingsTab extends React.Component {
     constructor() {
         super();
diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
index ae9cad4cfa..238f875e22 100644
--- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
@@ -23,7 +23,9 @@ import Field from "../../../elements/Field";
 import * as sdk from "../../../../..";
 import PlatformPeg from "../../../../../PlatformPeg";
 import {SettingLevel} from "../../../../../settings/SettingLevel";
+import {replaceableComponent} from "../../../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.tabs.user.PreferencesUserSettingsTab")
 export default class PreferencesUserSettingsTab extends React.Component {
     static ROOM_LIST_SETTINGS = [
         'breadcrumbs',
diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js
index a0d9016ce2..8a70811399 100644
--- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js
@@ -34,6 +34,7 @@ import SettingsStore from "../../../../../settings/SettingsStore";
 import {UIFeature} from "../../../../../settings/UIFeature";
 import {isE2eAdvancedPanelPossible} from "../../E2eAdvancedPanel";
 import CountlyAnalytics from "../../../../../CountlyAnalytics";
+import {replaceableComponent} from "../../../../../utils/replaceableComponent";
 
 export class IgnoredUser extends React.Component {
     static propTypes = {
@@ -59,6 +60,7 @@ export class IgnoredUser extends React.Component {
     }
 }
 
+@replaceableComponent("views.settings.tabs.user.SecurityUserSettingsTab")
 export default class SecurityUserSettingsTab extends React.Component {
     static propTypes = {
         closeSettingsFn: PropTypes.func.isRequired,
diff --git a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js
index a78cc10b92..bc6fe796b8 100644
--- a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js
@@ -25,7 +25,9 @@ import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
 import * as sdk from "../../../../../index";
 import Modal from "../../../../../Modal";
 import {SettingLevel} from "../../../../../settings/SettingLevel";
+import {replaceableComponent} from "../../../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.tabs.user.VoiceUserSettingsTab")
 export default class VoiceUserSettingsTab extends React.Component {
     constructor() {
         super();
diff --git a/src/components/views/terms/InlineTermsAgreement.js b/src/components/views/terms/InlineTermsAgreement.js
index 5f6e276976..473a97642c 100644
--- a/src/components/views/terms/InlineTermsAgreement.js
+++ b/src/components/views/terms/InlineTermsAgreement.js
@@ -20,7 +20,9 @@ import {_t, pickBestLanguage} from "../../../languageHandler";
 import * as sdk from "../../..";
 import {objectClone} from "../../../utils/objects";
 import StyledCheckbox from "../elements/StyledCheckbox";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.terms.InlineTermsAgreement")
 export default class InlineTermsAgreement extends React.Component {
     static propTypes = {
         policiesAndServicePairs: PropTypes.array.isRequired, // array of service/policy pairs
diff --git a/src/components/views/toasts/NonUrgentEchoFailureToast.tsx b/src/components/views/toasts/NonUrgentEchoFailureToast.tsx
index 76d0328e8b..abf5b10692 100644
--- a/src/components/views/toasts/NonUrgentEchoFailureToast.tsx
+++ b/src/components/views/toasts/NonUrgentEchoFailureToast.tsx
@@ -19,7 +19,9 @@ import { _t } from "../../../languageHandler";
 import AccessibleButton from "../elements/AccessibleButton";
 import Modal from "../../../Modal";
 import ServerOfflineDialog from "../dialogs/ServerOfflineDialog";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.toasts.NonUrgentEchoFailureToast")
 export default class NonUrgentEchoFailureToast extends React.PureComponent {
     private openDialog = () => {
         Modal.createTrackedDialog('Local Echo Server Error', '', ServerOfflineDialog, {});
diff --git a/src/components/views/toasts/VerificationRequestToast.tsx b/src/components/views/toasts/VerificationRequestToast.tsx
index 8c8a74b2be..010c8fd12f 100644
--- a/src/components/views/toasts/VerificationRequestToast.tsx
+++ b/src/components/views/toasts/VerificationRequestToast.tsx
@@ -29,6 +29,7 @@ import GenericToast from "./GenericToast";
 import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
 import {DeviceInfo} from "matrix-js-sdk/src/crypto/deviceinfo";
 import {Action} from "../../../dispatcher/actions";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     toastKey: string;
@@ -40,6 +41,7 @@ interface IState {
     device?: DeviceInfo;
 }
 
+@replaceableComponent("views.toasts.VerificationRequestToast")
 export default class VerificationRequestToast extends React.PureComponent<IProps, IState> {
     private intervalHandle: NodeJS.Timeout;
 
diff --git a/src/components/views/verification/VerificationCancelled.js b/src/components/views/verification/VerificationCancelled.js
index fc2a287359..0bbaea1804 100644
--- a/src/components/views/verification/VerificationCancelled.js
+++ b/src/components/views/verification/VerificationCancelled.js
@@ -18,7 +18,9 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.verification.VerificationCancelled")
 export default class VerificationCancelled extends React.Component {
     static propTypes = {
         onDone: PropTypes.func.isRequired,
diff --git a/src/components/views/verification/VerificationComplete.js b/src/components/views/verification/VerificationComplete.js
index 2214711b1f..cf2a72591c 100644
--- a/src/components/views/verification/VerificationComplete.js
+++ b/src/components/views/verification/VerificationComplete.js
@@ -18,7 +18,9 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.verification.VerificationComplete")
 export default class VerificationComplete extends React.Component {
     static propTypes = {
         onDone: PropTypes.func.isRequired,
diff --git a/src/components/views/verification/VerificationShowSas.js b/src/components/views/verification/VerificationShowSas.js
index 09374b91af..36f99b2140 100644
--- a/src/components/views/verification/VerificationShowSas.js
+++ b/src/components/views/verification/VerificationShowSas.js
@@ -21,11 +21,13 @@ import {PendingActionSpinner} from "../right_panel/EncryptionInfo";
 import AccessibleButton from "../elements/AccessibleButton";
 import DialogButtons from "../elements/DialogButtons";
 import { fixupColorFonts } from '../../../utils/FontManager';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 function capFirst(s) {
     return s.charAt(0).toUpperCase() + s.slice(1);
 }
 
+@replaceableComponent("views.verification.VerificationShowSas")
 export default class VerificationShowSas extends React.Component {
     static propTypes = {
         pending: PropTypes.bool,
diff --git a/src/components/views/voip/CallContainer.tsx b/src/components/views/voip/CallContainer.tsx
index 51925cb147..9d0047fc54 100644
--- a/src/components/views/voip/CallContainer.tsx
+++ b/src/components/views/voip/CallContainer.tsx
@@ -17,6 +17,7 @@ limitations under the License.
 import React from 'react';
 import IncomingCallBox from './IncomingCallBox';
 import CallPreview from './CallPreview';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
 
@@ -26,6 +27,7 @@ interface IState {
 
 }
 
+@replaceableComponent("views.voip.CallContainer")
 export default class CallContainer extends React.PureComponent<IProps, IState> {
     public render() {
         return <div className="mx_CallContainer">
diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx
index c08e52181b..29de068b0c 100644
--- a/src/components/views/voip/CallPreview.tsx
+++ b/src/components/views/voip/CallPreview.tsx
@@ -26,6 +26,7 @@ import PersistentApp from "../elements/PersistentApp";
 import SettingsStore from "../../../settings/SettingsStore";
 import { CallEvent, CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
 import { MatrixClientPeg } from '../../../MatrixClientPeg';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const SHOW_CALL_IN_STATES = [
     CallState.Connected,
@@ -85,6 +86,7 @@ function getPrimarySecondaryCalls(calls: MatrixCall[]): [MatrixCall, MatrixCall[
  * CallPreview shows a small version of CallView hovering over the UI in 'picture-in-picture'
  * (PiP mode). It displays the call(s) which is *not* in the room the user is currently viewing.
  */
+@replaceableComponent("views.voip.CallPreview")
 export default class CallPreview extends React.Component<IProps, IState> {
     private roomStoreToken: any;
     private dispatcherRef: string;
diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx
index 7cac682794..9bdc8fb11d 100644
--- a/src/components/views/voip/CallView.tsx
+++ b/src/components/views/voip/CallView.tsx
@@ -31,6 +31,7 @@ import {alwaysAboveLeftOf, alwaysAboveRightOf, ChevronFace, ContextMenuButton} f
 import CallContextMenu from '../context_menus/CallContextMenu';
 import { avatarUrlForMember } from '../../../Avatar';
 import DialpadContextMenu from '../context_menus/DialpadContextMenu';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
         // The call for us to display
@@ -100,6 +101,7 @@ const BOTTOM_PADDING = 10;
 const BOTTOM_MARGIN_TOP_BOTTOM = 10; // top margin plus bottom margin
 const CONTEXT_MENU_VPADDING = 8; // How far the context menu sits above the button (px)
 
+@replaceableComponent("views.voip.CallView")
 export default class CallView extends React.Component<IProps, IState> {
     private dispatcherRef: string;
     private contentRef = createRef<HTMLDivElement>();
diff --git a/src/components/views/voip/CallViewForRoom.tsx b/src/components/views/voip/CallViewForRoom.tsx
index 4cb4e66fbe..97960d1e0b 100644
--- a/src/components/views/voip/CallViewForRoom.tsx
+++ b/src/components/views/voip/CallViewForRoom.tsx
@@ -19,6 +19,7 @@ import React from 'react';
 import CallHandler from '../../../CallHandler';
 import CallView from './CallView';
 import dis from '../../../dispatcher/dispatcher';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     // What room we should display the call for
@@ -40,6 +41,7 @@ interface IState {
  * Wrapper for CallView that always display the call in a given room,
  * or nothing if there is no call in that room.
  */
+@replaceableComponent("views.voip.CallViewForRoom")
 export default class CallViewForRoom extends React.Component<IProps, IState> {
     private dispatcherRef: string;
 
diff --git a/src/components/views/voip/DialPad.tsx b/src/components/views/voip/DialPad.tsx
index da88f49adf..68092fb0be 100644
--- a/src/components/views/voip/DialPad.tsx
+++ b/src/components/views/voip/DialPad.tsx
@@ -16,6 +16,7 @@ limitations under the License.
 
 import * as React from "react";
 import AccessibleButton from "../elements/AccessibleButton";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const BUTTONS = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '0', '#'];
 
@@ -59,6 +60,7 @@ interface IProps {
     onDialPress?: (string) => void;
 }
 
+@replaceableComponent("views.voip.DialPad")
 export default class Dialpad extends React.PureComponent<IProps> {
     render() {
         const buttonNodes = [];
diff --git a/src/components/views/voip/DialPadModal.tsx b/src/components/views/voip/DialPadModal.tsx
index 9f031a48a3..cdd5bc6641 100644
--- a/src/components/views/voip/DialPadModal.tsx
+++ b/src/components/views/voip/DialPadModal.tsx
@@ -25,6 +25,7 @@ import dis from '../../../dispatcher/dispatcher';
 import Modal from "../../../Modal";
 import ErrorDialog from "../../views/dialogs/ErrorDialog";
 import CallHandler from "../../../CallHandler";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     onFinished: (boolean) => void;
@@ -34,6 +35,7 @@ interface IState {
     value: string;
 }
 
+@replaceableComponent("views.voip.DialPadModal")
 export default class DialpadModal extends React.PureComponent<IProps, IState> {
     constructor(props) {
         super(props);
diff --git a/src/components/views/voip/IncomingCallBox.tsx b/src/components/views/voip/IncomingCallBox.tsx
index a495093d85..0ca2a196c2 100644
--- a/src/components/views/voip/IncomingCallBox.tsx
+++ b/src/components/views/voip/IncomingCallBox.tsx
@@ -25,6 +25,7 @@ import CallHandler from '../../../CallHandler';
 import RoomAvatar from '../avatars/RoomAvatar';
 import FormButton from '../elements/FormButton';
 import { CallState } from 'matrix-js-sdk/src/webrtc/call';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
 }
@@ -33,6 +34,7 @@ interface IState {
     incomingCall: any;
 }
 
+@replaceableComponent("views.voip.IncomingCallBox")
 export default class IncomingCallBox extends React.Component<IProps, IState> {
     private dispatcherRef: string;
 
diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx
index 5210f28eb1..23dbe4d46b 100644
--- a/src/components/views/voip/VideoFeed.tsx
+++ b/src/components/views/voip/VideoFeed.tsx
@@ -18,6 +18,7 @@ import classnames from 'classnames';
 import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
 import React, {createRef} from 'react';
 import SettingsStore from "../../../settings/SettingsStore";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 export enum VideoFeedType {
     Local,
@@ -37,6 +38,7 @@ interface IProps {
     onResize?: (e: Event) => void,
 }
 
+@replaceableComponent("views.voip.VideoFeed")
 export default class VideoFeed extends React.Component<IProps> {
     private vid = createRef<HTMLVideoElement>();
 
diff --git a/test/accessibility/RovingTabIndex-test.js b/test/accessibility/RovingTabIndex-test.js
index 8be4a2976c..5aa93f99f3 100644
--- a/test/accessibility/RovingTabIndex-test.js
+++ b/test/accessibility/RovingTabIndex-test.js
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
+import '../skinned-sdk'; // Must be first for skinning to work
 import React from "react";
 import Adapter from "enzyme-adapter-react-16";
 import { configure, mount } from "enzyme";
diff --git a/test/components/views/messages/MKeyVerificationConclusion-test.js b/test/components/views/messages/MKeyVerificationConclusion-test.js
index 689151fe3f..45e122295b 100644
--- a/test/components/views/messages/MKeyVerificationConclusion-test.js
+++ b/test/components/views/messages/MKeyVerificationConclusion-test.js
@@ -1,3 +1,4 @@
+import '../../../skinned-sdk'; // Must be first for skinning to work
 import React from 'react';
 import TestRenderer from 'react-test-renderer';
 import { EventEmitter } from 'events';
diff --git a/test/components/views/rooms/SendMessageComposer-test.js b/test/components/views/rooms/SendMessageComposer-test.js
index 6eeac7ceea..64a90eee81 100644
--- a/test/components/views/rooms/SendMessageComposer-test.js
+++ b/test/components/views/rooms/SendMessageComposer-test.js
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
+import '../../../skinned-sdk'; // Must be first for skinning to work
 import Adapter from "enzyme-adapter-react-16";
 import { configure, mount } from "enzyme";
 import React from "react";
diff --git a/test/createRoom-test.js b/test/createRoom-test.js
index f7e8617c3f..ed8f9779f7 100644
--- a/test/createRoom-test.js
+++ b/test/createRoom-test.js
@@ -1,3 +1,4 @@
+import './skinned-sdk'; // Must be first for skinning to work
 import {_waitForMember, canEncryptToAllUsers} from '../src/createRoom';
 import {EventEmitter} from 'events';
 
diff --git a/test/editor/deserialize-test.js b/test/editor/deserialize-test.js
index 112ac7d02b..07b75aaae5 100644
--- a/test/editor/deserialize-test.js
+++ b/test/editor/deserialize-test.js
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
+import '../skinned-sdk'; // Must be first for skinning to work
 import {parseEvent} from "../../src/editor/deserialize";
 import {createPartCreator} from "./mock";