refactor(editor): add attachments (#11842)

Closes: [BS-3256](https://linear.app/affine-design/issue/BS-3256/重构-add-attachments-方法)
This commit is contained in:
fundon
2025-04-21 09:07:02 +00:00
parent 61ce1123ae
commit e60d5483ad
4 changed files with 83 additions and 107 deletions
@@ -1,10 +1,7 @@
import { SurfaceBlockModel } from '@blocksuite/affine-block-surface';
import { FileDropConfigExtension } from '@blocksuite/affine-components/drop-indicator';
import { AttachmentBlockSchema } from '@blocksuite/affine-model';
import {
FileSizeLimitService,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
import {
isInsideEdgelessEditor,
matchModels,
@@ -22,13 +19,10 @@ export const AttachmentDropOption = FileDropConfigExtension({
);
if (!attachmentFiles.length) return false;
const maxFileSize = std.store.get(FileSizeLimitService).maxFileSize;
if (targetModel && !matchModels(targetModel, [SurfaceBlockModel])) {
addSiblingAttachmentBlocks(
std.host,
std,
attachmentFiles,
maxFileSize,
targetModel,
placement
).catch(console.error);
@@ -1,4 +1,3 @@
import { FileSizeLimitService } from '@blocksuite/affine-shared/services';
import { openFileOrFiles } from '@blocksuite/affine-shared/utils';
import { type SlashMenuConfig } from '@blocksuite/affine-widget-slash-menu';
import { ExportToPdfIcon, FileIcon } from '@blocksuite/icons/lit';
@@ -24,14 +23,8 @@ export const attachmentSlashMenuConfig: SlashMenuConfig = {
(async () => {
const file = await openFileOrFiles();
if (!file) return;
const maxFileSize = std.store.get(FileSizeLimitService).maxFileSize;
await addSiblingAttachmentBlocks(
std.host,
[file],
maxFileSize,
model,
'after'
);
await addSiblingAttachmentBlocks(std, [file], model, 'after');
if (model.text?.length === 0) {
std.store.deleteBlock(model);
}
@@ -54,15 +47,7 @@ export const attachmentSlashMenuConfig: SlashMenuConfig = {
const file = await openFileOrFiles();
if (!file) return;
const maxFileSize = std.store.get(FileSizeLimitService).maxFileSize;
await addSiblingAttachmentBlocks(
std.host,
[file],
maxFileSize,
model,
'after'
);
await addSiblingAttachmentBlocks(std, [file], model, 'after');
if (model.text?.length === 0) {
std.store.deleteBlock(model);
}
@@ -1,7 +1,8 @@
import { toast } from '@blocksuite/affine-components/toast';
import type {
AttachmentBlockModel,
AttachmentBlockProps,
import {
type AttachmentBlockModel,
type AttachmentBlockProps,
AttachmentBlockSchema,
} from '@blocksuite/affine-model';
import {
EMBED_CARD_HEIGHT,
@@ -12,8 +13,8 @@ import {
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
import { humanFileSize } from '@blocksuite/affine-shared/utils';
import { Bound, type IVec, Point, Vec } from '@blocksuite/global/gfx';
import type { BlockStdScope, EditorHost } from '@blocksuite/std';
import { Bound, type IVec, Vec } from '@blocksuite/global/gfx';
import type { BlockStdScope } from '@blocksuite/std';
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
import type { BlockModel } from '@blocksuite/store';
@@ -34,7 +35,7 @@ function isAttachmentUploading(blockId: string) {
* This function will not verify the size of the file.
*/
export async function uploadAttachmentBlob(
editorHost: EditorHost,
std: BlockStdScope,
blockId: string,
blob: Blob,
filetype: string,
@@ -44,43 +45,40 @@ export async function uploadAttachmentBlob(
return;
}
const doc = editorHost.doc;
let sourceId: string | undefined;
try {
setAttachmentUploading(blockId);
sourceId = await doc.blobSync.set(blob);
sourceId = await std.store.blobSync.set(blob);
} catch (error) {
console.error(error);
if (error instanceof Error) {
toast(
editorHost,
std.host,
`Failed to upload attachment! ${error.message || error.toString()}`
);
}
} finally {
setAttachmentUploaded(blockId);
const block = doc.getBlock(blockId);
const block = std.store.getBlock(blockId);
doc.withoutTransact(() => {
std.store.withoutTransact(() => {
if (!block) return;
doc.updateBlock(block.model, {
std.store.updateBlock(block.model, {
sourceId,
} satisfies Partial<AttachmentBlockProps>);
});
editorHost.std
.getOptional(TelemetryProvider)
?.track('AttachmentUploadedEvent', {
page: `${isEdgeless ? 'whiteboard' : 'doc'} editor`,
module: 'attachment',
segment: 'attachment',
control: 'uploader',
type: filetype,
category: block && sourceId ? 'success' : 'failure',
});
std.getOptional(TelemetryProvider)?.track('AttachmentUploadedEvent', {
page: `${isEdgeless ? 'whiteboard' : 'doc'} editor`,
module: 'attachment',
segment: 'attachment',
control: 'uploader',
type: filetype,
category: block && sourceId ? 'success' : 'failure',
});
}
}
@@ -185,9 +183,8 @@ export function downloadAttachmentBlob(block: AttachmentBlockComponent) {
}
export async function getFileType(file: File) {
if (file.type) {
return file.type;
}
if (file.type) return file.type;
// If the file type is not available, try to get it from the buffer.
const buffer = await file.arrayBuffer();
const FileType = await import('file-type');
@@ -199,9 +196,8 @@ export async function getFileType(file: File) {
* Add a new attachment block before / after the specified block.
*/
export async function addSiblingAttachmentBlocks(
editorHost: EditorHost,
std: BlockStdScope,
files: File[],
maxFileSize: number,
targetModel: BlockModel,
place: 'before' | 'after' = 'after',
isEmbed?: boolean
@@ -210,10 +206,11 @@ export async function addSiblingAttachmentBlocks(
return;
}
const maxFileSize = std.store.get(FileSizeLimitService).maxFileSize;
const isSizeExceeded = files.some(file => file.size > maxFileSize);
if (isSizeExceeded) {
toast(
editorHost,
std.host,
`You can only upload files less than ${humanFileSize(
maxFileSize,
true,
@@ -224,29 +221,36 @@ export async function addSiblingAttachmentBlocks(
}
const doc = targetModel.doc;
const flavour = AttachmentBlockSchema.model.flavour;
// Get the types of all files
const types = await Promise.all(files.map(file => getFileType(file)));
const attachmentBlockProps: (Partial<AttachmentBlockProps> & {
flavour: 'affine:attachment';
})[] = files.map((file, index) => ({
flavour: 'affine:attachment',
name: file.name,
size: file.size,
type: types[index],
embed: isEmbed,
}));
const droppedInfos = await Promise.all(
files.map(async file => {
const { name, size } = file;
const type = await getFileType(file);
const props = {
flavour,
name,
size,
type,
embed: isEmbed,
} satisfies Partial<AttachmentBlockProps> & {
flavour: typeof flavour;
};
return { props, file };
})
);
const blockIds = doc.addSiblingBlocks(
targetModel,
attachmentBlockProps,
droppedInfos.map(info => info.props),
place
);
blockIds.forEach(
(blockId, index) =>
void uploadAttachmentBlob(editorHost, blockId, files[index], types[index])
);
const uploadPromises = blockIds.map(async (blockId, index) => {
const { props, file } = droppedInfos[index];
await uploadAttachmentBlob(std, blockId, file, props.type);
});
await Promise.all(uploadPromises);
return blockIds;
}
@@ -284,40 +288,38 @@ export async function addAttachments(
}
}
const CARD_STACK_GAP = 32;
const xy = [x, y];
const style = 'cubeThick';
const gap = 32;
const width = EMBED_CARD_WIDTH.cubeThick;
const height = EMBED_CARD_HEIGHT.cubeThick;
const dropInfos: { blockId: string; file: File }[] = files.map(
(file, index) => {
const point = new Point(
x + index * CARD_STACK_GAP,
y + index * CARD_STACK_GAP
);
const center = Vec.toVec(point);
const bound = Bound.fromCenter(
center,
EMBED_CARD_WIDTH.cubeThick,
EMBED_CARD_HEIGHT.cubeThick
);
const blockId = std.store.addBlock(
'affine:attachment',
{
name: file.name,
size: file.size,
type: file.type,
style: 'cubeThick',
xywh: bound.serialize(),
} satisfies Partial<AttachmentBlockProps>,
gfx.surface
);
const droppedInfos = files.map((file, index) => {
const { name, size } = file;
const center = Vec.addScalar(xy, index * gap);
const xywh = Bound.fromCenter(center, width, height).serialize();
const props = {
style,
name,
size,
xywh,
} satisfies Partial<AttachmentBlockProps>;
return { blockId, file };
}
);
return { file, props };
});
// upload file and update the attachment model
const uploadPromises = dropInfos.map(async ({ blockId, file }) => {
const filetype = await getFileType(file);
await uploadAttachmentBlob(std.host, blockId, file, filetype, true);
const uploadPromises = droppedInfos.map(async ({ props, file }) => {
const type = await getFileType(file);
const blockId = std.store.addBlock(
AttachmentBlockSchema.model.flavour,
{ ...props, type },
gfx.surface
);
await uploadAttachmentBlob(std, blockId, file, type, true);
return blockId;
});
const blockIds = await Promise.all(uploadPromises);
@@ -45,7 +45,6 @@ import {
getTextSelectionCommand,
} from '@blocksuite/affine-shared/commands';
import { REFERENCE_NODE } from '@blocksuite/affine-shared/consts';
import { FileSizeLimitService } from '@blocksuite/affine-shared/services';
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import {
createDefaultDoc,
@@ -415,9 +414,7 @@ const contentMediaToolGroup: KeyboardToolPanelGroup = {
const file = await openFileOrFiles();
if (!file) return;
const maxFileSize = std.store.get(FileSizeLimitService).maxFileSize;
await addSiblingAttachmentBlocks(std.host, [file], maxFileSize, model);
await addSiblingAttachmentBlocks(std, [file], model);
if (model.text?.length === 0) {
std.store.deleteBlock(model);
}
@@ -1015,9 +1012,7 @@ export const defaultKeyboardToolbarConfig: KeyboardToolbarConfig = {
const file = await openFileOrFiles();
if (!file) return;
const maxFileSize = std.store.get(FileSizeLimitService).maxFileSize;
await addSiblingAttachmentBlocks(std.host, [file], maxFileSize, model);
await addSiblingAttachmentBlocks(std, [file], model);
if (model.text?.length === 0) {
std.store.deleteBlock(model);
}