diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index 45e471314..63b340563 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -17,15 +17,20 @@ limitations under the License. 'use strict'; var React = require('react'); +var ReactDOM = require('react-dom'); +var classNames = require('classnames'); var DropTarget = require('react-dnd').DropTarget; var sdk = require('matrix-react-sdk') var dis = require('matrix-react-sdk/lib/dispatcher'); var Unread = require('matrix-react-sdk/lib/Unread'); var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg'); +var RoomNotifs = require('matrix-react-sdk/lib/RoomNotifs'); // turn this on for drop & drag console debugging galore var debug = false; +const TRUNCATE_AT = 10; + var roomListTarget = { canDrop: function() { return true; @@ -80,7 +85,8 @@ var RoomSubList = React.createClass({ getInitialState: function() { return { hidden: this.props.startAsHidden || false, - truncateAt: 20, + capTruncate: this.props.list.length > TRUNCATE_AT, + truncateAt: this.props.list.length > TRUNCATE_AT ? TRUNCATE_AT : -1, sortedList: [], }; }, @@ -109,17 +115,62 @@ var RoomSubList = React.createClass({ }); }, - onClick: function(ev) { - var isHidden = !this.state.hidden; - this.setState({ hidden : isHidden }); - - if (isHidden) { - // as good a way as any to reset the truncate state - this.setState({ truncateAt : 20 }); - this.props.onShowMoreRooms(); + // The header is collapsable if it is hidden or not stuck + // The dataset elements are added in the RoomList _initAndPositionStickyHeaders method + isCollapsableOnClick: function() { + var stuck = this.refs.header.dataset.stuck; + if (this.state.hidden || stuck === undefined || stuck === "none") { + return true; + } else { + return false; } + }, - this.props.onHeaderClick(isHidden); + onClick: function(ev) { + if (this.isCollapsableOnClick()) { + // The header iscCollapsable, so the click is to be interpreted as collapse and truncation logic + var isHidden = false; + var isTruncatable = this.props.list.length > TRUNCATE_AT; + + if (this.state.hidden && (this.state.capTruncate && isTruncatable)) { + isHidden = false; + this.setState({ + hidden : isHidden, + capTruncate : true, + truncateAt : TRUNCATE_AT + }); + } else if ((!this.state.hidden && this.state.capTruncate) + || (this.state.hidden && (this.state.capTruncate && !isTruncatable))) + { + isHidden = false; + this.setState({ + hidden : isHidden, + capTruncate : false, + truncateAt : -1 + }); + } else if (!this.state.hidden && !this.state.capTruncate) { + isHidden = true; + this.setState({ + hidden : isHidden, + capTruncate : true, + truncateAt : TRUNCATE_AT + }); + } else { + // Catch any weird states the system gets into + isHidden = false; + this.setState({ + hidden : isHidden, + capTruncate : true, + truncateAt : TRUNCATE_AT + }); + } + + this.props.onShowMoreRooms(); + this.props.onHeaderClick(isHidden); + } else { + // The header is stuck, so the click is to be interpreted as a scroll to the header + this.props.onHeaderClick(this.state.hidden, this.refs.header.dataset.originalPosition); + } }, tsOfNewestEvent: function(room) { @@ -186,6 +237,44 @@ var RoomSubList = React.createClass({ this.setState({ sortedList: list.sort(comparator) }); }, + _shouldShowNotifBadge: function(roomNotifState) { + const showBadgeInStates = [RoomNotifs.ALL_MESSAGES, RoomNotifs.ALL_MESSAGES_LOUD]; + return showBadgeInStates.indexOf(roomNotifState) > -1; + }, + + _shouldShowMentionBadge: function(roomNotifState) { + return roomNotifState != RoomNotifs.MUTE; + }, + + roomNotificationCount: function() { + var self = this; + + return this.props.list.reduce(function(result, room) { + var roomNotifState = RoomNotifs.getRoomNotifsState(room.roomId); + var highlight = room.getUnreadNotificationCount('highlight') > 0 || self.props.label === 'Invites'; + var notificationCount = room.getUnreadNotificationCount(); + + const notifBadges = notificationCount > 0 && self._shouldShowNotifBadge(roomNotifState); + const mentionBadges = highlight && self._shouldShowMentionBadge(roomNotifState); + const badges = notifBadges || mentionBadges; + + if (badges) { + result[0] += notificationCount; + if (highlight) { + result[1] = true; + } + } + return result; + }, [0, false]); + }, + + _updateSubListCount: function() { + // Force an update by setting the state to the current state + // Doing it this way rather than using forceUpdate(), so that the shouldComponentUpdate() + // method is honoured + this.setState(this.state); + }, + moveRoomTile: function(room, atIndex) { if (debug) console.log("moveRoomTile: id " + room.roomId + ", atIndex " + atIndex); //console.log("moveRoomTile before: " + JSON.stringify(this.state.rooms)); @@ -293,6 +382,7 @@ var RoomSubList = React.createClass({ unread={ Unread.doesRoomHaveUnreadMessages(room) } highlight={ room.getUnreadNotificationCount('highlight') > 0 || self.props.label === 'Invites' } isInvite={ self.props.label === 'Invites' } + refreshSubList={ self._updateSubListCount } incomingCall={ self.props.incomingCall && (self.props.incomingCall.roomId === room.roomId) ? self.props.incomingCall : null } /> ); }); @@ -300,35 +390,47 @@ var RoomSubList = React.createClass({ _getHeaderJsx: function() { var TintableSvg = sdk.getComponent("elements.TintableSvg"); - return ( -

