Compare commits

..

3 Commits

Author SHA1 Message Date
jared d801e01623 docs: update LOTUS_TODO + README for P5-4 and glassmorphism fix
CI / Build & Quality Checks (push) Successful in 10m40s
Mark P5-4 (animated chat backgrounds) complete with implementation
notes. Update README Settings section with animated backgrounds entry
and correct glassmorphism description (now explains the body-background
fix that makes backdrop-filter actually visible).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 11:49:30 -04:00
jared 8f0e05ffef feat(P5-4): animated chat backgrounds + pause toggle
5 new CSS-only animated backgrounds in the chat background picker:
- Digital Rain: two-layer vertical stripe scroll with parallax (wide
  stripes at 8s, narrow at 4s via single keyframe with split positions)
- Star Drift: three-layer radial-gradient star field drifting diagonally
- Grid Pulse: neon grid lines that expand/contract (backgroundSize keyframe)
- Aurora Flow: large radial gradient bands sweeping across 200% canvas
- Fireflies: three layers of glowing dots drifting across the viewport

All use vanilla-extract keyframes (GPU-composited transforms/positions,
no canvas, no JS timers). prefers-reduced-motion is respected in
getChatBg() by stripping the animation property at call time. A "Pause
Background Animations" toggle in Settings → Appearance provides an
in-app override for the same purpose.

