refactor(profile): drop extended-profile editing, keep right-panel display

Extended-profile fields (io.element.profile.*) are no longer edited in the
client, so remove the edit form and the dead code that came with it, keeping
only the read-only right-panel display (UserExtendedProfileInfo).

- Remove ExtendedProfileSettings (edit form) + its Settings integration and CSS
- Remove ExtendedProfileView (never-rendered, dead) + its CSS
- Inline the EXT_PROFILE_KEY_* constants into UserExtendedProfileInfo so it no
  longer depends on the deleted settings component
- Drop now-unused set/deleteExtendedProfileProperty from UserProfilesStore
- Prune dead settings|extended_profile_settings|* i18n keys (and the unused
  user_info.extended_profile header/not_provided keys) across all locales
- Regenerate _components.pcss
- Revert UserProfileSettings/AccountUserSettingsTab tests (+snapshot) to master,
  since their adaptations existed only for the removed edit form
This commit is contained in:
2026-06-06 15:44:51 +02:00
parent 098d5b0ca2
commit 3a6a93edfe
16 changed files with 18 additions and 1042 deletions
-2
View File
@@ -235,7 +235,6 @@
@import "./views/right_panel/_BaseCard.pcss";
@import "./views/right_panel/_EmptyState.pcss";
@import "./views/right_panel/_EncryptionInfo.pcss";
@import "./views/right_panel/_ExtendedProfileView.pcss";
@import "./views/right_panel/_ExtensionsCard.pcss";
@import "./views/right_panel/_PinnedMessagesCard.pcss";
@import "./views/right_panel/_RoomSummaryCard.pcss";
@@ -304,7 +303,6 @@
@import "./views/rooms/wysiwyg_composer/components/_FormattingButtons.pcss";
@import "./views/rooms/wysiwyg_composer/components/_LinkModal.pcss";
@import "./views/settings/_AvatarSetting.pcss";
@import "./views/settings/_ExtendedProfileSettings.pcss";
@import "./views/settings/_FontScalingPanel.pcss";
@import "./views/settings/_ImageSizePanel.pcss";
@import "./views/settings/_IntegrationManager.pcss";
@@ -1,79 +0,0 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
.mx_ExtendedProfileView {
/* Anpassung an das bestehende UserInfo Panel */
font-size: var(--cpd-font-size-body-sm);
padding: 0;
/* Styling für das Accordion */
.mx_ExtendedProfileView_accordion {
width: 100%;
}
/* Abstand zwischen den Feldern */
.mx_ExtendedProfileView_content {
padding: var(--cpd-space-2x) 0;
display: flex;
flex-direction: column;
gap: var(--cpd-space-4x);
}
/* Styling für die Felder */
.mx_ExtendedProfileView_field {
margin-bottom: var(--cpd-space-2x);
.mx_ExtendedProfileView_icon {
width: 20px;
height: 20px;
color: var(--cpd-color-icon-secondary);
flex-shrink: 0;
}
}
/* Labels und Werte */
.mx_ExtendedProfileView_label {
color: var(--cpd-color-text-secondary);
font-size: var(--cpd-font-size-body-xs);
margin-bottom: var(--cpd-space-1x);
}
.mx_ExtendedProfileView_value {
color: var(--cpd-color-text-primary);
word-break: break-word;
&.mx_ExtendedProfileView_multiline {
white-space: pre-line;
margin-top: var(--cpd-space-1x);
}
}
/* Spezielle Styles für Website-Links */
.mx_ExtendedProfileView_link {
color: var(--cpd-color-text-action-accent);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
/* Für Bereich "Weitere Informationen" */
.mx_ExtendedProfileView_further_info {
padding: var(--cpd-space-1x) 0;
}
/* Loading State */
.mx_ExtendedProfileView_loading {
padding: var(--cpd-space-4x) 0;
}
/* Error State */
.mx_ExtendedProfileView_error {
margin: var(--cpd-space-2x) 0;
}
}
@@ -1,76 +0,0 @@
.mx_ExtendedProfileSettings {
margin-top: 24px;
margin-bottom: 16px;
}
.mx_ExtendedProfileSettings h3 {
font-size: 16px;
font-weight: 600;
margin-top: 0;
margin-bottom: 16px;
}
.mx_ExtendedProfileSettings_fields {
display: flex;
flex-direction: column;
gap: 16px;
}
.mx_ExtendedProfileSettings_field {
width: 100%;
margin-bottom: 20px;
}
.mx_ExtendedProfileSettings_fieldsRow {
display: flex;
gap: 16px;
margin-bottom: 20px;
.mx_ExtendedProfileSettings_field {
flex: 1;
margin-bottom: 0;
}
}
/* Business contact field specific layouts */
.mx_ExtendedProfileSettings_address_row {
.mx_ExtendedProfileSettings_field_business_street {
flex: 1;
}
.mx_ExtendedProfileSettings_field_business_street_nr {
flex: 1;
}
}
.mx_ExtendedProfileSettings_postal_row {
.mx_ExtendedProfileSettings_field_business_plz {
flex: 1;
}
.mx_ExtendedProfileSettings_field_business_city {
flex: 1;
}
}
.mx_ExtendedProfileSettings_contact_row {
.mx_ExtendedProfileSettings_field_business_email {
flex: 1;
}
.mx_ExtendedProfileSettings_field_business_tel {
flex: 1;
}
}
.mx_ExtendedProfileSettings_buttons {
display: flex;
gap: 8px;
justify-content: flex-end;
margin-top: 8px;
}
.mx_ExtendedProfileSettings_multiline textarea {
min-height: 200px;
resize: vertical;
}
@@ -1,193 +0,0 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React, { useCallback, useEffect, useState } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { Alert } from "@vector-im/compound-web";
import GlobeIcon from "@vector-im/compound-design-tokens/assets/web/icons/web-browser";
import LocationIcon from "@vector-im/compound-design-tokens/assets/web/icons/location-pin";
import BadgeIcon from "@vector-im/compound-design-tokens/assets/web/icons/user-profile";
import StarIcon from "@vector-im/compound-design-tokens/assets/web/icons/favourite";
import { _t } from "../../../languageHandler";
import { SdkContextClass } from "../../../contexts/SDKContext";
import { Container } from "../right_panel/UserInfo";
import InlineSpinner from "../elements/InlineSpinner";
import {
EXT_PROFILE_KEY_FURTHER_INFO,
EXT_PROFILE_KEY_SPECIALIZATION,
EXT_PROFILE_KEY_TITLE,
EXT_PROFILE_KEY_WEBSITE,
} from "../settings/ExtendedProfileSettings";
import { Flex } from "@element-hq/web-shared-components";
interface ExtendedProfileViewProps {
userId: string;
}
type ExtendedProfileData = {
title: string | null;
specialization: string | null;
address: string | null;
website: string | null;
furtherInfo: string | null;
};
/**
* Komponente zur Anzeige erweiterter Profilfelder eines anderen Benutzers
*/
const ExtendedProfileView: React.FC<ExtendedProfileViewProps> = ({ userId }) => {
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<boolean>(false);
const [profileData, setProfileData] = useState<ExtendedProfileData | null>(null);
const [hasData, setHasData] = useState<boolean>(false);
//const client = useMatrixClientContext();
const userProfilesStore = SdkContextClass.instance.userProfilesStore;
// Laden der erweiterten Profilfelder für den angegebenen Benutzer
const fetchExtendedProfileData = useCallback(async () => {
if (!userId) return;
setLoading(true);
setError(false);
try {
const [
titleValue,
specializationValue,
websiteValue,
furtherInfoValue,
] = await Promise.all([
userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_TITLE),
userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_SPECIALIZATION),
userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_WEBSITE),
userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_FURTHER_INFO),
]);
const data: ExtendedProfileData = {
title: titleValue as string | null,
specialization: specializationValue as string | null,
address: null, // Address not yet implemented
website: websiteValue as string | null,
furtherInfo: furtherInfoValue as string | null,
};
setProfileData(data);
// Prüfen, ob mindestens ein Feld ausgefüllt ist
const hasAnyData = Object.values(data).some((value) => value !== null && value !== "");
setHasData(hasAnyData);
} catch (e) {
logger.error("Fehler beim Laden der erweiterten Profildaten", e);
setError(true);
} finally {
setLoading(false);
}
}, [userId, userProfilesStore]);
useEffect(() => {
fetchExtendedProfileData();
}, [fetchExtendedProfileData]);
// Wenn keine Daten vorhanden sind, zeigen wir nichts an
if (!loading && !hasData && !error) {
return null;
}
return (
<Container className="mx_ExtendedProfileView">
{loading ? (
<Flex justify="center" align="center" className="mx_ExtendedProfileView_loading">
<InlineSpinner />
</Flex>
) : error ? (
<Alert
type="critical"
title={_t("common|error")}
className="mx_ExtendedProfileView_error"
>
{_t("settings|extended_profile_settings|failed_to_load")}
</Alert>
) : hasData ? (
<div className="mx_ExtendedProfileView_content">
{(profileData?.title || profileData?.specialization) && (
<div className="mx_ExtendedProfileView_section mx_ExtendedProfileView_professional">
{profileData.title && (
<div className="mx_ExtendedProfileView_field">
<Flex align="center" gap="var(--cpd-space-2x)">
<BadgeIcon className="mx_ExtendedProfileView_icon" />
<div>
<div className="mx_ExtendedProfileView_label">{_t("settings|extended_profile_settings|professional_title")}</div>
<div className="mx_ExtendedProfileView_value">{profileData.title}</div>
</div>
</Flex>
</div>
)}
{profileData.specialization && (
<div className="mx_ExtendedProfileView_field">
<Flex align="center" gap="var(--cpd-space-2x)">
<StarIcon className="mx_ExtendedProfileView_icon" />
<div>
<div className="mx_ExtendedProfileView_label">{_t("settings|extended_profile_settings|specialization")}</div>
<div className="mx_ExtendedProfileView_value">{profileData.specialization}</div>
</div>
</Flex>
</div>
)}
</div>
)}
{profileData?.address && (
<div className="mx_ExtendedProfileView_field">
<Flex align="center" gap="var(--cpd-space-2x)">
<LocationIcon className="mx_ExtendedProfileView_icon" />
<div>
<div className="mx_ExtendedProfileView_label">{_t("settings|extended_profile_settings|business_city")}</div>
<div className="mx_ExtendedProfileView_value">{profileData.address}</div>
</div>
</Flex>
</div>
)}
{profileData?.website && (
<div className="mx_ExtendedProfileView_field">
<Flex align="center" gap="var(--cpd-space-2x)">
<GlobeIcon className="mx_ExtendedProfileView_icon" />
<div>
<div className="mx_ExtendedProfileView_label">{_t("settings|extended_profile_settings|website")}</div>
<div className="mx_ExtendedProfileView_value">
<a
href={profileData.website.startsWith("http") ? profileData.website : `https://${profileData.website}`}
target="_blank"
rel="noreferrer noopener"
className="mx_ExtendedProfileView_link"
>
{profileData.website}
</a>
</div>
</div>
</Flex>
</div>
)}
{profileData?.furtherInfo && (
<div className="mx_ExtendedProfileView_field">
<div className="mx_ExtendedProfileView_further_info">
<div className="mx_ExtendedProfileView_label">{_t("settings|extended_profile_settings|further_information")}</div>
<div className="mx_ExtendedProfileView_value mx_ExtendedProfileView_multiline">{profileData.furtherInfo}</div>
</div>
</div>
)}
</div>
) : null}
</Container>
);
};
export default ExtendedProfileView;
@@ -11,20 +11,19 @@ import { Text } from "@vector-im/compound-web";
import { SdkContextClass } from "../../../contexts/SDKContext";
import Spinner from "../elements/Spinner";
import { _t } from "../../../languageHandler";
// Extended profile property keys
import {
EXT_PROFILE_KEY_TITLE,
EXT_PROFILE_KEY_SPECIALIZATION,
EXT_PROFILE_KEY_PRACTICE_NAME,
EXT_PROFILE_KEY_BUSINESS_STREET,
EXT_PROFILE_KEY_BUSINESS_STREET_NR,
EXT_PROFILE_KEY_BUSINESS_CITY,
EXT_PROFILE_KEY_BUSINESS_PLZ,
EXT_PROFILE_KEY_BUSINESS_EMAIL,
EXT_PROFILE_KEY_BUSINESS_TEL,
EXT_PROFILE_KEY_WEBSITE,
EXT_PROFILE_KEY_FURTHER_INFO,
} from "../settings/ExtendedProfileSettings";
// Extended profile property keys (read-only display in the right panel).
const EXT_PROFILE_KEY_TITLE = "io.element.profile.title";
const EXT_PROFILE_KEY_SPECIALIZATION = "io.element.profile.specialization";
const EXT_PROFILE_KEY_WEBSITE = "io.element.profile.website";
const EXT_PROFILE_KEY_FURTHER_INFO = "io.element.profile.further_info";
const EXT_PROFILE_KEY_PRACTICE_NAME = "io.element.profile.practice_name";
const EXT_PROFILE_KEY_BUSINESS_STREET = "io.element.profile.business_street";
const EXT_PROFILE_KEY_BUSINESS_STREET_NR = "io.element.profile.business_street_nr";
const EXT_PROFILE_KEY_BUSINESS_CITY = "io.element.profile.business_city";
const EXT_PROFILE_KEY_BUSINESS_PLZ = "io.element.profile.business_plz";
const EXT_PROFILE_KEY_BUSINESS_EMAIL = "io.element.profile.business_email";
const EXT_PROFILE_KEY_BUSINESS_TEL = "io.element.profile.business_tel";
// Helper function to preserve line breaks
const renderMultilineText = (text: string): React.ReactNode[] => {
@@ -60,7 +59,7 @@ const UserExtendedProfileInfo: React.FC<UserExtendedProfileInfoProps> = ({ userI
const [error, setError] = useState<boolean>(false);
useEffect(() => {
const fetchProfileData = async () => {
const fetchProfileData = async (): Promise<void> => {
try {
setError(false);
setLoading(true);
@@ -1,536 +0,0 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React, { type ChangeEvent, useCallback, useEffect, useState } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { Alert } from "@vector-im/compound-web";
import classNames from "classnames";
import { _t } from "../../../languageHandler";
import { useToastContext } from "../../../contexts/ToastContext";
import InlineSpinner from "../elements/InlineSpinner";
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
import { SdkContextClass } from "../../../contexts/SDKContext";
import Field from "../elements/Field";
import AccessibleButton from "../elements/AccessibleButton";
const SpinnerToast: React.FC<{ children?: React.ReactNode }> = ({ children }) => (
<>
<InlineSpinner />
{children}
</>
);
// Extended profile property keys
export const EXT_PROFILE_KEY_TITLE = "io.element.profile.title";
export const EXT_PROFILE_KEY_SPECIALIZATION = "io.element.profile.specialization";
export const EXT_PROFILE_KEY_WEBSITE = "io.element.profile.website";
export const EXT_PROFILE_KEY_FURTHER_INFO = "io.element.profile.further_info";
export const EXT_PROFILE_KEY_PRACTICE_NAME = "io.element.profile.practice_name";
export const EXT_PROFILE_KEY_BUSINESS_STREET = "io.element.profile.business_street";
export const EXT_PROFILE_KEY_BUSINESS_STREET_NR = "io.element.profile.business_street_nr";
export const EXT_PROFILE_KEY_BUSINESS_CITY = "io.element.profile.business_city";
export const EXT_PROFILE_KEY_BUSINESS_PLZ = "io.element.profile.business_plz";
export const EXT_PROFILE_KEY_BUSINESS_EMAIL = "io.element.profile.business_email";
export const EXT_PROFILE_KEY_BUSINESS_TEL = "io.element.profile.business_tel";
// Unstable feature key for extended profiles
export const UNSTABLE_MSC4133_EXTENDED_PROFILES = "org.matrix.msc4133.extended_profiles";
interface ExtendedProfileSettingsProps {
className?: string;
}
/**
* Component for managing extended profile fields for medical professionals.
* Allows editing title, specialization, website, and further information.
*/
const ExtendedProfileSettings: React.FC<ExtendedProfileSettingsProps> = ({ className }) => {
const [title, setTitle] = useState<string>("");
const [specialization, setSpecialization] = useState<string>("");
const [website, setWebsite] = useState<string>("");
const [furtherInfo, setFurtherInfo] = useState<string>("");
const [practiceName, setPracticeName] = useState<string>("");
// Business contact field states
const [businessStreet, setBusinessStreet] = useState<string>("");
const [businessStreetNr, setBusinessStreetNr] = useState<string>("");
const [businessCity, setBusinessCity] = useState<string>("");
const [businessPlz, setBusinessPlz] = useState<string>("");
const [businessEmail, setBusinessEmail] = useState<string>("");
const [businessTel, setBusinessTel] = useState<string>("");
// Validation states for email and phone
const [businessEmailValid, setBusinessEmailValid] = useState<boolean>(true);
const [businessTelValid, setBusinessTelValid] = useState<boolean>(true);
// Store original values for cancel functionality
const [originalTitle, setOriginalTitle] = useState<string>("");
const [originalSpecialization, setOriginalSpecialization] = useState<string>("");
const [originalWebsite, setOriginalWebsite] = useState<string>("");
const [originalFurtherInfo, setOriginalFurtherInfo] = useState<string>("");
const [originalPracticeName, setOriginalPracticeName] = useState<string>("");
// Original business contact field values
const [originalBusinessStreet, setOriginalBusinessStreet] = useState<string>("");
const [originalBusinessStreetNr, setOriginalBusinessStreetNr] = useState<string>("");
const [originalBusinessCity, setOriginalBusinessCity] = useState<string>("");
const [originalBusinessPlz, setOriginalBusinessPlz] = useState<string>("");
const [originalBusinessEmail, setOriginalBusinessEmail] = useState<string>("");
const [originalBusinessTel, setOriginalBusinessTel] = useState<string>("");
const [isModified, setIsModified] = useState<boolean>(false);
const [isSaving, setIsSaving] = useState<boolean>(false);
const [saveError, setSaveError] = useState<boolean>(false);
const [loadError, setLoadError] = useState<boolean>(false);
const toastRack = useToastContext();
const client = useMatrixClientContext();
const userProfilesStore = SdkContextClass.instance.userProfilesStore;
const userId = client.getSafeUserId();
// Load extended profile data without checking server support
useEffect(() => {
const loadExtendedProfileData = async () => {
try {
setLoadError(false);
// Try to get existing data
try {
const titleValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_TITLE);
const specializationValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_SPECIALIZATION);
const websiteValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_WEBSITE);
const furtherInfoValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_FURTHER_INFO);
const practiceNameValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_PRACTICE_NAME);
// Fetch business contact fields
const businessStreetValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_BUSINESS_STREET);
const businessStreetNrValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_BUSINESS_STREET_NR);
const businessCityValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_BUSINESS_CITY);
const businessPlzValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_BUSINESS_PLZ);
const businessEmailValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_BUSINESS_EMAIL);
const businessTelValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_BUSINESS_TEL);
const titleStr = titleValue as string || "";
const specializationStr = specializationValue as string || "";
const websiteStr = websiteValue as string || "";
const furtherInfoStr = furtherInfoValue as string || "";
const practiceNameStr = practiceNameValue as string || "";
// Convert business contact field values to strings
const businessStreetStr = businessStreetValue as string || "";
const businessStreetNrStr = businessStreetNrValue as string || "";
const businessCityStr = businessCityValue as string || "";
const businessPlzStr = businessPlzValue as string || "";
const businessEmailStr = businessEmailValue as string || "";
const businessTelStr = businessTelValue as string || "";
setTitle(titleStr);
setSpecialization(specializationStr);
setWebsite(websiteStr);
setFurtherInfo(furtherInfoStr);
setPracticeName(practiceNameStr);
// Set business contact fields
setBusinessStreet(businessStreetStr);
setBusinessStreetNr(businessStreetNrStr);
setBusinessCity(businessCityStr);
setBusinessPlz(businessPlzStr);
setBusinessEmail(businessEmailStr);
setBusinessTel(businessTelStr);
// Store original values for cancel functionality
setOriginalTitle(titleStr);
setOriginalSpecialization(specializationStr);
setOriginalWebsite(websiteStr);
setOriginalFurtherInfo(furtherInfoStr);
setOriginalPracticeName(practiceNameStr);
// Store original business contact field values
setOriginalBusinessStreet(businessStreetStr);
setOriginalBusinessStreetNr(businessStreetNrStr);
setOriginalBusinessCity(businessCityStr);
setOriginalBusinessPlz(businessPlzStr);
setOriginalBusinessEmail(businessEmailStr);
setOriginalBusinessTel(businessTelStr);
} catch (e) {
// Just log the error but don't set loadError to true
// as we want the fields to show up even when empty
logger.warn("Failed to fetch extended profile properties", e);
}
} catch (e) {
logger.error("Failed to load extended profile data", e);
setLoadError(true);
}
};
loadExtendedProfileData();
}, [userId, userProfilesStore, client]);
// Check if any field has been modified
useEffect(() => {
const hasChanges =
title !== originalTitle ||
specialization !== originalSpecialization ||
website !== originalWebsite ||
furtherInfo !== originalFurtherInfo ||
practiceName !== originalPracticeName ||
businessStreet !== originalBusinessStreet ||
businessStreetNr !== originalBusinessStreetNr ||
businessCity !== originalBusinessCity ||
businessPlz !== originalBusinessPlz ||
businessEmail !== originalBusinessEmail ||
businessTel !== originalBusinessTel;
setIsModified(hasChanges);
}, [
title, specialization, website, furtherInfo, practiceName,
originalTitle, originalSpecialization, originalWebsite, originalFurtherInfo, originalPracticeName,
businessStreet, businessStreetNr, businessCity, businessPlz, businessEmail, businessTel,
originalBusinessStreet, originalBusinessStreetNr, originalBusinessCity, originalBusinessPlz,
originalBusinessEmail, originalBusinessTel
]);
// Field change handlers
const onTitleChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setTitle(e.target.value);
}, []);
const onSpecializationChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setSpecialization(e.target.value);
}, []);
const onWebsiteChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setWebsite(e.target.value);
}, []);
const onFurtherInfoChanged = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
setFurtherInfo(e.target.value);
}, []);
const onPracticeNameChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setPracticeName(e.target.value);
}, []);
// Business contact field change handlers
const onBusinessStreetChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setBusinessStreet(e.target.value);
}, []);
const onBusinessStreetNrChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setBusinessStreetNr(e.target.value);
}, []);
const onBusinessCityChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setBusinessCity(e.target.value);
}, []);
const onBusinessPlzChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setBusinessPlz(e.target.value);
}, []);
// Email validation with simple regex
const onBusinessEmailChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setBusinessEmail(value);
// Email format validation - basic check for something@something.something
const emailValid = value === "" || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
setBusinessEmailValid(emailValid);
}, []);
// Phone validation
const onBusinessTelChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setBusinessTel(value);
// Phone format validation - allow digits, spaces, plus, minus, parentheses
const phoneValid = value === "" || /^[0-9+\-() ]+$/.test(value);
setBusinessTelValid(phoneValid);
}, []);
// Cancel handler - restore all original values
const onCancel = useCallback(() => {
setTitle(originalTitle);
setSpecialization(originalSpecialization);
setWebsite(originalWebsite);
setFurtherInfo(originalFurtherInfo);
setPracticeName(originalPracticeName);
// Reset business contact fields to original values
setBusinessStreet(originalBusinessStreet);
setBusinessStreetNr(originalBusinessStreetNr);
setBusinessCity(originalBusinessCity);
setBusinessPlz(originalBusinessPlz);
setBusinessEmail(originalBusinessEmail);
setBusinessTel(originalBusinessTel);
// Reset validation states
setBusinessEmailValid(true);
setBusinessTelValid(true);
}, [
originalTitle, originalSpecialization, originalWebsite, originalFurtherInfo, originalPracticeName,
originalBusinessStreet, originalBusinessStreetNr, originalBusinessCity, originalBusinessPlz,
originalBusinessEmail, originalBusinessTel
]);
// Save handler - save all fields at once
const onSave = useCallback(async (): Promise<void> => {
if (!isModified) return;
// Validate email and phone fields before saving
if (!businessEmailValid || !businessTelValid) {
setSaveError(true);
return;
}
setIsSaving(true);
setSaveError(false);
const removeToast = toastRack.displayToast(
<SpinnerToast>{_t("settings|extended_profile_settings|saving_profile")}</SpinnerToast>
);
try {
// Save all modified fields
const savePromises = [];
if (title !== originalTitle) {
savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_TITLE, title));
}
if (specialization !== originalSpecialization) {
savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_SPECIALIZATION, specialization));
}
if (website !== originalWebsite) {
savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_WEBSITE, website));
}
if (furtherInfo !== originalFurtherInfo) {
savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_FURTHER_INFO, furtherInfo));
}
if (practiceName !== originalPracticeName) {
savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_PRACTICE_NAME, practiceName));
}
// Save business contact fields if modified
if (businessStreet !== originalBusinessStreet) {
savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_BUSINESS_STREET, businessStreet));
}
if (businessStreetNr !== originalBusinessStreetNr) {
savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_BUSINESS_STREET_NR, businessStreetNr));
}
if (businessCity !== originalBusinessCity) {
savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_BUSINESS_CITY, businessCity));
}
if (businessPlz !== originalBusinessPlz) {
savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_BUSINESS_PLZ, businessPlz));
}
if (businessEmail !== originalBusinessEmail) {
savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_BUSINESS_EMAIL, businessEmail));
}
if (businessTel !== originalBusinessTel) {
savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_BUSINESS_TEL, businessTel));
}
await Promise.all(savePromises);
// Update original values after successful save
setOriginalTitle(title);
setOriginalSpecialization(specialization);
setOriginalWebsite(website);
setOriginalFurtherInfo(furtherInfo);
setOriginalPracticeName(practiceName);
// Update original business contact field values
setOriginalBusinessStreet(businessStreet);
setOriginalBusinessStreetNr(businessStreetNr);
setOriginalBusinessCity(businessCity);
setOriginalBusinessPlz(businessPlz);
setOriginalBusinessEmail(businessEmail);
setOriginalBusinessTel(businessTel);
} catch (e) {
logger.error("Failed to save profile information", e);
setSaveError(true);
throw e;
} finally {
setIsSaving(false);
removeToast();
}
}, [
isModified, title, specialization, website, furtherInfo, practiceName,
originalTitle, originalSpecialization, originalWebsite, originalFurtherInfo, originalPracticeName,
businessStreet, businessStreetNr, businessCity, businessPlz, businessEmail, businessTel,
originalBusinessStreet, originalBusinessStreetNr, originalBusinessCity, originalBusinessPlz,
originalBusinessEmail, originalBusinessTel, businessEmailValid, businessTelValid,
toastRack, userProfilesStore
]);
return (
<div className={classNames("mx_ExtendedProfileSettings", className)}>
<div className="mx_ExtendedProfileSettings_section">
<h3 className="mx_ExtendedProfileSettings_heading">{_t("settings|extended_profile_settings|additional_information")}</h3>
{loadError && (
<Alert title={_t("common|error")} type="critical">
{_t("settings|extended_profile_settings|failed_to_load")}
</Alert>
)}
<div className="mx_ExtendedProfileSettings_fields">
<div className="mx_ExtendedProfileSettings_fieldsRow">
<div className="mx_ExtendedProfileSettings_field mx_ExtendedProfileSettings_field_title">
<Field
label={_t("settings|extended_profile_settings|professional_title")}
value={title}
onChange={onTitleChanged}
disabled={isSaving}
autoComplete="off"
/>
</div>
<div className="mx_ExtendedProfileSettings_field mx_ExtendedProfileSettings_field_specialization">
<Field
label={_t("settings|extended_profile_settings|specialization")}
value={specialization}
onChange={onSpecializationChanged}
disabled={isSaving}
autoComplete="off"
/>
</div>
</div>
<div className="mx_ExtendedProfileSettings_field">
<Field
label={_t("settings|extended_profile_settings|practice_name")}
value={practiceName}
onChange={onPracticeNameChanged}
disabled={isSaving}
autoComplete="off"
/>
</div>
{/* Street and number in one row, street takes 2/3 width */}
<div className="mx_ExtendedProfileSettings_fieldsRow mx_ExtendedProfileSettings_address_row">
<div className="mx_ExtendedProfileSettings_field mx_ExtendedProfileSettings_field_business_street">
<Field
label={_t("settings|extended_profile_settings|business_street")}
value={businessStreet}
onChange={onBusinessStreetChanged}
disabled={isSaving}
autoComplete="off"
/>
</div>
<div className="mx_ExtendedProfileSettings_field mx_ExtendedProfileSettings_field_business_street_nr">
<Field
label={_t("settings|extended_profile_settings|business_street_nr")}
value={businessStreetNr}
onChange={onBusinessStreetNrChanged}
disabled={isSaving}
autoComplete="off"
/>
</div>
</div>
{/* PLZ and City in one row, PLZ takes 1/3 width */}
<div className="mx_ExtendedProfileSettings_fieldsRow mx_ExtendedProfileSettings_postal_row">
<div className="mx_ExtendedProfileSettings_field mx_ExtendedProfileSettings_field_business_plz">
<Field
label={_t("settings|extended_profile_settings|business_plz")}
value={businessPlz}
onChange={onBusinessPlzChanged}
disabled={isSaving}
autoComplete="off"
/>
</div>
<div className="mx_ExtendedProfileSettings_field mx_ExtendedProfileSettings_field_business_city">
<Field
label={_t("settings|extended_profile_settings|business_city")}
value={businessCity}
onChange={onBusinessCityChanged}
disabled={isSaving}
autoComplete="off"
/>
</div>
</div>
{/* Email field with validation */}
<div className="mx_ExtendedProfileSettings_fieldsRow mx_ExtendedProfileSettings_contact_row">
<div className="mx_ExtendedProfileSettings_field">
<Field
label={_t("settings|extended_profile_settings|business_email")}
value={businessEmail}
onChange={onBusinessEmailChanged}
disabled={isSaving}
autoComplete="off"
type="email"
/>
</div>
{/* Telephone field with validation */}
<div className="mx_ExtendedProfileSettings_field">
<Field
label={_t("settings|extended_profile_settings|business_tel")}
value={businessTel}
onChange={onBusinessTelChanged}
disabled={isSaving}
autoComplete="off"
type="tel"
/>
</div>
</div>
<div className="mx_ExtendedProfileSettings_field">
<Field
label={_t("settings|extended_profile_settings|website")}
value={website}
onChange={onWebsiteChanged}
disabled={isSaving}
autoComplete="off"
/>
</div>
<div className="mx_ExtendedProfileSettings_field">
<Field
className="mx_ExtendedProfileSettings_multiline"
label={_t("settings|extended_profile_settings|further_information")}
value={furtherInfo}
onChange={onFurtherInfoChanged}
disabled={isSaving}
element="textarea"
autoComplete="off"
/>
</div>
{/* Single set of buttons for all fields */}
<div className="mx_ExtendedProfileSettings_buttons">
<AccessibleButton onClick={onCancel} kind="primary_outline" disabled={isSaving || !isModified}>
{_t("common|cancel")}
</AccessibleButton>
<AccessibleButton onClick={onSave} kind="primary" disabled={isSaving || !isModified}>
{_t("common|save")}
</AccessibleButton>
</div>
</div>
{saveError && (
<Alert title={_t("settings|extended_profile_settings|failed_to_save")} type="critical">
{businessEmailValid ? null : _t("settings|extended_profile_settings|invalid_email")}
<span> </span>
{businessTelValid ? null : _t("settings|extended_profile_settings|invalid_phone")}
</Alert>
)}
</div>
</div>
);
};
export default ExtendedProfileSettings;
@@ -28,7 +28,6 @@ import LogoutDialog, { shouldShowLogoutDialog } from "../dialogs/LogoutDialog";
import Modal from "../../../Modal";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { SettingsSection } from "./shared/SettingsSection.tsx";
import ExtendedProfileSettings from "./ExtendedProfileSettings";
const SpinnerToast: React.FC<{ children?: ReactNode }> = ({ children }) => (
<>
@@ -239,9 +238,6 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({
)}
{userIdentifier && <UsernameBox username={userIdentifier} />}
{/* Always display the Extended Profile Settings component */}
<ExtendedProfileSettings className="mx_UserProfileSettings_extendedProfile" />
<Flex gap="var(--cpd-space-4x)" className="mx_UserProfileSettings_profile_buttons">
{externalAccountManagementUrl && (
<ManageAccountButton externalAccountManagementUrl={externalAccountManagementUrl} />
-22
View File
@@ -2441,26 +2441,6 @@
}
},
"settings": {
"extended_profile_settings": {
"additional_information": "Zusätzliche Informationen",
"professional_title": "Titel",
"specialization": "Fachgebiet",
"practice_name": "Praxisname / Firmenname",
"business_street": "Strasse",
"business_street_nr": "Hausnummer",
"business_plz": "PLZ",
"business_city": "Ort",
"business_email": "Email (geschäftlich)",
"business_tel": "Telefon (geschäftlich)",
"website": "Webseite",
"further_information": "Weitere Informationen / Qualifikationen",
"saving_profile": "Profil wird gespeichert...",
"failed_to_load": "Fehler beim Laden der erweiterten Profileinstellungen.",
"failed_to_save": "Fehler beim Speichern der erweiterten Profileinstellungen.",
"server_not_supported": "Dieser Server unterstützt keine erweiterten Profileinstellungen.",
"invalid_email": "Ungültige E-Mail-Adresse.",
"invalid_phone": "Ungültige Telefonnummer."
},
"account": {
"dialog_title": "<strong>Einstellungen:</strong> Konto",
"title": "Konto"
@@ -3760,7 +3740,6 @@
},
"user_info": {
"extended_profile": {
"header": "Weitere Informationen",
"title": "Titel",
"specialization": "Fachgebiet",
"practice_name": "Praxisname / Firmenname",
@@ -3769,7 +3748,6 @@
"business_tel": "Telefon (geschäftlich)",
"website": "Webseite",
"further_info": "Weitere Infos / Qualifikationen",
"not_provided": "Nicht angegeben",
"error_loading_profile": "Fehler beim Laden des Profils"
},
"admin_tools_section": "Administrationswerkzeuge",
-22
View File
@@ -2484,26 +2484,6 @@
}
},
"settings": {
"extended_profile_settings": {
"additional_information": "Additional Information",
"professional_title": "Title",
"specialization": "Specialization",
"practice_name": "Practice Name",
"business_street": "Strasse",
"business_street_nr": "Street Number",
"business_plz": "ZIP Code",
"business_city": "City",
"business_email": "Business Email",
"business_tel": "Business Phone",
"website": "Website",
"further_information": "Further Information / Qualifications",
"saving_profile": "Saving profile...",
"failed_to_load": "Failed to load extended profile settings.",
"failed_to_save": "Failed to save extended profile settings.",
"server_not_supported": "Your server does not support extended profile settings.",
"invalid_email": "Invalid email.",
"invalid_phone": "Invalid phone number."
},
"account": {
"dialog_title": "<strong>Settings:</strong> Account",
"title": "Account"
@@ -3816,7 +3796,6 @@
},
"user_info": {
"extended_profile": {
"header": "Additional infos",
"title": "Title",
"specialization": "Speciality",
"practice_name": "Practice name",
@@ -3825,7 +3804,6 @@
"business_tel": "Business phone",
"website": "Website",
"further_info": "Further infos / qualifications",
"not_provided": "Not provided",
"error_loading_profile": "Error loading profile"
},
"admin_tools_section": "Admin Tools",
-22
View File
@@ -2488,26 +2488,6 @@
}
},
"settings": {
"extended_profile_settings": {
"additional_information": "Informations supplémentaires",
"professional_title": "Titre",
"specialization": "Spécialisation",
"practice_name": "Nom de l’entreprise",
"business_street": "Rue",
"business_street_nr": "Numéro de rue",
"business_plz": "Code postal",
"business_city": "Ville",
"business_email": "E-mail professionnel",
"business_tel": "Téléphone professionnel",
"website": "Site web",
"further_information": "Informations supplémentaires / qualifications",
"saving_profile": "Enregistrement des paramètres de profil étendus…",
"failed_to_load": "Erreur lors du chargement des paramètres de profil étendus.",
"failed_to_save": "Erreur lors de l'enregistrement des paramètres de profil étendus.",
"server_not_supported": "Le serveur d’accueil ne prend pas en charge les paramètres de profil étendus.",
"invalid_email": "Adresse e-mail invalide.",
"invalid_phone": "Numéro de téléphone invalide."
},
"account": {
"dialog_title": "<strong>Paramètres : </strong> Compte",
"title": "Compte"
@@ -3820,7 +3800,6 @@
},
"user_info": {
"extended_profile": {
"header": "Informations supplémentaires",
"title": "Titre",
"specialization": "Spécialisation",
"practice_name": "Nom de l’entreprise",
@@ -3829,7 +3808,6 @@
"business_tel": "Téléphone (professionnel)",
"website": "Site web",
"further_info": "Informations supplémentaires / qualifications",
"not_provided": "Non fourni",
"error_loading_profile": "Erreur lors du chargement du profil"
},
"admin_tools_section": "Outils d’administration",
-12
View File
@@ -2119,18 +2119,6 @@
}
},
"settings": {
"extended_profile_settings": {
"additional_information": "Informazioni aggiuntive",
"professional_title": "Titolo",
"specialization": "Specializzazione",
"address": "Indirizzo",
"website": "Sito web",
"further_information": "Ulteriori informazioni / dettagli",
"saving_profile": "Salvataggio del profilo esteso...",
"failed_to_load": "Impossibile caricare le impostazioni del profilo esteso.",
"failed_to_save": "Impossibile salvare le impostazioni del profilo esteso.",
"server_not_supported": "Il tuo server non supporta le impostazioni del profilo esteso."
},
"all_rooms_home": "Mostra tutte le stanze nella pagina principale",
"all_rooms_home_description": "Tutte le stanze in cui sei appariranno nella pagina principale.",
"always_show_message_timestamps": "Mostra sempre l'orario dei messaggi",
-2
View File
@@ -2195,13 +2195,11 @@
},
"user_info": {
"extended_profile": {
"header": "Informazioni aggiuntive",
"title": "Titolo",
"specialization": "Specializzazione",
"address": "Indirizzo",
"website": "Sito web",
"further_info": "Ulteriori informazioni / qualifiche",
"not_provided": "Non fornito",
"error_loading_profile": "Errore nel caricamento del profilo"
},
"admin_tools_section": "Administratoriaus įrankiai",
-46
View File
@@ -216,52 +216,6 @@ export class UserProfilesStore {
return null;
}
/**
* Set an extended profile property for the current user.
*
* @param property - The property key to set
* @param value - The value to set
* @returns true if successful, false otherwise
*/
public async setExtendedProfileProperty(property: string, value: ExtendedProfileValue): Promise<boolean> {
try {
await this.client.setExtendedProfileProperty(property, value);
const userId = this.client.getSafeUserId();
if (!this.extendedProfiles.has(userId)) {
this.extendedProfiles.set(userId, new Map());
}
this.extendedProfiles.get(userId)?.set(property, value);
return true;
} catch (e) {
logger.warn(`Error setting extended profile property ${property}`, e);
return false;
}
}
/**
* Delete an extended profile property for the current user.
*
* @param property - The property key to delete
* @returns true if successful, false otherwise
*/
public async deleteExtendedProfileProperty(property: string): Promise<boolean> {
try {
await this.client.deleteExtendedProfileProperty(property);
const userId = this.client.getSafeUserId();
if (this.extendedProfiles.has(userId)) {
this.extendedProfiles.get(userId)?.delete(property);
}
return true;
} catch (e) {
logger.warn(`Error deleting extended profile property ${property}`, e);
return false;
}
}
public flush(): void {
this.profiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
this.knownProfiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
@@ -18,7 +18,6 @@ import { mkStubRoom, stubClient } from "../../../../test-utils";
import { ToastContext, type ToastRack } from "../../../../../src/contexts/ToastContext";
import { OwnProfileStore } from "../../../../../src/stores/OwnProfileStore";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import { SdkContextClass } from "../../../../../src/contexts/SDKContext";
import Modal from "../../../../../src/Modal";
interface MockedAvatarSettingProps {
@@ -87,9 +86,6 @@ describe("ProfileSettings", () => {
beforeEach(() => {
client = stubClient();
// HealthChat's ExtendedProfileSettings reads the global SdkContextClass.instance.userProfilesStore,
// which requires the singleton client to be set (it is, on login, in the real app).
SdkContextClass.instance.client = client;
toastRack = {
displayToast: jest.fn().mockReturnValue(jest.fn()),
};
@@ -85,9 +85,6 @@ describe("<AccountUserSettingsTab />", () => {
stores = new SdkContextClass();
stores.client = mockClient;
// HealthChat's ExtendedProfileSettings (mounted via UserProfileSettings) reads the global
// SdkContextClass.instance.userProfilesStore, which requires the singleton client to be set.
SdkContextClass.instance.client = mockClient;
// stub out this store completely to avoid mocking initialisation
const mockOidcClientStore = {} as unknown as OidcClientStore;
jest.spyOn(stores, "oidcClientStore", "get").mockReturnValue(mockOidcClientStore);
@@ -42,14 +42,14 @@ exports[`<AccountUserSettingsTab /> 3pids should display 3pid email addresses an
>
<input
autocomplete="email"
id="mx_Field_113"
id="mx_Field_3"
label="Email Address"
placeholder="Email Address"
type="text"
value=""
/>
<label
for="mx_Field_113"
for="mx_Field_3"
>
Email Address
</label>
@@ -154,14 +154,14 @@ exports[`<AccountUserSettingsTab /> 3pids should display 3pid email addresses an
</span>
<input
autocomplete="tel-national"
id="mx_Field_114"
id="mx_Field_4"
label="Phone Number"
placeholder="Phone Number"
type="text"
value=""
/>
<label
for="mx_Field_114"
for="mx_Field_4"
>
Phone Number
</label>