Update profile information in User Menu

Fixes https://github.com/vector-im/riot-web/issues/14158 (we needed an HTTP avatar URL)
Fixes https://github.com/vector-im/riot-web/issues/14159
Fixes https://github.com/vector-im/riot-web/issues/14157
Also fixes an issue where it wasn't updating automatically when the user changed their profile info.

This is all achieved through a new OwnProfileStore which does the heavy lifting, as we have to keep at least 2 components updated.
pull/21833/head
Travis Ralston 2020-06-23 20:59:26 -06:00
parent 74e4ea7d48
commit 380aed4244
6 changed files with 170 additions and 31 deletions

View File

@ -65,6 +65,10 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations
.mx_LeftPanel2_userAvatarContainer {
position: relative; // to make default avatars work
margin-right: 8px;
.mx_LeftPanel2_userAvatar {
border-radius: 32px; // should match avatar size
}
}
.mx_LeftPanel2_userName {
@ -72,6 +76,11 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations
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 {

View File

@ -35,9 +35,6 @@ limitations under the License.
// Create another flexbox of columns to handle large user IDs
display: flex;
flex-direction: column;
// fit the container
flex: 1;
width: calc(100% - 40px); // 40px = 32px theme button + 8px margin to theme button
* {

View File

@ -15,6 +15,7 @@ limitations under the License.
*/
import * as React from "react";
import { createRef } from "react";
import TagPanel from "./TagPanel";
import classNames from "classnames";
import dis from "../../dispatcher/dispatcher";
@ -30,7 +31,9 @@ import RoomBreadcrumbs2 from "../views/rooms/RoomBreadcrumbs2";
import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore";
import { UPDATE_EVENT } from "../../stores/AsyncStore";
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 *
@ -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 listen to the noisy channel to avoid choppy reaction times.
this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize);
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
}
public componentWillUnmount() {
BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate);
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 => {
this.setState({searchFilter: term});
};
@ -149,16 +171,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
// 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;
}
const avatarSize = 32; // should match border-radius of the avatar
let breadcrumbs;
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 = (
<span className="mx_LeftPanel2_headerButtons">
<UserMenuButton />
@ -186,8 +199,8 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
<span className="mx_LeftPanel2_userAvatarContainer">
<BaseAvatar
idName={MatrixClientPeg.get().getUserId()}
name={displayName}
url={avatarUrl}
name={OwnProfileStore.instance.displayName || MatrixClientPeg.get().getUserId()}
url={OwnProfileStore.instance.getHttpAvatarUrl(avatarSize)}
width={avatarSize}
height={avatarSize}
resizeMethod="crop"

View File

@ -15,7 +15,6 @@ limitations under the License.
*/
import * as React from "react";
import {User} from "matrix-js-sdk/src/models/user";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import defaultDispatcher from "../../dispatcher/dispatcher";
import { ActionPayload } from "../../dispatcher/payloads";
@ -34,12 +33,13 @@ import {getHostingLink} from "../../utils/HostingLink";
import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
import SdkConfig from "../../SdkConfig";
import {getHomePageUrl} from "../../utils/pages";
import { OwnProfileStore } from "../../stores/OwnProfileStore";
import { UPDATE_EVENT } from "../../stores/AsyncStore";
interface IProps {
}
interface IState {
user: User;
menuDisplayed: boolean;
isDarkTheme: boolean;
}
@ -54,19 +54,10 @@ export default class UserMenuButton extends React.Component<IProps, IState> {
this.state = {
menuDisplayed: false,
user: MatrixClientPeg.get().getUser(MatrixClientPeg.get().getUserId()),
isDarkTheme: this.isUserOnDarkTheme(),
};
}
private get displayName(): string {
if (MatrixClientPeg.get().isGuest()) {
return _t("Guest");
} else if (this.state.user) {
return this.state.user.displayName;
} else {
return MatrixClientPeg.get().getUserId();
}
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
}
private get hasHomePage(): boolean {
@ -81,6 +72,7 @@ export default class UserMenuButton extends React.Component<IProps, IState> {
public componentWillUnmount() {
if (this.themeWatcherRef) SettingsStore.unwatchSetting(this.themeWatcherRef);
if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef);
OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate);
}
private isUserOnDarkTheme(): boolean {
@ -91,6 +83,12 @@ export default class UserMenuButton extends React.Component<IProps, IState> {
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 = () => {
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_name">
<span className="mx_UserMenuButton_contextMenu_displayName">
{this.displayName}
{OwnProfileStore.instance.displayName}
</span>
<span className="mx_UserMenuButton_contextMenu_userId">
{MatrixClientPeg.get().getUserId()}

View File

@ -422,6 +422,7 @@
"Upgrade your Riot": "Upgrade your Riot",
"A new version of Riot is available!": "A new version of Riot is available!",
"You: %(message)s": "You: %(message)s",
"Guest": "Guest",
"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.",
"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 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",
"Guest": "Guest",
"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|zero": "Uploading %(filename)s",

View 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});
}