diff --git a/res/css/views/right_panel/_ExtendedProfileView.pcss b/res/css/views/right_panel/_ExtendedProfileView.pcss new file mode 100644 index 0000000000..faa75bc877 --- /dev/null +++ b/res/css/views/right_panel/_ExtendedProfileView.pcss @@ -0,0 +1,79 @@ +/* +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; + } +} diff --git a/res/css/views/right_panel/_UserExtendedProfileInfo.pcss b/res/css/views/right_panel/_UserExtendedProfileInfo.pcss new file mode 100644 index 0000000000..e127ef68b4 --- /dev/null +++ b/res/css/views/right_panel/_UserExtendedProfileInfo.pcss @@ -0,0 +1,59 @@ +/* +Copyright 2025 New Vector Ltd + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +*/ + +.mx_UserExtendedProfileInfo { + padding: 16px; + background-color: $system; +} + +.mx_UserExtendedProfileInfo_loading { + display: flex; + justify-content: center; + padding: 20px 0; +} + +.mx_UserExtendedProfileInfo_heading { + margin-bottom: 12px; +} + +.mx_UserExtendedProfileInfo_content { + display: flex; + flex-direction: column; + gap: 12px; +} + +.mx_UserExtendedProfileInfo_row { + display: flex; + gap: 16px; +} + +.mx_UserExtendedProfileInfo_row .mx_UserExtendedProfileInfo_field { + flex: 1; +} + +.mx_UserExtendedProfileInfo_field { + margin-bottom: 4px; +} + +.mx_UserExtendedProfileInfo_label { + margin-bottom: 3px; + color: $tertiary-content; +} + +.mx_UserExtendedProfileInfo_value { + word-break: break-word; + overflow-wrap: break-word; +} + +/* Dark theme specific styles */ +.mx_UserExtendedProfileInfo_value a { + color: $accent; + text-decoration: none; +} + +.mx_UserExtendedProfileInfo_value a:hover { + text-decoration: underline; +} diff --git a/src/components/views/right_panel/ExtendedProfileView.tsx b/src/components/views/right_panel/ExtendedProfileView.tsx new file mode 100644 index 0000000000..1f351c6f84 --- /dev/null +++ b/src/components/views/right_panel/ExtendedProfileView.tsx @@ -0,0 +1,196 @@ +/* +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/globe"; +import LocationIcon from "@vector-im/compound-design-tokens/assets/web/icons/location"; +import BadgeIcon from "@vector-im/compound-design-tokens/assets/web/icons/badge"; +import StarIcon from "@vector-im/compound-design-tokens/assets/web/icons/star"; + +import { _t } from "../../../languageHandler"; +import { SdkContextClass } from "../../../contexts/SDKContext"; +import { Container } from "../right_panel/UserInfo"; +import InlineSpinner from "../elements/InlineSpinner"; +import { + EXT_PROFILE_KEY_ADDRESS, + EXT_PROFILE_KEY_FURTHER_INFO, + EXT_PROFILE_KEY_SPECIALIZATION, + EXT_PROFILE_KEY_TITLE, + EXT_PROFILE_KEY_WEBSITE, +} from "../settings/ExtendedProfileSettings"; +import { Flex } from "../../utils/Flex"; + +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 = ({ userId }) => { + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + const [profileData, setProfileData] = useState(null); + const [hasData, setHasData] = useState(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, + addressValue, + 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_ADDRESS), + userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_WEBSITE), + userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_FURTHER_INFO), + ]); + + const data = { + title: titleValue as string | null, + specialization: specializationValue as string | null, + address: addressValue as string | null, + 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 ( + + {loading ? ( + + + + ) : error ? ( + + {_t("Fehler beim Laden der Profilinformationen")} + + ) : hasData ? ( +
+ {(profileData?.title || profileData?.specialization) && ( +
+ {profileData.title && ( +
+ + +
+
{_t("settings|extended_profile_settings|professional_title")}
+
{profileData.title}
+
+
+
+ )} + + {profileData.specialization && ( +
+ + +
+
{_t("settings|extended_profile_settings|specialization")}
+
{profileData.specialization}
+
+
+
+ )} +
+ )} + + {profileData?.address && ( +
+ + +
+
{_t("settings|extended_profile_settings|address")}
+
{profileData.address}
+
+
+
+ )} + + {profileData?.website && ( +
+ + +
+
{_t("settings|extended_profile_settings|website")}
+ +
+
+
+ )} + + {profileData?.furtherInfo && ( +
+
+
{_t("settings|extended_profile_settings|further_information")}
+
{profileData.furtherInfo}
+
+
+ )} +
+ ) : null} +
+ ); +}; + +export default ExtendedProfileView; diff --git a/src/components/views/right_panel/UserExtendedProfileInfo.tsx b/src/components/views/right_panel/UserExtendedProfileInfo.tsx new file mode 100644 index 0000000000..905a1cc32a --- /dev/null +++ b/src/components/views/right_panel/UserExtendedProfileInfo.tsx @@ -0,0 +1,165 @@ +/* +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, { useEffect, useState } from "react"; +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_ADDRESS, + EXT_PROFILE_KEY_WEBSITE, + EXT_PROFILE_KEY_FURTHER_INFO, +} from "../settings/ExtendedProfileSettings"; + +// Helper function to preserve line breaks +const renderMultilineText = (text: string): React.ReactNode[] => { + const lines = text.split("\n"); + return lines.map((line, i) => ( + + {line} + {i < lines.length - 1 &&
} +
+ )); +}; + +interface UserExtendedProfileInfoProps { + userId: string; +} + +const UserExtendedProfileInfo: React.FC = ({ userId }) => { + //const client = useMatrixClientContext(); + const userProfilesStore = SdkContextClass.instance.userProfilesStore; + + const [title, setTitle] = useState(""); + const [specialization, setSpecialization] = useState(""); + const [address, setAddress] = useState(""); + const [website, setWebsite] = useState(""); + const [furtherInfo, setFurtherInfo] = useState(""); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + + useEffect(() => { + const fetchProfileData = async () => { + try { + setError(false); + setLoading(true); + + 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); + + setTitle(titleValue as string || ""); + setSpecialization(specializationValue as string || ""); + setAddress(addressValue as string || ""); + setWebsite(websiteValue as string || ""); + setFurtherInfo(furtherInfoValue as string || ""); + } catch { + setError(true); + } finally { + setLoading(false); + } + }; + + fetchProfileData(); + }, [userId, userProfilesStore]); + + if (loading) { + return ; + } + + if (error) { + return
{_t("user_info|extended_profile|error_loading_profile")}
; + } + + // Don't render anything if there's no data + if (!title && !specialization && !address && !website && !furtherInfo) { + return null; + } + + return ( +
+{/*
+ {_t("user_info|extended_profile|header")} +
*/} + {title && ( +
+
+ + {_t("user_info|extended_profile|title")} + +
+
+ {title} +
+
+ )} + {specialization && ( +
+
+ + {_t("user_info|extended_profile|specialization")} + +
+
+ {specialization} +
+
+ )} + {address && ( +
+
+ + {_t("user_info|extended_profile|address")} + +
+
+ {renderMultilineText(address)} +
+
+ )} + {website && ( +
+
+ + {_t("user_info|extended_profile|website")} + +
+ +
+ )} + {furtherInfo && ( +
+
+ + {_t("user_info|extended_profile|further_info")} + +
+
+ {renderMultilineText(furtherInfo)} +
+
+ )} +
+ ); +}; + +export default UserExtendedProfileInfo; diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index 4556063303..a135facb83 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -65,6 +65,7 @@ import { SdkContextClass } from "../../../contexts/SDKContext"; import { UserInfoAdminToolsContainer } from "./user_info/UserInfoAdminToolsContainer"; import { PowerLevelSection } from "./user_info/UserInfoPowerLevels"; import { UserInfoHeaderView } from "./user_info/UserInfoHeaderView"; +import UserExtendedProfileInfo from "./UserExtendedProfileInfo"; export interface IDevice extends Device { ambiguous?: boolean; @@ -731,6 +732,7 @@ const UserInfo: React.FC = ({ user, room, onClose, phase = RightPanelPha devices={devices} roomId={room?.roomId} /> + {phase === RightPanelPhases.MemberInfo && } ); diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 392b822399..a57a12f16c 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -3828,6 +3828,16 @@ } }, "user_info": { + "extended_profile": { + "header": "Weitere Informationen", + "title": "Titel", + "specialization": "Fachgebiet", + "address": "Adresse", + "website": "Webseite", + "further_info": "Weitere Infos / Qualifikationen", + "not_provided": "Nicht angegeben", + "error_loading_profile": "Fehler beim Laden des Profils" + }, "admin_tools_section": "Administrationswerkzeuge", "ban_button_room": "Bannen", "ban_button_space": "Bannen", diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 82200be44e..551d61cc93 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -3871,6 +3871,16 @@ } }, "user_info": { + "extended_profile": { + "header": "Additional infos", + "title": "Title", + "specialization": "Speciality", + "address": "Address", + "website": "Website", + "further_info": "Further infos / qualifications", + "not_provided": "Not provided", + "error_loading_profile": "Error loading profile" + }, "admin_tools_section": "Admin Tools", "ban_button_room": "Ban from room", "ban_button_space": "Ban from space", diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 66449e7ced..16f7b8c1ae 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -3865,6 +3865,16 @@ } }, "user_info": { + "extended_profile": { + "header": "Informations supplémentaires", + "title": "Titre", + "specialization": "Spécialisation", + "address": "Adresse", + "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", "ban_button_room": "Bannir du salon", "ban_button_space": "Bannir de l'espace", diff --git a/src/i18n/strings/lt.json b/src/i18n/strings/lt.json index 1d92542d59..d7b8185ed3 100644 --- a/src/i18n/strings/lt.json +++ b/src/i18n/strings/lt.json @@ -2185,6 +2185,16 @@ } }, "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", "ban_button_room": "Užblokuoti iš kambario", "ban_button_space": "Užblokuoti iš erdvės",