feat(editor): replace slot with rxjs subject (#10768)

This commit is contained in:
Mirone
2025-03-12 11:29:24 +09:00
committed by GitHub
parent 19f978d9aa
commit cd63e0ed8b
302 changed files with 1405 additions and 1251 deletions
@@ -441,7 +441,7 @@ const CREATE_AS_DOC = {
newDoc.addBlock('affine:surface', {}, rootId);
const noteId = newDoc.addBlock('affine:note', {}, rootId);
host.std.getOptional(RefNodeSlotsProvider)?.docLinkClicked.emit({
host.std.getOptional(RefNodeSlotsProvider)?.docLinkClicked.next({
pageId: newDoc.id,
host,
});
@@ -341,7 +341,7 @@ const OthersAIGroup: AIItemGroupConfig = {
icon: CommentIcon(),
handler: host => {
const panel = getAIPanelWidget(host);
AIProvider.slots.requestOpenWithChat.emit({
AIProvider.slots.requestOpenWithChat.next({
host,
autoSelect: true,
});
@@ -565,7 +565,7 @@ export function actionToResponse<T extends keyof BlockSuitePresets.AIActions>(
handler: () => {
reportResponse('result:continue-in-chat');
const panel = getAIPanelWidget(host);
AIProvider.slots.requestOpenWithChat.emit({ host });
AIProvider.slots.requestOpenWithChat.next({ host });
panel.hide();
},
},
@@ -604,11 +604,11 @@ export function actionToErrorResponse<
): ErrorConfig {
return {
upgrade: () => {
AIProvider.slots.requestUpgradePlan.emit({ host: panel.host });
AIProvider.slots.requestUpgradePlan.next({ host: panel.host });
panel.hide();
},
login: () => {
AIProvider.slots.requestLogin.emit({ host: panel.host });
AIProvider.slots.requestLogin.next({ host: panel.host });
panel.hide();
},
cancel: () => {
@@ -193,7 +193,7 @@ function buildPageResponseConfig<T extends keyof BlockSuitePresets.AIActions>(
icon: ChatWithAiIcon(),
handler: () => {
reportResponse('result:continue-in-chat');
AIProvider.slots.requestOpenWithChat.emit({ host });
AIProvider.slots.requestOpenWithChat.next({ host });
panel.hide();
},
},
@@ -258,11 +258,11 @@ export function buildFinishConfig<T extends keyof BlockSuitePresets.AIActions>(
export function buildErrorConfig(panel: AffineAIPanelWidget) {
return {
upgrade: () => {
AIProvider.slots.requestUpgradePlan.emit({ host: panel.host });
AIProvider.slots.requestUpgradePlan.next({ host: panel.host });
panel.hide();
},
login: () => {
AIProvider.slots.requestLogin.emit({ host: panel.host });
AIProvider.slots.requestLogin.next({ host: panel.host });
panel.hide();
},
cancel: () => {
@@ -273,12 +273,16 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
super.connectedCallback();
this._disposables.add(
AIProvider.slots.requestSendWithChat.on(
async ({ input, context, host }) => {
AIProvider.slots.requestSendWithChat.subscribe(
({ input, context, host }) => {
if (this.host === host) {
context && this.updateContext(context);
await this.updateComplete;
await this.send(input);
const { updateComplete, send } = this;
updateComplete
.then(() => {
return send(input);
})
.catch(console.error);
}
}
)
@@ -316,7 +316,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
.catch(console.error);
disposables.add(
AIProvider.slots.userInfo.on(userInfo => {
AIProvider.slots.userInfo.subscribe(userInfo => {
const { status, error } = this.chatContextValue;
this.avatarUrl = userInfo?.avatarUrl ?? '';
if (
@@ -329,7 +329,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
})
);
disposables.add(
this.host.selection.slots.changed.on(() => {
this.host.selection.slots.changed.subscribe(() => {
this._selectionValue = this.host.selection.value;
})
);
@@ -54,7 +54,7 @@ export class ChatPanelDocChip extends SignalWatcher(
const doc = this.docDisplayConfig.getDoc(this.chip.docId);
if (doc) {
this.disposables.add(
doc.slots.blockUpdated.on(
doc.slots.blockUpdated.subscribe(
throttle(this.autoUpdateChip, EXTRACT_DOC_THROTTLE)
)
);
@@ -392,7 +392,7 @@ export class ChatPanel extends SignalWatcher(
if (!this.doc) throw new Error('doc is required');
this._disposables.add(
AIProvider.slots.actions.on(({ event }) => {
AIProvider.slots.actions.subscribe(({ event }) => {
const { status } = this.chatContextValue;
if (
event === 'finished' &&
@@ -403,16 +403,19 @@ export class ChatPanel extends SignalWatcher(
})
);
this._disposables.add(
AIProvider.slots.userInfo.on(async () => {
await this._initPanel();
AIProvider.slots.userInfo.subscribe(() => {
this._initPanel().catch(console.error);
})
);
this._disposables.add(
AIProvider.slots.requestOpenWithChat.on(async ({ host }) => {
AIProvider.slots.requestOpenWithChat.subscribe(({ host }) => {
if (this.host === host) {
const context = await extractSelectedContent(host);
if (!context) return;
this.updateContext(context);
extractSelectedContent(host)
.then(context => {
if (!context) return;
this.updateContext(context);
})
.catch(console.error);
}
})
);
@@ -18,7 +18,7 @@ export const AIPreloadConfig = [
icon: LanguageIcon(),
text: 'Read a foreign language article with AI',
handler: () => {
AIProvider.slots.requestInsertTemplate.emit({
AIProvider.slots.requestInsertTemplate.next({
template: readAforeign,
mode: 'edgeless',
});
@@ -28,7 +28,7 @@ export const AIPreloadConfig = [
icon: MindmapIcon(),
text: 'Tidy an article with AI MindMap Action',
handler: () => {
AIProvider.slots.requestInsertTemplate.emit({
AIProvider.slots.requestInsertTemplate.next({
template: TidyMindMapV3,
mode: 'edgeless',
});
@@ -38,7 +38,7 @@ export const AIPreloadConfig = [
icon: ImageIcon(),
text: 'Add illustrations to the article',
handler: () => {
AIProvider.slots.requestInsertTemplate.emit({
AIProvider.slots.requestInsertTemplate.next({
template: redHat,
mode: 'edgeless',
});
@@ -48,7 +48,7 @@ export const AIPreloadConfig = [
icon: PenIcon(),
text: 'Complete writing with AI',
handler: () => {
AIProvider.slots.requestInsertTemplate.emit({
AIProvider.slots.requestInsertTemplate.next({
template: completeWritingWithAI,
mode: 'edgeless',
});
@@ -58,7 +58,7 @@ export const AIPreloadConfig = [
icon: SendIcon(),
text: 'Freely communicate with AI',
handler: () => {
AIProvider.slots.requestInsertTemplate.emit({
AIProvider.slots.requestInsertTemplate.next({
template: freelyCommunicateWithAI,
mode: 'edgeless',
});
@@ -75,7 +75,7 @@ export class AskAIToolbarButton extends WithDisposable(LitElement) {
aiPanel.hide();
extractSelectedContent(this.host)
.then(context => {
AIProvider.slots.requestSendWithChat.emit({
AIProvider.slots.requestSendWithChat.next({
input,
context,
host: this.host,
@@ -107,7 +107,7 @@ const othersGroup: AIItemGroupConfig = {
showWhen: () => true,
handler: host => {
const panel = getAIPanelWidget(host);
AIProvider.slots.requestOpenWithChat.emit({
AIProvider.slots.requestOpenWithChat.next({
host,
mode: 'edgeless',
autoSelect: true,
@@ -50,7 +50,7 @@ export function setupEdgelessElementToolbarAIEntry(
aiPanel.hide();
extractSelectedContent(edgeless.host)
.then(context => {
AIProvider.slots.requestSendWithChat.emit({
AIProvider.slots.requestSendWithChat.next({
input,
context,
host: edgeless.host,
@@ -13,7 +13,7 @@ class AICodeBlockWatcher extends LifeCycleWatcher {
override mounted() {
super.mounted();
const { view } = this.std;
view.viewUpdated.on(payload => {
view.viewUpdated.subscribe(payload => {
if (payload.type !== 'widget' || payload.method !== 'add') {
return;
}
@@ -52,7 +52,7 @@ function getAIEdgelessRootWatcher(framework: FrameworkProvider) {
override mounted() {
super.mounted();
const { view } = this.std;
view.viewUpdated.on(payload => {
view.viewUpdated.subscribe(payload => {
if (payload.type !== 'widget' || payload.method !== 'add') {
return;
}
@@ -11,7 +11,7 @@ class AIImageBlockWatcher extends LifeCycleWatcher {
override mounted() {
super.mounted();
const { view } = this.std;
view.viewUpdated.on(payload => {
view.viewUpdated.subscribe(payload => {
if (payload.type !== 'widget' || payload.method !== 'add') {
return;
}
@@ -23,7 +23,7 @@ function getAIPageRootWatcher(framework: FrameworkProvider) {
override mounted() {
super.mounted();
const { view } = this.std;
view.viewUpdated.on(payload => {
view.viewUpdated.subscribe(payload => {
if (payload.type !== 'widget' || payload.method !== 'add') {
return;
}
@@ -188,7 +188,7 @@ const PaymentRequiredErrorRenderer = (host: EditorHost) => html`
<ai-error-wrapper
.text=${"You've reached the current usage cap for AFFiNE AI. You can subscribe to AFFiNE AI(with free 7-day-trial) to continue the AI experience!"}
.actionText=${'Upgrade'}
.onClick=${() => AIProvider.slots.requestUpgradePlan.emit({ host })}
.onClick=${() => AIProvider.slots.requestUpgradePlan.next({ host })}
></ai-error-wrapper>
`;
@@ -196,7 +196,7 @@ const LoginRequiredErrorRenderer = (host: EditorHost) => html`
<ai-error-wrapper
.text=${'You need to login to AFFiNE Cloud to continue using AFFiNE AI.'}
.actionText=${'Login'}
.onClick=${() => AIProvider.slots.requestLogin.emit({ host })}
.onClick=${() => AIProvider.slots.requestLogin.next({ host })}
></ai-error-wrapper>
`;
@@ -1,14 +1,15 @@
import { BlockService } from '@blocksuite/affine/block-std';
import { Slot } from '@blocksuite/affine/global/slot';
import { RootBlockSchema } from '@blocksuite/affine/model';
import { Subject } from 'rxjs';
export class MindmapService extends BlockService {
static override readonly flavour = RootBlockSchema.model.flavour;
requestCenter = new Slot();
// eslint-disable-next-line rxjs/finnish
requestCenter = new Subject<void>();
center() {
this.requestCenter.emit();
this.requestCenter.next();
}
override mounted(): void {}
@@ -65,7 +65,7 @@ export class MindmapSurfaceBlock extends BlockComponent<SurfaceBlockModel> {
private _setupCenterEffect() {
this._disposables.add(
this.mindmapService.requestCenter.on(() => {
this.mindmapService.requestCenter.subscribe(() => {
let bound: Bound;
this.model.elementModels.forEach(el => {
@@ -85,7 +85,7 @@ export class MindmapSurfaceBlock extends BlockComponent<SurfaceBlockModel> {
private _setupRenderer() {
this._disposables.add(
this.model.elementUpdated.on(() => {
this.model.elementUpdated.subscribe(() => {
this.mindmapService.center();
})
);
@@ -1,6 +1,6 @@
import type { EditorHost } from '@blocksuite/affine/block-std';
import { Slot } from '@blocksuite/affine/global/slot';
import { captureException } from '@sentry/react';
import { Subject } from 'rxjs';
import type { ChatContextValue } from '../chat-panel/chat-context';
import {
@@ -123,22 +123,24 @@ export class AIProvider {
private readonly slots = {
// use case: when user selects "continue in chat" in an ask ai result panel
// do we need to pass the context to the chat panel?
requestOpenWithChat: new Slot<AIChatParams>(),
requestSendWithChat: new Slot<AISendParams>(),
requestInsertTemplate: new Slot<{
/* eslint-disable rxjs/finnish */
requestOpenWithChat: new Subject<AIChatParams>(),
requestSendWithChat: new Subject<AISendParams>(),
requestInsertTemplate: new Subject<{
template: string;
mode: 'page' | 'edgeless';
}>(),
requestLogin: new Slot<{ host: EditorHost }>(),
requestUpgradePlan: new Slot<{ host: EditorHost }>(),
requestLogin: new Subject<{ host: EditorHost }>(),
requestUpgradePlan: new Subject<{ host: EditorHost }>(),
// stream of AI actions triggered by users
actions: new Slot<{
actions: new Subject<{
action: keyof BlockSuitePresets.AIActions;
options: BlockSuitePresets.AITextActionOptions;
event: ActionEventType;
}>(),
// downstream can emit this slot to notify ai presets that user info has been updated
userInfo: new Slot<AIUserInfo | null>(),
userInfo: new Subject<AIUserInfo | null>(),
/* eslint-enable rxjs/finnish */
};
// track the history of triggered actions (in memory only)
@@ -162,7 +164,7 @@ export class AIProvider {
) => {
const options = args[0];
const slots = this.slots;
slots.actions.emit({
slots.actions.next({
action: id,
options,
event: 'started',
@@ -186,31 +188,31 @@ export class AIProvider {
try {
user = await AIProvider.userInfo;
yield* result;
slots.actions.emit({
slots.actions.next({
action: id,
options,
event: 'finished',
});
} catch (err) {
slots.actions.emit({
slots.actions.next({
action: id,
options,
event: 'error',
});
if (err instanceof PaymentRequiredError) {
slots.actions.emit({
slots.actions.next({
action: id,
options,
event: 'aborted:paywall',
});
} else if (err instanceof UnauthorizedError) {
slots.actions.emit({
slots.actions.next({
action: id,
options,
event: 'aborted:login-required',
});
} else {
slots.actions.emit({
slots.actions.next({
action: id,
options,
event: 'aborted:server-error',
@@ -232,7 +234,7 @@ export class AIProvider {
return result
.then(async result => {
user = await AIProvider.userInfo;
slots.actions.emit({
slots.actions.next({
action: id,
options,
event: 'finished',
@@ -240,13 +242,13 @@ export class AIProvider {
return result;
})
.catch(err => {
slots.actions.emit({
slots.actions.next({
action: id,
options,
event: 'error',
});
if (err instanceof PaymentRequiredError) {
slots.actions.emit({
slots.actions.next({
action: id,
options,
event: 'aborted:paywall',
@@ -527,13 +527,15 @@ Could you make a new website based on these notes and send back just the html fi
return client.forkSession(options);
});
const disposeRequestLoginHandler = AIProvider.slots.requestLogin.on(() => {
globalDialogService.open('sign-in', {});
});
const disposeRequestLoginHandler = AIProvider.slots.requestLogin.subscribe(
() => {
globalDialogService.open('sign-in', {});
}
);
setupTracker();
return () => {
disposeRequestLoginHandler.dispose();
disposeRequestLoginHandler.unsubscribe();
};
}
@@ -3,6 +3,7 @@ import type { EditorHost } from '@blocksuite/affine/block-std';
import type { GfxPrimitiveElementModel } from '@blocksuite/affine/block-std/gfx';
import type { BlockModel } from '@blocksuite/affine/store';
import { lowerCase, omit } from 'lodash-es';
import type { Subject } from 'rxjs';
import { AIProvider } from './ai-provider';
@@ -60,9 +61,9 @@ type AIActionEventProperties = {
workspaceId: string;
};
type BlocksuiteActionEvent = Parameters<
Parameters<typeof AIProvider.slots.actions.on>[0]
>[0];
type SubjectValue<T> = T extends Subject<infer U> ? U : never;
type BlocksuiteActionEvent = SubjectValue<typeof AIProvider.slots.actions>;
const trackAction = ({
eventName,
@@ -252,15 +253,15 @@ const toTrackedOptions = (
};
export function setupTracker() {
AIProvider.slots.requestUpgradePlan.on(() => {
AIProvider.slots.requestUpgradePlan.subscribe(() => {
track.$.paywall.aiAction.viewPlans();
});
AIProvider.slots.requestLogin.on(() => {
AIProvider.slots.requestLogin.subscribe(() => {
track.doc.editor.aiActions.requestSignIn();
});
AIProvider.slots.actions.on(event => {
AIProvider.slots.actions.subscribe(event => {
const properties = toTrackedOptions(event);
if (properties) {
trackAction(properties);
@@ -10,7 +10,7 @@ import {
Bound,
getCommonBoundWithRotation,
} from '@blocksuite/affine/global/gfx';
import { Slot } from '@blocksuite/affine/global/slot';
import { Subject } from 'rxjs';
import {
AFFINE_AI_PANEL_WIDGET,
@@ -22,7 +22,8 @@ export class CopilotTool extends BaseTool {
private _dragging = false;
draggingAreaUpdated = new Slot<boolean | void>();
// eslint-disable-next-line rxjs/finnish
draggingAreaUpdated = new Subject<boolean | void>();
dragLastPoint: [number, number] = [0, 0];
@@ -85,7 +86,7 @@ export class CopilotTool extends BaseTool {
if (!this._dragging) return;
this._dragging = false;
this.draggingAreaUpdated.emit(true);
this.draggingAreaUpdated.next(true);
}
override dragMove(e: PointerEventState): void {
@@ -108,7 +109,7 @@ export class CopilotTool extends BaseTool {
});
}
this.draggingAreaUpdated.emit();
this.draggingAreaUpdated.next();
}
override dragStart(e: PointerEventState): void {
@@ -116,7 +117,7 @@ export class CopilotTool extends BaseTool {
this._initDragState(e);
this._dragging = true;
this.draggingAreaUpdated.emit();
this.draggingAreaUpdated.next();
}
override mounted(): void {
@@ -167,7 +168,7 @@ export class CopilotTool extends BaseTool {
inoperable: true,
});
this.draggingAreaUpdated.emit(true);
this.draggingAreaUpdated.next(true);
}
}
@@ -4,7 +4,7 @@ export function reportResponse(event: ActionEventType) {
const lastAction = AIProvider.actionHistory.at(-1);
if (!lastAction) return;
AIProvider.slots.actions.emit({
AIProvider.slots.actions.next({
action: lastAction.action,
options: lastAction.options,
event,
@@ -200,7 +200,7 @@ export class EdgelessCopilotWidget extends WidgetComponent<RootBlockModel> {
const CopilotSelectionTool = this.gfx.tool.get('copilot');
this._disposables.add(
CopilotSelectionTool.draggingAreaUpdated.on(shouldShowPanel => {
CopilotSelectionTool.draggingAreaUpdated.subscribe(shouldShowPanel => {
this._visible = true;
this._updateSelection(CopilotSelectionTool.area);
if (shouldShowPanel) {
@@ -213,7 +213,7 @@ export class EdgelessCopilotWidget extends WidgetComponent<RootBlockModel> {
);
this._disposables.add(
this.gfx.viewport.viewportUpdated.on(() => {
this.gfx.viewport.viewportUpdated.subscribe(() => {
if (!this._visible) return;
this._updateSelection(CopilotSelectionTool.area);
@@ -258,7 +258,7 @@ export class EdgelessCopilotWidget extends WidgetComponent<RootBlockModel> {
lockToolbar(disabled: boolean) {
const legacySlot = this.std.get(EdgelessLegacySlotIdentifier);
legacySlot.toolbarLocked.emit(disabled);
legacySlot.toolbarLocked.next(disabled);
}
override render() {
@@ -9,7 +9,7 @@ import {
customImageProxyMiddleware,
ImageProxyService,
} from '@blocksuite/affine/blocks/image';
import { DisposableGroup } from '@blocksuite/affine/global/slot';
import { DisposableGroup } from '@blocksuite/affine/global/disposable';
import type { DocMode } from '@blocksuite/affine/model';
import { LinkPreviewerService } from '@blocksuite/affine/shared/services';
import type { Store } from '@blocksuite/affine/store';
@@ -49,13 +49,14 @@ const BlockSuiteEditorImpl = ({
defaultOpenProperty,
}: EditorProps) => {
useEffect(() => {
const disposable = page.slots.blockUpdated.once(() => {
const disposable = page.slots.blockUpdated.subscribe(() => {
disposable.unsubscribe();
page.workspace.meta.setDocMeta(page.id, {
updatedDate: Date.now(),
});
});
return () => {
disposable.dispose();
disposable.unsubscribe();
};
}, [page]);
@@ -154,15 +155,16 @@ export const BlockSuiteEditor = (props: EditorProps) => {
return;
}
const timer = setTimeout(() => {
disposable.dispose();
disposable.unsubscribe();
setError(new NoPageRootError(props.page));
}, 20 * 1000);
const disposable = props.page.slots.rootAdded.once(() => {
const disposable = props.page.slots.rootAdded.subscribe(() => {
disposable.unsubscribe();
setIsLoading(false);
clearTimeout(timer);
});
return () => {
disposable.dispose();
disposable.unsubscribe();
clearTimeout(timer);
};
}, [props.page]);
@@ -104,8 +104,11 @@ const StarterBarNotEmpty = ({ doc }: { doc: Store }) => {
const { id, created } = rootComponent.focusFirstParagraph();
if (created) {
std.view.viewUpdated.once(v => {
if (v.id === id) handleInlineAskAIAction(std.host, pageAIGroups);
const subscription = std.view.viewUpdated.subscribe(v => {
if (v.id === id) {
subscription.unsubscribe();
handleInlineAskAIAction(std.host, pageAIGroups);
}
});
} else {
handleInlineAskAIAction(std.host, pageAIGroups);
@@ -47,7 +47,7 @@ export class EdgelessEditor extends SignalWatcher(
override connectedCallback() {
super.connectedCallback();
this._disposables.add(
this.doc.slots.rootAdded.on(() => this.requestUpdate())
this.doc.slots.rootAdded.subscribe(() => this.requestUpdate())
);
this.std = new BlockStdScope({
store: this.doc,
@@ -59,7 +59,7 @@ export class PageEditor extends SignalWatcher(
override connectedCallback() {
super.connectedCallback();
this._disposables.add(
this.doc.slots.rootAdded.on(() => this.requestUpdate())
this.doc.slots.rootAdded.subscribe(() => this.requestUpdate())
);
this.std = new BlockStdScope({
store: this.doc,
@@ -44,9 +44,7 @@ export function patchDocModeService(
? docsService.list.primaryMode$(id)
: docService.doc.primaryMode$;
const sub = mode$.subscribe(m => handler((m || DEFAULT_MODE) as DocMode));
return {
dispose: sub.unsubscribe,
};
return sub;
};
}
@@ -9,7 +9,7 @@ export class EdgelessClipboardWatcher extends LifeCycleWatcher {
override mounted() {
super.mounted();
const { view } = this.std;
view.viewUpdated.on(payload => {
view.viewUpdated.subscribe(payload => {
if (payload.type !== 'block' || payload.method !== 'add') {
return;
}
@@ -14,7 +14,7 @@ import type {
Container,
ServiceIdentifier,
} from '@blocksuite/affine/global/di';
import { DisposableGroup } from '@blocksuite/affine/global/slot';
import { DisposableGroup } from '@blocksuite/affine/global/disposable';
import {
type ReferenceNodeConfig,
ReferenceNodeConfigIdentifier,
@@ -57,7 +57,7 @@ const EdgelessNoteToggleButton = ({ note }: { note: NoteBlockModel }) => {
const { selection } = gfx;
const dispose = selection.slots.updated.on(() => {
const dispose = selection.slots.updated.subscribe(() => {
if (selection.has(note.id) && selection.editing) {
note.doc.transact(() => {
note.edgeless.collapse = false;
@@ -65,7 +65,7 @@ const EdgelessNoteToggleButton = ({ note }: { note: NoteBlockModel }) => {
}
});
return () => dispose.dispose();
return () => dispose.unsubscribe();
}, [gfx, note]);
const toggle = useCallback(() => {