Merge pull request #4818 from matrix-org/travis/room-list/user-menu-polish
Update profile information in User Menu and truncate where needed
This commit is contained in:
commit
3b24f42def
6 changed files with 170 additions and 31 deletions
|
@ -65,6 +65,10 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations
|
||||||
.mx_LeftPanel2_userAvatarContainer {
|
.mx_LeftPanel2_userAvatarContainer {
|
||||||
position: relative; // to make default avatars work
|
position: relative; // to make default avatars work
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
|
|
||||||
|
.mx_LeftPanel2_userAvatar {
|
||||||
|
border-radius: 32px; // should match avatar size
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_LeftPanel2_userName {
|
.mx_LeftPanel2_userName {
|
||||||
|
@ -72,6 +76,11 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations
|
||||||
font-size: $font-15px;
|
font-size: $font-15px;
|
||||||
line-height: $font-20px;
|
line-height: $font-20px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
|
// Ellipsize any text overflow
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_LeftPanel2_headerButtons {
|
.mx_LeftPanel2_headerButtons {
|
||||||
|
|
|
@ -35,9 +35,6 @@ limitations under the License.
|
||||||
// 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;
|
||||||
|
|
||||||
// fit the container
|
|
||||||
flex: 1;
|
|
||||||
width: calc(100% - 40px); // 40px = 32px theme button + 8px margin to theme button
|
width: calc(100% - 40px); // 40px = 32px theme button + 8px margin to theme button
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import { createRef } from "react";
|
||||||
import TagPanel from "./TagPanel";
|
import TagPanel from "./TagPanel";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import dis from "../../dispatcher/dispatcher";
|
import dis from "../../dispatcher/dispatcher";
|
||||||
|
@ -30,7 +31,9 @@ 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 { createRef } from "react";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
import { throttle } from 'lodash';
|
||||||
|
import { OwnProfileStore } from "../../stores/OwnProfileStore";
|
||||||
|
|
||||||
/*******************************************************************
|
/*******************************************************************
|
||||||
* CAUTION *
|
* CAUTION *
|
||||||
|
@ -73,13 +76,32 @@ 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});
|
||||||
};
|
};
|
||||||
|
@ -149,16 +171,7 @@ 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;
|
const avatarSize = 32; // should match border-radius of the avatar
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
let breadcrumbs;
|
let breadcrumbs;
|
||||||
if (this.state.showBreadcrumbs) {
|
if (this.state.showBreadcrumbs) {
|
||||||
|
@ -169,7 +182,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = <span className="mx_LeftPanel2_userName">{displayName}</span>;
|
let name = <span className="mx_LeftPanel2_userName">{OwnProfileStore.instance.displayName}</span>;
|
||||||
let buttons = (
|
let buttons = (
|
||||||
<span className="mx_LeftPanel2_headerButtons">
|
<span className="mx_LeftPanel2_headerButtons">
|
||||||
<UserMenuButton />
|
<UserMenuButton />
|
||||||
|
@ -186,8 +199,8 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
||||||
<span className="mx_LeftPanel2_userAvatarContainer">
|
<span className="mx_LeftPanel2_userAvatarContainer">
|
||||||
<BaseAvatar
|
<BaseAvatar
|
||||||
idName={MatrixClientPeg.get().getUserId()}
|
idName={MatrixClientPeg.get().getUserId()}
|
||||||
name={displayName}
|
name={OwnProfileStore.instance.displayName || MatrixClientPeg.get().getUserId()}
|
||||||
url={avatarUrl}
|
url={OwnProfileStore.instance.getHttpAvatarUrl(avatarSize)}
|
||||||
width={avatarSize}
|
width={avatarSize}
|
||||||
height={avatarSize}
|
height={avatarSize}
|
||||||
resizeMethod="crop"
|
resizeMethod="crop"
|
||||||
|
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {User} from "matrix-js-sdk/src/models/user";
|
|
||||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
import { ActionPayload } from "../../dispatcher/payloads";
|
import { ActionPayload } from "../../dispatcher/payloads";
|
||||||
|
@ -34,12 +33,13 @@ import {getHostingLink} from "../../utils/HostingLink";
|
||||||
import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
|
import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
|
||||||
import SdkConfig from "../../SdkConfig";
|
import SdkConfig from "../../SdkConfig";
|
||||||
import {getHomePageUrl} from "../../utils/pages";
|
import {getHomePageUrl} from "../../utils/pages";
|
||||||
|
import { OwnProfileStore } from "../../stores/OwnProfileStore";
|
||||||
|
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
user: User;
|
|
||||||
menuDisplayed: boolean;
|
menuDisplayed: boolean;
|
||||||
isDarkTheme: boolean;
|
isDarkTheme: boolean;
|
||||||
}
|
}
|
||||||
|
@ -54,19 +54,10 @@ export default class UserMenuButton extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
menuDisplayed: false,
|
menuDisplayed: false,
|
||||||
user: MatrixClientPeg.get().getUser(MatrixClientPeg.get().getUserId()),
|
|
||||||
isDarkTheme: this.isUserOnDarkTheme(),
|
isDarkTheme: this.isUserOnDarkTheme(),
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
private get displayName(): string {
|
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
|
||||||
if (MatrixClientPeg.get().isGuest()) {
|
|
||||||
return _t("Guest");
|
|
||||||
} else if (this.state.user) {
|
|
||||||
return this.state.user.displayName;
|
|
||||||
} else {
|
|
||||||
return MatrixClientPeg.get().getUserId();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private get hasHomePage(): boolean {
|
private get hasHomePage(): boolean {
|
||||||
|
@ -81,6 +72,7 @@ export default class UserMenuButton extends React.Component<IProps, IState> {
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
if (this.themeWatcherRef) SettingsStore.unwatchSetting(this.themeWatcherRef);
|
if (this.themeWatcherRef) SettingsStore.unwatchSetting(this.themeWatcherRef);
|
||||||
if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef);
|
if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef);
|
||||||
|
OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
private isUserOnDarkTheme(): boolean {
|
private isUserOnDarkTheme(): boolean {
|
||||||
|
@ -91,6 +83,12 @@ export default class UserMenuButton extends React.Component<IProps, IState> {
|
||||||
return theme === "dark";
|
return theme === "dark";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 onThemeChanged = () => {
|
private onThemeChanged = () => {
|
||||||
this.setState({isDarkTheme: this.isUserOnDarkTheme()});
|
this.setState({isDarkTheme: this.isUserOnDarkTheme()});
|
||||||
};
|
};
|
||||||
|
@ -209,7 +207,7 @@ export default class UserMenuButton extends React.Component<IProps, IState> {
|
||||||
<div className="mx_UserMenuButton_contextMenu_header">
|
<div className="mx_UserMenuButton_contextMenu_header">
|
||||||
<div className="mx_UserMenuButton_contextMenu_name">
|
<div className="mx_UserMenuButton_contextMenu_name">
|
||||||
<span className="mx_UserMenuButton_contextMenu_displayName">
|
<span className="mx_UserMenuButton_contextMenu_displayName">
|
||||||
{this.displayName}
|
{OwnProfileStore.instance.displayName}
|
||||||
</span>
|
</span>
|
||||||
<span className="mx_UserMenuButton_contextMenu_userId">
|
<span className="mx_UserMenuButton_contextMenu_userId">
|
||||||
{MatrixClientPeg.get().getUserId()}
|
{MatrixClientPeg.get().getUserId()}
|
||||||
|
|
|
@ -422,6 +422,7 @@
|
||||||
"Upgrade your Riot": "Upgrade your Riot",
|
"Upgrade your Riot": "Upgrade your Riot",
|
||||||
"A new version of Riot is available!": "A new version of Riot is available!",
|
"A new version of Riot is available!": "A new version of Riot is available!",
|
||||||
"You: %(message)s": "You: %(message)s",
|
"You: %(message)s": "You: %(message)s",
|
||||||
|
"Guest": "Guest",
|
||||||
"There was an error joining the room": "There was an error joining the room",
|
"There was an error joining the room": "There was an error joining the room",
|
||||||
"Sorry, your homeserver is too old to participate in this room.": "Sorry, your homeserver is too old to participate in this room.",
|
"Sorry, your homeserver is too old to participate in this room.": "Sorry, your homeserver is too old to participate in this room.",
|
||||||
"Please contact your homeserver administrator.": "Please contact your homeserver administrator.",
|
"Please contact your homeserver administrator.": "Please contact your homeserver administrator.",
|
||||||
|
@ -2059,7 +2060,6 @@
|
||||||
"Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.",
|
"Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.",
|
||||||
"Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.",
|
"Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.",
|
||||||
"Failed to load timeline position": "Failed to load timeline position",
|
"Failed to load timeline position": "Failed to load timeline position",
|
||||||
"Guest": "Guest",
|
|
||||||
"Your profile": "Your profile",
|
"Your profile": "Your profile",
|
||||||
"Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others",
|
"Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others",
|
||||||
"Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s",
|
"Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s",
|
||||||
|
|
122
src/stores/OwnProfileStore.ts
Normal file
122
src/stores/OwnProfileStore.ts
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ActionPayload } from "../dispatcher/payloads";
|
||||||
|
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
|
||||||
|
import defaultDispatcher from "../dispatcher/dispatcher";
|
||||||
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
import { User } from "matrix-js-sdk/src/models/user";
|
||||||
|
import { throttle } from "lodash";
|
||||||
|
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||||
|
import { _t } from "../languageHandler";
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
displayName?: string;
|
||||||
|
avatarUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OwnProfileStore extends AsyncStoreWithClient<IState> {
|
||||||
|
private static internalInstance = new OwnProfileStore();
|
||||||
|
|
||||||
|
private monitoredUser: User;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
super(defaultDispatcher, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static get instance(): OwnProfileStore {
|
||||||
|
return OwnProfileStore.internalInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the display name for the user, or null if not present.
|
||||||
|
*/
|
||||||
|
public get displayName(): string {
|
||||||
|
if (!this.matrixClient) return this.state.displayName || null;
|
||||||
|
|
||||||
|
if (this.matrixClient.isGuest()) {
|
||||||
|
return _t("Guest");
|
||||||
|
} else if (this.state.displayName) {
|
||||||
|
return this.state.displayName;
|
||||||
|
} else {
|
||||||
|
return this.matrixClient.getUserId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the MXC URI of the user's avatar, or null if not present.
|
||||||
|
*/
|
||||||
|
public get avatarMxc(): string {
|
||||||
|
return this.state.avatarUrl || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the user's avatar as an HTTP URL of the given size. If the user's
|
||||||
|
* avatar is not present, this returns null.
|
||||||
|
* @param size The size of the avatar
|
||||||
|
* @returns The HTTP URL of the user's avatar
|
||||||
|
*/
|
||||||
|
public getHttpAvatarUrl(size: number): string {
|
||||||
|
if (!this.avatarMxc) return null;
|
||||||
|
return this.matrixClient.mxcUrlToHttp(this.avatarMxc, size, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async onNotReady() {
|
||||||
|
if (this.monitoredUser) {
|
||||||
|
this.monitoredUser.removeListener("User.displayName", this.onProfileUpdate);
|
||||||
|
this.monitoredUser.removeListener("User.avatarUrl", this.onProfileUpdate);
|
||||||
|
}
|
||||||
|
if (this.matrixClient) {
|
||||||
|
this.matrixClient.removeListener("RoomState.events", this.onStateEvents);
|
||||||
|
}
|
||||||
|
await this.reset({});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async onReady() {
|
||||||
|
const myUserId = this.matrixClient.getUserId();
|
||||||
|
this.monitoredUser = this.matrixClient.getUser(myUserId);
|
||||||
|
if (this.monitoredUser) {
|
||||||
|
this.monitoredUser.on("User.displayName", this.onProfileUpdate);
|
||||||
|
this.monitoredUser.on("User.avatarUrl", this.onProfileUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We also have to listen for membership events for ourselves as the above User events
|
||||||
|
// are fired only with presence, which matrix.org (and many others) has disabled.
|
||||||
|
this.matrixClient.on("RoomState.events", this.onStateEvents);
|
||||||
|
|
||||||
|
await this.onProfileUpdate(); // trigger an initial update
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async onAction(payload: ActionPayload) {
|
||||||
|
// we don't actually do anything here
|
||||||
|
}
|
||||||
|
|
||||||
|
private onProfileUpdate = async () => {
|
||||||
|
// We specifically do not use the User object we stored for profile info as it
|
||||||
|
// could easily be wrong (such as per-room instead of global profile).
|
||||||
|
const profileInfo = await this.matrixClient.getProfileInfo(this.matrixClient.getUserId());
|
||||||
|
await this.updateState({displayName: profileInfo.displayname, avatarUrl: profileInfo.avatar_url});
|
||||||
|
};
|
||||||
|
|
||||||
|
// TSLint wants this to be a member, but we don't want that.
|
||||||
|
// tslint:disable-next-line
|
||||||
|
private onStateEvents = throttle(async (ev: MatrixEvent) => {
|
||||||
|
const myUserId = MatrixClientPeg.get().getUserId();
|
||||||
|
if (ev.getType() === 'm.room.member' && ev.getSender() === myUserId && ev.getStateKey() === myUserId) {
|
||||||
|
await this.onProfileUpdate();
|
||||||
|
}
|
||||||
|
}, 200, {trailing: true, leading: true});
|
||||||
|
}
|
Loading…
Reference in a new issue