mirror of
https://github.com/layer-systems/website.git
synced 2026-09-12 21:53:40 +02:00
Address wallpaper review feedback: curated id validation, referrer policy, GIF, bitmap leak
Co-authored-by: mroxso <24775431+mroxso@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
0f43066137
commit
ae6eb695f3
@@ -164,7 +164,9 @@ function AppearanceSection() {
|
||||
|
||||
const MAX_WALLPAPER_FILE_BYTES = 5 * 1024 * 1024;
|
||||
const MAX_WALLPAPER_DIMENSION = 6000;
|
||||
const ALLOWED_WALLPAPER_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp', 'image/gif']);
|
||||
// GIFs are excluded: the upload path always re-encodes to JPEG, which would
|
||||
// silently drop animation/transparency, so we don't advertise GIF support.
|
||||
const ALLOWED_WALLPAPER_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp']);
|
||||
|
||||
export function WallpaperSection() {
|
||||
const { config, updateConfig } = useAppContext();
|
||||
@@ -237,7 +239,7 @@ export function WallpaperSection() {
|
||||
if (!file) return;
|
||||
|
||||
if (!ALLOWED_WALLPAPER_TYPES.has(file.type)) {
|
||||
toast({ title: 'Unsupported image type', description: 'Use JPEG, PNG, WebP, or GIF.', variant: 'destructive' });
|
||||
toast({ title: 'Unsupported image type', description: 'Use JPEG, PNG, or WebP.', variant: 'destructive' });
|
||||
return;
|
||||
}
|
||||
if (file.size > MAX_WALLPAPER_FILE_BYTES) {
|
||||
@@ -264,7 +266,10 @@ export function WallpaperSection() {
|
||||
canvas.width = bitmap.width;
|
||||
canvas.height = bitmap.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) throw new Error('This browser cannot process images.');
|
||||
if (!ctx) {
|
||||
bitmap.close();
|
||||
throw new Error('This browser cannot process images.');
|
||||
}
|
||||
ctx.drawImage(bitmap, 0, 0);
|
||||
bitmap.close();
|
||||
|
||||
@@ -355,7 +360,7 @@ export function WallpaperSection() {
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/webp,image/gif"
|
||||
accept="image/png,image/jpeg,image/webp"
|
||||
className="sr-only"
|
||||
aria-label="Upload a local image to use as wallpaper"
|
||||
onChange={(event) => { void onFileSelected(event.target.files?.[0]); }}
|
||||
@@ -382,6 +387,7 @@ export function WallpaperSection() {
|
||||
<img
|
||||
src={preview.url}
|
||||
alt="Wallpaper preview"
|
||||
referrerPolicy="no-referrer"
|
||||
className={cn(
|
||||
'h-24 w-full rounded-md border border-border bg-muted',
|
||||
fit === 'contain' ? 'object-contain' : 'object-cover',
|
||||
|
||||
@@ -20,7 +20,7 @@ import { swapDesktopSlots, type DesktopSlot, type GridGeometry } from '@/os/icon
|
||||
import { useIconLayout } from '@/os/useIconLayout';
|
||||
import { useAppContext } from '@/hooks/useAppContext';
|
||||
import { useDecodedImage } from '@/hooks/useDecodedImage';
|
||||
import { CURATED_WALLPAPERS, DEFAULT_CURATED_ID, isSafeWallpaperUrl, resolveCurated } from '@/lib/wallpaper';
|
||||
import { CURATED_WALLPAPERS, DEFAULT_CURATED_ID, DEFAULT_PRESENTATION, isSafeWallpaperUrl, resolveCurated } from '@/lib/wallpaper';
|
||||
import { sanitizeUrl } from '@/lib/nostrUtils';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
@@ -73,6 +73,31 @@ export function Desktop() {
|
||||
customWallpaper && sanitizedWallpaperUrl && isSafeWallpaperUrl(sanitizedWallpaperUrl) ? sanitizedWallpaperUrl : undefined;
|
||||
const curatedId = wallpaper.source === 'curated' ? wallpaper.id : undefined;
|
||||
|
||||
// `decoded.url` can lag behind `customWallpaper` (a newer selection is
|
||||
// still decoding, or failed) while the previous image keeps rendering. If
|
||||
// we read `customWallpaper.presentation` directly in that window, a fit/dim
|
||||
// change meant for the *new* (not-yet-visible) image would briefly apply to
|
||||
// the still-displayed old one. This tracks (by reference, mirroring the
|
||||
// `trackedSelection` pattern in the Settings wallpaper form) the
|
||||
// presentation that belongs to whichever url is actually decoded, adjusted
|
||||
// during render rather than in a useEffect body.
|
||||
//
|
||||
// This intentionally follows React's "adjusting state during render"
|
||||
// pattern and relies on reference equality: `activePresentation` must keep
|
||||
// returning the *same* `customWallpaper.presentation` object across
|
||||
// re-renders until the selection actually changes (it does today, because
|
||||
// it's read straight from `config` rather than freshly constructed here).
|
||||
// Don't refactor it into a `useMemo`/derived value that produces a new
|
||||
// object identity every render — that would break the `!==` check below
|
||||
// and re-run `setRenderedPresentation` on every render.
|
||||
const activePresentation = customWallpaper && decoded.url === customWallpaper.url ? customWallpaper.presentation : undefined;
|
||||
const [trackedPresentation, setTrackedPresentation] = useState(activePresentation);
|
||||
const [renderedPresentation, setRenderedPresentation] = useState(activePresentation ?? DEFAULT_PRESENTATION);
|
||||
if (activePresentation !== trackedPresentation) {
|
||||
setTrackedPresentation(activePresentation);
|
||||
if (activePresentation) setRenderedPresentation(activePresentation);
|
||||
}
|
||||
|
||||
const setCuratedWallpaper = useCallback((id: string) => {
|
||||
updateConfig((current) => ({ ...current, wallpaper: { version: 1, selection: { source: 'curated', id } } }));
|
||||
}, [updateConfig]);
|
||||
@@ -179,15 +204,16 @@ export function Desktop() {
|
||||
src={safeWallpaperImageUrl}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
referrerPolicy="no-referrer"
|
||||
className={cn(
|
||||
'pointer-events-none absolute inset-0 h-full w-full',
|
||||
customWallpaper.presentation.fit === 'contain' ? 'object-contain' : 'object-cover',
|
||||
renderedPresentation.fit === 'contain' ? 'object-contain' : 'object-cover',
|
||||
)}
|
||||
/>
|
||||
{customWallpaper.presentation.dim > 0 && (
|
||||
{renderedPresentation.dim > 0 && (
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 bg-black"
|
||||
style={{ opacity: customWallpaper.presentation.dim / 100 }}
|
||||
style={{ opacity: renderedPresentation.dim / 100 }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -69,6 +69,11 @@ describe('WallpaperPreferenceSchema', () => {
|
||||
expect(WallpaperPreferenceSchema.parse('not an object')).toEqual(DEFAULT_WALLPAPER);
|
||||
});
|
||||
|
||||
it('falls back to the default wallpaper for an unknown/stale curated id', () => {
|
||||
const value = { version: 1, selection: { source: 'curated', id: 'some-removed-id' } };
|
||||
expect(WallpaperPreferenceSchema.parse(value)).toEqual(DEFAULT_WALLPAPER);
|
||||
});
|
||||
|
||||
it('falls back to the default wallpaper for an out-of-range dim value', () => {
|
||||
const value = {
|
||||
version: 1,
|
||||
|
||||
@@ -67,7 +67,11 @@ export const DEFAULT_WALLPAPER: WallpaperPreference = {
|
||||
|
||||
/** Looks up a curated wallpaper by id, falling back to the default pattern for unknown/stale ids. */
|
||||
export function resolveCurated(id: string): CuratedWallpaper {
|
||||
return CURATED_WALLPAPERS.find((wallpaper) => wallpaper.id === id) ?? CURATED_WALLPAPERS[0];
|
||||
return (
|
||||
CURATED_WALLPAPERS.find((wallpaper) => wallpaper.id === id) ??
|
||||
CURATED_WALLPAPERS.find((wallpaper) => wallpaper.id === DEFAULT_CURATED_ID) ??
|
||||
CURATED_WALLPAPERS[0]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,9 +102,11 @@ const WallpaperPresentationSchema = z.object({
|
||||
dim: z.number().min(0).max(80),
|
||||
}) satisfies z.ZodType<WallpaperPresentation>;
|
||||
|
||||
const curatedIds = CURATED_WALLPAPERS.map((wallpaper) => wallpaper.id) as [string, ...string[]];
|
||||
|
||||
const CuratedSelectionSchema = z.object({
|
||||
source: z.literal('curated'),
|
||||
id: z.string().min(1).max(64),
|
||||
id: z.enum(curatedIds),
|
||||
});
|
||||
|
||||
const UrlSelectionSchema = z.object({
|
||||
|
||||
Reference in New Issue
Block a user