diff --git a/src/app/components/upload-card/UploadCardRenderer.tsx b/src/app/components/upload-card/UploadCardRenderer.tsx index 4628e2b26..fe616c746 100644 --- a/src/app/components/upload-card/UploadCardRenderer.tsx +++ b/src/app/components/upload-card/UploadCardRenderer.tsx @@ -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); }; diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index 6c54104a1..0444365f2 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -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( }); 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. diff --git a/src/app/pages/client/SidebarNav.tsx b/src/app/pages/client/SidebarNav.tsx index 8f0ec1ead..9a5910eae 100644 --- a/src/app/pages/client/SidebarNav.tsx +++ b/src/app/pages/client/SidebarNav.tsx @@ -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,42 @@ 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(null) as React.RefObject; const [glassmorphismSidebar] = useSetting(settingsAtom, 'glassmorphismSidebar'); + const [chatBackground] = useSetting(settingsAtom, 'chatBackground'); + const [lotusTerminal] = useSetting(settingsAtom, 'lotusTerminal'); + 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'); + return; + } + const effectiveBg = lotusTerminal && chatBackground === 'none' ? 'tactical' : chatBackground; + const bgStyle = getChatBg(effectiveBg, isDark); + 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) ?? ''; + return () => { + style.removeProperty('background-image'); + style.removeProperty('background-color'); + style.removeProperty('background-size'); + style.removeProperty('background-position'); + }; + }, [glassmorphismSidebar, chatBackground, lotusTerminal, isDark]); return ( diff --git a/src/app/utils/matrix.ts b/src/app/utils/matrix.ts index 21ddaca8b..cc83cee3c 100644 --- a/src/app/utils/matrix.ts +++ b/src/app/utils/matrix.ts @@ -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 => { + 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 + } +};