diff --git a/web/app/account/(commonLayout)/account-page/AvatarWithEdit.tsx b/web/app/account/(commonLayout)/account-page/AvatarWithEdit.tsx index 84d019f174c..c69842eb931 100644 --- a/web/app/account/(commonLayout)/account-page/AvatarWithEdit.tsx +++ b/web/app/account/(commonLayout)/account-page/AvatarWithEdit.tsx @@ -16,7 +16,7 @@ import Divider from '@/app/components/base/divider' import { useLocalFileUploader } from '@/app/components/base/image-uploader/hooks' import { DISABLE_UPLOAD_IMAGE_AS_ICON } from '@/config' import { updateUserProfile } from '@/service/common' -import { createCroppedAvatarImage } from './avatar-image' +import { createAvatarImageFile, createCroppedAvatarImage } from './avatar-image' type InputImageInfo = | { file: File } @@ -111,7 +111,7 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => { inputImageInfo.croppedAreaPixels, inputImageInfo.fileName, ) - const file = new File([blob], inputImageInfo.fileName, { type: blob.type }) + const file = createAvatarImageFile(blob, inputImageInfo.fileName) handleLocalFileUpload(file) }, [handleLocalFileUpload, inputImageInfo]) diff --git a/web/app/account/(commonLayout)/account-page/__tests__/AvatarWithEdit.spec.tsx b/web/app/account/(commonLayout)/account-page/__tests__/AvatarWithEdit.spec.tsx index f57b5ac4328..96bcab92141 100644 --- a/web/app/account/(commonLayout)/account-page/__tests__/AvatarWithEdit.spec.tsx +++ b/web/app/account/(commonLayout)/account-page/__tests__/AvatarWithEdit.spec.tsx @@ -45,9 +45,13 @@ vi.mock('@/app/components/base/image-uploader/hooks', () => ({ }), })) -vi.mock('../avatar-image', () => ({ - createCroppedAvatarImage: vi.fn(), -})) +vi.mock('../avatar-image', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + createCroppedAvatarImage: vi.fn(), + } +}) const mockedCreateCroppedAvatarImage = vi.mocked(createCroppedAvatarImage) diff --git a/web/app/account/(commonLayout)/account-page/__tests__/avatar-image.spec.ts b/web/app/account/(commonLayout)/account-page/__tests__/avatar-image.spec.ts index 74308136813..5b22a3c709f 100644 --- a/web/app/account/(commonLayout)/account-page/__tests__/avatar-image.spec.ts +++ b/web/app/account/(commonLayout)/account-page/__tests__/avatar-image.spec.ts @@ -1,6 +1,10 @@ import type { Area } from 'react-easy-crop' import { createImage } from '@/app/components/base/app-icon-picker/utils' -import { createCroppedAvatarImage, getBoundedAvatarImageSize } from '../avatar-image' +import { + createAvatarImageFile, + createCroppedAvatarImage, + getBoundedAvatarImageSize, +} from '../avatar-image' vi.mock('@/app/components/base/app-icon-picker/utils', async (importOriginal) => { const actual = @@ -58,4 +62,13 @@ describe('avatar image', () => { expect(context.drawImage).toHaveBeenCalledWith(image, 40, 20, 1000, 1000, 0, 0, 256, 256) expect(canvas.toBlob).toHaveBeenCalledWith(expect.any(Function), 'image/png', 0.85) }) + + it('matches the file extension to the browser output type', () => { + const blob = new Blob(['avatar'], { type: 'image/png' }) + + const file = createAvatarImageFile(blob, 'avatar.webp') + + expect(file.name).toBe('avatar.png') + expect(file.type).toBe('image/png') + }) }) diff --git a/web/app/account/(commonLayout)/account-page/avatar-image.ts b/web/app/account/(commonLayout)/account-page/avatar-image.ts index 84240d4211a..8d5b66b1d72 100644 --- a/web/app/account/(commonLayout)/account-page/avatar-image.ts +++ b/web/app/account/(commonLayout)/account-page/avatar-image.ts @@ -3,6 +3,11 @@ import { createImage, getMimeType } from '@/app/components/base/app-icon-picker/ const AVATAR_IMAGE_MAX_SIZE = 256 const AVATAR_IMAGE_QUALITY = 0.85 +const AVATAR_IMAGE_EXTENSION_BY_MIME_TYPE: Record = { + 'image/jpeg': 'jpg', + 'image/png': 'png', + 'image/webp': 'webp', +} export const getBoundedAvatarImageSize = ( crop: Pick, @@ -55,3 +60,15 @@ export const createCroppedAvatarImage = async ( ) }) } + +export const createAvatarImageFile = (blob: Blob, fileName: string) => { + const extension = AVATAR_IMAGE_EXTENSION_BY_MIME_TYPE[blob.type] + + if (!extension || getMimeType(fileName) === blob.type) + return new File([blob], fileName, { type: blob.type }) + + const extensionStart = fileName.lastIndexOf('.') + const baseName = extensionStart > 0 ? fileName.slice(0, extensionStart) : fileName + + return new File([blob], `${baseName}.${extension}`, { type: blob.type }) +}