Move all of the UserMenu into the UserMenu component

This commit is contained in:
Travis Ralston 2020-06-25 19:38:11 -06:00
parent dafce40d1b
commit bcfdd4d984
4 changed files with 100 additions and 109 deletions

View file

@ -54,7 +54,7 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations
display: flex; display: flex;
flex-direction: column; flex-direction: column;
// There's 2 rows when breadcrumbs are present: the top bit and the breadcrumbs // This is basically just breadcrumbs. The row above that is handled by the UserMenu
.mx_LeftPanel2_headerRow { .mx_LeftPanel2_headerRow {
// Create yet another flexbox, this time within the row, to ensure items stay // Create yet another flexbox, this time within the row, to ensure items stay
// aligned correctly. This is also a row-based flexbox. // aligned correctly. This is also a row-based flexbox.
@ -62,32 +62,6 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations
align-items: center; align-items: center;
} }
.mx_LeftPanel2_userAvatarContainer {
position: relative; // to make default avatars work
margin-right: 8px;
height: 32px; // to remove the unknown 4px gap the browser puts below it
.mx_LeftPanel2_userAvatar {
border-radius: 32px; // should match avatar size
}
}
.mx_LeftPanel2_userName {
font-weight: 600;
font-size: $font-15px;
line-height: $font-20px;
flex: 1;
// Ellipsize any text overflow
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.mx_LeftPanel2_headerButtons {
// No special styles: the rest of the layout happens to make it work.
}
.mx_LeftPanel2_breadcrumbsContainer { .mx_LeftPanel2_breadcrumbsContainer {
width: 100%; width: 100%;
overflow: hidden; overflow: hidden;

View file

@ -14,6 +14,38 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_UserMenu {
// Create a row-based flexbox to ensure items stay aligned correctly.
display: flex;
align-items: center;
.mx_UserMenu_userAvatarContainer {
position: relative; // to make default avatars work
margin-right: 8px;
height: 32px; // to remove the unknown 4px gap the browser puts below it
.mx_UserMenu_userAvatar {
border-radius: 32px; // should match avatar size
}
}
.mx_UserMenu_userName {
font-weight: 600;
font-size: $font-15px;
line-height: $font-20px;
flex: 1;
// Ellipsize any text overflow
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.mx_UserMenu_headerButtons {
// No special styles: the rest of the layout happens to make it work.
}
}
.mx_UserMenuButton { .mx_UserMenuButton {
> span { > span {
width: 16px; width: 16px;
@ -37,10 +69,10 @@ limitations under the License.
} }
} }
.mx_UserMenuButton_contextMenu { .mx_UserMenu_contextMenu {
width: 247px; width: 247px;
.mx_UserMenuButton_contextMenu_redRow { .mx_UserMenu_contextMenu_redRow {
.mx_AccessibleButton { .mx_AccessibleButton {
color: $warning-color !important; // !important to override styles from context menu color: $warning-color !important; // !important to override styles from context menu
} }
@ -50,12 +82,12 @@ limitations under the License.
} }
} }
.mx_UserMenuButton_contextMenu_header { .mx_UserMenu_contextMenu_header {
// Create a flexbox to organize the header a bit easier // Create a flexbox to organize the header a bit easier
display: flex; display: flex;
align-items: center; align-items: center;
.mx_UserMenuButton_contextMenu_name { .mx_UserMenu_contextMenu_name {
// Create another flexbox of columns to handle large user IDs // Create another flexbox of columns to handle large user IDs
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -72,19 +104,19 @@ limitations under the License.
white-space: nowrap; white-space: nowrap;
} }
.mx_UserMenuButton_contextMenu_displayName { .mx_UserMenu_contextMenu_displayName {
font-weight: bold; font-weight: bold;
font-size: $font-15px; font-size: $font-15px;
line-height: $font-20px; line-height: $font-20px;
} }
.mx_UserMenuButton_contextMenu_userId { .mx_UserMenu_contextMenu_userId {
font-size: $font-15px; font-size: $font-15px;
line-height: $font-24px; line-height: $font-24px;
} }
} }
.mx_UserMenuButton_contextMenu_themeButton { .mx_UserMenu_contextMenu_themeButton {
min-width: 32px; min-width: 32px;
max-width: 32px; max-width: 32px;
width: 32px; width: 32px;
@ -118,31 +150,31 @@ limitations under the License.
} }
} }
.mx_UserMenuButton_iconHome::before { .mx_UserMenu_iconHome::before {
mask-image: url('$(res)/img/feather-customised/home.svg'); mask-image: url('$(res)/img/feather-customised/home.svg');
} }
.mx_UserMenuButton_iconBell::before { .mx_UserMenu_iconBell::before {
mask-image: url('$(res)/img/feather-customised/notifications.svg'); mask-image: url('$(res)/img/feather-customised/notifications.svg');
} }
.mx_UserMenuButton_iconLock::before { .mx_UserMenu_iconLock::before {
mask-image: url('$(res)/img/feather-customised/lock.svg'); mask-image: url('$(res)/img/feather-customised/lock.svg');
} }
.mx_UserMenuButton_iconSettings::before { .mx_UserMenu_iconSettings::before {
mask-image: url('$(res)/img/feather-customised/settings.svg'); mask-image: url('$(res)/img/feather-customised/settings.svg');
} }
.mx_UserMenuButton_iconArchive::before { .mx_UserMenu_iconArchive::before {
mask-image: url('$(res)/img/feather-customised/archive.svg'); mask-image: url('$(res)/img/feather-customised/archive.svg');
} }
.mx_UserMenuButton_iconMessage::before { .mx_UserMenu_iconMessage::before {
mask-image: url('$(res)/img/feather-customised/message-circle.svg'); mask-image: url('$(res)/img/feather-customised/message-circle.svg');
} }
.mx_UserMenuButton_iconSignOut::before { .mx_UserMenu_iconSignOut::before {
mask-image: url('$(res)/img/feather-customised/sign-out.svg'); mask-image: url('$(res)/img/feather-customised/sign-out.svg');
} }
} }

