mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-28 15:55:19 +08:00
5590cdd8f1
Closes: [BS-3564](https://linear.app/affine-design/issue/BS-3564/ui-embed-view-报错-ui-加-title) Closes: [BS-3454](https://linear.app/affine-design/issue/BS-3454/点击-reload-后应该隐藏-attachment-embed-view-左下角-status(待新状态)) <img width="807" alt="Screenshot 2025-05-28 at 17 23 26" src="https://github.com/user-attachments/assets/9ecc29f8-73c6-4441-bc38-dfe9bd876542" /> <img width="820" alt="Screenshot 2025-05-28 at 17 45 37" src="https://github.com/user-attachments/assets/68e6db17-a814-4df4-a9fa-067ca03dec30" /> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added support for retrying failed uploads of attachments and images, allowing users to re-upload files directly from the error status interface. - The error status dialog now dynamically displays "Retry" for upload failures and "Reload" for download failures, with appropriate actions for each. - **Enhancements** - Improved clarity and consistency in file type display and icon usage for attachments and citations. - Button labels in the attachment interface now have capitalized text for better readability. - **Bug Fixes** - Streamlined error handling and status updates for attachment and image uploads/downloads, reducing redundant UI elements. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
231 lines
5.1 KiB
TypeScript
231 lines
5.1 KiB
TypeScript
import type { Disposable } from '@blocksuite/global/disposable';
|
|
import type { BlobEngine, BlobState } from '@blocksuite/sync';
|
|
import {
|
|
computed,
|
|
effect,
|
|
type ReadonlySignal,
|
|
signal,
|
|
} from '@preact/signals-core';
|
|
import type { TemplateResult } from 'lit-html';
|
|
|
|
export type ResourceKind = 'Blob' | 'File' | 'Image';
|
|
|
|
export type StateKind =
|
|
| 'loading'
|
|
| 'uploading'
|
|
| 'error'
|
|
| 'error:oversize'
|
|
| 'none';
|
|
|
|
export type StateInfo = {
|
|
icon: TemplateResult;
|
|
title?: string;
|
|
description?: string | null;
|
|
};
|
|
|
|
export type ResolvedStateInfoPart = {
|
|
loading: boolean;
|
|
error: boolean;
|
|
state: StateKind;
|
|
url: string | null;
|
|
needUpload: boolean;
|
|
};
|
|
|
|
export type ResolvedStateInfo = StateInfo & ResolvedStateInfoPart;
|
|
|
|
export class ResourceController implements Disposable {
|
|
readonly blobUrl$ = signal<string | null>(null);
|
|
|
|
// TODO(@fundon): default `loading` status.
|
|
readonly state$ = signal<Partial<BlobState>>({});
|
|
|
|
readonly resolvedState$ = computed<ResolvedStateInfoPart>(() => {
|
|
const url = this.blobUrl$.value;
|
|
const {
|
|
needUpload = false,
|
|
uploading = false,
|
|
downloading = false,
|
|
overSize = false,
|
|
errorMessage,
|
|
} = this.state$.value;
|
|
const hasExceeded = overSize;
|
|
const hasError = hasExceeded || Boolean(errorMessage);
|
|
const state = this.determineState(
|
|
hasExceeded,
|
|
hasError,
|
|
uploading,
|
|
downloading
|
|
);
|
|
|
|
const loading = state === 'uploading' || state === 'loading';
|
|
|
|
return {
|
|
error: hasError,
|
|
needUpload,
|
|
loading,
|
|
state,
|
|
url,
|
|
};
|
|
});
|
|
|
|
private engine?: BlobEngine;
|
|
|
|
constructor(
|
|
readonly blobId$: ReadonlySignal<string | undefined>,
|
|
readonly kind: ResourceKind = 'File'
|
|
) {}
|
|
|
|
// This is a tradeoff, initializing `Blob Sync Engine`.
|
|
setEngine(engine: BlobEngine) {
|
|
this.engine = engine;
|
|
return this;
|
|
}
|
|
|
|
determineState(
|
|
hasExceeded: boolean,
|
|
hasError: boolean,
|
|
uploading: boolean,
|
|
downloading: boolean
|
|
): StateKind {
|
|
if (hasExceeded) return 'error:oversize';
|
|
if (hasError) return 'error';
|
|
if (uploading) return 'uploading';
|
|
if (downloading) return 'loading';
|
|
return 'none';
|
|
}
|
|
|
|
resolveStateWith(
|
|
info: {
|
|
loadingIcon: TemplateResult;
|
|
errorIcon?: TemplateResult;
|
|
} & StateInfo
|
|
): ResolvedStateInfo {
|
|
const { error, loading, state, url, needUpload } =
|
|
this.resolvedState$.value;
|
|
|
|
const { icon, title, description, loadingIcon, errorIcon } = info;
|
|
|
|
const result = {
|
|
error,
|
|
loading,
|
|
state,
|
|
icon,
|
|
title,
|
|
description,
|
|
url,
|
|
needUpload,
|
|
};
|
|
|
|
if (loading) {
|
|
result.icon = loadingIcon ?? icon;
|
|
} else if (error) {
|
|
result.icon = errorIcon ?? icon;
|
|
result.description = this.state$.value.errorMessage ?? description;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
updateState(state: Partial<BlobState>) {
|
|
this.state$.value = { ...this.state$.value, ...state };
|
|
}
|
|
|
|
subscribe() {
|
|
return effect(() => {
|
|
const blobId = this.blobId$.value;
|
|
if (!blobId) return;
|
|
|
|
const blobState$ = this.engine?.blobState$(blobId);
|
|
if (!blobState$) return;
|
|
|
|
const subscription = blobState$.subscribe(state => {
|
|
let { uploading, downloading, errorMessage } = state;
|
|
if (state.overSize) {
|
|
uploading = false;
|
|
downloading = false;
|
|
} else if ((uploading || downloading) && errorMessage) {
|
|
errorMessage = null;
|
|
}
|
|
|
|
this.updateState({ ...state, uploading, downloading, errorMessage });
|
|
});
|
|
|
|
return () => subscription.unsubscribe();
|
|
});
|
|
}
|
|
|
|
async blob() {
|
|
const blobId = this.blobId$.peek();
|
|
if (!blobId) return null;
|
|
|
|
let blob: Blob | null = null;
|
|
let errorMessage: string | null = null;
|
|
|
|
try {
|
|
if (!this.engine) {
|
|
throw new Error('Blob engine is not initialized');
|
|
}
|
|
|
|
blob = (await this.engine.get(blobId)) ?? null;
|
|
|
|
if (!blob) errorMessage = `${this.kind} not found`;
|
|
} catch (err) {
|
|
console.error(err);
|
|
errorMessage = `Failed to retrieve ${this.kind}`;
|
|
}
|
|
|
|
if (errorMessage) this.updateState({ errorMessage });
|
|
|
|
return blob;
|
|
}
|
|
|
|
async createUrlWith(type?: string) {
|
|
let blob = await this.blob();
|
|
if (!blob) return null;
|
|
|
|
if (type) blob = new Blob([blob], { type });
|
|
|
|
return URL.createObjectURL(blob);
|
|
}
|
|
|
|
async refreshUrlWith(type?: string) {
|
|
// Resets the state.
|
|
this.state$.value = {};
|
|
|
|
const url = await this.createUrlWith(type);
|
|
if (!url) return;
|
|
|
|
const prevUrl = this.blobUrl$.peek();
|
|
|
|
this.blobUrl$.value = url;
|
|
|
|
if (!prevUrl) return;
|
|
|
|
// Releases the previous url.
|
|
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;
|
|
|
|
// Releases the current url.
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
}
|