diff --git a/res/css/views/rooms/_RoomBreadcrumbs.scss b/res/css/views/rooms/_RoomBreadcrumbs.scss index 67227c7115..7e7ab5f23b 100644 --- a/res/css/views/rooms/_RoomBreadcrumbs.scss +++ b/res/css/views/rooms/_RoomBreadcrumbs.scss @@ -17,8 +17,8 @@ limitations under the License. .mx_RoomBreadcrumbs { position: relative; height: 42px; - margin: 8px; - margin-bottom: 0; + padding: 8px; + padding-bottom: 0; overflow-x: visible; display: flex; flex-direction: row; @@ -34,6 +34,13 @@ limitations under the License. height: 32px; display: inline-block; transition: transform 0.3s, width 0.3s; + position: relative; + + .mx_RoomTile_badge { + position: absolute; + top: -3px; + right: -4px; + } } .mx_RoomBreadcrumbs_animate { diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index 97b2c48236..a1fc9bdca1 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -144,11 +144,14 @@ limitations under the License. font-size: 12px; } -.mx_RoomTile_unreadNotify .mx_RoomTile_badge { +.mx_RoomTile_unreadNotify .mx_RoomTile_badge, +.mx_RoomTile_badge.mx_RoomTile_badgeUnread { background-color: $roomtile-name-color; } -.mx_RoomTile_highlight .mx_RoomTile_badge { +.mx_RoomTile_highlight .mx_RoomTile_badge, +.mx_RoomTile_badge.mx_RoomTile_badgeRed +{ background-color: $warning-color; } diff --git a/src/RoomNotifs.js b/src/RoomNotifs.js index 77fb5efb76..39384b5bea 100644 --- a/src/RoomNotifs.js +++ b/src/RoomNotifs.js @@ -23,6 +23,8 @@ export const ALL_MESSAGES = 'all_messages'; export const MENTIONS_ONLY = 'mentions_only'; export const MUTE = 'mute'; +export const BADGE_STATES = [ALL_MESSAGES, ALL_MESSAGES_LOUD]; +export const MENTION_BADGE_STATES = [...BADGE_STATES, MENTIONS_ONLY]; function _shouldShowNotifBadge(roomNotifState) { const showBadgeInStates = [ALL_MESSAGES, ALL_MESSAGES_LOUD]; @@ -107,6 +109,28 @@ export function setRoomNotifsState(roomId, newState) { } } +export function getUnreadNotificationCount(room, type=null) { + let notificationCount = room.getUnreadNotificationCount(type); + + // Check notification counts in the old room just in case there's some lost + // there. We only go one level down to avoid performance issues, and theory + // is that 1st generation rooms will have already been read by the 3rd generation. + const createEvent = room.currentState.getStateEvents("m.room.create", ""); + if (createEvent && createEvent.getContent()['predecessor']) { + const oldRoomId = createEvent.getContent()['predecessor']['room_id']; + const oldRoom = MatrixClientPeg.get().getRoom(oldRoomId); + if (oldRoom) { + // We only ever care if there's highlights in the old room. No point in + // notifying the user for unread messages because they would have extreme + // difficulty changing their notification preferences away from "All Messages" + // and "Noisy". + notificationCount += oldRoom.getUnreadNotificationCount("highlight"); + } + } + + return notificationCount; +} + function setRoomNotifsStateMuted(roomId) { const cli = MatrixClientPeg.get(); const promises = []; @@ -204,4 +228,3 @@ function isRuleForRoom(roomId, rule) { function isMuteRule(rule) { return (rule.actions.length === 1 && rule.actions[0] === 'dont_notify'); } - diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index 825a80b844..d094d88c24 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -1,7 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd -Copyright 2018 New Vector Ltd +Copyright 2018, 2019 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -29,7 +29,6 @@ import { Group } from 'matrix-js-sdk'; import PropTypes from 'prop-types'; import RoomTile from "../views/rooms/RoomTile"; import LazyRenderList from "../views/elements/LazyRenderList"; -import MatrixClientPeg from "../../MatrixClientPeg"; // turn this on for drop & drag console debugging galore const debug = false; @@ -139,28 +138,6 @@ const RoomSubList = React.createClass({ this.setState(this.state); }, - getUnreadNotificationCount: function(room, type=null) { - let notificationCount = room.getUnreadNotificationCount(type); - - // Check notification counts in the old room just in case there's some lost - // there. We only go one level down to avoid performance issues, and theory - // is that 1st generation rooms will have already been read by the 3rd generation. - const createEvent = room.currentState.getStateEvents("m.room.create", ""); - if (createEvent && createEvent.getContent()['predecessor']) { - const oldRoomId = createEvent.getContent()['predecessor']['room_id']; - const oldRoom = MatrixClientPeg.get().getRoom(oldRoomId); - if (oldRoom) { - // We only ever care if there's highlights in the old room. No point in - // notifying the user for unread messages because they would have extreme - // difficulty changing their notification preferences away from "All Messages" - // and "Noisy". - notificationCount += oldRoom.getUnreadNotificationCount("highlight"); - } - } - - return notificationCount; - }, - makeRoomTile: function(room) { return 0} - notificationCount={this.getUnreadNotificationCount(room)} + highlight={this.props.isInvite || RoomNotifs.getUnreadNotificationCount(room, 'highlight') > 0} + notificationCount={RoomNotifs.getUnreadNotificationCount(room)} isInvite={this.props.isInvite} refreshSubList={this._updateSubListCount} incomingCall={null} diff --git a/src/components/views/rooms/RoomBreadcrumbs.js b/src/components/views/rooms/RoomBreadcrumbs.js index 69c36e5a65..314b2912cd 100644 --- a/src/components/views/rooms/RoomBreadcrumbs.js +++ b/src/components/views/rooms/RoomBreadcrumbs.js @@ -22,6 +22,8 @@ import AccessibleButton from '../elements/AccessibleButton'; import RoomAvatar from '../avatars/RoomAvatar'; import classNames from 'classnames'; import sdk from "../../../index"; +import * as RoomNotifs from '../../../RoomNotifs'; +import * as FormattingUtils from "../../../utils/FormattingUtils"; const MAX_ROOMS = 20; @@ -54,13 +56,21 @@ export default class RoomBreadcrumbs extends React.Component { } MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership); + MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt); + MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); + MatrixClientPeg.get().on("Event.decrypted", this.onEventDecrypted); } componentWillUnmount() { dis.unregister(this._dispatcherRef); const client = MatrixClientPeg.get(); - if (client) client.removeListener("Room.myMembership", this.onMyMembership); + if (client) { + client.removeListener("Room.myMembership", this.onMyMembership); + client.removeListener("Room.receipt", this.onRoomReceipt); + client.removeListener("Room.timeline", this.onRoomTimeline); + client.removeListener("Event.decrypted", this.onEventDecrypted); + } } componentDidUpdate() { @@ -97,6 +107,57 @@ export default class RoomBreadcrumbs extends React.Component { } }; + onRoomReceipt = (event, room) => { + if (this.state.rooms.map(r => r.room.roomId).includes(room.roomId)) { + this._calculateRoomBadges(room); + } + }; + + onRoomTimeline = (event, room) => { + if (this.state.rooms.map(r => r.room.roomId).includes(room.roomId)) { + this._calculateRoomBadges(room); + } + }; + + onEventDecrypted = (event) => { + if (this.state.rooms.map(r => r.room.roomId).includes(event.getRoomId())) { + this._calculateRoomBadges(MatrixClientPeg.get().getRoom(event.getRoomId())); + } + }; + + _calculateRoomBadges(room) { + if (!room) return; + + const rooms = this.state.rooms.slice(); + const roomModel = rooms.find((r) => r.room.roomId === room.roomId); + if (!roomModel) return; // No applicable room, so don't do math on it + + // Reset the notification variables for simplicity + roomModel.redBadge = false; + roomModel.formattedCount = "0"; + roomModel.showCount = false; + + const notifState = RoomNotifs.getRoomNotifsState(room.roomId); + if (RoomNotifs.MENTION_BADGE_STATES.includes(notifState)) { + const highlightNotifs = RoomNotifs.getUnreadNotificationCount(room, 'highlight'); + const unreadNotifs = RoomNotifs.getUnreadNotificationCount(room); + + const redBadge = highlightNotifs > 0; + const greyBadge = redBadge || (unreadNotifs > 0 && RoomNotifs.BADGE_STATES.includes(notifState)); + + if (redBadge || greyBadge) { + const notifCount = redBadge ? highlightNotifs : unreadNotifs; + const limitedCount = FormattingUtils.formatCount(notifCount); + + roomModel.redBadge = redBadge; + roomModel.formattedCount = limitedCount; + roomModel.showCount = true; + } + } + + this.setState({rooms}); + } + _appendRoomId(roomId) { const room = MatrixClientPeg.get().getRoom(roomId); if (!room) { @@ -138,13 +199,12 @@ export default class RoomBreadcrumbs extends React.Component { const Tooltip = sdk.getComponent('elements.Tooltip'); const IndicatorScrollbar = sdk.getComponent('structures.IndicatorScrollbar'); - // check for collapsed here and - // not at parent so we keep - // rooms in our state + // check for collapsed here and not at parent so we keep rooms in our state // when collapsing and expanding if (this.props.collapsed) { return null; } + const rooms = this.state.rooms; const avatars = rooms.map((r, i) => { const isFirst = i === 0; @@ -160,10 +220,23 @@ export default class RoomBreadcrumbs extends React.Component { tooltip = ; } + let badge; + if (r.showCount) { + const badgeClasses = classNames({ + 'mx_RoomTile_badge': true, + 'mx_RoomTile_badgeButton': true, + 'mx_RoomTile_badgeRed': r.redBadge, + 'mx_RoomTile_badgeUnread': !r.redBadge, + }); + + badge =
{r.formattedCount}
; + } + return ( this._viewRoom(r.room)} onMouseEnter={() => this._onMouseEnter(r.room)} onMouseLeave={() => this._onMouseLeave(r.room)}> + {badge} {tooltip} ); diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 4bf160007e..93b4a59fca 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -68,12 +68,11 @@ module.exports = React.createClass({ }, _shouldShowNotifBadge: function() { - const showBadgeInStates = [RoomNotifs.ALL_MESSAGES, RoomNotifs.ALL_MESSAGES_LOUD]; - return showBadgeInStates.indexOf(this.state.notifState) > -1; + return RoomNotifs.BADGE_STATES.includes(this.state.notifState); }, _shouldShowMentionBadge: function() { - return this.state.notifState !== RoomNotifs.MUTE; + return RoomNotifs.MENTION_BADGE_STATES.includes(this.state.notifState); }, _isDirectMessageRoom: function(roomId) {