diff --git a/skins/base/css/common.css b/skins/base/css/common.css
index d90ce11a..9527b253 100644
--- a/skins/base/css/common.css
+++ b/skins/base/css/common.css
@@ -43,9 +43,39 @@ html {
     overflow: -moz-scrollbars-none;
 }
 
-/* FIXME: why is all the dialog stuff in here rather than in per-component files? */
+.mx_ContextualMenu_background {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    opacity: 1.0;
+    z-index: 2000;
+}
 
-.mx_Dialog_Background {
+.mx_ContextualMenu {
+    text-align: center;
+    border: 1px solid #a9dbf4;
+    border-radius: 8px;
+    background-color: #fff;
+    position: fixed;
+    z-index: 1000;
+    padding: 6px;
+}
+
+.mx_ContextualMenu_chevron {
+    padding: 12px;
+    position: absolute;
+    right: -21px;
+    top: 0px;
+}
+
+.mx_ContextualMenu_field {
+    padding: 6px;
+}
+
+
+.mx_Dialog_background {
     position: fixed;
     top: 0;
     left: 0;
@@ -56,7 +86,7 @@ html {
     z-index: 2000;
 }
 
-.mx_Dialog_Wrapper {
+.mx_Dialog_wrapper {
     position: fixed;
     top: 0;
     left: 0;
diff --git a/skins/base/css/molecules/MemberInfo.css b/skins/base/css/molecules/MemberInfo.css
index 144212d7..52c48a79 100644
--- a/skins/base/css/molecules/MemberInfo.css
+++ b/skins/base/css/molecules/MemberInfo.css
@@ -14,60 +14,3 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-.mx_MemberInfo {
-    text-align: center;
-    border: 1px solid #a9dbf4;
-    border-radius: 8px;
-    background-color: #fff;
-    position: absolute;
-    width: 200px;
-    margin-left: -295px;
-    margin-top: 0px;
-    z-index: 1000;
-    padding: 6px;
-}
-
-.mx_MemberInfo_chevron {
-    padding: 12px;
-    position: absolute;
-    right: -21px;
-    top: 0px;
-}
-
-/*
- * a hacky shim to extend the hitmask of the overlay to overlap
- * better with the main menu itself
- */
-.mx_MemberInfo_shim {
-    position: absolute;
-    left: 212px;
-    width: 40px;
-    height: 100%;
-}
-
-.mx_MemberInfo_avatar {
-    padding: 6px;
-}
-
-.mx_MemberInfo_avatarImg {
-    border-radius: 128px;
-}
-
-.mx_MemberInfo_field {
-    padding: 6px;
-    overflow: hidden;
-    text-overflow: ellipsis;
-}
-
-.mx_MemberInfo_button {
-    vertical-align: middle;
-    max-width: 100px;
-    height: 36px;
-    background-color: #50e3c2;
-    line-height: 36px;
-    border-radius: 36px;
-    color: #fff;
-    margin: auto;
-    margin-top: 6px;
-    margin-bottom: 6px;
-}
diff --git a/skins/base/views/molecules/MemberInfo.js b/skins/base/views/molecules/MemberInfo.js
index e79b99a2..512ed9f4 100644
--- a/skins/base/views/molecules/MemberInfo.js
+++ b/skins/base/views/molecules/MemberInfo.js
@@ -27,84 +27,34 @@ module.exports = React.createClass({
     displayName: 'MemberInfo',
     mixins: [MemberInfoController],
 
-    componentDidMount: function() {
-        var self = this;
-
-        var memberInfo = this.getDOMNode();
-        var memberListScroll = document.getElementsByClassName("mx_MemberList_border")[0];
-        if (memberListScroll) {
-            memberInfo.style.top = (memberInfo.parentElement.offsetTop - memberListScroll.scrollTop) + "px";
-        }
-    },
-
-    getDuration: function(time) {
-        if (!time) return;
-        var t = parseInt(time / 1000);
-        var s = t % 60;
-        var m = parseInt(t / 60) % 60;
-        var h = parseInt(t / (60 * 60)) % 24;
-        var d = parseInt(t / (60 * 60 * 24));
-        if (t < 60) {
-            if (t < 0) {
-                return "0s";
-            }
-            return s + "s";
-        }
-        if (t < 60 * 60) {
-            return m + "m";
-        }
-        if (t < 24 * 60 * 60) {
-            return h + "h";
-        }
-        return d + "d ";
-    },
-
     render: function() {
-        var activeAgo = "unknown";
-        if (this.state.active >= 0) {
-            activeAgo = this.getDuration(this.state.active);
-        }
         var kickButton, banButton, muteButton, giveModButton;
         if (this.state.can.kick) {
-            kickButton = <div className="mx_MemberInfo_button" onClick={this.onKick}>
+            kickButton = <div className="mx_ContextualMenu_field" onClick={this.onKick}>
                 Kick
             </div>;
         }
         if (this.state.can.ban) {
-            banButton = <div className="mx_MemberInfo_button" onClick={this.onBan}>
+            banButton = <div className="mx_ContextualMenu_field" onClick={this.onBan}>
                 Ban
             </div>;
         }
         if (this.state.can.mute) {
             var muteLabel = this.state.muted ? "Unmute" : "Mute";
-            muteButton = <div className="mx_MemberInfo_button" onClick={this.onMuteToggle}>
+            muteButton = <div className="mx_ContextualMenu_field" onClick={this.onMuteToggle}>
                 {muteLabel}
             </div>;
         }
         if (this.state.can.modifyLevel) {
             var giveOpLabel = this.state.isTargetMod ? "Revoke Mod" : "Make Mod";
-            giveModButton = <div className="mx_MemberInfo_button" onClick={this.onModToggle}>
+            giveModButton = <div className="mx_ContextualMenu_field" onClick={this.onModToggle}>
                 {giveOpLabel}
             </div>
         }
 
-        var opLabel;
-        if (this.state.isTargetMod) {
-            var level = this.props.member.powerLevelNorm + "%";
-            opLabel = <div className="mx_MemberInfo_field">Moderator ({level})</div>
-        }
         return (
-            <div className="mx_MemberInfo">
-                <img className="mx_MemberInfo_chevron" src="img/chevron-right.png" width="9" height="16" />
-                <div className="mx_MemberInfo_shim"></div>
-                <div className="mx_MemberInfo_avatar">
-                    <MemberAvatar member={this.props.member} width={128} height={128} />
-                </div>
-                <div className="mx_MemberInfo_field">{this.props.member.userId}</div>
-                {opLabel}
-                <div className="mx_MemberInfo_field">Presence: {this.state.presence}</div>
-                <div className="mx_MemberInfo_field">Last active: {activeAgo}</div>
-                <div className="mx_MemberInfo_button" onClick={this.onChatClick}>Start chat</div>
+            <div>
+                <div className="mx_ContextualMenu_field" onClick={this.onChatClick}>Start chat</div>
                 {muteButton}
                 {kickButton}
                 {banButton}
diff --git a/skins/base/views/molecules/MemberTile.js b/skins/base/views/molecules/MemberTile.js
index a60fbb06..2cfceb4d 100644
--- a/skins/base/views/molecules/MemberTile.js
+++ b/skins/base/views/molecules/MemberTile.js
@@ -21,6 +21,7 @@ var React = require('react');
 var MatrixClientPeg = require("../../../../src/MatrixClientPeg");
 var ComponentBroker = require('../../../../src/ComponentBroker');
 var Modal = require("../../../../src/Modal");
+var ContextualMenu = require("../../../../src/ContextualMenu");
 var MemberTileController = require("../../../../src/controllers/molecules/MemberTile");
 var MemberInfo = ComponentBroker.get('molecules/MemberInfo');
 var ErrorDialog = ComponentBroker.get("organisms/ErrorDialog");
@@ -43,8 +44,11 @@ module.exports = React.createClass({
     },
 
     onClick: function(e) {
-        this.setState({ 'menu': true });
-        this.setState(this._calculateOpsPermissions());        
+        ContextualMenu.createMenu(MemberInfo, {
+            member: this.props.member,
+            right: window.innerWidth - e.pageX,
+            top: e.pageY,
+        });
     },
 
     getDuration: function(time) {
@@ -118,41 +122,6 @@ module.exports = React.createClass({
             nameClass += " mx_MemberTile_zalgo";
         }
 
-        var menu;
-        if (this.state.menu) {
-            var kickButton, banButton, muteButton, giveModButton;
-            if (this.state.can.kick) {
-                kickButton = <div className="mx_MemberTile_menuItem" onClick={this.onKick}>
-                    Kick
-                </div>;
-            }
-            if (this.state.can.ban) {
-                banButton = <div className="mx_MemberTile_menuItem" onClick={this.onBan}>
-                    Ban
-                </div>;
-            }
-            if (this.state.can.mute) {
-                var muteLabel = this.state.muted ? "Unmute" : "Mute";
-                muteButton = <div className="mx_MemberTile_menuItem" onClick={this.onMuteToggle}>
-                    {muteLabel}
-                </div>;
-            }
-            if (this.state.can.modifyLevel) {
-                var giveOpLabel = this.state.isTargetMod ? "Revoke Mod" : "Make Mod";
-                giveModButton = <div className="mx_MemberTile_menuItem" onClick={this.onModToggle}>
-                    {giveOpLabel}
-                </div>
-            }
-            menu = <div className="mx_MemberTile_menu">
-                        <img className="mx_MemberTile_chevron" src="img/chevron-right.png" width="9" height="16" />
-                        <div className="mx_MemberTile_menuItem" onClick={this.onChatClick}>Chat</div>
-                        {muteButton}
-                        {kickButton}
-                        {banButton}
-                        {giveModButton}
-                   </div>;
-        }
-
         var nameEl;
         if (this.state.hover) {
             var presence;
@@ -181,7 +150,6 @@ module.exports = React.createClass({
 
         return (
             <div className={mainClassName} title={ this.getPowerLabel() } onClick={ this.onClick } onMouseEnter={ this.mouseEnter } onMouseLeave={ this.mouseLeave }>
-                { menu }
                 <div className="mx_MemberTile_avatar">
                     <MemberAvatar member={this.props.member} />
                      { power }
diff --git a/src/Modal.js b/src/Modal.js
index d18b39e6..ba7660bf 100644
--- a/src/Modal.js
+++ b/src/Modal.js
@@ -47,11 +47,11 @@ module.exports = {
         // FIXME: If a dialog uses getDefaultProps it clobbers the onFinished
         // property set here so you can't close the dialog from a button click!
         var dialog = (
-            <div className="mx_Dialog_Wrapper">
+            <div className="mx_Dialog_wrapper">
                 <div className="mx_Dialog">
                     <Element {...props} onFinished={closeDialog}/>
                 </div>
-                <div className="mx_Dialog_Background" onClick={closeDialog}></div>
+                <div className="mx_Dialog_background" onClick={closeDialog}></div>
             </div>
         );
 
diff --git a/src/controllers/molecules/MemberInfo.js b/src/controllers/molecules/MemberInfo.js
index 5404c337..5c63d970 100644
--- a/src/controllers/molecules/MemberInfo.js
+++ b/src/controllers/molecules/MemberInfo.js
@@ -16,8 +16,6 @@ limitations under the License.
 
 /*
  * State vars:
- * 'presence' : string (online|offline|unavailable etc)
- * 'active' : number (ms ago; can be -1)
  * 'can': {
  *   kick: boolean,
  *   ban: boolean,
@@ -38,54 +36,15 @@ var ErrorDialog = ComponentBroker.get("organisms/ErrorDialog");
 module.exports = {
     componentDidMount: function() {
         var self = this;
-        // listen for presence changes
-        function updateUserState(event, user) {
-            if (!self.props.member) { return; }
-
-            if (user.userId === self.props.member.userId) {
-                self.setState({
-                    presence: user.presence,
-                    active: user.lastActiveAgo
-                });
-            }
-        }
-        MatrixClientPeg.get().on("User.presence", updateUserState);
-        this.userPresenceFn = updateUserState;
-
-        // listen for power level changes
-        function updatePowerLevel(event, member) {
-            if (!self.props.member) { return; }
-
-            if (member.roomId !== self.props.member.roomId) {
-                return;
-            }
-            // only interested in changes to us or them
-            var myUserId = MatrixClientPeg.get().credentials.userId;
-            if ([myUserId, self.props.member.userId].indexOf(member.userId) === -1) {
-                return;
-            }
-            self.setState(self._calculateOpsPermissions());
-        }
-        MatrixClientPeg.get().on("RoomMember.powerLevel", updatePowerLevel);
-        this.updatePowerLevelFn = updatePowerLevel;
 
         // work out the current state
         if (this.props.member) {
             var usr = MatrixClientPeg.get().getUser(this.props.member.userId) || {};
             var memberState = this._calculateOpsPermissions();
-            memberState.presence = usr.presence || "offline";
-            memberState.active = usr.lastActiveAgo || -1;
             this.setState(memberState);
         }
     },
 
-    componentWillUnmount: function() {
-        MatrixClientPeg.get().removeListener("User.presence", this.userPresenceFn);
-        MatrixClientPeg.get().removeListener(
-            "RoomMember.powerLevel", this.updatePowerLevelFn
-        );
-    },
-
     onKick: function() {
         var roomId = this.props.member.roomId;
         var target = this.props.member.userId;
@@ -244,8 +203,6 @@ module.exports = {
 
     getInitialState: function() {
         return {
-            presence: "offline",
-            active: -1,
             can: {
                 kick: false,
                 ban: false,
diff --git a/src/controllers/molecules/MemberTile.js b/src/controllers/molecules/MemberTile.js
index 19c08731..38bf43d6 100644
--- a/src/controllers/molecules/MemberTile.js
+++ b/src/controllers/molecules/MemberTile.js
@@ -32,264 +32,9 @@ module.exports = {
     //     });
     // },
 
-    onKick: function() {
-        var roomId = this.props.member.roomId;
-        var target = this.props.member.userId;
-        var self = this;
-        MatrixClientPeg.get().kick(roomId, target).done(function() {
-            // NO-OP; rely on the m.room.member event coming down else we could
-            // get out of sync if we force setState here!
-            console.log("Kick success");
-        }, function(err) {
-            Modal.createDialog(ErrorDialog, {
-                title: "Kick error",
-                description: err.message
-            });
-        });
-    },
-
-    onBan: function() {
-        var roomId = this.props.member.roomId;
-        var target = this.props.member.userId;
-        var self = this;
-        MatrixClientPeg.get().ban(roomId, target).done(function() {
-            // NO-OP; rely on the m.room.member event coming down else we could
-            // get out of sync if we force setState here!
-            console.log("Ban success");
-        }, function(err) {
-            Modal.createDialog(ErrorDialog, {
-                title: "Ban error",
-                description: err.message
-            });
-        });
-    },
-
-    onMuteToggle: function() {
-        var roomId = this.props.member.roomId;
-        var target = this.props.member.userId;
-        var self = this;
-        var room = MatrixClientPeg.get().getRoom(roomId);
-        if (!room) {
-            return;
-        }
-        var powerLevelEvent = room.currentState.getStateEvents(
-            "m.room.power_levels", ""
-        );
-        if (!powerLevelEvent) {
-            return;
-        }
-        var isMuted = this.state.muted;
-        var powerLevels = powerLevelEvent.getContent();
-        var levelToSend = (
-            (powerLevels.events ? powerLevels.events["m.room.message"] : null) ||
-            powerLevels.events_default
-        );
-        var level;
-        if (isMuted) { // unmute
-            level = levelToSend;
-        }
-        else { // mute
-            level = levelToSend - 1;
-        }
-
-        MatrixClientPeg.get().setPowerLevel(roomId, target, level, powerLevelEvent).done(
-        function() {
-            // NO-OP; rely on the m.room.member event coming down else we could
-            // get out of sync if we force setState here!
-            console.log("Mute toggle success");
-        }, function(err) {
-            Modal.createDialog(ErrorDialog, {
-                title: "Mute error",
-                description: err.message
-            });
-        });
-    },
-
-    onModToggle: function() {
-        var roomId = this.props.member.roomId;
-        var target = this.props.member.userId;
-        var room = MatrixClientPeg.get().getRoom(roomId);
-        if (!room) {
-            return;
-        }
-        var powerLevelEvent = room.currentState.getStateEvents(
-            "m.room.power_levels", ""
-        );
-        if (!powerLevelEvent) {
-            return;
-        }
-        var me = room.getMember(MatrixClientPeg.get().credentials.userId);
-        if (!me) {
-            return;
-        }
-        var defaultLevel = powerLevelEvent.getContent().users_default;
-        var modLevel = me.powerLevel - 1;
-        // toggle the level
-        var newLevel = this.state.isTargetMod ? defaultLevel : modLevel;
-        MatrixClientPeg.get().setPowerLevel(roomId, target, newLevel, powerLevelEvent).done(
-        function() {
-            // NO-OP; rely on the m.room.member event coming down else we could
-            // get out of sync if we force setState here!
-            console.log("Mod toggle success");
-        }, function(err) {
-            Modal.createDialog(ErrorDialog, {
-                title: "Mod error",
-                description: err.message
-            });
-        });
-    },
-
-    onChatClick: function() {
-        // check if there are any existing rooms with just us and them (1:1)
-        // If so, just view that room. If not, create a private room with them.
-        var rooms = MatrixClientPeg.get().getRooms();
-        var userIds = [
-            this.props.member.userId,
-            MatrixClientPeg.get().credentials.userId
-        ];
-        var existingRoomId = null;
-        for (var i = 0; i < rooms.length; i++) {
-            var members = rooms[i].getJoinedMembers();
-            if (members.length === 2) {
-                var hasTargetUsers = true;
-                for (var j = 0; j < members.length; j++) {
-                    if (userIds.indexOf(members[j].userId) === -1) {
-                        hasTargetUsers = false;
-                        break;
-                    }
-                }
-                if (hasTargetUsers) {
-                    existingRoomId = rooms[i].roomId;
-                    break;
-                }
-            }
-        }
-
-        if (existingRoomId) {
-            dis.dispatch({
-                action: 'view_room',
-                room_id: existingRoomId
-            });
-        }
-        else {
-            MatrixClientPeg.get().createRoom({
-                invite: [this.props.member.userId],
-                preset: "private_chat"
-            }).done(function(res) {
-                dis.dispatch({
-                    action: 'view_room',
-                    room_id: res.room_id
-                });
-            }, function(err) {
-                console.error(
-                    "Failed to create room: %s", JSON.stringify(err)
-                );
-            });
-        }
-    },
-
-    onLeaveClick: function() {
-        var roomId = this.props.member.roomId;
-        Modal.createDialog(QuestionDialog, {
-            title: "Leave room",
-            description: "Are you sure you want to leave the room?",
-            onFinished: function(should_leave) {
-                if (should_leave) {
-                    var d = MatrixClientPeg.get().leave(roomId);
-
-                    var modal = Modal.createDialog(Loader);
-
-                    d.then(function() {
-                        modal.close();
-                        dis.dispatch({action: 'view_next_room'});
-                    }, function(err) {
-                        modal.close();
-                        Modal.createDialog(ErrorDialog, {
-                            title: "Failed to leave room",
-                            description: err.toString()
-                        });
-                    });
-                }
-            }
-        });
-    },
-
     getInitialState: function() {
         return {
             hover: false,
-            menu: false,
-
-            // presence: "offline",
-            // active: -1,
-            can: {
-                kick: false,
-                ban: false,
-                mute: false,
-                modifyLevel: false
-            },
-            muted: false,
-            isTargetMod: false,
         }
     },
-
-    _calculateOpsPermissions: function() {
-        var defaultPerms = {
-            can: {},
-            muted: false,
-            modifyLevel: false
-        };
-        var room = MatrixClientPeg.get().getRoom(this.props.member.roomId);
-        if (!room) {
-            return defaultPerms;
-        }
-        var powerLevels = room.currentState.getStateEvents(
-            "m.room.power_levels", ""
-        );
-        if (!powerLevels) {
-            return defaultPerms;
-        }
-        var me = room.getMember(MatrixClientPeg.get().credentials.userId);
-        var them = this.props.member;
-        return {
-            can: this._calculateCanPermissions(
-                me, them, powerLevels.getContent()
-            ),
-            muted: this._isMuted(them, powerLevels.getContent()),
-            isTargetMod: them.powerLevel > powerLevels.getContent().users_default
-        };
-    },
-
-    _calculateCanPermissions: function(me, them, powerLevels) {
-        var can = {
-            kick: false,
-            ban: false,
-            mute: false,
-            modifyLevel: false
-        };
-        var canAffectUser = them.powerLevel < me.powerLevel;
-        if (!canAffectUser) {
-            //console.log("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel);
-            return can;
-        }
-        var editPowerLevel = (
-            (powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) ||
-            powerLevels.state_default
-        );
-        can.kick = me.powerLevel >= powerLevels.kick;
-        can.ban = me.powerLevel >= powerLevels.ban;
-        can.mute = me.powerLevel >= editPowerLevel;
-        can.modifyLevel = me.powerLevel > them.powerLevel;
-        return can;
-    },
-
-    _isMuted: function(member, powerLevelContent) {
-        if (!powerLevelContent || !member) {
-            return false;
-        }
-        var levelToSend = (
-            (powerLevelContent.events ? powerLevelContent.events["m.room.message"] : null) ||
-            powerLevelContent.events_default
-        );
-        return member.powerLevel < levelToSend;
-    },
 };