refactor(editor): file size limit service (#12026)

Closes: [BS-3359](https://linear.app/affine-design/issue/BS-3359/重构-filesizelimitservice-支持-handle-文件超出限制)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added an "Upgrade" button to attachment blocks that appears when file size limits are exceeded, enabling users to view storage plans and upgrade.

- **Refactor**
  - Unified file size limit handling across attachments and images for consistency.
  - Redesigned file size limit service with improved integration and dependency injection.

- **Chores**
  - Updated service registrations and dependency management for file size limit enforcement.
  - Integrated a new file size limit extension that triggers storage plan dialogs and tracking events on limit exceedance.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
fundon
2025-04-29 08:05:28 +00:00
parent 7e4af90c03
commit 0f87136fd7
9 changed files with 92 additions and 20 deletions

View File

@@ -11,11 +11,16 @@ import {
AttachmentBlockStyles, AttachmentBlockStyles,
} from '@blocksuite/affine-model'; } from '@blocksuite/affine-model';
import { import {
FileSizeLimitService, FileSizeLimitProvider,
ThemeProvider, ThemeProvider,
} 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 { AttachmentIcon, ResetIcon, WarningIcon } from '@blocksuite/icons/lit'; import {
AttachmentIcon,
ResetIcon,
UpgradeIcon,
WarningIcon,
} from '@blocksuite/icons/lit';
import { BlockSelection } from '@blocksuite/std'; import { BlockSelection } from '@blocksuite/std';
import { Slice } from '@blocksuite/store'; import { Slice } from '@blocksuite/store';
import { type BlobState } from '@blocksuite/sync'; import { type BlobState } from '@blocksuite/sync';
@@ -50,7 +55,7 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
}); });
private get _maxFileSize() { private get _maxFileSize() {
return this.std.store.get(FileSizeLimitService).maxFileSize; return this.std.get(FileSizeLimitProvider).maxFileSize;
} }
get isCitation() { get isCitation() {
@@ -185,7 +190,24 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
} }
protected renderUpgradeButton = () => { protected renderUpgradeButton = () => {
return null; if (this.std.store.readonly) return null;
const onOverFileSize = this.std.get(FileSizeLimitProvider).onOverFileSize;
return when(
onOverFileSize,
() => html`
<button
class="affine-attachment-content-button"
@click=${(event: MouseEvent) => {
event.stopPropagation();
onOverFileSize?.();
}}
>
${UpgradeIcon()} Upgrade
</button>
`
);
}; };
protected renderReloadButton = () => { protected renderReloadButton = () => {

View File

@@ -3,7 +3,7 @@ import {
type ImageBlockProps, type ImageBlockProps,
MAX_IMAGE_WIDTH, MAX_IMAGE_WIDTH,
} from '@blocksuite/affine-model'; } from '@blocksuite/affine-model';
import { FileSizeLimitService } from '@blocksuite/affine-shared/services'; import { FileSizeLimitProvider } from '@blocksuite/affine-shared/services';
import { import {
readImageSize, readImageSize,
transformModel, transformModel,
@@ -68,7 +68,7 @@ export const AttachmentEmbedProvider = createIdentifier<AttachmentEmbedService>(
export class AttachmentEmbedService extends Extension { export class AttachmentEmbedService extends Extension {
private get _maxFileSize() { private get _maxFileSize() {
return this.std.store.get(FileSizeLimitService).maxFileSize; return this.std.get(FileSizeLimitProvider).maxFileSize;
} }
get keys() { get keys() {

View File

@@ -10,7 +10,7 @@ import {
} from '@blocksuite/affine-shared/consts'; } from '@blocksuite/affine-shared/consts';
import { import {
type AttachmentUploadedEvent, type AttachmentUploadedEvent,
FileSizeLimitService, FileSizeLimitProvider,
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';
@@ -110,7 +110,7 @@ export async function getFileType(file: File) {
function hasExceeded( function hasExceeded(
std: BlockStdScope, std: BlockStdScope,
files: File[], files: File[],
maxFileSize = std.store.get(FileSizeLimitService).maxFileSize maxFileSize = std.get(FileSizeLimitProvider).maxFileSize
) { ) {
const exceeded = files.some(file => file.size > maxFileSize); const exceeded = files.some(file => file.size > maxFileSize);

View File

@@ -7,7 +7,7 @@ import {
ImageBlockSchema, ImageBlockSchema,
} from '@blocksuite/affine-model'; } from '@blocksuite/affine-model';
import { import {
FileSizeLimitService, FileSizeLimitProvider,
NativeClipboardProvider, NativeClipboardProvider,
} from '@blocksuite/affine-shared/services'; } from '@blocksuite/affine-shared/services';
import { import {
@@ -362,7 +362,7 @@ export function shouldResizeImage(node: Node, target: EventTarget | null) {
function hasExceeded( function hasExceeded(
std: BlockStdScope, std: BlockStdScope,
files: File[], files: File[],
maxFileSize = std.store.get(FileSizeLimitService).maxFileSize maxFileSize = std.get(FileSizeLimitProvider).maxFileSize
) { ) {
const exceeded = files.some(file => file.size > maxFileSize); const exceeded = files.some(file => file.size > maxFileSize);

View File

@@ -15,7 +15,6 @@ import { HighlightSelectionExtension } from '@blocksuite/affine-shared/selection
import { import {
BlockMetaService, BlockMetaService,
FeatureFlagService, FeatureFlagService,
FileSizeLimitService,
LinkPreviewerService, LinkPreviewerService,
} from '@blocksuite/affine-shared/services'; } from '@blocksuite/affine-shared/services';
import { import {
@@ -51,7 +50,6 @@ export class FoundationStoreExtension extends StoreExtensionProvider {
BlockMetaService, BlockMetaService,
// TODO(@mirone): maybe merge these services into a file setting service // TODO(@mirone): maybe merge these services into a file setting service
LinkPreviewerService, LinkPreviewerService,
FileSizeLimitService,
ImageProxyService, ImageProxyService,
]); ]);
} }

View File

@@ -19,6 +19,7 @@ import {
DocModeService, DocModeService,
EditPropsStore, EditPropsStore,
EmbedOptionService, EmbedOptionService,
FileSizeLimitService,
FontLoaderService, FontLoaderService,
PageViewportServiceExtension, PageViewportServiceExtension,
ThemeService, ThemeService,
@@ -109,6 +110,7 @@ export class FoundationViewExtension extends ViewExtensionProvider {
FileDropExtension, FileDropExtension,
ToolbarRegistryExtension, ToolbarRegistryExtension,
AutoClearSelectionService, AutoClearSelectionService,
FileSizeLimitService,
]); ]);
context.register(clipboardConfigs); context.register(clipboardConfigs);
if (this.isEdgeless(context.scope)) { if (this.isEdgeless(context.scope)) {

View File

@@ -1,10 +1,23 @@
import { StoreExtension } from '@blocksuite/store'; import { type Container, createIdentifier } from '@blocksuite/global/di';
import { Extension } from '@blocksuite/store';
// bytes.parse('2GB') export interface IFileSizeLimitService {
const maxFileSize = 2 * 1024 * 1024 * 1024; maxFileSize: number;
onOverFileSize?: () => void;
export class FileSizeLimitService extends StoreExtension { }
static override key = 'file-size-limit';
export const FileSizeLimitProvider = createIdentifier<IFileSizeLimitService>(
maxFileSize = maxFileSize; 'FileSizeLimitService'
);
export class FileSizeLimitService
extends Extension
implements IFileSizeLimitService
{
// 2GB
maxFileSize = 2 * 1024 * 1024 * 1024;
static override setup(di: Container) {
di.addImpl(FileSizeLimitProvider, FileSizeLimitService);
}
} }

View File

@@ -0,0 +1,35 @@
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import track from '@affine/track';
import type { Container } from '@blocksuite/affine/global/di';
import {
FileSizeLimitProvider,
type IFileSizeLimitService,
} from '@blocksuite/affine/shared/services';
import { Extension } from '@blocksuite/affine/store';
import type { FrameworkProvider } from '@toeverything/infra';
export function patchFileSizeLimitExtension(framework: FrameworkProvider) {
const workspaceDialogService = framework.get(WorkspaceDialogService);
class AffineFileSizeLimitService
extends Extension
implements IFileSizeLimitService
{
// 2GB
maxFileSize = 2 * 1024 * 1024 * 1024;
onOverFileSize() {
workspaceDialogService.open('setting', {
activeTab: 'plans',
scrollAnchor: 'cloudPricingPlan',
});
track.$.paywall.storage.viewPlans();
}
static override setup(di: Container) {
di.override(FileSizeLimitProvider, AffineFileSizeLimitService);
}
}
return AffineFileSizeLimitService;
}

View File

@@ -26,6 +26,7 @@ import { edgelessCopilotWidget } from '../ai/widgets/edgeless-copilot';
import { buildDocDisplayMetaExtension } from '../extensions/display-meta'; import { buildDocDisplayMetaExtension } from '../extensions/display-meta';
import { getEditorConfigExtension } from '../extensions/editor-config'; import { getEditorConfigExtension } from '../extensions/editor-config';
import { getPagePreviewThemeExtension } from '../extensions/entry/enable-preview'; import { getPagePreviewThemeExtension } from '../extensions/entry/enable-preview';
import { patchFileSizeLimitExtension } from '../extensions/file-size-limit';
import { getFontConfigExtension } from '../extensions/font-config'; import { getFontConfigExtension } from '../extensions/font-config';
import { patchPeekViewService } from '../extensions/peek-view-service'; import { patchPeekViewService } from '../extensions/peek-view-service';
import { getTelemetryExtension } from '../extensions/telemetry'; import { getTelemetryExtension } from '../extensions/telemetry';
@@ -58,6 +59,7 @@ class MigratingAffineViewExtension extends ViewExtensionProvider<
if (context.scope === 'page' || context.scope === 'edgeless') { if (context.scope === 'page' || context.scope === 'edgeless') {
context.register(getTelemetryExtension()); context.register(getTelemetryExtension());
context.register(getEditorConfigExtension(framework)); context.register(getEditorConfigExtension(framework));
context.register(patchFileSizeLimitExtension(framework));
} }
if ( if (
context.scope === 'preview-page' || context.scope === 'preview-page' ||