Compare commits
1 Commits
0cbc5d9a1c
...
d59945eb4c
| Author | SHA1 | Date | |
|---|---|---|---|
| d59945eb4c |
@@ -9,6 +9,8 @@ export const Sidebar = style([
|
|||||||
width: toRem(66),
|
width: toRem(66),
|
||||||
backgroundColor: color.Background.Container,
|
backgroundColor: color.Background.Container,
|
||||||
borderRight: `${config.borderWidth.B300} solid ${color.Background.ContainerLine}`,
|
borderRight: `${config.borderWidth.B300} solid ${color.Background.ContainerLine}`,
|
||||||
|
position: 'relative',
|
||||||
|
isolation: 'isolate',
|
||||||
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
@@ -185,6 +187,46 @@ export const SidebarAvatar = recipe({
|
|||||||
});
|
});
|
||||||
export type SidebarAvatarVariants = RecipeVariants<typeof SidebarAvatar>;
|
export type SidebarAvatarVariants = RecipeVariants<typeof SidebarAvatar>;
|
||||||
|
|
||||||
|
/** A vertical rendition of the user's nameplate behind the bottom settings avatar. */
|
||||||
|
export const SidebarAvatarNameplate = style({
|
||||||
|
position: 'absolute',
|
||||||
|
// The 50px avatar is centered in the 66px sidebar. Pin the rotated plate's
|
||||||
|
// bottom-right corner 8px left of it so the 66px cross-axis
|
||||||
|
// spans the whole sidebar and grows upward from its bottom edge.
|
||||||
|
right: toRem(58),
|
||||||
|
bottom: toRem(-16),
|
||||||
|
width: toRem(220),
|
||||||
|
height: toRem(66),
|
||||||
|
transformOrigin: 'right bottom',
|
||||||
|
transform: 'rotate(90deg)',
|
||||||
|
objectFit: 'cover',
|
||||||
|
objectPosition: 'right center',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
zIndex: -1,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SidebarAvatarLarge = style({
|
||||||
|
width: toRem(50),
|
||||||
|
height: toRem(50),
|
||||||
|
borderRadius: '50%',
|
||||||
|
backgroundColor: `color-mix(in srgb, ${color.Background.Container} 68%, transparent)`,
|
||||||
|
backdropFilter: 'blur(10px)',
|
||||||
|
WebkitBackdropFilter: 'blur(10px)',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SidebarSearchAvatar = style({
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: 1,
|
||||||
|
backgroundColor: `color-mix(in srgb, ${color.Background.Container} 68%, transparent)`,
|
||||||
|
backdropFilter: 'blur(10px)',
|
||||||
|
WebkitBackdropFilter: 'blur(10px)',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SidebarAvatarForeground = style({
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: 1,
|
||||||
|
});
|
||||||
|
|
||||||
export const SidebarFolder = recipe({
|
export const SidebarFolder = recipe({
|
||||||
base: [
|
base: [
|
||||||
ContainerColor({ variant: 'Background' }),
|
ContainerColor({ variant: 'Background' }),
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import React, {
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { as, Box, Button, Chip, color, config, Header, IconButton, Input, Menu, MenuItem, PopOut, RectCords, Scroll, Switch, Text, toRem } from 'folds';
|
import { as, Box, Button, Chip, color, config, Header, IconButton, Input, Menu, MenuItem, PopOut, RectCords, Scroll, Switch, Text, toRem } from 'folds';
|
||||||
import { useSetAtom } from 'jotai';
|
import { useSetAtom } from 'jotai';
|
||||||
|
import { Range } from 'react-range';
|
||||||
import { Icon, Icons } from '../../../components/icons';
|
import { Icon, Icons } from '../../../components/icons';
|
||||||
import { HexColorPicker } from 'react-colorful';
|
import { HexColorPicker } from 'react-colorful';
|
||||||
import { isKeyHotkey } from 'is-hotkey';
|
import { isKeyHotkey } from 'is-hotkey';
|
||||||
@@ -334,6 +335,66 @@ const emojiStyleNames: Record<EmojiStyle, string> = {
|
|||||||
[EmojiStyle.Twemoji]: 'Twemoji',
|
[EmojiStyle.Twemoji]: 'Twemoji',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function SidebarNameplate() {
|
||||||
|
const [sidebarNameplateOpacity, setSidebarNameplateOpacity] = useSetting(
|
||||||
|
settingsAtom,
|
||||||
|
'sidebarNameplateOpacity'
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box direction="Column" gap="100">
|
||||||
|
<Text size="L400">Collectibles</Text>
|
||||||
|
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||||
|
<SettingTile
|
||||||
|
title="Sidebar Nameplate Opacity"
|
||||||
|
description="Set to 0% to hide your equipped nameplate behind the bottom sidebar avatar."
|
||||||
|
after={
|
||||||
|
<Box gap="200" alignItems="Center" style={{ width: toRem(180) }}>
|
||||||
|
<Range
|
||||||
|
step={1}
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
values={[sidebarNameplateOpacity]}
|
||||||
|
onChange={(values) => setSidebarNameplateOpacity(values[0])}
|
||||||
|
renderTrack={(params) => (
|
||||||
|
<div
|
||||||
|
{...params.props}
|
||||||
|
style={{
|
||||||
|
...params.props.style,
|
||||||
|
width: '100%',
|
||||||
|
height: toRem(8),
|
||||||
|
borderRadius: toRem(4),
|
||||||
|
background: `linear-gradient(to right, ${color.Primary.Main} ${sidebarNameplateOpacity}%, ${color.Surface.ContainerLine} ${sidebarNameplateOpacity}%)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{params.children}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
renderThumb={(params) => (
|
||||||
|
<div
|
||||||
|
{...params.props}
|
||||||
|
style={{
|
||||||
|
...params.props.style,
|
||||||
|
width: toRem(16),
|
||||||
|
height: toRem(16),
|
||||||
|
borderRadius: '50%',
|
||||||
|
backgroundColor: color.Primary.Main,
|
||||||
|
border: `${toRem(2)} solid ${color.Surface.Container}`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Text size="T300" style={{ minWidth: toRem(32), textAlign: 'right' }}>
|
||||||
|
{sidebarNameplateOpacity}%
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SequenceCard>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
type EmojiStyleSelectorProps = {
|
type EmojiStyleSelectorProps = {
|
||||||
selected: EmojiStyle;
|
selected: EmojiStyle;
|
||||||
onSelect: (style: EmojiStyle) => void;
|
onSelect: (style: EmojiStyle) => void;
|
||||||
@@ -1222,6 +1283,7 @@ export function General({ requestClose }: GeneralProps) {
|
|||||||
<PageContent>
|
<PageContent>
|
||||||
<Box direction="Column" gap="700">
|
<Box direction="Column" gap="700">
|
||||||
<Appearance />
|
<Appearance />
|
||||||
|
<SidebarNameplate />
|
||||||
<DateAndTime />
|
<DateAndTime />
|
||||||
<Editor />
|
<Editor />
|
||||||
<Spaces />
|
<Spaces />
|
||||||
|
|||||||
@@ -52,15 +52,18 @@ export function useUserCollectibles(): [
|
|||||||
async (kind: CollectibleKind, collectible: StoredCollectible | undefined) => {
|
async (kind: CollectibleKind, collectible: StoredCollectible | undefined) => {
|
||||||
if (kind === 'profile_effect') {
|
if (kind === 'profile_effect') {
|
||||||
await saveProfileEffect(mx, collectible);
|
await saveProfileEffect(mx, collectible);
|
||||||
|
clearUserCollectiblesCache(mx.getUserId());
|
||||||
setCollectibles((prev) => ({ ...prev, profile_effect: collectible }));
|
setCollectibles((prev) => ({ ...prev, profile_effect: collectible }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (kind === 'nameplate') {
|
if (kind === 'nameplate') {
|
||||||
await saveNameplate(mx, collectible);
|
await saveNameplate(mx, collectible);
|
||||||
|
clearUserCollectiblesCache(mx.getUserId());
|
||||||
setCollectibles((prev) => ({ ...prev, nameplate: collectible }));
|
setCollectibles((prev) => ({ ...prev, nameplate: collectible }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await saveAvatarDecoration(mx, collectible);
|
await saveAvatarDecoration(mx, collectible);
|
||||||
|
clearUserCollectiblesCache(mx.getUserId());
|
||||||
setCollectibles((prev) => ({ ...prev, avatar_decoration: collectible }));
|
setCollectibles((prev) => ({ ...prev, avatar_decoration: collectible }));
|
||||||
},
|
},
|
||||||
[mx]
|
[mx]
|
||||||
@@ -70,8 +73,15 @@ export function useUserCollectibles(): [
|
|||||||
}
|
}
|
||||||
|
|
||||||
const collectibleCache = new Map<string, { data: CollectibleProfileFields; timestamp: number }>();
|
const collectibleCache = new Map<string, { data: CollectibleProfileFields; timestamp: number }>();
|
||||||
|
const collectibleCacheListeners = new Set<(userId: string) => void>();
|
||||||
const CACHE_TTL_MS = 5 * 60 * 1000;
|
const CACHE_TTL_MS = 5 * 60 * 1000;
|
||||||
|
|
||||||
|
export function clearUserCollectiblesCache(userId?: string): void {
|
||||||
|
if (!userId) return;
|
||||||
|
collectibleCache.delete(userId);
|
||||||
|
collectibleCacheListeners.forEach((listener) => listener(userId));
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchMxcBlobUrl(
|
async function fetchMxcBlobUrl(
|
||||||
mx: ReturnType<typeof useMatrixClient>,
|
mx: ReturnType<typeof useMatrixClient>,
|
||||||
mxc: string,
|
mxc: string,
|
||||||
@@ -100,6 +110,17 @@ export function useOtherUserCollectibles(userId: string): CollectibleProfileFiel
|
|||||||
const useAuthentication = useMediaAuthentication();
|
const useAuthentication = useMediaAuthentication();
|
||||||
const [collectibles, setCollectibles] = useState<CollectibleProfileFields>({});
|
const [collectibles, setCollectibles] = useState<CollectibleProfileFields>({});
|
||||||
const [assetUrls, setAssetUrls] = useState<Record<string, string | undefined>>({});
|
const [assetUrls, setAssetUrls] = useState<Record<string, string | undefined>>({});
|
||||||
|
const [cacheVersion, setCacheVersion] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleCacheClear = (clearedUserId: string) => {
|
||||||
|
if (clearedUserId === userId) {
|
||||||
|
setCacheVersion((version) => version + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
collectibleCacheListeners.add(handleCacheClear);
|
||||||
|
return () => collectibleCacheListeners.delete(handleCacheClear);
|
||||||
|
}, [userId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
@@ -152,7 +173,7 @@ export function useOtherUserCollectibles(userId: string): CollectibleProfileFiel
|
|||||||
cancelled = true;
|
cancelled = true;
|
||||||
blobUrls.forEach((url) => URL.revokeObjectURL(url));
|
blobUrls.forEach((url) => URL.revokeObjectURL(url));
|
||||||
};
|
};
|
||||||
}, [mx, useAuthentication, userId]);
|
}, [cacheVersion, mx, useAuthentication, userId]);
|
||||||
|
|
||||||
return { ...collectibles, assetUrls };
|
return { ...collectibles, assetUrls };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import React from 'react';
|
|||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import { SidebarAvatar, SidebarItem, SidebarItemTooltip } from '../../../components/sidebar';
|
import { SidebarAvatar, SidebarItem, SidebarItemTooltip } from '../../../components/sidebar';
|
||||||
import { searchModalAtom } from '../../../state/searchModal';
|
import { searchModalAtom } from '../../../state/searchModal';
|
||||||
|
import * as sidebarCss from '../../../components/sidebar/Sidebar.css';
|
||||||
|
|
||||||
export function SearchTab() {
|
export function SearchTab() {
|
||||||
const [opened, setOpen] = useAtom(searchModalAtom);
|
const [opened, setOpen] = useAtom(searchModalAtom);
|
||||||
@@ -14,7 +15,13 @@ export function SearchTab() {
|
|||||||
<SidebarItem active={opened}>
|
<SidebarItem active={opened}>
|
||||||
<SidebarItemTooltip tooltip="Search">
|
<SidebarItemTooltip tooltip="Search">
|
||||||
{(triggerRef) => (
|
{(triggerRef) => (
|
||||||
<SidebarAvatar as="button" ref={triggerRef} outlined onClick={open}>
|
<SidebarAvatar
|
||||||
|
as="button"
|
||||||
|
ref={triggerRef}
|
||||||
|
className={sidebarCss.SidebarSearchAvatar}
|
||||||
|
outlined
|
||||||
|
onClick={open}
|
||||||
|
>
|
||||||
<Icon src={Icons.Search} filled={opened} />
|
<Icon src={Icons.Search} filled={opened} />
|
||||||
</SidebarAvatar>
|
</SidebarAvatar>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -12,8 +12,11 @@ import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
|||||||
import { Settings } from '../../../features/settings';
|
import { Settings } from '../../../features/settings';
|
||||||
import { useUserProfile } from '../../../hooks/useUserProfile';
|
import { useUserProfile } from '../../../hooks/useUserProfile';
|
||||||
import { useOtherUserCollectibles } from '../../../hooks/useUserCollectibles';
|
import { useOtherUserCollectibles } from '../../../hooks/useUserCollectibles';
|
||||||
import { pickAvatarDecorationUrl } from '../../../utils/collectibleAssets';
|
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 avatarDecorationCss from '../../../styles/AvatarDecoration.css';
|
||||||
|
import * as sidebarCss from '../../../components/sidebar/Sidebar.css';
|
||||||
import { Modal500 } from '../../../components/Modal500';
|
import { Modal500 } from '../../../components/Modal500';
|
||||||
import { AccountSwitcher } from '../../../components/account-switcher';
|
import { AccountSwitcher } from '../../../components/account-switcher';
|
||||||
import { stopPropagation } from '../../../utils/keyboard';
|
import { stopPropagation } from '../../../utils/keyboard';
|
||||||
@@ -39,6 +42,11 @@ export function SettingsTab() {
|
|||||||
: undefined;
|
: undefined;
|
||||||
const { assetUrls } = useOtherUserCollectibles(userId ?? '');
|
const { assetUrls } = useOtherUserCollectibles(userId ?? '');
|
||||||
const avatarDecorationUrl = pickAvatarDecorationUrl(assetUrls);
|
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 openSettings = () => setSettings(true);
|
||||||
const closeSettings = () => setSettings(false);
|
const closeSettings = () => setSettings(false);
|
||||||
@@ -53,12 +61,36 @@ export function SettingsTab() {
|
|||||||
<SidebarItem active={settings}>
|
<SidebarItem active={settings}>
|
||||||
<SidebarAvatar
|
<SidebarAvatar
|
||||||
as="button"
|
as="button"
|
||||||
|
className={sidebarCss.SidebarAvatarLarge}
|
||||||
onClick={openSettings}
|
onClick={openSettings}
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
style={{ overflow: 'visible' }}
|
style={{ overflow: 'visible' }}
|
||||||
>
|
>
|
||||||
<div className={avatarDecorationCss.SidebarAvatarStack}>
|
<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
|
<UserAvatar
|
||||||
|
className={sidebarCss.SidebarAvatarForeground}
|
||||||
userId={userId ?? ''}
|
userId={userId ?? ''}
|
||||||
src={avatarUrl}
|
src={avatarUrl}
|
||||||
renderFallback={() => <Text size="H4">{nameInitials(displayName)}</Text>}
|
renderFallback={() => <Text size="H4">{nameInitials(displayName)}</Text>}
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export interface Settings {
|
|||||||
encUrlPreview: boolean;
|
encUrlPreview: boolean;
|
||||||
showHiddenEvents: boolean;
|
showHiddenEvents: boolean;
|
||||||
legacyUsernameColor: boolean;
|
legacyUsernameColor: boolean;
|
||||||
|
sidebarNameplateOpacity: number;
|
||||||
|
|
||||||
showNotifications: boolean;
|
showNotifications: boolean;
|
||||||
isNotificationSounds: boolean;
|
isNotificationSounds: boolean;
|
||||||
@@ -93,6 +94,7 @@ const defaultSettings: Settings = {
|
|||||||
encUrlPreview: false,
|
encUrlPreview: false,
|
||||||
showHiddenEvents: false,
|
showHiddenEvents: false,
|
||||||
legacyUsernameColor: false,
|
legacyUsernameColor: false,
|
||||||
|
sidebarNameplateOpacity: 60,
|
||||||
|
|
||||||
showNotifications: true,
|
showNotifications: true,
|
||||||
isNotificationSounds: true,
|
isNotificationSounds: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user