mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-04 22:40:29 +03:00
Fix support for MSC4133-less homeservers, add OIDC profile link
This commit is contained in:
parent
984803c52c
commit
aafd028af4
2 changed files with 165 additions and 64 deletions
|
|
@ -36,7 +36,7 @@ import {
|
||||||
Line,
|
Line,
|
||||||
} from 'folds';
|
} from 'folds';
|
||||||
import FocusTrap from 'focus-trap-react';
|
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 { isKeyHotkey } from 'is-hotkey';
|
||||||
import { SequenceCard } from '../../../components/sequence-card';
|
import { SequenceCard } from '../../../components/sequence-card';
|
||||||
import { SettingTile } from '../../../components/setting-tile';
|
import { SettingTile } from '../../../components/setting-tile';
|
||||||
|
|
@ -54,7 +54,7 @@ import { UserHero, UserHeroName } from '../../../components/user-profile/UserHer
|
||||||
import {
|
import {
|
||||||
ExtendedProfile,
|
ExtendedProfile,
|
||||||
useExtendedProfile,
|
useExtendedProfile,
|
||||||
useProfileFieldAllowed,
|
useProfileEditsAllowed,
|
||||||
} from '../../../hooks/useExtendedProfile';
|
} from '../../../hooks/useExtendedProfile';
|
||||||
import { ProfileFieldContextProvider, useProfileField } from './ProfileFieldContext';
|
import { ProfileFieldContextProvider, useProfileField } from './ProfileFieldContext';
|
||||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||||
|
|
@ -62,6 +62,10 @@ import { FilterByValues } from '../../../../types/utils';
|
||||||
import { CutoutCard } from '../../../components/cutout-card';
|
import { CutoutCard } from '../../../components/cutout-card';
|
||||||
import { ServerChip, ShareChip, TimezoneChip } from '../../../components/user-profile/UserChips';
|
import { ServerChip, ShareChip, TimezoneChip } from '../../../components/user-profile/UserChips';
|
||||||
import { SequenceCardStyle } from '../styles.css';
|
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() {
|
function ProfileAvatar() {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
|
|
@ -70,7 +74,7 @@ function ProfileAvatar() {
|
||||||
const avatarUrl = value
|
const avatarUrl = value
|
||||||
? mxcUrlToHttp(mx, value, useAuthentication, 96, 96, 'crop') ?? undefined
|
? mxcUrlToHttp(mx, value, useAuthentication, 96, 96, 'crop') ?? undefined
|
||||||
: undefined;
|
: undefined;
|
||||||
const disabled = !useProfileFieldAllowed('avatar_url') || busy;
|
const disabled = !useProfileEditsAllowed('avatar_url') || busy;
|
||||||
|
|
||||||
const [imageFile, setImageFile] = useState<File>();
|
const [imageFile, setImageFile] = useState<File>();
|
||||||
const imageFileURL = useObjectURL(imageFile);
|
const imageFileURL = useObjectURL(imageFile);
|
||||||
|
|
@ -178,7 +182,7 @@ function ProfileTextField<K extends keyof FilterByValues<ExtendedProfile, string
|
||||||
label,
|
label,
|
||||||
}: ProfileTextFieldProps<K>) {
|
}: ProfileTextFieldProps<K>) {
|
||||||
const { busy, defaultValue, value, setValue } = useProfileField<K>(field);
|
const { busy, defaultValue, value, setValue } = useProfileField<K>(field);
|
||||||
const disabled = !useProfileFieldAllowed(field) || busy;
|
const disabled = !useProfileEditsAllowed(field) || busy;
|
||||||
const hasChanges = defaultValue !== value;
|
const hasChanges = defaultValue !== value;
|
||||||
|
|
||||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (evt) => {
|
const handleChange: ChangeEventHandler<HTMLInputElement> = (evt) => {
|
||||||
|
|
@ -212,6 +216,7 @@ function ProfileTextField<K extends keyof FilterByValues<ExtendedProfile, string
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
variant="Secondary"
|
variant="Secondary"
|
||||||
radii="300"
|
radii="300"
|
||||||
|
disabled={disabled}
|
||||||
readOnly={disabled}
|
readOnly={disabled}
|
||||||
after={
|
after={
|
||||||
hasChanges &&
|
hasChanges &&
|
||||||
|
|
@ -237,7 +242,7 @@ function ProfileTextField<K extends keyof FilterByValues<ExtendedProfile, string
|
||||||
|
|
||||||
function ProfilePronouns() {
|
function ProfilePronouns() {
|
||||||
const { busy, value, setValue } = useProfileField('io.fsky.nyx.pronouns');
|
const { busy, value, setValue } = useProfileField('io.fsky.nyx.pronouns');
|
||||||
const disabled = !useProfileFieldAllowed('io.fsky.nyx.pronouns') || busy;
|
const disabled = !useProfileEditsAllowed('io.fsky.nyx.pronouns') || busy;
|
||||||
|
|
||||||
const [menuCords, setMenuCords] = useState<RectCords>();
|
const [menuCords, setMenuCords] = useState<RectCords>();
|
||||||
const [pendingPronoun, setPendingPronoun] = useState('');
|
const [pendingPronoun, setPendingPronoun] = useState('');
|
||||||
|
|
@ -361,7 +366,7 @@ function ProfilePronouns() {
|
||||||
|
|
||||||
function ProfileTimezone() {
|
function ProfileTimezone() {
|
||||||
const { busy, value, setValue } = useProfileField('us.cloke.msc4175.tz');
|
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<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(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 (
|
||||||
|
<CutoutCard style={{ padding: config.space.S200 }} variant="Surface">
|
||||||
|
<SettingTile
|
||||||
|
description="Change profile settings in your homeserver's account dashboard."
|
||||||
|
after={
|
||||||
|
<Button
|
||||||
|
size="300"
|
||||||
|
variant="Secondary"
|
||||||
|
fill="Soft"
|
||||||
|
radii="300"
|
||||||
|
outlined
|
||||||
|
onClick={openProviderProfileSettings}
|
||||||
|
>
|
||||||
|
<Text size="B300">Open</Text>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</CutoutCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function Profile() {
|
export function Profile() {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const userId = mx.getUserId() as string;
|
const userId = mx.getUserId() as string;
|
||||||
const server = getMxIdServer(userId);
|
const server = getMxIdServer(userId);
|
||||||
|
const authMetadata = useAuthMetadata();
|
||||||
|
const accountManagementActions = useAccountManagementActions();
|
||||||
|
|
||||||
const [extendedProfile, refreshExtendedProfile] = useExtendedProfile(userId);
|
const [extendedProfile, refreshExtendedProfile] = useExtendedProfile(userId);
|
||||||
|
const extendedProfileSupported = extendedProfile !== null;
|
||||||
|
const legacyProfile = useUserProfile(userId);
|
||||||
|
|
||||||
const [fieldDefaults, setFieldDefaults] = useState<ExtendedProfile>({});
|
const profileEditableThroughIDP =
|
||||||
|
authMetadata !== undefined &&
|
||||||
|
authMetadata.account_management_actions_supported?.includes(accountManagementActions.profile);
|
||||||
|
const profileEditableThroughClient = useProfileEditsAllowed(null);
|
||||||
|
|
||||||
|
const [fieldDefaults, setFieldDefaults] = useState<ExtendedProfile>({
|
||||||
|
displayname: legacyProfile.displayName,
|
||||||
|
avatar_url: legacyProfile.avatarUrl,
|
||||||
|
});
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (extendedProfile !== undefined) {
|
if (extendedProfile) {
|
||||||
setFieldDefaults(extendedProfile);
|
setFieldDefaults(extendedProfile);
|
||||||
}
|
}
|
||||||
}, [userId, setFieldDefaults, extendedProfile]);
|
}, [setFieldDefaults, extendedProfile]);
|
||||||
|
|
||||||
const useAuthentication = useMediaAuthentication();
|
const useAuthentication = useMediaAuthentication();
|
||||||
|
|
||||||
const [saveState, handleSave] = useAsyncCallback(
|
const [saveState, handleSave] = useAsyncCallback(
|
||||||
useCallback(
|
useCallback(
|
||||||
async (fields: ExtendedProfile) => {
|
async (fields: ExtendedProfile) => {
|
||||||
await Promise.all(
|
if (extendedProfileSupported) {
|
||||||
Object.entries(fields).map(async ([key, value]) => {
|
await Promise.all(
|
||||||
if (value === undefined) {
|
Object.entries(fields).map(async ([key, value]) => {
|
||||||
await mx.deleteExtendedProfileProperty(key);
|
if (value === undefined) {
|
||||||
} else {
|
await mx.deleteExtendedProfileProperty(key);
|
||||||
await mx.setExtendedProfileProperty(key, value);
|
} else {
|
||||||
}
|
await mx.setExtendedProfileProperty(key, value);
|
||||||
})
|
}
|
||||||
);
|
})
|
||||||
await refreshExtendedProfile();
|
);
|
||||||
// XXX: synthesise a profile update for ourselves because Synapse is broken and won't
|
await refreshExtendedProfile();
|
||||||
const user = mx.getUser(userId);
|
// XXX: synthesise a profile update for ourselves because Synapse is broken and won't
|
||||||
if (user) {
|
const user = mx.getUser(userId);
|
||||||
user.displayName = fields.displayname;
|
if (user) {
|
||||||
user.avatarUrl = fields.avatar_url;
|
user.displayName = fields.displayname;
|
||||||
user.emit(UserEvent.DisplayName, user.events.presence, user);
|
user.avatarUrl = fields.avatar_url;
|
||||||
user.emit(UserEvent.AvatarUrl, user.events.presence, user);
|
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"
|
gap="400"
|
||||||
radii="0"
|
radii="0"
|
||||||
>
|
>
|
||||||
<Box gap="300" direction='Column'>
|
{profileEditableThroughIDP && (
|
||||||
<ProfileAvatar />
|
<IdentityProviderSettings authMetadata={authMetadata} />
|
||||||
<ProfileTextField field="displayname" label="Display Name" />
|
)}
|
||||||
<ProfilePronouns />
|
{profileEditableThroughClient && (
|
||||||
<ProfileTimezone />
|
<>
|
||||||
</Box>
|
<Box gap="300" direction="Column">
|
||||||
<Box gap="300" alignItems="Center">
|
<ProfileAvatar />
|
||||||
<Button
|
<ProfileTextField field="displayname" label="Display Name" />
|
||||||
type="submit"
|
{extendedProfileSupported && (
|
||||||
size="300"
|
<>
|
||||||
variant={!busy && hasChanges ? 'Success' : 'Secondary'}
|
<ProfilePronouns />
|
||||||
fill={!busy && hasChanges ? 'Solid' : 'Soft'}
|
<ProfileTimezone />
|
||||||
outlined
|
</>
|
||||||
radii="300"
|
)}
|
||||||
disabled={!hasChanges || busy}
|
</Box>
|
||||||
onClick={save}
|
<Box gap="300" alignItems="Center">
|
||||||
>
|
<Button
|
||||||
<Text size="B300">Save</Text>
|
type="submit"
|
||||||
</Button>
|
size="300"
|
||||||
<Button
|
variant={!busy && hasChanges ? 'Success' : 'Secondary'}
|
||||||
type="reset"
|
fill={!busy && hasChanges ? 'Solid' : 'Soft'}
|
||||||
size="300"
|
outlined
|
||||||
variant="Secondary"
|
radii="300"
|
||||||
fill="Soft"
|
disabled={!hasChanges || busy}
|
||||||
outlined
|
onClick={save}
|
||||||
radii="300"
|
>
|
||||||
onClick={reset}
|
<Text size="B300">Save</Text>
|
||||||
disabled={!hasChanges || busy}
|
</Button>
|
||||||
>
|
<Button
|
||||||
<Text size="B300">Cancel</Text>
|
type="reset"
|
||||||
</Button>
|
size="300"
|
||||||
{saving && <Spinner size="300" />}
|
variant="Secondary"
|
||||||
</Box>
|
fill="Soft"
|
||||||
|
outlined
|
||||||
|
radii="300"
|
||||||
|
onClick={reset}
|
||||||
|
disabled={!hasChanges || busy}
|
||||||
|
>
|
||||||
|
<Text size="B300">Cancel</Text>
|
||||||
|
</Button>
|
||||||
|
{saving && <Spinner size="300" />}
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!(profileEditableThroughClient || profileEditableThroughIDP) && (
|
||||||
|
<CutoutCard style={{ padding: config.space.S200 }} variant="Critical">
|
||||||
|
<SettingTile>
|
||||||
|
<Box direction="Column" gap="200">
|
||||||
|
<Box gap="200" justifyContent="SpaceBetween">
|
||||||
|
<Text size="L400">Profile Editing Disabled</Text>
|
||||||
|
</Box>
|
||||||
|
<Box direction="Column">
|
||||||
|
<Text size="T200">
|
||||||
|
Your homeserver does not allow you to edit your profile.
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</SettingTile>
|
||||||
|
</CutoutCard>
|
||||||
|
)}
|
||||||
</SequenceCard>
|
</SequenceCard>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ export function useExtendedProfileSupported(): boolean {
|
||||||
|
|
||||||
export function useExtendedProfile(
|
export function useExtendedProfile(
|
||||||
userId: string
|
userId: string
|
||||||
): [ExtendedProfile | undefined, () => Promise<void>] {
|
): [ExtendedProfile | undefined | null, () => Promise<void>] {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const extendedProfileSupported = useExtendedProfileSupported();
|
const extendedProfileSupported = useExtendedProfileSupported();
|
||||||
const { data, refetch } = useQuery({
|
const { data, refetch } = useQuery({
|
||||||
|
|
@ -39,7 +39,7 @@ export function useExtendedProfile(
|
||||||
if (extendedProfileSupported) {
|
if (extendedProfileSupported) {
|
||||||
return extendedProfile.parse(await mx.getExtendedProfile(userId));
|
return extendedProfile.parse(await mx.getExtendedProfile(userId));
|
||||||
}
|
}
|
||||||
return undefined;
|
return null;
|
||||||
}, [mx, userId, extendedProfileSupported]),
|
}, [mx, userId, extendedProfileSupported]),
|
||||||
refetchOnMount: false,
|
refetchOnMount: false,
|
||||||
});
|
});
|
||||||
|
|
@ -54,15 +54,20 @@ export function useExtendedProfile(
|
||||||
|
|
||||||
const LEGACY_FIELDS = ['displayname', 'avatar_url'];
|
const LEGACY_FIELDS = ['displayname', 'avatar_url'];
|
||||||
|
|
||||||
export function useProfileFieldAllowed(field: string): boolean {
|
export function useProfileEditsAllowed(field: string | null): boolean {
|
||||||
const capabilities = useCapabilities();
|
const capabilities = useCapabilities();
|
||||||
const extendedProfileSupported = useExtendedProfileSupported();
|
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
|
// this field might have a pre-msc4133 capability. check that first
|
||||||
if (capabilities[`m.set_${field}`]?.enabled === false) {
|
if (capabilities[`m.set_${field}`]?.enabled === false) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!extendedProfileSupported) {
|
||||||
|
// the homeserver only supports legacy fields
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extendedProfileSupported) {
|
if (extendedProfileSupported) {
|
||||||
|
|
@ -81,6 +86,11 @@ export function useProfileFieldAllowed(field: string): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (field === null) {
|
||||||
|
// profile field modifications are not completely disabled
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
extendedProfileCapability.allowed !== undefined &&
|
extendedProfileCapability.allowed !== undefined &&
|
||||||
!extendedProfileCapability.allowed.includes(field)
|
!extendedProfileCapability.allowed.includes(field)
|
||||||
|
|
@ -98,6 +108,11 @@ export function useProfileFieldAllowed(field: string): boolean {
|
||||||
return true;
|
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
|
// `field` is an extended profile key and the homeserver lacks msc4133 support
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue