Add a context menu option to view a user's raw extended profile fields

This commit is contained in:
Ginger 2025-10-06 14:02:50 -04:00
parent d42bcc6e3d
commit 205ea1655a
No known key found for this signature in database
2 changed files with 116 additions and 60 deletions

View file

@ -22,6 +22,10 @@ import {
TooltipProvider, TooltipProvider,
Tooltip, Tooltip,
Badge, Badge,
Overlay,
OverlayBackdrop,
OverlayCenter,
Modal,
} from 'folds'; } from 'folds';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { getMxIdServer } from '../../utils/matrix'; import { getMxIdServer } from '../../utils/matrix';
@ -45,6 +49,10 @@ import { useIgnoredUsers } from '../../hooks/useIgnoredUsers';
import { CutoutCard } from '../cutout-card'; import { CutoutCard } from '../cutout-card';
import { SettingTile } from '../setting-tile'; import { SettingTile } from '../setting-tile';
import { useInterval } from '../../hooks/useInterval'; import { useInterval } from '../../hooks/useInterval';
import { TextViewer } from '../text-viewer';
import { ExtendedProfile } from '../../hooks/useExtendedProfile';
import { settingsAtom } from '../../state/settings';
import { useSetting } from '../../state/hooks/settings';
export function ServerChip({ server }: { server: string }) { export function ServerChip({ server }: { server: string }) {
const mx = useMatrixClient(); const mx = useMatrixClient();
@ -440,15 +448,24 @@ export function IgnoredUserAlert() {
); );
} }
export function OptionsChip({ userId }: { userId: string }) { export function OptionsChip({
userId,
extendedProfile,
}: {
userId: string;
extendedProfile: ExtendedProfile | null;
}) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const [cords, setCords] = useState<RectCords>(); const [developerToolsEnabled] = useSetting(settingsAtom, 'developerTools');
const open: MouseEventHandler<HTMLButtonElement> = (evt) => { const [profileFieldsOpen, setProfileFieldsOpen] = useState(false);
setCords(evt.currentTarget.getBoundingClientRect()); const [menuCoords, setMenuCoords] = useState<RectCords>();
const openMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
setMenuCoords(evt.currentTarget.getBoundingClientRect());
}; };
const close = () => setCords(undefined); const closeMenu = () => setMenuCoords(undefined);
const ignoredUsers = useIgnoredUsers(); const ignoredUsers = useIgnoredUsers();
const ignored = ignoredUsers.includes(userId); const ignored = ignoredUsers.includes(userId);
@ -463,57 +480,96 @@ export function OptionsChip({ userId }: { userId: string }) {
const ignoring = ignoreState.status === AsyncStatus.Loading; const ignoring = ignoreState.status === AsyncStatus.Loading;
return ( return (
<PopOut <>
anchor={cords} {extendedProfile && (
position="Bottom" <Overlay open={profileFieldsOpen} backdrop={<OverlayBackdrop />}>
align="Start" <OverlayCenter>
offset={4} <FocusTrap
content={ focusTrapOptions={{
<FocusTrap clickOutsideDeactivates: true,
focusTrapOptions={{ onDeactivate: () => setProfileFieldsOpen(false),
initialFocus: false, escapeDeactivates: stopPropagation,
onDeactivate: close, }}
clickOutsideDeactivates: true, >
escapeDeactivates: stopPropagation, <Modal variant="Surface" size="500">
isKeyForward: (evt: KeyboardEvent) => isKeyHotkey('arrowdown', evt), <TextViewer
isKeyBackward: (evt: KeyboardEvent) => isKeyHotkey('arrowup', evt), name="Profile Fields"
}} langName="json"
> text={JSON.stringify(extendedProfile, null, 2)}
<Menu> requestClose={() => setProfileFieldsOpen(false)}
<div style={{ padding: config.space.S100 }}> />
<MenuItem </Modal>
variant="Critical" </FocusTrap>
fill="None" </OverlayCenter>
size="300" </Overlay>
radii="300" )}
onClick={() => { <PopOut
toggleIgnore(); anchor={menuCoords}
close(); position="Bottom"
}} align="Start"
before={ offset={4}
ignoring ? ( content={
<Spinner variant="Critical" size="50" /> <FocusTrap
) : ( focusTrapOptions={{
<Icon size="50" src={Icons.Prohibited} /> initialFocus: false,
) onDeactivate: closeMenu,
} clickOutsideDeactivates: true,
disabled={ignoring} escapeDeactivates: stopPropagation,
> isKeyForward: (evt: KeyboardEvent) => isKeyHotkey('arrowdown', evt),
<Text size="B300">{ignored ? 'Unblock User' : 'Block User'}</Text> isKeyBackward: (evt: KeyboardEvent) => isKeyHotkey('arrowup', evt),
</MenuItem> }}
</div> >
</Menu> <Menu>
</FocusTrap> <div style={{ padding: config.space.S100 }}>
} <MenuItem
> variant="Critical"
<Chip variant="SurfaceVariant" radii="Pill" onClick={open} aria-pressed={!!cords}> fill="None"
{ignoring ? ( size="300"
<Spinner variant="Secondary" size="50" /> radii="300"
) : ( onClick={() => {
<Icon size="50" src={Icons.HorizontalDots} /> toggleIgnore();
)} closeMenu();
</Chip> }}
</PopOut> before={
ignoring ? (
<Spinner variant="Critical" size="50" />
) : (
<Icon size="50" src={Icons.Prohibited} />
)
}
disabled={ignoring}
>
<Text size="B300">{ignored ? 'Unblock User' : 'Block User'}</Text>
</MenuItem>
{extendedProfile && developerToolsEnabled && (
<MenuItem
variant="Surface"
fill="None"
size="300"
radii="300"
onClick={() => {
setProfileFieldsOpen(true);
closeMenu();
}}
before={<Icon size="50" src={Icons.BlockCode} />}
>
<Text size="B300">View Profile Fields</Text>
</MenuItem>
)}
</div>
</Menu>
</FocusTrap>
}
>
<Chip variant="SurfaceVariant" radii="Pill" onClick={openMenu} aria-pressed={!!menuCoords}>
{ignoring ? (
<Spinner variant="Secondary" size="50" />
) : (
<Icon size="50" src={Icons.HorizontalDots} />
)}
</Chip>
</PopOut>
</>
); );
} }
@ -555,8 +611,8 @@ export function TimezoneChip({ timezone }: { timezone: string }) {
offset={5} offset={5}
align="Center" align="Center"
tooltip={ tooltip={
<Tooltip variant='SurfaceVariant' style={{ maxWidth: toRem(280) }}> <Tooltip variant="SurfaceVariant" style={{ maxWidth: toRem(280) }}>
<Box direction="Column" alignItems='Start' gap="100"> <Box direction="Column" alignItems="Start" gap="100">
<Box gap="100"> <Box gap="100">
<Text size="L400">Timezone:</Text> <Text size="L400">Timezone:</Text>
<Badge size="400" variant="Primary"> <Badge size="400" variant="Primary">
@ -573,7 +629,7 @@ export function TimezoneChip({ timezone }: { timezone: string }) {
ref={triggerRef} ref={triggerRef}
variant="SurfaceVariant" variant="SurfaceVariant"
radii="Pill" radii="Pill"
style={{ cursor: "initial" }} style={{ cursor: 'initial' }}
before={<Icon size="50" src={Icons.RecentClock} />} before={<Icon size="50" src={Icons.RecentClock} />}
> >
<Text size="B300" truncate> <Text size="B300" truncate>

View file

@ -115,7 +115,7 @@ export function UserRoomProfile({ userId }: UserRoomProfileProps) {
{timezone && <TimezoneChip timezone={timezone} />} {timezone && <TimezoneChip timezone={timezone} />}
{creator ? <CreatorChip /> : <PowerChip userId={userId} />} {creator ? <CreatorChip /> : <PowerChip userId={userId} />}
{userId !== myUserId && <MutualRoomsChip userId={userId} />} {userId !== myUserId && <MutualRoomsChip userId={userId} />}
{userId !== myUserId && <OptionsChip userId={userId} />} {userId !== myUserId && <OptionsChip userId={userId} extendedProfile={extendedProfile ?? null} />}
</Box> </Box>
</Box> </Box>
{ignored && <IgnoredUserAlert />} {ignored && <IgnoredUserAlert />}