diff --git a/src/components/views/rooms/EntityTileRefactored.tsx b/src/components/views/rooms/EntityTileRefactored.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..3103c92dd47c874eff98d924ef4abd11969b0e87
--- /dev/null
+++ b/src/components/views/rooms/EntityTileRefactored.tsx
@@ -0,0 +1,169 @@
+/*
+Copyright 2015, 2016 OpenMarket Ltd
+Copyright 2018 New Vector Ltd
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React, { useCallback } from "react";
+import classNames from "classnames";
+
+import AccessibleButton from "../elements/AccessibleButton";
+import { _t, _td, TranslationKey } from "../../../languageHandler";
+import E2EIcon from "./E2EIcon";
+import { E2EState } from "../../../models/rooms/E2EState";
+import BaseAvatar from "../avatars/BaseAvatar";
+import PresenceLabel from "./PresenceLabel";
+import { PresenceState } from "../../../models/rooms/PresenceState";
+
+export enum PowerStatus {
+    Admin = "admin",
+    Moderator = "moderator",
+}
+
+const PowerLabel: Record<PowerStatus, TranslationKey> = {
+    [PowerStatus.Admin]: _td("power_level|admin"),
+    [PowerStatus.Moderator]: _td("power_level|mod"),
+};
+
+const PRESENCE_CLASS: Record<PresenceState, string> = {
+    "offline": "mx_EntityTile_offline",
+    "online": "mx_EntityTile_online",
+    "unavailable": "mx_EntityTile_unavailable",
+    "io.element.unreachable": "mx_EntityTile_unreachable",
+};
+
+function presenceClassForMember(presenceState?: PresenceState, lastActiveAgo?: number, showPresence?: boolean): string {
+    if (showPresence === false) {
+        return "mx_EntityTile_online_beenactive";
+    }
+
+    // offline is split into two categories depending on whether we have
+    // a last_active_ago for them.
+    if (presenceState === "offline") {
+        if (lastActiveAgo) {
+            return PRESENCE_CLASS["offline"] + "_beenactive";
+        } else {
+            return PRESENCE_CLASS["offline"] + "_neveractive";
+        }
+    } else if (presenceState) {
+        return PRESENCE_CLASS[presenceState];
+    } else {
+        return PRESENCE_CLASS["offline"] + "_neveractive";
+    }
+}
+
+interface IProps {
+    name?: string;
+    nameJSX?: JSX.Element;
+    title?: string;
+    avatarJsx?: JSX.Element; // <BaseAvatar />
+    className?: string;
+    presenceState?: PresenceState;
+    presenceLastActiveAgo: number;
+    presenceLastTs: number;
+    presenceCurrentlyActive?: boolean;
+    showInviteButton?: boolean;
+    onClick(): void;
+    showPresence?: boolean;
+    subtextLabel?: string;
+    e2eStatus?: E2EState;
+    powerStatus?: PowerStatus;
+}
+
+export default function EntityTileRefactored({
+    onClick = () => {},
+    presenceState = "offline",
+    presenceLastActiveAgo = 0,
+    presenceLastTs = 0,
+    showInviteButton = false,
+    showPresence = true,
+    ...props
+}: IProps): JSX.Element {
+    /**
+     * Creates the PresenceLabel component if needed
+     * @returns The PresenceLabel component if we need to render it, undefined otherwise
+     */
+    const getPresenceLabel = useCallback((): JSX.Element | undefined => {
+        if (!showPresence) return;
+        const activeAgo = presenceLastActiveAgo ? Date.now() - (presenceLastTs - presenceLastActiveAgo) : -1;
+        return (
+            <PresenceLabel
+                activeAgo={activeAgo}
+                currentlyActive={props.presenceCurrentlyActive}
+                presenceState={presenceState}
+            />
+        );
+    }, [presenceLastTs, presenceLastActiveAgo, presenceState, props.presenceCurrentlyActive, showPresence]);
+
+    const mainClassNames: Record<string, boolean> = {
+        mx_EntityTile: true,
+    };
+    if (props.className) mainClassNames[props.className] = true;
+
+    const presenceClass = presenceClassForMember(presenceState, presenceLastActiveAgo, showPresence);
+    mainClassNames[presenceClass] = true;
+
+    const name = props.nameJSX || props.name;
+    const nameAndPresence = (
+        <div className="mx_EntityTile_details">
+            <div className="mx_EntityTile_name">{name}</div>
+            {getPresenceLabel()}
+        </div>
+    );
+
+    let inviteButton;
+    if (showInviteButton) {
+        inviteButton = (
+            <div className="mx_EntityTile_invite">
+                <img
+                    alt={_t("action|invite")}
+                    src={require("../../../../res/img/plus.svg").default}
+                    width="16"
+                    height="16"
+                />
+            </div>
+        );
+    }
+
+    let powerLabel;
+    const powerStatus = props.powerStatus;
+    if (powerStatus) {
+        const powerText = _t(PowerLabel[powerStatus]);
+        powerLabel = <div className="mx_EntityTile_power">{powerText}</div>;
+    }
+
+    let e2eIcon;
+    const { e2eStatus } = props;
+    if (e2eStatus) {
+        e2eIcon = <E2EIcon status={e2eStatus} isUser={true} bordered={true} />;
+    }
+
+    const av = props.avatarJsx || <BaseAvatar name={props.name} size="36px" aria-hidden="true" />;
+
+    // The wrapping div is required to make the magic mouse listener work, for some reason.
+    return (
+        <div>
+            <AccessibleButton className={classNames(mainClassNames)} title={props.title} onClick={onClick}>
+                <div className="mx_EntityTile_avatar">
+                    {av}
+                    {e2eIcon}
+                </div>
+                {nameAndPresence}
+                {powerLabel}
+                {inviteButton}
+            </AccessibleButton>
+        </div>
+    );
+}
diff --git a/src/components/views/rooms/MemberTileNext.tsx b/src/components/views/rooms/MemberTileNext.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d498abb279e88b1b6a38c726674c68f91210ad3d
--- /dev/null
+++ b/src/components/views/rooms/MemberTileNext.tsx
@@ -0,0 +1,175 @@
+/*
+Copyright 2015, 2016 OpenMarket Ltd
+Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React, { useEffect, useState } from "react";
+import { RoomStateEvent, MatrixEvent, EventType } from "matrix-js-sdk/src/matrix";
+import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo";
+import { CryptoEvent } from "matrix-js-sdk/src/crypto";
+import { UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
+
+import dis from "../../../dispatcher/dispatcher";
+import { _t } from "../../../languageHandler";
+import { MatrixClientPeg } from "../../../MatrixClientPeg";
+import { Action } from "../../../dispatcher/actions";
+import { PowerStatus } from "./EntityTile";
+import DisambiguatedProfile from "../messages/DisambiguatedProfile";
+import UserIdentifierCustomisations from "../../../customisations/UserIdentifier";
+import { E2EState } from "../../../models/rooms/E2EState";
+import { asyncSome } from "../../../utils/arrays";
+import { getUserDeviceIds } from "../../../utils/crypto/deviceInfo";
+import { RoomMember } from "../../../models/rooms/RoomMember";
+import MemberAvatarNext from "../avatars/MemberAvatarNext";
+import EntityTileRefactored from "./EntityTileRefactored";
+
+interface IProps {
+    member: RoomMember;
+    showPresence?: boolean;
+}
+
+export default function MemberTile(props: IProps): JSX.Element {
+    // const [isRoomEncrypted, setIsRoomEncrypted] = useState(false);
+    const [e2eStatus, setE2eStatus] = useState<E2EState | undefined>();
+
+    useEffect(() => {
+        const cli = MatrixClientPeg.safeGet();
+
+        const updateE2EStatus = async (): Promise<void> => {
+            const { userId } = props.member;
+            const isMe = userId === cli.getUserId();
+            const userTrust = await cli.getCrypto()?.getUserVerificationStatus(userId);
+            if (!userTrust?.isCrossSigningVerified()) {
+                setE2eStatus(userTrust?.wasCrossSigningVerified() ? E2EState.Warning : E2EState.Normal);
+                return;
+            }
+
+            const deviceIDs = await getUserDeviceIds(cli, userId);
+            const anyDeviceUnverified = await asyncSome(deviceIDs, async (deviceId) => {
+                // For your own devices, we use the stricter check of cross-signing
+                // verification to encourage everyone to trust their own devices via
+                // cross-signing so that other users can then safely trust you.
+                // For other people's devices, the more general verified check that
+                // includes locally verified devices can be used.
+                const deviceTrust = await cli.getCrypto()?.getDeviceVerificationStatus(userId, deviceId);
+                return !deviceTrust || (isMe ? !deviceTrust.crossSigningVerified : !deviceTrust.isVerified());
+            });
+            setE2eStatus(anyDeviceUnverified ? E2EState.Warning : E2EState.Verified);
+        };
+
+        const onRoomStateEvents = (ev: MatrixEvent): void => {
+            if (ev.getType() !== EventType.RoomEncryption) return;
+            const { roomId } = props.member;
+            if (ev.getRoomId() !== roomId) return;
+
+            // The room is encrypted now.
+            cli.removeListener(RoomStateEvent.Events, onRoomStateEvents);
+            updateE2EStatus();
+        };
+
+        const onUserTrustStatusChanged = (userId: string, trustStatus: UserVerificationStatus): void => {
+            if (userId !== props.member.userId) return;
+            updateE2EStatus();
+        };
+
+        const onDeviceVerificationChanged = (userId: string, deviceId: string, deviceInfo: DeviceInfo): void => {
+            if (userId !== props.member.userId) return;
+            updateE2EStatus();
+        };
+
+        const { roomId } = props.member;
+        if (roomId) {
+            const isRoomEncrypted = cli.isRoomEncrypted(roomId);
+            if (isRoomEncrypted) {
+                cli.on(CryptoEvent.UserTrustStatusChanged, onUserTrustStatusChanged);
+                cli.on(CryptoEvent.DeviceVerificationChanged, onDeviceVerificationChanged);
+                updateE2EStatus();
+            } else {
+                // Listen for room to become encrypted
+                cli.on(RoomStateEvent.Events, onRoomStateEvents);
+            }
+        }
+
+        return () => {
+            if (cli) {
+                cli.removeListener(RoomStateEvent.Events, onRoomStateEvents);
+                cli.removeListener(CryptoEvent.UserTrustStatusChanged, onUserTrustStatusChanged);
+                cli.removeListener(CryptoEvent.DeviceVerificationChanged, onDeviceVerificationChanged);
+            }
+        };
+    }, [props.member]);
+
+    const onClick = (): void => {
+        dis.dispatch({
+            action: Action.ViewUser,
+            member: props.member,
+            push: true,
+        });
+    };
+
+    const getDisplayName = (): string => {
+        return props.member.name;
+    };
+
+    const getPowerLabel = (): string => {
+        return _t("member_list|power_label", {
+            userName: UserIdentifierCustomisations.getDisplayUserIdentifier(props.member.userId, {
+                roomId: props.member.roomId,
+            }),
+            powerLevelNumber: props.member.powerLevel,
+        }).trim();
+    };
+
+    const member = props.member;
+    const name = getDisplayName();
+
+    const av = <MemberAvatarNext member={member} size="36px" aria-hidden="true" />;
+
+    const powerStatusMap = new Map([
+        [100, PowerStatus.Admin],
+        [50, PowerStatus.Moderator],
+    ]);
+
+    // Find the nearest power level with a badge
+    let powerLevel = props.member.powerLevel;
+    for (const [pl] of powerStatusMap) {
+        if (props.member.powerLevel >= pl) {
+            powerLevel = pl;
+            break;
+        }
+    }
+
+    const powerStatus = powerStatusMap.get(powerLevel);
+
+    const nameJSX = <DisambiguatedProfile member={member} fallbackName={name || ""} />;
+
+    return (
+        <EntityTileRefactored
+            {...props}
+            presenceState={member.presence?.state}
+            presenceLastActiveAgo={member.presence?.lastActiveAgo || 0}
+            presenceLastTs={member.presence?.lastPresenceTime || 0}
+            presenceCurrentlyActive={member.presence?.currentlyActive || false}
+            avatarJsx={av}
+            title={getPowerLabel()}
+            name={name}
+            nameJSX={nameJSX}
+            powerStatus={powerStatus}
+            showPresence={props.showPresence}
+            e2eStatus={e2eStatus}
+            onClick={onClick}
+        />
+    );
+}