diff --git a/src/component-index.js b/src/component-index.js index b8efcdb4..b4c73a4b 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -36,6 +36,7 @@ module.exports.components['structures.SearchBox'] = require('./components/struct module.exports.components['structures.ViewSource'] = require('./components/structures/ViewSource'); module.exports.components['views.context_menus.MessageContextMenu'] = require('./components/views/context_menus/MessageContextMenu'); module.exports.components['views.context_menus.NotificationStateContextMenu'] = require('./components/views/context_menus/NotificationStateContextMenu'); +module.exports.components['views.context_menus.RoomTagContextMenu'] = require('./components/views/context_menus/RoomTagContextMenu'); module.exports.components['views.elements.ImageView'] = require('./components/views/elements/ImageView'); module.exports.components['views.elements.Spinner'] = require('./components/views/elements/Spinner'); module.exports.components['views.globals.GuestWarningBar'] = require('./components/views/globals/GuestWarningBar'); diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index a5a6fd21..74ddb879 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -149,11 +149,26 @@ var RoomSubList = React.createClass({ return this.tsOfNewestEvent(roomB) - this.tsOfNewestEvent(roomA); }, + lexicographicalComparator: function(roomA, roomB) { + return roomA.name > roomB.name ? 1 : -1; + }, + + // Generates the manual comparator using the given list manualComparator: function(roomA, roomB) { if (!roomA.tags[this.props.tagName] || !roomB.tags[this.props.tagName]) return 0; + + // Make sure the room tag has an order element, if not set it to be the bottom var a = roomA.tags[this.props.tagName].order; var b = roomB.tags[this.props.tagName].order; - return a == b ? this.recentsComparator(roomA, roomB) : ( a > b ? 1 : -1); + + // Order undefined room tag orders to the bottom + if (a === undefined && b !== undefined) { + return 1; + } else if (a !== undefined && b === undefined) { + return -1; + } + + return a == b ? this.lexicographicalComparator(roomA, roomB) : ( a > b ? 1 : -1); }, sortList: function(list, order) { @@ -164,6 +179,9 @@ var RoomSubList = React.createClass({ if (order === "manual") comparator = this.manualComparator; if (order === "recent") comparator = this.recentsComparator; + // Fix undefined orders here, and make sure the backend gets updated as well + this._fixUndefinedOrder(list); + //if (debug) console.log("sorting list for sublist " + this.props.label + " with length " + list.length + ", this.props.list = " + this.props.list); this.setState({ sortedList: list.sort(comparator) }); }, @@ -312,6 +330,46 @@ var RoomSubList = React.createClass({ this.props.onShowMoreRooms(); }, + // Fix any undefined order elements of a room in a manual ordered list + // room.tag[tagname].order + _fixUndefinedOrder: function(list) { + if (this.props.order === "manual") { + var order = 0.0; + var self = this; + + // Find the highest (lowest position) order of a room in a manual ordered list + list.forEach(function(room) { + if (room.tags.hasOwnProperty(self.props.tagName)) { + if (order < room.tags[self.props.tagName].order) { + order = room.tags[self.props.tagName].order; + } + } + }); + + // Fix any undefined order elements of a room in a manual ordered list + // Do this one at a time, as each time a rooms tag data is updated the RoomList + // gets triggered and another list is passed in. Doing it one at a time means that + // we always correctly calculate the highest order for the list - stops multiple + // rooms getting the same order. This is only really relevant for the first time this + // is run with historical room tag data, after that there should only be undefined + // in the list at a time anyway. + for (let i = 0; i < list.length; i++) { + if (list[i].tags[self.props.tagName].order === undefined) { + MatrixClientPeg.get().setRoomTag(list[i].roomId, self.props.tagName, {order: (order + 1.0) / 2.0}).finally(function() { + // Do any final stuff here + }).fail(function(err) { + var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Failed to add tag " + self.props.tagName + " to room", + description: err.toString() + }); + }); + break; + }; + }; + } + }, + render: function() { var connectDropTarget = this.props.connectDropTarget; var RoomDropTarget = sdk.getComponent('rooms.RoomDropTarget'); diff --git a/src/components/views/context_menus/NotificationStateContextMenu.js b/src/components/views/context_menus/NotificationStateContextMenu.js index 6b885273..8430f8ef 100644 --- a/src/components/views/context_menus/NotificationStateContextMenu.js +++ b/src/components/views/context_menus/NotificationStateContextMenu.js @@ -57,7 +57,7 @@ module.exports = React.createClass({ // Wrapping this in a q promise, as setRoomMutePushRule can return // a promise or a value q(cli.setRoomMutePushRule("global", roomId, areNotifsMuted)) - .then(function(s) { + .then(function() { self.setState({areNotifsMuted: areNotifsMuted}); // delay slightly so that the user can see their state change diff --git a/src/components/views/context_menus/RoomTagContextMenu.js b/src/components/views/context_menus/RoomTagContextMenu.js new file mode 100644 index 00000000..776f9522 --- /dev/null +++ b/src/components/views/context_menus/RoomTagContextMenu.js @@ -0,0 +1,171 @@ +/* +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 MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg'); +var dis = require('matrix-react-sdk/lib/dispatcher'); + +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() { + return { + isFavourite: this.props.room.tags.hasOwnProperty("m.favourite"), + isLowPriority: this.props.room.tags.hasOwnProperty("m.lowpriority"), + }; + }, + + _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"); + } + }, + + _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(); + }; + }, + + render: function() { + var myUserId = MatrixClientPeg.get().credentials.userId; + var myMember = this.props.room.getMember(myUserId); + + var favouriteClasses = classNames({ + 'mx_RoomTagContextMenu_field': true, + 'mx_RoomTagContextMenu_fieldSet': this.state.isFavourite, + 'mx_RoomTagContextMenu_fieldDisabled': false, + }); + + var lowPriorityClasses = classNames({ + 'mx_RoomTagContextMenu_field': true, + 'mx_RoomTagContextMenu_fieldSet': this.state.isLowPriority, + 'mx_RoomTagContextMenu_fieldDisabled': false, + }); + + var leaveClasses = classNames({ + 'mx_RoomTagContextMenu_field': true, + 'mx_RoomTagContextMenu_fieldSet': false, + 'mx_RoomTagContextMenu_fieldDisabled': false, + }); + + return ( +