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 ( +
+
+ + + Favourite +
+
+ + + Low Priority +
+
+
+ + Leave +
+
+ ); + } +}); diff --git a/src/skins/vector/css/common.css b/src/skins/vector/css/common.css index 69fe39af..afca0214 100644 --- a/src/skins/vector/css/common.css +++ b/src/skins/vector/css/common.css @@ -32,6 +32,8 @@ body { color: #454545; border: 0px; margin: 0px; + /* This should render the fonts the same accross browsers */ + -webkit-font-smoothing: subpixel-antialiased; } div.error { diff --git a/src/skins/vector/css/matrix-react-sdk/structures/ContextualMenu.css b/src/skins/vector/css/matrix-react-sdk/structures/ContextualMenu.css index 7523bd10..d317363d 100644 --- a/src/skins/vector/css/matrix-react-sdk/structures/ContextualMenu.css +++ b/src/skins/vector/css/matrix-react-sdk/structures/ContextualMenu.css @@ -55,7 +55,7 @@ limitations under the License. border-bottom: 8px solid transparent; } -.mx_ContextualMenu_chevron_right:after{ +.mx_ContextualMenu_chevron_right:after { content:''; width: 0; height: 0; diff --git a/src/skins/vector/css/matrix-react-sdk/structures/SearchBox.css b/src/skins/vector/css/matrix-react-sdk/structures/SearchBox.css index 59895238..d9e4e05f 100644 --- a/src/skins/vector/css/matrix-react-sdk/structures/SearchBox.css +++ b/src/skins/vector/css/matrix-react-sdk/structures/SearchBox.css @@ -16,8 +16,8 @@ limitations under the License. .mx_SearchBox { height: 24px; - margin-left: 18px; - margin-right: 18px; + margin-left: 16px; + margin-right: 16px; padding-top: 24px; padding-bottom: 22px; border-bottom: 1px solid rgba(0, 0, 0, 0.1); diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/RoomHeader.css b/src/skins/vector/css/matrix-react-sdk/views/rooms/RoomHeader.css index 82456877..056fa879 100644 --- a/src/skins/vector/css/matrix-react-sdk/views/rooms/RoomHeader.css +++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/RoomHeader.css @@ -233,7 +233,7 @@ limitations under the License. } .mx_RoomHeader_button { - margin-left: 8px; + margin-left: 12px; cursor: pointer; } diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/RoomTile.css b/src/skins/vector/css/matrix-react-sdk/views/rooms/RoomTile.css index eca63c78..c266b027 100644 --- a/src/skins/vector/css/matrix-react-sdk/views/rooms/RoomTile.css +++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/RoomTile.css @@ -32,13 +32,49 @@ limitations under the License. display: inline-block; padding-top: 5px; padding-bottom: 5px; - padding-left: 18px; + padding-left: 16px; padding-right: 6px; width: 24px; height: 24px; vertical-align: middle; } +.mx_RoomTile_avatar_container:hover: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; + left: 15px; + width: 24px; + height: 24px; + z-index: 4; +} + +.mx_RoomTile_avatar_container:hover:after, +.mx_RoomTile_avatar_container.mx_RoomTile_avatar_roomTagMenu:after { + display: block; + position: absolute; + content: ""; + border-radius: 40px; + background: #4A4A4A; + top: 5px; + width: 24px; + height: 24px; + opacity: 0.6; + z-index: 2; +} + +.collapsed .mx_RoomTile_avatar_container:hover:before { + display: none; +} + +.collapsed .mx_RoomTile_avatar_container:hover:after { + display: none; +} + .mx_RoomTile_name { display: inline-block; position: relative; @@ -116,13 +152,13 @@ limitations under the License. } .mx_RoomTile .mx_RoomTile_badge.mx_RoomTile_badgeButton, -.mx_RoomTile.mx_RoomTile_menu .mx_RoomTile_badge { +.mx_RoomTile.mx_RoomTile_notificationStateMenu .mx_RoomTile_badge { letter-spacing: 0.1em; opacity: 1; } .mx_RoomTile.mx_RoomTile_noBadges .mx_RoomTile_badge.mx_RoomTile_badgeButton, -.mx_RoomTile.mx_RoomTile_menu.mx_RoomTile_noBadges .mx_RoomTile_badge { +.mx_RoomTile.mx_RoomTile_notificationStateMenu.mx_RoomTile_noBadges .mx_RoomTile_badge { background-color: rgb(214, 214, 214); } diff --git a/src/skins/vector/css/vector-web/views/context_menus/RoomTagContextMenu.css b/src/skins/vector/css/vector-web/views/context_menus/RoomTagContextMenu.css new file mode 100644 index 00000000..947fd480 --- /dev/null +++ b/src/skins/vector/css/vector-web/views/context_menus/RoomTagContextMenu.css @@ -0,0 +1,79 @@ +/* +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: #ff0064; +} + +.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: #bbbbbb; + opacity: 0.4; +} + +.mx_RoomTagContextMenu_fieldSet .mx_RoomTagContextMenu_icon { + /* Something to indicate that the icon is the set tag */ +} diff --git a/src/skins/vector/img/icon-context-delete.svg b/src/skins/vector/img/icon-context-delete.svg new file mode 100644 index 00000000..fba9fa11 --- /dev/null +++ b/src/skins/vector/img/icon-context-delete.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/skins/vector/img/icon-context-fave-on.svg b/src/skins/vector/img/icon-context-fave-on.svg new file mode 100644 index 00000000..2ae172d8 --- /dev/null +++ b/src/skins/vector/img/icon-context-fave-on.svg @@ -0,0 +1,15 @@ + + + + DAE17B64-40B5-478A-8E8D-97AD1A6E25C8 + Created with sketchtool. + + + + + + + + + + diff --git a/src/skins/vector/img/icon-context-fave.svg b/src/skins/vector/img/icon-context-fave.svg new file mode 100644 index 00000000..451e1849 --- /dev/null +++ b/src/skins/vector/img/icon-context-fave.svg @@ -0,0 +1,15 @@ + + + + 8A6E1837-F0F1-432E-A0DA-6F3741F71EBF + Created with sketchtool. + + + + + + + + + + diff --git a/src/skins/vector/img/icon-context-low-on.svg b/src/skins/vector/img/icon-context-low-on.svg new file mode 100644 index 00000000..7578c633 --- /dev/null +++ b/src/skins/vector/img/icon-context-low-on.svg @@ -0,0 +1,15 @@ + + + + CD51482C-F2D4-4F63-AF9E-86513F9AF87F + Created with sketchtool. + + + + + + + + + + diff --git a/src/skins/vector/img/icon-context-low.svg b/src/skins/vector/img/icon-context-low.svg new file mode 100644 index 00000000..663f3ca9 --- /dev/null +++ b/src/skins/vector/img/icon-context-low.svg @@ -0,0 +1,15 @@ + + + + B160345F-40D3-4BE6-A860-6D04BF223EF7 + Created with sketchtool. + + + + + + + + + + diff --git a/src/skins/vector/img/icon_context_delete.svg b/src/skins/vector/img/icon_context_delete.svg new file mode 100644 index 00000000..896b94ad --- /dev/null +++ b/src/skins/vector/img/icon_context_delete.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/skins/vector/img/icon_context_fave.svg b/src/skins/vector/img/icon_context_fave.svg new file mode 100644 index 00000000..da7b14a1 --- /dev/null +++ b/src/skins/vector/img/icon_context_fave.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/skins/vector/img/icon_context_fave_on.svg b/src/skins/vector/img/icon_context_fave_on.svg new file mode 100644 index 00000000..e22e92d3 --- /dev/null +++ b/src/skins/vector/img/icon_context_fave_on.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/skins/vector/img/icon_context_low.svg b/src/skins/vector/img/icon_context_low.svg new file mode 100644 index 00000000..ea579ef4 --- /dev/null +++ b/src/skins/vector/img/icon_context_low.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/skins/vector/img/icon_context_low_on.svg b/src/skins/vector/img/icon_context_low_on.svg new file mode 100644 index 00000000..28300f9a --- /dev/null +++ b/src/skins/vector/img/icon_context_low_on.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/skins/vector/img/icons_ellipsis.svg b/src/skins/vector/img/icons_ellipsis.svg new file mode 100644 index 00000000..ba600cca --- /dev/null +++ b/src/skins/vector/img/icons_ellipsis.svg @@ -0,0 +1 @@ +