Files
cinny/src/app/pages/client/sidebar/SettingsTab.tsx
litruv d59945eb4c
All checks were successful
Trigger cinny-mobile / dispatch (push) Successful in 2s
Add configurable sidebar nameplate background.
Render the equipped nameplate behind the larger settings avatar with blur-aware sidebar controls, adjustable opacity, and immediate collectible cache invalidation.
2026-08-24 18:48:48 +10:00

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>
);
}