2018-04-11 22:58:04 +00:00
|
|
|
/*
|
|
|
|
Copyright 2015, 2016 OpenMarket Ltd
|
2018-06-25 08:41:28 +00:00
|
|
|
Copyright 2017 Vector Creations Ltd
|
|
|
|
Copyright 2018 New Vector Ltd
|
2018-04-11 22:58:04 +00:00
|
|
|
|
|
|
|
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.
|
|
|
|
*/
|
|
|
|
|
2018-06-25 08:51:23 +00:00
|
|
|
import React from 'react';
|
|
|
|
import classNames from 'classnames';
|
|
|
|
import sdk from '../../index';
|
|
|
|
import dis from '../../dispatcher';
|
|
|
|
import Unread from '../../Unread';
|
|
|
|
import * as RoomNotifs from '../../RoomNotifs';
|
|
|
|
import * as FormattingUtils from '../../utils/FormattingUtils';
|
2018-04-12 23:43:44 +00:00
|
|
|
import { KeyCode } from '../../Keyboard';
|
2018-06-27 08:49:58 +00:00
|
|
|
import { Group } from 'matrix-js-sdk';
|
2018-06-30 16:06:33 +00:00
|
|
|
import PropTypes from 'prop-types';
|
2018-04-11 22:58:04 +00:00
|
|
|
|
|
|
|
|
|
|
|
// turn this on for drop & drag console debugging galore
|
2018-06-25 08:51:23 +00:00
|
|
|
const debug = false;
|
2018-04-11 22:58:04 +00:00
|
|
|
|
2018-06-25 08:51:23 +00:00
|
|
|
const RoomSubList = React.createClass({
|
2018-04-11 22:58:04 +00:00
|
|
|
displayName: 'RoomSubList',
|
|
|
|
|
|
|
|
debug: debug,
|
|
|
|
|
|
|
|
propTypes: {
|
2018-06-30 16:06:33 +00:00
|
|
|
list: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
|
|
label: PropTypes.string.isRequired,
|
|
|
|
tagName: PropTypes.string,
|
2018-04-11 22:58:04 +00:00
|
|
|
|
2018-06-30 16:06:33 +00:00
|
|
|
order: PropTypes.string.isRequired,
|
2018-04-11 22:58:04 +00:00
|
|
|
|
|
|
|
// passed through to RoomTile and used to highlight room with `!` regardless of notifications count
|
2018-06-30 16:06:33 +00:00
|
|
|
isInvite: PropTypes.bool,
|
|
|
|
|
|
|
|
startAsHidden: PropTypes.bool,
|
|
|
|
showSpinner: PropTypes.bool, // true to show a spinner if 0 elements when expanded
|
|
|
|
collapsed: PropTypes.bool.isRequired, // is LeftPanel collapsed?
|
|
|
|
onHeaderClick: PropTypes.func,
|
|
|
|
alwaysShowHeader: PropTypes.bool,
|
|
|
|
incomingCall: PropTypes.object,
|
|
|
|
searchFilter: PropTypes.string,
|
|
|
|
emptyContent: PropTypes.node, // content shown if the list is empty
|
|
|
|
headerItems: PropTypes.node, // content shown in the sublist header
|
|
|
|
extraTiles: PropTypes.arrayOf(PropTypes.node), // extra elements added beneath tiles
|
2018-04-11 22:58:04 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
getInitialState: function() {
|
|
|
|
return {
|
|
|
|
hidden: this.props.startAsHidden || false,
|
|
|
|
sortedList: [],
|
|
|
|
};
|
|
|
|
},
|
|
|
|
|
|
|
|
getDefaultProps: function() {
|
|
|
|
return {
|
2018-06-25 08:51:23 +00:00
|
|
|
onHeaderClick: function() {
|
|
|
|
}, // NOP
|
2018-04-11 22:58:04 +00:00
|
|
|
extraTiles: [],
|
|
|
|
isInvite: false,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
|
|
|
|
componentWillMount: function() {
|
|
|
|
this.setState({
|
|
|
|
sortedList: this.applySearchFilter(this.props.list, this.props.searchFilter),
|
|
|
|
});
|
|
|
|
this.dispatcherRef = dis.register(this.onAction);
|
|
|
|
},
|
|
|
|
|
|
|
|
componentWillUnmount: function() {
|
|
|
|
dis.unregister(this.dispatcherRef);
|
|
|
|
},
|
|
|
|
|
|
|
|
componentWillReceiveProps: function(newProps) {
|
|
|
|
// order the room list appropriately before we re-render
|
|
|
|
//if (debug) console.log("received new props, list = " + newProps.list);
|
|
|
|
this.setState({
|
|
|
|
sortedList: this.applySearchFilter(newProps.list, newProps.searchFilter),
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
applySearchFilter: function(list, filter) {
|
|
|
|
if (filter === "") return list;
|
2018-06-13 08:51:35 +00:00
|
|
|
const lcFilter = filter.toLowerCase();
|
2018-06-14 10:16:57 +00:00
|
|
|
// case insensitive if room name includes filter,
|
|
|
|
// or if starts with `#` and one of room's aliases starts with filter
|
|
|
|
return list.filter((room) => (room.name && room.name.toLowerCase().includes(lcFilter)) ||
|
|
|
|
(filter[0] === '#' && room.getAliases().some((alias) => alias.toLowerCase().startsWith(lcFilter))));
|
2018-04-11 22:58:04 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
// The header is collapsable if it is hidden or not stuck
|
|
|
|
// The dataset elements are added in the RoomList _initAndPositionStickyHeaders method
|
|
|
|
isCollapsableOnClick: function() {
|
2018-06-25 08:51:23 +00:00
|
|
|
const stuck = this.refs.header.dataset.stuck;
|
2018-04-11 22:58:04 +00:00
|
|
|
if (this.state.hidden || stuck === undefined || stuck === "none") {
|
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
onAction: function(payload) {
|
|
|
|
// XXX: Previously RoomList would forceUpdate whenever on_room_read is dispatched,
|
|
|
|
// but this is no longer true, so we must do it here (and can apply the small
|
|
|
|
// optimisation of checking that we care about the room being read).
|
|
|
|
//
|
|
|
|
// Ultimately we need to transition to a state pushing flow where something
|
|
|
|
// explicitly notifies the components concerned that the notif count for a room
|
|
|
|
// has change (e.g. a Flux store).
|
|
|
|
if (payload.action === 'on_room_read' &&
|
|
|
|
this.props.list.some((r) => r.roomId === payload.roomId)
|
|
|
|
) {
|
|
|
|
this.forceUpdate();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
onClick: function(ev) {
|
|
|
|
if (this.isCollapsableOnClick()) {
|
|
|
|
// The header isCollapsable, so the click is to be interpreted as collapse and truncation logic
|
2018-06-25 08:51:23 +00:00
|
|
|
const isHidden = !this.state.hidden;
|
|
|
|
this.setState({hidden: isHidden});
|
2018-04-11 22:58:04 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
onRoomTileClick(roomId, ev) {
|
|
|
|
dis.dispatch({
|
|
|
|
action: 'view_room',
|
|
|
|
room_id: roomId,
|
2018-06-25 08:51:23 +00:00
|
|
|
clear_search: (ev && (ev.keyCode === KeyCode.ENTER || ev.keyCode === KeyCode.SPACE)),
|
2018-04-11 22:58:04 +00:00
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
_shouldShowNotifBadge: function(roomNotifState) {
|
|
|
|
const showBadgeInStates = [RoomNotifs.ALL_MESSAGES, RoomNotifs.ALL_MESSAGES_LOUD];
|
|
|
|
return showBadgeInStates.indexOf(roomNotifState) > -1;
|
|
|
|
},
|
|
|
|
|
|
|
|
_shouldShowMentionBadge: function(roomNotifState) {
|
2018-06-25 08:51:23 +00:00
|
|
|
return roomNotifState !== RoomNotifs.MUTE;
|
2018-04-11 22:58:04 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Total up all the notification counts from the rooms
|
|
|
|
*
|
|
|
|
* @returns {Array} The array takes the form [total, highlight] where highlight is a bool
|
|
|
|
*/
|
2018-10-17 12:45:36 +00:00
|
|
|
roomNotificationCount: function() {
|
2018-06-25 08:51:23 +00:00
|
|
|
const self = this;
|
2018-04-11 22:58:04 +00:00
|
|
|
|
|
|
|
if (this.props.isInvite) {
|
|
|
|
return [0, true];
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.props.list.reduce(function(result, room, index) {
|
2018-10-17 12:45:36 +00:00
|
|
|
const roomNotifState = RoomNotifs.getRoomNotifsState(room.roomId);
|
|
|
|
const highlight = room.getUnreadNotificationCount('highlight') > 0;
|
|
|
|
const 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;
|
2018-04-11 22:58:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
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);
|
|
|
|
},
|
|
|
|
|
|
|
|
makeRoomTiles: function() {
|
|
|
|
const RoomTile = sdk.getComponent("rooms.RoomTile");
|
|
|
|
return this.state.sortedList.map((room, index) => {
|
2018-10-17 14:02:35 +00:00
|
|
|
return <RoomTile
|
2018-04-11 22:58:04 +00:00
|
|
|
room={room}
|
|
|
|
roomSubList={this}
|
|
|
|
tagName={this.props.tagName}
|
|
|
|
key={room.roomId}
|
|
|
|
collapsed={this.props.collapsed || false}
|
|
|
|
unread={Unread.doesRoomHaveUnreadMessages(room)}
|
|
|
|
highlight={room.getUnreadNotificationCount('highlight') > 0 || this.props.isInvite}
|
|
|
|
isInvite={this.props.isInvite}
|
|
|
|
refreshSubList={this._updateSubListCount}
|
|
|
|
incomingCall={null}
|
|
|
|
onClick={this.onRoomTileClick}
|
|
|
|
/>;
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2018-06-27 08:16:37 +00:00
|
|
|
_onNotifBadgeClick: function(e) {
|
|
|
|
// prevent the roomsublist collapsing
|
|
|
|
e.preventDefault();
|
|
|
|
e.stopPropagation();
|
|
|
|
// find first room which has notifications and switch to it
|
|
|
|
for (const room of this.state.sortedList) {
|
|
|
|
const roomNotifState = RoomNotifs.getRoomNotifsState(room.roomId);
|
|
|
|
const highlight = room.getUnreadNotificationCount('highlight') > 0;
|
|
|
|
const notificationCount = room.getUnreadNotificationCount();
|
|
|
|
|
|
|
|
const notifBadges = notificationCount > 0 && this._shouldShowNotifBadge(roomNotifState);
|
|
|
|
const mentionBadges = highlight && this._shouldShowMentionBadge(roomNotifState);
|
|
|
|
|
|
|
|
if (notifBadges || mentionBadges) {
|
|
|
|
dis.dispatch({
|
|
|
|
action: 'view_room',
|
|
|
|
room_id: room.roomId,
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2018-06-27 08:49:58 +00:00
|
|
|
_onInviteBadgeClick: function(e) {
|
|
|
|
// prevent the roomsublist collapsing
|
|
|
|
e.preventDefault();
|
|
|
|
e.stopPropagation();
|
|
|
|
// switch to first room in sortedList as that'll be the top of the list for the user
|
|
|
|
if (this.state.sortedList && this.state.sortedList.length > 0) {
|
|
|
|
dis.dispatch({
|
|
|
|
action: 'view_room',
|
|
|
|
room_id: this.state.sortedList[0].roomId,
|
|
|
|
});
|
|
|
|
} else if (this.props.extraTiles && this.props.extraTiles.length > 0) {
|
|
|
|
// Group Invites are different in that they are all extra tiles and not rooms
|
|
|
|
// XXX: this is a horrible special case because Group Invite sublist is a hack
|
|
|
|
if (this.props.extraTiles[0].props && this.props.extraTiles[0].props.group instanceof Group) {
|
|
|
|
dis.dispatch({
|
|
|
|
action: 'view_group',
|
|
|
|
group_id: this.props.extraTiles[0].props.group.groupId,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
2018-04-11 22:58:04 +00:00
|
|
|
|
|
|
|
_getHeaderJsx: function() {
|
2018-10-19 10:07:36 +00:00
|
|
|
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
2018-06-25 08:51:23 +00:00
|
|
|
const subListNotifications = this.roomNotificationCount();
|
|
|
|
const subListNotifCount = subListNotifications[0];
|
|
|
|
const subListNotifHighlight = subListNotifications[1];
|
2018-04-11 22:58:04 +00:00
|
|
|
|
2018-06-25 08:51:23 +00:00
|
|
|
const chevronClasses = classNames({
|
2018-04-11 22:58:04 +00:00
|
|
|
'mx_RoomSubList_chevron': true,
|
|
|
|
'mx_RoomSubList_chevronRight': this.state.hidden,
|
|
|
|
'mx_RoomSubList_chevronDown': !this.state.hidden,
|
|
|
|
});
|
|
|
|
|
2018-06-25 08:51:23 +00:00
|
|
|
const badgeClasses = classNames({
|
2018-04-11 22:58:04 +00:00
|
|
|
'mx_RoomSubList_badge': true,
|
|
|
|
'mx_RoomSubList_badgeHighlight': subListNotifHighlight,
|
|
|
|
});
|
|
|
|
|
2018-06-25 08:51:23 +00:00
|
|
|
let badge;
|
2018-04-11 22:58:04 +00:00
|
|
|
if (subListNotifCount > 0) {
|
2018-06-27 08:16:37 +00:00
|
|
|
badge = <div className={badgeClasses} onClick={this._onNotifBadgeClick}>
|
|
|
|
{ FormattingUtils.formatCount(subListNotifCount) }
|
|
|
|
</div>;
|
2018-04-11 22:58:04 +00:00
|
|
|
} else if (this.props.isInvite) {
|
|
|
|
// no notifications but highlight anyway because this is an invite badge
|
2018-06-27 08:49:58 +00:00
|
|
|
badge = <div className={badgeClasses} onClick={this._onInviteBadgeClick}>!</div>;
|
2018-04-11 22:58:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// When collapsed, allow a long hover on the header to show user
|
|
|
|
// the full tag name and room count
|
2018-06-25 08:51:23 +00:00
|
|
|
let title;
|
2018-04-11 22:58:04 +00:00
|
|
|
if (this.props.collapsed) {
|
|
|
|
title = this.props.label;
|
|
|
|
}
|
|
|
|
|
2018-06-25 08:51:23 +00:00
|
|
|
let incomingCall;
|
2018-04-11 22:58:04 +00:00
|
|
|
if (this.props.incomingCall) {
|
2018-06-25 08:51:23 +00:00
|
|
|
const self = this;
|
2018-04-11 22:58:04 +00:00
|
|
|
// Check if the incoming call is for this section
|
2018-06-25 08:51:23 +00:00
|
|
|
const incomingCallRoom = this.props.list.filter(function(room) {
|
2018-04-11 22:58:04 +00:00
|
|
|
return self.props.incomingCall.roomId === room.roomId;
|
|
|
|
});
|
|
|
|
|
|
|
|
if (incomingCallRoom.length === 1) {
|
2018-06-25 08:51:23 +00:00
|
|
|
const IncomingCallBox = sdk.getComponent("voip.IncomingCallBox");
|
|
|
|
incomingCall =
|
|
|
|
<IncomingCallBox className="mx_RoomSubList_incomingCall" incomingCall={this.props.incomingCall} />;
|
2018-04-11 22:58:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-19 10:07:36 +00:00
|
|
|
let addRoomButton;
|
|
|
|
if (this.props.onAddRoom) {
|
2018-10-19 10:34:55 +00:00
|
|
|
addRoomButton = (
|
|
|
|
<AccessibleButton onClick={ this.props.onAddRoom } className="mx_RoomSubList_addRoom">
|
|
|
|
+
|
|
|
|
</AccessibleButton>
|
|
|
|
);
|
2018-10-19 10:07:36 +00:00
|
|
|
}
|
|
|
|
|
2018-06-25 08:51:23 +00:00
|
|
|
const tabindex = this.props.searchFilter === "" ? "0" : "-1";
|
2018-04-11 22:58:04 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="mx_RoomSubList_labelContainer" title={ title } ref="header">
|
|
|
|
<AccessibleButton onClick={ this.onClick } className="mx_RoomSubList_label" tabIndex={tabindex}>
|
|
|
|
<div className={chevronClasses}></div>
|
2018-07-30 16:00:11 +00:00
|
|
|
{ this.props.collapsed ? '' : this.props.label }
|
2018-04-11 22:58:04 +00:00
|
|
|
{ badge }
|
|
|
|
{ incomingCall }
|
|
|
|
</AccessibleButton>
|
2018-10-19 10:07:36 +00:00
|
|
|
{ addRoomButton }
|
2018-04-11 22:58:04 +00:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
},
|
|
|
|
|
|
|
|
render: function() {
|
|
|
|
let content;
|
2018-06-30 16:06:33 +00:00
|
|
|
|
|
|
|
if (this.props.showEmpty) {
|
|
|
|
// this is new behaviour with still controversial UX in that in hiding RoomSubLists the drop zones for DnD
|
|
|
|
// are also gone so when filtering users can't DnD rooms to some tags but is a lot cleaner otherwise.
|
|
|
|
if (this.state.sortedList.length === 0 && !this.props.searchFilter && this.props.extraTiles.length === 0) {
|
2018-06-25 08:41:28 +00:00
|
|
|
content = this.props.emptyContent;
|
|
|
|
} else {
|
2018-06-30 16:06:33 +00:00
|
|
|
content = this.makeRoomTiles();
|
|
|
|
content.push(...this.props.extraTiles);
|
2018-06-25 08:41:28 +00:00
|
|
|
}
|
2018-04-11 22:58:04 +00:00
|
|
|
} else {
|
2018-06-30 16:06:33 +00:00
|
|
|
if (this.state.sortedList.length === 0 && this.props.extraTiles.length === 0) {
|
|
|
|
// if no search filter is applied and there is a placeholder defined then show it, otherwise show nothing
|
|
|
|
if (!this.props.searchFilter && this.props.emptyContent) {
|
|
|
|
content = this.props.emptyContent;
|
|
|
|
} else {
|
|
|
|
// don't show an empty sublist
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
content = this.makeRoomTiles();
|
|
|
|
content.push(...this.props.extraTiles);
|
|
|
|
}
|
2018-04-11 22:58:04 +00:00
|
|
|
}
|
|
|
|
|
2018-10-18 14:25:22 +00:00
|
|
|
const len = this.state.sortedList.length + this.props.extraTiles.length;
|
|
|
|
|
|
|
|
if (len) {
|
|
|
|
if (this.state.hidden) {
|
|
|
|
return <div className={["mx_RoomSubList", "mx_RoomSubList_hidden"]}>
|
|
|
|
{this._getHeaderJsx()}
|
|
|
|
</div>;
|
|
|
|
} else {
|
2018-10-18 14:57:47 +00:00
|
|
|
const heightEstimation = (len * 40) + 31;
|
2018-10-18 14:58:23 +00:00
|
|
|
const style = {
|
|
|
|
flexBasis: `${heightEstimation}px`,
|
|
|
|
maxHeight: `${heightEstimation}px`,
|
|
|
|
};
|
2018-10-18 14:25:22 +00:00
|
|
|
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
2018-10-18 14:58:23 +00:00
|
|
|
return <div className={"mx_RoomSubList"} style={style}>
|
2018-10-18 14:25:22 +00:00
|
|
|
{this._getHeaderJsx()}
|
|
|
|
<GeminiScrollbarWrapper>
|
|
|
|
{ content }
|
|
|
|
</GeminiScrollbarWrapper>
|
|
|
|
</div>;
|
|
|
|
}
|
2018-06-25 08:51:23 +00:00
|
|
|
} else {
|
|
|
|
const Loader = sdk.getComponent("elements.Spinner");
|
2018-06-26 06:54:38 +00:00
|
|
|
if (this.props.showSpinner) {
|
2018-06-26 06:46:33 +00:00
|
|
|
content = <Loader />;
|
2018-06-26 06:54:38 +00:00
|
|
|
}
|
2018-06-26 06:46:33 +00:00
|
|
|
|
2018-04-11 22:58:04 +00:00
|
|
|
return (
|
|
|
|
<div className="mx_RoomSubList">
|
2018-06-25 08:51:23 +00:00
|
|
|
{this.props.alwaysShowHeader ? this._getHeaderJsx() : undefined}
|
2018-06-26 06:54:38 +00:00
|
|
|
{ this.state.hidden ? undefined : content }
|
2018-07-30 16:00:11 +00:00
|
|
|
<div className="mx_RoomSubList_resizer"></div>
|
2018-04-11 22:58:04 +00:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
2018-06-25 08:51:23 +00:00
|
|
|
},
|
2018-04-11 22:58:04 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
module.exports = RoomSubList;
|