diff --git a/src/actions/TagOrderActions.js b/src/actions/TagOrderActions.js
index dd4df6a9d4..3504adb09b 100644
--- a/src/actions/TagOrderActions.js
+++ b/src/actions/TagOrderActions.js
@@ -56,4 +56,51 @@ TagOrderActions.moveTag = function(matrixClient, tag, destinationIx) {
});
};
+/**
+ * Creates an action thunk that will do an asynchronous request to
+ * label a tag as removed in im.vector.web.tag_ordering account data.
+ *
+ * The reason this is implemented with new state `removedTags` is that
+ * we incrementally and initially populate `tags` with groups that
+ * have been joined. If we remove a group from `tags`, it will just
+ * get added (as it looks like a group we've recently joined).
+ *
+ * NB: If we ever support adding of tags (which is planned), we should
+ * take special care to remove the tag from `removedTags` when we add
+ * it.
+ *
+ * @param {MatrixClient} matrixClient the matrix client to set the
+ * account data on.
+ * @param {string} tag the tag to remove.
+ * @returns {function} an action thunk that will dispatch actions
+ * indicating the status of the request.
+ * @see asyncAction
+ */
+TagOrderActions.removeTag = function(matrixClient, tag) {
+ // Don't change tags, just removedTags
+ const tags = TagOrderStore.getOrderedTags();
+ const removedTags = TagOrderStore.getRemovedTagsAccountData() || [];
+
+ if (removedTags.includes(tag)) {
+ // Return a thunk that doesn't do anything, we don't even need
+ // an asynchronous action here, the tag is already removed.
+ return () => {};
+ }
+
+ removedTags.push(tag);
+
+ const storeId = TagOrderStore.getStoreId();
+
+ return asyncAction('TagOrderActions.removeTag', () => {
+ Analytics.trackEvent('TagOrderActions', 'removeTag');
+ return matrixClient.setAccountData(
+ 'im.vector.web.tag_ordering',
+ {tags, removedTags, _storeId: storeId},
+ );
+ }, () => {
+ // For an optimistic update
+ return {removedTags};
+ });
+};
+
export default TagOrderActions;
diff --git a/src/components/views/elements/TagTile.js b/src/components/views/elements/TagTile.js
index f52f758cc0..8d801d986d 100644
--- a/src/components/views/elements/TagTile.js
+++ b/src/components/views/elements/TagTile.js
@@ -21,6 +21,7 @@ import { MatrixClient } from 'matrix-js-sdk';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent } from '../../../Keyboard';
+import ContextualMenu from '../../structures/ContextualMenu';
import FlairStore from '../../../stores/FlairStore';
@@ -81,6 +82,35 @@ export default React.createClass({
});
},
+ onContextButtonClick: function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ // Hide the (...) immediately
+ this.setState({ hover: false });
+
+ const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu');
+ const elementRect = e.target.getBoundingClientRect();
+
+ // The window X and Y offsets are to adjust position when zoomed in to page
+ const x = elementRect.right + window.pageXOffset + 3;
+ const chevronOffset = 12;
+ let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset);
+ y = y - (chevronOffset + 8); // where 8 is half the height of the chevron
+
+ const self = this;
+ ContextualMenu.createMenu(TagTileContextMenu, {
+ chevronOffset: chevronOffset,
+ left: x,
+ top: y,
+ tag: this.props.tag,
+ onFinished: function() {
+ self.setState({ menuDisplayed: false });
+ },
+ });
+ this.setState({ menuDisplayed: true });
+ },
+
onMouseOver: function() {
this.setState({hover: true});
},
@@ -109,10 +139,15 @@ export default React.createClass({
const tip = this.state.hover ?