Merge pull request #2229 from matrix-org/bwindels/roomsectionheadercleanup

Redesign: room section header tidbits
This commit is contained in:
David Baker 2018-10-22 14:49:53 +01:00 committed by GitHub
commit 0992408930
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 72 additions and 180 deletions

View file

@ -15,25 +15,21 @@ limitations under the License.
*/ */
.mx_RoomSubList { .mx_RoomSubList {
min-height: 80px; min-height: 31px;
flex: 0; flex: 0 0 auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.mx_RoomSubList_hidden { .mx_RoomSubList_nonEmpty {
min-height: unset; min-height: 80px;
} flex: 1;
.mx_RoomSubList_resizer {
width: 100%;
height: 3px;
background-color: $roomsublist-background;
} }
.mx_RoomSubList_labelContainer { .mx_RoomSubList_labelContainer {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex: 0 0 auto;
} }
.mx_RoomSubList_label { .mx_RoomSubList_label {

View file

@ -23,6 +23,11 @@ limitations under the License.
flex-direction: column; flex-direction: column;
} }
/* hide resize handles next to collapsed / empty sublists */
.mx_RoomList .mx_RoomSubList:not(.mx_RoomSubList_nonEmpty) + .mx_ResizeHandle {
display: none;
}
.mx_RoomList_expandButton { .mx_RoomList_expandButton {
margin-left: 8px; margin-left: 8px;
cursor: pointer; cursor: pointer;

View file

@ -50,14 +50,10 @@ const RoomSubList = React.createClass({
showSpinner: PropTypes.bool, // true to show a spinner if 0 elements when expanded showSpinner: PropTypes.bool, // true to show a spinner if 0 elements when expanded
collapsed: PropTypes.bool.isRequired, // is LeftPanel collapsed? collapsed: PropTypes.bool.isRequired, // is LeftPanel collapsed?
onHeaderClick: PropTypes.func, onHeaderClick: PropTypes.func,
alwaysShowHeader: PropTypes.bool,
incomingCall: PropTypes.object, incomingCall: PropTypes.object,
onShowMoreRooms: PropTypes.func,
searchFilter: PropTypes.string, searchFilter: PropTypes.string,
emptyContent: PropTypes.node, // content shown if the list is empty
headerItems: PropTypes.node, // content shown in the sublist header headerItems: PropTypes.node, // content shown in the sublist header
extraTiles: PropTypes.arrayOf(PropTypes.node), // extra elements added beneath tiles extraTiles: PropTypes.arrayOf(PropTypes.node), // extra elements added beneath tiles
showEmpty: PropTypes.bool,
}, },
getInitialState: function() { getInitialState: function() {
@ -71,11 +67,8 @@ const RoomSubList = React.createClass({
return { return {
onHeaderClick: function() { onHeaderClick: function() {
}, // NOP }, // NOP
onShowMoreRooms: function() {
}, // NOP
extraTiles: [], extraTiles: [],
isInvite: false, isInvite: false,
showEmpty: true,
}; };
}, },
@ -138,7 +131,6 @@ const RoomSubList = React.createClass({
// The header isCollapsable, so the click is to be interpreted as collapse and truncation logic // The header isCollapsable, so the click is to be interpreted as collapse and truncation logic
const isHidden = !this.state.hidden; const isHidden = !this.state.hidden;
this.setState({hidden: isHidden}); this.setState({hidden: isHidden});
this.props.onShowMoreRooms();
this.props.onHeaderClick(isHidden); this.props.onHeaderClick(isHidden);
} else { } else {
// The header is stuck, so the click is to be interpreted as a scroll to the header // The header is stuck, so the click is to be interpreted as a scroll to the header
@ -271,25 +263,21 @@ const RoomSubList = React.createClass({
const subListNotifCount = subListNotifications[0]; const subListNotifCount = subListNotifications[0];
const subListNotifHighlight = subListNotifications[1]; const subListNotifHighlight = subListNotifications[1];
const chevronClasses = classNames({
'mx_RoomSubList_chevron': true,
'mx_RoomSubList_chevronRight': this.state.hidden,
'mx_RoomSubList_chevronDown': !this.state.hidden,
});
const badgeClasses = classNames({
'mx_RoomSubList_badge': true,
'mx_RoomSubList_badgeHighlight': subListNotifHighlight,
});
let badge; let badge;
if (subListNotifCount > 0) { if (this.state.hidden) {
badge = <div className={badgeClasses} onClick={this._onNotifBadgeClick}> const badgeClasses = classNames({
{ FormattingUtils.formatCount(subListNotifCount) } 'mx_RoomSubList_badge': true,
</div>; 'mx_RoomSubList_badgeHighlight': subListNotifHighlight,
} else if (this.props.isInvite) { });
// no notifications but highlight anyway because this is an invite badge if (subListNotifCount > 0) {
badge = <div className={badgeClasses} onClick={this._onInviteBadgeClick}>!</div>; badge = <div className={badgeClasses} onClick={this._onNotifBadgeClick}>
{ FormattingUtils.formatCount(subListNotifCount) }
</div>;
} else if (this.props.isInvite) {
// no notifications but highlight anyway because this is an invite badge
badge = <div className={badgeClasses} onClick={this._onInviteBadgeClick}>!</div>;
}
} }
// When collapsed, allow a long hover on the header to show user // When collapsed, allow a long hover on the header to show user
@ -323,12 +311,22 @@ const RoomSubList = React.createClass({
); );
} }
const tabindex = this.props.searchFilter === "" ? "0" : "-1"; const len = this.state.sortedList.length + this.props.extraTiles.length;
let chevron;
if (len) {
const chevronClasses = classNames({
'mx_RoomSubList_chevron': true,
'mx_RoomSubList_chevronRight': this.state.hidden,
'mx_RoomSubList_chevronDown': !this.state.hidden,
});
chevron = (<div className={chevronClasses}></div>);
}
const tabindex = this.props.searchFilter === "" ? "0" : "-1";
return ( return (
<div className="mx_RoomSubList_labelContainer" title={ title } ref="header"> <div className="mx_RoomSubList_labelContainer" title={ title } ref="header">
<AccessibleButton onClick={ this.onClick } className="mx_RoomSubList_label" tabIndex={tabindex}> <AccessibleButton onClick={ this.onClick } className="mx_RoomSubList_label" tabIndex={tabindex}>
<div className={chevronClasses}></div> { chevron }
{ this.props.collapsed ? '' : this.props.label } { this.props.collapsed ? '' : this.props.label }
{ badge } { badge }
{ incomingCall } { incomingCall }
@ -339,64 +337,43 @@ const RoomSubList = React.createClass({
}, },
render: function() { render: function() {
let content;
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) {
content = this.props.emptyContent;
} else {
content = this.makeRoomTiles();
content.push(...this.props.extraTiles);
}
} else {
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);
}
}
const len = this.state.sortedList.length + this.props.extraTiles.length; const len = this.state.sortedList.length + this.props.extraTiles.length;
if (len) { if (len) {
const subListClasses = classNames({
"mx_RoomSubList": true,
"mx_RoomSubList_nonEmpty": len && !this.state.hidden,
});
if (this.state.hidden) { if (this.state.hidden) {
return <div className={["mx_RoomSubList", "mx_RoomSubList_hidden"]}> return <div className={subListClasses}>
{this._getHeaderJsx()} {this._getHeaderJsx()}
</div>; </div>;
} else { } else {
const heightEstimation = (len * 40) + 31; const heightEstimation = (len * 40) + 31;
const style = { const style = {
flexBasis: `${heightEstimation}px`, flexGrow: `${heightEstimation}`,
maxHeight: `${heightEstimation}px`, maxHeight: `${heightEstimation}px`,
}; };
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
return <div className={"mx_RoomSubList"} style={style}> const tiles = this.makeRoomTiles();
tiles.push(...this.props.extraTiles);
return <div className={subListClasses} style={style}>
{this._getHeaderJsx()} {this._getHeaderJsx()}
<GeminiScrollbarWrapper> <GeminiScrollbarWrapper>
{ content } { tiles }
</GeminiScrollbarWrapper> </GeminiScrollbarWrapper>
</div>; </div>;
} }
} else { } else {
const Loader = sdk.getComponent("elements.Spinner"); const Loader = sdk.getComponent("elements.Spinner");
if (this.props.showSpinner) { let content;
if (this.props.showSpinner && !this.state.hidden) {
content = <Loader />; content = <Loader />;
} }
return ( return (
<div className="mx_RoomSubList"> <div className="mx_RoomSubList">
{this.props.alwaysShowHeader ? this._getHeaderJsx() : undefined} { this._getHeaderJsx() }
{ this.state.hidden ? undefined : content } { content }
<div className="mx_RoomSubList_resizer"></div>
</div> </div>
); );
} }

View file

@ -406,71 +406,6 @@ module.exports = React.createClass({
} }
}, },
onShowMoreRooms: function() {
// kick gemini in the balls to get it to wake up
// XXX: uuuuuuugh.
if (!this._gemScroll) return;
this._gemScroll.forceUpdate();
},
_getEmptyContent: function(section) {
if (this.state.selectedTags.length > 0) {
return null;
}
const RoomDropTarget = sdk.getComponent('rooms.RoomDropTarget');
if (this.props.collapsed) {
return <RoomDropTarget label="" />;
}
const StartChatButton = sdk.getComponent('elements.StartChatButton');
const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton');
const CreateRoomButton = sdk.getComponent('elements.CreateRoomButton');
let tip = null;
switch (section) {
case 'im.vector.fake.direct':
tip = <div className="mx_RoomList_emptySubListTip">
{ _t(
"Press <StartChatButton> to start a chat with someone",
{},
{ 'StartChatButton': <StartChatButton size="16" callout={true} /> },
) }
</div>;
break;
case 'im.vector.fake.recent':
tip = <div className="mx_RoomList_emptySubListTip">
{ _t(
"You're not in any rooms yet! Press <CreateRoomButton> to make a room or"+
" <RoomDirectoryButton> to browse the directory",
{},
{
'CreateRoomButton': <CreateRoomButton size="16" callout={true} />,
'RoomDirectoryButton': <RoomDirectoryButton size="16" callout={true} />,
},
) }
</div>;
break;
}
if (tip) {
return <div className="mx_RoomList_emptySubListTip_container">
{ tip }
</div>;
}
// We don't want to display drop targets if there are no room tiles to drag'n'drop
if (this.state.totalRoomCount === 0) {
return null;
}
const labelText = phraseForSection(section);
return <RoomDropTarget label={labelText} />;
},
_getHeaderItems: function(section) { _getHeaderItems: function(section) {
const StartChatButton = sdk.getComponent('elements.StartChatButton'); const StartChatButton = sdk.getComponent('elements.StartChatButton');
const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton'); const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton');
@ -508,28 +443,21 @@ module.exports = React.createClass({
render: function() { render: function() {
const RoomSubList = sdk.getComponent('structures.RoomSubList'); const RoomSubList = sdk.getComponent('structures.RoomSubList');
// XXX: we can't detect device-level (localStorage) settings onChange as the SettingsStore does not notify const mapProps = (subListsProps) => {
// so checking on every render is the sanest thing at this time.
const showEmpty = SettingsStore.getValue('RoomSubList.showEmpty');
const self = this;
function mapProps(subListsProps) {
const defaultProps = { const defaultProps = {
collapsed: self.props.collapsed, collapsed: this.props.collapsed,
searchFilter: self.props.searchFilter, searchFilter: this.props.searchFilter,
onShowMoreRooms: self.onShowMoreRooms, incomingCall: this.state.incomingCall,
showEmpty: showEmpty,
incomingCall: self.state.incomingCall,
}; };
subListsProps = subListsProps.filter((props => {
const len = props.list.length + (props.extraTiles ? props.extraTiles.length : 0);
return len !== 0 || props.onAddRoom;
}));
return subListsProps.reduce((components, props, i) => { return subListsProps.reduce((components, props, i) => {
props = Object.assign({}, defaultProps, props); props = Object.assign({}, defaultProps, props);
const isLast = i === subListsProps.length - 1; const isLast = i === subListsProps.length - 1;
const len = props.list.length + (props.extraTiles ? props.extraTiles.length : 0);
// empty and no add button? dont render
if (!len && !props.onAddRoom) {
return components;
}
const {key, label, ... otherProps} = props; const {key, label, ... otherProps} = props;
const chosenKey = key || label; const chosenKey = key || label;
@ -548,83 +476,69 @@ module.exports = React.createClass({
let subLists = [ let subLists = [
{ {
list: [], list: [],
extraTiles: this._makeGroupInviteTiles(self.props.searchFilter), extraTiles: this._makeGroupInviteTiles(this.props.searchFilter),
label: _t('Community Invites'), label: _t('Community Invites'),
order: "recent", order: "recent",
isInvite: true, isInvite: true,
}, },
{ {
list: self.state.lists['im.vector.fake.invite'], list: this.state.lists['im.vector.fake.invite'],
label: _t('Invites'), label: _t('Invites'),
order: "recent", order: "recent",
isInvite: true, isInvite: true,
}, },
{ {
list: self.state.lists['m.favourite'], list: this.state.lists['m.favourite'],
label: _t('Favourites'), label: _t('Favourites'),
tagName: "m.favourite", tagName: "m.favourite",
emptyContent: this._getEmptyContent('m.favourite'),
order: "manual", order: "manual",
}, },
{ {
list: self.state.lists['im.vector.fake.direct'], list: this.state.lists['im.vector.fake.direct'],
label: _t('People'), label: _t('People'),
tagName: "im.vector.fake.direct", tagName: "im.vector.fake.direct",
emptyContent: this._getEmptyContent('im.vector.fake.direct'),
headerItems: this._getHeaderItems('im.vector.fake.direct'), headerItems: this._getHeaderItems('im.vector.fake.direct'),
order: "recent", order: "recent",
alwaysShowHeader: true,
onAddRoom: () => {dis.dispatch({action: 'view_create_chat'})}, onAddRoom: () => {dis.dispatch({action: 'view_create_chat'})},
}, },
{ {
list: self.state.lists['im.vector.fake.recent'], list: this.state.lists['im.vector.fake.recent'],
label: _t('Rooms'), label: _t('Rooms'),
emptyContent: this._getEmptyContent('im.vector.fake.recent'),
headerItems: this._getHeaderItems('im.vector.fake.recent'), headerItems: this._getHeaderItems('im.vector.fake.recent'),
order: "recent", order: "recent",
onAddRoom: () => {dis.dispatch({action: 'view_create_room'})}, onAddRoom: () => {dis.dispatch({action: 'view_create_room'})},
}, },
]; ];
const tagSubLists = Object.keys(self.state.lists) const tagSubLists = Object.keys(this.state.lists)
.filter((tagName) => { .filter((tagName) => {
return !tagName.match(STANDARD_TAGS_REGEX); return !tagName.match(STANDARD_TAGS_REGEX);
}).map((tagName) => { }).map((tagName) => {
return { return {
list: self.state.lists[tagName], list: this.state.lists[tagName],
key: tagName, key: tagName,
label: labelForTagName(tagName), label: labelForTagName(tagName),
tagName: tagName, tagName: tagName,
emptyContent: this._getEmptyContent(tagName),
order: "manual", order: "manual",
}; };
}); });
subLists = subLists.concat(tagSubLists); subLists = subLists.concat(tagSubLists);
subLists = subLists.concat([ subLists = subLists.concat([
{ {
list: self.state.lists['m.lowpriority'], list: this.state.lists['m.lowpriority'],
label: _t('Low priority'), label: _t('Low priority'),
tagName: "m.lowpriority", tagName: "m.lowpriority",
emptyContent: this._getEmptyContent('m.lowpriority'),
order: "recent", order: "recent",
}, },
{ {
list: self.state.lists['im.vector.fake.archived'], list: this.state.lists['im.vector.fake.archived'],
emptyContent: self.props.collapsed ?
null :
<div className="mx_RoomList_emptySubListTip_container">
<div className="mx_RoomList_emptySubListTip">
{ _t('You have no historical rooms') }
</div>
</div>,
label: _t('Historical'), label: _t('Historical'),
order: "recent", order: "recent",
alwaysShowHeader: true,
startAsHidden: true, startAsHidden: true,
showSpinner: self.state.isLoadingLeftRooms, showSpinner: this.state.isLoadingLeftRooms,
onHeaderClick: self.onArchivedHeaderClick, onHeaderClick: this.onArchivedHeaderClick,
}, },
{ {
list: self.state.lists['m.server_notice'], list: this.state.lists['m.server_notice'],
label: _t('System Alerts'), label: _t('System Alerts'),
tagName: "m.lowpriority", tagName: "m.lowpriority",
order: "recent", order: "recent",