New user profile UI in User Settings (#12548)
* New user profile UI in User Settings Using new Edit In Place component. * Show avatar upload error * Fix avatar upload error * Wire up errors & feedback for display name setting * Implement avatar upload / remove progress toast * Add 768px breakpoint * Fix room profile display * Update to released compund-web with required components / fixes * Require compound-web 4.4.0 because we do need it * Update snapshots Because of course all the auto-generated IDs of unrelated things have changed. * Fix duplicate import * Fix CSS comment * Update snapshot * Run all the tests so the ids stay the same * Start of a test for ProfileSettings * More tests * Test that a toast appears * Test ToastRack * Update snapshots * Add the usernamee control * Fix playwright tests * New compound version for editinplace fixes * Fix useId to not just generate a constant ID * Use the label in the username component * Fix widths of test boxes * Update screenshots * Put ^ back on compound-web version * Split CSS for room & user profile settings and name the components correspondingly * Fix playwright test * Update room settings screenshot * Use original screenshot instead * Fix styling of unrelated buttons Needed to be added in other places otherwise the specificity changes. Also put the old screenshots back. * Add copyright year * Fix copyright year
This commit is contained in:
parent
c4c1faff97
commit
cfa322cd62
25 changed files with 919 additions and 307 deletions
|
@ -15,6 +15,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Toast } from "@vector-im/compound-web";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import TabbedView, { Tab, useActiveTabWithDefault } from "../../structures/TabbedView";
|
||||
|
@ -38,6 +39,7 @@ import { UserTab } from "./UserTab";
|
|||
import { NonEmptyArray } from "../../../@types/common";
|
||||
import { SDKContext, SdkContextClass } from "../../../contexts/SDKContext";
|
||||
import { useSettingValue } from "../../../hooks/useSettings";
|
||||
import { ToastContext, useActiveToast } from "../../../contexts/ToastContext";
|
||||
|
||||
interface IProps {
|
||||
initialTabId?: UserTab;
|
||||
|
@ -215,27 +217,34 @@ export default function UserSettingsDialog(props: IProps): JSX.Element {
|
|||
setShowMsc4108QrCode(false);
|
||||
};
|
||||
|
||||
const [activeToast, toastRack] = useActiveToast();
|
||||
|
||||
return (
|
||||
// XXX: SDKContext is provided within the LoggedInView subtree.
|
||||
// Modals function outside the MatrixChat React tree, so sdkContext is reprovided here to simulate that.
|
||||
// The longer term solution is to move our ModalManager into the React tree to inherit contexts properly.
|
||||
<SDKContext.Provider value={props.sdkContext}>
|
||||
<BaseDialog
|
||||
className="mx_UserSettingsDialog"
|
||||
hasCancel={true}
|
||||
onFinished={props.onFinished}
|
||||
title={titleForTabID(activeTabId)}
|
||||
>
|
||||
<div className="mx_SettingsDialog_content">
|
||||
<TabbedView
|
||||
tabs={getTabs()}
|
||||
activeTabId={activeTabId}
|
||||
screenName="UserSettings"
|
||||
onChange={setActiveTabId}
|
||||
responsive={true}
|
||||
/>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
<ToastContext.Provider value={toastRack}>
|
||||
<BaseDialog
|
||||
className="mx_UserSettingsDialog"
|
||||
hasCancel={true}
|
||||
onFinished={props.onFinished}
|
||||
title={titleForTabID(activeTabId)}
|
||||
>
|
||||
<div className="mx_SettingsDialog_content">
|
||||
<TabbedView
|
||||
tabs={getTabs()}
|
||||
activeTabId={activeTabId}
|
||||
screenName="UserSettings"
|
||||
onChange={setActiveTabId}
|
||||
responsive={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_SettingsDialog_toastContainer">
|
||||
{activeToast && <Toast>{activeToast}</Toast>}
|
||||
</div>
|
||||
</BaseDialog>
|
||||
</ToastContext.Provider>
|
||||
</SDKContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -22,14 +22,14 @@ import { _t } from "../../../languageHandler";
|
|||
import { copyPlaintext } from "../../../utils/strings";
|
||||
import AccessibleButton, { ButtonEvent } from "./AccessibleButton";
|
||||
|
||||
interface IProps {
|
||||
interface IProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children?: React.ReactNode;
|
||||
getTextToCopy: () => string | null;
|
||||
border?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const CopyableText: React.FC<IProps> = ({ children, getTextToCopy, border = true, className }) => {
|
||||
const CopyableText: React.FC<IProps> = ({ children, getTextToCopy, border = true, className, ...props }) => {
|
||||
const [tooltip, setTooltip] = useState<string | undefined>(undefined);
|
||||
|
||||
const onCopyClickInternal = async (e: ButtonEvent): Promise<void> => {
|
||||
|
@ -50,7 +50,7 @@ const CopyableText: React.FC<IProps> = ({ children, getTextToCopy, border = true
|
|||
});
|
||||
|
||||
return (
|
||||
<div className={combinedClassName}>
|
||||
<div className={combinedClassName} {...props}>
|
||||
{children}
|
||||
<AccessibleButton
|
||||
title={tooltip ?? _t("action|copy")}
|
||||
|
|
|
@ -202,7 +202,7 @@ export default class RoomProfileSettings extends React.Component<IProps, IState>
|
|||
let profileSettingsButtons;
|
||||
if (this.state.canSetName || this.state.canSetTopic || this.state.canSetAvatar) {
|
||||
profileSettingsButtons = (
|
||||
<div className="mx_ProfileSettings_buttons">
|
||||
<div className="mx_RoomProfileSettings_buttons">
|
||||
<AccessibleButton
|
||||
onClick={this.cancelProfileChanges}
|
||||
kind="primary_outline"
|
||||
|
@ -218,9 +218,9 @@ export default class RoomProfileSettings extends React.Component<IProps, IState>
|
|||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={this.saveProfile} autoComplete="off" noValidate={true} className="mx_ProfileSettings">
|
||||
<div className="mx_ProfileSettings_profile">
|
||||
<div className="mx_ProfileSettings_profile_controls">
|
||||
<form onSubmit={this.saveProfile} autoComplete="off" noValidate={true} className="mx_RoomProfileSettings">
|
||||
<div className="mx_RoomProfileSettings_profile">
|
||||
<div className="mx_RoomProfileSettings_profile_controls">
|
||||
<Field
|
||||
label={_t("room_settings|general|name_field_label")}
|
||||
type="text"
|
||||
|
@ -231,8 +231,8 @@ export default class RoomProfileSettings extends React.Component<IProps, IState>
|
|||
/>
|
||||
<Field
|
||||
className={classNames(
|
||||
"mx_ProfileSettings_profile_controls_topic",
|
||||
"mx_ProfileSettings_profile_controls_topic--room",
|
||||
"mx_RoomProfileSettings_profile_controls_topic",
|
||||
"mx_RoomProfileSettings_profile_controls_topic--room",
|
||||
)}
|
||||
id="profileTopic" // See: NewRoomIntro.tsx
|
||||
label={_t("room_settings|general|topic_field_label")}
|
||||
|
|
|
@ -14,12 +14,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef, useCallback, useEffect, useRef, useState } from "react";
|
||||
import React, { createRef, useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds";
|
||||
import { useId } from "../../../utils/useId";
|
||||
|
||||
interface IProps {
|
||||
/**
|
||||
|
@ -75,9 +76,8 @@ const AvatarSetting: React.FC<IProps> = ({ avatar, avatarAltText, onChange, remo
|
|||
}
|
||||
}, [avatar]);
|
||||
|
||||
// TODO: Use useId() as soon as we're using React 18.
|
||||
// Prevents ID collisions when this component is used more than once on the same page.
|
||||
const a11yId = useRef(`hover-text-${Math.random()}`);
|
||||
const a11yId = useId();
|
||||
|
||||
const onFileChanged = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
|
@ -95,7 +95,7 @@ const AvatarSetting: React.FC<IProps> = ({ avatar, avatarAltText, onChange, remo
|
|||
element="div"
|
||||
onClick={uploadAvatar}
|
||||
className="mx_AvatarSetting_avatarPlaceholder mx_AvatarSetting_avatarDisplay"
|
||||
aria-labelledby={disabled ? undefined : a11yId.current}
|
||||
aria-labelledby={disabled ? undefined : a11yId}
|
||||
// Inhibit tab stop as we have explicit upload/remove buttons
|
||||
tabIndex={-1}
|
||||
/>
|
||||
|
@ -122,7 +122,7 @@ const AvatarSetting: React.FC<IProps> = ({ avatar, avatarAltText, onChange, remo
|
|||
<AccessibleButton
|
||||
onClick={uploadAvatar}
|
||||
className="mx_AvatarSetting_uploadButton"
|
||||
aria-labelledby={a11yId.current}
|
||||
aria-labelledby={a11yId}
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
|
@ -151,7 +151,7 @@ const AvatarSetting: React.FC<IProps> = ({ avatar, avatarAltText, onChange, remo
|
|||
{avatarElement}
|
||||
<div className="mx_AvatarSetting_hover" aria-hidden="true">
|
||||
<div className="mx_AvatarSetting_hoverBg" />
|
||||
{!disabled && <span id={a11yId.current}>{_t("action|upload")}</span>}
|
||||
{!disabled && <span id={a11yId}>{_t("action|upload")}</span>}
|
||||
</div>
|
||||
{uploadAvatarBtn}
|
||||
{removeAvatarBtn}
|
||||
|
|
|
@ -1,198 +0,0 @@
|
|||
/*
|
||||
Copyright 2019 - 2024 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 React, { createRef } from "react";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import Field from "../elements/Field";
|
||||
import { OwnProfileStore } from "../../../stores/OwnProfileStore";
|
||||
import Modal from "../../../Modal";
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import AvatarSetting from "./AvatarSetting";
|
||||
import UserIdentifierCustomisations from "../../../customisations/UserIdentifier";
|
||||
import PosthogTrackers from "../../../PosthogTrackers";
|
||||
import { SettingsSubsectionHeading } from "./shared/SettingsSubsectionHeading";
|
||||
|
||||
interface IState {
|
||||
originalDisplayName: string;
|
||||
displayName: string;
|
||||
originalAvatarUrl: string | null;
|
||||
avatarFile?: File | null;
|
||||
// If true, the user has indicated that they wish to remove the avatar and this should happen on save.
|
||||
avatarRemovalPending: boolean;
|
||||
enableProfileSave?: boolean;
|
||||
}
|
||||
|
||||
export default class ProfileSettings extends React.Component<{}, IState> {
|
||||
private readonly userId: string;
|
||||
private avatarUpload: React.RefObject<HTMLInputElement> = createRef();
|
||||
|
||||
public constructor(props: {}) {
|
||||
super(props);
|
||||
|
||||
this.userId = MatrixClientPeg.safeGet().getSafeUserId();
|
||||
const avatarUrl = OwnProfileStore.instance.avatarMxc;
|
||||
this.state = {
|
||||
originalDisplayName: OwnProfileStore.instance.displayName ?? "",
|
||||
displayName: OwnProfileStore.instance.displayName ?? "",
|
||||
originalAvatarUrl: avatarUrl,
|
||||
avatarFile: null,
|
||||
avatarRemovalPending: false,
|
||||
enableProfileSave: false,
|
||||
};
|
||||
}
|
||||
|
||||
private onChange = (file: File): void => {
|
||||
PosthogTrackers.trackInteraction("WebProfileSettingsAvatarUploadButton");
|
||||
this.setState({
|
||||
avatarFile: file,
|
||||
avatarRemovalPending: false,
|
||||
enableProfileSave: true,
|
||||
});
|
||||
};
|
||||
|
||||
private removeAvatar = (): void => {
|
||||
// clear file upload field so same file can be selected
|
||||
if (this.avatarUpload.current) {
|
||||
this.avatarUpload.current.value = "";
|
||||
}
|
||||
this.setState({
|
||||
avatarFile: null,
|
||||
avatarRemovalPending: true,
|
||||
enableProfileSave: true,
|
||||
});
|
||||
};
|
||||
|
||||
private cancelProfileChanges = async (e: ButtonEvent): Promise<void> => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.state.enableProfileSave) return;
|
||||
this.setState({
|
||||
enableProfileSave: false,
|
||||
displayName: this.state.originalDisplayName,
|
||||
avatarFile: null,
|
||||
avatarRemovalPending: false,
|
||||
});
|
||||
};
|
||||
|
||||
private saveProfile = async (e: ButtonEvent): Promise<void> => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.state.enableProfileSave) return;
|
||||
this.setState({ enableProfileSave: false });
|
||||
|
||||
const newState: Partial<IState> = {};
|
||||
|
||||
const displayName = this.state.displayName.trim();
|
||||
try {
|
||||
const client = MatrixClientPeg.safeGet();
|
||||
if (this.state.originalDisplayName !== this.state.displayName) {
|
||||
await client.setDisplayName(displayName);
|
||||
newState.originalDisplayName = displayName;
|
||||
newState.displayName = displayName;
|
||||
}
|
||||
|
||||
if (this.state.avatarFile) {
|
||||
logger.log(
|
||||
`Uploading new avatar, ${this.state.avatarFile.name} of type ${this.state.avatarFile.type},` +
|
||||
` (${this.state.avatarFile.size}) bytes`,
|
||||
);
|
||||
const { content_uri: uri } = await client.uploadContent(this.state.avatarFile);
|
||||
await client.setAvatarUrl(uri);
|
||||
newState.originalAvatarUrl = uri;
|
||||
newState.avatarFile = null;
|
||||
} else if (this.state.avatarRemovalPending) {
|
||||
await client.setAvatarUrl(""); // use empty string as Synapse 500s on undefined
|
||||
newState.originalAvatarUrl = null;
|
||||
newState.avatarRemovalPending = false;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.log("Failed to save profile", err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: _t("settings|general|error_saving_profile_title"),
|
||||
description: err instanceof Error ? err.message : _t("settings|general|error_saving_profile"),
|
||||
});
|
||||
}
|
||||
|
||||
this.setState<any>(newState);
|
||||
};
|
||||
|
||||
private onDisplayNameChanged = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
displayName: e.target.value,
|
||||
enableProfileSave: true,
|
||||
});
|
||||
};
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const userIdentifier = UserIdentifierCustomisations.getDisplayUserIdentifier(this.userId, {
|
||||
withDisplayName: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={this.saveProfile} autoComplete="off" noValidate={true} className="mx_ProfileSettings">
|
||||
<div className="mx_ProfileSettings_profile">
|
||||
<div className="mx_ProfileSettings_profile_controls">
|
||||
<SettingsSubsectionHeading heading={_t("common|profile")} />
|
||||
<Field
|
||||
label={_t("common|display_name")}
|
||||
type="text"
|
||||
value={this.state.displayName}
|
||||
autoComplete="off"
|
||||
onChange={this.onDisplayNameChanged}
|
||||
/>
|
||||
<p>
|
||||
{userIdentifier && (
|
||||
<span className="mx_ProfileSettings_profile_controls_userId">{userIdentifier}</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<AvatarSetting
|
||||
avatar={
|
||||
this.state.avatarRemovalPending
|
||||
? undefined
|
||||
: this.state.avatarFile ?? this.state.originalAvatarUrl ?? undefined
|
||||
}
|
||||
avatarAltText={_t("common|user_avatar")}
|
||||
onChange={this.onChange}
|
||||
removeAvatar={this.removeAvatar}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_ProfileSettings_buttons">
|
||||
<AccessibleButton
|
||||
onClick={this.cancelProfileChanges}
|
||||
kind="primary_outline"
|
||||
disabled={!this.state.enableProfileSave}
|
||||
>
|
||||
{_t("action|cancel")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
onClick={this.saveProfile}
|
||||
kind="primary"
|
||||
disabled={!this.state.enableProfileSave}
|
||||
>
|
||||
{_t("action|save")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
181
src/components/views/settings/UserProfileSettings.tsx
Normal file
181
src/components/views/settings/UserProfileSettings.tsx
Normal file
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
Copyright 2019 - 2024 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 React, { ChangeEvent, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { EditInPlace, Alert } from "@vector-im/compound-web";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { OwnProfileStore } from "../../../stores/OwnProfileStore";
|
||||
import AvatarSetting from "./AvatarSetting";
|
||||
import PosthogTrackers from "../../../PosthogTrackers";
|
||||
import { formatBytes } from "../../../utils/FormattingUtils";
|
||||
import { useToastContext } from "../../../contexts/ToastContext";
|
||||
import InlineSpinner from "../elements/InlineSpinner";
|
||||
import UserIdentifierCustomisations from "../../../customisations/UserIdentifier";
|
||||
import { useId } from "../../../utils/useId";
|
||||
import CopyableText from "../elements/CopyableText";
|
||||
|
||||
const SpinnerToast: React.FC = ({ children }) => (
|
||||
<>
|
||||
<InlineSpinner />
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
|
||||
interface UsernameBoxProps {
|
||||
username: string;
|
||||
}
|
||||
|
||||
const UsernameBox: React.FC<UsernameBoxProps> = ({ username }) => {
|
||||
const labelId = useId();
|
||||
return (
|
||||
<div className="mx_UserProfileSettings_profile_controls_userId">
|
||||
<div className="mx_UserProfileSettings_profile_controls_userId_label" id={labelId}>
|
||||
{_t("settings|general|username")}
|
||||
</div>
|
||||
<CopyableText getTextToCopy={() => username} aria-labelledby={labelId}>
|
||||
{username}
|
||||
</CopyableText>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* A group of settings views to allow the user to set their profile information.
|
||||
*/
|
||||
const UserProfileSettings: React.FC = () => {
|
||||
const [avatarURL, setAvatarURL] = useState(OwnProfileStore.instance.avatarMxc);
|
||||
const [displayName, setDisplayName] = useState(OwnProfileStore.instance.displayName ?? "");
|
||||
const [initialDisplayName, setInitialDisplayName] = useState(OwnProfileStore.instance.displayName ?? "");
|
||||
const [avatarError, setAvatarError] = useState<boolean>(false);
|
||||
const [maxUploadSize, setMaxUploadSize] = useState<number | undefined>();
|
||||
const [displayNameError, setDisplayNameError] = useState<boolean>(false);
|
||||
|
||||
const toastRack = useToastContext();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const mediaConfig = await MatrixClientPeg.safeGet().getMediaConfig();
|
||||
setMaxUploadSize(mediaConfig["m.upload.size"]);
|
||||
} catch (e) {
|
||||
logger.warn("Failed to get media config", e);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const onAvatarRemove = useCallback(async () => {
|
||||
const removeToast = toastRack.displayToast(
|
||||
<SpinnerToast>{_t("settings|general|avatar_remove_progress")}</SpinnerToast>,
|
||||
);
|
||||
try {
|
||||
await MatrixClientPeg.safeGet().setAvatarUrl(""); // use empty string as Synapse 500s on undefined
|
||||
setAvatarURL("");
|
||||
} finally {
|
||||
removeToast();
|
||||
}
|
||||
}, [toastRack]);
|
||||
|
||||
const onAvatarChange = useCallback(
|
||||
async (avatarFile: File) => {
|
||||
PosthogTrackers.trackInteraction("WebProfileSettingsAvatarUploadButton");
|
||||
logger.log(
|
||||
`Uploading new avatar, ${avatarFile.name} of type ${avatarFile.type}, (${avatarFile.size}) bytes`,
|
||||
);
|
||||
const removeToast = toastRack.displayToast(
|
||||
<SpinnerToast>{_t("settings|general|avatar_save_progress")}</SpinnerToast>,
|
||||
);
|
||||
try {
|
||||
setAvatarError(false);
|
||||
const client = MatrixClientPeg.safeGet();
|
||||
const { content_uri: uri } = await client.uploadContent(avatarFile);
|
||||
await client.setAvatarUrl(uri);
|
||||
setAvatarURL(uri);
|
||||
} catch (e) {
|
||||
setAvatarError(true);
|
||||
} finally {
|
||||
removeToast();
|
||||
}
|
||||
},
|
||||
[toastRack],
|
||||
);
|
||||
|
||||
const onDisplayNameChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
setDisplayName(e.target.value);
|
||||
}, []);
|
||||
|
||||
const onDisplayNameCancel = useCallback(() => {
|
||||
setDisplayName(OwnProfileStore.instance.displayName ?? "");
|
||||
}, []);
|
||||
|
||||
const onDisplayNameSave = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
setDisplayNameError(false);
|
||||
await MatrixClientPeg.safeGet().setDisplayName(displayName);
|
||||
setInitialDisplayName(displayName);
|
||||
} catch (e) {
|
||||
setDisplayNameError(true);
|
||||
}
|
||||
}, [displayName]);
|
||||
|
||||
const userIdentifier = useMemo(
|
||||
() =>
|
||||
UserIdentifierCustomisations.getDisplayUserIdentifier(MatrixClientPeg.safeGet().getSafeUserId(), {
|
||||
withDisplayName: true,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx_UserProfileSettings">
|
||||
<h2>{_t("common|profile")}</h2>
|
||||
<div>{_t("settings|general|profile_subtitle")}</div>
|
||||
<div className="mx_UserProfileSettings_profile">
|
||||
<AvatarSetting
|
||||
avatar={avatarURL ?? undefined}
|
||||
avatarAltText={_t("common|user_avatar")}
|
||||
onChange={onAvatarChange}
|
||||
removeAvatar={onAvatarRemove}
|
||||
/>
|
||||
<EditInPlace
|
||||
className="mx_UserProfileSettings_profile_displayName"
|
||||
label={_t("settings|general|display_name")}
|
||||
value={displayName}
|
||||
disableSaveButton={displayName === initialDisplayName}
|
||||
saveButtonLabel={_t("common|save")}
|
||||
cancelButtonLabel={_t("common|cancel")}
|
||||
savedLabel={_t("common|saved")}
|
||||
onChange={onDisplayNameChanged}
|
||||
onCancel={onDisplayNameCancel}
|
||||
onSave={onDisplayNameSave}
|
||||
error={displayNameError ? _t("settings|general|display_name_error") : undefined}
|
||||
/>
|
||||
</div>
|
||||
{avatarError && (
|
||||
<Alert title={_t("settings|general|avatar_upload_error_title")} type="critical">
|
||||
{maxUploadSize === undefined
|
||||
? _t("settings|general|avatar_upload_error_text_generic")
|
||||
: _t("settings|general|avatar_upload_error_text", { size: formatBytes(maxUploadSize) })}
|
||||
</Alert>
|
||||
)}
|
||||
{userIdentifier && <UsernameBox username={userIdentifier} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserProfileSettings;
|
|
@ -22,7 +22,7 @@ import { logger } from "matrix-js-sdk/src/logger";
|
|||
|
||||
import { Icon as WarningIcon } from "../../../../../../res/img/feather-customised/warning-triangle.svg";
|
||||
import { UserFriendlyError, _t } from "../../../../../languageHandler";
|
||||
import ProfileSettings from "../../ProfileSettings";
|
||||
import UserProfileSettings from "../../UserProfileSettings";
|
||||
import * as languageHandler from "../../../../../languageHandler";
|
||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
import LanguageDropdown from "../../../elements/LanguageDropdown";
|
||||
|
@ -561,7 +561,7 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
|
|||
return (
|
||||
<SettingsTab data-testid="mx_GeneralUserSettingsTab">
|
||||
<SettingsSection>
|
||||
<ProfileSettings />
|
||||
<UserProfileSettings />
|
||||
{this.renderAccountSection()}
|
||||
{this.renderLanguageSection()}
|
||||
{supportsMultiLanguageSpellCheck ? this.renderSpellCheckSection() : null}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue