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"