mirror of
https://github.com/Novaloop-AG/healthchat-web.git
synced 2026-11-04 01:19:41 +00:00
feat: add ability to edit custom user attributes
This commit is contained in:
@@ -323,6 +323,7 @@
|
|||||||
@import "./views/settings/_UpdateCheckButton.pcss";
|
@import "./views/settings/_UpdateCheckButton.pcss";
|
||||||
@import "./views/settings/_UserPersonalInfoSettings.pcss";
|
@import "./views/settings/_UserPersonalInfoSettings.pcss";
|
||||||
@import "./views/settings/_UserProfileSettings.pcss";
|
@import "./views/settings/_UserProfileSettings.pcss";
|
||||||
|
@import "./views/settings/_ExtendedProfileSettings.pcss";
|
||||||
@import "./views/settings/encryption/_AdvancedPanel.pcss";
|
@import "./views/settings/encryption/_AdvancedPanel.pcss";
|
||||||
@import "./views/settings/encryption/_ChangeRecoveryKey.pcss";
|
@import "./views/settings/encryption/_ChangeRecoveryKey.pcss";
|
||||||
@import "./views/settings/encryption/_EncryptionCard.pcss";
|
@import "./views/settings/encryption/_EncryptionCard.pcss";
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ExtendedProfileSettings_buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ExtendedProfileSettings_multiline textarea {
|
||||||
|
min-height: 200px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
@@ -0,0 +1,302 @@
|
|||||||
|
/*
|
||||||
|
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, ErrorMessage } 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_ADDRESS = "io.element.profile.address";
|
||||||
|
export const EXT_PROFILE_KEY_WEBSITE = "io.element.profile.website";
|
||||||
|
export const EXT_PROFILE_KEY_FURTHER_INFO = "io.element.profile.further_info";
|
||||||
|
|
||||||
|
// 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, address, website, and further information.
|
||||||
|
*/
|
||||||
|
const ExtendedProfileSettings: React.FC<ExtendedProfileSettingsProps> = ({ className }) => {
|
||||||
|
const [title, setTitle] = useState<string>("");
|
||||||
|
const [specialization, setSpecialization] = useState<string>("");
|
||||||
|
const [address, setAddress] = useState<string>("");
|
||||||
|
const [website, setWebsite] = useState<string>("");
|
||||||
|
const [furtherInfo, setFurtherInfo] = useState<string>("");
|
||||||
|
|
||||||
|
// Store original values for cancel functionality
|
||||||
|
const [originalTitle, setOriginalTitle] = useState<string>("");
|
||||||
|
const [originalSpecialization, setOriginalSpecialization] = useState<string>("");
|
||||||
|
const [originalAddress, setOriginalAddress] = useState<string>("");
|
||||||
|
const [originalWebsite, setOriginalWebsite] = useState<string>("");
|
||||||
|
const [originalFurtherInfo, setOriginalFurtherInfo] = 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 addressValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_ADDRESS);
|
||||||
|
const websiteValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_WEBSITE);
|
||||||
|
const furtherInfoValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_FURTHER_INFO);
|
||||||
|
|
||||||
|
const titleStr = titleValue as string || "";
|
||||||
|
const specializationStr = specializationValue as string || "";
|
||||||
|
const addressStr = addressValue as string || "";
|
||||||
|
const websiteStr = websiteValue as string || "";
|
||||||
|
const furtherInfoStr = furtherInfoValue as string || "";
|
||||||
|
|
||||||
|
setTitle(titleStr);
|
||||||
|
setSpecialization(specializationStr);
|
||||||
|
setAddress(addressStr);
|
||||||
|
setWebsite(websiteStr);
|
||||||
|
setFurtherInfo(furtherInfoStr);
|
||||||
|
|
||||||
|
// Store original values for cancel functionality
|
||||||
|
setOriginalTitle(titleStr);
|
||||||
|
setOriginalSpecialization(specializationStr);
|
||||||
|
setOriginalAddress(addressStr);
|
||||||
|
setOriginalWebsite(websiteStr);
|
||||||
|
setOriginalFurtherInfo(furtherInfoStr);
|
||||||
|
} 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 ||
|
||||||
|
address !== originalAddress ||
|
||||||
|
website !== originalWebsite ||
|
||||||
|
furtherInfo !== originalFurtherInfo;
|
||||||
|
|
||||||
|
setIsModified(hasChanges);
|
||||||
|
}, [title, specialization, address, website, furtherInfo,
|
||||||
|
originalTitle, originalSpecialization, originalAddress, originalWebsite, originalFurtherInfo]);
|
||||||
|
|
||||||
|
// Field change handlers
|
||||||
|
const onTitleChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setTitle(e.target.value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onSpecializationChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setSpecialization(e.target.value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onAddressChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setAddress(e.target.value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onWebsiteChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setWebsite(e.target.value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onFurtherInfoChanged = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
setFurtherInfo(e.target.value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Cancel handler - restore all original values
|
||||||
|
const onCancel = useCallback(() => {
|
||||||
|
setTitle(originalTitle);
|
||||||
|
setSpecialization(originalSpecialization);
|
||||||
|
setAddress(originalAddress);
|
||||||
|
setWebsite(originalWebsite);
|
||||||
|
setFurtherInfo(originalFurtherInfo);
|
||||||
|
}, [originalTitle, originalSpecialization, originalAddress, originalWebsite, originalFurtherInfo]);
|
||||||
|
|
||||||
|
// Save handler - save all fields at once
|
||||||
|
const onSave = useCallback(async (): Promise<void> => {
|
||||||
|
if (!isModified) 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 (address !== originalAddress) {
|
||||||
|
savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_ADDRESS, address));
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(savePromises);
|
||||||
|
|
||||||
|
// Update original values after successful save
|
||||||
|
setOriginalTitle(title);
|
||||||
|
setOriginalSpecialization(specialization);
|
||||||
|
setOriginalAddress(address);
|
||||||
|
setOriginalWebsite(website);
|
||||||
|
setOriginalFurtherInfo(furtherInfo);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("Failed to save profile information", e);
|
||||||
|
setSaveError(true);
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
removeToast();
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
isModified, title, specialization, address, website, furtherInfo,
|
||||||
|
originalTitle, originalSpecialization, originalAddress, originalWebsite, originalFurtherInfo,
|
||||||
|
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|address")}
|
||||||
|
value={address}
|
||||||
|
onChange={onAddressChanged}
|
||||||
|
disabled={isSaving}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</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 && (
|
||||||
|
<div className="mx_ExtendedProfileSettings_error">
|
||||||
|
<ErrorMessage>{_t("settings|extended_profile_settings|failed_to_save")}</ErrorMessage>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExtendedProfileSettings;
|
||||||
@@ -28,6 +28,7 @@ import LogoutDialog, { shouldShowLogoutDialog } from "../dialogs/LogoutDialog";
|
|||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||||
import { SettingsSection } from "./shared/SettingsSection.tsx";
|
import { SettingsSection } from "./shared/SettingsSection.tsx";
|
||||||
|
import ExtendedProfileSettings from "./ExtendedProfileSettings";
|
||||||
|
|
||||||
const SpinnerToast: React.FC<{ children?: ReactNode }> = ({ children }) => (
|
const SpinnerToast: React.FC<{ children?: ReactNode }> = ({ children }) => (
|
||||||
<>
|
<>
|
||||||
@@ -237,6 +238,10 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
{userIdentifier && <UsernameBox username={userIdentifier} />}
|
{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">
|
<Flex gap="var(--cpd-space-4x)" className="mx_UserProfileSettings_profile_buttons">
|
||||||
{externalAccountManagementUrl && (
|
{externalAccountManagementUrl && (
|
||||||
<ManageAccountButton externalAccountManagementUrl={externalAccountManagementUrl} />
|
<ManageAccountButton externalAccountManagementUrl={externalAccountManagementUrl} />
|
||||||
|
|||||||
@@ -2441,6 +2441,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
"extended_profile_settings": {
|
||||||
|
"additional_information": "Zusätzliche Informationen",
|
||||||
|
"professional_title": "Titel",
|
||||||
|
"specialization": "Fachgebiet",
|
||||||
|
"address": "Adresse",
|
||||||
|
"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."
|
||||||
|
},
|
||||||
"account": {
|
"account": {
|
||||||
"dialog_title": "<strong>Einstellungen:</strong> Konto",
|
"dialog_title": "<strong>Einstellungen:</strong> Konto",
|
||||||
"title": "Konto"
|
"title": "Konto"
|
||||||
|
|||||||
@@ -2484,6 +2484,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
"extended_profile_settings": {
|
||||||
|
"additional_information": "Additional Information",
|
||||||
|
"professional_title": "Title",
|
||||||
|
"specialization": "Specialization",
|
||||||
|
"address": "Address",
|
||||||
|
"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."
|
||||||
|
},
|
||||||
"account": {
|
"account": {
|
||||||
"dialog_title": "<strong>Settings:</strong> Account",
|
"dialog_title": "<strong>Settings:</strong> Account",
|
||||||
"title": "Account"
|
"title": "Account"
|
||||||
|
|||||||
@@ -2488,6 +2488,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
"extended_profile_settings": {
|
||||||
|
"additional_information": "Informations supplémentaires",
|
||||||
|
"professional_title": "Titre",
|
||||||
|
"specialization": "Spécialisation",
|
||||||
|
"address": "Adresse",
|
||||||
|
"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."
|
||||||
|
},
|
||||||
"account": {
|
"account": {
|
||||||
"dialog_title": "<strong>Paramètres : </strong> Compte",
|
"dialog_title": "<strong>Paramètres : </strong> Compte",
|
||||||
"title": "Compte"
|
"title": "Compte"
|
||||||
|
|||||||
@@ -2119,6 +2119,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"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": "Mostra tutte le stanze nella pagina principale",
|
||||||
"all_rooms_home_description": "Tutte le stanze in cui sei appariranno 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",
|
"always_show_message_timestamps": "Mostra sempre l'orario dei messaggi",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { LruCache } from "../utils/LruCache";
|
|||||||
const cacheSize = 500;
|
const cacheSize = 500;
|
||||||
|
|
||||||
type StoreProfileValue = IMatrixProfile | undefined | null;
|
type StoreProfileValue = IMatrixProfile | undefined | null;
|
||||||
|
type ExtendedProfileValue = string | number | boolean | object | null;
|
||||||
|
|
||||||
interface GetOptions {
|
interface GetOptions {
|
||||||
/** Whether calling the function shouuld raise an Error. */
|
/** Whether calling the function shouuld raise an Error. */
|
||||||
@@ -33,6 +34,8 @@ interface GetOptions {
|
|||||||
export class UserProfilesStore {
|
export class UserProfilesStore {
|
||||||
private profiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
|
private profiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
|
||||||
private knownProfiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
|
private knownProfiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
|
||||||
|
private extendedProfilesSupport = new Map<string, boolean>();
|
||||||
|
private extendedProfiles = new Map<string, Map<string, ExtendedProfileValue>>();
|
||||||
|
|
||||||
public constructor(private client: MatrixClient) {
|
public constructor(private client: MatrixClient) {
|
||||||
client.on(RoomMemberEvent.Membership, this.onRoomMembershipEvent);
|
client.on(RoomMemberEvent.Membership, this.onRoomMembershipEvent);
|
||||||
@@ -115,9 +118,155 @@ export class UserProfilesStore {
|
|||||||
return profile;
|
return profile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a server supports extended profiles for a user.
|
||||||
|
* Caches the result.
|
||||||
|
*
|
||||||
|
* @param userId - User Id to check
|
||||||
|
* @returns true if the server supports extended profiles, false otherwise
|
||||||
|
*/
|
||||||
|
public async doesServerSupportExtendedProfiles(userId: string): Promise<boolean> {
|
||||||
|
const serverId = userId.split(":")[1];
|
||||||
|
if (this.extendedProfilesSupport.has(serverId)) {
|
||||||
|
return this.extendedProfilesSupport.get(serverId) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const supported = await this.client.doesServerSupportExtendedProfiles();
|
||||||
|
this.extendedProfilesSupport.set(serverId, supported);
|
||||||
|
return supported;
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(`Error checking extended profiles support for serverId ${serverId}`, e);
|
||||||
|
this.extendedProfilesSupport.set(serverId, false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an extended profile property from cache if available.
|
||||||
|
*
|
||||||
|
* @param userId - User Id for which to get the extended profile property
|
||||||
|
* @param property - The property key to get
|
||||||
|
* @returns The property value if cached, undefined if not cached
|
||||||
|
*/
|
||||||
|
public getExtendedProfileProperty(userId: string, property: string): ExtendedProfileValue | undefined {
|
||||||
|
const userExtendedProfile = this.extendedProfiles.get(userId);
|
||||||
|
if (!userExtendedProfile) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return userExtendedProfile.get(property);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or fetch an extended profile property.
|
||||||
|
*
|
||||||
|
* @param userId - User Id for which to get the extended profile property
|
||||||
|
* @param property - The property key to get
|
||||||
|
* @returns The property value, null if it doesn't exist
|
||||||
|
*/
|
||||||
|
public async getOrFetchExtendedProfileProperty(userId: string, property: string): Promise<ExtendedProfileValue | null> {
|
||||||
|
const cached = this.getExtendedProfileProperty(userId, property);
|
||||||
|
if (cached !== undefined) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.fetchExtendedProfileProperty(userId, property);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch an extended profile property from the API.
|
||||||
|
* Caches the result.
|
||||||
|
*
|
||||||
|
* @param userId - User Id for which to fetch the extended profile property
|
||||||
|
* @param property - The property key to fetch
|
||||||
|
* @returns The property value, null if it doesn't exist or there was an error
|
||||||
|
*/
|
||||||
|
public async fetchExtendedProfileProperty(userId: string, property: string): Promise<ExtendedProfileValue | null> {
|
||||||
|
// Removed server support check - assume support is always available
|
||||||
|
try {
|
||||||
|
const rawValue = await this.client.getExtendedProfileProperty(userId, property);
|
||||||
|
// Ensure the value is of type ExtendedProfileValue
|
||||||
|
const value: ExtendedProfileValue = this.validateExtendedProfileValue(rawValue);
|
||||||
|
// Cache the value
|
||||||
|
if (!this.extendedProfiles.has(userId)) {
|
||||||
|
this.extendedProfiles.set(userId, new Map());
|
||||||
|
}
|
||||||
|
this.extendedProfiles.get(userId)?.set(property, value);
|
||||||
|
|
||||||
|
return value;
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(`Error retrieving extended profile property ${property} for userId ${userId}`, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hilfsmethode, um den Rückgabewert zu validieren
|
||||||
|
private validateExtendedProfileValue(value: unknown): ExtendedProfileValue {
|
||||||
|
if (value === null ||
|
||||||
|
typeof value === "string" ||
|
||||||
|
typeof value === "number" ||
|
||||||
|
typeof value === "boolean" ||
|
||||||
|
(typeof value === "object" && value !== null)) {
|
||||||
|
return value as ExtendedProfileValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback für ungültige Werte
|
||||||
|
logger.warn(`Invalid extended profile value type: ${typeof value}`);
|
||||||
|
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 {
|
public flush(): void {
|
||||||
this.profiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
|
this.profiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
|
||||||
this.knownProfiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
|
this.knownProfiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
|
||||||
|
this.extendedProfilesSupport = new Map<string, boolean>();
|
||||||
|
this.extendedProfiles = new Map<string, Map<string, ExtendedProfileValue>>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user