All checks were successful
Trigger cinny-mobile / dispatch (push) Successful in 2s
Render the equipped nameplate behind the larger settings avatar with blur-aware sidebar controls, adjustable opacity, and immediate collectible cache invalidation.
202 lines
7.7 KiB
TypeScript
202 lines
7.7 KiB
TypeScript
import React, { MouseEventHandler, useState } from 'react';
|
|
import { Box, config, Line, Menu, MenuItem, PopOut, RectCords, Text } from 'folds';
|
|
import { Icon, Icons } from '../../../components/icons';
|
|
import FocusTrap from 'focus-trap-react';
|
|
import { useNavigateWithTransition } from '../../../hooks/useViewTransitions';
|
|
import { SidebarItem, SidebarAvatar } from '../../../components/sidebar';
|
|
import { UserAvatar } from '../../../components/user-avatar';
|
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
|
import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix';
|
|
import { nameInitials } from '../../../utils/common';
|
|
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
|
import { Settings } from '../../../features/settings';
|
|
import { useUserProfile } from '../../../hooks/useUserProfile';
|
|
import { useOtherUserCollectibles } from '../../../hooks/useUserCollectibles';
|
|
import { useSetting } from '../../../state/hooks/settings';
|
|
import { settingsAtom } from '../../../state/settings';
|
|
import { pickAvatarDecorationUrl, pickNameplateUrl } from '../../../utils/collectibleAssets';
|
|
import * as avatarDecorationCss from '../../../styles/AvatarDecoration.css';
|
|
import * as sidebarCss from '../../../components/sidebar/Sidebar.css';
|
|
import { Modal500 } from '../../../components/Modal500';
|
|
import { AccountSwitcher } from '../../../components/account-switcher';
|
|
import { stopPropagation } from '../../../utils/keyboard';
|
|
import { PluginButtonSlot } from '../../../features/settings/plugins/PluginButtonSlot';
|
|
import { getLoginPath } from '../../pathUtils';
|
|
import { logoutClient } from '../../../../client/initMatrix';
|
|
import { CustomStatusDialog } from '../../../components/custom-status';
|
|
|
|
export function SettingsTab() {
|
|
const mx = useMatrixClient();
|
|
const useAuthentication = useMediaAuthentication();
|
|
const userId = mx.getUserId();
|
|
const profile = useUserProfile(userId ?? '');
|
|
const navigate = useNavigateWithTransition();
|
|
|
|
const [settings, setSettings] = useState(false);
|
|
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
|
|
const [customStatusOpen, setCustomStatusOpen] = useState(false);
|
|
|
|
const displayName = profile.displayName ?? getMxIdLocalPart(userId ?? '') ?? userId ?? '';
|
|
const avatarUrl = profile.avatarUrl && userId
|
|
? mxcUrlToHttp(mx, profile.avatarUrl, useAuthentication, 96, 96, 'crop') ?? undefined
|
|
: undefined;
|
|
const { assetUrls } = useOtherUserCollectibles(userId ?? '');
|
|
const avatarDecorationUrl = pickAvatarDecorationUrl(assetUrls);
|
|
const nameplateUrl = pickNameplateUrl(assetUrls);
|
|
const [sidebarNameplateOpacity] = useSetting(settingsAtom, 'sidebarNameplateOpacity');
|
|
const nameplateIsVideo = Boolean(
|
|
assetUrls['nameplate:animated'] || assetUrls['nameplate:asset.webm']
|
|
);
|
|
|
|
const openSettings = () => setSettings(true);
|
|
const closeSettings = () => setSettings(false);
|
|
|
|
const handleContextMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
|
evt.preventDefault();
|
|
const cords = evt.currentTarget.getBoundingClientRect();
|
|
setMenuAnchor(cords);
|
|
};
|
|
|
|
return (
|
|
<SidebarItem active={settings}>
|
|
<SidebarAvatar
|
|
as="button"
|
|
className={sidebarCss.SidebarAvatarLarge}
|
|
onClick={openSettings}
|
|
onContextMenu={handleContextMenu}
|
|
style={{ overflow: 'visible' }}
|
|
>
|
|
<div className={avatarDecorationCss.SidebarAvatarStack}>
|
|
{sidebarNameplateOpacity > 0 &&
|
|
nameplateUrl &&
|
|
(nameplateIsVideo ? (
|
|
<video
|
|
className={sidebarCss.SidebarAvatarNameplate}
|
|
src={nameplateUrl}
|
|
style={{ opacity: sidebarNameplateOpacity / 100 }}
|
|
autoPlay
|
|
loop
|
|
muted
|
|
playsInline
|
|
aria-hidden="true"
|
|
/>
|
|
) : (
|
|
<img
|
|
className={sidebarCss.SidebarAvatarNameplate}
|
|
src={nameplateUrl}
|
|
style={{ opacity: sidebarNameplateOpacity / 100 }}
|
|
alt=""
|
|
draggable={false}
|
|
/>
|
|
))}
|
|
<UserAvatar
|
|
className={sidebarCss.SidebarAvatarForeground}
|
|
userId={userId ?? ''}
|
|
src={avatarUrl}
|
|
renderFallback={() => <Text size="H4">{nameInitials(displayName)}</Text>}
|
|
/>
|
|
{avatarDecorationUrl && (
|
|
<img
|
|
className={avatarDecorationCss.AvatarDecorationOverlay}
|
|
src={avatarDecorationUrl}
|
|
alt=""
|
|
draggable={false}
|
|
/>
|
|
)}
|
|
</div>
|
|
</SidebarAvatar>
|
|
|
|
<PopOut
|
|
anchor={menuAnchor}
|
|
position="Right"
|
|
align="Start"
|
|
offset={6}
|
|
content={
|
|
<FocusTrap
|
|
focusTrapOptions={{
|
|
initialFocus: false,
|
|
returnFocusOnDeactivate: false,
|
|
onDeactivate: () => setMenuAnchor(undefined),
|
|
clickOutsideDeactivates: true,
|
|
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
|
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
|
escapeDeactivates: stopPropagation,
|
|
}}
|
|
>
|
|
<Menu>
|
|
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
|
<AccountSwitcher currentUserId={userId ?? ''} />
|
|
</Box>
|
|
<Line variant="Surface" size="300" />
|
|
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
|
<MenuItem
|
|
onClick={() => {
|
|
setMenuAnchor(undefined);
|
|
navigate(`${getLoginPath()}?add-account=true`);
|
|
}}
|
|
size="300"
|
|
radii="300"
|
|
>
|
|
<Box gap="200" alignItems="Center">
|
|
<Icon size="100" src={Icons.Plus} />
|
|
<Text as="span" size="T300" truncate>
|
|
Add Account
|
|
</Text>
|
|
</Box>
|
|
</MenuItem>
|
|
<MenuItem
|
|
onClick={() => {
|
|
setMenuAnchor(undefined);
|
|
logoutClient(mx);
|
|
}}
|
|
size="300"
|
|
radii="300"
|
|
variant="Critical"
|
|
fill="None"
|
|
>
|
|
<Text as="span" size="T300" truncate>
|
|
Logout
|
|
</Text>
|
|
</MenuItem>
|
|
</Box>
|
|
<Line variant="Surface" size="300" />
|
|
<Box direction="Row" gap="100" style={{ padding: config.space.S100 }}>
|
|
<PluginButtonSlot location="user-menu" />
|
|
</Box>
|
|
<Line variant="Surface" size="300" />
|
|
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
|
<MenuItem
|
|
onClick={() => {
|
|
setMenuAnchor(undefined);
|
|
setCustomStatusOpen(true);
|
|
}}
|
|
size="300"
|
|
radii="300"
|
|
>
|
|
<Box gap="200" alignItems="Center">
|
|
<Icon size="100" src={Icons.Message} />
|
|
<Text as="span" size="T300" truncate>
|
|
Set Custom Status
|
|
</Text>
|
|
</Box>
|
|
</MenuItem>
|
|
</Box>
|
|
</Menu>
|
|
</FocusTrap>
|
|
}
|
|
/>
|
|
|
|
<CustomStatusDialog
|
|
open={customStatusOpen}
|
|
onClose={() => setCustomStatusOpen(false)}
|
|
/>
|
|
|
|
{settings && (
|
|
<Modal500 requestClose={closeSettings}>
|
|
<Settings requestClose={closeSettings} />
|
|
</Modal500>
|
|
)}
|
|
</SidebarItem>
|
|
);
|
|
}
|