Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d801e01623 | |||
| 8f0e05ffef | |||
| bf75f4657d |
+3
-1
@@ -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:
|
||||
|
||||
@@ -991,6 +991,8 @@ Themes:
|
||||
**[AUDIT REQUIRED]** Study how existing wallpapers are applied in `lotus-terminal.css.ts` to extend the system correctly.
|
||||
**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
|
||||
|
||||
@@ -175,7 +175,8 @@ Emoji reaction buttons styled for terminal mode via `button[data-reaction-key]`
|
||||
|
||||
### 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 (5–80%) 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 { useMediaConfig } from '../../hooks/useMediaConfig';
|
||||
import { compressImage, formatFileSize, isCompressible } from '../../utils/imageCompression';
|
||||
import { tryDeleteMxcContent } from '../../utils/matrix';
|
||||
|
||||
type PreviewImageProps = {
|
||||
fileItem: TUploadItem;
|
||||
@@ -274,6 +275,10 @@ export function UploadCardRenderer({
|
||||
};
|
||||
|
||||
const removeUpload = () => {
|
||||
if (upload.status === UploadStatus.Success) {
|
||||
// Upload already completed — delete the orphaned MXC from the server.
|
||||
tryDeleteMxcContent(mx, upload.mxc);
|
||||
}
|
||||
cancelUpload();
|
||||
onRemove(file);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { CSSProperties } from 'react';
|
||||
import { ChatBackground } from '../../state/settings';
|
||||
import {
|
||||
animRainKeyframe,
|
||||
animStarsDriftKeyframe,
|
||||
animGridPulseKeyframe,
|
||||
animAuroraKeyframe,
|
||||
animFirefliesKeyframe,
|
||||
} from '../../styles/Animations.css';
|
||||
|
||||
export const BG_OPTIONS: { value: ChatBackground; label: string }[] = [
|
||||
{ value: 'none', label: 'None' },
|
||||
@@ -19,6 +26,11 @@ export const BG_OPTIONS: { value: ChatBackground; label: string }[] = [
|
||||
{ value: 'waves', label: 'Waves' },
|
||||
{ value: 'neon', label: 'Neon Grid' },
|
||||
{ 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> = {
|
||||
@@ -184,6 +196,71 @@ const DARK: Record<ChatBackground, CSSProperties> = {
|
||||
'radial-gradient(ellipse at 60% 90%, rgba(0,212,255,0.06) 0%, transparent 50%)',
|
||||
].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> = {
|
||||
@@ -340,7 +417,82 @@ const LIGHT: Record<ChatBackground, CSSProperties> = {
|
||||
'radial-gradient(ellipse at 60% 90%, rgba(0,160,200,0.07) 0%, transparent 50%)',
|
||||
].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 =>
|
||||
isDark ? DARK[bg] : LIGHT[bg];
|
||||
export const getChatBg = (
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -65,6 +65,7 @@ import {
|
||||
getImageInfo,
|
||||
getMxIdLocalPart,
|
||||
mxcUrlToHttp,
|
||||
tryDeleteMxcContent,
|
||||
} from '../../utils/matrix';
|
||||
import { compressImage } from '../../utils/imageCompression';
|
||||
import { useTypingStatusUpdater } from '../../hooks/useTypingStatusUpdater';
|
||||
@@ -434,6 +435,8 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
});
|
||||
const compressedMxc = (uploadRes as { content_uri: string }).content_uri;
|
||||
if (compressedMxc) {
|
||||
// Delete the pre-uploaded original so only one copy lives on the server.
|
||||
tryDeleteMxcContent(mx, upload.mxc);
|
||||
mxc = compressedMxc;
|
||||
// Build a synthetic fileItem that refers to the compressed file so
|
||||
// getImageMsgContent picks up the correct dimensions and type.
|
||||
|
||||
@@ -61,6 +61,7 @@ export function RoomView({ eventId }: { eventId?: string }) {
|
||||
const roomViewRef = useRef<HTMLDivElement>(null) as React.RefObject<HTMLDivElement>;
|
||||
const [chatBackground] = useSetting(settingsAtom, 'chatBackground');
|
||||
const [lotusTerminal] = useSetting(settingsAtom, 'lotusTerminal');
|
||||
const [pauseAnimations] = useSetting(settingsAtom, 'pauseAnimations');
|
||||
const theme = useTheme();
|
||||
const isDark = theme.kind === ThemeKind.Dark;
|
||||
|
||||
@@ -98,8 +99,12 @@ export function RoomView({ eventId }: { eventId?: string }) {
|
||||
|
||||
const chatBgStyle = useMemo(
|
||||
() =>
|
||||
getChatBg(lotusTerminal && chatBackground === 'none' ? 'tactical' : chatBackground, isDark),
|
||||
[chatBackground, lotusTerminal, isDark],
|
||||
getChatBg(
|
||||
lotusTerminal && chatBackground === 'none' ? 'tactical' : chatBackground,
|
||||
isDark,
|
||||
pauseAnimations,
|
||||
),
|
||||
[chatBackground, lotusTerminal, isDark, pauseAnimations],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -331,6 +331,7 @@ function Appearance() {
|
||||
settingsAtom,
|
||||
'glassmorphismSidebar',
|
||||
);
|
||||
const [pauseAnimations, setPauseAnimations] = useSetting(settingsAtom, 'pauseAnimations');
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
@@ -390,6 +391,14 @@ function Appearance() {
|
||||
</Box>
|
||||
</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">
|
||||
<SettingTile
|
||||
title="Show Profile on Every Message"
|
||||
@@ -1056,6 +1065,7 @@ function Calls() {
|
||||
|
||||
function ChatBgGrid() {
|
||||
const [chatBackground, setChatBackground] = useSetting(settingsAtom, 'chatBackground');
|
||||
const [pauseAnimations] = useSetting(settingsAtom, 'pauseAnimations');
|
||||
const theme = useTheme();
|
||||
const isDark = theme.kind === ThemeKind.Dark;
|
||||
|
||||
@@ -1080,7 +1090,7 @@ function ChatBgGrid() {
|
||||
: '2px solid rgba(128,128,128,0.25)',
|
||||
padding: 0,
|
||||
overflow: 'hidden',
|
||||
...getChatBg(opt.value as ChatBackground, isDark),
|
||||
...getChatBg(opt.value as ChatBackground, isDark, pauseAnimations),
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useRef } from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Scroll } from 'folds';
|
||||
import classNames from 'classnames';
|
||||
|
||||
@@ -23,10 +23,46 @@ import {
|
||||
import { CreateTab } from './sidebar/CreateTab';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { useTheme, ThemeKind } from '../../hooks/useTheme';
|
||||
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;
|
||||
|
||||
// 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 (
|
||||
<Sidebar className={classNames(glassmorphismSidebar && SidebarGlass)}>
|
||||
|
||||
@@ -26,7 +26,12 @@ export type ChatBackground =
|
||||
| 'hexgrid'
|
||||
| 'waves'
|
||||
| 'neon'
|
||||
| 'aurora';
|
||||
| 'aurora'
|
||||
| 'anim-rain'
|
||||
| 'anim-stars'
|
||||
| 'anim-pulse'
|
||||
| 'anim-aurora'
|
||||
| 'anim-fireflies';
|
||||
export enum MessageLayout {
|
||||
Modern = 0,
|
||||
Compact = 1,
|
||||
@@ -94,6 +99,8 @@ export interface Settings {
|
||||
deafenKey: string;
|
||||
|
||||
warnOnUnverifiedDevices: boolean;
|
||||
|
||||
pauseAnimations: boolean;
|
||||
}
|
||||
|
||||
const defaultSettings: Settings = {
|
||||
@@ -157,6 +164,8 @@ const defaultSettings: Settings = {
|
||||
deafenKey: 'KeyM',
|
||||
|
||||
warnOnUnverifiedDevices: false,
|
||||
|
||||
pauseAnimations: false,
|
||||
};
|
||||
|
||||
export const getSettings = (): Settings => {
|
||||
|
||||
@@ -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' },
|
||||
});
|
||||
|
||||
@@ -401,3 +401,20 @@ export const creatorsSupported = (version: string): boolean => {
|
||||
const unsupportedVersion = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'];
|
||||
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
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user