diff --git a/blocksuite/affine/blocks/attachment/src/attachment-block.ts b/blocksuite/affine/blocks/attachment/src/attachment-block.ts index bbf2b0b289..35d459f6c9 100644 --- a/blocksuite/affine/blocks/attachment/src/attachment-block.ts +++ b/blocksuite/affine/blocks/attachment/src/attachment-block.ts @@ -64,6 +64,11 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent { + protected renderNormalButton = (needUpload: boolean) => { + const label = needUpload ? 'retry' : 'reload'; + const run = async () => { + if (needUpload) { + await this.resourceController.upload(); + return; + } + + this.refreshData(); + }; + return html` `; }; protected renderWithHorizontal( classInfo: ClassInfo, - { icon, title, description, kind, state }: AttachmentResolvedStateInfo + { + icon, + title, + description, + kind, + state, + needUpload, + }: AttachmentResolvedStateInfo ) { return html`
@@ -261,7 +283,7 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent ${choose(state, [ - ['error', this.renderReloadButton], + ['error', () => this.renderNormalButton(needUpload)], ['error:oversize', this.renderUpgradeButton], ])}
@@ -274,7 +296,14 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent @@ -294,7 +323,7 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent ${kind} ${choose(state, [ - ['error', this.renderReloadButton], + ['error', () => this.renderNormalButton(needUpload)], ['error:oversize', this.renderUpgradeButton], ])} @@ -305,7 +334,7 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent(() => { const size = this.model.props.size; const name = this.model.props.name$.value; - const kind = getAttachmentFileIcon(name.split('.').pop() ?? ''); + const kind = getAttachmentFileIcon(this.filetype); const resolvedState = this.resourceController.resolveStateWith({ loadingIcon: LoadingIcon(), @@ -359,11 +388,16 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent + needUpload ? this.resourceController.upload() : this.reload(); + return html` this.reload()} + .needUpload=${needUpload} + .action=${action} > `; })} @@ -372,10 +406,10 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent { const { name, footnoteIdentifier } = this.model.props; - const fileType = name.split('.').pop() ?? ''; - const fileTypeIcon = getAttachmentFileIcon(fileType); + const icon = getAttachmentFileIcon(this.filetype); + return html` @@ -367,8 +369,8 @@ export class ImageBlockPageComponent extends SignalWatcher( class="drag-target" draggable="false" loading="lazy" - src=${this.block.blobUrl} - alt=${this.block.model.props.caption$.value ?? 'Image'} + src=${blobUrl} + alt=${caption} @error=${this._handleError} /> @@ -377,12 +379,16 @@ export class ImageBlockPageComponent extends SignalWatcher( ${when(loading, () => html`
${icon}
`)} ${when( - error && description, + Boolean(error && description), () => html` this.block.refreshData()} + .needUpload=${needUpload} + .action=${() => + needUpload + ? this.block.resourceController.upload() + : this.block.refreshData()} >` )} `; diff --git a/blocksuite/affine/blocks/image/src/image-edgeless-block.ts b/blocksuite/affine/blocks/image/src/image-edgeless-block.ts index 43ddc01373..4dacc45e70 100644 --- a/blocksuite/affine/blocks/image/src/image-edgeless-block.ts +++ b/blocksuite/affine/blocks/image/src/image-edgeless-block.ts @@ -137,6 +137,8 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent ${when( @@ -152,17 +154,18 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent + ${when(loading, () => html`
${icon}
`)} ${when( - resovledState.loading, - () => html`
${resovledState.icon}
` - )} - ${when( - resovledState.error && resovledState.description, + Boolean(error && description), () => html` this.refreshData()} + .message=${description} + .needUpload=${needUpload} + .action=${() => + needUpload + ? this.resourceController.upload() + : this.refreshData()} >` )} `, diff --git a/blocksuite/affine/components/src/resource/resource.ts b/blocksuite/affine/components/src/resource/resource.ts index 60efd2df92..bca8fc77a7 100644 --- a/blocksuite/affine/components/src/resource/resource.ts +++ b/blocksuite/affine/components/src/resource/resource.ts @@ -28,6 +28,7 @@ export type ResolvedStateInfoPart = { error: boolean; state: StateKind; url: string | null; + needUpload: boolean; }; export type ResolvedStateInfo = StateInfo & ResolvedStateInfoPart; @@ -41,6 +42,7 @@ export class ResourceController implements Disposable { readonly resolvedState$ = computed(() => { const url = this.blobUrl$.value; const { + needUpload = false, uploading = false, downloading = false, overSize = false, @@ -57,7 +59,13 @@ export class ResourceController implements Disposable { const loading = state === 'uploading' || state === 'loading'; - return { error: hasError, loading, state, url }; + return { + error: hasError, + needUpload, + loading, + state, + url, + }; }); private engine?: BlobEngine; @@ -92,7 +100,8 @@ export class ResourceController implements Disposable { errorIcon?: TemplateResult; } & StateInfo ): ResolvedStateInfo { - const { error, loading, state, url } = this.resolvedState$.value; + const { error, loading, state, url, needUpload } = + this.resolvedState$.value; const { icon, title, description, loadingIcon, errorIcon } = info; @@ -104,11 +113,11 @@ export class ResourceController implements Disposable { title, description, url, + needUpload, }; if (loading) { result.icon = loadingIcon ?? icon; - result.title = state === 'uploading' ? 'Uploading...' : 'Loading...'; } else if (error) { result.icon = errorIcon ?? icon; result.description = this.state$.value.errorMessage ?? description; @@ -130,13 +139,15 @@ export class ResourceController implements Disposable { if (!blobState$) return; const subscription = blobState$.subscribe(state => { - let { uploading, downloading } = state; - if (state.overSize || state.errorMessage) { + let { uploading, downloading, errorMessage } = state; + if (state.overSize) { uploading = false; downloading = false; + } else if ((uploading || downloading) && errorMessage) { + errorMessage = null; } - this.updateState({ ...state, uploading, downloading }); + this.updateState({ ...state, uploading, downloading, errorMessage }); }); return () => subscription.unsubscribe(); @@ -178,6 +189,9 @@ export class ResourceController implements Disposable { } async refreshUrlWith(type?: string) { + // Resets the state. + this.state$.value = {}; + const url = await this.createUrlWith(type); if (!url) return; @@ -191,6 +205,21 @@ export class ResourceController implements Disposable { URL.revokeObjectURL(prevUrl); } + // Re-upload to the server. + async upload() { + const blobId = this.blobId$.peek(); + if (!blobId) return; + + const state = this.state$.peek(); + if (!state.needUpload) return; + if (state.uploading) return; + + // Resets the state. + this.state$.value = {}; + + return await this.engine?.upload(blobId); + } + dispose() { const url = this.blobUrl$.peek(); if (!url) return; diff --git a/blocksuite/affine/components/src/resource/status.ts b/blocksuite/affine/components/src/resource/status.ts index e6a239d637..9c81cc09a4 100644 --- a/blocksuite/affine/components/src/resource/status.ts +++ b/blocksuite/affine/components/src/resource/status.ts @@ -2,7 +2,7 @@ import { fontBaseStyle, panelBaseColorsStyle, } from '@blocksuite/affine-shared/styles'; -import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme'; +import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme'; import { createButtonPopper, stopPropagation, @@ -15,7 +15,8 @@ import { property, query } from 'lit/decorators.js'; @requiredProperties({ message: PropTypes.string, - reload: PropTypes.instanceOf(Function), + needUpload: PropTypes.boolean, + action: PropTypes.instanceOf(Function), }) export class ResourceStatus extends WithDisposable(LitElement) { static override styles = css` @@ -32,7 +33,7 @@ export class ResourceStatus extends WithDisposable(LitElement) { cursor: pointer; color: ${unsafeCSSVarV2('button/pureWhiteText')}; background: ${unsafeCSSVarV2('status/error')}; - box-shadow: var(--affine-overlay-shadow); + box-shadow: ${unsafeCSSVar('overlayShadow')}; } ${panelBaseColorsStyle('.popper')} @@ -43,28 +44,36 @@ export class ResourceStatus extends WithDisposable(LitElement) { padding: 8px; border-radius: 8px; width: 260px; - font-size: var(--affine-font-sm); font-style: normal; font-weight: 400; line-height: 22px; + font-size: ${unsafeCSSVar('fontSm')}; &[data-show] { display: flex; flex-direction: column; - gap: 8px; + gap: 4px; } } + .header { + font-weight: 500; + } + .content { + font-feature-settings: + 'liga' off, + 'clig' off; color: ${unsafeCSSVarV2('text/primary')}; } .footer { display: flex; justify-content: flex-end; + margin-top: 4px; } - button.reload { + button.action { display: flex; align-items: center; padding: 2px 12px; @@ -102,23 +111,35 @@ export class ResourceStatus extends WithDisposable(LitElement) { this._popper?.toggle(); }); this.disposables.addFromEvent( - this._reloadButton, + this._actionButton, 'click', (_: MouseEvent) => { this._popper?.hide(); - this.reload(); + this.action(); } ); this.disposables.add(() => this._popper?.dispose()); } override render() { + const { message, needUpload } = this; + const { type, label } = needUpload + ? { + type: 'Upload', + label: 'Retry', + } + : { + type: 'Download', + label: 'Reload', + }; + return html`
-
${this.message}
+
${type} failed
+
${message}
`; @@ -130,12 +151,15 @@ export class ResourceStatus extends WithDisposable(LitElement) { @query('button.status') private accessor _trigger!: HTMLButtonElement; - @query('button.reload') - private accessor _reloadButton!: HTMLButtonElement; + @query('button.action') + private accessor _actionButton!: HTMLButtonElement; @property({ attribute: false }) accessor message!: string; @property({ attribute: false }) - accessor reload!: () => void; + accessor needUpload!: boolean; + + @property({ attribute: false }) + accessor action!: () => void; } diff --git a/blocksuite/affine/shared/src/services/telemetry-service/types.ts b/blocksuite/affine/shared/src/services/telemetry-service/types.ts index 71141175be..0917e91cbd 100644 --- a/blocksuite/affine/shared/src/services/telemetry-service/types.ts +++ b/blocksuite/affine/shared/src/services/telemetry-service/types.ts @@ -65,7 +65,7 @@ export interface AttachmentReloadedEvent extends TelemetryEvent { page: 'doc editor' | 'whiteboard editor'; segment: 'doc' | 'whiteboard'; module: 'attachment'; - control: 'reload'; + control: 'reload' | 'retry'; category: 'card' | 'embed'; type: string; // file type } diff --git a/blocksuite/framework/sync/src/blob/engine.ts b/blocksuite/framework/sync/src/blob/engine.ts index 89546eebab..f8a66a6815 100644 --- a/blocksuite/framework/sync/src/blob/engine.ts +++ b/blocksuite/framework/sync/src/blob/engine.ts @@ -108,6 +108,10 @@ export class BlobEngine { return this.main.blobState$?.(key) ?? null; } + upload(key: string) { + return this.main.upload?.(key) ?? null; + } + start() { if (this._abort) { return; diff --git a/blocksuite/framework/sync/src/blob/source.ts b/blocksuite/framework/sync/src/blob/source.ts index 6cf9239df4..3dd3cef1d1 100644 --- a/blocksuite/framework/sync/src/blob/source.ts +++ b/blocksuite/framework/sync/src/blob/source.ts @@ -5,6 +5,8 @@ export interface BlobState { downloading: boolean; errorMessage?: string | null; overSize: boolean; + needUpload: boolean; + needDownload: boolean; } export interface BlobSource { @@ -16,4 +18,6 @@ export interface BlobSource { list: () => Promise; // This state is only available when uploading to the server or downloading from the server. blobState$?: (key: string) => Observable | null; + // Re-upload to the server. + upload?: (key: string) => Promise; } diff --git a/blocksuite/playground/apps/_common/sync/blob/mock-server.ts b/blocksuite/playground/apps/_common/sync/blob/mock-server.ts index a9600522fc..05590f3d74 100644 --- a/blocksuite/playground/apps/_common/sync/blob/mock-server.ts +++ b/blocksuite/playground/apps/_common/sync/blob/mock-server.ts @@ -112,7 +112,13 @@ export class MockServerBlobSource implements BlobSource { } function defaultState(): BlobState { - return { uploading: false, downloading: false, overSize: false }; + return { + uploading: false, + downloading: false, + overSize: false, + needDownload: false, + needUpload: false, + }; } function nextState( diff --git a/packages/frontend/core/src/modules/workspace/entities/workspace.ts b/packages/frontend/core/src/modules/workspace/entities/workspace.ts index 7982bc1408..535735d3ea 100644 --- a/packages/frontend/core/src/modules/workspace/entities/workspace.ts +++ b/packages/frontend/core/src/modules/workspace/entities/workspace.ts @@ -58,6 +58,7 @@ export class Workspace extends Entity { }, /* eslint-disable rxjs/finnish */ blobState$: key => this.engine.blob.blobState$(key), + upload: key => this.engine.blob.upload(key), name: 'blob', readonly: false, },