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

View File

@@ -1,10 +1,7 @@
import { SurfaceBlockModel } from '@blocksuite/affine-block-surface'; import { SurfaceBlockModel } from '@blocksuite/affine-block-surface';
import { FileDropConfigExtension } from '@blocksuite/affine-components/drop-indicator'; import { FileDropConfigExtension } from '@blocksuite/affine-components/drop-indicator';
import { AttachmentBlockSchema } from '@blocksuite/affine-model'; import { AttachmentBlockSchema } from '@blocksuite/affine-model';
import { import { TelemetryProvider } from '@blocksuite/affine-shared/services';
FileSizeLimitService,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
import { import {
isInsideEdgelessEditor, isInsideEdgelessEditor,
matchModels, matchModels,
@@ -22,13 +19,10 @@ export const AttachmentDropOption = FileDropConfigExtension({
); );
if (!attachmentFiles.length) return false; if (!attachmentFiles.length) return false;
const maxFileSize = std.store.get(FileSizeLimitService).maxFileSize;
if (targetModel && !matchModels(targetModel, [SurfaceBlockModel])) { if (targetModel && !matchModels(targetModel, [SurfaceBlockModel])) {
addSiblingAttachmentBlocks( addSiblingAttachmentBlocks(
std.host, std,
attachmentFiles, attachmentFiles,
maxFileSize,
targetModel, targetModel,
placement placement
).catch(console.error); ).catch(console.error);

View File

@@ -1,4 +1,3 @@
import { FileSizeLimitService } from '@blocksuite/affine-shared/services';
import { openFileOrFiles } from '@blocksuite/affine-shared/utils'; import { openFileOrFiles } from '@blocksuite/affine-shared/utils';
import { type SlashMenuConfig } from '@blocksuite/affine-widget-slash-menu'; import { type SlashMenuConfig } from '@blocksuite/affine-widget-slash-menu';
import { ExportToPdfIcon, FileIcon } from '@blocksuite/icons/lit'; import { ExportToPdfIcon, FileIcon } from '@blocksuite/icons/lit';
@@ -24,14 +23,8 @@ export const attachmentSlashMenuConfig: SlashMenuConfig = {
(async () => { (async () => {
const file = await openFileOrFiles(); const file = await openFileOrFiles();
if (!file) return; if (!file) return;
const maxFileSize = std.store.get(FileSizeLimitService).maxFileSize;
await addSiblingAttachmentBlocks( await addSiblingAttachmentBlocks(std, [file], model, 'after');
std.host,
[file],
maxFileSize,
model,
'after'
);
if (model.text?.length === 0) { if (model.text?.length === 0) {
std.store.deleteBlock(model); std.store.deleteBlock(model);
} }
@@ -54,15 +47,7 @@ export const attachmentSlashMenuConfig: SlashMenuConfig = {
const file = await openFileOrFiles(); const file = await openFileOrFiles();
if (!file) return; if (!file) return;
const maxFileSize = std.store.get(FileSizeLimitService).maxFileSize; await addSiblingAttachmentBlocks(std, [file], model, 'after');
await addSiblingAttachmentBlocks(
std.host,
[file],
maxFileSize,
model,
'after'
);
if (model.text?.length === 0) { if (model.text?.length === 0) {
std.store.deleteBlock(model); std.store.deleteBlock(model);
} }

View File

@@ -1,7 +1,8 @@
import { toast } from '@blocksuite/affine-components/toast'; import { toast } from '@blocksuite/affine-components/toast';
import type { import {
AttachmentBlockModel, type AttachmentBlockModel,
AttachmentBlockProps, type AttachmentBlockProps,
AttachmentBlockSchema,
} from '@blocksuite/affine-model'; } from '@blocksuite/affine-model';
import { import {
EMBED_CARD_HEIGHT, EMBED_CARD_HEIGHT,
@@ -12,8 +13,8 @@ import {
TelemetryProvider, TelemetryProvider,
} from '@blocksuite/affine-shared/services'; } from '@blocksuite/affine-shared/services';
import { humanFileSize } from '@blocksuite/affine-shared/utils'; import { humanFileSize } from '@blocksuite/affine-shared/utils';
import { Bound, type IVec, Point, Vec } from '@blocksuite/global/gfx'; import { Bound, type IVec, Vec } from '@blocksuite/global/gfx';
import type { BlockStdScope, EditorHost } from '@blocksuite/std'; import type { BlockStdScope } from '@blocksuite/std';
import { GfxControllerIdentifier } from '@blocksuite/std/gfx'; import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
import type { BlockModel } from '@blocksuite/store'; import type { BlockModel } from '@blocksuite/store';
@@ -34,7 +35,7 @@ function isAttachmentUploading(blockId: string) {
* This function will not verify the size of the file. * This function will not verify the size of the file.
*/ */
export async function uploadAttachmentBlob( export async function uploadAttachmentBlob(
editorHost: EditorHost, std: BlockStdScope,
blockId: string, blockId: string,
blob: Blob, blob: Blob,
filetype: string, filetype: string,
@@ -44,43 +45,40 @@ export async function uploadAttachmentBlob(
return; return;
} }
const doc = editorHost.doc;
let sourceId: string | undefined; let sourceId: string | undefined;
try { try {
setAttachmentUploading(blockId); setAttachmentUploading(blockId);
sourceId = await doc.blobSync.set(blob); sourceId = await std.store.blobSync.set(blob);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
if (error instanceof Error) { if (error instanceof Error) {
toast( toast(
editorHost, std.host,
`Failed to upload attachment! ${error.message || error.toString()}` `Failed to upload attachment! ${error.message || error.toString()}`
); );
} }
} finally { } finally {
setAttachmentUploaded(blockId); setAttachmentUploaded(blockId);
const block = doc.getBlock(blockId); const block = std.store.getBlock(blockId);
doc.withoutTransact(() => { std.store.withoutTransact(() => {
if (!block) return; if (!block) return;
doc.updateBlock(block.model, { std.store.updateBlock(block.model, {
sourceId, sourceId,
} satisfies Partial<AttachmentBlockProps>); } satisfies Partial<AttachmentBlockProps>);
}); });
editorHost.std std.getOptional(TelemetryProvider)?.track('AttachmentUploadedEvent', {
.getOptional(TelemetryProvider) page: `${isEdgeless ? 'whiteboard' : 'doc'} editor`,
?.track('AttachmentUploadedEvent', { module: 'attachment',
page: `${isEdgeless ? 'whiteboard' : 'doc'} editor`, segment: 'attachment',
module: 'attachment', control: 'uploader',
segment: 'attachment', type: filetype,
control: 'uploader', category: block && sourceId ? 'success' : 'failure',
type: filetype, });
category: block && sourceId ? 'success' : 'failure',
});
} }
} }
@@ -185,9 +183,8 @@ export function downloadAttachmentBlob(block: AttachmentBlockComponent) {
} }
export async function getFileType(file: File) { export async function getFileType(file: File) {
if (file.type) { if (file.type) return file.type;
return file.type;
}
// If the file type is not available, try to get it from the buffer. // If the file type is not available, try to get it from the buffer.
const buffer = await file.arrayBuffer(); const buffer = await file.arrayBuffer();
const FileType = await import('file-type'); 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. * Add a new attachment block before / after the specified block.
*/ */
export async function addSiblingAttachmentBlocks( export async function addSiblingAttachmentBlocks(
editorHost: EditorHost, std: BlockStdScope,
files: File[], files: File[],
maxFileSize: number,
targetModel: BlockModel, targetModel: BlockModel,
place: 'before' | 'after' = 'after', place: 'before' | 'after' = 'after',
isEmbed?: boolean isEmbed?: boolean
@@ -210,10 +206,11 @@ export async function addSiblingAttachmentBlocks(
return; return;
} }
const maxFileSize = std.store.get(FileSizeLimitService).maxFileSize;
const isSizeExceeded = files.some(file => file.size > maxFileSize); const isSizeExceeded = files.some(file => file.size > maxFileSize);
if (isSizeExceeded) { if (isSizeExceeded) {
toast( toast(
editorHost, std.host,
`You can only upload files less than ${humanFileSize( `You can only upload files less than ${humanFileSize(
maxFileSize, maxFileSize,
true, true,
@@ -224,29 +221,36 @@ export async function addSiblingAttachmentBlocks(
} }
const doc = targetModel.doc; const doc = targetModel.doc;
const flavour = AttachmentBlockSchema.model.flavour;
// Get the types of all files const droppedInfos = await Promise.all(
const types = await Promise.all(files.map(file => getFileType(file))); files.map(async file => {
const attachmentBlockProps: (Partial<AttachmentBlockProps> & { const { name, size } = file;
flavour: 'affine:attachment'; const type = await getFileType(file);
})[] = files.map((file, index) => ({ const props = {
flavour: 'affine:attachment', flavour,
name: file.name, name,
size: file.size, size,
type: types[index], type,
embed: isEmbed, embed: isEmbed,
})); } satisfies Partial<AttachmentBlockProps> & {
flavour: typeof flavour;
};
return { props, file };
})
);
const blockIds = doc.addSiblingBlocks( const blockIds = doc.addSiblingBlocks(
targetModel, targetModel,
attachmentBlockProps, droppedInfos.map(info => info.props),
place place
); );
blockIds.forEach( const uploadPromises = blockIds.map(async (blockId, index) => {
(blockId, index) => const { props, file } = droppedInfos[index];
void uploadAttachmentBlob(editorHost, blockId, files[index], types[index]) await uploadAttachmentBlob(std, blockId, file, props.type);
); });
await Promise.all(uploadPromises);
return blockIds; 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( const droppedInfos = files.map((file, index) => {
(file, index) => { const { name, size } = file;
const point = new Point( const center = Vec.addScalar(xy, index * gap);
x + index * CARD_STACK_GAP, const xywh = Bound.fromCenter(center, width, height).serialize();
y + index * CARD_STACK_GAP const props = {
); style,
const center = Vec.toVec(point); name,
const bound = Bound.fromCenter( size,
center, xywh,
EMBED_CARD_WIDTH.cubeThick, } satisfies Partial<AttachmentBlockProps>;
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
);
return { blockId, file }; return { file, props };
} });
);
// upload file and update the attachment model // upload file and update the attachment model
const uploadPromises = dropInfos.map(async ({ blockId, file }) => { const uploadPromises = droppedInfos.map(async ({ props, file }) => {
const filetype = await getFileType(file); const type = await getFileType(file);
await uploadAttachmentBlob(std.host, blockId, file, filetype, true);
const blockId = std.store.addBlock(
AttachmentBlockSchema.model.flavour,
{ ...props, type },
gfx.surface
);
await uploadAttachmentBlob(std, blockId, file, type, true);
return blockId; return blockId;
}); });
const blockIds = await Promise.all(uploadPromises); const blockIds = await Promise.all(uploadPromises);

View File

@@ -45,7 +45,6 @@ import {
getTextSelectionCommand, getTextSelectionCommand,
} from '@blocksuite/affine-shared/commands'; } from '@blocksuite/affine-shared/commands';
import { REFERENCE_NODE } from '@blocksuite/affine-shared/consts'; import { REFERENCE_NODE } from '@blocksuite/affine-shared/consts';
import { FileSizeLimitService } from '@blocksuite/affine-shared/services';
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types'; import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import { import {
createDefaultDoc, createDefaultDoc,
@@ -415,9 +414,7 @@ const contentMediaToolGroup: KeyboardToolPanelGroup = {
const file = await openFileOrFiles(); const file = await openFileOrFiles();
if (!file) return; if (!file) return;
const maxFileSize = std.store.get(FileSizeLimitService).maxFileSize; await addSiblingAttachmentBlocks(std, [file], model);
await addSiblingAttachmentBlocks(std.host, [file], maxFileSize, model);
if (model.text?.length === 0) { if (model.text?.length === 0) {
std.store.deleteBlock(model); std.store.deleteBlock(model);
} }
@@ -1015,9 +1012,7 @@ export const defaultKeyboardToolbarConfig: KeyboardToolbarConfig = {
const file = await openFileOrFiles(); const file = await openFileOrFiles();
if (!file) return; if (!file) return;
const maxFileSize = std.store.get(FileSizeLimitService).maxFileSize; await addSiblingAttachmentBlocks(std, [file], model);
await addSiblingAttachmentBlocks(std.host, [file], maxFileSize, model);
if (model.text?.length === 0) { if (model.text?.length === 0) {
std.store.deleteBlock(model); std.store.deleteBlock(model);
} }