- { this.props.collapsed ? '' : this.props.label } - -

- ); - }, - _createOverflowTile: function(overflowCount, totalCount) { - var BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); - // XXX: this is duplicated from RoomTile - factor it out + var subListNotifications = this.roomNotificationCount(); + var subListNotifCount = subListNotifications[0]; + var subListNotifHighlight = subListNotifications[1]; + + var roomCount = this.props.list.length > 0 ? this.props.list.length : ''; + var isTruncatable = this.props.list.length > TRUNCATE_AT; + if (!this.state.hidden && this.state.capTruncate && isTruncatable) { + roomCount = TRUNCATE_AT + " of " + roomCount; + } + + var chevronClasses = classNames({ + 'mx_RoomSubList_chevron': true, + 'mx_RoomSubList_chevronUp': this.state.hidden, + 'mx_RoomSubList_chevronRight': !this.state.hidden && this.state.capTruncate, + 'mx_RoomSubList_chevronDown': !this.state.hidden && !this.state.capTruncate, + }); + + var badgeClasses = classNames({ + 'mx_RoomSubList_badge': true, + 'mx_RoomSubList_badgeHighlight': subListNotifHighlight, + }); + + var badge; + if (subListNotifCount > 0) { + badge =
{subListNotifCount > 99 ? "99+" : subListNotifCount}
; + } + return ( -
-
- +
+
+ { this.props.collapsed ? '' : this.props.label } +
{roomCount}
+
+ {badge}
-
and { overflowCount } others...
); }, - _showFullMemberList: function() { - this.setState({ - truncateAt: -1 - }); - this.props.onShowMoreRooms(); - }, + _createOverflowTile: function() {}, // NOP // Fix any undefined order elements of a room in a manual ordered list // room.tag[tagname].order 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 d9e4e05fd..0b5362593 100644 --- a/src/skins/vector/css/matrix-react-sdk/structures/SearchBox.css +++ b/src/skins/vector/css/matrix-react-sdk/structures/SearchBox.css @@ -20,7 +20,6 @@ limitations under the License. margin-right: 16px; padding-top: 24px; padding-bottom: 22px; - border-bottom: 1px solid rgba(0, 0, 0, 0.1); display: flex; display: -webkit-flex; diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/RoomList.css b/src/skins/vector/css/matrix-react-sdk/views/rooms/RoomList.css index a22418534..4dcda646f 100644 --- a/src/skins/vector/css/matrix-react-sdk/views/rooms/RoomList.css +++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/RoomList.css @@ -15,7 +15,6 @@ limitations under the License. */ .mx_RoomList { - padding-top: 8px; padding-bottom: 12px; min-height: 400px; } @@ -33,3 +32,8 @@ limitations under the License. overflow-x: hidden ! important; overflow-y: scroll ! important; } + +/* Make sure the scrollbar is above the sticky headers from RoomList */ +.mx_RoomList_scrollbar .gm-scrollbar.-vertical { + z-index: 5; +} 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 c266b027d..254565035 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 @@ -109,9 +109,8 @@ limitations under the License. } .collapsed .mx_RoomTile_badge { - top: -2px; + top: 0px; min-width: 12px; - height: 16px; border-radius: 16px; padding: 0px 4px 0px 4px; z-index: 200; @@ -129,22 +128,22 @@ limitations under the License. display: block; width: 0; height: 0; - margin-left: 6px; - border-top: 8px solid #ff0064; - border-right: 10px solid transparent; + margin-left: 5px; + border-top: 5px solid #ff0064; + border-right: 7px solid transparent; } .mx_RoomTile_badge { display: inline-block; - min-width: 19px; - height: 17px; + min-width: 15px; + height: 15px; position: absolute; right: 8px; /*gutter */ top: 9px; - border-radius: 14px; + border-radius: 8px; color: #fff; font-weight: 600; - font-size: 11px; + font-size: 10px; text-align: center; padding-top: 1px; padding-left: 4px; @@ -175,7 +174,7 @@ limitations under the License. } .mx_RoomTile_selected { - background-color: rgba(118,207,166,0.2); + background-color: rgba(255, 255, 255, 0.8); } .mx_RoomTile .mx_RoomTile_name.mx_RoomTile_badgeShown { @@ -187,5 +186,3 @@ limitations under the License. right: 0px; } -.mx_RoomTile:hover { -} diff --git a/src/skins/vector/css/vector-web/structures/LeftPanel.css b/src/skins/vector/css/vector-web/structures/LeftPanel.css index 316246cb3..fb023ffea 100644 --- a/src/skins/vector/css/vector-web/structures/LeftPanel.css +++ b/src/skins/vector/css/vector-web/structures/LeftPanel.css @@ -49,6 +49,7 @@ limitations under the License. flex: 1 1 0; overflow-y: auto; + z-index: 5; } .mx_LeftPanel.collapsed .mx_BottomLeftMenu { @@ -62,7 +63,7 @@ limitations under the License. -webkit-order: 3; order: 3; - border-top: 1px solid rgba(0, 0, 0, 0.1); + border-top: 1px solid rgba(118, 207, 166, 0.2); margin-left: 16px; /* gutter */ margin-right: 16px; /* gutter */ -webkit-flex: 0 0 60px; diff --git a/src/skins/vector/css/vector-web/structures/RoomSubList.css b/src/skins/vector/css/vector-web/structures/RoomSubList.css index b54fea981..4d9b0668f 100644 --- a/src/skins/vector/css/vector-web/structures/RoomSubList.css +++ b/src/skins/vector/css/vector-web/structures/RoomSubList.css @@ -20,23 +20,128 @@ limitations under the License. width: 100%; } +.mx_RoomSubList_labelContainer { + height: 31px; /* mx_RoomSubList_label height including border */ + width: 235px; /* LHS Panel width */ + position: relative; +} + .mx_RoomSubList_label { + position: relative; text-transform: uppercase; color: #3d3b39; font-weight: 600; - font-size: 13px; + font-size: 12px; + width: 203px; /* padding + width = LHS Panel width */ + height: 17px; /* padding + height = 29px, same as mx_RoomSubList_stickyContainer */ padding-left: 16px; /* gutter */ padding-right: 16px; /* gutter */ - margin-top: 8px; - margin-bottom: 4px; + padding-top: 6px; + padding-bottom: 6px; cursor: pointer; + background-color: rgba(118, 207, 166, 0.2); + border-top: solid 2px #eaf5f0; +} + +.mx_RoomSubList_label.mx_RoomSubList_fixed { + position: fixed; + top: 0; + z-index: 4; + /* pointer-events: none; */ +} + +.collapsed .mx_RoomSubList_label { + height: 17px; + width: 28px; /* collapsed LHS Panel width */ +} + +.collapsed .mx_RoomSubList_labelContainer { + width: 28px; /* collapsed LHS Panel width */ +} + +.mx_RoomSubList_roomCount { + display: inline-block; + font-size: 12px; + font-weight: normal; + color: #76cfa6; + padding-left: 5px; + text-transform: none; +} + +.collapsed .mx_RoomSubList_roomCount { + display: none; +} + +.mx_RoomSubList_badge { + display: inline-block; + min-width: 15px; + height: 15px; + position: absolute; + right: 8px; /*gutter */ + top: 7px; + border-radius: 8px; + color: #fff; + font-weight: 600; + font-size: 10px; + text-align: center; + padding-top: 1px; + padding-left: 4px; + padding-right: 4px; + background-color: #76cfa6; +} + +.collapsed .mx_RoomSubList_badge { + display: none; +} + +.mx_RoomSubList_badgeHighlight { + background-color: #ff0064; +} + +/* This is the bottom of the speech bubble */ +.mx_RoomSubList_badgeHighlight:after { + content: ""; + position: absolute; + display: block; + width: 0; + height: 0; + margin-left: 5px; + border-top: 5px solid #ff0064; + border-right: 7px solid transparent; +} + +/* Hide the bottom of speech bubble */ +.collapsed .mx_RoomSubList_badgeHighlight:after { + display: none; } .mx_RoomSubList_chevron { - padding-left: 4px; pointer-events: none; + position: absolute; + right: 41px; + top: 11px; } -.collapsed .mx_RoomSubList_chevron { - padding-left: 12px; +.mx_RoomSubList_chevronDown { + width: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 6px solid #76cfa6; +} + +.mx_RoomSubList_chevronUp { + width: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-bottom: 6px solid #76cfa6; +} + +.mx_RoomSubList_chevronRight { + width: 0; + height: 0; + border-top: 5px solid transparent; + border-left: 6px solid #76cfa6; + border-bottom: 5px solid transparent; }