Files
cinny/src/app/pages/client/SidebarNav.tsx
T
jared dcd8201e16 fix(wave-3): audit fixes — ACL guards, presence, moderation, theming perf
Wave-3 bug-hunt fixes (findings in LOTUS_TODO), reviewed + gate-green:
- 🔴 ACL editor [H1–H4]: block saving an empty allow-list (was a one-click
  federation brick), warn on self-ban (case-insensitive glob match of
  mx.getDomain() vs allow/deny), accept real globs (1.2.3.*, *.evil.*), and
  gate Save behind a confirm dialog.
- 🔴 [P1] room context menu no longer acts on the wrong room after a live
  reorder (key by roomId, not list index). 🔴 [P2] status writes no longer
  force presence to online over Invisible/DND (shared presenceStateFromSetting).
- 🟠 [P3] timed mutes restored on boot; [P4] custom-status auto-clear now fires
  (always-mounted StatusExpiryMonitor); [P5] timezone also PUT to the m.tz
  profile field so it's visible to others; [H6] RoomInsights single-pass
  min/max (was Math.min(...spread) stack overflow); [H7/H8] mod-log labels.
- 🟡 [P6/P7] favorites collapse+filter, [P8] charCount reset, [P9] DM preview
  refresh on decrypt; theming [T-P1] lazy decorations, [T-P2] drop the redundant
  always-on body animation, [T-P4] live useReducedMotion, [T-P5] decoration key.
- NATIVE-CINNY LAW: notification presets + Powers permissions use folds icons.

DEFERRED: [H5] invite-QR is fetched from api.qrserver.com (third-party leak);
local generation needs a bundled QR lib (not added). tsc/eslint/prettier clean,
build OK, 677 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 21:40:07 -04:00

128 lines
4.7 KiB
TypeScript

import React, { useEffect, useRef } from 'react';
import { Scroll } from 'folds';
import classNames from 'classnames';
import {
Sidebar,
SidebarContent,
SidebarStackSeparator,
SidebarStack,
} from '../../components/sidebar';
import { SidebarGlass } from '../../components/sidebar/Sidebar.css';
import {
DirectTab,
HomeTab,
SpaceTabs,
InboxTab,
ExploreTab,
SettingsTab,
UnverifiedTab,
SearchTab,
BookmarksTab,
} from './sidebar';
import { CreateTab } from './sidebar/CreateTab';
import { useSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings';
import { useTheme, ThemeKind } from '../../hooks/useTheme';
import { useReducedMotion } from '../../hooks/useReducedMotion';
import { getChatBg } from '../../features/lotus/chatBackground';
export function SidebarNav() {
const scrollRef = useRef<HTMLDivElement>(null) as React.RefObject<HTMLDivElement>;
const [glassmorphismSidebar] = useSetting(settingsAtom, 'glassmorphismSidebar');
const [chatBackground] = useSetting(settingsAtom, 'chatBackground');
const [lotusTerminal] = useSetting(settingsAtom, 'lotusTerminal');
const [pauseAnimations] = useSetting(settingsAtom, 'pauseAnimations');
const theme = useTheme();
const isDark = theme.kind === ThemeKind.Dark;
const reduced = useReducedMotion();
// backdrop-filter only blurs content directly behind the element in the z-axis.
// The sidebar is a flex sibling of the room view, so nothing sits behind it by default.
// Fix: mirror the active chat background onto document.body so the sidebar blurs through it.
useEffect(() => {
const { style } = document.body;
const hasBg = chatBackground !== 'none' || glassmorphismSidebar || lotusTerminal;
if (!hasBg) {
style.removeProperty('background-image');
style.removeProperty('background-color');
style.removeProperty('background-size');
style.removeProperty('background-position');
style.removeProperty('animation');
style.removeProperty('will-change');
return;
}
const effectiveBg = chatBackground !== 'none' ? chatBackground : 'tactical';
const bgStyle = getChatBg(effectiveBg, isDark, pauseAnimations || reduced);
style.backgroundImage = (bgStyle.backgroundImage as string | undefined) ?? '';
style.backgroundColor = (bgStyle.backgroundColor as string | undefined) ?? '';
style.backgroundSize = (bgStyle.backgroundSize as string | undefined) ?? '';
style.backgroundPosition = (bgStyle.backgroundPosition as string | undefined) ?? '';
// The animated body mirror (animation + will-change) exists solely so the
// glassmorphism sidebar can blur through document.body. When glass is OFF nothing
// samples this layer, yet SidebarNav is always mounted, so writing an animated bg +
// will-change here would leave a permanent invisible animated compositor layer
// app-wide. Only mirror the animation when glass is on; the static background above
// (needed by lotusTerminal / non-animated cases) is still written regardless.
if (glassmorphismSidebar) {
style.animation = (bgStyle.animation as string | undefined) ?? '';
if (bgStyle.animation) {
style.willChange = 'background-position, background-size';
} else {
style.removeProperty('will-change');
}
} else {
style.removeProperty('animation');
style.removeProperty('will-change');
}
return () => {
style.removeProperty('background-image');
style.removeProperty('background-color');
style.removeProperty('background-size');
style.removeProperty('background-position');
style.removeProperty('animation');
style.removeProperty('will-change');
};
}, [glassmorphismSidebar, chatBackground, lotusTerminal, isDark, pauseAnimations, reduced]);
return (
<Sidebar className={classNames(glassmorphismSidebar && SidebarGlass)}>
<SidebarContent
scrollable={
<Scroll
ref={scrollRef}
variant={glassmorphismSidebar ? undefined : 'Background'}
size="0"
>
<SidebarStack>
<HomeTab />
<DirectTab />
</SidebarStack>
<SpaceTabs scrollRef={scrollRef} />
<SidebarStackSeparator />
<SidebarStack>
<ExploreTab />
<CreateTab />
</SidebarStack>
</Scroll>
}
sticky={
<>
<SidebarStackSeparator />
<SidebarStack>
<SearchTab />
<BookmarksTab />
<UnverifiedTab />
<InboxTab />
<SettingsTab />
</SidebarStack>
</>
}
/>
</Sidebar>
);
}