From 3dc45886f780f2bafda052b4efb01fae8fad0bca Mon Sep 17 00:00:00 2001
From: Luke Barnard <lukeb@openmarket.com>
Date: Thu, 9 Mar 2017 17:05:51 +0000
Subject: [PATCH 1/2] Merge the two RoomTile context menus into one

---
 src/component-index.js                        |  10 +-
 .../NotificationStateContextMenu.js           | 144 -------
 .../views/context_menus/RoomTagContextMenu.js | 255 -----------
 .../context_menus/RoomTileContextMenu.js      | 404 ++++++++++++++++++
 src/skins/vector/css/_components.scss         |   3 +-
 .../views/rooms/_RoomTile.scss                |  39 +-
 .../_NotificationStateContextMenu.scss        |  56 ---
 .../context_menus/_RoomTagContextMenu.scss    |  78 ----
 .../context_menus/_RoomTileContextMenu.scss   | 114 +++++
 9 files changed, 525 insertions(+), 578 deletions(-)
 delete mode 100644 src/components/views/context_menus/NotificationStateContextMenu.js
 delete mode 100644 src/components/views/context_menus/RoomTagContextMenu.js
 create mode 100644 src/components/views/context_menus/RoomTileContextMenu.js
 delete mode 100644 src/skins/vector/css/vector-web/views/context_menus/_NotificationStateContextMenu.scss
 delete mode 100644 src/skins/vector/css/vector-web/views/context_menus/_RoomTagContextMenu.scss
 create mode 100644 src/skins/vector/css/vector-web/views/context_menus/_RoomTileContextMenu.scss

diff --git a/src/component-index.js b/src/component-index.js
index 4e6eefb6..2b67aa15 100644
--- a/src/component-index.js
+++ b/src/component-index.js
@@ -30,12 +30,12 @@ import structures$BottomLeftMenu from './components/structures/BottomLeftMenu';
 structures$BottomLeftMenu && (module.exports.components['structures.BottomLeftMenu'] = structures$BottomLeftMenu);
 import structures$CompatibilityPage from './components/structures/CompatibilityPage';
 structures$CompatibilityPage && (module.exports.components['structures.CompatibilityPage'] = structures$CompatibilityPage);
+import structures$HomePage from './components/structures/HomePage';
+structures$HomePage && (module.exports.components['structures.HomePage'] = structures$HomePage);
 import structures$LeftPanel from './components/structures/LeftPanel';
 structures$LeftPanel && (module.exports.components['structures.LeftPanel'] = structures$LeftPanel);
 import structures$RightPanel from './components/structures/RightPanel';
 structures$RightPanel && (module.exports.components['structures.RightPanel'] = structures$RightPanel);
-import structures$HomePage from './components/structures/HomePage';
-structures$HomePage && (module.exports.components['structures.HomePage'] = structures$HomePage);
 import structures$RoomDirectory from './components/structures/RoomDirectory';
 structures$RoomDirectory && (module.exports.components['structures.RoomDirectory'] = structures$RoomDirectory);
 import structures$RoomSubList from './components/structures/RoomSubList';
@@ -46,10 +46,8 @@ import structures$ViewSource from './components/structures/ViewSource';
 structures$ViewSource && (module.exports.components['structures.ViewSource'] = structures$ViewSource);
 import views$context_menus$MessageContextMenu from './components/views/context_menus/MessageContextMenu';
 views$context_menus$MessageContextMenu && (module.exports.components['views.context_menus.MessageContextMenu'] = views$context_menus$MessageContextMenu);
-import views$context_menus$NotificationStateContextMenu from './components/views/context_menus/NotificationStateContextMenu';
-views$context_menus$NotificationStateContextMenu && (module.exports.components['views.context_menus.NotificationStateContextMenu'] = views$context_menus$NotificationStateContextMenu);
-import views$context_menus$RoomTagContextMenu from './components/views/context_menus/RoomTagContextMenu';
-views$context_menus$RoomTagContextMenu && (module.exports.components['views.context_menus.RoomTagContextMenu'] = views$context_menus$RoomTagContextMenu);
+import views$context_menus$RoomTileContextMenu from './components/views/context_menus/RoomTileContextMenu';
+views$context_menus$RoomTileContextMenu && (module.exports.components['views.context_menus.RoomTileContextMenu'] = views$context_menus$RoomTileContextMenu);
 import views$dialogs$BugReportDialog from './components/views/dialogs/BugReportDialog';
 views$dialogs$BugReportDialog && (module.exports.components['views.dialogs.BugReportDialog'] = views$dialogs$BugReportDialog);
 import views$dialogs$ChangelogDialog from './components/views/dialogs/ChangelogDialog';