View file

@ -22,18 +22,13 @@ import dis from "../../dispatcher/dispatcher";
import { _t } from "../../languageHandler"; import { _t } from "../../languageHandler";
import RoomList2 from "../views/rooms/RoomList2"; import RoomList2 from "../views/rooms/RoomList2";
import { Action } from "../../dispatcher/actions"; import { Action } from "../../dispatcher/actions";
import { MatrixClientPeg } from "../../MatrixClientPeg"; import UserMenu from "./UserMenu";
import BaseAvatar from '../views/avatars/BaseAvatar';
import UserMenu from "./UserMenuButton";
import RoomSearch from "./RoomSearch"; import RoomSearch from "./RoomSearch";
import AccessibleButton from "../views/elements/AccessibleButton"; import AccessibleButton from "../views/elements/AccessibleButton";
import RoomBreadcrumbs2 from "../views/rooms/RoomBreadcrumbs2"; import RoomBreadcrumbs2 from "../views/rooms/RoomBreadcrumbs2";
import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore"; import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore";
import { UPDATE_EVENT } from "../../stores/AsyncStore"; import { UPDATE_EVENT } from "../../stores/AsyncStore";
import ResizeNotifier from "../../utils/ResizeNotifier"; import ResizeNotifier from "../../utils/ResizeNotifier";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { throttle } from 'lodash';
import { OwnProfileStore } from "../../stores/OwnProfileStore";
/******************************************************************* /*******************************************************************
* CAUTION * * CAUTION *
@ -76,32 +71,13 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
// We watch the middle panel because we don't actually get resized, the middle panel does. // We watch the middle panel because we don't actually get resized, the middle panel does.
// We listen to the noisy channel to avoid choppy reaction times. // We listen to the noisy channel to avoid choppy reaction times.
this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize); this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize);
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
} }
public componentWillUnmount() { public componentWillUnmount() {
BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate); BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate);
this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize); this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize);
OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate);
} }
// TSLint wants this to be a member, but we don't want that.
// tslint:disable-next-line
private onRoomStateUpdate = throttle((ev: MatrixEvent) => {
const myUserId = MatrixClientPeg.get().getUserId();
if (ev.getType() === 'm.room.member' && ev.getSender() === myUserId && ev.getStateKey() === myUserId) {
// noinspection JSIgnoredPromiseFromCall
this.onProfileUpdate();
}
}, 200, {trailing: true, leading: true});
private onProfileUpdate = async () => {
// the store triggered an update, so force a layout update. We don't
// have any state to store here for that to magically happen.
this.forceUpdate();
};
private onSearch = (term: string): void => { private onSearch = (term: string): void => {
this.setState({searchFilter: term}); this.setState({searchFilter: term});
}; };
@ -170,7 +146,6 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
// TODO: Presence // TODO: Presence
// TODO: Breadcrumbs toggle // TODO: Breadcrumbs toggle
// TODO: Menu button // TODO: Menu button
const avatarSize = 32; // should match border-radius of the avatar
let breadcrumbs; let breadcrumbs;
if (this.state.showBreadcrumbs) { if (this.state.showBreadcrumbs) {
@ -181,34 +156,9 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
); );
} }
let name = <span className="mx_LeftPanel2_userName">{OwnProfileStore.instance.displayName}</span>;
let buttons = (
<span className="mx_LeftPanel2_headerButtons">
<UserMenu />
</span>
);
if (this.props.isMinimized) {
name = null;
buttons = null;
}
return ( return (
<div className="mx_LeftPanel2_userHeader"> <div className="mx_LeftPanel2_userHeader">
<div className="mx_LeftPanel2_headerRow"> <UserMenu isMinimized={this.props.isMinimized} />
<span className="mx_LeftPanel2_userAvatarContainer">
<BaseAvatar
idName={MatrixClientPeg.get().getUserId()}
name={OwnProfileStore.instance.displayName || MatrixClientPeg.get().getUserId()}
url={OwnProfileStore.instance.getHttpAvatarUrl(avatarSize)}
width={avatarSize}
height={avatarSize}
resizeMethod="crop"
className="mx_LeftPanel2_userAvatar"
/>
</span>
{name}
{buttons}
</div>
{breadcrumbs} {breadcrumbs}
</div> </div>
); );

View file

@ -35,8 +35,10 @@ import SdkConfig from "../../SdkConfig";
import {getHomePageUrl} from "../../utils/pages"; import {getHomePageUrl} from "../../utils/pages";
import { OwnProfileStore } from "../../stores/OwnProfileStore"; import { OwnProfileStore } from "../../stores/OwnProfileStore";
import { UPDATE_EVENT } from "../../stores/AsyncStore"; import { UPDATE_EVENT } from "../../stores/AsyncStore";
import BaseAvatar from '../views/avatars/BaseAvatar';
interface IProps { interface IProps {
isMinimized: boolean;
} }
interface IState { interface IState {
@ -158,14 +160,14 @@ export default class UserMenu extends React.Component<IProps, IState> {
defaultDispatcher.dispatch({action: 'view_home_page'}); defaultDispatcher.dispatch({action: 'view_home_page'});
}; };
public render() { private renderMenuButton(): React.ReactNode {
let contextMenu; let contextMenu;
if (this.state.menuDisplayed) { if (this.state.menuDisplayed) {
let hostingLink; let hostingLink;
const signupLink = getHostingLink("user-context-menu"); const signupLink = getHostingLink("user-context-menu");
if (signupLink) { if (signupLink) {
hostingLink = ( hostingLink = (
<div className="mx_UserMenuButton_contextMenu_header"> <div className="mx_UserMenu_contextMenu_header">
{_t( {_t(
"<a>Upgrade</a> to your own domain", {}, "<a>Upgrade</a> to your own domain", {},
{ {
@ -188,7 +190,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
homeButton = ( homeButton = (
<li> <li>
<AccessibleButton onClick={this.onHomeClick}> <AccessibleButton onClick={this.onHomeClick}>
<span className="mx_IconizedContextMenu_icon mx_UserMenuButton_iconHome" /> <span className="mx_IconizedContextMenu_icon mx_UserMenu_iconHome" />
<span>{_t("Home")}</span> <span>{_t("Home")}</span>
</AccessibleButton> </AccessibleButton>
</li> </li>
@ -203,18 +205,18 @@ export default class UserMenu extends React.Component<IProps, IState> {
top={elementRect.top + elementRect.height} top={elementRect.top + elementRect.height}
onFinished={this.onCloseMenu} onFinished={this.onCloseMenu}
> >
<div className="mx_IconizedContextMenu mx_UserMenuButton_contextMenu"> <div className="mx_IconizedContextMenu mx_UserMenu_contextMenu">
<div className="mx_UserMenuButton_contextMenu_header"> <div className="mx_UserMenu_contextMenu_header">
<div className="mx_UserMenuButton_contextMenu_name"> <div className="mx_UserMenu_contextMenu_name">
<span className="mx_UserMenuButton_contextMenu_displayName"> <span className="mx_UserMenu_contextMenu_displayName">
{OwnProfileStore.instance.displayName} {OwnProfileStore.instance.displayName}
</span> </span>
<span className="mx_UserMenuButton_contextMenu_userId"> <span className="mx_UserMenu_contextMenu_userId">
{MatrixClientPeg.get().getUserId()} {MatrixClientPeg.get().getUserId()}
</span> </span>
</div> </div>
<div <div
className="mx_UserMenuButton_contextMenu_themeButton" className="mx_UserMenu_contextMenu_themeButton"
onClick={this.onSwitchThemeClick} onClick={this.onSwitchThemeClick}
title={this.state.isDarkTheme ? _t("Switch to light mode") : _t("Switch to dark mode")} title={this.state.isDarkTheme ? _t("Switch to light mode") : _t("Switch to dark mode")}
> >
@ -231,31 +233,31 @@ export default class UserMenu extends React.Component<IProps, IState> {
{homeButton} {homeButton}
<li> <li>
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}> <AccessibleButton onClick={(e) => this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}>
<span className="mx_IconizedContextMenu_icon mx_UserMenuButton_iconBell" /> <span className="mx_IconizedContextMenu_icon mx_UserMenu_iconBell" />
<span>{_t("Notification settings")}</span> <span>{_t("Notification settings")}</span>
</AccessibleButton> </AccessibleButton>
</li> </li>
<li> <li>
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, USER_SECURITY_TAB)}> <AccessibleButton onClick={(e) => this.onSettingsOpen(e, USER_SECURITY_TAB)}>
<span className="mx_IconizedContextMenu_icon mx_UserMenuButton_iconLock" /> <span className="mx_IconizedContextMenu_icon mx_UserMenu_iconLock" />
<span>{_t("Security & privacy")}</span> <span>{_t("Security & privacy")}</span>
</AccessibleButton> </AccessibleButton>
</li> </li>
<li> <li>
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, null)}> <AccessibleButton onClick={(e) => this.onSettingsOpen(e, null)}>
<span className="mx_IconizedContextMenu_icon mx_UserMenuButton_iconSettings" /> <span className="mx_IconizedContextMenu_icon mx_UserMenu_iconSettings" />
<span>{_t("All settings")}</span> <span>{_t("All settings")}</span>
</AccessibleButton> </AccessibleButton>
</li> </li>
<li> <li>
<AccessibleButton onClick={this.onShowArchived}> <AccessibleButton onClick={this.onShowArchived}>
<span className="mx_IconizedContextMenu_icon mx_UserMenuButton_iconArchive" /> <span className="mx_IconizedContextMenu_icon mx_UserMenu_iconArchive" />
<span>{_t("Archived rooms")}</span> <span>{_t("Archived rooms")}</span>
</AccessibleButton> </AccessibleButton>
</li> </li>
<li> <li>
<AccessibleButton onClick={this.onProvideFeedback}> <AccessibleButton onClick={this.onProvideFeedback}>
<span className="mx_IconizedContextMenu_icon mx_UserMenuButton_iconMessage" /> <span className="mx_IconizedContextMenu_icon mx_UserMenu_iconMessage" />
<span>{_t("Feedback")}</span> <span>{_t("Feedback")}</span>
</AccessibleButton> </AccessibleButton>
</li> </li>
@ -263,9 +265,9 @@ export default class UserMenu extends React.Component<IProps, IState> {
</div> </div>
<div className="mx_IconizedContextMenu_optionList"> <div className="mx_IconizedContextMenu_optionList">
<ul> <ul>
<li className="mx_UserMenuButton_contextMenu_redRow"> <li className="mx_UserMenu_contextMenu_redRow">
<AccessibleButton onClick={this.onSignOutClick}> <AccessibleButton onClick={this.onSignOutClick}>
<span className="mx_IconizedContextMenu_icon mx_UserMenuButton_iconSignOut" /> <span className="mx_IconizedContextMenu_icon mx_UserMenu_iconSignOut" />
<span>{_t("Sign out")}</span> <span>{_t("Sign out")}</span>
</AccessibleButton> </AccessibleButton>
</li> </li>
@ -291,4 +293,37 @@ export default class UserMenu extends React.Component<IProps, IState> {
</React.Fragment> </React.Fragment>
); );
} }
public render() {
const avatarSize = 32; // should match border-radius of the avatar
let name = <span className="mx_UserMenu_userName">{OwnProfileStore.instance.displayName}</span>;
let buttons = (
<span className="mx_UserMenu_headerButtons">
{this.renderMenuButton()}
</span>
);
if (this.props.isMinimized) {
name = null;
buttons = null;
}
return (
<div className="mx_UserMenu">
<span className="mx_UserMenu_userAvatarContainer">
<BaseAvatar
idName={MatrixClientPeg.get().getUserId()}
name={OwnProfileStore.instance.displayName || MatrixClientPeg.get().getUserId()}
url={OwnProfileStore.instance.getHttpAvatarUrl(avatarSize)}
width={avatarSize}
height={avatarSize}
resizeMethod="crop"
className="mx_UserMenu_userAvatar"
/>
</span>
{name}
{buttons}
</div>
);
}
} }