Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions desktop/src/features/agents/ui/AgentCreationPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
} from "@/features/profile/ui/ProfileAvatarEditor.utils";
import { AvatarCustomColorPanel } from "@/features/profile/ui/AvatarCustomColorPanel";
import { useAvatarUpload } from "@/features/profile/useAvatarUpload";
import { downscaleSquareImageToDataUrl } from "@/features/profile/lib/downscaleSquareImage";
import { cn } from "@/shared/lib/cn";
import { Button } from "@/shared/ui/button";
import { useEmojiBurst } from "@/shared/ui/EmojiBurstProvider";
Expand Down Expand Up @@ -55,7 +56,7 @@ export function AgentCreationPreview({
onCommitAvatar,
onUploadPendingChange,
onSelectAvatar,
processImage,
processImage = downscaleSquareImageToDataUrl,
shape = "circle",
testIdPrefix = "agent-avatar",
variant = "default",
Expand Down Expand Up @@ -127,7 +128,6 @@ export function AgentCreationPreview({
},
processImage,
});

useEmojiMartStyles(
emojiPickerContainerRef,
isAvatarMenuOpen && activeTab === "emoji",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import * as React from "react";
import { toast } from "sonner";

import { AgentCreationPreview } from "@/features/agents/ui/AgentCreationPreview";
import { downscaleIconToDataUrl } from "@/features/communities/lib/downscaleIcon";
import {
communityIconQueryKey,
useActiveCommunityIcon,
Expand Down Expand Up @@ -111,7 +110,6 @@ export function CommunityIconSettingsCard({
onClearAvatar={clearIconPreview}
onCommitAvatar={persistIcon}
onSelectAvatar={previewIcon}
processImage={downscaleIconToDataUrl}
shape="rounded-square"
testIdPrefix="community-icon"
variant={compact ? "compact" : "default"}
Expand Down
143 changes: 143 additions & 0 deletions desktop/src/features/profile/lib/downscaleSquareImage.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import assert from "node:assert/strict";
import test from "node:test";

import { downscaleSquareImageToDataUrl } from "./downscaleSquareImage.ts";

async function withImageEnvironment({ bitmap, context, toDataURL }, run) {
const bitmapDescriptor = Object.getOwnPropertyDescriptor(
globalThis,
"createImageBitmap",
);
const documentDescriptor = Object.getOwnPropertyDescriptor(
globalThis,
"document",
);
const canvas = {
height: 0,
width: 0,
getContext: () => context,
toDataURL,
};
Object.defineProperty(globalThis, "createImageBitmap", {
configurable: true,
value: async () => bitmap,
});
Object.defineProperty(globalThis, "document", {
configurable: true,
value: {
createElement: (tag) => {
assert.equal(tag, "canvas");
return canvas;
},
},
});
try {
await run(canvas);
} finally {
if (bitmapDescriptor) {
Object.defineProperty(globalThis, "createImageBitmap", bitmapDescriptor);
} else {
delete globalThis.createImageBitmap;
}
if (documentDescriptor) {
Object.defineProperty(globalThis, "document", documentDescriptor);
} else {
delete globalThis.document;
}
}
}

test("center-crops and downsizes a landscape image to 128px WebP", async () => {
const drawCalls = [];
const encodes = [];
let closed = false;
const bitmap = {
height: 200,
width: 400,
close: () => {
closed = true;
},
};
const context = {
imageSmoothingQuality: "low",
drawImage: (...args) => drawCalls.push(args),
};

await withImageEnvironment(
{
bitmap,
context,
toDataURL: (type, quality) => {
encodes.push([type, quality]);
return "data:image/webp;base64,processed";
},
},
async (canvas) => {
assert.equal(
await downscaleSquareImageToDataUrl({ name: "avatar.png" }),
"data:image/webp;base64,processed",
);
assert.equal(canvas.width, 128);
assert.equal(canvas.height, 128);
},
);

assert.equal(context.imageSmoothingQuality, "high");
assert.deepEqual(drawCalls, [[bitmap, 100, 0, 200, 200, 0, 0, 128, 128]]);
assert.deepEqual(encodes, [["image/webp", 0.85]]);
assert.equal(closed, true);
});

test("falls back to PNG when the WebView cannot encode WebP", async () => {
const formats = [];
const bitmap = { height: 120, width: 120, close() {} };
const context = { drawImage() {}, imageSmoothingQuality: "low" };

await withImageEnvironment(
{
bitmap,
context,
toDataURL: (type) => {
formats.push(type);
return type === "image/png"
? "data:image/png;base64,processed"
: "data:image/png;base64,webp-unsupported";
},
},
async () => {
assert.equal(
await downscaleSquareImageToDataUrl({ name: "avatar.png" }),
"data:image/png;base64,processed",
);
},
);

assert.deepEqual(formats, ["image/webp", "image/png"]);
});

test("closes the decoded bitmap when canvas processing fails", async () => {
let closed = false;
const bitmap = {
height: 120,
width: 120,
close: () => {
closed = true;
},
};

await withImageEnvironment(
{
bitmap,
context: null,
toDataURL: () => "",
},
async () => {
await assert.rejects(
downscaleSquareImageToDataUrl({ name: "avatar.png" }),
/Could not process that image/,
);
},
);

assert.equal(closed, true);
});
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
/**
* Downscale an image file to a small square data-URL for use as a community
* icon. The result is inlined into the kind:9033 command (and the NIP-11
* document the relay serves) so it renders
* across communities without cross-relay media fetches; the relay caps icon
* data-URLs at 96 KB, and 128px WebP/PNG output stays far under that.
* Center-crop an image file to a small square data URL for avatars and icons.
* The 128px WebP/PNG output is compact enough for inline profile data while
* retaining enough detail for every current avatar surface.
*/

const ICON_SIZE = 128;

export async function downscaleIconToDataUrl(file: File): Promise<string> {
export async function downscaleSquareImageToDataUrl(
file: File,
): Promise<string> {
const bitmap = await createImageBitmap(file);
try {
const side = Math.min(bitmap.width, bitmap.height);
Expand Down