Simple structuring of the room list itself

This covers the larger parts of the design, but doesn't deal with the nuances of hover states, badge sizing, etc.
This commit is contained in:
Travis Ralston 2020-06-04 21:21:04 -06:00
parent 4c1bc50649
commit 0c15b2bdb6
8 changed files with 205 additions and 116 deletions

View file

@ -16,7 +16,27 @@ limitations under the License.
@import "../../../../node_modules/react-resizable/css/styles.css"; @import "../../../../node_modules/react-resizable/css/styles.css";
.mx_RoomList2 .mx_RoomSubList2_labelContainer { .mx_RoomSublist2 {
z-index: 12; // The sublist is a column of rows, essentially
background-color: purple; 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;
}
} }

View file

@ -14,5 +14,81 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
// Note: the room tile expects to be in a flexbox column container
.mx_RoomTile2 { .mx_RoomTile2 {
width: 100%;
padding-bottom: 12px;
// The tile is also a flexbox row itself
display: flex;
flex-wrap: wrap;
.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,11 @@ $header-divider-color: #91A1C0;
// ******************** // ********************
// TODO: Update variables for new room list
// TODO: Dark theme
$roomtile2-preview-color: #9e9e9e;
$roomtile2-badge-color: #61708b;
$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

@ -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}
/> />
); );
} }

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_RoomSublist2_chevron': true,
'mx_RoomSublist2_chevronRight': false, // isCollapsed
'mx_RoomSublist2_chevronDown': true, // !isCollapsed
});
chevron = (<div className={chevronClasses}/>);
}
return ( return (
<RovingTabIndexWrapper inputRef={this.headerButton}> <RovingTabIndexWrapper inputRef={this.headerButton}>
{({onFocus, isActive, ref}) => { {({onFocus, isActive, ref}) => {
@ -127,52 +123,55 @@ 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_RoomSublist2_badge': true, // const showCount = localStorage.getItem("mx_rls_count") || notifHighlight;
'mx_RoomSublist2_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_RoomSublist2_addButton" // 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 (
@ -184,11 +183,8 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
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>
); );
}} }}
@ -243,13 +239,14 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
// TODO: CSS TBD // TODO: CSS TBD
// TODO: Show N more instead of infinity more? // TODO: Show N more instead of infinity more?
// TODO: Safely use the same height of a tile, not hardcoded hacks // TODO: Safely use the same height of a tile, not hardcoded hacks
const moreTileHeightPx = `${layout.tileHeight}px`;
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', backgroundColor: 'green', cursor: 'pointer'}} style={{height: moreTileHeightPx, lineHeight: moreTileHeightPx, backgroundColor: 'transparent', cursor: 'pointer'}}
key='showall' key='showall'
> >
{_t("Show %(n)s more rooms", {n: numMissing})} {_t("Show %(n)s more", {n: numMissing})}
</div> </div>
)); ));
} }

View file

@ -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?
@ -192,33 +193,22 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
// 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_RoomTile2': true, 'mx_RoomTile2': true,
// 'mx_RoomTile_selected': this.state.selected,
'mx_RoomTile2_unread': isUnread,
'mx_RoomTile2_unreadNotify': this.state.notificationState.color >= NotificationColor.Grey,
'mx_RoomTile2_highlight': this.state.notificationState.color >= NotificationColor.Red,
'mx_RoomTile2_invited': this.roomIsInvite,
// 'mx_RoomTile_menuDisplayed': isMenuDisplayed,
'mx_RoomTile2_noBadges': !hasBadge,
// 'mx_RoomTile_transparent': this.props.transparent,
// 'mx_RoomTile_hasSubtext': subtext && !this.props.collapsed,
}); });
const avatarClasses = classNames({
'mx_RoomTile2_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_RoomTile2_badge': true, 'mx_RoomTile2_badge': true,
'mx_RoomTile2_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 +216,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_RoomTile2_name': true,
'mx_RoomTile2_invite': this.roomIsInvite,
'mx_RoomTile2_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_RoomTile2_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 +245,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_RoomTile2_avatarContainer"> <RoomAvatar room={this.props.room} width={avatarSize} height={avatarSize}/>
<RoomAvatar room={this.props.room} width={24} height={24}/>
</div>
</div> </div>
<div className="mx_RoomTile2_nameContainer"> <div className="mx_RoomTile2_nameContainer">
<div className="mx_RoomTile2_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,10 +1134,7 @@
"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.", "Show %(n)s more": "Show %(n)s more",
"Jump to first invite.": "Jump to first invite.",
"Add room": "Add room",
"Show %(n)s more rooms": "Show %(n)s more rooms",
"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.",
"%(count)s unread messages including mentions.|one": "1 unread mention.", "%(count)s unread messages including mentions.|one": "1 unread mention.",
@ -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

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
const TILE_HEIGHT_PX = 34; const TILE_HEIGHT_PX = 44;
interface ISerializedListLayout { interface ISerializedListLayout {
numTiles: number; numTiles: number;