Merge branch 'travis/room-list/css-layout' into travis/room-list/breadcrumbs

This commit is contained in:
Travis Ralston 2020-06-08 17:11:28 -06:00
commit 17d98e8956
15 changed files with 443 additions and 159 deletions

View file

@ -12,6 +12,7 @@
@import "./structures/_HeaderButtons.scss"; @import "./structures/_HeaderButtons.scss";
@import "./structures/_HomePage.scss"; @import "./structures/_HomePage.scss";
@import "./structures/_LeftPanel.scss"; @import "./structures/_LeftPanel.scss";
@import "./structures/_LeftPanel2.scss";
@import "./structures/_MainSplit.scss"; @import "./structures/_MainSplit.scss";
@import "./structures/_MatrixChat.scss"; @import "./structures/_MatrixChat.scss";
@import "./structures/_MyGroups.scss"; @import "./structures/_MyGroups.scss";
@ -177,10 +178,12 @@
@import "./views/rooms/_RoomDropTarget.scss"; @import "./views/rooms/_RoomDropTarget.scss";
@import "./views/rooms/_RoomHeader.scss"; @import "./views/rooms/_RoomHeader.scss";
@import "./views/rooms/_RoomList.scss"; @import "./views/rooms/_RoomList.scss";
@import "./views/rooms/_RoomList2.scss";
@import "./views/rooms/_RoomPreviewBar.scss"; @import "./views/rooms/_RoomPreviewBar.scss";
@import "./views/rooms/_RoomRecoveryReminder.scss"; @import "./views/rooms/_RoomRecoveryReminder.scss";
@import "./views/rooms/_RoomSublist2.scss"; @import "./views/rooms/_RoomSublist2.scss";
@import "./views/rooms/_RoomTile.scss"; @import "./views/rooms/_RoomTile.scss";
@import "./views/rooms/_RoomTile2.scss";
@import "./views/rooms/_RoomUpgradeWarningBar.scss"; @import "./views/rooms/_RoomUpgradeWarningBar.scss";
@import "./views/rooms/_SearchBar.scss"; @import "./views/rooms/_SearchBar.scss";
@import "./views/rooms/_SendMessageComposer.scss"; @import "./views/rooms/_SendMessageComposer.scss";

View file

