update profile layout and add pronouns
This commit is contained in:
parent
199e66b117
commit
f167780d15
6 changed files with 319 additions and 241 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import React, { MouseEventHandler, useCallback, useMemo, useState } from 'react';
|
||||
import React, { MouseEventHandler, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { isKeyHotkey } from 'is-hotkey';
|
||||
|
|
@ -19,13 +19,12 @@ import {
|
|||
Scroll,
|
||||
Avatar,
|
||||
} from 'folds';
|
||||
import { putAccountData } from 'neurone/lib/api/matrix/client/v3/user';
|
||||
import { getMxIdServer } from '../../utils/matrix';
|
||||
import { useCloseUserRoomProfile } from '../../state/hooks/userRoomProfile';
|
||||
import { stopPropagation } from '../../utils/keyboard';
|
||||
import { copyToClipboard } from '../../utils/dom';
|
||||
import { getExploreServerPath } from '../../pages/pathUtils';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||
import { AsyncStatus } from '../../hooks/useAsyncCallback';
|
||||
import { factoryRoomIdByAtoZ } from '../../utils/sort';
|
||||
import { useMutualRooms } from '../../hooks/useMutualRooms';
|
||||
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
|
||||
|
|
@ -35,11 +34,9 @@ import { RoomAvatar, RoomIcon } from '../room-avatar';
|
|||
import { nameInitials } from '../../utils/common';
|
||||
import { getMatrixToUser } from '../../plugins/matrix-to';
|
||||
import { useTimeoutToggle } from '../../hooks/useTimeoutToggle';
|
||||
import { getIgnoredUsersContent, useIgnoredUsers } from '../../hooks/useIgnoredUsers';
|
||||
import { CutoutCard } from '../cutout-card';
|
||||
import { SettingTile } from '../setting-tile';
|
||||
import { useClient } from '../../hooks/useClient';
|
||||
import { AccountDataType } from '../../../types/matrix/accountData';
|
||||
import { useRoomHeroes } from '../../hooks/storage/useRoomHeros';
|
||||
import { useGetRoomName } from '../../hooks/storage/useGetRoomName';
|
||||
import { useGetRoomAvatar } from '../../hooks/storage/useGetRoomAvatar';
|
||||
|
|
@ -447,86 +444,3 @@ export function IgnoredUserAlert() {
|
|||
</CutoutCard>
|
||||
);
|
||||
}
|
||||
|
||||
export function OptionsChip({ userId }: { userId: string }) {
|
||||
const ct = useClient();
|
||||
const [cords, setCords] = useState<RectCords>();
|
||||
|
||||
const open: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
setCords(evt.currentTarget.getBoundingClientRect());
|
||||
};
|
||||
|
||||
const close = () => setCords(undefined);
|
||||
|
||||
const ignoredUsers = useIgnoredUsers();
|
||||
const ignored = ignoredUsers.includes(userId);
|
||||
|
||||
const [ignoreState, toggleIgnore] = useAsyncCallback(
|
||||
useCallback(async () => {
|
||||
const users = ignoredUsers.filter((u) => u !== userId);
|
||||
if (!ignored) users.push(userId);
|
||||
|
||||
return ct.requestAuthJSON(
|
||||
putAccountData({
|
||||
pathParams: { userId: ct.session.userId, type: AccountDataType.IgnoredUserList },
|
||||
body: getIgnoredUsersContent(users),
|
||||
})
|
||||
);
|
||||
}, [ct, ignoredUsers, userId, ignored])
|
||||
);
|
||||
const ignoring = ignoreState.status === AsyncStatus.Loading;
|
||||
|
||||
return (
|
||||
<PopOut
|
||||
anchor={cords}
|
||||
position="Bottom"
|
||||
align="Start"
|
||||
offset={4}
|
||||
content={
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
onDeactivate: close,
|
||||
clickOutsideDeactivates: true,
|
||||
escapeDeactivates: stopPropagation,
|
||||
isKeyForward: (evt: KeyboardEvent) => isKeyHotkey('arrowdown', evt),
|
||||
isKeyBackward: (evt: KeyboardEvent) => isKeyHotkey('arrowup', evt),
|
||||
}}
|
||||
>
|
||||
<Menu>
|
||||
<div style={{ padding: config.space.S100 }}>
|
||||
<MenuItem
|
||||
variant="Critical"
|
||||
fill="None"
|
||||
size="300"
|
||||
radii="300"
|
||||
onClick={() => {
|
||||
toggleIgnore();
|
||||
close();
|
||||
}}
|
||||
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>
|
||||
</div>
|
||||
</Menu>
|
||||
</FocusTrap>
|
||||
}
|
||||
>
|
||||
<Chip variant="SurfaceVariant" radii="Pill" onClick={open} aria-pressed={!!cords}>
|
||||
{ignoring ? (
|
||||
<Spinner variant="Secondary" size="50" />
|
||||
) : (
|
||||
<Icon size="50" src={Icons.HorizontalDots} />
|
||||
)}
|
||||
</Chip>
|
||||
</PopOut>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import React, { ReactNode, useState } from 'react';
|
||||
import {
|
||||
Avatar,
|
||||
Badge,
|
||||
Box,
|
||||
config,
|
||||
Icon,
|
||||
Icons,
|
||||
Modal,
|
||||
|
|
@ -9,8 +11,6 @@ import {
|
|||
OverlayBackdrop,
|
||||
OverlayCenter,
|
||||
Text,
|
||||
Tooltip,
|
||||
TooltipProvider,
|
||||
} from 'folds';
|
||||
import classNames from 'classnames';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
|
|
@ -19,7 +19,7 @@ import * as css from './styles.css';
|
|||
import { UserAvatar } from '../user-avatar';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
import { getMxIdLocalPart } from '../../utils/matrix';
|
||||
import { BreakWord, DotDivider, HoverUnderline, LineClamp3 } from '../../styles/Text.css';
|
||||
import { BreakWord, LineClamp3 } from '../../styles/Text.css';
|
||||
import { UserPresence } from '../../hooks/useUserPresence';
|
||||
import { AvatarPresence, PresenceBadge } from '../presence';
|
||||
import { ImageViewer } from '../image-viewer';
|
||||
|
|
@ -27,6 +27,7 @@ import { stopPropagation } from '../../utils/keyboard';
|
|||
import { LINKIFY_OPTS } from '../../plugins/react-custom-html-parser';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { UserPronoun } from '../../hooks/useUserProfile';
|
||||
|
||||
export function UserHero({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
|
|
@ -139,47 +140,49 @@ export function UserTimezone({ timezone }: { timezone: string }) {
|
|||
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
|
||||
let time: string | undefined;
|
||||
let offset: string | undefined;
|
||||
|
||||
try {
|
||||
time = new Intl.DateTimeFormat('en-US', {
|
||||
const parts = new Intl.DateTimeFormat('en-US', {
|
||||
timeZone: timezone,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: !hour24Clock,
|
||||
}).format(new Date());
|
||||
timeZoneName: 'shortOffset',
|
||||
}).formatToParts(new Date());
|
||||
|
||||
time = parts
|
||||
.filter((p) => (p.type !== 'timeZoneName' && p.type !== 'literal') || p.type === 'literal')
|
||||
.map((p) => p.value)
|
||||
.join('')
|
||||
.trim();
|
||||
|
||||
offset = parts.find((p) => p.type === 'timeZoneName')?.value; // e.g. "GMT+5:30"
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider
|
||||
position="Top"
|
||||
offset={0}
|
||||
tooltip={
|
||||
<Tooltip>
|
||||
<Text size="L400">{timezone}</Text>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
{(triggerRef) => (
|
||||
<Text as="span" ref={triggerRef} className={HoverUnderline} size="T200" priority="300">
|
||||
{time}
|
||||
</Text>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
<Box as="span" alignItems="Center" gap="100">
|
||||
<Icon style={{ opacity: config.opacity.P300 }} size="50" src={Icons.Clock} />
|
||||
<Text as="span" size="T200" priority="300">
|
||||
{time} local time{offset ? ` · ${offset}` : ` · ${timezone}`}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
type UserHeroNameProps = {
|
||||
displayName?: string;
|
||||
userId: string;
|
||||
timezone?: string;
|
||||
pronouns?: UserPronoun[];
|
||||
};
|
||||
export function UserHeroName({ displayName, userId, timezone }: UserHeroNameProps) {
|
||||
export function UserHeroName({ displayName, userId, pronouns }: UserHeroNameProps) {
|
||||
const username = getMxIdLocalPart(userId);
|
||||
|
||||
return (
|
||||
<Box grow="Yes" direction="Column" gap="100">
|
||||
<Box alignItems="Baseline" gap="100" wrap="Wrap">
|
||||
<Box alignItems="Center" gap="100" wrap="Wrap">
|
||||
<Text
|
||||
size="H4"
|
||||
className={classNames(BreakWord, LineClamp3)}
|
||||
|
|
@ -188,12 +191,17 @@ export function UserHeroName({ displayName, userId, timezone }: UserHeroNameProp
|
|||
{displayName ?? username ?? userId}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box alignItems="Center" gap="100" wrap="Wrap">
|
||||
<Box alignItems="Center" gap="200" wrap="Wrap">
|
||||
<Text size="T200" className={classNames(BreakWord, LineClamp3)} title={username}>
|
||||
@{username}
|
||||
</Text>
|
||||
{timezone && <span className={DotDivider} />}
|
||||
{timezone && <UserTimezone timezone={timezone} />}
|
||||
{pronouns?.map((pronoun) =>
|
||||
pronoun.summary ? (
|
||||
<Badge key={pronoun.summary} variant="Primary" fill="Soft" size="400">
|
||||
<Text size="L400">{pronoun.summary}</Text>
|
||||
</Badge>
|
||||
) : undefined
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,27 @@
|
|||
import { Box, Button, color, config, Icon, Icons, Spinner, Text, Input } from 'folds';
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
color,
|
||||
config,
|
||||
Icon,
|
||||
Icons,
|
||||
Spinner,
|
||||
Text,
|
||||
Input,
|
||||
RectCords,
|
||||
PopOut,
|
||||
Menu,
|
||||
MenuItem,
|
||||
toRem,
|
||||
Line,
|
||||
IconButton,
|
||||
} from 'folds';
|
||||
import React, { MouseEventHandler, useCallback, useRef, useState } from 'react';
|
||||
import { postBan, postInvite, postKick, postUnban } from 'neurone/lib/api/matrix/client/v3/rooms';
|
||||
import { parseMatrixResp } from 'neurone/lib/matrix-request';
|
||||
import { putAccountData } from 'neurone/lib/api/matrix/client/v3/user';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { isKeyHotkey } from 'is-hotkey';
|
||||
import { useRoom } from '../../hooks/useRoom';
|
||||
import { CutoutCard } from '../cutout-card';
|
||||
import { SettingTile } from '../setting-tile';
|
||||
|
|
@ -11,6 +31,9 @@ import { useSetting } from '../../state/hooks/settings';
|
|||
import { settingsAtom } from '../../state/settings';
|
||||
import { timeDayMonYear, timeHourMinute } from '../../utils/time';
|
||||
import { useClient } from '../../hooks/useClient';
|
||||
import { getIgnoredUsersContent, useIgnoredUsers } from '../../hooks/useIgnoredUsers';
|
||||
import { AccountDataType } from '../../../types/matrix/accountData';
|
||||
import { stopPropagation } from '../../utils/keyboard';
|
||||
|
||||
type UserKickAlertProps = {
|
||||
reason?: string;
|
||||
|
|
@ -293,97 +316,206 @@ export function UserModeration({ userId, canKick, canBan, canInvite }: UserModer
|
|||
if (!canBan && !canKick && !canInvite) return null;
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="400">
|
||||
<Box direction="Column" gap="200">
|
||||
<Box grow="Yes" direction="Column" gap="100">
|
||||
<Text size="L400">Moderation</Text>
|
||||
<Input
|
||||
ref={reasonInputRef}
|
||||
placeholder="Reason"
|
||||
<Box direction="Column" gap="100">
|
||||
<Box grow="Yes" direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<Text size="L400">Moderation</Text>
|
||||
<Input
|
||||
ref={reasonInputRef}
|
||||
placeholder="Reason (Optional)"
|
||||
size="300"
|
||||
variant="Background"
|
||||
outlined
|
||||
radii="300"
|
||||
disabled={disabled}
|
||||
/>
|
||||
{kickState.status === AsyncStatus.Error && (
|
||||
<Text style={{ color: color.Critical.Main }} className={BreakWord} size="T200">
|
||||
<b>{kickState.error.message}</b>
|
||||
</Text>
|
||||
)}
|
||||
{banState.status === AsyncStatus.Error && (
|
||||
<Text style={{ color: color.Critical.Main }} className={BreakWord} size="T200">
|
||||
<b>{banState.error.message}</b>
|
||||
</Text>
|
||||
)}
|
||||
{inviteState.status === AsyncStatus.Error && (
|
||||
<Text style={{ color: color.Critical.Main }} className={BreakWord} size="T200">
|
||||
<b>{inviteState.error.message}</b>
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Box shrink="No" gap="100" direction="Column">
|
||||
{canInvite && (
|
||||
<MenuItem
|
||||
style={{ flexGrow: 1 }}
|
||||
size="300"
|
||||
variant="Background"
|
||||
variant="Secondary"
|
||||
fill="None"
|
||||
radii="300"
|
||||
after={
|
||||
inviteState.status === AsyncStatus.Loading ? (
|
||||
<Spinner size="50" variant="Secondary" fill="Soft" />
|
||||
) : (
|
||||
<Icon size="50" src={Icons.Mail} />
|
||||
)
|
||||
}
|
||||
onClick={invite}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{kickState.status === AsyncStatus.Error && (
|
||||
<Text style={{ color: color.Critical.Main }} className={BreakWord} size="T200">
|
||||
<b>{kickState.error.message}</b>
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} size="B300">
|
||||
Invite to Room
|
||||
</Text>
|
||||
)}
|
||||
{banState.status === AsyncStatus.Error && (
|
||||
<Text style={{ color: color.Critical.Main }} className={BreakWord} size="T200">
|
||||
<b>{banState.error.message}</b>
|
||||
</MenuItem>
|
||||
)}
|
||||
{canKick && (
|
||||
<MenuItem
|
||||
style={{ flexGrow: 1 }}
|
||||
size="300"
|
||||
variant="Warning"
|
||||
fill="None"
|
||||
radii="300"
|
||||
after={
|
||||
kickState.status === AsyncStatus.Loading ? (
|
||||
<Spinner size="50" variant="Critical" fill="Soft" />
|
||||
) : (
|
||||
<Icon size="50" src={Icons.ArrowLeft} />
|
||||
)
|
||||
}
|
||||
onClick={kick}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} size="B300">
|
||||
Kick from Room
|
||||
</Text>
|
||||
)}
|
||||
{inviteState.status === AsyncStatus.Error && (
|
||||
<Text style={{ color: color.Critical.Main }} className={BreakWord} size="T200">
|
||||
<b>{inviteState.error.message}</b>
|
||||
</MenuItem>
|
||||
)}
|
||||
{canBan && (
|
||||
<MenuItem
|
||||
style={{ flexGrow: 1 }}
|
||||
size="300"
|
||||
variant="Critical"
|
||||
fill="None"
|
||||
radii="300"
|
||||
after={
|
||||
banState.status === AsyncStatus.Loading ? (
|
||||
<Spinner size="50" variant="Critical" fill="Solid" />
|
||||
) : (
|
||||
<Icon size="50" src={Icons.NoEntry} />
|
||||
)
|
||||
}
|
||||
onClick={ban}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} size="B300">
|
||||
Ban from Room
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Box shrink="No" gap="200">
|
||||
{canInvite && (
|
||||
<Button
|
||||
style={{ flexGrow: 1 }}
|
||||
size="300"
|
||||
variant="Secondary"
|
||||
fill="Soft"
|
||||
radii="300"
|
||||
before={
|
||||
inviteState.status === AsyncStatus.Loading ? (
|
||||
<Spinner size="50" variant="Secondary" fill="Soft" />
|
||||
) : (
|
||||
<Icon size="50" src={Icons.ArrowRight} />
|
||||
)
|
||||
}
|
||||
onClick={invite}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Text size="B300">Invite</Text>
|
||||
</Button>
|
||||
)}
|
||||
{canKick && (
|
||||
<Button
|
||||
style={{ flexGrow: 1 }}
|
||||
size="300"
|
||||
variant="Critical"
|
||||
fill="Soft"
|
||||
radii="300"
|
||||
before={
|
||||
kickState.status === AsyncStatus.Loading ? (
|
||||
<Spinner size="50" variant="Critical" fill="Soft" />
|
||||
) : (
|
||||
<Icon size="50" src={Icons.ArrowLeft} />
|
||||
)
|
||||
}
|
||||
onClick={kick}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Text size="B300">Kick</Text>
|
||||
</Button>
|
||||
)}
|
||||
{canBan && (
|
||||
<Button
|
||||
style={{ flexGrow: 1 }}
|
||||
size="300"
|
||||
variant="Critical"
|
||||
fill="Solid"
|
||||
radii="300"
|
||||
before={
|
||||
banState.status === AsyncStatus.Loading ? (
|
||||
<Spinner size="50" variant="Critical" fill="Solid" />
|
||||
) : (
|
||||
<Icon size="50" src={Icons.Prohibited} />
|
||||
)
|
||||
}
|
||||
onClick={ban}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Text size="B300">Ban</Text>
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function OptionsButton({ userId, canBan, canInvite, canKick }: UserModerationProps) {
|
||||
const ct = useClient();
|
||||
const [cords, setCords] = useState<RectCords>();
|
||||
|
||||
const open: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
setCords(evt.currentTarget.getBoundingClientRect());
|
||||
};
|
||||
|
||||
const close = () => setCords(undefined);
|
||||
|
||||
const ignoredUsers = useIgnoredUsers();
|
||||
const ignored = ignoredUsers.includes(userId);
|
||||
|
||||
const [ignoreState, toggleIgnore] = useAsyncCallback(
|
||||
useCallback(async () => {
|
||||
const users = ignoredUsers.filter((u) => u !== userId);
|
||||
if (!ignored) users.push(userId);
|
||||
|
||||
return ct.requestAuthJSON(
|
||||
putAccountData({
|
||||
pathParams: { userId: ct.session.userId, type: AccountDataType.IgnoredUserList },
|
||||
body: getIgnoredUsersContent(users),
|
||||
})
|
||||
);
|
||||
}, [ct, ignoredUsers, userId, ignored])
|
||||
);
|
||||
const ignoring = ignoreState.status === AsyncStatus.Loading;
|
||||
|
||||
return (
|
||||
<PopOut
|
||||
anchor={cords}
|
||||
position="Bottom"
|
||||
align="Center"
|
||||
offset={4}
|
||||
content={
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
onDeactivate: close,
|
||||
clickOutsideDeactivates: true,
|
||||
escapeDeactivates: stopPropagation,
|
||||
isKeyForward: (evt: KeyboardEvent) => isKeyHotkey('arrowdown', evt),
|
||||
isKeyBackward: (evt: KeyboardEvent) => isKeyHotkey('arrowup', evt),
|
||||
}}
|
||||
>
|
||||
<Menu style={{ minWidth: toRem(175) }}>
|
||||
<Box direction="Column" gap="0">
|
||||
{(canInvite || canBan || canKick) && (
|
||||
<>
|
||||
<div style={{ padding: config.space.S100 }}>
|
||||
<UserModeration
|
||||
userId={userId}
|
||||
canInvite={canInvite}
|
||||
canKick={canKick}
|
||||
canBan={canBan}
|
||||
/>
|
||||
</div>
|
||||
<Line size="300" />
|
||||
</>
|
||||
)}
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<MenuItem
|
||||
variant="Surface"
|
||||
fill="Soft"
|
||||
size="300"
|
||||
radii="300"
|
||||
onClick={() => {
|
||||
toggleIgnore();
|
||||
close();
|
||||
}}
|
||||
after={
|
||||
ignoring ? (
|
||||
<Spinner variant="Critical" size="50" />
|
||||
) : (
|
||||
<Icon size="50" src={Icons.Prohibited} />
|
||||
)
|
||||
}
|
||||
disabled={ignoring}
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} size="B300">
|
||||
{ignored ? 'Unblock User' : 'Block User'}
|
||||
</Text>
|
||||
</MenuItem>
|
||||
</Box>
|
||||
</Box>
|
||||
</Menu>
|
||||
</FocusTrap>
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
style={{ flexShrink: 0 }}
|
||||
variant="SurfaceVariant"
|
||||
fill="Soft"
|
||||
size="400"
|
||||
radii="400"
|
||||
onClick={open}
|
||||
aria-pressed={!!cords}
|
||||
>
|
||||
<Icon size="50" src={Icons.VerticalDots} />
|
||||
</IconButton>
|
||||
</PopOut>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Box, Button, config, Icon, Icons, Text } from 'folds';
|
|||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { ObjectReader } from 'neurone/lib/utils';
|
||||
import { UserHero, UserHeroAvatar, UserHeroCover, UserHeroName } from './UserHero';
|
||||
import { UserHero, UserHeroAvatar, UserHeroCover, UserHeroName, UserTimezone } from './UserHero';
|
||||
import { getMxIdServer } from '../../utils/matrix';
|
||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||
import {
|
||||
|
|
@ -12,10 +12,10 @@ import {
|
|||
} from '../../hooks/usePowerLevels';
|
||||
import { useRoom } from '../../hooks/useRoom';
|
||||
import { useUserPresence } from '../../hooks/useUserPresence';
|
||||
import { IgnoredUserAlert, MutualRoomsChip, OptionsChip, ServerChip, ShareChip } from './UserChips';
|
||||
import { IgnoredUserAlert, MutualRoomsChip, ServerChip, ShareChip } from './UserChips';
|
||||
import { useCloseUserRoomProfile } from '../../state/hooks/userRoomProfile';
|
||||
import { PowerChip } from './PowerChip';
|
||||
import { UserInviteAlert, UserBanAlert, UserModeration, UserKickAlert } from './UserModeration';
|
||||
import { UserInviteAlert, UserBanAlert, UserKickAlert, OptionsButton } from './UserModeration';
|
||||
import { useIgnoredUsers } from '../../hooks/useIgnoredUsers';
|
||||
import { Membership } from '../../../types/matrix/room';
|
||||
import { useRoomCreators } from '../../hooks/useRoomCreators';
|
||||
|
|
@ -33,7 +33,6 @@ import {
|
|||
usePowerLevelTagsState,
|
||||
} from '../../hooks/usePowerLevelTags';
|
||||
import { useUserProfile } from '../../hooks/useUserProfile';
|
||||
import { UserActionButton } from './styles.css';
|
||||
|
||||
type UserRoomProfileProps = {
|
||||
userId: string;
|
||||
|
|
@ -101,21 +100,6 @@ export function UserRoomProfile({ userId }: UserRoomProfileProps) {
|
|||
bannerUrl={bannerUrl}
|
||||
hasStatus={!!presence?.status}
|
||||
/>
|
||||
{userId !== myUserId && (
|
||||
<Box grow="Yes" style={{ position: 'relative' }}>
|
||||
<Button
|
||||
className={UserActionButton}
|
||||
size="300"
|
||||
variant="Primary"
|
||||
fill="Solid"
|
||||
radii="300"
|
||||
before={<Icon size="50" src={Icons.Message} filled />}
|
||||
onClick={handleMessage}
|
||||
>
|
||||
<Text size="B300">Message</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
<UserHeroAvatar
|
||||
userId={userId}
|
||||
avatarUrl={avatarUrl}
|
||||
|
|
@ -124,17 +108,31 @@ export function UserRoomProfile({ userId }: UserRoomProfileProps) {
|
|||
</UserHero>
|
||||
<Box direction="Column" gap="500" style={{ padding: config.space.S400 }}>
|
||||
<Box direction="Column" gap="400">
|
||||
<UserHeroName
|
||||
displayName={displayName}
|
||||
userId={userId}
|
||||
timezone={profile?.timezone}
|
||||
/>
|
||||
<Box alignItems="Start" gap="200" grow="Yes">
|
||||
<UserHeroName
|
||||
displayName={displayName}
|
||||
userId={userId}
|
||||
pronouns={profile?.pronouns}
|
||||
/>
|
||||
{userId !== myUserId && (
|
||||
<OptionsButton
|
||||
userId={userId}
|
||||
canInvite={canInvite && membership === 'leave'}
|
||||
canKick={canKickUser && membership === 'join'}
|
||||
canBan={canBanUser && membership !== 'ban'}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
{profile?.timezone && <UserTimezone timezone={profile.timezone} />}
|
||||
<Text size="T200">
|
||||
now hierarchy does not have room summary info we need to load it when we will be
|
||||
rendering it
|
||||
</Text>
|
||||
<Box alignItems="Center" gap="200" wrap="Wrap">
|
||||
{server && <ServerChip server={server} />}
|
||||
<ShareChip userId={userId} />
|
||||
{creator ? <CreatorChip /> : <PowerChip userId={userId} />}
|
||||
{userId !== myUserId && <MutualRoomsChip userId={userId} />}
|
||||
{userId !== myUserId && <OptionsChip userId={userId} />}
|
||||
</Box>
|
||||
</Box>
|
||||
{ignored && <IgnoredUserAlert />}
|
||||
|
|
@ -165,12 +163,21 @@ export function UserRoomProfile({ userId }: UserRoomProfileProps) {
|
|||
ts={memberEventResult.value.event.origin_server_ts}
|
||||
/>
|
||||
)}
|
||||
<UserModeration
|
||||
userId={userId}
|
||||
canInvite={canInvite && membership === Membership.Leave}
|
||||
canKick={canKickUser && membership === Membership.Join}
|
||||
canBan={canBanUser && membership !== Membership.Ban}
|
||||
/>
|
||||
{userId !== myUserId && (
|
||||
<Box direction="Row" gap="200">
|
||||
<Button
|
||||
style={{ flexGrow: 1 }}
|
||||
size="300"
|
||||
variant="Primary"
|
||||
fill="Solid"
|
||||
radii="300"
|
||||
before={<Icon size="50" src={Icons.Message} />}
|
||||
onClick={handleMessage}
|
||||
>
|
||||
<Text size="B300">Message</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</PowerLevelTagsContextProvider>
|
||||
|
|
|
|||
|
|
@ -106,11 +106,3 @@ export const UserHeroStatusContent = style({
|
|||
color: color.Surface.OnContainer,
|
||||
borderRadius: config.radii.R500,
|
||||
});
|
||||
|
||||
export const UserActionButton = style({
|
||||
position: 'absolute',
|
||||
right: config.space.S400,
|
||||
zIndex: 1,
|
||||
transform: 'translateY(-10%)',
|
||||
outline: `${config.borderWidth.B600} solid ${color.Surface.Container}`,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,13 +2,27 @@ import { useCallback } from 'react';
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getProfile, GetProfileOK } from 'neurone/lib/api/matrix/client/v3/profile';
|
||||
import { parseMatrixResp } from 'neurone/lib/matrix-request';
|
||||
import { ObjectReader } from 'neurone/lib/utils';
|
||||
import { useClient } from './useClient';
|
||||
|
||||
type PronounResponse = {
|
||||
grammatical_gender?: string;
|
||||
language?: string;
|
||||
summary?: string;
|
||||
};
|
||||
|
||||
export type UserPronoun = {
|
||||
grammaticalGender?: string;
|
||||
language?: string;
|
||||
summary?: string;
|
||||
};
|
||||
|
||||
export type UserProfile = {
|
||||
avatarUrl?: string;
|
||||
displayName?: string;
|
||||
bannerUrl?: string;
|
||||
timezone?: string;
|
||||
pronouns?: UserPronoun[];
|
||||
};
|
||||
export const useUserProfile = (userId: string): UserProfile | undefined => {
|
||||
const ct = useClient();
|
||||
|
|
@ -25,14 +39,25 @@ export const useUserProfile = (userId: string): UserProfile | undefined => {
|
|||
'chat.commet.profile_banner'?: string;
|
||||
'm.banner_url'?: string;
|
||||
'm.tz'?: string;
|
||||
'io.fsky.nyx.pronouns': PronounResponse[];
|
||||
'm.pronouns': PronounResponse[];
|
||||
}
|
||||
>(response);
|
||||
|
||||
const pronouns = profile?.['m.pronouns'] ?? profile?.['io.fsky.nyx.pronouns'];
|
||||
|
||||
return {
|
||||
avatarUrl: profile?.avatar_url || undefined,
|
||||
displayName: profile?.displayname || undefined,
|
||||
bannerUrl: profile?.['m.banner_url'] ?? profile?.['chat.commet.profile_banner'] ?? undefined,
|
||||
timezone: profile?.['m.tz'] ?? undefined,
|
||||
bannerUrl:
|
||||
ObjectReader.string(profile, 'm.banner_url') ??
|
||||
ObjectReader.string(profile, 'chat.commet.profile_banner'),
|
||||
timezone: ObjectReader.string(profile, 'm.tz'),
|
||||
pronouns: pronouns?.map((p) => ({
|
||||
grammaticalGender: ObjectReader.string(p, 'grammatical_gender'),
|
||||
language: ObjectReader.string(p, 'language'),
|
||||
summary: ObjectReader.string(p, 'summary'),
|
||||
})),
|
||||
};
|
||||
}, [ct, userId]);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue