diff --git a/packages/frontend/component/src/components/block-suite-editor/index.tsx b/packages/frontend/component/src/components/block-suite-editor/index.tsx index f0d7223791..fba1ee9a71 100644 --- a/packages/frontend/component/src/components/block-suite-editor/index.tsx +++ b/packages/frontend/component/src/components/block-suite-editor/index.tsx @@ -13,6 +13,7 @@ import { blockSuiteEditorHeaderStyle, blockSuiteEditorStyle, } from './index.css'; +import { getPresets } from './preset'; export type EditorProps = { page: Page; @@ -59,19 +60,23 @@ const BlockSuiteEditorImpl = (props: EditorProps): ReactElement => { editor.page = page; } + const presets = getPresets(); + editor.pagePreset = presets.pageModePreset; + editor.edgelessPreset = presets.edgelessModePreset; + useEffect(() => { const disposes = [] as ((() => void) | undefined)[]; if (editor) { - const dipose = editor.slots.pageModeSwitched.on(mode => { + const dispose = editor.slots.pageModeSwitched.on(mode => { onModeChange?.(mode); }); - disposes.push(() => dipose.dispose()); - } + disposes.push(() => dispose?.dispose()); - if (editor.page && onLoad) { - disposes.push(onLoad?.(page, editor)); + if (editor.page && onLoad) { + disposes.push(onLoad?.(page, editor)); + } } return () => { diff --git a/packages/frontend/component/src/components/block-suite-editor/preset.ts b/packages/frontend/component/src/components/block-suite-editor/preset.ts new file mode 100644 index 0000000000..4da074e566 --- /dev/null +++ b/packages/frontend/component/src/components/block-suite-editor/preset.ts @@ -0,0 +1,44 @@ +import { + AttachmentService, + EdgelessPreset, + PagePreset, +} from '@blocksuite/blocks'; +import bytes from 'bytes'; + +class CustomAttachmentService extends AttachmentService { + override mounted(): void { + //TODO: get user type from store + const userType = 'pro'; + if (userType === 'pro') { + this.maxFileSize = bytes.parse('100MB'); + } else { + this.maxFileSize = bytes.parse('10MB'); + } + } +} + +export function getPresets() { + const pageModePreset = PagePreset.map(preset => { + if (preset.schema.model.flavour === 'affine:attachment') { + return { + ...preset, + service: CustomAttachmentService, + }; + } + return preset; + }); + const edgelessModePreset = EdgelessPreset.map(preset => { + if (preset.schema.model.flavour === 'affine:attachment') { + return { + ...preset, + service: CustomAttachmentService, + }; + } + return preset; + }); + + return { + pageModePreset, + edgelessModePreset, + }; +} diff --git a/packages/frontend/core/src/components/affine/new-workspace-setting-detail/profile.tsx b/packages/frontend/core/src/components/affine/new-workspace-setting-detail/profile.tsx index be07d7922e..04844e7df0 100644 --- a/packages/frontend/core/src/components/affine/new-workspace-setting-detail/profile.tsx +++ b/packages/frontend/core/src/components/affine/new-workspace-setting-detail/profile.tsx @@ -75,11 +75,32 @@ export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => { }, [update] ); + + const handleUploadAvatar = useCallback( + async (file: File) => { + await update(file) + .then(() => { + pushNotification({ + title: 'Update workspace avatar success', + type: 'success', + }); + }) + .catch(error => { + pushNotification({ + title: 'Update workspace avatar failed', + message: error, + type: 'error', + }); + }); + }, + [pushNotification, update] + ); + return (
diff --git a/packages/frontend/core/src/components/affine/setting-modal/account-setting/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/account-setting/index.tsx index 34c50bbf58..ed4321a27e 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/account-setting/index.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/account-setting/index.tsx @@ -1,4 +1,5 @@ import { FlexWrapper, Input } from '@affine/component'; +import { pushNotificationAtom } from '@affine/component/notification-center'; import { SettingHeader, SettingRow, @@ -15,6 +16,7 @@ import { useMutation, useQuery } from '@affine/workspace/affine/gql'; import { ArrowRightSmallIcon, CameraIcon } from '@blocksuite/icons'; import { Avatar } from '@toeverything/components/avatar'; import { Button } from '@toeverything/components/button'; +import { validateAndReduceImage } from '@toeverything/hooks/use-block-suite-workspace-avatar-url'; import bytes from 'bytes'; import { useSetAtom } from 'jotai'; import { @@ -39,6 +41,7 @@ import * as style from './style.css'; export const UserAvatar = () => { const t = useAFFiNEI18N(); const user = useCurrentUser(); + const pushNotification = useSetAtom(pushNotificationAtom); const { trigger: avatarTrigger } = useMutation({ mutation: uploadAvatarMutation, @@ -49,14 +52,28 @@ export const UserAvatar = () => { const handleUpdateUserAvatar = useCallback( async (file: File) => { - await avatarTrigger({ - avatar: file, - }); - // XXX: This is a hack to force the user to update, since next-auth can not only use update function without params - user.update({ name: user.name }).catch(console.error); + try { + const reducedFile = await validateAndReduceImage(file); + await avatarTrigger({ + avatar: reducedFile, // Pass the reducedFile directly to the avatarTrigger + }); + // XXX: This is a hack to force the user to update, since next-auth can not only use update function without params + await user.update({ name: user.name }); + pushNotification({ + title: 'Update user avatar success', + type: 'success', + }); + } catch (e) { + pushNotification({ + title: 'Update user avatar failed', + message: String(e), + type: 'error', + }); + } }, - [avatarTrigger, user] + [avatarTrigger, pushNotification, user] ); + const handleRemoveUserAvatar = useCallback( async (e: MouseEvent) => { e.stopPropagation(); @@ -66,6 +83,7 @@ export const UserAvatar = () => { }, [removeAvatarTrigger, user] ); + return ( => { + // Declare a new async function that wraps the decode logic + const decodeAndReduceImage = async (): Promise => { + const img = new Image(); + const url = URL.createObjectURL(file); + img.src = url; + + await img.decode().catch(() => { + URL.revokeObjectURL(url); + throw new Error('Image could not be decoded'); + }); + + img.onload = img.onerror = () => { + URL.revokeObjectURL(url); + }; + + const sizeInMB = file.size / (1024 * 1024); + if (sizeInMB > 10 || img.width > 4000 || img.height > 4000) { + // Compress the file to less than 10MB + const compressedImg = await reduce().toBlob(file, { + max: 4000, + unsharpAmount: 80, + unsharpRadius: 0.6, + unsharpThreshold: 2, + }); + return compressedImg; + } + + return file; + }; + + try { + const reducedBlob = await decodeAndReduceImage(); + + return new File([reducedBlob], file.name, { type: file.type }); + } catch (error) { + throw new Error('Image could not be reduce :' + error); + } +}; + export function useBlockSuiteWorkspaceAvatarUrl( blockSuiteWorkspace: Workspace ) { @@ -23,21 +65,29 @@ export function useBlockSuiteWorkspaceAvatarUrl( suspense: true, fallbackData: null, }); + const setAvatar = useCallback( - async (file: File | null) => { + async (file: File | null): Promise => { assertExists(blockSuiteWorkspace); if (!file) { blockSuiteWorkspace.meta.setAvatar(''); - return; + return false; + } + try { + const reducedFile = await validateAndReduceImage(file); + const blobs = blockSuiteWorkspace.blobs; + const blobId = await blobs.set(reducedFile); + blockSuiteWorkspace.meta.setAvatar(blobId); + await mutate(blobId); + return true; + } catch (error) { + console.error(error); + throw error; } - const blob = new Blob([file], { type: file.type }); - const blobs = blockSuiteWorkspace.blobs; - const blobId = await blobs.set(blob); - blockSuiteWorkspace.meta.setAvatar(blobId); - await mutate(blobId); }, [blockSuiteWorkspace, mutate] ); + useEffect(() => { if (blockSuiteWorkspace) { const dispose = blockSuiteWorkspace.meta.commonFieldsUpdated.on(() => { diff --git a/yarn.lock b/yarn.lock index 6b2ca80911..188e05c365 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12518,9 +12518,11 @@ __metadata: "@blocksuite/lit": "npm:0.0.0-20231101080734-aa27dc89-nightly" "@blocksuite/store": "npm:0.0.0-20231101080734-aa27dc89-nightly" "@testing-library/react": "npm:^14.0.0" + "@types/image-blob-reduce": "npm:^4.1.3" "@types/lodash.debounce": "npm:^4.0.7" fake-indexeddb: "npm:^5.0.0" foxact: "npm:^0.2.20" + image-blob-reduce: "npm:^4.1.0" jotai: "npm:^2.4.3" lodash.debounce: "npm:^4.0.8" p-queue: "npm:^7.4.1" @@ -13147,6 +13149,15 @@ __metadata: languageName: node linkType: hard +"@types/image-blob-reduce@npm:^4.1.3": + version: 4.1.3 + resolution: "@types/image-blob-reduce@npm:4.1.3" + dependencies: + "@types/pica": "npm:*" + checksum: bb7bb308b0e9ab519117e23c6e87f2c95e518268d74a3de92b34758a7c1b482c993a2215233faddc44ffb67effdf02b0d229a80933360acebcacff6d66599dfd + languageName: node + linkType: hard + "@types/ioredis4@npm:@types/ioredis@^4.28.10": version: 4.28.10 resolution: "@types/ioredis@npm:4.28.10" @@ -13470,6 +13481,13 @@ __metadata: languageName: node linkType: hard +"@types/pica@npm:*": + version: 9.0.3 + resolution: "@types/pica@npm:9.0.3" + checksum: b58795431bfe0bbbd32f64767ab6d620f82f38a7405db6e00598065f215d72b201ad411fa0c70072526e1bf1c752d0732a79bf5c88c5b3621c58500dd736b870 + languageName: node + linkType: hard + "@types/prettier@npm:^2.1.5": version: 2.7.3 resolution: "@types/prettier@npm:2.7.3" @@ -21858,6 +21876,13 @@ __metadata: languageName: node linkType: hard +"glur@npm:^1.1.2": + version: 1.1.2 + resolution: "glur@npm:1.1.2" + checksum: 3d991f38cb352886723a84b00310673312c01573c928ef49a52e74ec693cedaa95117ef6f7e0dd5167b40a3383355042ed721cfaabd54b0e9532d01d7ff72861 + languageName: node + linkType: hard + "gonzales-pe@npm:^4.2.3, gonzales-pe@npm:^4.3.0": version: 4.3.0 resolution: "gonzales-pe@npm:4.3.0" @@ -22736,6 +22761,15 @@ __metadata: languageName: node linkType: hard +"image-blob-reduce@npm:^4.1.0": + version: 4.1.0 + resolution: "image-blob-reduce@npm:4.1.0" + dependencies: + pica: "npm:^9.0.0" + checksum: e244f3088776a3d3dec455351f248120561d060339f560b1d5abaee52c30b879e8a200306242b63a06200bd5200c910835345869fe2c25b0c08a4391d46e4055 + languageName: node + linkType: hard + "image-size@npm:^0.7.4": version: 0.7.5 resolution: "image-size@npm:0.7.5" @@ -27293,6 +27327,16 @@ __metadata: languageName: node linkType: hard +"multimath@npm:^2.0.0": + version: 2.0.0 + resolution: "multimath@npm:2.0.0" + dependencies: + glur: "npm:^1.1.2" + object-assign: "npm:^4.1.1" + checksum: 4e795c8a5d73e8c8c60a0575f08aa75f9cd4dc1acc90b7160388606e4571ac737e7ece12912153ca375318749e3fc429f85359f5be2dc8a86b1da63c4ccd5613 + languageName: node + linkType: hard + "multipipe@npm:^1.0.2": version: 1.0.2 resolution: "multipipe@npm:1.0.2" @@ -28916,6 +28960,18 @@ __metadata: languageName: node linkType: hard +"pica@npm:^9.0.0": + version: 9.0.1 + resolution: "pica@npm:9.0.1" + dependencies: + glur: "npm:^1.1.2" + multimath: "npm:^2.0.0" + object-assign: "npm:^4.1.1" + webworkify: "npm:^1.5.0" + checksum: 5053c607c8aee02f22b43d97d102eb95da673b9080bb2d3973072d60dd849644093c0d3899afed4b5001a4742b5d504a833352adabfb0e4d33aa7b59448c1c4b + languageName: node + linkType: hard + "picocolors@npm:^1.0.0": version: 1.0.0 resolution: "picocolors@npm:1.0.0" @@ -35157,6 +35213,13 @@ __metadata: languageName: node linkType: hard +"webworkify@npm:^1.5.0": + version: 1.5.0 + resolution: "webworkify@npm:1.5.0" + checksum: 2ee5b8e418da45657544bfb397599d60abc009c59d1b92f73c46a6beb9a1e08d6fabab36cd823bc4e8da0c2e498b4cf883aa5d3704d98cafe1b5faa23b92ffcc + languageName: node + linkType: hard + "well-known-symbols@npm:^2.0.0": version: 2.0.0 resolution: "well-known-symbols@npm:2.0.0"