diff --git a/src/components/views/context_menus/NotificationStateContextMenu.js b/src/components/views/context_menus/NotificationStateContextMenu.js
deleted file mode 100644
index d4b40d17..00000000
--- a/src/components/views/context_menus/NotificationStateContextMenu.js
+++ /dev/null
@@ -1,144 +0,0 @@
-/*
-Copyright 2015, 2016 OpenMarket 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.
-*/
-
-'use strict';
-
-var q = require("q");
-var React = require('react');
-var classNames = require('classnames');
-var RoomNotifs = require('matrix-react-sdk/lib/RoomNotifs');
-var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
-
-module.exports = React.createClass({
-    displayName: 'NotificationStateContextMenu',
-
-    propTypes: {
-        room: React.PropTypes.object.isRequired,
-        /* callback called when the menu is dismissed */
-        onFinished: React.PropTypes.func,
-    },
-
-    getInitialState() {
-        return {
-            roomNotifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
-        }
-    },
-
-    componentWillMount: function() {
-        this._unmounted = false;
-    },
-
-    componentWillUnmount: function() {
-        this._unmounted = true;
-    },
-
-    _save: function(newState) {
-        const oldState = this.state.roomNotifState;
-        const roomId = this.props.room.roomId;
-        var cli = MatrixClientPeg.get();
-
-        if (cli.isGuest()) return;
-
-        this.setState({
-            roomNotifState: newState,
-        });
-        RoomNotifs.setRoomNotifsState(this.props.room.roomId, newState).done(() => {
-            // delay slightly so that the user can see their state change
-            // before closing the menu
-            return q.delay(500).then(() => {
-                if (this._unmounted) return;
-                // Close the context menu
-                if (this.props.onFinished) {
-                    this.props.onFinished();
-                };
-            });
-        }, (error) => {
-            // TODO: some form of error notification to the user
-            // to inform them that their state change failed.
-            // For now we at least set the state back
-            if (this._unmounted) return;
-            this.setState({
-                roomNotifState: oldState,
-            });
-        });
-    },
-
-    _onClickAlertMe: function() {
-        this._save(RoomNotifs.ALL_MESSAGES_LOUD);
-    },
-
-    _onClickAllNotifs: function() {
-        this._save(RoomNotifs.ALL_MESSAGES);
-    },
-
-    _onClickMentions: function() {
-        this._save(RoomNotifs.MENTIONS_ONLY);
-    },
-
-    _onClickMute: function() {
-        this._save(RoomNotifs.MUTE);
-    },
-
-    render: function() {
-        var alertMeClasses = classNames({
-            'mx_NotificationStateContextMenu_field': true,
-            'mx_NotificationStateContextMenu_fieldSet': this.state.roomNotifState == RoomNotifs.ALL_MESSAGES_LOUD,
-        });
-
-        var allNotifsClasses = classNames({
-            'mx_NotificationStateContextMenu_field': true,
-            'mx_NotificationStateContextMenu_fieldSet': this.state.roomNotifState == RoomNotifs.ALL_MESSAGES,
-        });
-
-        var mentionsClasses = classNames({
-            'mx_NotificationStateContextMenu_field': true,
-            'mx_NotificationStateContextMenu_fieldSet': this.state.roomNotifState == RoomNotifs.MENTIONS_ONLY,
-        });
-
-        var muteNotifsClasses = classNames({
-            'mx_NotificationStateContextMenu_field': true,
-            'mx_NotificationStateContextMenu_fieldSet': this.state.roomNotifState == RoomNotifs.MUTE,
-        });
-
-        return (
-            <div>
-                <div className="mx_NotificationStateContextMenu_picker" >
-                    <img src="img/notif-slider.svg" width="20" height="107" />
-                </div>
-                <div className={ alertMeClasses } onClick={this._onClickAlertMe} >
-                    <img className="mx_NotificationStateContextMenu_activeIcon" src="img/notif-active.svg" width="12" height="12" />
-                    <img className="mx_NotificationStateContextMenu_icon mx_filterFlipColor" src="img/icon-context-mute-off-copy.svg" width="16" height="12" />
-                    All messages (loud)
-                </div>
-                <div className={ allNotifsClasses } onClick={this._onClickAllNotifs} >
-                    <img className="mx_NotificationStateContextMenu_activeIcon" src="img/notif-active.svg" width="12" height="12" />
-                    <img className="mx_NotificationStateContextMenu_icon mx_filterFlipColor" src="img/icon-context-mute-off.svg" width="16" height="12" />
-                    All messages
-                </div>
-                <div className={ mentionsClasses } onClick={this._onClickMentions} >
-                    <img className="mx_NotificationStateContextMenu_activeIcon" src="img/notif-active.svg" width="12" height="12" />
-                    <img className="mx_NotificationStateContextMenu_icon mx_filterFlipColor" src="img/icon-context-mute-mentions.svg" width="16" height="12" />
-                    Mentions only
-                </div>
-                <div className={ muteNotifsClasses } onClick={this._onClickMute} >
-                    <img className="mx_NotificationStateContextMenu_activeIcon" src="img/notif-active.svg" width="12" height="12" />
-                    <img className="mx_NotificationStateContextMenu_icon mx_filterFlipColor" src="img/icon-context-mute.svg" width="16" height="12" />
-                    Mute
-                </div>
-            </div>
-        );
-    }
-});
diff --git a/src/components/views/context_menus/RoomTagContextMenu.js b/src/components/views/context_menus/RoomTagContextMenu.js
deleted file mode 100644
index 8a44051f..00000000
--- a/src/components/views/context_menus/RoomTagContextMenu.js
+++ /dev/null
@@ -1,255 +0,0 @@
-/*
-Copyright 2015, 2016 OpenMarket 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.
-*/
-
-'use strict';
-
-import q from 'q';
-import React from 'react';
-import classNames from 'classnames';
-import sdk from 'matrix-react-sdk';
-import MatrixClientPeg from 'matrix-react-sdk/lib/MatrixClientPeg';
-import dis from 'matrix-react-sdk/lib/dispatcher';
-import DMRoomMap from 'matrix-react-sdk/lib/utils/DMRoomMap';
-import Rooms from 'matrix-react-sdk/lib/Rooms';
-import Modal from 'matrix-react-sdk/lib/Modal';
-
-module.exports = React.createClass({
-    displayName: 'RoomTagContextMenu',
-
-    propTypes: {
-        room: React.PropTypes.object.isRequired,
-        /* callback called when the menu is dismissed */
-        onFinished: React.PropTypes.func,
-    },
-
-    getInitialState: function() {
-        const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
-        return {
-            isFavourite: this.props.room.tags.hasOwnProperty("m.favourite"),
-            isLowPriority: this.props.room.tags.hasOwnProperty("m.lowpriority"),
-            isDirectMessage: Boolean(dmRoomMap.getUserIdForRoomId(this.props.room.roomId)),
-        };
-    },
-
-    _toggleTag: function(tagNameOn, tagNameOff) {
-        var self = this;
-        const roomId = this.props.room.roomId;
-        var cli = MatrixClientPeg.get();
-        if (!cli.isGuest()) {
-            q.delay(500).then(function() {
-                if (tagNameOff !== null && tagNameOff !== undefined) {
-                    cli.deleteRoomTag(roomId, tagNameOff).finally(function() {
-                        // Close the context menu
-                        if (self.props.onFinished) {
-                            self.props.onFinished();
-                        };
-                    }).fail(function(err) {
-                        var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
-                        Modal.createDialog(ErrorDialog, {
-                            title: "Failed to remove tag " + tagNameOff + " from room",
-                            description: err.toString()
-                        });
-                    });
-                }
-
-                if (tagNameOn !== null && tagNameOn !== undefined) {
-                    // If the tag ordering meta data is required, it is added by
-                    // the RoomSubList when it sorts its rooms
-                    cli.setRoomTag(roomId, tagNameOn, {}).finally(function() {
-                        // Close the context menu
-                        if (self.props.onFinished) {
-                            self.props.onFinished();
-                        };
-                    }).fail(function(err) {
-                        var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
-                        Modal.createDialog(ErrorDialog, {
-                            title: "Failed to add tag " + tagNameOn + " to room",
-                            description: err.toString()
-                        });
-                    });
-                }
-            });
-        }
-    },
-
-    _onClickFavourite: function() {
-        // Tag room as 'Favourite'
-        if (!this.state.isFavourite && this.state.isLowPriority) {
-            this.setState({
-                isFavourite: true,
-                isLowPriority: false,
-            });
-            this._toggleTag("m.favourite", "m.lowpriority");
-        } else if (this.state.isFavourite) {
-            this.setState({isFavourite: false});
-            this._toggleTag(null, "m.favourite");
-        } else if (!this.state.isFavourite) {
-            this.setState({isFavourite: true});
-            this._toggleTag("m.favourite");
-        }
-    },
-
-    _onClickLowPriority: function() {
-        // Tag room as 'Low Priority'
-        if (!this.state.isLowPriority && this.state.isFavourite) {
-            this.setState({
-                isFavourite: false,
-                isLowPriority: true,
-            });
-            this._toggleTag("m.lowpriority", "m.favourite");
-        } else if (this.state.isLowPriority) {
-            this.setState({isLowPriority: false});
-            this._toggleTag(null, "m.lowpriority");
-        } else if (!this.state.isLowPriority) {
-            this.setState({isLowPriority: true});
-            this._toggleTag("m.lowpriority");
-        }
-    },
-
-    _onClickDM: function() {
-        const newIsDirectMessage = !this.state.isDirectMessage;
-        this.setState({
-            isDirectMessage: newIsDirectMessage,
-        });
-
-        if (MatrixClientPeg.get().isGuest()) return;
-
-        let newTarget;
-        if (newIsDirectMessage) {
-            const guessedTarget = Rooms.guessDMRoomTarget(
-                this.props.room,
-                this.props.room.getMember(MatrixClientPeg.get().credentials.userId),
-            );
-            newTarget = guessedTarget.userId;
-        } else {
-            newTarget = null;
-        }
-
-        // give some time for the user to see the icon change first, since
-        // this will hide the context menu once it completes
-        q.delay(500).done(() => {
-            return Rooms.setDMRoom(this.props.room.roomId, newTarget).finally(() => {
-                // Close the context menu
-                if (this.props.onFinished) {
-                    this.props.onFinished();
-                };
-            }, (err) => {
-                var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
-                Modal.createDialog(ErrorDialog, {
-                    title: "Failed to set Direct Message status of room",
-                    description: err.toString()
-                });
-            });
-        });
-    },
-
-    _onClickLeave: function() {
-        // Leave room
-        dis.dispatch({
-            action: 'leave_room',
-            room_id: this.props.room.roomId,
-        });
-
-        // Close the context menu
-        if (this.props.onFinished) {
-            this.props.onFinished();
-        };
-    },
-
-    _onClickForget: function() {
-        // FIXME: duplicated with RoomSettings (and dead code in RoomView)
-        MatrixClientPeg.get().forget(this.props.room.roomId).done(function() {
-            dis.dispatch({ action: 'view_next_room' });
-        }, function(err) {
-            var errCode = err.errcode || "unknown error code";
-            var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
-            Modal.createDialog(ErrorDialog, {
-                title: "Error",
-                description: `Failed to forget room (${errCode})`
-            });
-        });
-
-        // Close the context menu
-        if (this.props.onFinished) {
-            this.props.onFinished();
-        };
-    },
-
-    render: function() {
-        const myUserId = MatrixClientPeg.get().credentials.userId;
-        const myMember = this.props.room.getMember(myUserId);
-
-        const favouriteClasses = classNames({
-            'mx_RoomTagContextMenu_field': true,
-            'mx_RoomTagContextMenu_fieldSet': this.state.isFavourite,
-            'mx_RoomTagContextMenu_fieldDisabled': false,
-        });
-
-        const lowPriorityClasses = classNames({
-            'mx_RoomTagContextMenu_field': true,
-            'mx_RoomTagContextMenu_fieldSet': this.state.isLowPriority,
-            'mx_RoomTagContextMenu_fieldDisabled': false,
-        });
-
-        const leaveClasses = classNames({
-            'mx_RoomTagContextMenu_field': true,
-            'mx_RoomTagContextMenu_fieldSet': false,
-            'mx_RoomTagContextMenu_fieldDisabled': false,
-        });
-
-        const dmClasses = classNames({
-            'mx_RoomTagContextMenu_field': true,
-            'mx_RoomTagContextMenu_fieldSet': this.state.isDirectMessage,
-            'mx_RoomTagContextMenu_fieldDisabled': false,
-        });
-
-        if (myMember && (myMember.membership === "leave" || myMember.membership === "ban")) {
-            return (
-                <div>
-                    <div className={ leaveClasses } onClick={ this._onClickForget } >
-                        <img className="mx_RoomTagContextMenu_icon" src="img/icon_context_delete.svg" width="15" height="15" />
-                        Forget
-                    </div>
-                </div>
-            );
-        }
-
-        return (
-            <div>
-                <div className={ favouriteClasses } onClick={this._onClickFavourite} >
-                    <img className="mx_RoomTagContextMenu_icon" src="img/icon_context_fave.svg" width="15" height="15" />
-                    <img className="mx_RoomTagContextMenu_icon_set" src="img/icon_context_fave_on.svg" width="15" height="15" />
-                    Favourite
-                </div>
-                <div className={ lowPriorityClasses } onClick={this._onClickLowPriority} >
-                    <img className="mx_RoomTagContextMenu_icon" src="img/icon_context_low.svg" width="15" height="15" />
-                    <img className="mx_RoomTagContextMenu_icon_set" src="img/icon_context_low_on.svg" width="15" height="15" />
-                    Low Priority
-                </div>
-                <div className={ dmClasses } onClick={this._onClickDM} >
-                    <img className="mx_RoomTagContextMenu_icon" src="img/icon_context_person.svg" width="15" height="15" />
-                    <img className="mx_RoomTagContextMenu_icon_set" src="img/icon_context_person_on.svg" width="15" height="15" />
-                    Direct Chat
-                </div>
-                <hr className="mx_RoomTagContextMenu_separator" />
-                <div className={ leaveClasses } onClick={(myMember && myMember.membership === "join") ? this._onClickLeave : null} >
-                    <img className="mx_RoomTagContextMenu_icon" src="img/icon_context_delete.svg" width="15" height="15" />
-                    Leave
-                </div>
-            </div>
-        );
-    }
-});
diff --git a/src/components/views/context_menus/RoomTileContextMenu.js b/src/components/views/context_menus/RoomTileContextMenu.js
new file mode 100644
index 00000000..6430cd37
--- /dev/null
+++ b/src/components/views/context_menus/RoomTileContextMenu.js
@@ -0,0 +1,404 @@
+/*
+Copyright 2015, 2016 OpenMarket 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.
+*/
+
+'use strict';
+
+import q from 'q';
+import React from 'react';
+import classNames from 'classnames';
+import sdk from 'matrix-react-sdk';
+import MatrixClientPeg from 'matrix-react-sdk/lib/MatrixClientPeg';
+import dis from 'matrix-react-sdk/lib/dispatcher';
+import DMRoomMap from 'matrix-react-sdk/lib/utils/DMRoomMap';
+import Rooms from 'matrix-react-sdk/lib/Rooms';
+import * as RoomNotifs from 'matrix-react-sdk/lib/RoomNotifs';
+import Modal from 'matrix-react-sdk/lib/Modal';
+
+module.exports = React.createClass({
+    displayName: 'RoomTileContextMenu',
+
+    propTypes: {
+        room: React.PropTypes.object.isRequired,
+        /* callback called when the menu is dismissed */
+        onFinished: React.PropTypes.func,
+    },
+
+    getInitialState() {
+        const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
+        return {
+            roomNotifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
+            isFavourite: this.props.room.tags.hasOwnProperty("m.favourite"),
+            isLowPriority: this.props.room.tags.hasOwnProperty("m.lowpriority"),
+            isDirectMessage: Boolean(dmRoomMap.getUserIdForRoomId(this.props.room.roomId)),
+        }
+    },
+
+    componentWillMount: function() {
+        this._unmounted = false;
+    },
+
+    componentWillUnmount: function() {
+        this._unmounted = true;
+    },
+
+    _toggleTag: function(tagNameOn, tagNameOff) {
+        var self = this;
+        const roomId = this.props.room.roomId;
+        var cli = MatrixClientPeg.get();
+        if (!cli.isGuest()) {
+            q.delay(500).then(function() {
+                if (tagNameOff !== null && tagNameOff !== undefined) {
+                    cli.deleteRoomTag(roomId, tagNameOff).finally(function() {
+                        // Close the context menu
+                        if (self.props.onFinished) {
+                            self.props.onFinished();
+                        };
+                    }).fail(function(err) {
+                        var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+                        Modal.createDialog(ErrorDialog, {
+                            title: "Failed to remove tag " + tagNameOff + " from room",
+                            description: err.toString()
+                        });
+                    });
+                }
+
+                if (tagNameOn !== null && tagNameOn !== undefined) {
+                    // If the tag ordering meta data is required, it is added by
+                    // the RoomSubList when it sorts its rooms
+                    cli.setRoomTag(roomId, tagNameOn, {}).finally(function() {
+                        // Close the context menu
+                        if (self.props.onFinished) {
+                            self.props.onFinished();
+                        };
+                    }).fail(function(err) {
+                        var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+                        Modal.createDialog(ErrorDialog, {
+                            title: "Failed to add tag " + tagNameOn + " to room",
+                            description: err.toString()
+                        });
+                    });
+                }
+            });
+        }
+    },
+
+    _onClickFavourite: function() {
+        // Tag room as 'Favourite'
+        if (!this.state.isFavourite && this.state.isLowPriority) {
+            this.setState({
+                isFavourite: true,
+                isLowPriority: false,
+            });
+            this._toggleTag("m.favourite", "m.lowpriority");
+        } else if (this.state.isFavourite) {
+            this.setState({isFavourite: false});
+            this._toggleTag(null, "m.favourite");
+        } else if (!this.state.isFavourite) {
+            this.setState({isFavourite: true});
+            this._toggleTag("m.favourite");
+        }
+    },
+
+    _onClickLowPriority: function() {
+        // Tag room as 'Low Priority'
+        if (!this.state.isLowPriority && this.state.isFavourite) {
+            this.setState({
+                isFavourite: false,
+                isLowPriority: true,
+            });
+            this._toggleTag("m.lowpriority", "m.favourite");
+        } else if (this.state.isLowPriority) {
+            this.setState({isLowPriority: false});
+            this._toggleTag(null, "m.lowpriority");
+        } else if (!this.state.isLowPriority) {
+            this.setState({isLowPriority: true});
+            this._toggleTag("m.lowpriority");
+        }
+    },
+
+    _onClickDM: function() {
+        const newIsDirectMessage = !this.state.isDirectMessage;
+        this.setState({
+            isDirectMessage: newIsDirectMessage,
+        });
+
+        if (MatrixClientPeg.get().isGuest()) return;
+
+        let newTarget;
+        if (newIsDirectMessage) {
+            const guessedTarget = Rooms.guessDMRoomTarget(
+                this.props.room,
+                this.props.room.getMember(MatrixClientPeg.get().credentials.userId),
+            );
+            newTarget = guessedTarget.userId;
+        } else {
+            newTarget = null;
+        }
+
+        // give some time for the user to see the icon change first, since
+        // this will hide the context menu once it completes
+        q.delay(500).done(() => {
+            return Rooms.setDMRoom(this.props.room.roomId, newTarget).finally(() => {
+                // Close the context menu
+                if (this.props.onFinished) {
+                    this.props.onFinished();
+                };
+            }, (err) => {
+                var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+                Modal.createDialog(ErrorDialog, {
+                    title: "Failed to set Direct Message status of room",
+                    description: err.toString()
+                });
+            });
+        });
+    },
+
+    _onClickLeave: function() {
+        // Leave room
+        dis.dispatch({
+            action: 'leave_room',
+            room_id: this.props.room.roomId,
+        });
+
+        // Close the context menu
+        if (this.props.onFinished) {
+            this.props.onFinished();
+        };
+    },
+
+    _onClickReject: function() {
+        dis.dispatch({
+            action: 'reject_invite',
+            room_id: this.props.room.roomId,
+        });
+
+        // Close the context menu
+        if (this.props.onFinished) {
+            this.props.onFinished();
+        };
+    },
+
+    _onClickForget: function() {
+        // FIXME: duplicated with RoomSettings (and dead code in RoomView)
+        MatrixClientPeg.get().forget(this.props.room.roomId).done(function() {
+            dis.dispatch({ action: 'view_next_room' });
+        }, function(err) {
+            var errCode = err.errcode || "unknown error code";
+            var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+            Modal.createDialog(ErrorDialog, {
+                title: "Error",
+                description: `Failed to forget room (${errCode})`
+            });
+        });
+
+        // Close the context menu
+        if (this.props.onFinished) {
+            this.props.onFinished();
+        };
+    },
+
+    _saveNotifState: function(newState) {
+        const oldState = this.state.roomNotifState;
+        const roomId = this.props.room.roomId;
+        var cli = MatrixClientPeg.get();
+
+        if (cli.isGuest()) return;
+
+        this.setState({
+            roomNotifState: newState,
+        });
+        RoomNotifs.setRoomNotifsState(this.props.room.roomId, newState).done(() => {
+            // delay slightly so that the user can see their state change
+            // before closing the menu
+            return q.delay(500).then(() => {
+                if (this._unmounted) return;
+                // Close the context menu
+                if (this.props.onFinished) {
+                    this.props.onFinished();
+                };
+            });
+        }, (error) => {
+            // TODO: some form of error notification to the user
+            // to inform them that their state change failed.
+            // For now we at least set the state back
+            if (this._unmounted) return;
+            this.setState({
+                roomNotifState: oldState,
+            });
+        });
+    },
+
+    _onClickAlertMe: function() {
+        this._saveNotifState(RoomNotifs.ALL_MESSAGES_LOUD);
+    },
+
+    _onClickAllNotifs: function() {
+        this._saveNotifState(RoomNotifs.ALL_MESSAGES);
+    },
+
+    _onClickMentions: function() {
+        this._saveNotifState(RoomNotifs.MENTIONS_ONLY);
+    },
+
+    _onClickMute: function() {
+        this._saveNotifState(RoomNotifs.MUTE);
+    },
+
+    _renderNotifMenu: function() {
+        var alertMeClasses = classNames({
+            'mx_RoomTileContextMenu_notif_field': true,
+            'mx_RoomTileContextMenu_notif_fieldSet': this.state.roomNotifState == RoomNotifs.ALL_MESSAGES_LOUD,
+        });
+
+        var allNotifsClasses = classNames({
+            'mx_RoomTileContextMenu_notif_field': true,
+            'mx_RoomTileContextMenu_notif_fieldSet': this.state.roomNotifState == RoomNotifs.ALL_MESSAGES,
+        });
+
+        var mentionsClasses = classNames({
+            'mx_RoomTileContextMenu_notif_field': true,
+            'mx_RoomTileContextMenu_notif_fieldSet': this.state.roomNotifState == RoomNotifs.MENTIONS_ONLY,
+        });
+
+        var muteNotifsClasses = classNames({
+            'mx_RoomTileContextMenu_notif_field': true,
+            'mx_RoomTileContextMenu_notif_fieldSet': this.state.roomNotifState == RoomNotifs.MUTE,
+        });
+
+        return (
+            <div>
+                <div className="mx_RoomTileContextMenu_notif_picker" >
+                    <img src="img/notif-slider.svg" width="20" height="107" />
+                </div>
+                <div className={ alertMeClasses } onClick={this._onClickAlertMe} >
+                    <img className="mx_RoomTileContextMenu_notif_activeIcon" src="img/notif-active.svg" width="12" height="12" />
+                    <img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src="img/icon-context-mute-off-copy.svg" width="16" height="12" />
+                    All messages (loud)
+                </div>
+                <div className={ allNotifsClasses } onClick={this._onClickAllNotifs} >
+                    <img className="mx_RoomTileContextMenu_notif_activeIcon" src="img/notif-active.svg" width="12" height="12" />
+                    <img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src="img/icon-context-mute-off.svg" width="16" height="12" />
+                    All messages
+                </div>
+                <div className={ mentionsClasses } onClick={this._onClickMentions} >
+                    <img className="mx_RoomTileContextMenu_notif_activeIcon" src="img/notif-active.svg" width="12" height="12" />
+                    <img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src="img/icon-context-mute-mentions.svg" width="16" height="12" />
+                    Mentions only
+                </div>
+                <div className={ muteNotifsClasses } onClick={this._onClickMute} >
+                    <img className="mx_RoomTileContextMenu_notif_activeIcon" src="img/notif-active.svg" width="12" height="12" />
+                    <img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src="img/icon-context-mute.svg" width="16" height="12" />
+                    Mute
+                </div>
+            </div>
+        );
+    },
+
+    _renderLeaveMenu: function(membership) {
+        if (!membership) {
+            return null;
+        }
+
+        let leaveClickHandler = null;
+        let leaveText = null;
+
+        switch (membership) {
+            case "join":
+                leaveClickHandler = this._onClickLeave;
+                leaveText = "Leave";
+                break;
+            case "leave":
+            case "ban":
+                leaveClickHandler = this._onClickForget;
+                leaveText = "Forget";
+                break;
+            case "invite":
+                leaveClickHandler = this._onClickReject;
+                leaveText = "Reject";
+                break;
+        }
+
+        return (
+            <div>
+                <div className="mx_RoomTileContextMenu_leave" onClick={ leaveClickHandler } >
+                    <img className="mx_RoomTileContextMenu_tag_icon" src="img/icon_context_delete.svg" width="15" height="15" />
+                    { leaveText }
+                </div>
+            </div>
+        );
+    },
+
+    _renderRoomTagMenu: function() {
+        const favouriteClasses = classNames({
+            'mx_RoomTileContextMenu_tag_field': true,
+            'mx_RoomTileContextMenu_tag_fieldSet': this.state.isFavourite,
+            'mx_RoomTileContextMenu_tag_fieldDisabled': false,
+        });
+
+        const lowPriorityClasses = classNames({
+            'mx_RoomTileContextMenu_tag_field': true,
+            'mx_RoomTileContextMenu_tag_fieldSet': this.state.isLowPriority,
+            'mx_RoomTileContextMenu_tag_fieldDisabled': false,
+        });
+
+        const dmClasses = classNames({
+            'mx_RoomTileContextMenu_tag_field': true,
+            'mx_RoomTileContextMenu_tag_fieldSet': this.state.isDirectMessage,
+            'mx_RoomTileContextMenu_tag_fieldDisabled': false,
+        });
+
+        return (
+            <div>
+                <div className={ favouriteClasses } onClick={this._onClickFavourite} >
+                    <img className="mx_RoomTileContextMenu_tag_icon" src="img/icon_context_fave.svg" width="15" height="15" />
+                    <img className="mx_RoomTileContextMenu_tag_icon_set" src="img/icon_context_fave_on.svg" width="15" height="15" />
+                    Favourite
+                </div>
+                <div className={ lowPriorityClasses } onClick={this._onClickLowPriority} >
+                    <img className="mx_RoomTileContextMenu_tag_icon" src="img/icon_context_low.svg" width="15" height="15" />
+                    <img className="mx_RoomTileContextMenu_tag_icon_set" src="img/icon_context_low_on.svg" width="15" height="15" />
+                    Low Priority
+                </div>
+                <div className={ dmClasses } onClick={this._onClickDM} >
+                    <img className="mx_RoomTileContextMenu_tag_icon" src="img/icon_context_person.svg" width="15" height="15" />
+                    <img className="mx_RoomTileContextMenu_tag_icon_set" src="img/icon_context_person_on.svg" width="15" height="15" />
+                    Direct Chat
+                </div>
+            </div>
+        );
+    },
+
+    render: function() {
+        const myMember = this.props.room.getMember(
+            MatrixClientPeg.get().credentials.userId
+        );
+
+        // Can't set notif level or tags on non-join rooms
+        if (myMember.membership !== 'join') {
+            return this._renderLeaveMenu(myMember.membership);
+        }
+
+        return (
+            <div>
+                { this._renderNotifMenu() }
+                <hr className="mx_RoomTileContextMenu_separator" />
+                { this._renderLeaveMenu(myMember.membership) }
+                <hr className="mx_RoomTileContextMenu_separator" />
+                { this._renderRoomTagMenu() }
+            </div>
+        );
+    }
+});
diff --git a/src/skins/vector/css/_components.scss b/src/skins/vector/css/_components.scss
index 6a1fefb0..650cf3d5 100644
--- a/src/skins/vector/css/_components.scss
+++ b/src/skins/vector/css/_components.scss
@@ -61,8 +61,7 @@
 @import "./vector-web/structures/_RoomSubList.scss";
 @import "./vector-web/structures/_ViewSource.scss";
 @import "./vector-web/views/context_menus/_MessageContextMenu.scss";
