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.
This commit is contained in:
parent
74e4ea7d48
commit
380aed4244
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 {
|
||||
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 {
|
||||
|
|
|
@ -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
|
||||
|
||||
* {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()}
|
||||
|
|
|
@ -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",
|
||||
|
|
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