@ -23,14 +23,6 @@ limitations under the License.
flex: 0 0 auto; flex: 0 0 auto;
} }
// TODO: Remove temporary indicator of new room list implementation.
// This border is meant to visually distinguish between the two components when the
// user has turned on the new room list implementation, at least until the designs
// themselves give it away.
.mx_LeftPanel2 .mx_LeftPanel {
border-left: 5px #e26dff solid;
}
.mx_LeftPanel_container.collapsed { .mx_LeftPanel_container.collapsed {
min-width: unset; min-width: unset;
/* Collapsed LeftPanel 50px */ /* Collapsed LeftPanel 50px */

View file

@ -0,0 +1,94 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
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.
*/
// TODO: Rename to mx_LeftPanel during replacement of old component
// TODO: Put these variables in the right place, or namespace them.
$tagPanelWidth: 70px;
$roomListMinimizedWidth: 50px;
.mx_LeftPanel2 {
background-color: $header-panel-bg-color;
min-width: 260px;
max-width: 50%;
// Create a row-based flexbox for the TagPanel and the room list
display: flex;
.mx_LeftPanel2_tagPanelContainer {
flex-grow: 0;
flex-shrink: 0;
flex-basis: $tagPanelWidth;
height: 100%;
// Create another flexbox so the TagPanel fills the container
display: flex;
// TagPanel handles its own CSS
}
// Note: The 'room list' in this context is actually everything that isn't the tag
// panel, such as the menu options, breadcrumbs, filtering, etc
.mx_LeftPanel2_roomListContainer {
width: calc(100% - $tagPanelWidth);
// Create another flexbox (this time a column) for the room list components
display: flex;
flex-direction: column;
.mx_LeftPanel2_userHeader {
padding: 14px 12px 20px; // 14px top, 12px sides, 20px bottom
// Create another flexbox column for the rows to stack within
display: flex;
flex-direction: column;
// There's 2 rows when breadcrumbs are present: the top bit and the breadcrumbs
.mx_LeftPanel2_headerRow {
// Create yet another flexbox, this time within the row, to ensure items stay
// aligned correctly. This is also a row-based flexbox.
display: flex;
align-items: center;
}
.mx_LeftPanel2_userAvatarContainer {
position: relative; // to make default avatars work
margin-right: 8px;
}
.mx_LeftPanel2_userName {
font-weight: 600;
font-size: $font-15px;
line-height: $font-20px;
}
.mx_LeftPanel2_breadcrumbsContainer {
// TODO: Improve CSS for breadcrumbs (currently shoved into the view rather than placed)
width: 100%;
overflow: hidden;
}
}
.mx_LeftPanel2_filterContainer {
// TODO: Improve CSS for filtering and its input
}
.mx_LeftPanel2_actualRoomListContainer {
flex-grow: 1; // fill the available space
overflow-y: auto;
}
}
}

View file

@ -66,7 +66,7 @@ limitations under the License.
} }
/* not the left panel, and not the resize handle, so the roomview/groupview/... */ /* not the left panel, and not the resize handle, so the roomview/groupview/... */
.mx_MatrixChat > :not(.mx_LeftPanel_container):not(.mx_ResizeHandle) { .mx_MatrixChat > :not(.mx_LeftPanel_container):not(.mx_LeftPanel2):not(.mx_ResizeHandle) {
background-color: $primary-bg-color; background-color: $primary-bg-color;
flex: 1 1 0; flex: 1 1 0;

View file

@ -0,0 +1,25 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
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.
*/
// TODO: Rename to mx_RoomList during replacement of old component
.mx_RoomList2 {
// Create a column-based flexbox for the sublists. That's pretty much all we have to
// worry about in this stylesheet.
display: flex;
flex-direction: column;
flex-wrap: wrap;
}

View file

@ -14,8 +14,42 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
// TODO: Rename to mx_RoomSublist during replacement of old component
// TODO: Just use the 3 selectors we need from this instead of importing it.
// We're going to end up with heavy modifications anyways.
@import "../../../../node_modules/react-resizable/css/styles.css"; @import "../../../../node_modules/react-resizable/css/styles.css";
.mx_RoomList2 .mx_RoomSubList_labelContainer { .mx_RoomSublist2 {
z-index: 12; // The sublist is a column of rows, essentially
display: flex;
flex-direction: column;
margin-left: 8px;
margin-top: 12px;
margin-bottom: 12px;
.mx_RoomSublist2_headerContainer {
text-transform: uppercase;
opacity: 0.5;
line-height: $font-16px;
font-size: $font-12px;
padding-bottom: 8px;
}
.mx_RoomSublist2_resizeBox {
// Create another flexbox column for the tiles
display: flex;
flex-direction: column;
overflow: hidden;
.mx_RoomSublist2_showMoreButton {
height: 44px; // 1 room tile high
cursor: pointer;
// We create a flexbox to cheat at alignment
display: flex;
align-items: center;
}
}
} }

View file

@ -0,0 +1,103 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
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.
*/
// TODO: Rename to mx_RoomTile during replacement of old component
// Note: the room tile expects to be in a flexbox column container
.mx_RoomTile2 {
width: calc(100% - 11px); // 8px for padding (4px on either side), 3px for margin
margin-bottom: 4px;
margin-right: 3px;
padding: 4px;
// The tile is also a flexbox row itself
display: flex;
flex-wrap: wrap;
&.mx_RoomTile2_selected {
background-color: $roomtile2-selected-bg-color;
border-radius: 32px;
}
.mx_RoomTile2_avatarContainer {
margin-right: 8px;
}
.mx_RoomTile2_nameContainer {
// Create a new column layout flexbox for the name parts
display: flex;
flex-direction: column;
justify-content: center;
.mx_RoomTile2_name,
.mx_RoomTile2_messagePreview {
margin: 0 2px;
}
// TODO: Ellipsis on the name and preview
.mx_RoomTile2_name {
font-weight: 600;
font-size: $font-14px;
line-height: $font-19px;
}
.mx_RoomTile2_messagePreview {
font-size: $font-13px;
line-height: $font-18px;
color: $roomtile2-preview-color;
}
}
.mx_RoomTile2_badgeContainer {
flex-grow: 1;
// Create another flexbox row because it's super easy to position the badge at
// the end this way.
display: flex;
align-items: center;
justify-content: flex-end;
.mx_RoomTile2_badge {
background-color: $roomtile2-badge-color;
&:not(.mx_RoomTile2_badgeEmpty) {
border-radius: 16px;
font-size: $font-10px;
line-height: $font-14px;
text-align: center;
font-weight: bold;
margin-right: 14px;
color: #fff; // TODO: Variable
// TODO: Confirm padding on counted badges
padding: 2px 5px;
}
&.mx_RoomTile2_badgeEmpty {
width: 6px;
height: 6px;
border-radius: 6px;
margin-right: 18px;
}
&.mx_RoomTile2_badgeHighlight {
// TODO: Use a more specific variable
background-color: $warning-color;
}
}
}
}

View file

@ -172,6 +172,12 @@ $header-divider-color: #91A1C0;
// ******************** // ********************
// TODO: Update variables for new room list
// TODO: Dark theme
$roomtile2-preview-color: #9e9e9e;
$roomtile2-badge-color: #61708b;
$roomtile2-selected-bg-color: #FFF;
$roomtile-name-color: #61708b; $roomtile-name-color: #61708b;
$roomtile-badge-fg-color: $accent-fg-color; $roomtile-badge-fg-color: $accent-fg-color;
$roomtile-selected-color: #212121; $roomtile-selected-color: #212121;

View file

@ -27,7 +27,7 @@ import RoomViewStore from './stores/RoomViewStore';
*/ */
class ActiveRoomObserver { class ActiveRoomObserver {
constructor() { constructor() {
this._listeners = {}; this._listeners = {}; // key=roomId, value=function(isActive:boolean)
this._activeRoomId = RoomViewStore.getRoomId(); this._activeRoomId = RoomViewStore.getRoomId();
// TODO: We could self-destruct when the last listener goes away, or at least // TODO: We could self-destruct when the last listener goes away, or at least
@ -35,6 +35,10 @@ class ActiveRoomObserver {
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate.bind(this)); this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate.bind(this));
} }
get activeRoomId(): string {
return this._activeRoomId;
}
addListener(roomId, listener) { addListener(roomId, listener) {
if (!this._listeners[roomId]) this._listeners[roomId] = []; if (!this._listeners[roomId]) this._listeners[roomId] = [];
this._listeners[roomId].push(listener); this._listeners[roomId].push(listener);
@ -51,23 +55,23 @@ class ActiveRoomObserver {
} }
} }
_emit(roomId) { _emit(roomId, isActive: boolean) {
if (!this._listeners[roomId]) return; if (!this._listeners[roomId]) return;
for (const l of this._listeners[roomId]) { for (const l of this._listeners[roomId]) {
l.call(); l.call(null, isActive);
} }
} }
_onRoomViewStoreUpdate() { _onRoomViewStoreUpdate() {
// emit for the old room ID // emit for the old room ID
if (this._activeRoomId) this._emit(this._activeRoomId); if (this._activeRoomId) this._emit(this._activeRoomId, false);
// update our cache // update our cache
this._activeRoomId = RoomViewStore.getRoomId(); this._activeRoomId = RoomViewStore.getRoomId();
// and emit for the new one // and emit for the new one
if (this._activeRoomId) this._emit(this._activeRoomId); if (this._activeRoomId) this._emit(this._activeRoomId, true);
} }
} }