-@import "./vector-web/views/context_menus/_NotificationStateContextMenu.scss";
-@import "./vector-web/views/context_menus/_RoomTagContextMenu.scss";
+@import "./vector-web/views/context_menus/_RoomTileContextMenu.scss";
 @import "./vector-web/views/dialogs/_ChangelogDialog.scss";
 @import "./vector-web/views/directory/_NetworkDropdown.scss";
 @import "./vector-web/views/elements/_ImageView.scss";
diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/_RoomTile.scss b/src/skins/vector/css/matrix-react-sdk/views/rooms/_RoomTile.scss
index 34cc21d4..08efa145 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/rooms/_RoomTile.scss
+++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/_RoomTile.scss
@@ -59,41 +59,6 @@ limitations under the License.
     z-index: 2;
 }
 
-.mx_RoomTile:hover .mx_RoomTile_avatar_container:before,
-.mx_RoomTile_avatar_container.mx_RoomTile_avatar_roomTagMenu:before {
-    display: block;
-    position: absolute;
-    content: "";
-    border-radius: 40px;
-    background-image: url("../../img/icons_ellipsis.svg");
-    background-size: 25px;
-    width: 24px;
-    height: 24px;
-    z-index: 4;
-}
-
-.mx_RoomTile:hover .mx_RoomTile_avatar_container:after,
-.mx_RoomTile_avatar_container.mx_RoomTile_avatar_roomTagMenu:after {
-    display: block;
-    position: absolute;
-    content: "";
-    border-radius: 40px;
-    background: $primary-fg-color;
-    bottom: 0;
-    width: 24px;
-    height: 24px;
-    opacity: 0.6;
-    z-index: 1;
-}
-
-.collapsed .mx_RoomTile:hover .mx_RoomTile_avatar_container:before {
-    display: none;
-}
-
-.collapsed .mx_RoomTile:hover .mx_RoomTile_avatar_container:after {
-    display: none;
-}
-
 .mx_RoomTile_name {
     display: inline-block;
     position: relative;
@@ -164,13 +129,13 @@ limitations under the License.
 }
 
 .mx_RoomTile .mx_RoomTile_badge.mx_RoomTile_badgeButton,
