feat: optimize profile image sizes, silent PWA updates, storage cleanup fixes

- Resize avatars/icons to 256px and banners to 1280px (client crop + server safety net)
- Add server-side resizeProfileImage() for federation/API uploads without crop modal
- Fix unconstrained crop on RegisterPage and CreateSpace (was missing maxOutputDimension)
- PWA: switch to autoUpdate with skipWaiting/clientsClaim for seamless deploys
- Storage janitor: exclude profile images from unlinked cleanup, delete stale thumbnails
- Add deleteAttachmentByFilename() to clean orphaned attachment records for profile images
- Migration: one-time cleanup of stale profile image attachment records
- GeneralPanel: wrap in <form> to prevent implicit submission
This commit is contained in:
Jannis Braun
2026-03-15 19:16:48 +01:00
parent 4d230711fc
commit 2e6fa3cdc6
15 changed files with 250 additions and 30 deletions
+2 -2
View File
@@ -4,7 +4,7 @@ import { LoginPage } from './components/auth/LoginPage';
import { RegisterPage } from './components/auth/RegisterPage';
import { AppLayout } from './components/layout/AppLayout';
import { JoinPage } from './components/JoinPage';
import { SwUpdatePrompt } from './components/ui/SwUpdatePrompt';
import { SwAutoUpdate } from './components/ui/SwUpdatePrompt';
import { useAuthStore } from './stores/authStore';
function ProtectedRoute({ children }: { children: React.ReactNode }) {
@@ -29,7 +29,7 @@ function AuthRedirect({ children }: { children: React.ReactNode }) {
export function App() {
return (
<>
<SwUpdatePrompt />
<SwAutoUpdate />
<Routes>
<Route
path="/login"
@@ -473,6 +473,7 @@ export function RegisterPage() {
title="Crop Avatar"
aspectRatio={1}
cropShape="round"
maxOutputDimension={256}
/>
)}
</div>
@@ -300,6 +300,7 @@ export function CreateSpaceModal() {
title="Crop Space Icon"
cropShape="round"
aspectRatio={1}
maxOutputDimension={256}
/>
</>
);
@@ -66,7 +66,7 @@ export function GeneralPanel() {
};
return (
<div className="space-y-5">
<form className="space-y-5" onSubmit={(e) => e.preventDefault()}>
<div className="text-xs text-txt-tertiary">
Configure your Backspace instance. These settings affect all users.
</div>
@@ -178,6 +178,6 @@ export function GeneralPanel() {
</div>
</div>
)}
</div>
</form>
);
}
@@ -741,7 +741,7 @@ export function AccountPanel() {
title="Crop Avatar"
cropShape="round"
aspectRatio={1}
maxOutputDimension={512}
maxOutputDimension={256}
/>
<ImageCropModal
isOpen={bannerCropSrc !== null}
@@ -751,7 +751,7 @@ export function AccountPanel() {
title="Crop Banner"
cropShape="rect"
aspectRatio={3}
maxOutputDimension={1920}
maxOutputDimension={1280}
/>
<DeleteAccountModal
@@ -601,7 +601,7 @@ export function OverviewPanel({ spaceId }: OverviewPanelProps) {
title="Crop Space Icon"
cropShape="round"
aspectRatio={1}
maxOutputDimension={512}
maxOutputDimension={256}
/>
<ImageCropModal
@@ -612,7 +612,7 @@ export function OverviewPanel({ spaceId }: OverviewPanelProps) {
title="Crop Space Banner"
cropShape="rect"
aspectRatio={16 / 9}
maxOutputDimension={1920}
maxOutputDimension={1280}
/>
</>
);
@@ -1,22 +1,22 @@
import { useRegisterSW } from 'virtual:pwa-register/react';
import { useEffect } from 'react';
export function SwUpdatePrompt() {
const {
needRefresh: [needRefresh],
updateServiceWorker,
} = useRegisterSW();
export function SwAutoUpdate() {
useRegisterSW({
onRegisteredSW(_swUrl, registration) {
if (!registration) return;
setInterval(() => {
registration.update();
}, 60_000);
},
});
if (!needRefresh) return null;
useEffect(() => {
if (!navigator.serviceWorker) return;
const onControllerChange = () => window.location.reload();
navigator.serviceWorker.addEventListener('controllerchange', onControllerChange);
return () => navigator.serviceWorker.removeEventListener('controllerchange', onControllerChange);
}, []);
return (
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-[9999] glass-pill px-4 py-2.5 flex items-center gap-3 text-sm text-txt-primary shadow-lg">
<span>A new version is available</span>
<button
onClick={() => updateServiceWorker(true)}
className="px-3 py-1 rounded-md bg-accent-primary text-white text-xs font-medium hover:opacity-90 transition-opacity"
>
Reload
</button>
</div>
);
return null;
}