diff --git a/src/app/features/settings/account/Profile.tsx b/src/app/features/settings/account/Profile.tsx index 331c463a..618be81f 100644 --- a/src/app/features/settings/account/Profile.tsx +++ b/src/app/features/settings/account/Profile.tsx @@ -36,7 +36,7 @@ import { Line, } from 'folds'; import FocusTrap from 'focus-trap-react'; -import { UserEvent } from 'matrix-js-sdk'; +import { UserEvent, ValidatedAuthMetadata } from 'matrix-js-sdk'; import { isKeyHotkey } from 'is-hotkey'; import { SequenceCard } from '../../../components/sequence-card'; import { SettingTile } from '../../../components/setting-tile'; @@ -54,7 +54,7 @@ import { UserHero, UserHeroName } from '../../../components/user-profile/UserHer import { ExtendedProfile, useExtendedProfile, - useProfileFieldAllowed, + useProfileEditsAllowed, } from '../../../hooks/useExtendedProfile'; import { ProfileFieldContextProvider, useProfileField } from './ProfileFieldContext'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; @@ -62,6 +62,10 @@ import { FilterByValues } from '../../../../types/utils'; import { CutoutCard } from '../../../components/cutout-card'; import { ServerChip, ShareChip, TimezoneChip } from '../../../components/user-profile/UserChips'; import { SequenceCardStyle } from '../styles.css'; +import { useUserProfile } from '../../../hooks/useUserProfile'; +import { useAuthMetadata } from '../../../hooks/useAuthMetadata'; +import { useAccountManagementActions } from '../../../hooks/useAccountManagement'; +import { withSearchParam } from '../../../pages/pathUtils'; function ProfileAvatar() { const mx = useMatrixClient(); @@ -70,7 +74,7 @@ function ProfileAvatar() { const avatarUrl = value ? mxcUrlToHttp(mx, value, useAuthentication, 96, 96, 'crop') ?? undefined : undefined; - const disabled = !useProfileFieldAllowed('avatar_url') || busy; + const disabled = !useProfileEditsAllowed('avatar_url') || busy; const [imageFile, setImageFile] = useState(); const imageFileURL = useObjectURL(imageFile); @@ -178,7 +182,7 @@ function ProfileTextField) { const { busy, defaultValue, value, setValue } = useProfileField(field); - const disabled = !useProfileFieldAllowed(field) || busy; + const disabled = !useProfileEditsAllowed(field) || busy; const hasChanges = defaultValue !== value; const handleChange: ChangeEventHandler = (evt) => { @@ -212,6 +216,7 @@ function ProfileTextField(); const [pendingPronoun, setPendingPronoun] = useState(''); @@ -361,7 +366,7 @@ function ProfilePronouns() { function ProfileTimezone() { const { busy, value, setValue } = useProfileField('us.cloke.msc4175.tz'); - const disabled = !useProfileFieldAllowed('us.cloke.msc4175.tz') || busy; + const disabled = !useProfileEditsAllowed('us.cloke.msc4175.tz') || busy; const inputRef = useRef(null); const scrollRef = useRef(null); @@ -515,45 +520,99 @@ function ProfileTimezone() { ); } +function IdentityProviderSettings({ authMetadata }: { authMetadata: ValidatedAuthMetadata }) { + const accountManagementActions = useAccountManagementActions(); + + const openProviderProfileSettings = useCallback(() => { + const authUrl = authMetadata?.account_management_uri ?? authMetadata?.issuer; + if (!authUrl) return; + + window.open( + withSearchParam(authUrl, { + action: accountManagementActions.profile, + }), + '_blank' + ); + }, [authMetadata, accountManagementActions]); + + return ( + + + Open + + } + /> + + ); +} + export function Profile() { const mx = useMatrixClient(); const userId = mx.getUserId() as string; const server = getMxIdServer(userId); + const authMetadata = useAuthMetadata(); + const accountManagementActions = useAccountManagementActions(); const [extendedProfile, refreshExtendedProfile] = useExtendedProfile(userId); + const extendedProfileSupported = extendedProfile !== null; + const legacyProfile = useUserProfile(userId); - const [fieldDefaults, setFieldDefaults] = useState({}); + const profileEditableThroughIDP = + authMetadata !== undefined && + authMetadata.account_management_actions_supported?.includes(accountManagementActions.profile); + const profileEditableThroughClient = useProfileEditsAllowed(null); + + const [fieldDefaults, setFieldDefaults] = useState({ + displayname: legacyProfile.displayName, + avatar_url: legacyProfile.avatarUrl, + }); useLayoutEffect(() => { - if (extendedProfile !== undefined) { + if (extendedProfile) { setFieldDefaults(extendedProfile); } - }, [userId, setFieldDefaults, extendedProfile]); + }, [setFieldDefaults, extendedProfile]); const useAuthentication = useMediaAuthentication(); const [saveState, handleSave] = useAsyncCallback( useCallback( async (fields: ExtendedProfile) => { - await Promise.all( - Object.entries(fields).map(async ([key, value]) => { - if (value === undefined) { - await mx.deleteExtendedProfileProperty(key); - } else { - await mx.setExtendedProfileProperty(key, value); - } - }) - ); - await refreshExtendedProfile(); - // XXX: synthesise a profile update for ourselves because Synapse is broken and won't - const user = mx.getUser(userId); - if (user) { - user.displayName = fields.displayname; - user.avatarUrl = fields.avatar_url; - user.emit(UserEvent.DisplayName, user.events.presence, user); - user.emit(UserEvent.AvatarUrl, user.events.presence, user); + if (extendedProfileSupported) { + await Promise.all( + Object.entries(fields).map(async ([key, value]) => { + if (value === undefined) { + await mx.deleteExtendedProfileProperty(key); + } else { + await mx.setExtendedProfileProperty(key, value); + } + }) + ); + await refreshExtendedProfile(); + // XXX: synthesise a profile update for ourselves because Synapse is broken and won't + const user = mx.getUser(userId); + if (user) { + user.displayName = fields.displayname; + user.avatarUrl = fields.avatar_url; + user.emit(UserEvent.DisplayName, user.events.presence, user); + user.emit(UserEvent.AvatarUrl, user.events.presence, user); + } + } else { + await mx.setDisplayName(fields.displayname ?? ''); + await mx.setAvatarUrl(fields.avatar_url ?? ''); + setFieldDefaults(fields); } }, - [mx, userId, refreshExtendedProfile] + [mx, userId, refreshExtendedProfile, extendedProfileSupported, setFieldDefaults] ) ); @@ -604,39 +663,66 @@ export function Profile() { gap="400" radii="0" > - - - - - - - - - - {saving && } - + {profileEditableThroughIDP && ( + + )} + {profileEditableThroughClient && ( + <> + + + + {extendedProfileSupported && ( + <> + + + + )} + + + + + {saving && } + + + )} + {!(profileEditableThroughClient || profileEditableThroughIDP) && ( + + + + + Profile Editing Disabled + + + + Your homeserver does not allow you to edit your profile. + + + + + + )} ); diff --git a/src/app/hooks/useExtendedProfile.ts b/src/app/hooks/useExtendedProfile.ts index 00a8c1be..50de4f88 100644 --- a/src/app/hooks/useExtendedProfile.ts +++ b/src/app/hooks/useExtendedProfile.ts @@ -30,7 +30,7 @@ export function useExtendedProfileSupported(): boolean { export function useExtendedProfile( userId: string -): [ExtendedProfile | undefined, () => Promise] { +): [ExtendedProfile | undefined | null, () => Promise] { const mx = useMatrixClient(); const extendedProfileSupported = useExtendedProfileSupported(); const { data, refetch } = useQuery({ @@ -39,7 +39,7 @@ export function useExtendedProfile( if (extendedProfileSupported) { return extendedProfile.parse(await mx.getExtendedProfile(userId)); } - return undefined; + return null; }, [mx, userId, extendedProfileSupported]), refetchOnMount: false, }); @@ -54,15 +54,20 @@ export function useExtendedProfile( const LEGACY_FIELDS = ['displayname', 'avatar_url']; -export function useProfileFieldAllowed(field: string): boolean { +export function useProfileEditsAllowed(field: string | null): boolean { const capabilities = useCapabilities(); const extendedProfileSupported = useExtendedProfileSupported(); - if (LEGACY_FIELDS.includes(field)) { + if (field && LEGACY_FIELDS.includes(field)) { // this field might have a pre-msc4133 capability. check that first if (capabilities[`m.set_${field}`]?.enabled === false) { return false; } + + if (!extendedProfileSupported) { + // the homeserver only supports legacy fields + return true; + } } if (extendedProfileSupported) { @@ -81,6 +86,11 @@ export function useProfileFieldAllowed(field: string): boolean { return false; } + if (field === null) { + // profile field modifications are not completely disabled + return true; + } + if ( extendedProfileCapability.allowed !== undefined && !extendedProfileCapability.allowed.includes(field) @@ -98,6 +108,11 @@ export function useProfileFieldAllowed(field: string): boolean { return true; } + if (field === null) { + // the homeserver only supports legacy fields. assume profile editing is generally allowed + return true; + } + // `field` is an extended profile key and the homeserver lacks msc4133 support return false; }