View file

@ -24,6 +24,9 @@ import SearchBox from "./SearchBox";
import RoomList2 from "../views/rooms/RoomList2"; import RoomList2 from "../views/rooms/RoomList2";
import TopLeftMenuButton from "./TopLeftMenuButton"; import TopLeftMenuButton from "./TopLeftMenuButton";
import { Action } from "../../dispatcher/actions"; import { Action } from "../../dispatcher/actions";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import BaseAvatar from '../views/avatars/BaseAvatar';
import RoomBreadcrumbs from "../views/rooms/RoomBreadcrumbs";
/******************************************************************* /*******************************************************************
* CAUTION * * CAUTION *
@ -82,24 +85,53 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
} }
} }
private renderHeader(): React.ReactNode {
// TODO: Update when profile info changes
// TODO: Presence
// TODO: Breadcrumbs toggle
// TODO: Menu button
const avatarSize = 32;
// TODO: Don't do this profile lookup in render()
const client = MatrixClientPeg.get();
let displayName = client.getUserId();
let avatarUrl: string = null;
const myUser = client.getUser(client.getUserId());
if (myUser) {
displayName = myUser.rawDisplayName;
avatarUrl = myUser.avatarUrl;
}
return (
<div className="mx_LeftPanel2_userHeader">
<div className="mx_LeftPanel2_headerRow">
<span className="mx_LeftPanel2_userAvatarContainer">
<BaseAvatar
idName={MatrixClientPeg.get().getUserId()}
name={displayName}
url={avatarUrl}
width={avatarSize}
height={avatarSize}
resizeMethod="crop"
className="mx_LeftPanel2_userAvatar"
/>
</span>
<span className="mx_LeftPanel2_userName">{displayName}</span>
</div>
<div className="mx_LeftPanel2_headerRow mx_LeftPanel2_breadcrumbsContainer">
<RoomBreadcrumbs />
</div>
</div>
);
}
public render(): React.ReactNode { public render(): React.ReactNode {
const tagPanel = ( const tagPanel = (
<div className="mx_LeftPanel_tagPanelContainer"> <div className="mx_LeftPanel2_tagPanelContainer">
<TagPanel/> <TagPanel/>
</div> </div>
); );
const exploreButton = (
<div
className={classNames("mx_LeftPanel_explore", {"mx_LeftPanel_explore_hidden": this.state.searchExpanded})}>
<AccessibleButton onClick={() => dis.dispatch({action: 'view_room_directory'})}>
{_t("Explore")}
</AccessibleButton>
</div>
);
const searchBox = (<SearchBox const searchBox = (<SearchBox
className="mx_LeftPanel_filterRooms" className="mx_LeftPanel2_filterRoomsSearch"
enableRoomSearchFocus={true} enableRoomSearchFocus={true}
blurredPlaceholder={_t('Filter')} blurredPlaceholder={_t('Filter')}
placeholder={_t('Filter rooms…')} placeholder={_t('Filter rooms…')}
@ -124,29 +156,25 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
// TODO: Conference handling / calls // TODO: Conference handling / calls
const containerClasses = classNames({ const containerClasses = classNames({
"mx_LeftPanel_container": true, "mx_LeftPanel2": true,
"mx_fadable": true,
"collapsed": false, // TODO: Collapsed support
"mx_LeftPanel_container_hasTagPanel": true, // TODO: TagPanel support
"mx_fadable_faded": false,
"mx_LeftPanel2": true, // TODO: Remove flag when RoomList2 ships (used as an indicator)
}); });
return ( return (
<div className={containerClasses}> <div className={containerClasses}>
{tagPanel} {tagPanel}
<aside className="mx_LeftPanel dark-panel"> <aside className="mx_LeftPanel2_roomListContainer">
<TopLeftMenuButton collapsed={false}/> {this.renderHeader()}
<div <div
className="mx_LeftPanel_exploreAndFilterRow" className="mx_LeftPanel2_filterContainer"
onKeyDown={() => {/*TODO*/}} onKeyDown={() => {/*TODO*/}}
onFocus={() => {/*TODO*/}} onFocus={() => {/*TODO*/}}
onBlur={() => {/*TODO*/}} onBlur={() => {/*TODO*/}}
> >
{exploreButton}
{searchBox} {searchBox}
</div> </div>
{roomList} <div className="mx_LeftPanel2_actualRoomListContainer">
{roomList}
</div>
</aside> </aside>
</div> </div>
); );

