diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js
index 18567b57..dedd715f 100644
--- a/src/components/structures/RightPanel.js
+++ b/src/components/structures/RightPanel.js
@@ -22,7 +22,7 @@ import classNames from 'classnames';
 import { _t } from 'matrix-react-sdk/lib/languageHandler';
 import sdk from 'matrix-react-sdk';
 import dis from 'matrix-react-sdk/lib/dispatcher';
-import MatrixClient from 'matrix-js-sdk';
+import { MatrixClient } from 'matrix-js-sdk';
 import Analytics from 'matrix-react-sdk/lib/Analytics';
 import rate_limited_func from 'matrix-react-sdk/lib/ratelimitedfunc';
 import AccessibleButton from 'matrix-react-sdk/lib/components/views/elements/AccessibleButton';
@@ -90,6 +90,7 @@ module.exports = React.createClass({
         RoomMemberList: 'RoomMemberList',
         GroupMemberList: 'GroupMemberList',
         GroupRoomList: 'GroupRoomList',
+        GroupRoomInfo: 'GroupRoomInfo',
         FilePanel: 'FilePanel',
         NotificationPanel: 'NotificationPanel',
         RoomMemberInfo: 'RoomMemberInfo',
@@ -205,7 +206,6 @@ module.exports = React.createClass({
                 } else if (this.props.groupId) {
                     this.setState({
                         phase: this.Phase.GroupMemberList,
-                        groupId: payload.groupId,
                         member: payload.member,
                     });
                 }
@@ -213,13 +213,20 @@ module.exports = React.createClass({
         } else if (payload.action === "view_group") {
             this.setState({
                 phase: this.Phase.GroupMemberList,
-                groupId: payload.groupId,
                 member: null,
             });
+        } else if (payload.action === "view_group_room") {
+            this.setState({
+                phase: this.Phase.GroupRoomInfo,
+                groupRoomId: payload.groupRoomId,
+            });
+        } else if (payload.action === "view_group_room_list") {
+            this.setState({
+                phase: this.Phase.GroupRoomList,
+            });
         } else if (payload.action === "view_group_user") {
             this.setState({
                 phase: this.Phase.GroupMemberInfo,
-                groupId: payload.groupId,
                 member: payload.member,
             });
         } else if (payload.action === "view_room") {
@@ -242,6 +249,7 @@ module.exports = React.createClass({
         const GroupMemberList = sdk.getComponent('groups.GroupMemberList');
         const GroupMemberInfo = sdk.getComponent('groups.GroupMemberInfo');
         const GroupRoomList = sdk.getComponent('groups.GroupRoomList');
+        const GroupRoomInfo = sdk.getComponent('groups.GroupRoomInfo');
 
         const TintableSvg = sdk.getComponent("elements.TintableSvg");
 
@@ -305,7 +313,7 @@ module.exports = React.createClass({
                     analytics={['Right Panel', 'Group Member List Button', 'click']}
                 />,
                 <HeaderButton key="_roomsButton" title={_t('Rooms')} iconSrc="img/icons-room.svg"
-                    isHighlighted={this.state.phase === this.Phase.GroupRoomList}
+                    isHighlighted={[this.Phase.GroupRoomList, this.Phase.GroupRoomInfo].includes(this.state.phase)}
                     clickPhase={this.Phase.GroupRoomList}
                     analytics={['Right Panel', 'Group Room List Button', 'click']}
                 />,
@@ -340,6 +348,11 @@ module.exports = React.createClass({
                     groupMember={this.state.member}
                     groupId={this.props.groupId}
                     key={this.state.member.user_id} />;
+            } else if (this.state.phase == this.Phase.GroupRoomInfo) {
+                panel = <GroupRoomInfo
+                    groupRoomId={this.state.groupRoomId}
+                    groupId={this.props.groupId}
+                    key={this.state.groupRoomId} />;
             } else if (this.state.phase == this.Phase.NotificationPanel) {
                 panel = <NotificationPanel />;
             } else if (this.state.phase == this.Phase.FilePanel) {
diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js
index 323af86c..126ae404 100644
--- a/src/components/structures/RoomDirectory.js
+++ b/src/components/structures/RoomDirectory.js
@@ -409,7 +409,7 @@ module.exports = React.createClass({
 
             perms = null;
             if (guestRead || guestJoin) {
-                perms = <div className="mx_RoomDirectory_perms">{guestRead} {guestJoin}</div>;
+                perms = <div className="mx_RoomDirectory_perms">{guestRead}{guestJoin}</div>;
             }
 
             var topic = rooms[i].topic || '';
diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js
index 8fb7562f..c3f51bc8 100644
--- a/src/components/structures/RoomSubList.js
+++ b/src/components/structures/RoomSubList.js
@@ -246,10 +246,14 @@ var RoomSubList = React.createClass({
     roomNotificationCount: function(truncateAt) {
         var self = this;
 
+        if (this.props.isInvite) {
+            return [0, true];
+        }
+
         return this.props.list.reduce(function(result, room, index) {
             if (truncateAt === undefined || index >= truncateAt) {
                 var roomNotifState = RoomNotifs.getRoomNotifsState(room.roomId);
-                var highlight = room.getUnreadNotificationCount('highlight') > 0 || self.props.isInvite;
+                var highlight = room.getUnreadNotificationCount('highlight') > 0;
                 var notificationCount = room.getUnreadNotificationCount();
 
                 const notifBadges = notificationCount > 0 && self._shouldShowNotifBadge(roomNotifState);
@@ -394,7 +398,8 @@ var RoomSubList = React.createClass({
         var subListNotifCount = subListNotifications[0];
         var subListNotifHighlight = subListNotifications[1];
 
-        var roomCount = this.props.list.length > 0 ? this.props.list.length : '';
+        var totalTiles = this.props.list.length + (this.props.extraTiles || []).length;
+        var roomCount = totalTiles > 0 ? totalTiles : '';
 
         var chevronClasses = classNames({
             'mx_RoomSubList_chevron': true,
diff --git a/src/components/views/dialogs/DevtoolsDialog.js b/src/components/views/dialogs/DevtoolsDialog.js
index 7760ea84..a7155ad1 100644
--- a/src/components/views/dialogs/DevtoolsDialog.js
+++ b/src/components/views/dialogs/DevtoolsDialog.js
@@ -15,35 +15,24 @@ limitations under the License.
 */
 
 import React from 'react';
+import PropTypes from 'prop-types';
 import sdk from 'matrix-react-sdk';
 import { _t } from 'matrix-react-sdk/lib/languageHandler';
 import MatrixClientPeg from 'matrix-react-sdk/lib/MatrixClientPeg';
 
-class SendCustomEvent extends React.Component {
-    static propTypes = {
-        roomId: React.PropTypes.string.isRequired,
-        onBack: React.PropTypes.func.isRequired,
-
-        eventType: React.PropTypes.string.isRequired,
-        evContent: React.PropTypes.string.isRequired,
+class DevtoolsComponent extends React.Component {
+    static contextTypes = {
+        roomId: PropTypes.string.isRequired,
     };
+}
 
-    static defaultProps = {
-        eventType: '',
-        evContent: '{\n\n}',
-    };
+class GenericEditor extends DevtoolsComponent {
+    // static propTypes = {onBack: PropTypes.func.isRequired};
 
     constructor(props, context) {
         super(props, context);
-        this._send = this._send.bind(this);
-        this.onBack = this.onBack.bind(this);
         this._onChange = this._onChange.bind(this);
-
-        this.state = {
-            message: null,
-            input_eventType: this.props.eventType,
-            input_evContent: this.props.evContent,
-        };
+        this.onBack = this.onBack.bind(this);
     }
 
     onBack() {
@@ -54,6 +43,10 @@ class SendCustomEvent extends React.Component {
         }
     }
 
+    _onChange(e) {
+        this.setState({[e.target.id]: e.target.type === 'checkbox' ? e.target.checked : e.target.value});
+    }
+
     _buttons() {
         return <div className="mx_Dialog_buttons">
             <button onClick={this.onBack}>{ _t('Back') }</button>
@@ -61,19 +54,64 @@ class SendCustomEvent extends React.Component {
         </div>;
     }
 
+    textInput(id, label) {
+        return <div className="mx_DevTools_inputRow">
+            <div className="mx_DevTools_inputLabelCell">
+                <label htmlFor={id}>{ label }</label>
+            </div>
+            <div className="mx_DevTools_inputCell">
+                <input id={id} onChange={this._onChange} value={this.state[id]} size="32" />
+            </div>
+        </div>;
+    }
+}
+
+class SendCustomEvent extends GenericEditor {
+    static getLabel() { return _t('Send Custom Event'); }
+
+    static propTypes = {
+        onBack: PropTypes.func.isRequired,
+        forceStateEvent: PropTypes.bool,
+        inputs: PropTypes.object,
+    };
+
+    constructor(props, context) {
+        super(props, context);
+        this._send = this._send.bind(this);
+
+        const {eventType, stateKey, evContent} = Object.assign({
+            eventType: '',
+            stateKey: '',
+            evContent: '{\n\n}',
+        }, this.props.inputs);
+
+        this.state = {
+            isStateEvent: Boolean(this.props.forceStateEvent),
+
+            eventType,
+            stateKey,
+            evContent,
+        };
+    }
+
     send(content) {
-        return MatrixClientPeg.get().sendEvent(this.props.roomId, this.state.input_eventType, content);
+        const cli = MatrixClientPeg.get();
+        if (this.state.isStateEvent) {
+            return cli.sendStateEvent(this.context.roomId, this.state.eventType, content, this.state.stateKey);
+        } else {
+            return cli.sendEvent(this.context.roomId, this.state.eventType, content);
+        }
     }
 
     async _send() {
-        if (this.state.input_eventType === '') {
+        if (this.state.eventType === '') {
             this.setState({ message: _t('You must specify an event type!') });
             return;
         }
 
         let message;
         try {
-            const content = JSON.parse(this.state.input_evContent);
+            const content = JSON.parse(this.state.evContent);
             await this.send(content);
             message = _t('Event sent!');
         } catch (e) {
@@ -82,14 +120,6 @@ class SendCustomEvent extends React.Component {
         this.setState({ message });
     }
 
-    _additionalFields() {
-        return <div />;
-    }
-
-    _onChange(e) {
-        this.setState({[`input_${e.target.id}`]: e.target.value});
-    }
-
     render() {
         if (this.state.message) {
             return <div>
@@ -102,87 +132,176 @@ class SendCustomEvent extends React.Component {
 
         return <div>
             <div className="mx_Dialog_content">
-                { this._additionalFields() }
-                <div className="mx_TextInputDialog_label">
-                    <label htmlFor="eventType"> { _t('Event Type') } </label>
-                </div>
-                <div>
-                    <input id="eventType" onChange={this._onChange} value={this.state.input_eventType} className="mx_TextInputDialog_input" size="64" />
-                </div>
+                { this.textInput('eventType', _t('Event Type')) }
+                { this.state.isStateEvent && this.textInput('stateKey', _t('State Key')) }
 
-                <div className="mx_TextInputDialog_label">
+                <br />
+
+                <div className="mx_UserSettings_profileLabelCell">
                     <label htmlFor="evContent"> { _t('Event Content') } </label>
                 </div>
                 <div>
-                    <textarea id="evContent" onChange={this._onChange} value={this.state.input_evContent} className="mx_TextInputDialog_input" cols="63" rows="5" />
+                    <textarea id="evContent" onChange={this._onChange} value={this.state.evContent} className="mx_TextInputDialog_input" cols="63" rows="5" />
                 </div>
             </div>
-            { this._buttons() }
+            <div className="mx_Dialog_buttons">
+                <button onClick={this.onBack}>{ _t('Back') }</button>
+                { !this.state.message && <button onClick={this._send}>{ _t('Send') }</button> }
+                { !this.state.message && !this.props.forceStateEvent && <div style={{float: "right"}}>
+                    <input id="isStateEvent" className="mx_DevTools_tgl mx_DevTools_tgl-flip" type="checkbox" onChange={this._onChange} checked={this.state.isStateEvent} />
+                    <label className="mx_DevTools_tgl-btn" data-tg-off="Event" data-tg-on="State Event" htmlFor="isStateEvent" />
+                </div> }
+            </div>
         </div>;
     }
 }
 
-class SendCustomStateEvent extends SendCustomEvent {
+class SendAccountData extends GenericEditor {
+    static getLabel() { return _t('Send Account Data'); }
+
     static propTypes = {
-        roomId: React.PropTypes.string.isRequired,
-        onBack: React.PropTypes.func.isRequired,
-
-        eventType: React.PropTypes.string.isRequired,
-        evContent: React.PropTypes.string.isRequired,
-        stateKey: React.PropTypes.string.isRequired,
-    };
-
-    static defaultProps = {
-        eventType: '',
-        evContent: '{\n\n}',
-        stateKey: '',
+        isRoomAccountData: PropTypes.bool,
+        forceMode: PropTypes.bool,
+        inputs: PropTypes.object,
     };
 
     constructor(props, context) {
         super(props, context);
-        this.state['input_stateKey'] = this.props.stateKey;
+        this._send = this._send.bind(this);
+
+        const {eventType, evContent} = Object.assign({
+            eventType: '',
+            evContent: '{\n\n}',
+        }, this.props.inputs);
+
+        this.state = {
+            isRoomAccountData: Boolean(this.props.isRoomAccountData),
+
+            eventType,
+            evContent,
+        };
     }
 
     send(content) {
         const cli = MatrixClientPeg.get();
-        return cli.sendStateEvent(this.props.roomId, this.state.input_eventType, content, this.state.input_stateKey);
+        if (this.state.isRoomAccountData) {
+            return cli.setRoomAccountData(this.context.roomId, this.state.eventType, content);
+        }
+        return cli.setAccountData(this.state.eventType, content);
     }
 
-    _additionalFields() {
+    async _send() {
+        if (this.state.eventType === '') {
+            this.setState({ message: _t('You must specify an event type!') });
+            return;
+        }
+
+        let message;
+        try {
+            const content = JSON.parse(this.state.evContent);
+            await this.send(content);
+            message = _t('Event sent!');
+        } catch (e) {
+            message = _t('Failed to send custom event.') + ' (' + e.toString() + ')';
+        }
+        this.setState({ message });
+    }
+
+    render() {
+        if (this.state.message) {
+            return <div>
+                <div className="mx_Dialog_content">
+                    { this.state.message }
+                </div>
+                { this._buttons() }
+            </div>;
+        }
+
         return <div>
-            <div className="mx_TextInputDialog_label">
-                <label htmlFor="stateKey"> { _t('State Key') } </label>
+            <div className="mx_Dialog_content">
+                { this.textInput('eventType', _t('Event Type')) }
+                <br />
+
+                <div className="mx_UserSettings_profileLabelCell">
+                    <label htmlFor="evContent"> { _t('Event Content') } </label>
+                </div>
+                <div>
+                    <textarea id="evContent" onChange={this._onChange} value={this.state.evContent} className="mx_TextInputDialog_input" cols="63" rows="5" />
+                </div>
             </div>
-            <div>
-                <input id="stateKey" onChange={this._onChange} value={this.state.input_stateKey} className="mx_TextInputDialog_input" size="64" />
+            <div className="mx_Dialog_buttons">
+                <button onClick={this.onBack}>{ _t('Back') }</button>
+                { !this.state.message && <button onClick={this._send}>{ _t('Send') }</button> }
+                { !this.state.message && <div style={{float: "right"}}>
+                    <input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip" type="checkbox" onChange={this._onChange} checked={this.state.isRoomAccountData} disabled={this.props.forceMode} />
+                    <label className="mx_DevTools_tgl-btn" data-tg-off="Account Data" data-tg-on="Room Data" htmlFor="isRoomAccountData" />
+                </div> }
             </div>
         </div>;
     }
 }
 
-class RoomStateExplorer extends React.Component {
+class FilteredList extends React.Component {
     static propTypes = {
-        setMode: React.PropTypes.func.isRequired,
-        roomId: React.PropTypes.string.isRequired,
-        onBack: React.PropTypes.func.isRequired,
+        children: PropTypes.any,
+    };
+
+    constructor(props, context) {
+        super(props, context);
+        this.onQuery = this.onQuery.bind(this);
+
+        this.state = {
+            query: '',
+        };
+    }
+
+    onQuery(ev) {
+        this.setState({ query: ev.target.value });
+    }
+
+    filterChildren() {
+        if (this.state.query) {
+            const lowerQuery = this.state.query.toLowerCase();
+            return this.props.children.filter((child) => child.key.toLowerCase().includes(lowerQuery));
+        }
+        return this.props.children;
+    }
+
+    render() {
+        return <div>
+            <input size="64"
+                   onChange={this.onQuery}
+                   value={this.state.query}
+                   placeholder={_t('Filter results')}
+                   className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query" />
+            { this.filterChildren() }
+        </div>;
+    }
+}
+
+class RoomStateExplorer extends DevtoolsComponent {
+    static getLabel() { return _t('Explore Room State'); }
+
+
+    static propTypes = {
+        onBack: PropTypes.func.isRequired,
     };
 
     constructor(props, context) {
         super(props, context);
 
-        const room = MatrixClientPeg.get().getRoom(this.props.roomId);
+        const room = MatrixClientPeg.get().getRoom(this.context.roomId);
         this.roomStateEvents = room.currentState.events;
 
         this.onBack = this.onBack.bind(this);
         this.editEv = this.editEv.bind(this);
-        this.onQuery = this.onQuery.bind(this);
-    }
 
-    state = {
-        query: '',
-        eventType: null,
-        event: null,
-    };
+        this.state = {
+            eventType: null,
+            event: null,
+            editing: false,
+        };
+    }
 
     browseEventType(eventType) {
         return () => {
@@ -197,7 +316,9 @@ class RoomStateExplorer extends React.Component {
     }
 
     onBack() {
-        if (this.state.event) {
+        if (this.state.editing) {
+            this.setState({ editing: false });
+        } else if (this.state.event) {
             this.setState({ event: null });
         } else if (this.state.eventType) {
             this.setState({ eventType: null });
@@ -207,20 +328,19 @@ class RoomStateExplorer extends React.Component {
     }
 
     editEv() {
-        const ev = this.state.event;
-        this.props.setMode(SendCustomStateEvent, {
-            eventType: ev.getType(),
-            evContent: JSON.stringify(ev.getContent(), null, '\t'),
-            stateKey: ev.getStateKey(),
-        });
-    }
-
-    onQuery(ev) {
-        this.setState({ query: ev.target.value });
+        this.setState({ editing: true });
     }
 
     render() {
         if (this.state.event) {
+            if (this.state.editing) {
+                return <SendCustomEvent forceStateEvent={true} onBack={this.onBack} inputs={{
+                    eventType: this.state.event.getType(),
+                    evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
+                    stateKey: this.state.event.getStateKey(),
+                }} />;
+            }
+
             return <div className="mx_ViewSource">
                 <div className="mx_Dialog_content">
                     <pre>{ JSON.stringify(this.state.event.event, null, 2) }</pre>
@@ -234,11 +354,9 @@ class RoomStateExplorer extends React.Component {
 
         const rows = [];
 
+        const classes = 'mx_DevTools_RoomStateExplorer_button';
         if (this.state.eventType === null) {
             Object.keys(this.roomStateEvents).forEach((evType) => {
-                // Skip this entry if does not contain search query
-                if (this.state.query && !evType.toLowerCase().includes(this.state.query.toLowerCase())) return;
-
                 const stateGroup = this.roomStateEvents[evType];
                 const stateKeys = Object.keys(stateGroup);
 
@@ -249,7 +367,7 @@ class RoomStateExplorer extends React.Component {
                     onClickFn = this.onViewSourceClick(stateGroup[stateKeys[0]]);
                 }
 
-                rows.push(<button className="mx_DevTools_RoomStateExplorer_button" key={evType} onClick={onClickFn}>
+                rows.push(<button className={classes} key={evType} onClick={onClickFn}>
                     { evType }
                 </button>);
             });
@@ -257,12 +375,8 @@ class RoomStateExplorer extends React.Component {
             const evType = this.state.eventType;
             const stateGroup = this.roomStateEvents[evType];
             Object.keys(stateGroup).forEach((stateKey) => {
-                // Skip this entry if does not contain search query
-                if (this.state.query && !stateKey.toLowerCase().includes(this.state.query.toLowerCase())) return;
-
                 const ev = stateGroup[stateKey];
-                rows.push(<button className="mx_DevTools_RoomStateExplorer_button" key={stateKey}
-                                  onClick={this.onViewSourceClick(ev)}>
+                rows.push(<button className={classes} key={stateKey} onClick={this.onViewSourceClick(ev)}>
                     { stateKey }
                 </button>);
             });
@@ -270,8 +384,9 @@ class RoomStateExplorer extends React.Component {
 
         return <div>
             <div className="mx_Dialog_content">
-                <input onChange={this.onQuery} placeholder={_t('Filter results')} size="64" className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query" value={this.state.query} />
-                { rows }
+                <FilteredList>
+                    { rows }
+                </FilteredList>
             </div>
             <div className="mx_Dialog_buttons">
                 <button onClick={this.onBack}>{ _t('Back') }</button>
@@ -280,40 +395,157 @@ class RoomStateExplorer extends React.Component {
     }
 }
 
-export default class DevtoolsDialog extends React.Component {
+class AccountDataExplorer extends DevtoolsComponent {
+    static getLabel() { return _t('Explore Account Data'); }
+
     static propTypes = {
-        roomId: React.PropTypes.string.isRequired,
-        onFinished: React.PropTypes.func.isRequired,
+        onBack: PropTypes.func.isRequired,
     };
 
-    state = {
-        mode: null,
-        modeArgs: {},
+    constructor(props, context) {
+        super(props, context);
+
+        this.onBack = this.onBack.bind(this);
+        this.editEv = this.editEv.bind(this);
+        this._onChange = this._onChange.bind(this);
+
+        this.state = {
+            isRoomAccountData: false,
+            event: null,
+            editing: false,
+        };
+    }
+
+    getData() {
+        const cli = MatrixClientPeg.get();
+        if (this.state.isRoomAccountData) {
+            return cli.getRoom(this.context.roomId).accountData;
+        }
+        return cli.store.accountData;
+    }
+
+    onViewSourceClick(event) {
+        return () => {
+            this.setState({ event });
+        };
+    }
+
+    onBack() {
+        if (this.state.editing) {
+            this.setState({ editing: false });
+        } else if (this.state.event) {
+            this.setState({ event: null });
+        } else {
+            this.props.onBack();
+        }
+    }
+
+    _onChange(e) {
+        this.setState({[e.target.id]: e.target.type === 'checkbox' ? e.target.checked : e.target.value});
+    }
+
+    editEv() {
+        this.setState({ editing: true });
+    }
+
+    render() {
+        if (this.state.event) {
+            if (this.state.editing) {
+                return <SendAccountData isRoomAccountData={this.state.isRoomAccountData} onBack={this.onBack} inputs={{
+                    eventType: this.state.event.getType(),
+                    evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
+                }} forceMode={true} />;
+            }
+
+            return <div className="mx_ViewSource">
+                <div className="mx_Dialog_content">
+                    <pre>{ JSON.stringify(this.state.event.event, null, 2) }</pre>
+                </div>
+                <div className="mx_Dialog_buttons">
+                    <button onClick={this.onBack}>{ _t('Back') }</button>
+                    <button onClick={this.editEv}>{ _t('Edit') }</button>
+                </div>
+            </div>;
+        }
+
+        const rows = [];
+
+        const classes = 'mx_DevTools_RoomStateExplorer_button';
+
+        const data = this.getData();
+        Object.keys(data).forEach((evType) => {
+            const ev = data[evType];
+            rows.push(<button className={classes} key={evType} onClick={this.onViewSourceClick(ev)}>
+                { evType }
+            </button>);
+        });
+
+        return <div>
+            <div className="mx_Dialog_content">
+                <FilteredList>
+                    { rows }
+                </FilteredList>
+            </div>
+            <div className="mx_Dialog_buttons">
+                <button onClick={this.onBack}>{ _t('Back') }</button>
+                { !this.state.message && <div style={{float: "right"}}>
+                    <input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip" type="checkbox" onChange={this._onChange} checked={this.state.isRoomAccountData} />
+                    <label className="mx_DevTools_tgl-btn" data-tg-off="Account Data" data-tg-on="Room Data" htmlFor="isRoomAccountData" />
+                </div> }
+            </div>
+        </div>;
+    }
+}
+
+const Entries = [
+    SendCustomEvent,
+    RoomStateExplorer,
+    SendAccountData,
+    AccountDataExplorer,
+];
+
+export default class DevtoolsDialog extends React.Component {
+    static childContextTypes = {
+        roomId: PropTypes.string.isRequired,
+        // client: PropTypes.instanceOf(MatixClient),
+    };
+
+    static propTypes = {
+        roomId: PropTypes.string.isRequired,
+        onFinished: PropTypes.func.isRequired,
     };
 
     constructor(props, context) {
         super(props, context);
         this.onBack = this.onBack.bind(this);
-        this.setMode = this.setMode.bind(this);
         this.onCancel = this.onCancel.bind(this);
+
+        this.state = {
+            mode: null,
+        };
     }
 
     componentWillUnmount() {
         this._unmounted = true;
     }
 
+    getChildContext() {
+        return { roomId: this.props.roomId };
+    }
+
     _setMode(mode) {
         return () => {
-            this.setMode(mode);
+            this.setState({ mode });
         };
     }
 
-    setMode(mode, modeArgs={}) {
-        this.setState({ mode, modeArgs });
-    }
-
     onBack() {
-        this.setState({ mode: null });
+        if (this.prevMode) {
+            this.setState({ mode: this.prevMode });
+            this.prevMode = null;
+        } else {
+            this.setState({ mode: null });
+        }
     }
 
     onCancel() {
@@ -324,14 +556,27 @@ export default class DevtoolsDialog extends React.Component {
         let body;
 
         if (this.state.mode) {
-            body =
-                <this.state.mode {...this.props} {...this.state.modeArgs} onBack={this.onBack} setMode={this.setMode} />;
-        } else {
             body = <div>
-                <div className="mx_Dialog_content">
-                    <button onClick={this._setMode(SendCustomEvent)}>{ _t('Send Custom Event') }</button>
-                    <button onClick={this._setMode(SendCustomStateEvent)}>{ _t('Send Custom State Event') }</button>
-                    <button onClick={this._setMode(RoomStateExplorer)}>{ _t('Explore Room State') }</button>
+                <div className="mx_DevTools_label_left">{ this.state.mode.getLabel() }</div>
+                <div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</div>
+                <div className="mx_DevTools_label_bottom" />
+                <this.state.mode onBack={this.onBack} />
+            </div>;
+        } else {
+            const classes = "mx_DevTools_RoomStateExplorer_button";
+            body = <div>
+                <div>
+                    <div className="mx_DevTools_label_left">{ _t('Toolbox') }</div>
+                    <div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</div>
+                    <div className="mx_DevTools_label_bottom" />
+
+                    <div className="mx_Dialog_content">
+                        { Entries.map((Entry) => {
+                            const label = Entry.getLabel();
+                            const onClick = this._setMode(Entry);
+                            return <button className={classes} key={label} onClick={onClick}>{ label }</button>;
+                        }) }
+                    </div>
                 </div>
                 <div className="mx_Dialog_buttons">
                     <button onClick={this.onCancel}>{ _t('Cancel') }</button>
@@ -342,7 +587,6 @@ export default class DevtoolsDialog extends React.Component {
         const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
         return (
             <BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished} title={_t('Developer Tools')}>
-                <div>Room ID: { this.props.roomId }</div>
                 { body }
             </BaseDialog>
         );
diff --git a/src/components/views/elements/InlineSpinner.js b/src/components/views/elements/InlineSpinner.js
new file mode 100644
index 00000000..adb916fc
--- /dev/null
+++ b/src/components/views/elements/InlineSpinner.js
@@ -0,0 +1,33 @@
+/*
+Copyright 2017 New Vector Ltd.
+
+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.
+*/
+
+const React = require('react');
+
+module.exports = React.createClass({
+    displayName: 'InlineSpinner',
+
+    render: function() {
+        var w = this.props.w || 16;
+        var h = this.props.h || 16;
+        var imgClass = this.props.imgClassName || "";
+
+        return (
+            <div className="mx_InlineSpinner">
+                <img src="img/spinner.gif" width={w} height={h} className={imgClass}/>
+            </div>
+        );
+    }
+});
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 543ee7bb..4702ae93 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -70,6 +70,7 @@
     "What's new?": "What's new?",
     "A new version of Riot is available.": "A new version of Riot is available.",
     "To return to your account in future you need to <u>set a password</u>": "To return to your account in future you need to <u>set a password</u>",
+    "Toolbox": "Toolbox",
     "Set Password": "Set Password",
     "Error encountered (%(errorDetail)s).": "Error encountered (%(errorDetail)s).",
     "Checking for an update...": "Checking for an update...",
@@ -106,7 +107,8 @@
     "Edit": "Edit",
     "Filter results": "Filter results",
     "Send Custom Event": "Send Custom Event",
-    "Send Custom State Event": "Send Custom State Event",
+    "Send Account Data": "Send Account Data",
+    "Explore Account Data": "Explore Account Data",
     "Explore Room State": "Explore Room State",
     "Developer Tools": "Developer Tools",
     "You have successfully set a password!": "You have successfully set a password!",
diff --git a/src/skins/vector/css/_components.scss b/src/skins/vector/css/_components.scss
index 82c2c186..230b5ae4 100644
--- a/src/skins/vector/css/_components.scss
+++ b/src/skins/vector/css/_components.scss
@@ -35,7 +35,6 @@
 @import "./matrix-react-sdk/views/elements/_ProgressBar.scss";
 @import "./matrix-react-sdk/views/elements/_RichText.scss";
 @import "./matrix-react-sdk/views/elements/_RoleButton.scss";
-@import "./matrix-react-sdk/views/groups/_GroupInviteTile.scss";
 @import "./matrix-react-sdk/views/groups/_GroupRoomList.scss";
 @import "./matrix-react-sdk/views/login/_InteractiveAuthEntryComponents.scss";
 @import "./matrix-react-sdk/views/login/_ServerConfig.scss";
@@ -55,6 +54,8 @@
 @import "./matrix-react-sdk/views/rooms/_MemberInfo.scss";
 @import "./matrix-react-sdk/views/rooms/_MemberList.scss";
 @import "./matrix-react-sdk/views/rooms/_MessageComposer.scss";
+@import "./matrix-react-sdk/views/rooms/_PinnedEventTile.scss";
+@import "./matrix-react-sdk/views/rooms/_PinnedEventsPanel.scss";
 @import "./matrix-react-sdk/views/rooms/_PresenceLabel.scss";
 @import "./matrix-react-sdk/views/rooms/_RoomHeader.scss";
 @import "./matrix-react-sdk/views/rooms/_RoomList.scss";
@@ -68,8 +69,6 @@
 @import "./matrix-react-sdk/views/voip/_CallView.scss";
 @import "./matrix-react-sdk/views/voip/_IncomingCallbox.scss";
 @import "./matrix-react-sdk/views/voip/_VideoView.scss";
-@import "./matrix-react-sdk/views/rooms/_PinnedEventsPanel.scss";
-@import "./matrix-react-sdk/views/rooms/_PinnedEventTile.scss";
 @import "./vector-web/_fonts.scss";
 @import "./vector-web/structures/_CompatibilityPage.scss";
 @import "./vector-web/structures/_HomePage.scss";
@@ -86,6 +85,7 @@
 @import "./vector-web/views/dialogs/_SetPasswordDialog.scss";
 @import "./vector-web/views/directory/_NetworkDropdown.scss";
 @import "./vector-web/views/elements/_ImageView.scss";
+@import "./vector-web/views/elements/_InlineSpinner.scss";
 @import "./vector-web/views/elements/_Spinner.scss";
 @import "./vector-web/views/globals/_MatrixToolbar.scss";
 @import "./vector-web/views/messages/_DateSeparator.scss";
diff --git a/src/skins/vector/css/matrix-react-sdk/structures/_GroupView.scss b/src/skins/vector/css/matrix-react-sdk/structures/_GroupView.scss
index 1a92fc10..583ab2ce 100644
--- a/src/skins/vector/css/matrix-react-sdk/structures/_GroupView.scss
+++ b/src/skins/vector/css/matrix-react-sdk/structures/_GroupView.scss
@@ -68,7 +68,7 @@ limitations under the License.
     box-shadow: none;
 }
 
-.mx_GroupView_header_name:hover div:not(.mx_GroupView_editable) {
+.mx_GroupView_header_isUserMember .mx_GroupView_header_name:hover div:not(.mx_GroupView_editable) {
     color: $accent-color;
     cursor: pointer;
 }
@@ -204,6 +204,7 @@ limitations under the License.
     flex-grow: 1;
     border-top: 1px solid $primary-hairline-color;
     padding-top: 10px;
+    word-break: break-word;
 }
 
 .mx_GroupView .mx_RoomView_messageListWrapper {
diff --git a/src/skins/vector/css/matrix-react-sdk/structures/_MyGroups.scss b/src/skins/vector/css/matrix-react-sdk/structures/_MyGroups.scss
index ddcf46fb..d7cbda9a 100644
--- a/src/skins/vector/css/matrix-react-sdk/structures/_MyGroups.scss
+++ b/src/skins/vector/css/matrix-react-sdk/structures/_MyGroups.scss
@@ -18,6 +18,9 @@ limitations under the License.
     max-width: 960px;
     margin-left: auto;
     margin-right: auto;
+
+    display: flex;
+    flex-direction: column;
 }
 
 .mx_MyGroups .mx_RoomHeader_simpleHeader {
@@ -61,10 +64,26 @@ limitations under the License.
 /* Until the button is wired up */
 .mx_MyGroups_joinBox {
     visibility: hidden;
+
+    /* When joinBox wraps onto its own row, it should take up zero height so
+       that there isn't an awkward gap between MyGroups_createBox and
+       MyGroups_content.
+    */
+    height: 0px;
+    margin: 0px;
 }
 
 .mx_MyGroups_content {
     margin-left: 2px;
+
+    flex: 1 0 0;
+
+    display: flex;
+    flex-direction: column;
+}
+
+.mx_MyGroups_content h3 {
+    margin-bottom: 10px;
 }
 
 .mx_MyGroups_placeholder {
@@ -75,19 +94,22 @@ limitations under the License.
     text-align: center;
 }
 
-.mx_MyGroups_joinedGroups {
+.mx_MyGroups_joinedGroups .gm-scroll-view {
+    border-top: 1px solid $primary-hairline-color;
+    overflow-x: hidden;
+
     display: flex;
     flex-direction: row;
     flex-flow: wrap;
-    justify-content: space-around;
+    align-content: flex-start;
 }
 
-.mx_MyGroups_joinedGroups .mx_GroupTile {
+.mx_MyGroups_joinedGroups .gm-scroll-view .mx_GroupTile {
     min-width: 300px;
-    flex: 1 0 25%;
+    max-width: 33%;
+    flex: 1 0 300px;
     height: 75px;
-    margin-bottom: 15px;
-    margin-right: 10px;
+    margin: 10px 0px;
     display: flex;
     align-items: flex-start;
     cursor: pointer;
@@ -100,6 +122,12 @@ limitations under the License.
     justify-content: center;
 }
 
+.mx_GroupTile_profile h3.mx_GroupTile_name,
+.mx_GroupTile_profile .mx_GroupTile_groupId,
+.mx_GroupTile_profile .mx_GroupTile_desc {
+    padding-right: 10px;
+}
+
 .mx_GroupTile_profile h3.mx_GroupTile_name {
     margin: 0px;
     font-size: 15px;
diff --git a/src/skins/vector/css/matrix-react-sdk/views/dialogs/_CreateGroupDialog.scss b/src/skins/vector/css/matrix-react-sdk/views/dialogs/_CreateGroupDialog.scss
index ebe89027..500e12ee 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/dialogs/_CreateGroupDialog.scss
+++ b/src/skins/vector/css/matrix-react-sdk/views/dialogs/_CreateGroupDialog.scss
@@ -33,3 +33,30 @@ limitations under the License.
     background-color: $primary-bg-color;
 }
 
+.mx_CreateGroupDialog_input_hasPrefixAndSuffix {
+    border-radius: 0px;
+}
+
+.mx_CreateGroupDialog_input_group {
+    display: flex;
+}
+
+.mx_CreateGroupDialog_prefix,
+.mx_CreateGroupDialog_suffix {
+    height: 35px;
+    padding: 0px 5px;
+    line-height: 37px;
+    background-color: $input-border-color;
+    border: 1px solid $input-border-color;
+    text-align: center;
+}
+
+.mx_CreateGroupDialog_prefix {
+    border-right: 0px;
+    border-radius: 3px 0px 0px 3px;
+}
+
+.mx_CreateGroupDialog_suffix {
+    border-left: 0px;
+    border-radius: 0px 3px 3px 0px;
+}
diff --git a/src/skins/vector/css/matrix-react-sdk/views/elements/_RichText.scss b/src/skins/vector/css/matrix-react-sdk/views/elements/_RichText.scss
index 0927cfbc..8b7c8f06 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/elements/_RichText.scss
+++ b/src/skins/vector/css/matrix-react-sdk/views/elements/_RichText.scss
@@ -3,7 +3,8 @@
 // --Matthew
 
 .mx_UserPill,
-.mx_RoomPill {
+.mx_RoomPill,
+.mx_AtRoomPill {
     border-radius: 16px;
     display: inline-block;
     height: 20px;
@@ -24,7 +25,8 @@
     padding-right: 5px;
 }
 
-.mx_EventTile_highlight .mx_EventTile_content .markdown-body a.mx_UserPill_me {
+.mx_EventTile_highlight .mx_EventTile_content .markdown-body a.mx_UserPill_me,
+.mx_EventTile_content .mx_AtRoomPill {
     color: $accent-fg-color;
     background-color: $mention-user-pill-bg-color;
     padding-right: 5px;
@@ -39,7 +41,8 @@
 }
 
 .mx_UserPill .mx_BaseAvatar,
-.mx_RoomPill .mx_BaseAvatar {
+.mx_RoomPill .mx_BaseAvatar,
+.mx_AtRoomPill .mx_BaseAvatar {
     position: relative;
     left: -3px;
     top: 2px;
diff --git a/src/skins/vector/css/matrix-react-sdk/views/groups/_GroupInviteTile.scss b/src/skins/vector/css/matrix-react-sdk/views/groups/_GroupInviteTile.scss
deleted file mode 100644
index 6b4034b9..00000000
--- a/src/skins/vector/css/matrix-react-sdk/views/groups/_GroupInviteTile.scss
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
-Copyright 2017 New Vector Ltd
-
-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.
-*/
-
-.mx_GroupInviteTile {
-    position: relative;
-    cursor: pointer;
-    font-size: 13px;
-    display: block;
-    height: 34px;
-}
-
-.mx_GroupInviteTile_nameContainer {
-    display: inline-block;
-    width: 180px;
-    height: 24px;
-}
-
-.mx_GroupInviteTile_avatarContainer {
-    display: inline-block;
-    padding-top: 5px;
-    padding-bottom: 5px;
-    padding-left: 16px;
-    padding-right: 6px;
-    width: 24px;
-    height: 24px;
-    vertical-align: middle;
-}
-
-.mx_GroupInviteTile_name {
-    display: inline-block;
-    position: relative;
-    width: 165px;
-    vertical-align: middle;
-    padding-left: 6px;
-    padding-right: 6px;
-    padding-top: 2px;
-    padding-bottom: 3px;
-    color: $roomtile-name-color;
-    white-space: nowrap;
-    overflow: hidden;
-    text-overflow: ellipsis;
-}
-
-.mx_GroupInviteTile_badge {
-    display: inline-block;
-    min-width: 15px;
-    height: 15px;
-    position: absolute;
-    right: 8px; /*gutter */
-    top: 9px;
-    border-radius: 8px;
-    color: $accent-fg-color;
-    background-color: $group-alert-color;
-    font-weight: 600;
-    font-size: 10px;
-    text-align: center;
-    padding-top: 1px;
-    padding-left: 4px;
-    padding-right: 4px;
-}
-
diff --git a/src/skins/vector/css/matrix-react-sdk/views/groups/_GroupRoomList.scss b/src/skins/vector/css/matrix-react-sdk/views/groups/_GroupRoomList.scss
index 91f0c347..fb41ebaa 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/groups/_GroupRoomList.scss
+++ b/src/skins/vector/css/matrix-react-sdk/views/groups/_GroupRoomList.scss
@@ -19,18 +19,3 @@ limitations under the License.
     color: $primary-fg-color;
     cursor: pointer;
 }
-
-.mx_GroupRoomTile_delete {
-    opacity: 0.4;
-    position: absolute;
-    top: 6px;
-    right: 10px;
-    cursor: pointer;
-
-    display: none;
-}
-
-.mx_GroupRoomTile:hover > .mx_GroupRoomTile_delete {
-    display: initial;
-}
-
diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/_AppsDrawer.scss b/src/skins/vector/css/matrix-react-sdk/views/rooms/_AppsDrawer.scss
index 14b8cb45..f9c09287 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/rooms/_AppsDrawer.scss
+++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/_AppsDrawer.scss
@@ -74,7 +74,7 @@ limitations under the License.
     margin: 0;
     padding: 2px 10px;
     // background-color: $e2e-verified-color;
-    // border-bottom: 1px solid $primary-hairline-color;
+    border-bottom: 1px solid $primary-hairline-color;
     font-size: 10px;
 }
 
diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/_EntityTile.scss b/src/skins/vector/css/matrix-react-sdk/views/rooms/_EntityTile.scss
index 712e4bae..031894af 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/rooms/_EntityTile.scss
+++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/_EntityTile.scss
@@ -57,7 +57,7 @@ limitations under the License.
     font-size: 14px;
     text-overflow: ellipsis;
     white-space: nowrap;
-    max-width: 135px;
+    max-width: 155px;
 }
 
 .mx_EntityTile_details {
diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/_MemberInfo.scss b/src/skins/vector/css/matrix-react-sdk/views/rooms/_MemberInfo.scss
index 8920c6f6..5d47275e 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/rooms/_MemberInfo.scss
+++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/_MemberInfo.scss
@@ -94,3 +94,19 @@ limitations under the License.
     cursor: pointer;
 }
 
+.mx_MemberInfo label {
+    font-size: 13px;
+}
+
+.mx_MemberInfo label .mx_MemberInfo_label_text {
+    display: inline-block;
+    max-width: 180px;
+    vertical-align: text-top;
+}
+
+.mx_MemberInfo input[type="radio"] {
+    vertical-align: -2px;
+    margin-right: 5px;
+    margin-left: 8px;
+}
+
diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/_RoomHeader.scss b/src/skins/vector/css/matrix-react-sdk/views/rooms/_RoomHeader.scss
index fd4bdca0..85ad08ec 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/rooms/_RoomHeader.scss
+++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/_RoomHeader.scss
@@ -232,3 +232,17 @@ limitations under the License.
 .mx_RoomHeader_voipButtons {
     margin-top: 18px;
 }
+
+.mx_RoomHeader_pinnedButton {
+    position: relative;
+}
+
+.mx_RoomHeader_unreadPinsIndicator {
+    position: absolute;
+    right: 0;
+    bottom: 4px;
+    width: 8px;
+    height: 8px;
+    border-radius: 8px;
+    background-color: $warning-color;
+}
diff --git a/src/skins/vector/css/themes/_base.scss b/src/skins/vector/css/themes/_base.scss
index f1117a7a..1a0fa5b4 100644
--- a/src/skins/vector/css/themes/_base.scss
+++ b/src/skins/vector/css/themes/_base.scss
@@ -23,7 +23,6 @@ $mention-user-pill-bg-color: #ff0064;
 $other-user-pill-bg-color: rgba(0, 0, 0, 0.1);
 
 // groups
-$group-alert-color: #774f7e;
 $group-my-groups-placeholder-bg: #f7f7f7;
 $group-my-groups-placeholder-fg: #888;
 
diff --git a/src/skins/vector/css/vector-web/structures/_RoomDirectory.scss b/src/skins/vector/css/vector-web/structures/_RoomDirectory.scss
index 91cf86c9..9cd3e728 100644
--- a/src/skins/vector/css/vector-web/structures/_RoomDirectory.scss
+++ b/src/skins/vector/css/vector-web/structures/_RoomDirectory.scss
@@ -100,6 +100,7 @@ limitations under the License.
     display: inline;
     padding-left: 5px;
     padding-right: 5px;
+    margin-right: 5px;
     height: 15px;
     border-radius: 11px;
     background-color: $plinth-bg-color;
diff --git a/src/skins/vector/css/vector-web/views/dialogs/_DevtoolsDialog.scss b/src/skins/vector/css/vector-web/views/dialogs/_DevtoolsDialog.scss
index 975ee8c1..8918373e 100644
--- a/src/skins/vector/css/vector-web/views/dialogs/_DevtoolsDialog.scss
+++ b/src/skins/vector/css/vector-web/views/dialogs/_DevtoolsDialog.scss
@@ -17,3 +17,150 @@ limitations under the License.
 .mx_DevTools_RoomStateExplorer_button, .mx_DevTools_RoomStateExplorer_query {
     margin-bottom: 10px;
 }
+
+.mx_DevTools_label_left {
+    float: left;
+}
+
+.mx_DevTools_label_right {
+    float: right;
+}
+
+.mx_DevTools_label_bottom {
+    clear: both;
+    border-bottom: 1px solid #e5e5e5;
+}
+
+.mx_DevTools_inputRow
+{
+    display: table-row;
+}
+
+.mx_DevTools_inputLabelCell
+{
+    padding-bottom: 21px;
+    display: table-cell;
+    font-weight: bold;
+    padding-right: 24px;
+}
+
+.mx_DevTools_inputCell {
+    display: table-cell;
+    padding-bottom: 21px;
+    width: 240px;
+}
+
+.mx_DevTools_inputCell input
+{
+    display: inline-block;
+    border: 0;
+    border-bottom: 1px solid $input-underline-color;
+    padding: 0;
+    width: 240px;
+    color: $input-fg-color;
+    font-family: 'Open Sans', Helvetica, Arial, Sans-Serif;
+    font-size: 16px;
+}
+
+.mx_DevTools_tgl {
+    display: none;
+
+    // add default box-sizing for this scope
+    &,
+    &:after,
+    &:before,
+    & *,
+    & *:after,
+    & *:before,
+    & + .mx_DevTools_tgl-btn {
+        box-sizing: border-box;
+        &::selection {
+            background: none;
+        }
+    }
+
+    + .mx_DevTools_tgl-btn {
+        outline: 0;
+        display: block;
+        width: 7em;
+        height: 2em;
+        position: relative;
+        cursor: pointer;
+        user-select: none;
+        &:after,
+        &:before {
+            position: relative;
+            display: block;
+            content: "";
+            width: 50%;
+            height: 100%;
+        }
+
+        &:after {
+            left: 0;
+        }
+
+        &:before {
+            display: none;
+        }
+    }
+
+    &:checked + .mx_DevTools_tgl-btn:after {
+        left: 50%;
+    }
+}
+
+.mx_DevTools_tgl-flip {
+    + .mx_DevTools_tgl-btn {
+        padding: 2px;
+        transition: all .2s ease;
+        font-family: sans-serif;
+        perspective: 100px;
+        &:after,
+        &:before {
+            display: inline-block;
+            transition: all .4s ease;
+            width: 100%;
+            text-align: center;
+            position: absolute;
+            line-height: 2em;
+            font-weight: bold;
+            color: #fff;
+            top: 0;
+            left: 0;
+            backface-visibility: hidden;
+            border-radius: 4px;
+        }
+
+        &:after {
+            content: attr(data-tg-on);
+            background: #02C66F;
+            transform: rotateY(-180deg);
+        }
+
+        &:before {
+            background: #FF3A19;
+            content: attr(data-tg-off);
+        }
+
+        &:active:before {
+            transform: rotateY(-20deg);
+        }
+    }
+
+    &:checked + .mx_DevTools_tgl-btn {
+        &:before {
+            transform: rotateY(180deg);
+        }
+
+        &:after {
+            transform: rotateY(0);
+            left: 0;
+            background: #7FC6A6;
+        }
+
+        &:active:after {
+            transform: rotateY(20deg);
+        }
+    }
+}
diff --git a/src/skins/vector/css/vector-web/views/elements/_InlineSpinner.scss b/src/skins/vector/css/vector-web/views/elements/_InlineSpinner.scss
new file mode 100644
index 00000000..612b6209
--- /dev/null
+++ b/src/skins/vector/css/vector-web/views/elements/_InlineSpinner.scss
@@ -0,0 +1,24 @@
+/*
+Copyright 2017 New Vector Ltd.
+
+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.
+*/
+
+.mx_InlineSpinner {
+    display: inline;
+}
+
+.mx_InlineSpinner img {
+    margin: 0px 6px;
+    vertical-align: -3px;
+}