Merge pull request #2861 from matrix-org/travis/breadcrumbs/badges
Add badges to breadcrumb rooms
This commit is contained in:
commit
5066d68875
6 changed files with 120 additions and 38 deletions
|
@ -17,8 +17,8 @@ limitations under the License.
|
||||||
.mx_RoomBreadcrumbs {
|
.mx_RoomBreadcrumbs {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 42px;
|
height: 42px;
|
||||||
margin: 8px;
|
padding: 8px;
|
||||||
margin-bottom: 0;
|
padding-bottom: 0;
|
||||||
overflow-x: visible;
|
overflow-x: visible;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -34,6 +34,13 @@ limitations under the License.
|
||||||
height: 32px;
|
height: 32px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
transition: transform 0.3s, width 0.3s;
|
transition: transform 0.3s, width 0.3s;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.mx_RoomTile_badge {
|
||||||
|
position: absolute;
|
||||||
|
top: -3px;
|
||||||
|
right: -4px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomBreadcrumbs_animate {
|
.mx_RoomBreadcrumbs_animate {
|
||||||
|
|
|
@ -144,11 +144,14 @@ limitations under the License.
|
||||||
font-size: 12px;
|
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;
|
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;
|
background-color: $warning-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,8 @@ export const ALL_MESSAGES = 'all_messages';
|
||||||
export const MENTIONS_ONLY = 'mentions_only';
|
export const MENTIONS_ONLY = 'mentions_only';
|
||||||
export const MUTE = 'mute';
|
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) {
|
function _shouldShowNotifBadge(roomNotifState) {
|
||||||
const showBadgeInStates = [ALL_MESSAGES, ALL_MESSAGES_LOUD];
|
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) {
|
function setRoomNotifsStateMuted(roomId) {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const promises = [];
|
const promises = [];
|
||||||
|
@ -204,4 +228,3 @@ function isRuleForRoom(roomId, rule) {
|
||||||
function isMuteRule(rule) {
|
function isMuteRule(rule) {
|
||||||
return (rule.actions.length === 1 && rule.actions[0] === 'dont_notify');
|
return (rule.actions.length === 1 && rule.actions[0] === 'dont_notify');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017 Vector Creations 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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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 PropTypes from 'prop-types';
|
||||||
import RoomTile from "../views/rooms/RoomTile";
|
import RoomTile from "../views/rooms/RoomTile";
|
||||||
import LazyRenderList from "../views/elements/LazyRenderList";
|
import LazyRenderList from "../views/elements/LazyRenderList";
|
||||||
import MatrixClientPeg from "../../MatrixClientPeg";
|
|
||||||
|
|
||||||
// turn this on for drop & drag console debugging galore
|
// turn this on for drop & drag console debugging galore
|
||||||
const debug = false;
|
const debug = false;
|
||||||
|
@ -139,28 +138,6 @@ const RoomSubList = React.createClass({
|
||||||
this.setState(this.state);
|
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) {
|
makeRoomTile: function(room) {
|
||||||
return <RoomTile
|
return <RoomTile
|
||||||
room={room}
|
room={room}
|
||||||
|
@ -169,8 +146,8 @@ const RoomSubList = React.createClass({
|
||||||
key={room.roomId}
|
key={room.roomId}
|
||||||
collapsed={this.props.collapsed || false}
|
collapsed={this.props.collapsed || false}
|
||||||
unread={Unread.doesRoomHaveUnreadMessages(room)}
|
unread={Unread.doesRoomHaveUnreadMessages(room)}
|
||||||
highlight={this.props.isInvite || this.getUnreadNotificationCount(room, 'highlight') > 0}
|
highlight={this.props.isInvite || RoomNotifs.getUnreadNotificationCount(room, 'highlight') > 0}
|
||||||
notificationCount={this.getUnreadNotificationCount(room)}
|
notificationCount={RoomNotifs.getUnreadNotificationCount(room)}
|
||||||
isInvite={this.props.isInvite}
|
isInvite={this.props.isInvite}
|
||||||
refreshSubList={this._updateSubListCount}
|
refreshSubList={this._updateSubListCount}
|
||||||
incomingCall={null}
|
incomingCall={null}
|
||||||
|
|
|
@ -22,6 +22,8 @@ import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import RoomAvatar from '../avatars/RoomAvatar';
|
import RoomAvatar from '../avatars/RoomAvatar';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import sdk from "../../../index";
|
import sdk from "../../../index";
|
||||||
|
import * as RoomNotifs from '../../../RoomNotifs';
|
||||||
|
import * as FormattingUtils from "../../../utils/FormattingUtils";
|
||||||
|
|
||||||
const MAX_ROOMS = 20;
|
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.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() {
|
componentWillUnmount() {
|
||||||
dis.unregister(this._dispatcherRef);
|
dis.unregister(this._dispatcherRef);
|
||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
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() {
|
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) {
|
_appendRoomId(roomId) {
|
||||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||||
if (!room) {
|
if (!room) {
|
||||||
|
@ -138,13 +199,12 @@ export default class RoomBreadcrumbs extends React.Component {
|
||||||
const Tooltip = sdk.getComponent('elements.Tooltip');
|
const Tooltip = sdk.getComponent('elements.Tooltip');
|
||||||
const IndicatorScrollbar = sdk.getComponent('structures.IndicatorScrollbar');
|
const IndicatorScrollbar = sdk.getComponent('structures.IndicatorScrollbar');
|
||||||
|
|
||||||
// check for collapsed here and
|
// check for collapsed here and not at parent so we keep rooms in our state
|
||||||
// not at parent so we keep
|
|
||||||
// rooms in our state
|
|
||||||
// when collapsing and expanding
|
// when collapsing and expanding
|
||||||
if (this.props.collapsed) {
|
if (this.props.collapsed) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rooms = this.state.rooms;
|
const rooms = this.state.rooms;
|
||||||
const avatars = rooms.map((r, i) => {
|
const avatars = rooms.map((r, i) => {
|
||||||
const isFirst = i === 0;
|
const isFirst = i === 0;
|
||||||
|
@ -160,10 +220,23 @@ export default class RoomBreadcrumbs extends React.Component {
|
||||||
tooltip = <Tooltip label={r.room.name} />;
|
tooltip = <Tooltip label={r.room.name} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 = <div className={badgeClasses}>{r.formattedCount}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccessibleButton className={classes} key={r.room.roomId} onClick={() => this._viewRoom(r.room)}
|
<AccessibleButton className={classes} key={r.room.roomId} onClick={() => this._viewRoom(r.room)}
|
||||||
onMouseEnter={() => this._onMouseEnter(r.room)} onMouseLeave={() => this._onMouseLeave(r.room)}>
|
onMouseEnter={() => this._onMouseEnter(r.room)} onMouseLeave={() => this._onMouseLeave(r.room)}>
|
||||||
<RoomAvatar room={r.room} width={32} height={32} />
|
<RoomAvatar room={r.room} width={32} height={32} />
|
||||||
|
{badge}
|
||||||
{tooltip}
|
{tooltip}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
|
|
|
@ -68,12 +68,11 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_shouldShowNotifBadge: function() {
|
_shouldShowNotifBadge: function() {
|
||||||
const showBadgeInStates = [RoomNotifs.ALL_MESSAGES, RoomNotifs.ALL_MESSAGES_LOUD];
|
return RoomNotifs.BADGE_STATES.includes(this.state.notifState);
|
||||||
return showBadgeInStates.indexOf(this.state.notifState) > -1;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_shouldShowMentionBadge: function() {
|
_shouldShowMentionBadge: function() {
|
||||||
return this.state.notifState !== RoomNotifs.MUTE;
|
return RoomNotifs.MENTION_BADGE_STATES.includes(this.state.notifState);
|
||||||
},
|
},
|
||||||
|
|
||||||
_isDirectMessageRoom: function(roomId) {
|
_isDirectMessageRoom: function(roomId) {
|
||||||
|
|
Loading…
Reference in a new issue