View file

@ -96,7 +96,7 @@ const TAG_AESTHETICS: {
defaultHidden: false, defaultHidden: false,
}, },
[DefaultTagID.DM]: { [DefaultTagID.DM]: {
sectionLabel: _td("Direct Messages"), sectionLabel: _td("People"),
isInvite: false, isInvite: false,
defaultHidden: false, defaultHidden: false,
addRoomLabel: _td("Start chat"), addRoomLabel: _td("Start chat"),
@ -200,6 +200,7 @@ export default class RoomList2 extends React.Component<IProps, IState> {
addRoomLabel={aesthetics.addRoomLabel} addRoomLabel={aesthetics.addRoomLabel}
isInvite={aesthetics.isInvite} isInvite={aesthetics.isInvite}
layout={this.state.layouts.get(orderedTagId)} layout={this.state.layouts.get(orderedTagId)}
showMessagePreviews={orderedTagId === DefaultTagID.DM}
/> />
); );
} }
@ -216,7 +217,7 @@ export default class RoomList2 extends React.Component<IProps, IState> {
onFocus={this.props.onFocus} onFocus={this.props.onFocus}
onBlur={this.props.onBlur} onBlur={this.props.onBlur}
onKeyDown={onKeyDownHandler} onKeyDown={onKeyDownHandler}
className="mx_RoomList mx_RoomList2" className="mx_RoomList2"
role="tree" role="tree"
aria-label={_t("Rooms")} aria-label={_t("Rooms")}
// Firefox sometimes makes this element focusable due to // Firefox sometimes makes this element focusable due to

View file

@ -20,15 +20,13 @@ import * as React from "react";
import { createRef } from "react"; import { createRef } from "react";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import classNames from 'classnames'; import classNames from 'classnames';
import * as RoomNotifs from '../../../RoomNotifs';
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import AccessibleButton from "../../views/elements/AccessibleButton"; import AccessibleButton from "../../views/elements/AccessibleButton";
import AccessibleTooltipButton from "../../views/elements/AccessibleTooltipButton";
import * as FormattingUtils from '../../../utils/FormattingUtils';
import RoomTile2 from "./RoomTile2"; import RoomTile2 from "./RoomTile2";
import { ResizableBox, ResizeCallbackData } from "react-resizable"; import { ResizableBox, ResizeCallbackData } from "react-resizable";
import { ListLayout } from "../../../stores/room-list/ListLayout"; import { ListLayout } from "../../../stores/room-list/ListLayout";
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
/******************************************************************* /*******************************************************************
* CAUTION * * CAUTION *
@ -43,6 +41,7 @@ interface IProps {
rooms?: Room[]; rooms?: Room[];
startAsHidden: boolean; startAsHidden: boolean;
label: string; label: string;
showMessagePreviews: boolean;
onAddRoom?: () => void; onAddRoom?: () => void;
addRoomLabel: string; addRoomLabel: string;
isInvite: boolean; isInvite: boolean;
@ -93,7 +92,13 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
if (this.props.rooms) { if (this.props.rooms) {
for (const room of this.props.rooms) { for (const room of this.props.rooms) {
tiles.push(<RoomTile2 room={room} key={`room-${room.roomId}`}/>); tiles.push(
<RoomTile2
room={room}
key={`room-${room.roomId}`}
showMessagePreview={this.props.showMessagePreviews}
/>
);
} }
} }
@ -101,25 +106,16 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
} }
private renderHeader(): React.ReactElement { private renderHeader(): React.ReactElement {
const notifications = !this.props.isInvite // TODO: Handle badge count
? RoomNotifs.aggregateNotificationCount(this.props.rooms) // const notifications = !this.props.isInvite
: {count: 0, highlight: true}; // ? RoomNotifs.aggregateNotificationCount(this.props.rooms)
const notifCount = notifications.count; // : {count: 0, highlight: true};
const notifHighlight = notifications.highlight; // const notifCount = notifications.count;
// const notifHighlight = notifications.highlight;
// TODO: Title on collapsed // TODO: Title on collapsed
// TODO: Incoming call box // TODO: Incoming call box
let chevron = null;
if (this.hasTiles()) {
const chevronClasses = classNames({
'mx_RoomSubList_chevron': true,
'mx_RoomSubList_chevronRight': false, // isCollapsed
'mx_RoomSubList_chevronDown': true, // !isCollapsed
});
chevron = (<div className={chevronClasses}/>);
}
return ( return (
<RovingTabIndexWrapper inputRef={this.headerButton}> <RovingTabIndexWrapper inputRef={this.headerButton}>
{({onFocus, isActive, ref}) => { {({onFocus, isActive, ref}) => {
@ -127,68 +123,68 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
const tabIndex = isActive ? 0 : -1; const tabIndex = isActive ? 0 : -1;
// TODO: Collapsed state // TODO: Collapsed state
let badge; // TODO: Handle badge count
if (true) { // !isCollapsed // let badge;
const badgeClasses = classNames({ // if (true) { // !isCollapsed
'mx_RoomSubList_badge': true, // const showCount = localStorage.getItem("mx_rls_count") || notifHighlight;
'mx_RoomSubList_badgeHighlight': notifHighlight, // const badgeClasses = classNames({
}); // 'mx_RoomSublist2_badge': true,
// Wrap the contents in a div and apply styles to the child div so that the browser default outline works // 'mx_RoomSublist2_badgeHighlight': notifHighlight,
if (notifCount > 0) { // 'mx_RoomSublist2_badgeEmpty': !showCount,
badge = ( // });
<AccessibleButton // // Wrap the contents in a div and apply styles to the child div so that the browser default outline works
tabIndex={tabIndex} // if (notifCount > 0) {
className={badgeClasses} // const count = <div>{FormattingUtils.formatCount(notifCount)}</div>;
aria-label={_t("Jump to first unread room.")} // badge = (
> // <AccessibleButton
<div> // tabIndex={tabIndex}
{FormattingUtils.formatCount(notifCount)} // className={badgeClasses}
</div> // aria-label={_t("Jump to first unread room.")}
</AccessibleButton> // >
); // {showCount ? count : null}
} else if (this.props.isInvite && this.hasTiles()) { // </AccessibleButton>
// Render the `!` badge for invites // );
badge = ( // } else if (this.props.isInvite && this.hasTiles()) {
<AccessibleButton // // Render the `!` badge for invites
tabIndex={tabIndex} // badge = (
className={badgeClasses} // <AccessibleButton
aria-label={_t("Jump to first invite.")} // tabIndex={tabIndex}
> // className={badgeClasses}
<div> // aria-label={_t("Jump to first invite.")}
{FormattingUtils.formatCount(this.numTiles)} // >
</div> // <div>
</AccessibleButton> // {FormattingUtils.formatCount(this.numTiles)}
); // </div>
} // </AccessibleButton>
} // );
// }
// }
let addRoomButton = null; // TODO: Aux button
if (!!this.props.onAddRoom) { // let addRoomButton = null;
addRoomButton = ( // if (!!this.props.onAddRoom) {
<AccessibleTooltipButton // addRoomButton = (
tabIndex={tabIndex} // <AccessibleTooltipButton
onClick={this.onAddRoom} // tabIndex={tabIndex}
className="mx_RoomSubList_addRoom" // onClick={this.onAddRoom}
title={this.props.addRoomLabel || _t("Add room")} // className="mx_RoomSublist2_addButton"
/> // title={this.props.addRoomLabel || _t("Add room")}
); // />
} // );
// }
// TODO: a11y (see old component) // TODO: a11y (see old component)
return ( return (
<div className={"mx_RoomSubList_labelContainer"}> <div className={"mx_RoomSublist2_headerContainer"}>
<AccessibleButton <AccessibleButton
inputRef={ref} inputRef={ref}
tabIndex={tabIndex} tabIndex={tabIndex}
className={"mx_RoomSubList_label"} className={"mx_RoomSublist2_headerText"}
role="treeitem" role="treeitem"
aria-level="1" aria-level="1"
> >
{chevron}
<span>{this.props.label}</span> <span>{this.props.label}</span>
</AccessibleButton> </AccessibleButton>
{badge}
{addRoomButton}
</div> </div>
); );
}} }}
@ -204,9 +200,8 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
const classes = classNames({ const classes = classNames({
// TODO: Proper collapse support // TODO: Proper collapse support
'mx_RoomSubList': true, 'mx_RoomSublist2': true,
'mx_RoomSubList_hidden': false, // len && isCollapsed 'mx_RoomSublist2_collapsed': false, // len && isCollapsed
'mx_RoomSubList_nonEmpty': this.hasTiles(), // len && !isCollapsed
}); });
let content = null; let content = null;
@ -244,7 +239,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
visibleTiles.splice(visibleTiles.length - 1, 1, ( visibleTiles.splice(visibleTiles.length - 1, 1, (
<div <div
onClick={this.onShowAllClick} onClick={this.onShowAllClick}
style={{height: '34px', lineHeight: '34px', cursor: 'pointer'}} className='mx_RoomSublist2_showMoreButton'
key='showall' key='showall'
> >
{_t("Show %(n)s more", {n: numMissing})} {_t("Show %(n)s more", {n: numMissing})}

View file

@ -23,7 +23,6 @@ import classNames from "classnames";
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
import AccessibleButton from "../../views/elements/AccessibleButton"; import AccessibleButton from "../../views/elements/AccessibleButton";
import RoomAvatar from "../../views/avatars/RoomAvatar"; import RoomAvatar from "../../views/avatars/RoomAvatar";
import Tooltip from "../../views/elements/Tooltip";
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import { Key } from "../../../Keyboard"; import { Key } from "../../../Keyboard";
import * as RoomNotifs from '../../../RoomNotifs'; import * as RoomNotifs from '../../../RoomNotifs';
@ -32,6 +31,7 @@ import * as Unread from '../../../Unread';
import * as FormattingUtils from "../../../utils/FormattingUtils"; import * as FormattingUtils from "../../../utils/FormattingUtils";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import ActiveRoomObserver from "../../../ActiveRoomObserver";
/******************************************************************* /*******************************************************************
* CAUTION * * CAUTION *
@ -51,6 +51,7 @@ enum NotificationColor {
interface IProps { interface IProps {
room: Room; room: Room;
showMessagePreview: boolean;
// TODO: Allow falsifying counts (for invites and stuff) // TODO: Allow falsifying counts (for invites and stuff)
// TODO: Transparency? Was this ever used? // TODO: Transparency? Was this ever used?
@ -65,6 +66,7 @@ interface INotificationState {
interface IState { interface IState {
hover: boolean; hover: boolean;
notificationState: INotificationState; notificationState: INotificationState;
selected: boolean;
} }
export default class RoomTile2 extends React.Component<IProps, IState> { export default class RoomTile2 extends React.Component<IProps, IState> {
@ -87,12 +89,14 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
this.state = { this.state = {
hover: false, hover: false,
notificationState: this.getNotificationState(), notificationState: this.getNotificationState(),
selected: ActiveRoomObserver.activeRoomId === this.props.room.roomId,
}; };
this.props.room.on("Room.receipt", this.handleRoomEventUpdate); this.props.room.on("Room.receipt", this.handleRoomEventUpdate);
this.props.room.on("Room.timeline", this.handleRoomEventUpdate); this.props.room.on("Room.timeline", this.handleRoomEventUpdate);
this.props.room.on("Room.redaction", this.handleRoomEventUpdate); this.props.room.on("Room.redaction", this.handleRoomEventUpdate);
MatrixClientPeg.get().on("Event.decrypted", this.handleRoomEventUpdate); MatrixClientPeg.get().on("Event.decrypted", this.handleRoomEventUpdate);
ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate);
} }
public componentWillUnmount() { public componentWillUnmount() {
@ -100,6 +104,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
this.props.room.removeListener("Room.receipt", this.handleRoomEventUpdate); this.props.room.removeListener("Room.receipt", this.handleRoomEventUpdate);
this.props.room.removeListener("Room.timeline", this.handleRoomEventUpdate); this.props.room.removeListener("Room.timeline", this.handleRoomEventUpdate);
this.props.room.removeListener("Room.redaction", this.handleRoomEventUpdate); this.props.room.removeListener("Room.redaction", this.handleRoomEventUpdate);
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
} }
if (MatrixClientPeg.get()) { if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("Event.decrypted", this.handleRoomEventUpdate); MatrixClientPeg.get().removeListener("Event.decrypted", this.handleRoomEventUpdate);
@ -186,39 +191,33 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
}); });
}; };
private onActiveRoomUpdate = (isActive: boolean) => {
this.setState({selected: isActive});
};
public render(): React.ReactElement { public render(): React.ReactElement {
// TODO: Collapsed state // TODO: Collapsed state
// TODO: Invites // TODO: Invites
// TODO: a11y proper // TODO: a11y proper
// TODO: Render more than bare minimum // TODO: Render more than bare minimum
const hasBadge = this.state.notificationState.color > NotificationColor.Bold;
const isUnread = this.state.notificationState.color > NotificationColor.None;
const classes = classNames({ const classes = classNames({
'mx_RoomTile': true, 'mx_RoomTile2': true,
// 'mx_RoomTile_selected': this.state.selected, 'mx_RoomTile2_selected': this.state.selected,
'mx_RoomTile_unread': isUnread,
'mx_RoomTile_unreadNotify': this.state.notificationState.color >= NotificationColor.Grey,
'mx_RoomTile_highlight': this.state.notificationState.color >= NotificationColor.Red,
'mx_RoomTile_invited': this.roomIsInvite,
// 'mx_RoomTile_menuDisplayed': isMenuDisplayed,
'mx_RoomTile_noBadges': !hasBadge,
// 'mx_RoomTile_transparent': this.props.transparent,
// 'mx_RoomTile_hasSubtext': subtext && !this.props.collapsed,
}); });
const avatarClasses = classNames({
'mx_RoomTile_avatar': true,
});
let badge; let badge;
const hasBadge = this.state.notificationState.color > NotificationColor.Bold;
if (hasBadge) { if (hasBadge) {
const hasNotif = this.state.notificationState.color >= NotificationColor.Red;
const isEmptyBadge = !localStorage.getItem("mx_rl_rt_badgeCount");
const badgeClasses = classNames({ const badgeClasses = classNames({
'mx_RoomTile_badge': true, 'mx_RoomTile2_badge': true,
'mx_RoomTile_badgeButton': false, // this.state.badgeHover || isMenuDisplayed 'mx_RoomTile2_badgeHighlight': hasNotif,
'mx_RoomTile2_badgeEmpty': isEmptyBadge,
}); });
badge = <div className={badgeClasses}>{this.state.notificationState.symbol}</div>; const symbol = this.state.notificationState.symbol;
badge = <div className={badgeClasses}>{isEmptyBadge ? null : symbol}</div>;
} }
// TODO: the original RoomTile uses state for the room name. Do we need to? // TODO: the original RoomTile uses state for the room name. Do we need to?
@ -226,20 +225,21 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
if (typeof name !== 'string') name = ''; if (typeof name !== 'string') name = '';
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
const nameClasses = classNames({
'mx_RoomTile_name': true,
'mx_RoomTile_invite': this.roomIsInvite,
'mx_RoomTile_badgeShown': hasBadge,
});
// TODO: Support collapsed state properly // TODO: Support collapsed state properly
let tooltip = null; // TODO: Tooltip?
if (false) { // isCollapsed
if (this.state.hover) { let messagePreview = null;
tooltip = <Tooltip className="mx_RoomTile_tooltip" label={this.props.room.name} /> if (this.props.showMessagePreview) {
} // TODO: Actually get the real message preview from state
messagePreview = <div className="mx_RoomTile2_messagePreview">I just ate a pie.</div>;
} }
const nameClasses = classNames({
"mx_RoomTile2_name": true,
"mx_RoomTile2_nameWithPreview": !!messagePreview,
});
const avatarSize = 32;
return ( return (
<React.Fragment> <React.Fragment>
<RovingTabIndexWrapper inputRef={this.roomTile}> <RovingTabIndexWrapper inputRef={this.roomTile}>
@ -254,20 +254,18 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
onClick={this.onTileClick} onClick={this.onTileClick}
role="treeitem" role="treeitem"
> >
<div className={avatarClasses}> <div className="mx_RoomTile2_avatarContainer">
<div className="mx_RoomTile_avatar_container"> <RoomAvatar room={this.props.room} width={avatarSize} height={avatarSize}/>
<RoomAvatar room={this.props.room} width={24} height={24}/>
</div>
</div> </div>
<div className="mx_RoomTile_nameContainer"> <div className="mx_RoomTile2_nameContainer">
<div className="mx_RoomTile_labelContainer"> <div title={name} className={nameClasses} tabIndex={-1} dir="auto">
<div title={name} className={nameClasses} tabIndex={-1} dir="auto"> {name}
{name}
</div>
</div> </div>
{messagePreview}
</div>
<div className="mx_RoomTile2_badgeContainer">
{badge} {badge}
</div> </div>
{tooltip}
</AccessibleButton> </AccessibleButton>
} }
</RovingTabIndexWrapper> </RovingTabIndexWrapper>

View file

@ -1090,6 +1090,7 @@
"Low priority": "Low priority", "Low priority": "Low priority",
"Historical": "Historical", "Historical": "Historical",
"System Alerts": "System Alerts", "System Alerts": "System Alerts",
"People": "People",
"This room": "This room", "This room": "This room",
"Joining room …": "Joining room …", "Joining room …": "Joining room …",
"Loading …": "Loading …", "Loading …": "Loading …",
@ -1133,9 +1134,6 @@
"Securely back up your keys to avoid losing them. <a>Learn more.</a>": "Securely back up your keys to avoid losing them. <a>Learn more.</a>", "Securely back up your keys to avoid losing them. <a>Learn more.</a>": "Securely back up your keys to avoid losing them. <a>Learn more.</a>",
"Not now": "Not now", "Not now": "Not now",
"Don't ask me again": "Don't ask me again", "Don't ask me again": "Don't ask me again",
"Jump to first unread room.": "Jump to first unread room.",
"Jump to first invite.": "Jump to first invite.",
"Add room": "Add room",
"Show %(n)s more": "Show %(n)s more", "Show %(n)s more": "Show %(n)s more",
"Options": "Options", "Options": "Options",
"%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.", "%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.",
@ -2017,6 +2015,9 @@
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.", "Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
"Active call": "Active call", "Active call": "Active call",
"There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?", "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?",
"Jump to first unread room.": "Jump to first unread room.",
"Jump to first invite.": "Jump to first invite.",
"Add room": "Add room",
"You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?", "You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?",
"You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?", "You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?",
"Search failed": "Search failed", "Search failed": "Search failed",

View file

@ -16,7 +16,7 @@ limitations under the License.
import { TagID } from "./models"; import { TagID } from "./models";
const TILE_HEIGHT_PX = 34; const TILE_HEIGHT_PX = 44;
interface ISerializedListLayout { interface ISerializedListLayout {
numTiles: number; numTiles: number;