BG labels de-duplicated ("Digital Rain", "Star Drift", "Aurora Flow")
to avoid the duplicate "Stars" and "Aurora" entries that had appeared.
LIGHT anim-fireflies background corrected from near-black #0a0a10 to
warm white #fffdf0. Four unused keyframe exports removed from
Animations.css.ts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 11:46:16 -04:00
jared bf75f4657d fix: glassmorphism sidebar background visibility + image upload cleanup
Glassmorphism: the sidebar is a flex sibling of the room view, so
backdrop-filter had nothing behind it to blur. Fix: apply the active
chat background to document.body when glassmorphismSidebar is on
(cleaned up when it's turned off or the component unmounts). Now the
sidebar blurs through the same background pattern as the room view,
making the frosted-glass effect obvious.

Image upload cleanup: delete the pre-uploaded original MXC from the
homeserver after the compressed version is successfully uploaded
(Synapse 1.97+ DELETE /_matrix/client/v1/media/{server}/{mediaId}).
Also delete on cancel when a successful upload is removed by the user.
Both are best-effort — failures are swallowed so UX is unaffected.
Added tryDeleteMxcContent() utility to src/app/utils/matrix.ts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 11:25:19 -04:00
11 changed files with 285 additions and 9 deletions
+3 -1
View File
@@ -978,7 +978,7 @@ Themes:
--- ---
### [ ] P5-4 · Animated Chat Backgrounds (CSS-animated wallpapers) ### [x] P5-4 · Animated Chat Backgrounds (CSS-animated wallpapers)
**What:** 5 new animated wallpaper options in the chat background picker: **What:** 5 new animated wallpaper options in the chat background picker:
@@ -991,6 +991,8 @@ Themes:
**[AUDIT REQUIRED]** Study how existing wallpapers are applied in `lotus-terminal.css.ts` to extend the system correctly. **[AUDIT REQUIRED]** Study how existing wallpapers are applied in `lotus-terminal.css.ts` to extend the system correctly.
**Complexity:** Medium. **Complexity:** Medium.
**COMPLETED June 2026.** Five new CSS-only animated backgrounds added to the chat background picker via vanilla-extract keyframes: **Digital Rain** (two-layer vertical stripe scroll with parallax depth — wide stripes at 8s, narrow at 4s), **Star Drift** (three-layer radial-gradient dots drifting diagonally), **Grid Pulse** (orange/cyan neon grid lines expanding/contracting via backgroundSize keyframe), **Aurora Flow** (four large radial gradient ellipses sweeping across a 200% canvas), **Fireflies** (three layers of warm glowing radial-gradient dots drifting). All implemented in `src/app/styles/Animations.css.ts` (keyframes) + `src/app/features/lotus/chatBackground.ts` (CSSProperties). `getChatBg` updated with optional `pauseAnimations?: boolean` parameter; also respects `prefers-reduced-motion: reduce` by checking `window.matchMedia` at call time. "Pause Background Animations" toggle added to Settings → Appearance. Glassmorphism sidebar bug fixed simultaneously: sidebar now applies chat background to `document.body` so `backdrop-filter` has content to blur through (previously sidebar was a flex sibling with nothing behind it).
--- ---
### [x] P5-5 · Night Light / Blue Light Filter ### [x] P5-5 · Night Light / Blue Light Filter
+2 -1
View File
@@ -175,7 +175,8 @@ Emoji reaction buttons styled for terminal mode via `button[data-reaction-key]`
### Settings (Appearance) ### Settings (Appearance)
- **Glassmorphism Sidebar**: Settings → Appearance toggle (off by default). When enabled, the left sidebar becomes semi-transparent (`background: rgba(3,5,8,0.55)`) with `backdrop-filter: blur(12px)` so chat background patterns show through as a frosted glass effect. Implemented as a vanilla-extract `SidebarGlass` class applied to the `<Sidebar>` container in `SidebarNav.tsx`. - **Animated Chat Backgrounds**: Five CSS-only animated wallpapers added to the background picker — Digital Rain (two-layer vertical stripe scroll with parallax), Star Drift (three-layer radial-gradient star field drifting diagonally), Grid Pulse (neon grid lines expanding/contracting via `backgroundSize` keyframe), Aurora Flow (four radial gradient ellipses sweeping across a 200% canvas), Fireflies (three layers of glowing dots drifting). All use vanilla-extract `keyframes()` — no canvas, GPU-composited. Respects `prefers-reduced-motion: reduce` (animation stripped at call time). "Pause Background Animations" toggle in Settings → Appearance provides an in-app override. Implemented in `src/app/styles/Animations.css.ts` + `src/app/features/lotus/chatBackground.ts`.
- **Glassmorphism Sidebar**: Settings → Appearance toggle (off by default). When enabled, the left sidebar becomes semi-transparent (`background: rgba(3,5,8,0.55)`) with `backdrop-filter: blur(12px)` so chat background patterns show through as a frosted glass effect. Fix: the active chat background is mirrored onto `document.body` via `useEffect` in `SidebarNav.tsx` so the blur has content to work through (previously the sidebar was a flex sibling with nothing physically behind it). Implemented as a vanilla-extract `SidebarGlass` class applied to the `<Sidebar>` container in `SidebarNav.tsx`.
- **Night Light / Blue Light Filter**: Warm orange overlay (`rgba(255,140,0,N%)`) across the entire UI. Toggle + intensity slider (580%) in Settings → Appearance. `position:fixed; pointer-events:none; z-index:9998`. Persists across sessions. - **Night Light / Blue Light Filter**: Warm orange overlay (`rgba(255,140,0,N%)`) across the entire UI. Toggle + intensity slider (580%) in Settings → Appearance. `position:fixed; pointer-events:none; z-index:9998`. Persists across sessions.
@@ -13,6 +13,7 @@ import {
import { useObjectURL } from '../../hooks/useObjectURL'; import { useObjectURL } from '../../hooks/useObjectURL';
import { useMediaConfig } from '../../hooks/useMediaConfig'; import { useMediaConfig } from '../../hooks/useMediaConfig';
import { compressImage, formatFileSize, isCompressible } from '../../utils/imageCompression'; import { compressImage, formatFileSize, isCompressible } from '../../utils/imageCompression';
import { tryDeleteMxcContent } from '../../utils/matrix';
type PreviewImageProps = { type PreviewImageProps = {
fileItem: TUploadItem; fileItem: TUploadItem;
@@ -274,6 +275,10 @@ export function UploadCardRenderer({
}; };
const removeUpload = () => { const removeUpload = () => {
if (upload.status === UploadStatus.Success) {
// Upload already completed — delete the orphaned MXC from the server.
tryDeleteMxcContent(mx, upload.mxc);
}
cancelUpload(); cancelUpload();
onRemove(file); onRemove(file);
}; };
+154 -2
View File
@@ -1,5 +1,12 @@
import { CSSProperties } from 'react'; import { CSSProperties } from 'react';
import { ChatBackground } from '../../state/settings'; import { ChatBackground } from '../../state/settings';
import {
animRainKeyframe,
animStarsDriftKeyframe,
animGridPulseKeyframe,
animAuroraKeyframe,
animFirefliesKeyframe,
} from '../../styles/Animations.css';
export const BG_OPTIONS: { value: ChatBackground; label: string }[] = [ export const BG_OPTIONS: { value: ChatBackground; label: string }[] = [
{ value: 'none', label: 'None' }, { value: 'none', label: 'None' },
@@ -19,6 +26,11 @@ export const BG_OPTIONS: { value: ChatBackground; label: string }[] = [
{ value: 'waves', label: 'Waves' }, { value: 'waves', label: 'Waves' },
{ value: 'neon', label: 'Neon Grid' }, { value: 'neon', label: 'Neon Grid' },
{ value: 'aurora', label: 'Aurora' }, { value: 'aurora', label: 'Aurora' },
{ value: 'anim-rain', label: 'Digital Rain' },
{ value: 'anim-stars', label: 'Star Drift' },
{ value: 'anim-pulse', label: 'Grid Pulse' },
{ value: 'anim-aurora', label: 'Aurora Flow' },
{ value: 'anim-fireflies', label: 'Fireflies' },
]; ];
const DARK: Record<ChatBackground, CSSProperties> = { const DARK: Record<ChatBackground, CSSProperties> = {
@@ -184,6 +196,71 @@ const DARK: Record<ChatBackground, CSSProperties> = {
'radial-gradient(ellipse at 60% 90%, rgba(0,212,255,0.06) 0%, transparent 50%)', 'radial-gradient(ellipse at 60% 90%, rgba(0,212,255,0.06) 0%, transparent 50%)',
].join(','), ].join(','),
}, },
// Animated: Matrix digital rain — scrolling vertical green stripes
'anim-rain': {
backgroundColor: '#010804',
backgroundImage: [
'repeating-linear-gradient(180deg, rgba(0,255,136,0.13) 0px, rgba(0,255,136,0.13) 1px, transparent 1px, transparent 20px)',
'repeating-linear-gradient(180deg, rgba(0,255,136,0.07) 0px, rgba(0,255,136,0.07) 1px, transparent 1px, transparent 8px)',
].join(','),
backgroundSize: '40px 200px, 12px 200px',
backgroundPosition: '0 0, 0 0',
animation: `${animRainKeyframe} 8s linear infinite`,
},
// Animated: drifting star field — three layers at different speeds
'anim-stars': {
backgroundColor: '#050510',
backgroundImage: [
'radial-gradient(circle, rgba(255,255,255,0.85) 1px, transparent 1px)',
'radial-gradient(circle, rgba(200,220,255,0.55) 1px, transparent 1px)',
'radial-gradient(circle, rgba(180,200,255,0.3) 1px, transparent 1px)',
].join(','),
backgroundSize: '130px 130px, 190px 190px, 260px 260px',
backgroundPosition: '0 0, 65px 32px, 32px 97px',
animation: `${animStarsDriftKeyframe} 25s linear infinite`,
},
// Animated: neon grid pulse — grid lines that expand/contract
'anim-pulse': {
backgroundColor: '#030508',
backgroundImage: [
'linear-gradient(rgba(255,107,0,0.12) 1px, transparent 1px)',
'linear-gradient(90deg, rgba(255,107,0,0.12) 1px, transparent 1px)',
'linear-gradient(rgba(0,212,255,0.06) 1px, transparent 1px)',
'linear-gradient(90deg, rgba(0,212,255,0.06) 1px, transparent 1px)',
].join(','),
backgroundSize: '60px 60px, 60px 60px, 12px 12px, 12px 12px',
animation: `${animGridPulseKeyframe} 4s ease-in-out infinite`,
},
// Animated: aurora borealis — slowly drifting gradient bands
'anim-aurora': {
backgroundColor: '#020a10',
backgroundImage: [
'radial-gradient(ellipse at 20% 30%, rgba(0,255,136,0.10) 0%, transparent 55%)',
'radial-gradient(ellipse at 80% 70%, rgba(0,100,255,0.10) 0%, transparent 55%)',
'radial-gradient(ellipse at 50% 10%, rgba(191,95,255,0.08) 0%, transparent 50%)',
'radial-gradient(ellipse at 60% 90%, rgba(0,212,255,0.08) 0%, transparent 50%)',
].join(','),
backgroundSize: '200% 200%',
backgroundPosition: '0% 0%',
animation: `${animAuroraKeyframe} 20s ease-in-out infinite`,
},
// Animated: fireflies — three layers of glowing dots at different speeds
'anim-fireflies': {
backgroundColor: '#030508',
backgroundImage: [
'radial-gradient(circle, rgba(255,220,50,0.55) 1.5px, rgba(255,160,0,0.15) 3px, transparent 4px)',
'radial-gradient(circle, rgba(255,200,30,0.45) 1px, rgba(255,140,0,0.12) 2.5px, transparent 3.5px)',
'radial-gradient(circle, rgba(255,240,100,0.35) 1px, transparent 2px)',
].join(','),
backgroundSize: '200px 200px, 280px 280px, 160px 160px',
backgroundPosition: '0 0, 120px 80px, 60px 140px',
animation: `${animFirefliesKeyframe} 15s linear infinite`,
},
}; };
const LIGHT: Record<ChatBackground, CSSProperties> = { const LIGHT: Record<ChatBackground, CSSProperties> = {
@@ -340,7 +417,82 @@ const LIGHT: Record<ChatBackground, CSSProperties> = {
'radial-gradient(ellipse at 60% 90%, rgba(0,160,200,0.07) 0%, transparent 50%)', 'radial-gradient(ellipse at 60% 90%, rgba(0,160,200,0.07) 0%, transparent 50%)',
].join(','), ].join(','),
}, },
// Animated light variants
'anim-rain': {
backgroundColor: '#f0fff4',
backgroundImage: [
'repeating-linear-gradient(180deg, rgba(0,160,80,0.14) 0px, rgba(0,160,80,0.14) 1px, transparent 1px, transparent 20px)',
'repeating-linear-gradient(180deg, rgba(0,160,80,0.07) 0px, rgba(0,160,80,0.07) 1px, transparent 1px, transparent 8px)',
].join(','),
backgroundSize: '40px 200px, 12px 200px',
backgroundPosition: '0 0, 0 0',
animation: `${animRainKeyframe} 8s linear infinite`,
},
'anim-stars': {
backgroundColor: '#f5f5ff',
backgroundImage: [
'radial-gradient(circle, rgba(60,60,160,0.50) 1px, transparent 1px)',
'radial-gradient(circle, rgba(80,80,180,0.35) 1px, transparent 1px)',
'radial-gradient(circle, rgba(100,100,200,0.20) 1px, transparent 1px)',
].join(','),
backgroundSize: '130px 130px, 190px 190px, 260px 260px',
backgroundPosition: '0 0, 65px 32px, 32px 97px',
animation: `${animStarsDriftKeyframe} 25s linear infinite`,
},
'anim-pulse': {
backgroundColor: '#ffffff',
backgroundImage: [
'linear-gradient(rgba(0,98,184,0.14) 1px, transparent 1px)',
'linear-gradient(90deg, rgba(0,98,184,0.14) 1px, transparent 1px)',
'linear-gradient(rgba(0,98,184,0.06) 1px, transparent 1px)',
'linear-gradient(90deg, rgba(0,98,184,0.06) 1px, transparent 1px)',
].join(','),
backgroundSize: '60px 60px, 60px 60px, 12px 12px, 12px 12px',
animation: `${animGridPulseKeyframe} 4s ease-in-out infinite`,
},
'anim-aurora': {
backgroundColor: '#f0f8f4',
backgroundImage: [
'radial-gradient(ellipse at 20% 30%, rgba(0,160,80,0.12) 0%, transparent 55%)',
'radial-gradient(ellipse at 80% 70%, rgba(0,80,200,0.12) 0%, transparent 55%)',
'radial-gradient(ellipse at 50% 10%, rgba(140,60,220,0.09) 0%, transparent 50%)',
'radial-gradient(ellipse at 60% 90%, rgba(0,160,200,0.09) 0%, transparent 50%)',
].join(','),
backgroundSize: '200% 200%',
backgroundPosition: '0% 0%',
animation: `${animAuroraKeyframe} 20s ease-in-out infinite`,
},
'anim-fireflies': {
backgroundColor: '#fffdf0',
backgroundImage: [
'radial-gradient(circle, rgba(180,120,0,0.55) 1.5px, rgba(160,90,0,0.15) 3px, transparent 4px)',
'radial-gradient(circle, rgba(160,100,0,0.45) 1px, rgba(140,80,0,0.12) 2.5px, transparent 3.5px)',
'radial-gradient(circle, rgba(200,140,0,0.35) 1px, transparent 2px)',
].join(','),
backgroundSize: '200px 200px, 280px 280px, 160px 160px',
backgroundPosition: '0 0, 120px 80px, 60px 140px',
animation: `${animFirefliesKeyframe} 15s linear infinite`,
},
}; };
export const getChatBg = (bg: ChatBackground, isDark: boolean): CSSProperties => export const getChatBg = (
isDark ? DARK[bg] : LIGHT[bg]; bg: ChatBackground,
isDark: boolean,
pauseAnimations?: boolean,
): CSSProperties => {
const style = isDark ? DARK[bg] : LIGHT[bg];
const reducedMotion =
typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if ((pauseAnimations || reducedMotion) && style.animation) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { animation: _anim, ...rest } = style;
return rest;
}
return style;
};
+3
View File
@@ -65,6 +65,7 @@ import {
getImageInfo, getImageInfo,
getMxIdLocalPart, getMxIdLocalPart,
mxcUrlToHttp, mxcUrlToHttp,
tryDeleteMxcContent,
} from '../../utils/matrix'; } from '../../utils/matrix';
import { compressImage } from '../../utils/imageCompression'; import { compressImage } from '../../utils/imageCompression';
import { useTypingStatusUpdater } from '../../hooks/useTypingStatusUpdater'; import { useTypingStatusUpdater } from '../../hooks/useTypingStatusUpdater';
@@ -434,6 +435,8 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
}); });
const compressedMxc = (uploadRes as { content_uri: string }).content_uri; const compressedMxc = (uploadRes as { content_uri: string }).content_uri;
if (compressedMxc) { if (compressedMxc) {
// Delete the pre-uploaded original so only one copy lives on the server.
tryDeleteMxcContent(mx, upload.mxc);
mxc = compressedMxc; mxc = compressedMxc;
// Build a synthetic fileItem that refers to the compressed file so // Build a synthetic fileItem that refers to the compressed file so
// getImageMsgContent picks up the correct dimensions and type. // getImageMsgContent picks up the correct dimensions and type.
+7 -2
View File
@@ -61,6 +61,7 @@ export function RoomView({ eventId }: { eventId?: string }) {
const roomViewRef = useRef<HTMLDivElement>(null) as React.RefObject<HTMLDivElement>; const roomViewRef = useRef<HTMLDivElement>(null) as React.RefObject<HTMLDivElement>;
const [chatBackground] = useSetting(settingsAtom, 'chatBackground'); const [chatBackground] = useSetting(settingsAtom, 'chatBackground');
const [lotusTerminal] = useSetting(settingsAtom, 'lotusTerminal'); const [lotusTerminal] = useSetting(settingsAtom, 'lotusTerminal');
const [pauseAnimations] = useSetting(settingsAtom, 'pauseAnimations');
const theme = useTheme(); const theme = useTheme();
const isDark = theme.kind === ThemeKind.Dark; const isDark = theme.kind === ThemeKind.Dark;
@@ -98,8 +99,12 @@ export function RoomView({ eventId }: { eventId?: string }) {
const chatBgStyle = useMemo( const chatBgStyle = useMemo(
() => () =>
getChatBg(lotusTerminal && chatBackground === 'none' ? 'tactical' : chatBackground, isDark), getChatBg(
[chatBackground, lotusTerminal, isDark], lotusTerminal && chatBackground === 'none' ? 'tactical' : chatBackground,
isDark,
pauseAnimations,
),
[chatBackground, lotusTerminal, isDark, pauseAnimations],
); );
return ( return (
+11 -1
View File
@@ -331,6 +331,7 @@ function Appearance() {
settingsAtom, settingsAtom,
'glassmorphismSidebar', 'glassmorphismSidebar',
); );
const [pauseAnimations, setPauseAnimations] = useSetting(settingsAtom, 'pauseAnimations');
return ( return (
<Box direction="Column" gap="100"> <Box direction="Column" gap="100">
@@ -390,6 +391,14 @@ function Appearance() {
</Box> </Box>
</SequenceCard> </SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Pause Background Animations"
description="Stop background animation to reduce motion or improve performance."
after={<Switch variant="Primary" value={pauseAnimations} onChange={setPauseAnimations} />}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column"> <SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile <SettingTile
title="Show Profile on Every Message" title="Show Profile on Every Message"
@@ -1056,6 +1065,7 @@ function Calls() {
function ChatBgGrid() { function ChatBgGrid() {
const [chatBackground, setChatBackground] = useSetting(settingsAtom, 'chatBackground'); const [chatBackground, setChatBackground] = useSetting(settingsAtom, 'chatBackground');
const [pauseAnimations] = useSetting(settingsAtom, 'pauseAnimations');
const theme = useTheme(); const theme = useTheme();
const isDark = theme.kind === ThemeKind.Dark; const isDark = theme.kind === ThemeKind.Dark;
@@ -1080,7 +1090,7 @@ function ChatBgGrid() {
: '2px solid rgba(128,128,128,0.25)', : '2px solid rgba(128,128,128,0.25)',
padding: 0, padding: 0,
overflow: 'hidden', overflow: 'hidden',
...getChatBg(opt.value as ChatBackground, isDark), ...getChatBg(opt.value as ChatBackground, isDark, pauseAnimations),
}} }}
/> />
<Text <Text
+37 -1
View File
@@ -1,4 +1,4 @@
import React, { useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import { Scroll } from 'folds'; import { Scroll } from 'folds';
import classNames from 'classnames'; import classNames from 'classnames';
@@ -23,10 +23,46 @@ import {
import { CreateTab } from './sidebar/CreateTab'; import { CreateTab } from './sidebar/CreateTab';
import { useSetting } from '../../state/hooks/settings'; import { useSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings'; import { settingsAtom } from '../../state/settings';
import { useTheme, ThemeKind } from '../../hooks/useTheme';
import { getChatBg } from '../../features/lotus/chatBackground';
export function SidebarNav() { export function SidebarNav() {
const scrollRef = useRef<HTMLDivElement>(null) as React.RefObject<HTMLDivElement>; const scrollRef = useRef<HTMLDivElement>(null) as React.RefObject<HTMLDivElement>;
const [glassmorphismSidebar] = useSetting(settingsAtom, 'glassmorphismSidebar'); 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;
// 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;
if (!glassmorphismSidebar) {
style.removeProperty('background-image');
style.removeProperty('background-color');
style.removeProperty('background-size');
style.removeProperty('background-position');
style.removeProperty('animation');
return;
}
const effectiveBg = lotusTerminal && chatBackground === 'none' ? 'tactical' : chatBackground;
const bgStyle = getChatBg(effectiveBg, isDark, pauseAnimations);
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) ?? '';
style.animation = (bgStyle.animation as string | undefined) ?? '';
return () => {
style.removeProperty('background-image');
style.removeProperty('background-color');
style.removeProperty('background-size');
style.removeProperty('background-position');
style.removeProperty('animation');
};
}, [glassmorphismSidebar, chatBackground, lotusTerminal, isDark, pauseAnimations]);
return ( return (
<Sidebar className={classNames(glassmorphismSidebar && SidebarGlass)}> <Sidebar className={classNames(glassmorphismSidebar && SidebarGlass)}>
+10 -1
View File
@@ -26,7 +26,12 @@ export type ChatBackground =
| 'hexgrid' | 'hexgrid'
| 'waves' | 'waves'
| 'neon' | 'neon'
| 'aurora'; | 'aurora'
| 'anim-rain'
| 'anim-stars'
| 'anim-pulse'
| 'anim-aurora'
| 'anim-fireflies';
export enum MessageLayout { export enum MessageLayout {
Modern = 0, Modern = 0,
Compact = 1, Compact = 1,
@@ -94,6 +99,8 @@ export interface Settings {
deafenKey: string; deafenKey: string;
warnOnUnverifiedDevices: boolean; warnOnUnverifiedDevices: boolean;
pauseAnimations: boolean;
} }
const defaultSettings: Settings = { const defaultSettings: Settings = {
@@ -157,6 +164,8 @@ const defaultSettings: Settings = {
deafenKey: 'KeyM', deafenKey: 'KeyM',
warnOnUnverifiedDevices: false, warnOnUnverifiedDevices: false,
pauseAnimations: false,
}; };
export const getSettings = (): Settings => { export const getSettings = (): Settings => {
+36
View File
@@ -69,3 +69,39 @@ export const MsgAppearClass = style({
}, },
}, },
}); });
// Animated chat background keyframes
// Animated chat background keyframes
/** Matrix rain — two stripe layers scroll at different speeds for parallax depth */
export const animRainKeyframe = keyframes({
from: { backgroundPosition: '0 0, 0 0' },
to: { backgroundPosition: '0 200px, 0 100px' },
});
/** Drifting stars — three layers drift diagonally */
export const animStarsDriftKeyframe = keyframes({
from: { backgroundPosition: '0 0, 65px 32px, 32px 97px' },
to: { backgroundPosition: '130px 130px, 195px 162px, 162px 227px' },
});
/** Grid pulse — expands/contracts backgroundSize slightly */
export const animGridPulseKeyframe = keyframes({
'0%': { backgroundSize: '60px 60px, 60px 60px, 12px 12px, 12px 12px' },
'50%': { backgroundSize: '66px 66px, 66px 66px, 13px 13px, 13px 13px' },
'100%': { backgroundSize: '60px 60px, 60px 60px, 12px 12px, 12px 12px' },
});
/** Aurora — sweeps backgroundPosition in large cycle */
export const animAuroraKeyframe = keyframes({
'0%': { backgroundPosition: '0% 0%' },
'50%': { backgroundPosition: '-50% -25%' },
'100%': { backgroundPosition: '0% 0%' },
});
/** Fireflies — three layers of glowing dots drift diagonally */
export const animFirefliesKeyframe = keyframes({
from: { backgroundPosition: '0 0, 120px 80px, 60px 140px' },
to: { backgroundPosition: '200px 150px, 320px 230px, 260px 290px' },
});
+17
View File
@@ -401,3 +401,20 @@ export const creatorsSupported = (version: string): boolean => {
const unsupportedVersion = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']; const unsupportedVersion = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'];
return !unsupportedVersion.includes(version); return !unsupportedVersion.includes(version);
}; };
// Best-effort deletion of a user-owned MXC URI from the homeserver.
// Synapse 1.97+ supports DELETE /_matrix/client/v1/media/{server}/{mediaId} for media owners.
// Failures are silently ignored — this is cleanup only, not critical path.
export const tryDeleteMxcContent = async (mx: MatrixClient, mxcUrl: string): Promise<void> => {
try {
const path = mxcUrl.replace('mxc://', '');
const token = mx.getAccessToken();
if (!token || !path.includes('/')) return;
await fetch(`${mx.getHomeserverUrl()}/_matrix/client/v1/media/${path}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` },
});
} catch {
// Intentionally swallowed
}
};