fix: settings dropdowns, background animations, ringing, avatar decorations
Settings dropdowns (Bug #3): - Add reusable SettingsSelect component using Menu+PopOut+FocusTrap — exact same pattern as Message Layout, so all dropdowns look consistent - Replace raw <select> for Seasonal Theme, UI Font, AFK Timeout, and Join & Leave Sounds with SettingsSelect Animated chat backgrounds bleeding onto content (Bug #6 / #7): - Remove filter:brightness() and opacity animations from chatBackground.ts (animRainGlowKeyframe, animGridBrightnessKeyframe, animFirefliesGlowKeyframe, animFirefliesBlinkKeyframe). These were applied to the Page element which caused ALL descendants (messages, composer) to flash in sync. Also created a CSS stacking context on Page that pushed SeasonalEffect (position:fixed; z-index:9997) behind the animated background layer. - Only backgroundPosition / backgroundSize animations remain — safe, do not affect descendants, and do not create stacking contexts. - Remove now-unused animation keyframe imports from chatBackground.ts. Voice ringing in persistent rooms (Bug #5): - Narrow the ringing condition from (Invite|Knock|Restricted) to only Invite, matching exactly the rooms where the call button is visible. - Add room.isCallRoom() early-exit so m.join_rule:call rooms never ring. Avatar decoration images not loading (Bug #8): - Change loading="lazy" → loading="eager" in DecorationPreviewCell. Lazy loading does not reliably trigger for images inside nested overflow scroll containers (the settings panel scroll area), so images never loaded. Docs: LOTUS_BUGS.md updated with root cause and resolution for all 5 new bugs. Docs: LOTUS_TODO.md adds P5-35/P5-36 (deferred desktop notification/jump list). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -154,6 +154,82 @@ function SelectTheme({ disabled }: { disabled?: boolean }) {
|
||||
);
|
||||
}
|
||||
|
||||
type SettingsSelectOption<T extends string> = { value: T; label: string };
|
||||
|
||||
function SettingsSelect<T extends string>({
|
||||
value,
|
||||
options,
|
||||
onChange,
|
||||
}: {
|
||||
value: T;
|
||||
options: SettingsSelectOption<T>[];
|
||||
onChange: (v: T) => void;
|
||||
}) {
|
||||
const [menuCords, setMenuCords] = useState<RectCords>();
|
||||
const selectedLabel = options.find((o) => o.value === value)?.label ?? value;
|
||||
|
||||
const handleMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
setMenuCords(evt.currentTarget.getBoundingClientRect());
|
||||
};
|
||||
|
||||
const handleSelect = (v: T) => {
|
||||
onChange(v);
|
||||
setMenuCords(undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
size="300"
|
||||
variant="Secondary"
|
||||
outlined
|
||||
fill="Soft"
|
||||
radii="300"
|
||||
after={<Icon size="300" src={Icons.ChevronBottom} />}
|
||||
onClick={handleMenu}
|
||||
>
|
||||
<Text size="T300">{selectedLabel}</Text>
|
||||
</Button>
|
||||
<PopOut
|
||||
anchor={menuCords}
|
||||
offset={5}
|
||||
position="Bottom"
|
||||
align="End"
|
||||
content={
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
onDeactivate: () => setMenuCords(undefined),
|
||||
clickOutsideDeactivates: true,
|
||||
isKeyForward: (evt: KeyboardEvent) =>
|
||||
evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
|
||||
isKeyBackward: (evt: KeyboardEvent) =>
|
||||
evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
|
||||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<Menu>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
{options.map((opt) => (
|
||||
<MenuItem
|
||||
key={opt.value}
|
||||
size="300"
|
||||
variant={opt.value === value ? 'Primary' : 'Surface'}
|
||||
radii="300"
|
||||
onClick={() => handleSelect(opt.value)}
|
||||
>
|
||||
<Text size="T300">{opt.label}</Text>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Box>
|
||||
</Menu>
|
||||
</FocusTrap>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SystemThemePreferences() {
|
||||
const themeKind = useSystemThemeKind();
|
||||
const themeNames = useThemeNames();
|
||||
@@ -417,42 +493,25 @@ function Appearance() {
|
||||
title="Seasonal Theme"
|
||||
description="Decorative overlays that activate automatically on holidays and events, or choose one manually."
|
||||
after={
|
||||
<select
|
||||
<SettingsSelect
|
||||
value={seasonalThemeOverride ?? 'auto'}
|
||||
onChange={(e) =>
|
||||
setSeasonalThemeOverride(
|
||||
e.target.value as typeof seasonalThemeOverride,
|
||||
)
|
||||
}
|
||||
style={{
|
||||
background: 'var(--bg-surface-variant)',
|
||||
color: 'inherit',
|
||||
border: '1px solid var(--border-interactive)',
|
||||
borderRadius: '6px',
|
||||
padding: '4px 8px',
|
||||
fontSize: '14px',
|
||||
fontFamily: 'inherit',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<option value="auto">🗓 Auto (date-based)</option>
|
||||
<option value="off">Off</option>
|
||||
<optgroup label="Holidays">
|
||||
<option value="newyear">🎆 New Year (Dec 31–Jan 2)</option>
|
||||
<option value="lunar">🏮 Lunar New Year (Jan 22–Feb 5)</option>
|
||||
<option value="valentines">💖 Valentine's Day (Feb 10–15)</option>
|
||||
<option value="stpatricks">🍀 St. Patrick's Day (Mar 15–18)</option>
|
||||
<option value="aprilfools">🃏 April Fool's Day (Apr 1)</option>
|
||||
<option value="earthday">🌱 Earth Day (Apr 22)</option>
|
||||
<option value="autumn">🍂 Autumn (Sep 21–Oct 31)</option>
|
||||
<option value="halloween">🎃 Halloween (Oct 15–Nov 1)</option>
|
||||
<option value="christmas">❄️ Christmas (Dec 10–Jan 2)</option>
|
||||
</optgroup>
|
||||
<optgroup label="Events">
|
||||
<option value="arcade">👾 Retro Arcade Day (Sep 12)</option>
|
||||
<option value="deepspace">🚀 Deep Space Week (Oct 4–10)</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
onChange={(v) => setSeasonalThemeOverride(v as typeof seasonalThemeOverride)}
|
||||
options={[
|
||||
{ value: 'auto', label: '🗓 Auto (date-based)' },
|
||||
{ value: 'off', label: 'Off' },
|
||||
{ value: 'newyear', label: '🎆 New Year' },
|
||||
{ value: 'lunar', label: '🏮 Lunar New Year' },
|
||||
{ value: 'valentines', label: '💖 Valentine\'s Day' },
|
||||
{ value: 'stpatricks', label: '🍀 St. Patrick\'s Day' },
|
||||
{ value: 'aprilfools', label: '🃏 April Fool\'s Day' },
|
||||
{ value: 'earthday', label: '🌱 Earth Day' },
|
||||
{ value: 'autumn', label: '🍂 Autumn' },
|
||||
{ value: 'halloween', label: '🎃 Halloween' },
|
||||
{ value: 'christmas', label: '❄️ Christmas' },
|
||||
{ value: 'arcade', label: '👾 Retro Arcade Day' },
|
||||
{ value: 'deepspace', label: '🚀 Deep Space Week' },
|
||||
]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
@@ -556,26 +615,18 @@ function Appearance() {
|
||||
title="UI Font"
|
||||
description="Font used throughout the interface."
|
||||
after={
|
||||
<select
|
||||
<SettingsSelect
|
||||
value={fontFamily ?? 'inter'}
|
||||
onChange={(e) =>
|
||||
setFontFamily(e.target.value as 'system' | 'inter' | 'jetbrains-mono' | 'fira-code')
|
||||
onChange={(v) =>
|
||||
setFontFamily(v as 'system' | 'inter' | 'jetbrains-mono' | 'fira-code')
|
||||
}
|
||||
style={{
|
||||
background: 'var(--bg-surface)',
|
||||
color: 'inherit',
|
||||
border: '1px solid var(--border-interactive-normal)',
|
||||
borderRadius: '6px',
|
||||
padding: '4px 8px',
|
||||
fontSize: '14px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<option value="system">System Default</option>
|
||||
<option value="inter">Inter (default)</option>
|
||||
<option value="jetbrains-mono">JetBrains Mono</option>
|
||||
<option value="fira-code">Fira Code</option>
|
||||
</select>
|
||||
options={[
|
||||
{ value: 'system', label: 'System Default' },
|
||||
{ value: 'inter', label: 'Inter (default)' },
|
||||
{ value: 'jetbrains-mono', label: 'JetBrains Mono' },
|
||||
{ value: 'fira-code', label: 'Fira Code' },
|
||||
]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
@@ -1263,25 +1314,17 @@ function Calls() {
|
||||
title="Idle Timeout"
|
||||
description="How long to wait before auto-muting."
|
||||
after={
|
||||
<select
|
||||
value={afkTimeoutMinutes}
|
||||
onChange={(e) => setAfkTimeoutMinutes(Number(e.target.value))}
|
||||
style={{
|
||||
background: 'var(--bg-surface)',
|
||||
color: 'inherit',
|
||||
border: '1px solid var(--border-interactive-normal)',
|
||||
borderRadius: '6px',
|
||||
padding: '4px 8px',
|
||||
fontSize: 'inherit',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<option value={1}>1 minute</option>
|
||||
<option value={5}>5 minutes</option>
|
||||
<option value={10}>10 minutes</option>
|
||||
<option value={20}>20 minutes</option>
|
||||
<option value={30}>30 minutes</option>
|
||||
</select>
|
||||
<SettingsSelect
|
||||
value={String(afkTimeoutMinutes ?? 10)}
|
||||
onChange={(v) => setAfkTimeoutMinutes(Number(v))}
|
||||
options={[
|
||||
{ value: '1', label: '1 minute' },
|
||||
{ value: '5', label: '5 minutes' },
|
||||
{ value: '10', label: '10 minutes' },
|
||||
{ value: '20', label: '20 minutes' },
|
||||
{ value: '30', label: '30 minutes' },
|
||||
]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
@@ -1291,26 +1334,16 @@ function Calls() {
|
||||
title="Join & Leave Sounds"
|
||||
description="Play a sound when someone joins or leaves a call you are in."
|
||||
after={
|
||||
<select
|
||||
<SettingsSelect
|
||||
value={callJoinLeaveSound}
|
||||
onChange={(e) =>
|
||||
handleJoinLeaveSoundChange(e.target.value as 'off' | 'chime' | 'soft' | 'retro')
|
||||
}
|
||||
style={{
|
||||
background: 'var(--bg-surface)',
|
||||
color: 'inherit',
|
||||
border: '1px solid var(--border-interactive-normal)',
|
||||
borderRadius: '6px',
|
||||
padding: '4px 8px',
|
||||
fontSize: 'inherit',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<option value="off">Off</option>
|
||||
<option value="chime">Chime</option>
|
||||
<option value="soft">Soft</option>
|
||||
<option value="retro">Retro</option>
|
||||
</select>
|
||||
onChange={(v) => handleJoinLeaveSoundChange(v as 'off' | 'chime' | 'soft' | 'retro')}
|
||||
options={[
|
||||
{ value: 'off', label: 'Off' },
|
||||
{ value: 'chime', label: 'Chime' },
|
||||
{ value: 'soft', label: 'Soft' },
|
||||
{ value: 'retro', label: 'Retro' },
|
||||
]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
|
||||
Reference in New Issue
Block a user