/* Copyright 2024 New Vector Ltd. Copyright 2019-2024 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ import React, { ChangeEvent, ReactNode, useCallback, useEffect, useMemo, useState } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import { EditInPlace, Alert, ErrorMessage } from "@vector-im/compound-web"; import PopOutIcon from "@vector-im/compound-design-tokens/assets/web/icons/pop-out"; import SignOutIcon from "@vector-im/compound-design-tokens/assets/web/icons/sign-out"; import { _t } from "../../../languageHandler"; 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"; import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; import AccessibleButton from "../elements/AccessibleButton"; import LogoutDialog, { shouldShowLogoutDialog } from "../dialogs/LogoutDialog"; import Modal from "../../../Modal"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import { Flex } from "../../utils/Flex"; const SpinnerToast: React.FC<{ children?: ReactNode }> = ({ children }) => ( <> {children} ); interface UsernameBoxProps { username: string; } const UsernameBox: React.FC = ({ username }) => { const labelId = useId(); return (
{_t("settings|general|username")}
username} aria-labelledby={labelId}> {username}
); }; interface ManageAccountButtonProps { externalAccountManagementUrl: string; } const ManageAccountButton: React.FC = ({ externalAccountManagementUrl }) => ( {_t("settings|general|oidc_manage_button")} ); const SignOutButton: React.FC = () => { const client = useMatrixClientContext(); const onClick = useCallback(async () => { if (await shouldShowLogoutDialog(client)) { Modal.createDialog(LogoutDialog); } else { defaultDispatcher.dispatch({ action: "logout" }); } }, [client]); return ( {_t("action|sign_out")} ); }; interface UserProfileSettingsProps { // The URL to redirect the user to in order to manage their account. externalAccountManagementUrl?: string; // Whether the homeserver allows the user to set their display name. canSetDisplayName: boolean; // Whether the homeserver allows the user to set their avatar. canSetAvatar: boolean; } /** * A group of settings views to allow the user to set their profile information. */ const UserProfileSettings: React.FC = ({ externalAccountManagementUrl, canSetDisplayName, canSetAvatar, }) => { const [avatarURL, setAvatarURL] = useState(OwnProfileStore.instance.avatarMxc); const [displayName, setDisplayName] = useState(OwnProfileStore.instance.displayName ?? ""); const [avatarError, setAvatarError] = useState(false); const [maxUploadSize, setMaxUploadSize] = useState(); const [displayNameError, setDisplayNameError] = useState(false); const toastRack = useToastContext(); const client = useMatrixClientContext(); useEffect(() => { (async () => { try { const mediaConfig = await client.getMediaConfig(); setMaxUploadSize(mediaConfig["m.upload.size"]); } catch (e) { logger.warn("Failed to get media config", e); } })(); }, [client]); const onAvatarRemove = useCallback(async () => { const removeToast = toastRack.displayToast( {_t("settings|general|avatar_remove_progress")}, ); try { await client.setAvatarUrl(""); // use empty string as Synapse 500s on undefined setAvatarURL(""); } finally { removeToast(); } }, [toastRack, client]); 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( {_t("settings|general|avatar_save_progress")}, ); try { setAvatarError(false); const { content_uri: uri } = await client.uploadContent(avatarFile); await client.setAvatarUrl(uri); setAvatarURL(uri); } catch (e) { setAvatarError(true); } finally { removeToast(); } }, [toastRack, client], ); const onDisplayNameChanged = useCallback((e: ChangeEvent) => { setDisplayName(e.target.value); }, []); const onDisplayNameCancel = useCallback(() => { setDisplayName(OwnProfileStore.instance.displayName ?? ""); }, []); const onDisplayNameSave = useCallback(async (): Promise => { try { setDisplayNameError(false); await client.setDisplayName(displayName); } catch (e) { setDisplayNameError(true); throw e; } }, [displayName, client]); const userIdentifier = useMemo( () => UserIdentifierCustomisations.getDisplayUserIdentifier(client.getSafeUserId(), { withDisplayName: true, }), [client], ); const someFieldsDisabled = !canSetDisplayName || !canSetAvatar; return (

{_t("common|profile")}

{someFieldsDisabled ? _t("settings|general|profile_subtitle_oidc") : _t("settings|general|profile_subtitle")}
{displayNameError && {_t("settings|general|display_name_error")}}
{avatarError && ( {maxUploadSize === undefined ? _t("settings|general|avatar_upload_error_text_generic") : _t("settings|general|avatar_upload_error_text", { size: formatBytes(maxUploadSize) })} )} {userIdentifier && } {externalAccountManagementUrl && ( )}
); }; export default UserProfileSettings;