mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
feat(core): temporary expansion files are limited to 100M (#4833)
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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 (
|
||||
<div className={style.profileWrapper}>
|
||||
<Upload
|
||||
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
|
||||
fileChange={update}
|
||||
fileChange={handleUploadAvatar}
|
||||
data-testid="upload-avatar"
|
||||
disabled={!isOwner}
|
||||
>
|
||||
|
||||
@@ -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<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
@@ -66,6 +83,7 @@ export const UserAvatar = () => {
|
||||
},
|
||||
[removeAvatarTrigger, user]
|
||||
);
|
||||
|
||||
return (
|
||||
<Upload
|
||||
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"foxact": "^0.2.20",
|
||||
"image-blob-reduce": "^4.1.0",
|
||||
"jotai": "^2.4.3",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"p-queue": "^7.4.1",
|
||||
@@ -24,6 +25,7 @@
|
||||
"@blocksuite/lit": "0.0.0-20231101080734-aa27dc89-nightly",
|
||||
"@blocksuite/store": "0.0.0-20231101080734-aa27dc89-nightly",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@types/image-blob-reduce": "^4.1.3",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"fake-indexeddb": "^5.0.0",
|
||||
"vitest": "0.34.6",
|
||||
|
||||
@@ -1,8 +1,50 @@
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import reduce from 'image-blob-reduce';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
|
||||
// validate and reduce image size and return as file
|
||||
export const validateAndReduceImage = async (file: File): Promise<File> => {
|
||||
// Declare a new async function that wraps the decode logic
|
||||
const decodeAndReduceImage = async (): Promise<Blob> => {
|
||||
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<boolean> => {
|
||||
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(() => {
|
||||
|
||||
Reference in New Issue
Block a user