Compare commits

...

28 Commits

Author SHA1 Message Date
EYHN
2ec7de7e32 fix(core): add linked doc button (#10417) 2025-02-25 13:03:56 +08:00
liuyi
e5e5c0a8ba perf(core): only full sync before exporting (#10408) 2025-02-25 04:41:56 +00:00
EYHN
c644a46b8d fix(nbstore): local doc update lost (#10422) 2025-02-25 04:26:49 +00:00
Peng Xiao
7e892b3a7e fix(core): unused blobs query (#10399) 2025-02-25 10:58:43 +08:00
JimmFly
848145150d fix(core): close popover after successful invite in member editor (#10388) 2025-02-25 09:51:22 +08:00
JimmFly
dee6be11fb fix(core): reorder plan card action button conditions (#10387) 2025-02-25 09:51:10 +08:00
JimmFly
abda70d2c8 fix(core): fix permission checks for export workspace (#10401) 2025-02-25 09:50:43 +08:00
Saul-Mirone
40104f2f87 refactor(editor): remove unused any convension (#10410) 2025-02-24 15:57:49 +00:00
fundon
162b7adc1b fix(editor): should check text length and stop event propagation when adding a link (#10391) 2025-02-24 11:10:05 +00:00
Saul-Mirone
6289981fd1 refactor(editor): optimize extension register and effects (#10406)
Key Changes:

1. **Code Reorganization and Consolidation**
   - Created new centralized extension management through new files:
     - `enableEditorExtension` in `extensions/entry/enable-editor.ts`
     - `enablePreviewExtension` in `extensions/entry/enable-preview.ts`
   - Removed several spec-related files that are now consolidated:
     - Removed `specs/edgeless.ts`
     - Removed `specs/page.ts`
     - Removed `specs/preview.ts`

2. **Template Management**
   - Added new `register-templates.ts` file to handle template registration
   - Moved template registration logic from `specs/edgeless.ts` to this new file
   - Templates now include both edgeless and sticker templates

3. **Extension Management Changes**
   - Simplified extension enabling process through new centralized functions
   - `enableEditorExtension` now handles both page and edgeless modes
   - `enablePreviewExtension` consolidates preview-related extensions
   - Removed duplicate code for extension management

4. **Preview Functionality Updates**
   - Streamlined preview spec management
   - Consolidated footnote configuration
   - Improved theme and preview extension handling

5. **Dependencies and Effects**
   - Updated how effects are registered and managed
   - Simplified initialization process in `index.ts`
   - More organized approach to handling framework providers

The main theme of this PR appears to be code consolidation and simplification, moving from multiple specialized files to more centralized, reusable extension management. This should make the codebase more maintainable and reduce duplication while keeping the same functionality.

The changes primarily affect the editor's extension system, preview functionality, and template management, making these systems more modular and easier to maintain.
2025-02-24 10:37:59 +00:00
EYHN
0e581c915c feat(core): add resetSync button (#10404) 2025-02-24 10:22:34 +00:00
EYHN
59a791fe1f fix(nbstore): fix doc sync logic (#10400) 2025-02-24 10:22:34 +00:00
donteatfriedrice
378bb3795d refactor(editor): use doc title and id as snapshot file name (#10397)
[BS-2549](https://linear.app/affine-design/issue/BS-2549/snap-shot-导出建议使用文档名称作为文件名,而不是一个-id)
2025-02-24 09:32:32 +00:00
Saul-Mirone
60b994f38b refactor(editor): modular custom specs (#10398)
Key Changes:

1. **Removal of Scroll Anchoring Widget**
- Removed the scroll anchoring widget import and its related implementation from `blocksuite/affine/block-root/src/common-specs/widgets.ts`

2. **Enhanced React-Lit Integration**
- Added `ReactWebComponent` type export in `packages/frontend/component/src/lit-react/index.ts`
- Refactored text renderer component to use React integration:
  - Added React import and created `LitTextRenderer` component using `createReactComponentFromLit`
  - Moved the component declaration to a more appropriate location

3. **AI Feature Flag Integration**
- Added feature flag check for AI functionality in `enableAIExtension`
- Only enables AI extensions if the `enable_ai` flag is true

4. **Component Restructuring**
- Moved several components and utilities to dedicated extension files
- Consolidated Lit adapter implementations
- Removed direct widget imports in favor of extension-based approach
- Reorganized editor component structure for better maintainability

5. **File Reorganization**
- Removed `specs/custom/spec-patchers.ts` and distributed its functionality across multiple extension files
- Created new extension files for various features like:
  - Attachment embed views
  - Doc mode service
  - Doc URL handling
  - Edgeless clipboard
  - Mobile support
  - Note configuration
  - Various service patches (notification, peek view, quick search, etc.)

6. **Mobile Support Improvements**
- Refactored mobile extension enablement to be more modular
- Moved mobile-specific widget omissions into a dedicated extension

7. **Type System Improvements**
- Added more specific type imports for editors and components
- Enhanced type safety across the codebase

This PR appears to be a significant refactoring effort focused on:
1. Improving code organization through better separation of concerns
2. Enhancing the integration between React and Lit components
3. Adding feature flag support for AI capabilities
4. Making the codebase more maintainable and modular
5. Improving mobile support
6. Strengthening type safety

The changes suggest a move towards a more extension-based architecture, where functionality is more clearly separated into distinct modules rather than being centralized in larger files.
2025-02-24 08:30:01 +00:00
donteatfriedrice
1b2a4377fd feat(editor): update footnote node style and config (#10392)
[BS-2581](https://linear.app/affine-design/issue/BS-2581/优化-footnote-node-正文样式)
2025-02-24 08:15:04 +00:00
CatsJuice
8b4175c44d chore(core): update free pricing plan description (#10393) 2025-02-24 07:37:30 +00:00
EYHN
da7ab51e2d fix(core): remove unnecessary doc loading (#10395) 2025-02-24 07:22:05 +00:00
EYHN
a59e640423 fix(nbstore): leave awareness when destroy (#10394) 2025-02-24 07:22:04 +00:00
doouding
9bb74bce6b fix: drag bookmark from note to edgeless (#10389) 2025-02-24 06:13:05 +00:00
doouding
a0a97d0751 fix: drag connector and group element (#10385) 2025-02-24 06:13:05 +00:00
forehalo
b9e3fc54fd fix(server): include check of prerelease versions (#10386) 2025-02-24 04:44:44 +00:00
forehalo
b71fe291d1 fix(core): version control session (#10384) 2025-02-24 04:44:43 +00:00
forehalo
f02b57d58b fix(server): too much redundant updates events (#10383) 2025-02-24 04:44:43 +00:00
forehalo
2e0f0c624a chore: set base version to 0.20 (#10382) 2025-02-24 04:44:42 +00:00
Saul-Mirone
9435118ef1 refactor(editor): optimize ai code structure (#10381)
Let me analyze this diff and provide a clear description of the changes.

This PR introduces several significant changes focused on AI integration and code organization in the AFFiNE codebase:

1. **Enhanced SpecBuilder Functionality** (`blocksuite/affine/shared/src/utils/spec/spec-builder.ts`):
   - Added method chaining by returning `this` from `extend`, `omit`, and `replace` methods
   - Added new utility methods:
     - `hasAll(target: ExtensionType[])`: Checks if all specified extensions exist
     - `hasOneOf(target: ExtensionType[])`: Checks if at least one specified extension exists

2. **AI Extensions Modularization**:
   - Split the large AI-related code into separate modular files under `packages/frontend/core/src/blocksuite/ai/extensions/`:
     - `ai-code.ts`: Code block AI integration
     - `ai-edgeless-root.ts`: Edgeless mode AI features
     - `ai-image.ts`: Image block AI capabilities
     - `ai-page-root.ts`: Page root AI integration
     - `ai-paragraph.ts`: Paragraph block AI features
     - `enable-ai.ts`: Central AI extension enablement logic

3. **Widget Improvements**:
   - Enhanced `AffineAIPanelWidget` and `EdgelessCopilotWidget` with proper widget extensions
   - Moved widget-specific extensions into their respective files
   - Added proper type definitions and component registrations

4. **Code Organization**:
   - Simplified exports in `index.ts`
   - Better separation of concerns between different AI-related components
   - More modular approach to AI feature integration

5. **AI Integration Architecture**:
   - Introduced a new `enableAIExtension` function that handles:
     - Replacing standard blocks with AI-enhanced versions
     - Conditional enabling of AI features based on the current spec configuration
     - Extension of AI chat capabilities

The changes primarily focus on improving code organization, maintainability, and the architecture of AI feature integration in the AFFiNE editor. The modularization will make it easier to maintain and extend AI capabilities across different block types and editor modes.
2025-02-24 04:30:08 +00:00
doodlewind
67889d9364 fix(editor): turbo renderer stale frame lag on zooming (#10376)
Before:

https://github.com/user-attachments/assets/593e91a3-042e-4619-93a0-dca21fa942aa

After:

https://github.com/user-attachments/assets/779d7582-f7b2-4135-a97a-d1f65c7cb467

This is only a bug fix that ensures correct baseline rendering result. There'll be more optimizations specific for zooming TBD.
2025-02-24 03:49:04 +00:00
pengx17
5fe4b2b3e4 fix(core): remove tag page semicolon (#10379) 2025-02-24 03:14:06 +00:00
donteatfriedrice
2d41c2ff8d chore: bump theme (#10358) 2025-02-24 10:08:47 +08:00
195 changed files with 2476 additions and 2297 deletions

1
.github/CODEOWNERS vendored
View File

@@ -1 +1,2 @@
/blocksuite/ @toeverything/blocksuite-core
/packages/frontend/core/src/blocksuite @toeverything/blocksuite-core

View File

@@ -3,4 +3,4 @@ name: affine
description: AFFiNE cloud chart
type: application
version: 0.0.0
appVersion: "0.19.0"
appVersion: "0.20.0"

View File

@@ -3,7 +3,7 @@ name: graphql
description: AFFiNE GraphQL server
type: application
version: 0.0.0
appVersion: "0.19.0"
appVersion: "0.20.0"
dependencies:
- name: gcloud-sql-proxy
version: 0.0.0

View File

@@ -3,7 +3,7 @@ name: sync
description: AFFiNE Sync Server
type: application
version: 0.0.0
appVersion: "0.19.0"
appVersion: "0.20.0"
dependencies:
- name: gcloud-sql-proxy
version: 0.0.0

View File

@@ -98,5 +98,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -26,7 +26,7 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.11",
"@toeverything/theme": "^1.1.12",
"file-type": "^20.0.0",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
@@ -42,5 +42,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -25,7 +25,7 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.11",
"@toeverything/theme": "^1.1.12",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
"zod": "^3.23.8"
@@ -40,5 +40,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -24,7 +24,7 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.11",
"@toeverything/theme": "^1.1.12",
"@types/mdast": "^4.0.4",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
@@ -41,5 +41,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -26,7 +26,7 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.11",
"@toeverything/theme": "^1.1.12",
"@types/mdast": "^4.0.4",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
@@ -42,5 +42,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -26,7 +26,7 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.11",
"@toeverything/theme": "^1.1.12",
"@types/mdast": "^4.0.4",
"date-fns": "^4.0.0",
"lit": "^3.2.0",
@@ -44,5 +44,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -23,7 +23,7 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.11",
"@toeverything/theme": "^1.1.12",
"@types/mdast": "^4.0.4",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
@@ -39,5 +39,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -25,7 +25,7 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.11",
"@toeverything/theme": "^1.1.12",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
"zod": "^3.23.8"
@@ -40,5 +40,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -25,7 +25,7 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.11",
"@toeverything/theme": "^1.1.12",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
"yjs": "^13.6.21",
@@ -44,5 +44,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -24,7 +24,7 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.11",
"@toeverything/theme": "^1.1.12",
"@types/mdast": "^4.0.4",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
@@ -41,5 +41,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -26,7 +26,7 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.11",
"@toeverything/theme": "^1.1.12",
"file-type": "^20.0.0",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
@@ -42,5 +42,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -24,7 +24,7 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.11",
"@toeverything/theme": "^1.1.12",
"@types/katex": "^0.16.7",
"@types/mdast": "^4.0.4",
"katex": "^0.16.11",
@@ -43,5 +43,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -23,7 +23,7 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.11",
"@toeverything/theme": "^1.1.12",
"@types/mdast": "^4.0.4",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
@@ -42,5 +42,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -26,7 +26,7 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.11",
"@toeverything/theme": "^1.1.12",
"@types/mdast": "^4.0.4",
"@vanilla-extract/css": "^1.17.0",
"lit": "^3.2.0",
@@ -43,5 +43,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -23,7 +23,7 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.11",
"@toeverything/theme": "^1.1.12",
"@types/mdast": "^4.0.4",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
@@ -39,5 +39,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -45,7 +45,7 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.11",
"@toeverything/theme": "^1.1.12",
"@types/lodash-es": "^4.17.12",
"@types/mdast": "^4.0.4",
"@vanilla-extract/css": "^1.17.0",
@@ -69,5 +69,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -6,19 +6,19 @@ import {
PageViewportServiceExtension,
ThemeService,
} from '@blocksuite/affine-shared/services';
import { dragHandleWidget } from '@blocksuite/affine-widget-drag-handle';
import { docRemoteSelectionWidget } from '@blocksuite/affine-widget-remote-selection';
import { scrollAnchoringWidget } from '@blocksuite/affine-widget-scroll-anchoring';
import { FlavourExtension } from '@blocksuite/block-std';
import type { ExtensionType } from '@blocksuite/store';
import { RootBlockAdapterExtensions } from '../adapters/extension';
import {
docRemoteSelectionWidget,
dragHandleWidget,
embedCardToolbarWidget,
formatBarWidget,
innerModalWidget,
linkedDocWidget,
modalWidget,
scrollAnchoringWidget,
slashMenuWidget,
viewportOverlayWidget,
} from './widgets';

View File

@@ -1,6 +1,3 @@
import { AFFINE_DRAG_HANDLE_WIDGET } from '@blocksuite/affine-widget-drag-handle';
import { AFFINE_DOC_REMOTE_SELECTION_WIDGET } from '@blocksuite/affine-widget-remote-selection';
import { AFFINE_SCROLL_ANCHORING_WIDGET } from '@blocksuite/affine-widget-scroll-anchoring';
import { WidgetViewExtension } from '@blocksuite/block-std';
import { literal, unsafeStatic } from 'lit/static-html.js';
@@ -32,11 +29,6 @@ export const linkedDocWidget = WidgetViewExtension(
AFFINE_LINKED_DOC_WIDGET,
literal`${unsafeStatic(AFFINE_LINKED_DOC_WIDGET)}`
);
export const dragHandleWidget = WidgetViewExtension(
'affine:page',
AFFINE_DRAG_HANDLE_WIDGET,
literal`${unsafeStatic(AFFINE_DRAG_HANDLE_WIDGET)}`
);
export const embedCardToolbarWidget = WidgetViewExtension(
'affine:page',
AFFINE_EMBED_CARD_TOOLBAR_WIDGET,
@@ -47,18 +39,8 @@ export const formatBarWidget = WidgetViewExtension(
AFFINE_FORMAT_BAR_WIDGET,
literal`${unsafeStatic(AFFINE_FORMAT_BAR_WIDGET)}`
);
export const docRemoteSelectionWidget = WidgetViewExtension(
'affine:page',
AFFINE_DOC_REMOTE_SELECTION_WIDGET,
literal`${unsafeStatic(AFFINE_DOC_REMOTE_SELECTION_WIDGET)}`
);
export const viewportOverlayWidget = WidgetViewExtension(
'affine:page',
AFFINE_VIEWPORT_OVERLAY_WIDGET,
literal`${unsafeStatic(AFFINE_VIEWPORT_OVERLAY_WIDGET)}`
);
export const scrollAnchoringWidget = WidgetViewExtension(
'affine:page',
AFFINE_SCROLL_ANCHORING_WIDGET,
literal`${unsafeStatic(AFFINE_SCROLL_ANCHORING_WIDGET)}`
);

View File

@@ -1,6 +1,6 @@
import { AFFINE_EDGELESS_AUTO_CONNECT_WIDGET } from '@blocksuite/affine-widget-edgeless-auto-connect';
import { AFFINE_FRAME_TITLE_WIDGET } from '@blocksuite/affine-widget-frame-title';
import { AFFINE_EDGELESS_REMOTE_SELECTION_WIDGET } from '@blocksuite/affine-widget-remote-selection';
import { autoConnectWidget } from '@blocksuite/affine-widget-edgeless-auto-connect';
import { frameTitleWidget } from '@blocksuite/affine-widget-frame-title';
import { edgelessRemoteSelectionWidget } from '@blocksuite/affine-widget-remote-selection';
import {
BlockServiceWatcher,
BlockViewExtension,
@@ -20,31 +20,16 @@ import { EDGELESS_SELECTED_RECT_WIDGET } from './components/rects/edgeless-selec
import { EDGELESS_TOOLBAR_WIDGET } from './components/toolbar/edgeless-toolbar.js';
import { EdgelessRootService } from './edgeless-root-service.js';
export const edgelessRemoteSelectionWidget = WidgetViewExtension(
'affine:page',
AFFINE_EDGELESS_REMOTE_SELECTION_WIDGET,
literal`${unsafeStatic(AFFINE_EDGELESS_REMOTE_SELECTION_WIDGET)}`
);
export const edgelessZoomToolbarWidget = WidgetViewExtension(
'affine:page',
AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET,
literal`${unsafeStatic(AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET)}`
);
export const frameTitleWidget = WidgetViewExtension(
'affine:page',
AFFINE_FRAME_TITLE_WIDGET,
literal`${unsafeStatic(AFFINE_FRAME_TITLE_WIDGET)}`
);
export const elementToolbarWidget = WidgetViewExtension(
'affine:page',
EDGELESS_ELEMENT_TOOLBAR_WIDGET,
literal`${unsafeStatic(EDGELESS_ELEMENT_TOOLBAR_WIDGET)}`
);
export const autoConnectWidget = WidgetViewExtension(
'affine:page',
AFFINE_EDGELESS_AUTO_CONNECT_WIDGET,
literal`${unsafeStatic(AFFINE_EDGELESS_AUTO_CONNECT_WIDGET)}`
);
export const edgelessDraggingAreaWidget = WidgetViewExtension(
'affine:page',
EDGELESS_DRAGGING_AREA_WIDGET,

View File

@@ -29,7 +29,10 @@ async function exportDocs(collection: Workspace, docs: Store[]) {
snapshots
.filter((snapshot): snapshot is DocSnapshot => !!snapshot)
.map(async snapshot => {
const snapshotName = `${snapshot.meta.title || 'untitled'}.snapshot.json`;
// Use the title and id as the snapshot file name
const title = snapshot.meta.title || 'untitled';
const id = snapshot.meta.id;
const snapshotName = `${title}-${id}.snapshot.json`;
await zip.file(snapshotName, JSON.stringify(snapshot, null, 2));
})
);
@@ -63,6 +66,7 @@ async function exportDocs(collection: Workspace, docs: Store[]) {
}
const downloadBlob = await zip.generate();
// Use the collection id as the zip file name
return download(downloadBlob, `${collection.id}.bs.zip`);
}

View File

@@ -24,7 +24,7 @@
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.11",
"@toeverything/theme": "^1.1.12",
"fractional-indexing": "^3.2.0",
"lit": "^3.2.0",
"lodash.chunk": "^4.2.0",
@@ -44,5 +44,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -22,7 +22,7 @@
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.11",
"@toeverything/theme": "^1.1.12",
"fractional-indexing": "^3.2.0",
"html2canvas": "^1.4.1",
"lit": "^3.2.0",
@@ -46,5 +46,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -1,4 +1,8 @@
import type { SurfaceBlockProps } from '@blocksuite/block-std/gfx';
import {
SURFACE_TEXT_UNIQ_IDENTIFIER,
SURFACE_YMAP_UNIQ_IDENTIFIER,
} from '@blocksuite/block-std/gfx';
import type {
FromSnapshotPayload,
SnapshotNode,
@@ -7,10 +11,6 @@ import type {
import { BaseBlockTransformer } from '@blocksuite/store';
import * as Y from 'yjs';
const SURFACE_TEXT_UNIQ_IDENTIFIER = 'affine:surface:text';
// Used for group children field
const SURFACE_YMAP_UNIQ_IDENTIFIER = 'affine:surface:ymap';
export class SurfaceBlockTransformer extends BaseBlockTransformer<SurfaceBlockProps> {
private _elementToJSON(element: Y.Map<unknown>) {
const value: Record<string, unknown> = {};

View File

@@ -39,5 +39,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -24,7 +24,7 @@
"@lit/context": "^1.1.2",
"@lottiefiles/dotlottie-wc": "^0.4.0",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.11",
"@toeverything/theme": "^1.1.12",
"@types/hast": "^3.0.4",
"@types/mdast": "^4.0.4",
"collapse-white-space": "^2.1.0",
@@ -74,5 +74,5 @@
"@types/katex": "^0.16.7",
"@types/lodash.clonedeep": "^4.5.9"
},
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -1,4 +1,4 @@
import type { EditorHost } from '@blocksuite/block-std';
import { type EditorHost, TextSelection } from '@blocksuite/block-std';
import type { TemplateResult } from 'lit';
import {
@@ -26,6 +26,7 @@ export interface TextFormatConfig {
hotkey?: string;
activeWhen: (host: EditorHost) => boolean;
action: (host: EditorHost) => void;
textChecker?: (host: EditorHost) => boolean;
}
export const textFormatConfigs: TextFormatConfig[] = [
@@ -124,5 +125,14 @@ export const textFormatConfigs: TextFormatConfig[] = [
action: host => {
host.std.command.chain().pipe(toggleLink).run();
},
// should check text length
textChecker: host => {
const textSelection = host.std.selection.find(TextSelection);
if (!textSelection || textSelection.isCollapsed()) return false;
return Boolean(
textSelection.from.length + (textSelection.to?.length ?? 0)
);
},
},
];

View File

@@ -25,6 +25,7 @@ export interface FootNoteNodeConfig {
customPopupRenderer?: FootNotePopupRenderer;
interactive?: boolean;
hidePopup?: boolean;
disableHoverEffect?: boolean;
onPopupClick?: FootNotePopupClickHandler;
}
@@ -33,7 +34,9 @@ export class FootNoteNodeConfigProvider {
private _customPopupRenderer?: FootNotePopupRenderer;
private _hidePopup: boolean;
private _interactive: boolean;
private _disableHoverEffect: boolean;
private _onPopupClick?: FootNotePopupClickHandler;
get customNodeRenderer() {
return this._customNodeRenderer;
}
@@ -58,6 +61,10 @@ export class FootNoteNodeConfigProvider {
return this._interactive;
}
get disableHoverEffect() {
return this._disableHoverEffect;
}
constructor(
config: FootNoteNodeConfig,
readonly std: BlockStdScope
@@ -66,6 +73,7 @@ export class FootNoteNodeConfigProvider {
this._customPopupRenderer = config.customPopupRenderer;
this._hidePopup = config.hidePopup ?? false;
this._interactive = config.interactive ?? true;
this._disableHoverEffect = config.disableHoverEffect ?? false;
this._onPopupClick = config.onPopupClick;
}
@@ -85,6 +93,10 @@ export class FootNoteNodeConfigProvider {
this._interactive = interactive;
}
setDisableHoverEffect(disableHoverEffect: boolean) {
this._disableHoverEffect = disableHoverEffect;
}
setPopupClick(onPopupClick: FootNotePopupClickHandler) {
this._onPopupClick = onPopupClick;
}

View File

@@ -19,6 +19,7 @@ import { shift } from '@floating-ui/dom';
import { baseTheme } from '@toeverything/theme';
import { css, html, nothing, unsafeCSS } from 'lit';
import { property } from 'lit/decorators.js';
import { classMap } from 'lit-html/directives/class-map.js';
import { ref } from 'lit-html/directives/ref.js';
import { HoverController } from '../../../../../hover/controller';
@@ -37,7 +38,7 @@ export class AffineFootnoteNode extends WithDisposable(ShadowlessElement) {
.footnote-content-default {
display: inline-block;
background: ${unsafeCSSVarV2('button/primary')};
background: ${unsafeCSSVarV2('block/footnote/numberBgHover')};
color: ${unsafeCSSVarV2('button/pureWhiteText')};
width: 14px;
height: 14px;
@@ -48,6 +49,21 @@ export class AffineFootnoteNode extends WithDisposable(ShadowlessElement) {
text-align: center;
text-overflow: ellipsis;
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
transition: background 0.3s ease-in-out;
}
.footnote-node.hover-effect {
.footnote-content-default {
color: var(--affine-text-primary-color);
background: ${unsafeCSSVarV2('block/footnote/numberBg')};
}
}
.footnote-node.hover-effect:hover {
.footnote-content-default {
color: ${unsafeCSSVarV2('button/pureWhiteText')};
background: ${unsafeCSSVarV2('block/footnote/numberBgHover')};
}
}
`;
@@ -67,6 +83,10 @@ export class AffineFootnoteNode extends WithDisposable(ShadowlessElement) {
return this.config?.hidePopup;
}
get disableHoverEffect() {
return this.config?.disableHoverEffect;
}
get onPopupClick() {
return this.config?.onPopupClick;
}
@@ -142,7 +162,7 @@ export class AffineFootnoteNode extends WithDisposable(ShadowlessElement) {
},
};
},
{ enterDelay: 500 }
{ enterDelay: 300 }
);
override render() {
@@ -156,9 +176,14 @@ export class AffineFootnoteNode extends WithDisposable(ShadowlessElement) {
? this.customNodeRenderer(footnote, this.std)
: this._FootNoteDefaultContent(footnote);
const nodeClasses = classMap({
'footnote-node': true,
'hover-effect': !this.disableHoverEffect,
});
return html`<span
${this.hidePopup ? '' : ref(this._whenHover.setReference)}
class="footnote-node"
class=${nodeClasses}
>${node}<v-text .str=${ZERO_WIDTH_NON_JOINER}></v-text
></span>`;
}

View File

@@ -14,6 +14,7 @@ export class FootNotePopupChip extends LitElement {
gap: 4px;
box-sizing: border-box;
cursor: default;
transition: width 0.3s ease-in-out;
}
.prefix-icon,

View File

@@ -26,7 +26,6 @@ export class FootNotePopup extends SignalWatcher(WithDisposable(LitElement)) {
.footnote-popup-container {
border-radius: 4px;
box-shadow: ${unsafeCSSVar('overlayPanelShadow')};
border-radius: 4px;
background-color: ${unsafeCSSVarV2('layer/background/primary')};
border: 0.5px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
}

View File

@@ -20,8 +20,14 @@ export const textFormatKeymap = (std: BlockStdScope) =>
const textSelection = selection.find(TextSelection);
if (!textSelection) return;
const allowed = config.textChecker?.(std.host) ?? true;
if (!allowed) return;
const event = ctx.get('keyboardState').raw;
event.stopPropagation();
event.preventDefault();
config.action(std.host);
ctx.get('keyboardState').raw.preventDefault();
return true;
},
};

View File

@@ -23,7 +23,7 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.11",
"@toeverything/theme": "^1.1.12",
"date-fns": "^4.0.0",
"lit": "^3.2.0",
"yjs": "^13.6.21",
@@ -43,5 +43,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -26,7 +26,7 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.11",
"@toeverything/theme": "^1.1.12",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
"zod": "^3.23.8"
@@ -41,5 +41,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -25,7 +25,7 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.11",
"@toeverything/theme": "^1.1.12",
"@vanilla-extract/css": "^1.17.0",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
@@ -41,5 +41,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -17,7 +17,7 @@
"@blocksuite/global": "workspace:*",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@toeverything/theme": "^1.1.11",
"@toeverything/theme": "^1.1.12",
"fractional-indexing": "^3.2.0",
"yjs": "^13.6.21",
"zod": "^3.23.8"
@@ -31,5 +31,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -63,10 +63,6 @@ export class BrushElementModel extends GfxPrimitiveElementModel<BrushProps> {
return 'brush';
}
static override propsToY(props: BrushProps) {
return props;
}
override containsBound(bounds: Bound) {
const points = getPointsFromBoundWithRotation(this);
return points.some(point => bounds.containsPoint(point));

View File

@@ -125,8 +125,8 @@ export class ConnectorElementModel extends GfxPrimitiveElementModel<ConnectorEle
return 'connector';
}
static override propsToY(props: ConnectorElementProps) {
if (props.text && !(props.text instanceof Y.Text)) {
static propsToY(props: ConnectorElementProps) {
if (typeof props.text === 'string') {
props.text = new Y.Text(props.text);
}

View File

@@ -35,8 +35,8 @@ export class GroupElementModel extends GfxGroupLikeElementModel<GroupElementProp
return 'group';
}
static override propsToY(props: Record<string, unknown>) {
if ('title' in props && !(props.title instanceof Y.Text)) {
static propsToY(props: Record<string, unknown>) {
if (typeof props.title === 'string') {
props.title = new Y.Text(props.title as string);
}

View File

@@ -180,7 +180,7 @@ export class MindmapElementModel extends GfxGroupLikeElementModel<MindmapElement
return 'mindmap';
}
static override propsToY(props: Record<string, unknown>) {
static propsToY(props: Record<string, unknown>) {
if (
props.children &&
!isNodeType(props.children as Record<string, unknown>) &&

View File

@@ -67,8 +67,8 @@ export class ShapeElementModel extends GfxPrimitiveElementModel<ShapeProps> {
return 'shape';
}
static override propsToY(props: ShapeProps) {
if (props.text && !(props.text instanceof Y.Text)) {
static propsToY(props: ShapeProps) {
if (typeof props.text === 'string') {
props.text = new Y.Text(props.text);
}

View File

@@ -30,9 +30,9 @@ export class TextElementModel extends GfxPrimitiveElementModel<TextElementProps>
return 'text';
}
static override propsToY(props: Record<string, unknown>) {
if (props.text && !(props.text instanceof Y.Text)) {
props.text = new Y.Text(props.text as string);
static propsToY(props: Record<string, unknown>) {
if (typeof props.text === 'string') {
props.text = new Y.Text(props.text);
}
return props;

View File

@@ -22,7 +22,7 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.11",
"@toeverything/theme": "^1.1.12",
"@types/hast": "^3.0.4",
"@types/mdast": "^4.0.4",
"dompurify": "^3.2.4",
@@ -77,5 +77,5 @@
"@types/lodash.mergewith": "^4",
"vitest": "3.0.6"
},
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -13,10 +13,20 @@ export class SpecBuilder {
extend(extensions: ExtensionType[]) {
this._value = [...this._value, ...extensions];
return this;
}
omit(target: ExtensionType) {
this._value = this._value.filter(extension => extension !== target);
return this;
}
hasAll(target: ExtensionType[]) {
return target.every(t => this._value.includes(t));
}
hasOneOf(target: ExtensionType[]) {
return target.some(t => this._value.includes(t));
}
replace(target: ExtensionType[], newExtension: ExtensionType[]) {
@@ -24,5 +34,6 @@ export class SpecBuilder {
...this._value.filter(extension => !target.includes(extension)),
...newExtension,
];
return this;
}
}

View File

@@ -66,13 +66,12 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
);
});
const debounceOptions = { leading: false, trailing: true };
const debouncedRefresh = debounce(
() => {
this.refresh().catch(console.error);
},
1000, // During this period, fallback to DOM
debounceOptions
{ leading: false, trailing: true }
);
this.disposables.add(
this.std.store.slots.blockUpdated.on(() => {
@@ -97,11 +96,12 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
return this.std.get(GfxControllerIdentifier).viewport;
}
async refresh(force = false) {
if (this.state === 'paused' && !force) return;
async refresh() {
if (this.state === 'paused') return;
this.clearCanvas();
if (this.viewport.zoom > zoomThreshold) {
this.clearCanvas();
return;
} else if (this.canUseBitmapCache()) {
this.drawCachedBitmap(this.layoutCache!);
} else {
@@ -115,8 +115,9 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
}
invalidate() {
this.clearCache();
this.clearCanvas();
this.layoutCache = null;
this.clearTile();
this.clearCanvas(); // Should clear immediately after content updates
}
private updateLayoutCache() {
@@ -124,11 +125,6 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
this.layoutCache = layout;
}
private clearCache() {
this.layoutCache = null;
this.clearTile();
}
private clearTile() {
if (this.tile) {
this.tile.bitmap.close();
@@ -154,17 +150,13 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
this.worker.onmessage = (e: MessageEvent) => {
if (e.data.type === 'bitmapPainted') {
this.handlePaintedBitmap(e.data.bitmap, layout, resolve);
this.handlePaintedBitmap(e.data.bitmap, resolve);
}
};
});
}
private handlePaintedBitmap(
bitmap: ImageBitmap,
layout: ViewportLayout,
resolve: () => void
) {
private handlePaintedBitmap(bitmap: ImageBitmap, resolve: () => void) {
if (this.tile) {
this.tile.bitmap.close();
}
@@ -172,7 +164,6 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
bitmap,
zoom: this.viewport.zoom,
};
this.drawCachedBitmap(layout);
resolve();
}

View File

@@ -28,7 +28,7 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.11",
"@toeverything/theme": "^1.1.12",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
"zod": "^3.23.8"
@@ -43,5 +43,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -1,4 +1,15 @@
import { WidgetViewExtension } from '@blocksuite/block-std';
import { literal, unsafeStatic } from 'lit/static-html.js';
import { AFFINE_DRAG_HANDLE_WIDGET } from './consts';
export * from './consts';
export * from './drag-handle';
export * from './utils';
export type { DragBlockPayload } from './watchers/drag-event-watcher';
export const dragHandleWidget = WidgetViewExtension(
'affine:page',
AFFINE_DRAG_HANDLE_WIDGET,
literal`${unsafeStatic(AFFINE_DRAG_HANDLE_WIDGET)}`
);

View File

@@ -1,6 +1,16 @@
import type { SurfaceBlockModel } from '@blocksuite/affine-block-surface';
import type { ConnectorElementModel } from '@blocksuite/affine-model';
import type { BlockStdScope } from '@blocksuite/block-std';
import { isGfxGroupCompatibleModel } from '@blocksuite/block-std/gfx';
import {
GfxController,
type GfxModel,
isGfxGroupCompatibleModel,
} from '@blocksuite/block-std/gfx';
import {
assertType,
type IVec,
type SerializedXYWH,
} from '@blocksuite/global/utils';
import type { TransformerMiddleware } from '@blocksuite/store';
/**
@@ -18,6 +28,7 @@ export const gfxBlocksFilter = (
const surface = store.getBlocksByFlavour('affine:surface')[0]
.model as SurfaceBlockModel;
const idsToCheck = ids.slice();
const gfx = std.get(GfxController);
for (const id of idsToCheck) {
const blockOrElem = store.getBlock(id)?.model ?? surface.getElementById(id);
@@ -45,5 +56,62 @@ export const gfxBlocksFilter = (
return;
}
});
slots.afterExport.on(payload => {
if (payload.type !== 'block') {
return;
}
if (payload.model.flavour === 'affine:surface') {
const { snapshot } = payload;
const elementsMap = snapshot.props.elements as Record<
string,
{ type: string }
>;
Object.entries(elementsMap).forEach(([elementId, val]) => {
if (val.type === 'connector') {
assertType<{
type: 'connector';
source: { position: IVec; id?: string };
target: { position: IVec; id?: string };
xywh: SerializedXYWH;
}>(val);
const connectorElem = gfx.getElementById(
elementId
) as ConnectorElementModel;
if (!connectorElem) {
delete elementsMap[elementId];
return;
}
// should be deleted during the import process
val.xywh = connectorElem.xywh;
['source', 'target'].forEach(key => {
const endpoint = val[key as 'source' | 'target'];
if (endpoint.id && !selectedIds.has(endpoint.id)) {
const endElem = gfx.getElementById(endpoint.id);
if (!endElem) {
delete elementsMap[elementId];
return;
}
const endElemBound = (endElem as GfxModel).elementBound;
val[key as 'source' | 'target'] = {
position: endElemBound.getRelativePoint(
endpoint.position ?? [0.5, 0.5]
),
};
}
});
}
});
}
});
};
};

View File

@@ -290,13 +290,28 @@ export function getSnapshotRect(snapshot: SliceSnapshot): Bound | null {
if (block.flavour === 'affine:surface') {
if (block.props.elements) {
Object.values(
block.props.elements as Record<string, { xywh: SerializedXYWH }>
block.props.elements as Record<
string,
{ type: string; xywh: SerializedXYWH }
>
).forEach(elem => {
if (elem.xywh) {
bound = bound
? bound.unite(Bound.deserialize(elem.xywh))
: Bound.deserialize(elem.xywh);
}
if (elem.type === 'connector') {
let connectorBound: Bound | undefined;
if (elem.xywh) {
connectorBound = Bound.deserialize(elem.xywh);
}
if (connectorBound) {
bound = bound ? bound.unite(connectorBound) : connectorBound;
}
}
});
}

View File

@@ -1,5 +1,4 @@
import { ParagraphBlockComponent } from '@blocksuite/affine-block-paragraph';
import { SurfaceBlockModel } from '@blocksuite/affine-block-surface';
import { DropIndicator } from '@blocksuite/affine-components/drop-indicator';
import {
AttachmentBlockModel,
@@ -45,11 +44,14 @@ import {
type GfxModel,
GfxPrimitiveElementModel,
isGfxGroupCompatibleModel,
SURFACE_YMAP_UNIQ_IDENTIFIER,
SurfaceBlockModel,
} from '@blocksuite/block-std/gfx';
import {
assertType,
Bound,
groupBy,
type IVec,
last,
Point,
Rect,
@@ -752,7 +754,11 @@ export class DragEventWatcher {
const idRemap = new Map<string, string>();
let elemMap: Record<
string,
{ type: string; children?: { json: Record<string, unknown> } }
{
type: string;
xywh?: SerializedXYWH;
children?: { json: Record<string, unknown> };
}
> = {};
const blockMap: Record<
string,
@@ -766,7 +772,7 @@ export class DragEventWatcher {
const constructor = surface.getConstructor(elem.type);
const isGroup = Object.isPrototypeOf.call(
GfxGroupLikeElementModel.prototype,
constructor
constructor.prototype
);
return isGroup;
@@ -782,24 +788,40 @@ export class DragEventWatcher {
if (block.flavour === 'affine:surface') {
elemMap = (block.props.elements as typeof elemMap) ?? {};
Object.entries(elemMap).forEach(([elemId, elem]) => {
if (isGroupLikeElem(elem)) {
// only add the group to the root if it's not a child of any other element
if (
Object.values(containerTree).every(
childSet => !childSet.has(elemId)
)
) {
containerTree['root'].add(elem.type);
}
if (
Object.values(containerTree).every(
childSet => !childSet.has(elemId)
)
) {
containerTree['root'].add(elemId);
}
if (isGroupLikeElem(elem)) {
Object.keys(elem.children?.json ?? {}).forEach(childId => {
containerTree[elemId] = containerTree[elemId] ?? new Set();
containerTree[elemId].add(childId);
// if the child was already added to the root, remove it
containerTree['root'].delete(childId);
});
} else {
containerTree['root'].add(elemId);
return;
} else if (elem.type === 'connector') {
assertType<{
type: 'connector';
source: { position: IVec; id?: string };
target: { position: IVec; id?: string };
}>(elem);
if (elem.source.id) {
containerTree[elemId] = containerTree[elemId] ?? new Set();
containerTree[elemId].add(elem.source.id);
containerTree['root'].delete(elem.source.id);
}
if (elem.target.id) {
containerTree[elemId] = containerTree[elemId] ?? new Set();
containerTree[elemId].add(elem.target.id);
containerTree['root'].delete(elem.target.id);
}
}
});
@@ -876,19 +898,58 @@ export class DragEventWatcher {
idRemap.set(id, slices.content[0].id);
}
} else if (elemMap[id]) {
if (elemMap[id].children) {
const childJson = elemMap[id].children.json;
Object.keys(childJson).forEach(childId => {
if (idRemap.has(childId)) {
const remappedId = idRemap.get(childId)!;
childJson[remappedId] = childJson[childId];
delete childJson[childId];
} else {
delete childJson[childId];
const elem = elemMap[id];
Object.entries(elem).forEach(([_, val]) => {
if (
val instanceof Object &&
Reflect.has(val, SURFACE_YMAP_UNIQ_IDENTIFIER)
) {
const childJson = Reflect.get(val, 'json') as Record<
string,
unknown
>;
Object.keys(childJson).forEach(oldChildId => {
if (idRemap.has(oldChildId)) {
const remappedId = idRemap.get(oldChildId)!;
const val = structuredClone(childJson[oldChildId]);
if (elem.type === 'mindmap') {
assertType<{ parent?: string }>(val);
if (val.parent) {
val.parent = idRemap.get(val.parent);
}
}
childJson[remappedId] = val;
delete childJson[oldChildId];
} else {
delete childJson[oldChildId];
}
});
}
});
if (elem.type === 'connector') {
assertType<{
type: 'connector';
source: { position: IVec; id?: string };
target: { position: IVec; id?: string };
}>(elem);
(['source', 'target'] as const).forEach(key => {
const endpoint = elem[key];
if (endpoint.id) {
if (idRemap.get(endpoint.id)) {
endpoint.id = idRemap.get(endpoint.id);
} else {
delete endpoint.id;
}
}
});
}
const newId = surface.addElement(elemMap[id]);
const newId = surface.addElement(elem);
idRemap.set(id, newId);
}
};
@@ -918,8 +979,45 @@ export class DragEventWatcher {
if (block.flavour === 'affine:surface') {
if (block.props.elements) {
Object.values(
block.props.elements as Record<string, { xywh: SerializedXYWH }>
block.props.elements as Record<
string,
{ type: string; xywh?: SerializedXYWH }
>
).forEach(elem => {
if (elem.type === 'connector') {
assertType<{
type: 'connector';
xywh?: SerializedXYWH;
source: { position: IVec; id?: string };
target: { position: IVec; id?: string };
}>(elem);
const connectorBound = elem.xywh
? Bound.deserialize(elem.xywh)
: new Bound(0, 0, 0, 0);
delete elem.xywh;
(['source', 'target'] as const).forEach(key => {
const endpoint = elem[key];
if (!endpoint.id) {
const originalPos = endpoint.position;
elem[key] = {
position: ignoreOriginalPos
? [
originalPos[0] - connectorBound.x + modelX,
originalPos[1] - connectorBound.y + modelY,
]
: [
originalPos[0] - rect.x + modelX,
originalPos[1] - rect.y + modelY,
],
};
}
});
}
if (elem.xywh) {
const elemBound = Bound.deserialize(elem.xywh);
@@ -956,6 +1054,7 @@ export class DragEventWatcher {
if (
block.flavour === 'affine:attachment' ||
block.flavour === 'affine:bookmark' ||
block.flavour.startsWith('affine:embed-')
) {
const style = 'vertical' as EmbedCardStyle;
@@ -1037,6 +1136,7 @@ export class DragEventWatcher {
block.id === content[idx].id &&
(block.flavour === 'affine:image' ||
block.flavour === 'affine:attachment' ||
block.flavour === 'affine:bookmark' ||
block.flavour.startsWith('affine:embed-'))
) {
store.updateBlock(block as BlockModel, {

View File

@@ -22,7 +22,7 @@
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.3",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.11",
"@toeverything/theme": "^1.1.12",
"lit": "^3.2.0"
},
"exports": {
@@ -35,5 +35,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -13,7 +13,7 @@ import {
} from '@blocksuite/affine-model';
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
import { matchModels, stopPropagation } from '@blocksuite/affine-shared/utils';
import { WidgetComponent } from '@blocksuite/block-std';
import { WidgetComponent, WidgetViewExtension } from '@blocksuite/block-std';
import {
type GfxController,
GfxControllerIdentifier,
@@ -28,6 +28,7 @@ import { css, html, nothing, type TemplateResult } from 'lit';
import { state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { literal, unsafeStatic } from 'lit/static-html.js';
const PAGE_VISIBLE_INDEX_LABEL_WIDTH = 44;
const PAGE_VISIBLE_INDEX_LABEL_HEIGHT = 24;
@@ -613,6 +614,12 @@ export class EdgelessAutoConnectWidget extends WidgetComponent<RootBlockModel> {
private accessor _show = false;
}
export const autoConnectWidget = WidgetViewExtension(
'affine:page',
AFFINE_EDGELESS_AUTO_CONNECT_WIDGET,
literal`${unsafeStatic(AFFINE_EDGELESS_AUTO_CONNECT_WIDGET)}`
);
declare global {
interface HTMLElementTagNameMap {
'affine-edgeless-auto-connect-widget': EdgelessAutoConnectWidget;

View File

@@ -20,7 +20,7 @@
"@blocksuite/global": "workspace:*",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.11",
"@toeverything/theme": "^1.1.12",
"lit": "^3.2.0"
},
"exports": {
@@ -33,5 +33,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -1,7 +1,8 @@
import { FrameBlockModel, type RootBlockModel } from '@blocksuite/affine-model';
import { WidgetComponent } from '@blocksuite/block-std';
import { WidgetComponent, WidgetViewExtension } from '@blocksuite/block-std';
import { html } from 'lit';
import { repeat } from 'lit/directives/repeat.js';
import { literal, unsafeStatic } from 'lit/static-html.js';
import type { AffineFrameTitle } from './frame-title.js';
@@ -36,3 +37,9 @@ export class AffineFrameTitleWidget extends WidgetComponent<RootBlockModel> {
}
export * from './styles.js';
export const frameTitleWidget = WidgetViewExtension(
'affine:page',
AFFINE_FRAME_TITLE_WIDGET,
literal`${unsafeStatic(AFFINE_FRAME_TITLE_WIDGET)}`
);

View File

@@ -21,7 +21,7 @@
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.3",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.11",
"@toeverything/theme": "^1.1.12",
"lit": "^3.2.0"
},
"exports": {
@@ -34,5 +34,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -1,6 +1,20 @@
import type * as CommandsType from '@blocksuite/affine-shared/commands';
import { WidgetViewExtension } from '@blocksuite/block-std';
import { literal, unsafeStatic } from 'lit/static-html.js';
declare type _GLOBAL_ = typeof CommandsType;
import { AFFINE_DOC_REMOTE_SELECTION_WIDGET } from './doc';
import { AFFINE_EDGELESS_REMOTE_SELECTION_WIDGET } from './edgeless';
export * from './doc';
export * from './edgeless';
export const docRemoteSelectionWidget = WidgetViewExtension(
'affine:page',
AFFINE_DOC_REMOTE_SELECTION_WIDGET,
literal`${unsafeStatic(AFFINE_DOC_REMOTE_SELECTION_WIDGET)}`
);
export const edgelessRemoteSelectionWidget = WidgetViewExtension(
'affine:page',
AFFINE_EDGELESS_REMOTE_SELECTION_WIDGET,
literal`${unsafeStatic(AFFINE_EDGELESS_REMOTE_SELECTION_WIDGET)}`
);

View File

@@ -18,7 +18,7 @@
"@blocksuite/block-std": "workspace:*",
"@blocksuite/global": "workspace:*",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.11",
"@toeverything/theme": "^1.1.12",
"lit": "^3.2.0"
},
"exports": {
@@ -31,5 +31,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -1 +1,12 @@
import { WidgetViewExtension } from '@blocksuite/block-std';
import { literal, unsafeStatic } from 'lit/static-html.js';
import { AFFINE_SCROLL_ANCHORING_WIDGET } from './scroll-anchoring.js';
export * from './scroll-anchoring.js';
export const scrollAnchoringWidget = WidgetViewExtension(
'affine:page',
AFFINE_SCROLL_ANCHORING_WIDGET,
literal`${unsafeStatic(AFFINE_SCROLL_ANCHORING_WIDGET)}`
);

View File

@@ -63,5 +63,6 @@
"devDependencies": {
"@vanilla-extract/vite-plugin": "^5.0.0",
"vitest": "3.0.6"
}
},
"version": "0.20.0"
}

View File

@@ -48,5 +48,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -58,6 +58,8 @@ export {
prop,
} from './model/surface/local-element-model.js';
export {
SURFACE_TEXT_UNIQ_IDENTIFIER,
SURFACE_YMAP_UNIQ_IDENTIFIER,
SurfaceBlockModel,
type SurfaceBlockProps,
type SurfaceMiddleware,

View File

@@ -199,10 +199,6 @@ export abstract class GfxPrimitiveElementModel<
this.seed = randomSeed();
}
static propsToY(props: Record<string, unknown>) {
return props;
}
containsBound(bounds: Bound): boolean {
return getPointsFromBoundWithRotation(this).some(point =>
bounds.containsPoint(point)

View File

@@ -12,11 +12,20 @@ import { createDecoratorState } from './decorators/common.js';
import { initializeObservers, initializeWatchers } from './decorators/index.js';
import {
GfxGroupLikeElementModel,
GfxPrimitiveElementModel,
type GfxPrimitiveElementModel,
syncElementFromY,
} from './element-model.js';
import type { GfxLocalElementModel } from './local-element-model.js';
/**
* Used for text field
*/
export const SURFACE_TEXT_UNIQ_IDENTIFIER = 'affine:surface:text';
/**
* Used for field that use Y.Map. E.g. group children field
*/
export const SURFACE_YMAP_UNIQ_IDENTIFIER = 'affine:surface:ymap';
export type SurfaceBlockProps = {
elements: Boxed<Y.Map<Y.Map<unknown>>>;
};
@@ -390,8 +399,28 @@ export class SurfaceBlockModel extends BlockModel<SurfaceBlockProps> {
throw new Error(`Invalid element type: ${type}`);
}
Object.entries(props).forEach(([key, val]) => {
if (val instanceof Object) {
if (Reflect.has(val, SURFACE_TEXT_UNIQ_IDENTIFIER)) {
const yText = new Y.Text();
yText.applyDelta(Reflect.get(val, 'delta'));
Reflect.set(props, key, yText);
}
if (Reflect.has(val, SURFACE_YMAP_UNIQ_IDENTIFIER)) {
const childJson = Reflect.get(val, 'json') as Record<string, unknown>;
const childrenYMap = new Y.Map<unknown>();
Object.keys(childJson).forEach(childId => {
childrenYMap.set(childId, childJson[childId]);
});
Reflect.set(props, key, childrenYMap);
}
}
});
// @ts-expect-error ignore
return (ctor.propsToY ?? GfxPrimitiveElementModel.propsToY)(props);
return ctor.propsToY ? ctor.propsToY(props) : props;
}
private _watchGroupRelationChange() {

View File

@@ -51,5 +51,5 @@
"devDependencies": {
"vitest": "3.0.6"
},
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -34,5 +34,5 @@
"devDependencies": {
"vitest": "3.0.6"
},
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -46,5 +46,5 @@
"!dist/__tests__",
"shim.d.ts"
],
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -32,5 +32,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -28,7 +28,7 @@
"@lit/context": "^1.1.3",
"@lottiefiles/dotlottie-wc": "^0.4.0",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.11",
"@toeverything/theme": "^1.1.12",
"@vanilla-extract/css": "^1.17.0",
"lit": "^3.2.0",
"yjs": "^13.6.21",
@@ -52,5 +52,5 @@
"vite-plugin-wasm": "^3.4.1",
"vitest": "^3.0.0"
},
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -49,5 +49,5 @@
"vite-plugin-wasm": "^3.3.0",
"vite-plugin-web-components-hmr": "^0.1.3"
},
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -29,5 +29,5 @@
"type": "git",
"url": "https://github.com/toeverything/blocksuite.git"
},
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -19,5 +19,5 @@
],
"ext": "ts,md,json"
},
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@affine/monorepo",
"version": "0.19.0",
"version": "0.20.0",
"private": true,
"author": "toeverything",
"license": "MIT",

View File

@@ -1,6 +1,6 @@
{
"name": "@affine/server-native",
"version": "0.19.0",
"version": "0.20.0",
"engines": {
"node": ">= 10.16.0 < 11 || >= 11.8.0"
},

View File

@@ -1,7 +1,7 @@
{
"name": "@affine/server",
"private": true,
"version": "0.19.0",
"version": "0.20.0",
"description": "Affine Node.js server",
"type": "module",
"bin": {

View File

@@ -146,3 +146,26 @@ test('should tell downgrade if client version is higher than allowed', async t =
'Unsupported client with version [0.23.0], required version is [>=0.20.0 <=0.22.0].'
);
});
test('should test prerelease version', async t => {
runtime.fetch
.withArgs('client/versionControl.requiredVersion')
.resolves('>=0.19.0');
let res = await app
.GET('/guarded/test')
.set('x-affine-version', '0.19.0-canary.1');
// 0.19.0-canary.1 is lower than 0.19.0 obviously
t.is(res.status, 403);
res = await app
.GET('/guarded/test')
.set('x-affine-version', '0.20.0-canary.1');
t.is(res.status, 200);
res = await app.GET('/guarded/test').set('x-affine-version', '0.20.0-beta.2');
t.is(res.status, 200);
});

View File

@@ -299,6 +299,7 @@ export class AuthController {
res.send({ id: user.id });
}
@UseNamedGuard('version')
@Throttle('default', { limit: 1200 })
@Public()
@Get('/session')

View File

@@ -45,7 +45,9 @@ type EventResponse<Data = any> = Data extends never
data: Data;
};
type RoomType = 'sync' | `${string}:awareness`;
// 019 only receives space:broadcast-doc-updates and send space:push-doc-updates
// 020 only receives space:broadcast-doc-update and send space:push-doc-update
type RoomType = 'sync' | `${string}:awareness` | 'sync-019';
function Room(
spaceId: string,
@@ -214,7 +216,16 @@ export class SpaceSyncGateway
): Promise<EventResponse<{ clientId: string; success: true }>> {
await this.assertVersion(client, clientVersion);
await this.selectAdapter(client, spaceType).join(user.id, spaceId);
// TODO(@forehalo): remove this after 0.19 goes out of life
// simple match 0.19.x
if (/^0.19.[\d]$/.test(clientVersion)) {
const room = Room(spaceId, 'sync-019');
if (!client.rooms.has(room)) {
await client.join(room);
}
} else {
await this.selectAdapter(client, spaceType).join(user.id, spaceId);
}
return { data: { clientId: client.id, success: true } };
}
@@ -270,6 +281,8 @@ export class SpaceSyncGateway
/**
* @deprecated use [space:push-doc-update] instead, client should always merge updates on their own
*
* only 0.19.x client will send this event
*/
@SubscribeMessage('space:push-doc-updates')
async onReceiveDocUpdates(
@@ -289,23 +302,19 @@ export class SpaceSyncGateway
user.id
);
// could be put in [adapter.push]
// but the event should be kept away from adapter
// so
// broadcast to 0.19.x clients
client
.to(adapter.room(spaceId))
.to(Room(spaceId, 'sync-019'))
.emit('space:broadcast-doc-updates', { ...message, timestamp });
// TODO(@forehalo): remove backward compatibility
if (spaceType === SpaceType.Workspace) {
const id = new DocID(docId, spaceId);
client.to(adapter.room(spaceId)).emit('server-updates', {
workspaceId: spaceId,
guid: id.guid,
updates,
// broadcast to new clients
updates.forEach(update => {
client.to(adapter.room(spaceId)).emit('space:broadcast-doc-update', {
...message,
update,
timestamp,
});
}
});
return {
data: {
@@ -333,9 +342,8 @@ export class SpaceSyncGateway
user.id
);
// TODO(@forehalo): separate different version of clients into different rooms,
// so the clients won't receive useless updates events
client.to(adapter.room(spaceId)).emit('space:broadcast-doc-updates', {
// broadcast to 0.19.x clients
client.to(Room(spaceId, 'sync-019')).emit('space:broadcast-doc-updates', {
spaceType,
spaceId,
docId,
@@ -445,163 +453,8 @@ export class SpaceSyncGateway
.to(adapter.room(spaceId, roomType))
.emit('space:broadcast-awareness-update', message);
// TODO(@forehalo): remove backward compatibility
if (spaceType === SpaceType.Workspace) {
client
.to(adapter.room(spaceId, roomType))
.emit('server-awareness-broadcast', {
workspaceId: spaceId,
awarenessUpdate: message.awarenessUpdate,
});
}
return {};
}
// TODO(@forehalo): remove
// deprecated section
@SubscribeMessage('client-handshake-sync')
async handleClientHandshakeSync(
@CurrentUser() user: CurrentUser,
@MessageBody('workspaceId') workspaceId: string,
@MessageBody('version') version: string,
@ConnectedSocket() client: Socket
): Promise<EventResponse<{ clientId: string }>> {
await this.assertVersion(client, version);
return this.onJoinSpace(user, client, {
spaceType: SpaceType.Workspace,
spaceId: workspaceId,
clientVersion: version,
});
}
@SubscribeMessage('client-leave-sync')
async handleLeaveSync(
@MessageBody() workspaceId: string,
@ConnectedSocket() client: Socket
): Promise<EventResponse> {
return this.onLeaveSpace(client, {
spaceType: SpaceType.Workspace,
spaceId: workspaceId,
});
}
@SubscribeMessage('client-pre-sync')
async loadDocStats(
@ConnectedSocket() client: Socket,
@MessageBody()
{ workspaceId, timestamp }: { workspaceId: string; timestamp?: number }
): Promise<EventResponse<Record<string, number>>> {
return this.onLoadDocTimestamps(client, {
spaceType: SpaceType.Workspace,
spaceId: workspaceId,
timestamp,
});
}
@SubscribeMessage('client-update-v2')
async handleClientUpdateV2(
@CurrentUser() user: CurrentUser,
@MessageBody()
{
workspaceId,
guid,
updates,
}: {
workspaceId: string;
guid: string;
updates: string[];
},
@ConnectedSocket() client: Socket
): Promise<EventResponse<{ accepted: true; timestamp?: number }>> {
return this.onReceiveDocUpdates(client, user, {
spaceType: SpaceType.Workspace,
spaceId: workspaceId,
docId: guid,
updates,
});
}
@SubscribeMessage('doc-load-v2')
async loadDocV2(
@ConnectedSocket() client: Socket,
@MessageBody()
{
workspaceId,
guid,
stateVector,
}: {
workspaceId: string;
guid: string;
stateVector?: string;
}
): Promise<
EventResponse<{ missing: string; state?: string; timestamp: number }>
> {
return this.onLoadSpaceDoc(client, {
spaceType: SpaceType.Workspace,
spaceId: workspaceId,
docId: guid,
stateVector,
});
}
@SubscribeMessage('client-handshake-awareness')
async handleClientHandshakeAwareness(
@ConnectedSocket() client: Socket,
@CurrentUser() user: CurrentUser,
@MessageBody('workspaceId') workspaceId: string,
@MessageBody('version') version: string
): Promise<EventResponse<{ clientId: string }>> {
return this.onJoinAwareness(client, user, {
spaceType: SpaceType.Workspace,
spaceId: workspaceId,
docId: workspaceId,
clientVersion: version,
});
}
@SubscribeMessage('client-leave-awareness')
async handleLeaveAwareness(
@MessageBody() workspaceId: string,
@ConnectedSocket() client: Socket
): Promise<EventResponse> {
return this.onLeaveAwareness(client, {
spaceType: SpaceType.Workspace,
spaceId: workspaceId,
docId: workspaceId,
});
}
@SubscribeMessage('awareness-init')
async handleInitAwareness(
@MessageBody() workspaceId: string,
@ConnectedSocket() client: Socket
): Promise<EventResponse<{ clientId: string }>> {
return this.onLoadAwareness(client, {
spaceType: SpaceType.Workspace,
spaceId: workspaceId,
docId: workspaceId,
});
}
@SubscribeMessage('awareness-update')
async handleHelpGatheringAwareness(
@MessageBody()
{
workspaceId,
awarenessUpdate,
}: { workspaceId: string; awarenessUpdate: string },
@ConnectedSocket() client: Socket
): Promise<EventResponse> {
return this.onUpdateAwareness(client, {
spaceType: SpaceType.Workspace,
spaceId: workspaceId,
docId: workspaceId,
awarenessUpdate,
});
}
}
abstract class SyncSocketAdapter {
@@ -647,7 +500,8 @@ abstract class SyncSocketAdapter {
): Promise<void>;
push(spaceId: string, docId: string, updates: Buffer[], editorId: string) {
this.assertIn(spaceId);
// TODO(@forehalo): enable this after 0.19 goes out of life
// this.assertIn(spaceId);
return this.storage.pushDocUpdates(spaceId, docId, updates, editorId);
}

View File

@@ -20,7 +20,12 @@ export class VersionService {
return true;
}
if (!clientVersion || !semver.satisfies(clientVersion, range)) {
if (
!clientVersion ||
!semver.satisfies(clientVersion, range, {
includePrerelease: true,
})
) {
throw new UnsupportedClientVersion({
clientVersion: clientVersion ?? 'unset_or_invalid',
requiredVersion,

View File

@@ -11,5 +11,5 @@
"@types/debug": "^4.1.12",
"vitest": "3.0.6"
},
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -21,5 +21,5 @@
"dependencies": {
"zod": "^3.24.1"
},
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -44,5 +44,5 @@
"electron": "*",
"react-dom": "^19.0.0"
},
"version": "0.19.0"
"version": "0.20.0"
}

View File

@@ -1,7 +1,7 @@
{
"name": "@affine/nbstore",
"type": "module",
"version": "0.19.0",
"version": "0.20.0",
"private": true,
"sideEffects": false,
"exports": {

View File

@@ -61,6 +61,11 @@ export class AwarenessFrontend {
handleSyncCollect
);
awareness.once('destroy', () => {
awareness.off('update', handleAwarenessUpdate);
unsubscribe();
});
return () => {
awareness.off('update', handleAwarenessUpdate);
unsubscribe();

View File

@@ -15,8 +15,12 @@ export class BlobFrontend {
return this.sync.uploadBlob(blob);
}
fullSync() {
return this.sync.fullSync();
fullDownload() {
return this.sync.fullDownload();
}
fullUpload() {
return this.sync.fullUpload();
}
addPriority(_id: string, _priority: number) {

View File

@@ -399,31 +399,23 @@ export class DocFrontend {
this.statusUpdatedSubject$.next(job.docId);
}
/**
* skip listen doc update when apply update
*/
private skipDocUpdate = false;
applyUpdate(docId: string, update: Uint8Array) {
const doc = this.status.docs.get(docId);
if (doc && !isEmptyUpdate(update)) {
try {
this.skipDocUpdate = true;
applyUpdate(doc, update, NBSTORE_ORIGIN);
} catch (err) {
console.error('failed to apply update yjs doc', err);
} finally {
this.skipDocUpdate = false;
}
}
}
private readonly handleDocUpdate = (
update: Uint8Array,
_origin: any,
origin: any,
doc: YDoc
) => {
if (this.skipDocUpdate) {
if (origin === NBSTORE_ORIGIN) {
return;
}
if (!this.status.docs.has(doc.guid)) {
@@ -534,4 +526,8 @@ export class DocFrontend {
sub?.unsubscribe();
});
}
async resetSync() {
await this.sync.resetSync();
}
}

View File

@@ -9,6 +9,8 @@ import type { PeerStorageOptions } from '../types';
export interface BlobSyncState {
isStorageOverCapacity: boolean;
total: number;
synced: number;
}
export interface BlobSync {
@@ -18,7 +20,8 @@ export interface BlobSync {
signal?: AbortSignal
): Promise<BlobRecord | null>;
uploadBlob(blob: BlobRecord, signal?: AbortSignal): Promise<void>;
fullSync(signal?: AbortSignal): Promise<void>;
fullDownload(signal?: AbortSignal): Promise<void>;
fullUpload(signal?: AbortSignal): Promise<void>;
setMaxBlobSize(size: number): void;
onReachedMaxBlobSize(cb: (byteSize: number) => void): () => void;
}
@@ -26,6 +29,8 @@ export interface BlobSync {
export class BlobSyncImpl implements BlobSync {
readonly state$ = new BehaviorSubject<BlobSyncState>({
isStorageOverCapacity: false,
total: Object.values(this.storages.remotes).length ? 1 : 0,
synced: 0,
});
private abort: AbortController | null = null;
private maxBlobSize: number = 1024 * 1024 * 100; // 100MB
@@ -34,19 +39,24 @@ export class BlobSyncImpl implements BlobSync {
constructor(readonly storages: PeerStorageOptions<BlobStorage>) {}
async downloadBlob(blobId: string, signal?: AbortSignal) {
const localBlob = await this.storages.local.get(blobId, signal);
if (localBlob) {
return localBlob;
}
for (const storage of Object.values(this.storages.remotes)) {
const data = await storage.get(blobId, signal);
if (data) {
await this.storages.local.set(data, signal);
return data;
try {
const localBlob = await this.storages.local.get(blobId, signal);
if (localBlob) {
return localBlob;
}
for (const storage of Object.values(this.storages.remotes)) {
const data = await storage.get(blobId, signal);
if (data) {
await this.storages.local.set(data, signal);
return data;
}
}
return null;
} catch (e) {
console.error('error when download blob', e);
return null;
}
return null;
}
async uploadBlob(blob: BlobRecord, signal?: AbortSignal) {
@@ -62,7 +72,11 @@ export class BlobSyncImpl implements BlobSync {
return await remote.set(blob, signal);
} catch (err) {
if (err instanceof OverCapacityError) {
this.state$.next({ isStorageOverCapacity: true });
this.state$.next({
isStorageOverCapacity: true,
total: this.state$.value.total,
synced: this.state$.value.synced,
});
}
throw err;
}
@@ -70,71 +84,95 @@ export class BlobSyncImpl implements BlobSync {
);
}
async fullSync(signal?: AbortSignal) {
async fullDownload(signal?: AbortSignal) {
throwIfAborted(signal);
await this.storages.local.connection.waitForConnected(signal);
const localList = (await this.storages.local.list(signal)).map(b => b.key);
this.state$.next({
...this.state$.value,
synced: localList.length,
});
for (const [remotePeer, remote] of Object.entries(this.storages.remotes)) {
let localList: string[] = [];
let remoteList: string[] = [];
await Promise.allSettled(
Object.entries(this.storages.remotes).map(
async ([remotePeer, remote]) => {
await remote.connection.waitForConnected(signal);
await remote.connection.waitForConnected(signal);
const remoteList = (await remote.list(signal)).map(b => b.key);
try {
localList = (await this.storages.local.list(signal)).map(b => b.key);
throwIfAborted(signal);
remoteList = (await remote.list(signal)).map(b => b.key);
throwIfAborted(signal);
} catch (err) {
if (err === MANUALLY_STOP) {
throw err;
}
console.error(`error when sync`, err);
continue;
}
this.state$.next({
...this.state$.value,
total: Math.max(this.state$.value.total, remoteList.length),
});
const needUpload = difference(localList, remoteList);
for (const key of needUpload) {
try {
const data = await this.storages.local.get(key, signal);
throwIfAborted(signal);
if (data) {
await remote.set(data, signal);
throwIfAborted(signal);
const needDownload = difference(remoteList, localList);
for (const key of needDownload) {
try {
const data = await remote.get(key, signal);
throwIfAborted(signal);
if (data) {
await this.storages.local.set(data, signal);
this.state$.next({
...this.state$.value,
synced: this.state$.value.synced + 1,
});
throwIfAborted(signal);
}
} catch (err) {
if (err === MANUALLY_STOP) {
throw err;
}
console.error(
`error when sync ${key} from [${remotePeer}] to [local]`,
err
);
}
}
} catch (err) {
if (err === MANUALLY_STOP) {
throw err;
}
console.error(
`error when sync ${key} from [local] to [${remotePeer}]`,
err
);
}
}
)
);
}
const needDownload = difference(remoteList, localList);
async fullUpload(signal?: AbortSignal) {
throwIfAborted(signal);
await this.storages.local.connection.waitForConnected(signal);
const localList = (await this.storages.local.list(signal)).map(b => b.key);
await Promise.allSettled(
Object.entries(this.storages.remotes).map(
async ([remotePeer, remote]) => {
await remote.connection.waitForConnected(signal);
const remoteList = (await remote.list(signal)).map(b => b.key);
for (const key of needDownload) {
try {
const data = await remote.get(key, signal);
throwIfAborted(signal);
if (data) {
await this.storages.local.set(data, signal);
throwIfAborted(signal);
const needUpload = difference(localList, remoteList);
for (const key of needUpload) {
try {
const data = await this.storages.local.get(key, signal);
throwIfAborted(signal);
if (data) {
await remote.set(data, signal);
throwIfAborted(signal);
}
} catch (err) {
if (err === MANUALLY_STOP) {
throw err;
}
console.error(
`error when sync ${key} from [local] to [${remotePeer}]`,
err
);
}
}
} catch (err) {
if (err === MANUALLY_STOP) {
throw err;
}
console.error(
`error when sync ${key} from [${remotePeer}] to [local]`,
err
);
}
}
}
)
);
}
start() {
@@ -144,16 +182,12 @@ export class BlobSyncImpl implements BlobSync {
const abort = new AbortController();
this.abort = abort;
// TODO(@eyhn): fix this, large blob may cause iOS to crash?
if (!BUILD_CONFIG.isIOS) {
this.fullSync(abort.signal).catch(error => {
if (error === MANUALLY_STOP) {
return;
}
console.error('sync blob error', error);
});
}
this.fullUpload(abort.signal).catch(error => {
if (error === MANUALLY_STOP) {
return;
}
console.error('sync blob error', error);
});
}
stop() {

View File

@@ -27,6 +27,7 @@ export interface DocSync {
readonly state$: Observable<DocSyncState>;
docState$(docId: string): Observable<DocSyncDocState>;
addPriority(id: string, priority: number): () => void;
resetSync(): Promise<void>;
}
export class DocSyncImpl implements DocSync {
@@ -127,4 +128,13 @@ export class DocSyncImpl implements DocSync {
const undo = this.peers.map(peer => peer.addPriority(id, priority));
return () => undo.forEach(fn => fn());
}
async resetSync() {
const running = this.abort !== null;
this.stop();
await this.sync.clearClocks();
if (running) {
this.start();
}
}
}

View File

@@ -680,6 +680,7 @@ export class DocSyncPeer {
const cachedClocks = await this.syncMetadata.getPeerRemoteClocks(
this.peerId
);
this.status.remoteClocks.clear();
throwIfAborted(signal);
for (const [id, v] of Object.entries(cachedClocks)) {
this.status.remoteClocks.set(id, v);
@@ -690,8 +691,9 @@ export class DocSyncPeer {
const maxClockValue = this.status.remoteClocks.max;
const newClocks = await this.remote.getDocTimestamps(maxClockValue);
for (const [id, v] of Object.entries(newClocks)) {
this.actions.updateRemoteClock(id, v);
this.status.remoteClocks.set(id, v);
}
this.statusUpdatedSubject$.next(true);
for (const [id, v] of Object.entries(newClocks)) {
await this.syncMetadata.setPeerRemoteClock(this.peerId, {

View File

@@ -222,6 +222,10 @@ class WorkerDocSync implements DocSync {
subscription.unsubscribe();
};
}
resetSync(): Promise<void> {
return this.client.call('docSync.resetSync');
}
}
class WorkerBlobSync implements BlobSync {
@@ -253,26 +257,23 @@ class WorkerBlobSync implements BlobSync {
uploadBlob(blob: BlobRecord, _signal?: AbortSignal): Promise<void> {
return this.client.call('blobSync.uploadBlob', blob);
}
fullSync(signal?: AbortSignal): Promise<void> {
return new Promise((resolve, reject) => {
const abortListener = () => {
reject(signal?.reason);
subscription.unsubscribe();
};
fullDownload(signal?: AbortSignal): Promise<void> {
const download = this.client.call('blobSync.fullDownload');
signal?.addEventListener('abort', abortListener);
const subscription = this.client.ob$('blobSync.fullSync').subscribe({
next() {
signal?.removeEventListener('abort', abortListener);
resolve();
},
error(err) {
signal?.removeEventListener('abort', abortListener);
reject(err);
},
});
signal?.addEventListener('abort', () => {
download.cancel();
});
return download;
}
fullUpload(signal?: AbortSignal): Promise<void> {
const upload = this.client.call('blobSync.fullUpload');
signal?.addEventListener('abort', () => {
upload.cancel();
});
return upload;
}
}

View File

@@ -231,22 +231,13 @@ class StoreConsumer {
const undo = this.docSync.addPriority(docId, priority);
return () => undo();
}),
'docSync.resetSync': () => this.docSync.resetSync(),
'blobSync.downloadBlob': key => this.blobSync.downloadBlob(key),
'blobSync.uploadBlob': blob => this.blobSync.uploadBlob(blob),
'blobSync.fullSync': () =>
new Observable(subscriber => {
const abortController = new AbortController();
this.blobSync
.fullSync(abortController.signal)
.then(() => {
subscriber.next(true);
subscriber.complete();
})
.catch(error => {
subscriber.error(error);
});
return () => abortController.abort(MANUALLY_STOP);
}),
'blobSync.fullDownload': (_, { signal }) =>
this.blobSync.fullDownload(signal),
'blobSync.fullUpload': (_, { signal }) =>
this.blobSync.fullUpload(signal),
'blobSync.state': () => this.blobSync.state$,
'blobSync.setMaxBlobSize': size => this.blobSync.setMaxBlobSize(size),
'blobSync.onReachedMaxBlobSize': () =>

View File

@@ -81,12 +81,14 @@ interface GroupedWorkerOps {
state: [void, DocSyncState];
docState: [string, DocSyncDocState];
addPriority: [{ docId: string; priority: number }, boolean];
resetSync: [void, void];
};
blobSync: {
downloadBlob: [string, BlobRecord | null];
uploadBlob: [BlobRecord, void];
fullSync: [void, boolean];
fullDownload: [void, void];
fullUpload: [void, void];
setMaxBlobSize: [number, void];
onReachedMaxBlobSize: [void, number];
state: [void, BlobSyncState];

View File

@@ -1,6 +1,6 @@
{
"name": "@affine/admin",
"version": "0.19.0",
"version": "0.20.0",
"private": true,
"dependencies": {
"@affine/component": "workspace:*",

View File

@@ -1,6 +1,6 @@
{
"name": "@affine/android",
"version": "0.19.0",
"version": "0.20.0",
"description": "AFFiNE Desktop Web application",
"private": true,
"scripts": {

View File

@@ -1,7 +1,7 @@
{
"name": "@affine/electron-renderer",
"private": true,
"version": "0.19.0",
"version": "0.20.0",
"type": "module",
"scripts": {
"build": "affine bundle",
@@ -16,7 +16,7 @@
"@emotion/react": "^11.14.0",
"@sentry/react": "^8.44.0",
"@toeverything/infra": "workspace:*",
"@toeverything/theme": "^1.1.11",
"@toeverything/theme": "^1.1.12",
"@vanilla-extract/css": "^1.17.0",
"async-call-rpc": "^6.4.2",
"next-themes": "^0.4.4",

View File

@@ -1,7 +1,7 @@
{
"name": "@affine/electron",
"private": true,
"version": "0.19.0",
"version": "0.20.0",
"main": "./dist/main.js",
"author": "toeverything",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@affine/ios",
"version": "0.19.0",
"version": "0.20.0",
"description": "AFFiNE Desktop Web application",
"private": true,
"browser": "src/index.tsx",

View File

@@ -1,6 +1,6 @@
{
"name": "@affine/mobile",
"version": "0.19.0",
"version": "0.20.0",
"description": "AFFiNE Desktop Web application",
"private": true,
"browser": "src/index.tsx",

Some files were not shown because too many files have changed in this diff Show More