move non-Riot-specific components over from riot-web
This commit is contained in:
parent
979452628f
commit
8ab8f76254
37 changed files with 6609 additions and 0 deletions
401
src/components/structures/RoomSubList.js
Normal file
401
src/components/structures/RoomSubList.js
Normal file
|
@ -0,0 +1,401 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
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 React = require('react');
|
||||
var ReactDOM = require('react-dom');
|
||||
var classNames = require('classnames');
|
||||
var sdk = require('matrix-react-sdk');
|
||||
import { Droppable } from 'react-beautiful-dnd';
|
||||
import { _t } from 'matrix-react-sdk/lib/languageHandler';
|
||||
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');
|
||||
var FormattingUtils = require('matrix-react-sdk/lib/utils/FormattingUtils');
|
||||
var AccessibleButton = require('matrix-react-sdk/lib/components/views/elements/AccessibleButton');
|
||||
import Modal from 'matrix-react-sdk/lib/Modal';
|
||||
import { KeyCode } from 'matrix-react-sdk/lib/Keyboard';
|
||||
|
||||
|
||||
// turn this on for drop & drag console debugging galore
|
||||
var debug = false;
|
||||
|
||||
const TRUNCATE_AT = 10;
|
||||
|
||||
var RoomSubList = React.createClass({
|
||||
displayName: 'RoomSubList',
|
||||
|
||||
debug: debug,
|
||||
|
||||
propTypes: {
|
||||
list: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
|
||||
label: React.PropTypes.string.isRequired,
|
||||
tagName: React.PropTypes.string,
|
||||
editable: React.PropTypes.bool,
|
||||
|
||||
order: React.PropTypes.string.isRequired,
|
||||
|
||||
// passed through to RoomTile and used to highlight room with `!` regardless of notifications count
|
||||
isInvite: React.PropTypes.bool,
|
||||
|
||||
startAsHidden: React.PropTypes.bool,
|
||||
showSpinner: React.PropTypes.bool, // true to show a spinner if 0 elements when expanded
|
||||
collapsed: React.PropTypes.bool.isRequired, // is LeftPanel collapsed?
|
||||
onHeaderClick: React.PropTypes.func,
|
||||
alwaysShowHeader: React.PropTypes.bool,
|
||||
incomingCall: React.PropTypes.object,
|
||||
onShowMoreRooms: React.PropTypes.func,
|
||||
searchFilter: React.PropTypes.string,
|
||||
emptyContent: React.PropTypes.node, // content shown if the list is empty
|
||||
headerItems: React.PropTypes.node, // content shown in the sublist header
|
||||
extraTiles: React.PropTypes.arrayOf(React.PropTypes.node), // extra elements added beneath tiles
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
hidden: this.props.startAsHidden || false,
|
||||
truncateAt: TRUNCATE_AT,
|
||||
sortedList: [],
|
||||
};
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
onHeaderClick: function() {}, // NOP
|
||||
onShowMoreRooms: function() {}, // NOP
|
||||
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;
|
||||
return list.filter((room) => {
|
||||
return room.name && room.name.toLowerCase().indexOf(filter.toLowerCase()) >= 0
|
||||
});
|
||||
},
|
||||
|
||||
// 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;
|
||||
}
|
||||
},
|
||||
|
||||
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
|
||||
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 : 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);
|
||||
}
|
||||
},
|
||||
|
||||
onRoomTileClick(roomId, ev) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: roomId,
|
||||
clear_search: (ev && (ev.keyCode == KeyCode.ENTER || ev.keyCode == KeyCode.SPACE)),
|
||||
});
|
||||
},
|
||||
|
||||
_shouldShowNotifBadge: function(roomNotifState) {
|
||||
const showBadgeInStates = [RoomNotifs.ALL_MESSAGES, RoomNotifs.ALL_MESSAGES_LOUD];
|
||||
return showBadgeInStates.indexOf(roomNotifState) > -1;
|
||||
},
|
||||
|
||||
_shouldShowMentionBadge: function(roomNotifState) {
|
||||
return roomNotifState != RoomNotifs.MUTE;
|
||||
},
|
||||
|
||||
/**
|
||||
* Total up all the notification counts from the rooms
|
||||
*
|
||||
* @param {Number} If supplied will only total notifications for rooms outside the truncation number
|
||||
* @returns {Array} The array takes the form [total, highlight] where highlight is a bool
|
||||
*/
|
||||
roomNotificationCount: function(truncateAt) {
|
||||
var self = this;
|
||||
|
||||
if (this.props.isInvite) {
|
||||
return [0, true];
|
||||
}
|
||||
|
||||
return this.props.list.reduce(function(result, room, index) {
|
||||
if (truncateAt === undefined || index >= truncateAt) {
|
||||
var roomNotifState = RoomNotifs.getRoomNotifsState(room.roomId);
|
||||
var highlight = room.getUnreadNotificationCount('highlight') > 0;
|
||||
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);
|
||||
},
|
||||
|
||||
makeRoomTiles: function() {
|
||||
const DNDRoomTile = sdk.getComponent("rooms.DNDRoomTile");
|
||||
const RoomTile = sdk.getComponent("rooms.RoomTile");
|
||||
return this.state.sortedList.map((room, index) => {
|
||||
// XXX: is it evil to pass in this as a prop to RoomTile? Yes.
|
||||
|
||||
// We should only use <DNDRoomTile /> when editable
|
||||
const RoomTileComponent = this.props.editable ? DNDRoomTile : RoomTile;
|
||||
return <RoomTileComponent
|
||||
index={index} // For DND
|
||||
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}
|
||||
/>;
|
||||
});
|
||||
},
|
||||
|
||||
_getHeaderJsx: function() {
|
||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
var subListNotifications = this.roomNotificationCount();
|
||||
var subListNotifCount = subListNotifications[0];
|
||||
var subListNotifHighlight = subListNotifications[1];
|
||||
|
||||
var totalTiles = this.props.list.length + (this.props.extraTiles || []).length;
|
||||
var roomCount = totalTiles > 0 ? totalTiles : '';
|
||||
|
||||
var chevronClasses = classNames({
|
||||
'mx_RoomSubList_chevron': true,
|
||||
'mx_RoomSubList_chevronRight': this.state.hidden,
|
||||
'mx_RoomSubList_chevronDown': !this.state.hidden,
|
||||
});
|
||||
|
||||
var badgeClasses = classNames({
|
||||
'mx_RoomSubList_badge': true,
|
||||
'mx_RoomSubList_badgeHighlight': subListNotifHighlight,
|
||||
});
|
||||
|
||||
var badge;
|
||||
if (subListNotifCount > 0) {
|
||||
badge = <div className={badgeClasses}>{ FormattingUtils.formatCount(subListNotifCount) }</div>;
|
||||
} else if (this.props.isInvite) {
|
||||
// no notifications but highlight anyway because this is an invite badge
|
||||
badge = <div className={badgeClasses}>!</div>;
|
||||
}
|
||||
|
||||
// When collapsed, allow a long hover on the header to show user
|
||||
// the full tag name and room count
|
||||
var title;
|
||||
if (this.props.collapsed) {
|
||||
title = this.props.label;
|
||||
if (roomCount !== '') {
|
||||
title += " [" + roomCount + "]";
|
||||
}
|
||||
}
|
||||
|
||||
var incomingCall;
|
||||
if (this.props.incomingCall) {
|
||||
var self = this;
|
||||
// Check if the incoming call is for this section
|
||||
var incomingCallRoom = this.props.list.filter(function(room) {
|
||||
return self.props.incomingCall.roomId === room.roomId;
|
||||
});
|
||||
|
||||
if (incomingCallRoom.length === 1) {
|
||||
var IncomingCallBox = sdk.getComponent("voip.IncomingCallBox");
|
||||
incomingCall = <IncomingCallBox className="mx_RoomSubList_incomingCall" incomingCall={ this.props.incomingCall }/>;
|
||||
}
|
||||
}
|
||||
|
||||
var tabindex = this.props.searchFilter === "" ? "0" : "-1";
|
||||
|
||||
return (
|
||||
<div className="mx_RoomSubList_labelContainer" title={ title } ref="header">
|
||||
<AccessibleButton onClick={ this.onClick } className="mx_RoomSubList_label" tabIndex={tabindex}>
|
||||
{ this.props.collapsed ? '' : this.props.label }
|
||||
<div className="mx_RoomSubList_roomCount">{ roomCount }</div>
|
||||
<div className={chevronClasses}></div>
|
||||
{ badge }
|
||||
{ incomingCall }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
_createOverflowTile: function(overflowCount, totalCount) {
|
||||
var content = <div className="mx_RoomSubList_chevronDown"></div>;
|
||||
|
||||
var overflowNotifications = this.roomNotificationCount(TRUNCATE_AT);
|
||||
var overflowNotifCount = overflowNotifications[0];
|
||||
var overflowNotifHighlight = overflowNotifications[1];
|
||||
if (overflowNotifCount && !this.props.collapsed) {
|
||||
content = FormattingUtils.formatCount(overflowNotifCount);
|
||||
}
|
||||
|
||||
var badgeClasses = classNames({
|
||||
'mx_RoomSubList_moreBadge': true,
|
||||
'mx_RoomSubList_moreBadgeNotify': overflowNotifCount && !this.props.collapsed,
|
||||
'mx_RoomSubList_moreBadgeHighlight': overflowNotifHighlight && !this.props.collapsed,
|
||||
});
|
||||
|
||||
return (
|
||||
<AccessibleButton className="mx_RoomSubList_ellipsis" onClick={this._showFullMemberList}>
|
||||
<div className="mx_RoomSubList_line"></div>
|
||||
<div className="mx_RoomSubList_more">{ _t("more") }</div>
|
||||
<div className={ badgeClasses }>{ content }</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
},
|
||||
|
||||
_showFullMemberList: function() {
|
||||
this.setState({
|
||||
truncateAt: -1
|
||||
});
|
||||
|
||||
this.props.onShowMoreRooms();
|
||||
this.props.onHeaderClick(false);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var connectDropTarget = this.props.connectDropTarget;
|
||||
var TruncatedList = sdk.getComponent('elements.TruncatedList');
|
||||
|
||||
var label = this.props.collapsed ? null : this.props.label;
|
||||
|
||||
let content;
|
||||
if (this.state.sortedList.length === 0 && !this.props.searchFilter && this.props.extraTiles.length === 0) {
|
||||
content = this.props.emptyContent;
|
||||
} else {
|
||||
content = this.makeRoomTiles();
|
||||
content.push(...this.props.extraTiles);
|
||||
}
|
||||
|
||||
if (this.state.sortedList.length > 0 || this.props.extraTiles.length > 0 || this.props.editable) {
|
||||
var subList;
|
||||
var classes = "mx_RoomSubList";
|
||||
|
||||
if (!this.state.hidden) {
|
||||
subList = <TruncatedList className={ classes } truncateAt={this.state.truncateAt}
|
||||
createOverflowElement={this._createOverflowTile} >
|
||||
{ content }
|
||||
</TruncatedList>;
|
||||
}
|
||||
else {
|
||||
subList = <TruncatedList className={ classes }>
|
||||
</TruncatedList>;
|
||||
}
|
||||
|
||||
const subListContent = <div>
|
||||
{ this._getHeaderJsx() }
|
||||
{ subList }
|
||||
</div>;
|
||||
|
||||
return this.props.editable ?
|
||||
<Droppable
|
||||
droppableId={"room-sub-list-droppable_" + this.props.tagName}
|
||||
type="draggable-RoomTile"
|
||||
>
|
||||
{ (provided, snapshot) => (
|
||||
<div ref={provided.innerRef}>
|
||||
{ subListContent }
|
||||
</div>
|
||||
) }
|
||||
</Droppable> : subListContent;
|
||||
}
|
||||
else {
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
return (
|
||||
<div className="mx_RoomSubList">
|
||||
{ this.props.alwaysShowHeader ? this._getHeaderJsx() : undefined }
|
||||
{ (this.props.showSpinner && !this.state.hidden) ? <Loader /> : undefined }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = RoomSubList;
|
Loading…
Add table
Add a link
Reference in a new issue