-.mx_RoomTile.mx_RoomTile_notificationStateMenu .mx_RoomTile_badge {
+.mx_RoomTile.mx_RoomTile_menuDisplayed .mx_RoomTile_badge {
     letter-spacing: 0.1em;
     opacity: 1;
 }
 
 .mx_RoomTile.mx_RoomTile_noBadges .mx_RoomTile_badge.mx_RoomTile_badgeButton,
-.mx_RoomTile.mx_RoomTile_notificationStateMenu.mx_RoomTile_noBadges .mx_RoomTile_badge {
+.mx_RoomTile.mx_RoomTile_menuDisplayed.mx_RoomTile_noBadges .mx_RoomTile_badge {
     background-color: $neutral-badge-color;
 }
 
diff --git a/src/skins/vector/css/vector-web/views/context_menus/_NotificationStateContextMenu.scss b/src/skins/vector/css/vector-web/views/context_menus/_NotificationStateContextMenu.scss
deleted file mode 100644
index 1f068526..00000000
--- a/src/skins/vector/css/vector-web/views/context_menus/_NotificationStateContextMenu.scss
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
-Copyright 2015, 2016 OpenMarket 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_NotificationStateContextMenu_picker {
-    position: absolute;
-    top: 16px;
-    left: 5px;
-}
-
-.mx_NotificationStateContextMenu_field {
-    padding-top: 4px;
-    padding-right: 6px;
-    padding-bottom: 10px;
-    padding-left: 8px; /* 20px */
-    cursor: pointer;
-    white-space: nowrap;
-    display: flex;
-    align-items: center;
-}
-
-.mx_NotificationStateContextMenu_field.mx_NotificationStateContextMenu_fieldSet {
-    font-weight: bold;
-}
-
-.mx_NotificationStateContextMenu_field.mx_NotificationStateContextMenu_fieldDisabled {
-    color: rgba(0, 0, 0, 0.2);
-}
-
-.mx_NotificationStateContextMenu_icon {
-    padding-right: 4px;
-    padding-left: 4px;
-}
-
-.mx_NotificationStateContextMenu_activeIcon {
-    display: inline-block;
-    opacity: 0;
-    position: relative;
-    left: -5px;
-}
-
-.mx_NotificationStateContextMenu_fieldSet .mx_NotificationStateContextMenu_activeIcon {
-    opacity: 1;
-}
diff --git a/src/skins/vector/css/vector-web/views/context_menus/_RoomTagContextMenu.scss b/src/skins/vector/css/vector-web/views/context_menus/_RoomTagContextMenu.scss
deleted file mode 100644
index 16a3ab79..00000000
--- a/src/skins/vector/css/vector-web/views/context_menus/_RoomTagContextMenu.scss
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
-Copyright 2015, 2016 OpenMarket 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_RoomTagContextMenu_field {
-    padding-top: 8px;
-    padding-right: 20px;
-    padding-bottom: 8px;
-    cursor: pointer;
-    white-space: nowrap;
-    display: flex;
-    align-items: center;
-    line-height: 16px;
-}
-
-.mx_RoomTagContextMenu_field:first-child {
-    padding-top: 4px;
-}
-
-.mx_RoomTagContextMenu_field:last-child {
-    padding-bottom: 4px;
-    color: $warning-color;
-}
-
-.mx_RoomTagContextMenu_field.mx_RoomTagContextMenu_fieldSet {
-    font-weight: bold;
-}
-
-.mx_RoomTagContextMenu_field.mx_RoomTagContextMenu_fieldSet .mx_RoomTagContextMenu_icon {
-    display: none;
-}
-
-.mx_RoomTagContextMenu_field.mx_RoomTagContextMenu_fieldSet .mx_RoomTagContextMenu_icon_set {
-    display: inline-block;
-}
-
-.mx_RoomTagContextMenu_field.mx_RoomTagContextMenu_fieldDisabled {
-    color: rgba(0, 0, 0, 0.2);
-}
-
-.mx_RoomTagContextMenu_icon {
-    padding-right: 8px;
-    padding-left: 4px;
-    display: inline-block
-}
-
-.mx_RoomTagContextMenu_icon_set {
-    padding-right: 8px;
-    padding-left: 4px;
-    display: none;
-}
-
-.mx_RoomTagContextMenu_separator {
-    margin-top: 0;
-    margin-bottom: 0;
-    border-bottom-style: none;
-    border-left-style: none;
-    border-right-style: none;
-    border-top-style: solid;
-    border-top-width: 1px;
-    border-color: $menu-border-color;
-}
-
-.mx_RoomTagContextMenu_fieldSet .mx_RoomTagContextMenu_icon {
-    /* Something to indicate that the icon is the set tag */
-}
diff --git a/src/skins/vector/css/vector-web/views/context_menus/_RoomTileContextMenu.scss b/src/skins/vector/css/vector-web/views/context_menus/_RoomTileContextMenu.scss
new file mode 100644
index 00000000..598f8ac2
--- /dev/null
+++ b/src/skins/vector/css/vector-web/views/context_menus/_RoomTileContextMenu.scss
@@ -0,0 +1,114 @@
+/*
+Copyright 2015, 2016 OpenMarket 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_RoomTileContextMenu_tag_field, .mx_RoomTileContextMenu_leave {
+    padding-top: 8px;
+    padding-right: 20px;
+    padding-bottom: 8px;
+    cursor: pointer;
+    white-space: nowrap;
+    display: flex;
+    align-items: center;
+    line-height: 16px;
+}
+
+.mx_RoomTileContextMenu_tag_field.mx_RoomTileContextMenu_tag_fieldSet {
+    font-weight: bold;
+}
+
+.mx_RoomTileContextMenu_tag_field.mx_RoomTileContextMenu_tag_fieldSet .mx_RoomTileContextMenu_tag_icon {
+    display: none;
+}
+
+.mx_RoomTileContextMenu_tag_field.mx_RoomTileContextMenu_tag_fieldSet .mx_RoomTileContextMenu_tag_icon_set {
+    display: inline-block;
+}
+
+.mx_RoomTileContextMenu_tag_field.mx_RoomTileContextMenu_tag_fieldDisabled {
+    color: rgba(0, 0, 0, 0.2);
+}
+
+.mx_RoomTileContextMenu_tag_icon {
+    padding-right: 8px;
+    padding-left: 4px;
+    display: inline-block
+}
+
+.mx_RoomTileContextMenu_tag_icon_set {
+    padding-right: 8px;
+    padding-left: 4px;
+    display: none;
+}
+
+.mx_RoomTileContextMenu_separator {
+    margin-top: 0;
+    margin-bottom: 0;
+    border-bottom-style: none;
+    border-left-style: none;
+    border-right-style: none;
+    border-top-style: solid;
+    border-top-width: 1px;
+    border-color: $menu-border-color;
+}
+
+.mx_RoomTileContextMenu_leave {
+    color: $warning-color;
+}
+
+.mx_RoomTileContextMenu_tag_fieldSet .mx_RoomTileContextMenu_tag_icon {
+    /* Something to indicate that the icon is the set tag */
+}
+
+.mx_RoomTileContextMenu_notif_picker {
+    position: absolute;
+    top: 16px;
+    left: 5px;
+}
+
+.mx_RoomTileContextMenu_notif_field {
+    padding-top: 4px;
+    padding-right: 6px;
+    padding-bottom: 10px;
+    padding-left: 8px; /* 20px */
+    cursor: pointer;
+    white-space: nowrap;
+    display: flex;
+    align-items: center;
+}
+
+.mx_RoomTileContextMenu_notif_field.mx_RoomTileContextMenu_notif_fieldSet {
+    font-weight: bold;
+}
+
+.mx_RoomTileContextMenu_notif_field.mx_RoomTileContextMenu_notif_fieldDisabled {
+    color: rgba(0, 0, 0, 0.2);
+}
+
+.mx_RoomTileContextMenu_notif_icon {
+    padding-right: 4px;
+    padding-left: 4px;
+}
+
+.mx_RoomTileContextMenu_notif_activeIcon {
+    display: inline-block;
+    opacity: 0;
+    position: relative;
+    left: -5px;
+}
+
+.mx_RoomTileContextMenu_notif_fieldSet .mx_RoomTileContextMenu_notif_activeIcon {
+    opacity: 1;
+}

From 32b9ee7f6f50bec215d2639e3cf34b76c7b3ae64 Mon Sep 17 00:00:00 2001
From: Luke Barnard <lukeb@openmarket.com>
Date: Tue, 14 Mar 2017 14:07:10 +0000
Subject: [PATCH 2/2] Copyright

---
 src/components/views/context_menus/RoomTileContextMenu.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/components/views/context_menus/RoomTileContextMenu.js b/src/components/views/context_menus/RoomTileContextMenu.js
index 6430cd37..0998194e 100644
--- a/src/components/views/context_menus/RoomTileContextMenu.js
+++ b/src/components/views/context_menus/RoomTileContextMenu.js
@@ -1,5 +1,6 @@
 /*
 Copyright 2015, 2016 OpenMarket Ltd
+Copyright 2017 Vector Creations Ltd
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.