diff --git a/blocksuite/affine/components/src/virtual-keyboard/controller.ts b/blocksuite/affine/components/src/virtual-keyboard/controller.ts index 7e99c209e5..fa394c6f89 100644 --- a/blocksuite/affine/components/src/virtual-keyboard/controller.ts +++ b/blocksuite/affine/components/src/virtual-keyboard/controller.ts @@ -1,8 +1,11 @@ import { IS_IOS } from '@blocksuite/global/env'; +import type * as GlobalTypes from '@blocksuite/global/types'; import { DisposableGroup } from '@blocksuite/global/utils'; import { signal } from '@preact/signals-core'; import type { ReactiveController, ReactiveControllerHost } from 'lit'; +declare type _GLOBAL_ = typeof GlobalTypes; + function notSupportedWarning() { console.warn('VirtualKeyboard API and VisualViewport API are not supported'); } diff --git a/blocksuite/blocks/src/root-block/edgeless/edgeless-root-service.ts b/blocksuite/blocks/src/root-block/edgeless/edgeless-root-service.ts index a58dac6de2..0e09f2a868 100644 --- a/blocksuite/blocks/src/root-block/edgeless/edgeless-root-service.ts +++ b/blocksuite/blocks/src/root-block/edgeless/edgeless-root-service.ts @@ -154,7 +154,7 @@ export class EdgelessRootService extends RootService implements SurfaceContext { let readonly = doc.readonly; this.disposables.add( - doc.awarenessStore.slots.update.on(() => { + effect(() => { if (readonly !== doc.readonly) { readonly = doc.readonly; slots.readonlyUpdated.emit(readonly); diff --git a/blocksuite/framework/global/src/types/index.ts b/blocksuite/framework/global/src/types/index.ts index 2a095904fe..58765e8dce 100644 --- a/blocksuite/framework/global/src/types/index.ts +++ b/blocksuite/framework/global/src/types/index.ts @@ -1,4 +1 @@ -export interface BlockSuiteFlags { - readonly: Record; -} export * from './virtual-keyboard.js'; diff --git a/blocksuite/framework/store/shim.d.ts b/blocksuite/framework/store/shim.d.ts deleted file mode 100644 index c8c2ea92c2..0000000000 --- a/blocksuite/framework/store/shim.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -declare module 'y-protocols/awareness.js' { - import { Awareness as _Awareness } from 'y-protocols/awareness'; - type UnknownRecord = Record; - export class Awareness< - State extends UnknownRecord = UnknownRecord, - > extends _Awareness { - constructor( - doc: Y.Doc - ): Awareness; - - getLocalState(): State; - getStates(): Map; - setLocalState(state: State): void; - setLocalStateField( - field: Field, - value: State[Field] - ): void; - } - export { applyAwarenessUpdate, encodeAwarenessUpdate, modifyAwarenessUpdate, removeAwarenessStates } from 'y-protocols/awareness' -} diff --git a/blocksuite/framework/store/src/__tests__/doc.unit.spec.ts b/blocksuite/framework/store/src/__tests__/doc.unit.spec.ts index 0dec121105..5ae37b21da 100644 --- a/blocksuite/framework/store/src/__tests__/doc.unit.spec.ts +++ b/blocksuite/framework/store/src/__tests__/doc.unit.spec.ts @@ -259,7 +259,7 @@ test('local readonly', () => { expect(doc1.readonly).toBeTruthy(); expect(doc2?.readonly).toBeTruthy(); - expect(doc3?.readonly).toBeTruthy(); + expect(doc3?.readonly).toBeFalsy(); doc1.readonly = false; diff --git a/blocksuite/framework/store/src/index.ts b/blocksuite/framework/store/src/index.ts index 526a50bbef..26bd467296 100644 --- a/blocksuite/framework/store/src/index.ts +++ b/blocksuite/framework/store/src/index.ts @@ -1,6 +1,3 @@ -// oxlint-disable-next-line @typescript-eslint/triple-slash-reference -/// - export * from './adapter'; export * from './extension'; export * from './model'; diff --git a/blocksuite/framework/store/src/model/doc.ts b/blocksuite/framework/store/src/model/doc.ts index c40b60af67..382393fadf 100644 --- a/blocksuite/framework/store/src/model/doc.ts +++ b/blocksuite/framework/store/src/model/doc.ts @@ -54,7 +54,6 @@ export interface Doc { clearQuery(query: Query, readonly?: boolean): void; get loaded(): boolean; - get readonly(): boolean; get awarenessStore(): AwarenessStore; get workspace(): Workspace; diff --git a/blocksuite/framework/store/src/model/store/store.ts b/blocksuite/framework/store/src/model/store/store.ts index f71b883978..2f028aba92 100644 --- a/blocksuite/framework/store/src/model/store/store.ts +++ b/blocksuite/framework/store/src/model/store/store.ts @@ -47,7 +47,7 @@ export class Store { mode: 'loose', }; - protected _readonly?: boolean; + protected _readonly = signal(false); protected readonly _schema: Schema; @@ -175,10 +175,16 @@ export class Store { } get canRedo() { + if (this.readonly) { + return false; + } return this._doc.canRedo; } get canUndo() { + if (this.readonly) { + return false; + } return this._doc.canUndo; } @@ -214,18 +220,16 @@ export class Store { return this._doc.meta; } + get readonly$() { + return this._readonly; + } + get readonly() { - if (this._doc.readonly) { - return true; - } - return this._readonly === true; + return this._readonly.value === true; } set readonly(value: boolean) { - this._doc.awarenessStore.setReadonly(this._doc, value); - if (this._readonly !== undefined && this._readonly !== value) { - this._readonly = value; - } + this._readonly.value = value; } get ready() { @@ -233,6 +237,11 @@ export class Store { } get redo() { + if (this.readonly) { + return () => { + console.error('cannot undo in readonly mode'); + }; + } return this._doc.redo.bind(this._doc); } @@ -263,6 +272,11 @@ export class Store { } get undo() { + if (this.readonly) { + return () => { + console.error('cannot undo in readonly mode'); + }; + } return this._doc.undo.bind(this._doc); } @@ -300,7 +314,9 @@ export class Store { this._crud = new DocCRUD(this._yBlocks, doc.schema); this._schema = schema; - this._readonly = readonly; + if (readonly !== undefined) { + this._readonly.value = readonly; + } if (query) { this._query = query; } diff --git a/blocksuite/framework/store/src/test/test-doc.ts b/blocksuite/framework/store/src/test/test-doc.ts index b687dcb725..ffdb67bd77 100644 --- a/blocksuite/framework/store/src/test/test-doc.ts +++ b/blocksuite/framework/store/src/test/test-doc.ts @@ -1,4 +1,4 @@ -import { type Disposable, Slot } from '@blocksuite/global/utils'; +import { Slot } from '@blocksuite/global/utils'; import { signal } from '@preact/signals-core'; import * as Y from 'yjs'; @@ -17,8 +17,6 @@ type DocOptions = { }; export class TestDoc implements Doc { - private _awarenessUpdateDisposable: Disposable | null = null; - private readonly _canRedo$ = signal(false); private readonly _canUndo$ = signal(false); @@ -86,8 +84,8 @@ export class TestDoc implements Doc { private _shouldTransact = true; private readonly _updateCanUndoRedoSignals = () => { - const canRedo = this.readonly ? false : this._history.canRedo(); - const canUndo = this.readonly ? false : this._history.canUndo(); + const canRedo = this._history.canRedo(); + const canUndo = this._history.canUndo(); if (this._canRedo$.peek() !== canRedo) { this._canRedo$.value = canRedo; } @@ -164,10 +162,6 @@ export class TestDoc implements Doc { return this.workspace.meta.getDocMeta(this.id); } - get readonly(): boolean { - return this.awarenessStore.isReadonly(this); - } - get ready() { return this._ready; } @@ -272,7 +266,6 @@ export class TestDoc implements Doc { dispose() { this.slots.historyUpdated.dispose(); - this._awarenessUpdateDisposable?.dispose(); if (this.ready) { this._yBlocks.unobserveDeep(this._handleYEvents); @@ -322,13 +315,6 @@ export class TestDoc implements Doc { this._handleYBlockAdd(id); }); - this._awarenessUpdateDisposable = this.awarenessStore.slots.update.on( - () => { - // change readonly state will affect the undo/redo state - this._updateCanUndoRedoSignals(); - } - ); - initFn?.(); this._ready = true; @@ -337,18 +323,10 @@ export class TestDoc implements Doc { } redo() { - if (this.readonly) { - console.error('cannot modify data in readonly mode'); - return; - } this._history.redo(); } undo() { - if (this.readonly) { - console.error('cannot modify data in readonly mode'); - return; - } this._history.undo(); } diff --git a/blocksuite/framework/store/src/test/test-workspace.ts b/blocksuite/framework/store/src/test/test-workspace.ts index a9eab3dbef..4fc4ce57f4 100644 --- a/blocksuite/framework/store/src/test/test-workspace.ts +++ b/blocksuite/framework/store/src/test/test-workspace.ts @@ -1,5 +1,4 @@ import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions'; -import type { BlockSuiteFlags } from '@blocksuite/global/types'; import { NoopLogger, Slot } from '@blocksuite/global/utils'; import { AwarenessEngine, @@ -11,8 +10,6 @@ import { MemoryBlobSource, NoopDocSource, } from '@blocksuite/sync'; -import clonedeep from 'lodash.clonedeep'; -import merge from 'lodash.merge'; import { Awareness } from 'y-protocols/awareness.js'; import * as Y from 'yjs'; @@ -26,7 +23,7 @@ import type { } from '../model/index.js'; import type { Schema } from '../schema/index.js'; import { type IdGenerator, nanoid } from '../utils/id-generator.js'; -import { AwarenessStore, type RawAwarenessState } from '../yjs/index.js'; +import { AwarenessStore } from '../yjs/index.js'; import { TestDoc } from './test-doc.js'; import { TestMeta } from './test-meta.js'; @@ -34,7 +31,6 @@ export type DocCollectionOptions = { schema: Schema; id?: string; idGenerator?: IdGenerator; - defaultFlags?: Partial; docSources?: { main: DocSource; shadows?: DocSource[]; @@ -46,10 +42,6 @@ export type DocCollectionOptions = { awarenessSources?: AwarenessSource[]; }; -const FLAGS_PRESET = { - readonly: {}, -} satisfies BlockSuiteFlags; - /** * Test only * Do not use this in production @@ -95,7 +87,6 @@ export class TestWorkspace implements Workspace { id, schema, idGenerator, - defaultFlags, awarenessSources = [], docSources = { main: new NoopDocSource(), @@ -108,10 +99,7 @@ export class TestWorkspace implements Workspace { this.id = id || ''; this.doc = new Y.Doc({ guid: id }); - this.awarenessStore = new AwarenessStore( - new Awareness(this.doc), - merge(clonedeep(FLAGS_PRESET), defaultFlags) - ); + this.awarenessStore = new AwarenessStore(new Awareness(this.doc)); const logger = new NoopLogger(); diff --git a/blocksuite/framework/store/src/yjs/awareness.ts b/blocksuite/framework/store/src/yjs/awareness.ts index b1a721ca2b..2ea84fd661 100644 --- a/blocksuite/framework/store/src/yjs/awareness.ts +++ b/blocksuite/framework/store/src/yjs/awareness.ts @@ -1,11 +1,5 @@ -import type { BlockSuiteFlags } from '@blocksuite/global/types'; import { Slot } from '@blocksuite/global/utils'; -import { type Signal, signal } from '@preact/signals-core'; -import clonedeep from 'lodash.clonedeep'; -import merge from 'lodash.merge'; -import type { Awareness as YAwareness } from 'y-protocols/awareness.js'; - -import type { Doc } from '../model/doc.js'; +import type { Awareness } from 'y-protocols/awareness.js'; export interface UserInfo { name: string; @@ -17,7 +11,6 @@ type UserSelection = Array>; export type RawAwarenessState = { user?: UserInfo; color?: string; - flags: BlockSuiteFlags; // use v2 to avoid crush on old clients selectionV2: Record; }; @@ -29,18 +22,14 @@ export interface AwarenessEvent { } export class AwarenessStore { - private readonly _flags: Signal; - private readonly _onAwarenessChange = (diff: { added: number[]; removed: number[]; updated: number[]; }) => { - this._flags.value = this.awareness.getLocalState()?.flags ?? {}; - const { added, removed, updated } = diff; - const states = this.awareness.getStates(); + const states = this.getStates(); added.forEach(id => { this.slots.update.emit({ id, @@ -63,30 +52,16 @@ export class AwarenessStore { }); }; - readonly awareness: YAwareness; + readonly awareness: Awareness; readonly slots = { update: new Slot(), }; - constructor( - awareness: YAwareness, - defaultFlags: BlockSuiteFlags - ) { - this._flags = signal(defaultFlags); + constructor(awareness: Awareness) { this.awareness = awareness; this.awareness.on('change', this._onAwarenessChange); this.awareness.setLocalStateField('selectionV2', {}); - this._initFlags(defaultFlags); - } - - private _initFlags(defaultFlags: BlockSuiteFlags) { - const upstreamFlags = this.awareness.getLocalState()?.flags; - const flags = clonedeep(defaultFlags); - if (upstreamFlags) { - merge(flags, upstreamFlags); - } - this.awareness.setLocalStateField('flags', flags); } destroy() { @@ -95,10 +70,6 @@ export class AwarenessStore { this.awareness.destroy(); } - getFlag(field: Key) { - return this._flags.value[field]; - } - getLocalSelection( selectionManagerId: string ): ReadonlyArray> { @@ -109,24 +80,22 @@ export class AwarenessStore { } getStates(): Map { - return this.awareness.getStates(); + return this.awareness.getStates() as Map; } - isReadonly(blockCollection: Doc): boolean { - const rd = this.getFlag('readonly'); - if (rd && typeof rd === 'object') { - return Boolean((rd as Record)[blockCollection.id]); - } else { - return false; - } + getLocalState(): RawAwarenessState { + return this.awareness.getLocalState() as RawAwarenessState; } - setFlag( - field: Key, - value: BlockSuiteFlags[Key] - ) { - const oldFlags = this.awareness.getLocalState()?.flags ?? {}; - this.awareness.setLocalStateField('flags', { ...oldFlags, [field]: value }); + setLocalState(state: RawAwarenessState): void { + this.awareness.setLocalState(state); + } + + setLocalStateField( + field: Field, + value: RawAwarenessState[Field] + ): void { + this.awareness.setLocalStateField(field, value); } setLocalSelection(selectionManagerId: string, selection: UserSelection) { @@ -136,12 +105,4 @@ export class AwarenessStore { [selectionManagerId]: selection, }); } - - setReadonly(blockCollection: Doc, value: boolean): void { - const flags = this.getFlag('readonly') ?? {}; - this.setFlag('readonly', { - ...flags, - [blockCollection.id]: value, - } as BlockSuiteFlags['readonly']); - } } diff --git a/blocksuite/presets/src/fragments/doc-title/doc-title.ts b/blocksuite/presets/src/fragments/doc-title/doc-title.ts index 68d0f85905..b9223a3165 100644 --- a/blocksuite/presets/src/fragments/doc-title/doc-title.ts +++ b/blocksuite/presets/src/fragments/doc-title/doc-title.ts @@ -3,6 +3,7 @@ import { ShadowlessElement } from '@blocksuite/block-std'; import type { RichText, RootBlockModel } from '@blocksuite/blocks'; import { assertExists, WithDisposable } from '@blocksuite/global/utils'; import type { Store } from '@blocksuite/store'; +import { effect } from '@preact/signals-core'; import { css, html } from 'lit'; import { property, query, state } from 'lit/decorators.js'; @@ -111,7 +112,7 @@ export class DocTitle extends WithDisposable(ShadowlessElement) { this._isReadonly = this.doc.readonly; this._disposables.add( - this.doc.awarenessStore.slots.update.on(() => { + effect(() => { if (this._isReadonly !== this.doc.readonly) { this._isReadonly = this.doc.readonly; this.requestUpdate(); diff --git a/blocksuite/tests-legacy/edgeless/basic.spec.ts b/blocksuite/tests-legacy/edgeless/basic.spec.ts index 7c17f081bf..671fca22c0 100644 --- a/blocksuite/tests-legacy/edgeless/basic.spec.ts +++ b/blocksuite/tests-legacy/edgeless/basic.spec.ts @@ -19,7 +19,6 @@ import { Shape, shiftClickView, switchEditorMode, - toggleEditorReadonly, ZOOM_BAR_RESPONSIVE_SCREEN_WIDTH, zoomByMouseWheel, zoomResetByKeyboard, @@ -33,6 +32,7 @@ import { focusRichText, initEmptyEdgelessState, redoByClick, + switchReadonly, type, undoByClick, waitNextFrame, @@ -203,7 +203,7 @@ test('zoom by pinch when edgeless is readonly', async ({ page }) => { await zoomResetByKeyboard(page); await assertZoomLevel(page, 100); - await toggleEditorReadonly(page); + await switchReadonly(page); const from = [ { x: CENTER_X - 100, y: CENTER_Y }, @@ -217,7 +217,8 @@ test('zoom by pinch when edgeless is readonly', async ({ page }) => { await multiTouchMove(page, from, to); await multiTouchUp(page, to); - await toggleEditorReadonly(page); + await switchReadonly(page, false); + await waitNextFrame(page); await assertZoomLevel(page, 50); }); diff --git a/blocksuite/tests-legacy/utils/actions/click.ts b/blocksuite/tests-legacy/utils/actions/click.ts index dc813e80f8..101508d7db 100644 --- a/blocksuite/tests-legacy/utils/actions/click.ts +++ b/blocksuite/tests-legacy/utils/actions/click.ts @@ -1,5 +1,6 @@ import type { IPoint } from '@blocksuite/global/utils'; import type { Page } from '@playwright/test'; +import type { Store } from '@store/index.js'; import { toViewCoord } from './edgeless.js'; import { waitNextFrame } from './misc.js'; @@ -110,14 +111,12 @@ export async function clickTestOperationsMenuItem(page: Page, name: string) { export async function switchReadonly(page: Page, value = true) { await page.evaluate(_value => { const defaultPage = document.querySelector( - 'affine-page-root' + 'affine-page-root,affine-edgeless-root' ) as HTMLElement & { - doc: { - awarenessStore: { setFlag: (key: string, value: unknown) => void }; - }; + doc: Store; }; const doc = defaultPage.doc; - doc.awarenessStore.setFlag('readonly', { 'doc:home': _value }); + doc.readonly = _value; }, value); } diff --git a/blocksuite/tests-legacy/utils/actions/misc.ts b/blocksuite/tests-legacy/utils/actions/misc.ts index e33d0b7bd1..e849449170 100644 --- a/blocksuite/tests-legacy/utils/actions/misc.ts +++ b/blocksuite/tests-legacy/utils/actions/misc.ts @@ -85,9 +85,6 @@ async function initEmptyEditor({ await new Promise(resolve => doc.slots.rootAdded.once(resolve)); } - for (const [key, value] of Object.entries(flags)) { - doc.awarenessStore.setFlag(key as keyof typeof flags, value); - } // add app root from https://github.com/toeverything/blocksuite/commit/947201981daa64c5ceeca5fd549460c34e2dabfa const appRoot = document.querySelector('#app'); if (!appRoot) { @@ -95,6 +92,11 @@ async function initEmptyEditor({ } const createEditor = () => { const editor = document.createElement('affine-editor-container'); + for (const [key, value] of Object.entries(flags)) { + doc + .get(window.$blocksuite.blocks.FeatureFlagService) + .setFlag(key, value); + } doc .get(window.$blocksuite.blocks.FeatureFlagService) .setFlag('enable_advanced_block_visibility', true); diff --git a/packages/frontend/core/src/modules/workspace/impls/doc.ts b/packages/frontend/core/src/modules/workspace/impls/doc.ts index a906774df5..1316309f6f 100644 --- a/packages/frontend/core/src/modules/workspace/impls/doc.ts +++ b/packages/frontend/core/src/modules/workspace/impls/doc.ts @@ -1,5 +1,5 @@ import { SpecProvider } from '@blocksuite/affine/blocks'; -import { type Disposable, Slot } from '@blocksuite/affine/global/utils'; +import { Slot } from '@blocksuite/affine/global/utils'; import { type AwarenessStore, type Doc, @@ -20,8 +20,6 @@ type DocOptions = { }; export class DocImpl implements Doc { - private _awarenessUpdateDisposable: Disposable | null = null; - private readonly _canRedo = signal(false); private readonly _canUndo = signal(false); @@ -89,8 +87,8 @@ export class DocImpl implements Doc { private _shouldTransact = true; private readonly _updateCanUndoRedoSignals = () => { - const canRedo = this.readonly ? false : this._history.canRedo(); - const canUndo = this.readonly ? false : this._history.canUndo(); + const canRedo = this._history.canRedo(); + const canUndo = this._history.canUndo(); if (this._canRedo.peek() !== canRedo) { this._canRedo.value = canRedo; } @@ -159,10 +157,6 @@ export class DocImpl implements Doc { return this.workspace.meta.getDocMeta(this.id); } - get readonly(): boolean { - return this.awarenessStore.isReadonly(this); - } - get ready() { return this._ready; } @@ -267,7 +261,6 @@ export class DocImpl implements Doc { dispose() { this.slots.historyUpdated.dispose(); - this._awarenessUpdateDisposable?.dispose(); if (this.ready) { this._yBlocks.unobserveDeep(this._handleYEvents); @@ -320,13 +313,6 @@ export class DocImpl implements Doc { this._handleYBlockAdd(id); }); - this._awarenessUpdateDisposable = this.awarenessStore.slots.update.on( - () => { - // change readonly state will affect the undo/redo state - this._updateCanUndoRedoSignals(); - } - ); - initFn?.(); this._ready = true; @@ -335,18 +321,10 @@ export class DocImpl implements Doc { } redo() { - if (this.readonly) { - console.error('cannot modify data in readonly mode'); - return; - } this._history.redo(); } undo() { - if (this.readonly) { - console.error('cannot modify data in readonly mode'); - return; - } this._history.undo(); } diff --git a/packages/frontend/core/src/modules/workspace/impls/workspace.ts b/packages/frontend/core/src/modules/workspace/impls/workspace.ts index 2797fc8c0d..9b3e6a1c46 100644 --- a/packages/frontend/core/src/modules/workspace/impls/workspace.ts +++ b/packages/frontend/core/src/modules/workspace/impls/workspace.ts @@ -2,7 +2,6 @@ import { BlockSuiteError, ErrorCode, } from '@blocksuite/affine/global/exceptions'; -import type { BlockSuiteFlags } from '@blocksuite/affine/global/types'; import { NoopLogger, Slot } from '@blocksuite/affine/global/utils'; import { AwarenessStore, @@ -33,10 +32,6 @@ type WorkspaceOptions = { blobSource?: BlobSource; }; -const FLAGS_PRESET = { - readonly: {}, -} satisfies BlockSuiteFlags; - export class WorkspaceImpl implements Workspace { protected readonly _schema: Schema; @@ -73,10 +68,7 @@ export class WorkspaceImpl implements Workspace { this.id = id || ''; this.doc = new Y.Doc({ guid: id }); - this.awarenessStore = new AwarenessStore(new Awareness(this.doc), { - ...FLAGS_PRESET, - readonly: {}, - }); + this.awarenessStore = new AwarenessStore(new Awareness(this.doc)); blobSource = blobSource ?? new MemoryBlobSource(); const logger = new NoopLogger();