mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-04 08:38:34 +00:00
feat(editor): comment extension (#12948)
#### PR Dependency Tree * **PR #12948** 👈 * **PR #12980** This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Introduced inline comment functionality, allowing users to add, resolve, and highlight comments directly within text. * Added a new toolbar action for inserting comments when supported. * Inline comments are visually highlighted and can be interacted with in the editor. * **Enhancements** * Integrated a feature flag to enable or disable the comment feature. * Improved inline manager rendering to support wrapper specs for advanced formatting. * **Developer Tools** * Added mock comment provider for testing and development environments. * **Chores** * Updated dependencies and project references to support the new inline comment module. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -48,6 +48,7 @@
|
|||||||
"@blocksuite/affine-gfx-template": "workspace:*",
|
"@blocksuite/affine-gfx-template": "workspace:*",
|
||||||
"@blocksuite/affine-gfx-text": "workspace:*",
|
"@blocksuite/affine-gfx-text": "workspace:*",
|
||||||
"@blocksuite/affine-gfx-turbo-renderer": "workspace:*",
|
"@blocksuite/affine-gfx-turbo-renderer": "workspace:*",
|
||||||
|
"@blocksuite/affine-inline-comment": "workspace:*",
|
||||||
"@blocksuite/affine-inline-footnote": "workspace:*",
|
"@blocksuite/affine-inline-footnote": "workspace:*",
|
||||||
"@blocksuite/affine-inline-latex": "workspace:*",
|
"@blocksuite/affine-inline-latex": "workspace:*",
|
||||||
"@blocksuite/affine-inline-link": "workspace:*",
|
"@blocksuite/affine-inline-link": "workspace:*",
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import { PointerViewExtension } from '@blocksuite/affine-gfx-pointer/view';
|
|||||||
import { ShapeViewExtension } from '@blocksuite/affine-gfx-shape/view';
|
import { ShapeViewExtension } from '@blocksuite/affine-gfx-shape/view';
|
||||||
import { TemplateViewExtension } from '@blocksuite/affine-gfx-template/view';
|
import { TemplateViewExtension } from '@blocksuite/affine-gfx-template/view';
|
||||||
import { TextViewExtension } from '@blocksuite/affine-gfx-text/view';
|
import { TextViewExtension } from '@blocksuite/affine-gfx-text/view';
|
||||||
|
import { InlineCommentViewExtension } from '@blocksuite/affine-inline-comment/view';
|
||||||
import { FootnoteViewExtension } from '@blocksuite/affine-inline-footnote/view';
|
import { FootnoteViewExtension } from '@blocksuite/affine-inline-footnote/view';
|
||||||
import { LatexViewExtension as InlineLatexViewExtension } from '@blocksuite/affine-inline-latex/view';
|
import { LatexViewExtension as InlineLatexViewExtension } from '@blocksuite/affine-inline-latex/view';
|
||||||
import { LinkViewExtension } from '@blocksuite/affine-inline-link/view';
|
import { LinkViewExtension } from '@blocksuite/affine-inline-link/view';
|
||||||
@@ -95,6 +96,7 @@ export function getInternalViewExtensions() {
|
|||||||
RootViewExtension,
|
RootViewExtension,
|
||||||
|
|
||||||
// Inline
|
// Inline
|
||||||
|
InlineCommentViewExtension,
|
||||||
FootnoteViewExtension,
|
FootnoteViewExtension,
|
||||||
LinkViewExtension,
|
LinkViewExtension,
|
||||||
ReferenceViewExtension,
|
ReferenceViewExtension,
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
{ "path": "../gfx/template" },
|
{ "path": "../gfx/template" },
|
||||||
{ "path": "../gfx/text" },
|
{ "path": "../gfx/text" },
|
||||||
{ "path": "../gfx/turbo-renderer" },
|
{ "path": "../gfx/turbo-renderer" },
|
||||||
|
{ "path": "../inlines/comment" },
|
||||||
{ "path": "../inlines/footnote" },
|
{ "path": "../inlines/footnote" },
|
||||||
{ "path": "../inlines/latex" },
|
{ "path": "../inlines/latex" },
|
||||||
{ "path": "../inlines/link" },
|
{ "path": "../inlines/link" },
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"@blocksuite/affine-components": "workspace:*",
|
"@blocksuite/affine-components": "workspace:*",
|
||||||
"@blocksuite/affine-ext-loader": "workspace:*",
|
"@blocksuite/affine-ext-loader": "workspace:*",
|
||||||
"@blocksuite/affine-gfx-turbo-renderer": "workspace:*",
|
"@blocksuite/affine-gfx-turbo-renderer": "workspace:*",
|
||||||
|
"@blocksuite/affine-inline-comment": "workspace:*",
|
||||||
"@blocksuite/affine-inline-latex": "workspace:*",
|
"@blocksuite/affine-inline-latex": "workspace:*",
|
||||||
"@blocksuite/affine-inline-link": "workspace:*",
|
"@blocksuite/affine-inline-link": "workspace:*",
|
||||||
"@blocksuite/affine-inline-preset": "workspace:*",
|
"@blocksuite/affine-inline-preset": "workspace:*",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { CommentInlineSpecExtension } from '@blocksuite/affine-inline-comment';
|
||||||
import { LatexInlineSpecExtension } from '@blocksuite/affine-inline-latex';
|
import { LatexInlineSpecExtension } from '@blocksuite/affine-inline-latex';
|
||||||
import { LinkInlineSpecExtension } from '@blocksuite/affine-inline-link';
|
import { LinkInlineSpecExtension } from '@blocksuite/affine-inline-link';
|
||||||
import {
|
import {
|
||||||
@@ -44,5 +45,6 @@ export const CodeBlockInlineManagerExtension =
|
|||||||
LatexInlineSpecExtension.identifier,
|
LatexInlineSpecExtension.identifier,
|
||||||
LinkInlineSpecExtension.identifier,
|
LinkInlineSpecExtension.identifier,
|
||||||
CodeBlockUnitSpecExtension.identifier,
|
CodeBlockUnitSpecExtension.identifier,
|
||||||
|
CommentInlineSpecExtension.identifier,
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
{ "path": "../../components" },
|
{ "path": "../../components" },
|
||||||
{ "path": "../../ext-loader" },
|
{ "path": "../../ext-loader" },
|
||||||
{ "path": "../../gfx/turbo-renderer" },
|
{ "path": "../../gfx/turbo-renderer" },
|
||||||
|
{ "path": "../../inlines/comment" },
|
||||||
{ "path": "../../inlines/latex" },
|
{ "path": "../../inlines/latex" },
|
||||||
{ "path": "../../inlines/link" },
|
{ "path": "../../inlines/link" },
|
||||||
{ "path": "../../inlines/preset" },
|
{ "path": "../../inlines/preset" },
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ function toggleStyle(
|
|||||||
return [k, v];
|
return [k, v];
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
) as AffineTextAttributes;
|
||||||
|
|
||||||
inlineEditor.formatText(inlineRange, newAttributes, {
|
inlineEditor.formatText(inlineRange, newAttributes, {
|
||||||
mode: 'merge',
|
mode: 'merge',
|
||||||
|
|||||||
@@ -38,9 +38,13 @@ import type {
|
|||||||
ToolbarActionGroup,
|
ToolbarActionGroup,
|
||||||
ToolbarModuleConfig,
|
ToolbarModuleConfig,
|
||||||
} from '@blocksuite/affine-shared/services';
|
} from '@blocksuite/affine-shared/services';
|
||||||
import { ActionPlacement } from '@blocksuite/affine-shared/services';
|
import {
|
||||||
|
ActionPlacement,
|
||||||
|
CommentProviderIdentifier,
|
||||||
|
} from '@blocksuite/affine-shared/services';
|
||||||
import { tableViewMeta } from '@blocksuite/data-view/view-presets';
|
import { tableViewMeta } from '@blocksuite/data-view/view-presets';
|
||||||
import {
|
import {
|
||||||
|
CommentIcon,
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
DatabaseTableViewIcon,
|
DatabaseTableViewIcon,
|
||||||
DeleteIcon,
|
DeleteIcon,
|
||||||
@@ -161,7 +165,7 @@ const highlightActionGroup = {
|
|||||||
} as const satisfies ToolbarAction;
|
} as const satisfies ToolbarAction;
|
||||||
|
|
||||||
const turnIntoDatabase = {
|
const turnIntoDatabase = {
|
||||||
id: 'd.convert-to-database',
|
id: 'e.convert-to-database',
|
||||||
tooltip: 'Create Table',
|
tooltip: 'Create Table',
|
||||||
icon: DatabaseTableViewIcon(),
|
icon: DatabaseTableViewIcon(),
|
||||||
when({ chain }) {
|
when({ chain }) {
|
||||||
@@ -208,7 +212,7 @@ const turnIntoDatabase = {
|
|||||||
} as const satisfies ToolbarAction;
|
} as const satisfies ToolbarAction;
|
||||||
|
|
||||||
const turnIntoLinkedDoc = {
|
const turnIntoLinkedDoc = {
|
||||||
id: 'e.convert-to-linked-doc',
|
id: 'f.convert-to-linked-doc',
|
||||||
tooltip: 'Create Linked Doc',
|
tooltip: 'Create Linked Doc',
|
||||||
icon: LinkedPageIcon(),
|
icon: LinkedPageIcon(),
|
||||||
when({ chain }) {
|
when({ chain }) {
|
||||||
@@ -266,11 +270,26 @@ const turnIntoLinkedDoc = {
|
|||||||
},
|
},
|
||||||
} as const satisfies ToolbarAction;
|
} as const satisfies ToolbarAction;
|
||||||
|
|
||||||
|
const commentAction = {
|
||||||
|
id: 'd.comment',
|
||||||
|
when: ({ std, chain }) =>
|
||||||
|
isFormatSupported(chain).run()[0] &&
|
||||||
|
!!std.getOptional(CommentProviderIdentifier),
|
||||||
|
icon: CommentIcon(),
|
||||||
|
run: ({ std }) => {
|
||||||
|
const commentProvider = std.getOptional(CommentProviderIdentifier);
|
||||||
|
if (!commentProvider) return;
|
||||||
|
|
||||||
|
commentProvider.addComment(std.selection.value);
|
||||||
|
},
|
||||||
|
} as const satisfies ToolbarAction;
|
||||||
|
|
||||||
export const builtinToolbarConfig = {
|
export const builtinToolbarConfig = {
|
||||||
actions: [
|
actions: [
|
||||||
conversionsActionGroup,
|
conversionsActionGroup,
|
||||||
inlineTextActionGroup,
|
inlineTextActionGroup,
|
||||||
highlightActionGroup,
|
highlightActionGroup,
|
||||||
|
commentAction,
|
||||||
turnIntoDatabase,
|
turnIntoDatabase,
|
||||||
turnIntoLinkedDoc,
|
turnIntoLinkedDoc,
|
||||||
{
|
{
|
||||||
|
|||||||
46
blocksuite/affine/inlines/comment/package.json
Normal file
46
blocksuite/affine/inlines/comment/package.json
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"name": "@blocksuite/affine-inline-comment",
|
||||||
|
"description": "Inline comment for BlockSuite.",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc"
|
||||||
|
},
|
||||||
|
"sideEffects": false,
|
||||||
|
"keywords": [],
|
||||||
|
"author": "toeverything",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@blocksuite/affine-ext-loader": "workspace:*",
|
||||||
|
"@blocksuite/affine-model": "workspace:*",
|
||||||
|
"@blocksuite/affine-rich-text": "workspace:*",
|
||||||
|
"@blocksuite/affine-shared": "workspace:*",
|
||||||
|
"@blocksuite/global": "workspace:*",
|
||||||
|
"@blocksuite/std": "workspace:*",
|
||||||
|
"@blocksuite/store": "workspace:*",
|
||||||
|
"@lit/context": "^1.1.2",
|
||||||
|
"@preact/signals-core": "^1.8.0",
|
||||||
|
"@toeverything/theme": "^1.1.15",
|
||||||
|
"@types/lodash-es": "^4.17.12",
|
||||||
|
"lit": "^3.2.0",
|
||||||
|
"lit-html": "^3.2.1",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
|
"rxjs": "^7.8.1",
|
||||||
|
"yjs": "^13.6.21",
|
||||||
|
"zod": "^3.23.8"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vitest": "3.1.3"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts",
|
||||||
|
"./view": "./src/view.ts",
|
||||||
|
"./store": "./src/store.ts"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src",
|
||||||
|
"dist",
|
||||||
|
"!src/__tests__",
|
||||||
|
"!dist/__tests__"
|
||||||
|
],
|
||||||
|
"version": "0.21.0"
|
||||||
|
}
|
||||||
11
blocksuite/affine/inlines/comment/src/effects.ts
Normal file
11
blocksuite/affine/inlines/comment/src/effects.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { InlineComment } from './inline-comment';
|
||||||
|
|
||||||
|
export function effects() {
|
||||||
|
customElements.define('inline-comment', InlineComment);
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'inline-comment': InlineComment;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
blocksuite/affine/inlines/comment/src/index.ts
Normal file
1
blocksuite/affine/inlines/comment/src/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './inline-spec';
|
||||||
161
blocksuite/affine/inlines/comment/src/inline-comment-manager.ts
Normal file
161
blocksuite/affine/inlines/comment/src/inline-comment-manager.ts
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import { getInlineEditorByModel } from '@blocksuite/affine-rich-text';
|
||||||
|
import { getSelectedBlocksCommand } from '@blocksuite/affine-shared/commands';
|
||||||
|
import {
|
||||||
|
type CommentId,
|
||||||
|
CommentProviderIdentifier,
|
||||||
|
} from '@blocksuite/affine-shared/services';
|
||||||
|
import type { AffineInlineEditor } from '@blocksuite/affine-shared/types';
|
||||||
|
import { DisposableGroup } from '@blocksuite/global/disposable';
|
||||||
|
import {
|
||||||
|
LifeCycleWatcher,
|
||||||
|
type TextRangePoint,
|
||||||
|
TextSelection,
|
||||||
|
} from '@blocksuite/std';
|
||||||
|
import type { BaseSelection, BlockModel } from '@blocksuite/store';
|
||||||
|
|
||||||
|
import { extractCommentIdFromDelta, findCommentedTexts } from './utils';
|
||||||
|
|
||||||
|
export class InlineCommentManager extends LifeCycleWatcher {
|
||||||
|
static override key = 'inline-comment-manager';
|
||||||
|
|
||||||
|
private readonly _disposables = new DisposableGroup();
|
||||||
|
|
||||||
|
private get _provider() {
|
||||||
|
return this.std.getOptional(CommentProviderIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
override mounted() {
|
||||||
|
const provider = this._provider;
|
||||||
|
if (!provider) return;
|
||||||
|
|
||||||
|
this._disposables.add(provider.onCommentAdded(this._handleAddComment));
|
||||||
|
this._disposables.add(
|
||||||
|
provider.onCommentDeleted(this._handleDeleteAndResolve)
|
||||||
|
);
|
||||||
|
this._disposables.add(
|
||||||
|
provider.onCommentResolved(this._handleDeleteAndResolve)
|
||||||
|
);
|
||||||
|
this._disposables.add(
|
||||||
|
this.std.selection.slots.changed.subscribe(this._handleSelectionChanged)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
override unmounted() {
|
||||||
|
this._disposables.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly _handleAddComment = (
|
||||||
|
id: CommentId,
|
||||||
|
selections: BaseSelection[]
|
||||||
|
) => {
|
||||||
|
const needCommentTexts = selections
|
||||||
|
.map(selection => {
|
||||||
|
if (!selection.is(TextSelection)) return [];
|
||||||
|
const [_, { selectedBlocks }] = this.std.command
|
||||||
|
.chain()
|
||||||
|
.pipe(getSelectedBlocksCommand, {
|
||||||
|
textSelection: selection,
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
|
||||||
|
if (!selectedBlocks) return [];
|
||||||
|
|
||||||
|
type MakeRequired<T, K extends keyof T> = T & {
|
||||||
|
[key in K]: NonNullable<T[key]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
return selectedBlocks
|
||||||
|
.map(
|
||||||
|
({ model }) =>
|
||||||
|
[model, getInlineEditorByModel(this.std, model)] as const
|
||||||
|
)
|
||||||
|
.filter(
|
||||||
|
(
|
||||||
|
pair
|
||||||
|
): pair is [MakeRequired<BlockModel, 'text'>, AffineInlineEditor] =>
|
||||||
|
!!pair[0].text && !!pair[1]
|
||||||
|
)
|
||||||
|
.map(([model, inlineEditor]) => {
|
||||||
|
let from: TextRangePoint;
|
||||||
|
let to: TextRangePoint | null;
|
||||||
|
if (model.id === selection.from.blockId) {
|
||||||
|
from = selection.from;
|
||||||
|
to = null;
|
||||||
|
} else if (model.id === selection.to?.blockId) {
|
||||||
|
from = selection.to;
|
||||||
|
to = null;
|
||||||
|
} else {
|
||||||
|
from = {
|
||||||
|
blockId: model.id,
|
||||||
|
index: 0,
|
||||||
|
length: model.text.yText.length,
|
||||||
|
};
|
||||||
|
to = null;
|
||||||
|
}
|
||||||
|
return [new TextSelection({ from, to }), inlineEditor] as const;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.flat();
|
||||||
|
|
||||||
|
if (needCommentTexts.length === 0) return;
|
||||||
|
|
||||||
|
needCommentTexts.forEach(([selection, inlineEditor]) => {
|
||||||
|
inlineEditor.formatText(
|
||||||
|
selection.from,
|
||||||
|
{
|
||||||
|
[`comment-${id}`]: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
withoutTransact: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly _handleDeleteAndResolve = (id: CommentId) => {
|
||||||
|
const commentedTexts = findCommentedTexts(this.std, id);
|
||||||
|
if (commentedTexts.length === 0) return;
|
||||||
|
|
||||||
|
this.std.store.withoutTransact(() => {
|
||||||
|
commentedTexts.forEach(([selection, inlineEditor]) => {
|
||||||
|
inlineEditor.formatText(
|
||||||
|
selection.from,
|
||||||
|
{
|
||||||
|
[`comment-${id}`]: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
withoutTransact: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly _handleSelectionChanged = (selections: BaseSelection[]) => {
|
||||||
|
if (selections.length === 1) {
|
||||||
|
const selection = selections[0];
|
||||||
|
|
||||||
|
// InlineCommentManager only handle text selection
|
||||||
|
if (!selection.is(TextSelection)) return;
|
||||||
|
|
||||||
|
if (!selection.isCollapsed()) {
|
||||||
|
this._provider?.highlightComment(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const model = this.std.store.getModelById(selection.from.blockId);
|
||||||
|
if (!model) return;
|
||||||
|
|
||||||
|
const inlineEditor = getInlineEditorByModel(this.std, model);
|
||||||
|
if (!inlineEditor) return;
|
||||||
|
|
||||||
|
const delta = inlineEditor.getDeltaByRangeIndex(selection.from.index);
|
||||||
|
if (!delta) return;
|
||||||
|
|
||||||
|
const commentIds = extractCommentIdFromDelta(delta);
|
||||||
|
if (commentIds.length !== 0) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._provider?.highlightComment(null);
|
||||||
|
};
|
||||||
|
}
|
||||||
95
blocksuite/affine/inlines/comment/src/inline-comment.ts
Normal file
95
blocksuite/affine/inlines/comment/src/inline-comment.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import {
|
||||||
|
type CommentId,
|
||||||
|
CommentProviderIdentifier,
|
||||||
|
} from '@blocksuite/affine-shared/services';
|
||||||
|
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||||
|
import { WithDisposable } from '@blocksuite/global/lit';
|
||||||
|
import {
|
||||||
|
type BlockStdScope,
|
||||||
|
PropTypes,
|
||||||
|
requiredProperties,
|
||||||
|
ShadowlessElement,
|
||||||
|
stdContext,
|
||||||
|
} from '@blocksuite/std';
|
||||||
|
import { consume } from '@lit/context';
|
||||||
|
import { css, type PropertyValues } from 'lit';
|
||||||
|
import { property, state } from 'lit/decorators.js';
|
||||||
|
import { html } from 'lit-html';
|
||||||
|
import { isEqual } from 'lodash-es';
|
||||||
|
|
||||||
|
@requiredProperties({
|
||||||
|
commentIds: PropTypes.arrayOf(id => typeof id === 'string'),
|
||||||
|
})
|
||||||
|
export class InlineComment extends WithDisposable(ShadowlessElement) {
|
||||||
|
static override styles = css`
|
||||||
|
inline-comment {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: ${unsafeCSSVarV2('block/comment/highlightDefault')};
|
||||||
|
border-bottom: 2px solid
|
||||||
|
${unsafeCSSVarV2('block/comment/highlightUnderline')};
|
||||||
|
}
|
||||||
|
|
||||||
|
inline-comment.highlighted {
|
||||||
|
background-color: ${unsafeCSSVarV2('block/comment/highlightActive')};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
attribute: false,
|
||||||
|
hasChanged: (newVal: string[], oldVal: string[]) =>
|
||||||
|
!isEqual(newVal, oldVal),
|
||||||
|
})
|
||||||
|
accessor commentIds!: string[];
|
||||||
|
|
||||||
|
@consume({ context: stdContext })
|
||||||
|
private accessor _std!: BlockStdScope;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor highlighted = false;
|
||||||
|
|
||||||
|
private get _provider() {
|
||||||
|
return this._std.getOptional(CommentProviderIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly _handleClick = () => {
|
||||||
|
const provider = this._provider;
|
||||||
|
provider && this.commentIds.forEach(id => provider.highlightComment(id));
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly _handleHighlight = (id: CommentId | null) => {
|
||||||
|
if (this.highlighted) {
|
||||||
|
if (!id || !this.commentIds.includes(id)) {
|
||||||
|
this.highlighted = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (id && this.commentIds.includes(id)) {
|
||||||
|
this.highlighted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
override connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
const provider = this._provider;
|
||||||
|
if (provider) {
|
||||||
|
this.disposables.addFromEvent(this, 'click', this._handleClick);
|
||||||
|
this.disposables.add(
|
||||||
|
provider.onCommentHighlighted(this._handleHighlight)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override willUpdate(_changedProperties: PropertyValues<this>) {
|
||||||
|
if (_changedProperties.has('highlighted')) {
|
||||||
|
if (this.highlighted) {
|
||||||
|
this.classList.add('highlighted');
|
||||||
|
} else {
|
||||||
|
this.classList.remove('highlighted');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override render() {
|
||||||
|
return html`<slot></slot>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
38
blocksuite/affine/inlines/comment/src/inline-spec.ts
Normal file
38
blocksuite/affine/inlines/comment/src/inline-spec.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { type CommentId } from '@blocksuite/affine-shared/services';
|
||||||
|
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
|
||||||
|
import { dynamicSchema, InlineSpecExtension } from '@blocksuite/std/inline';
|
||||||
|
import { html, nothing } from 'lit-html';
|
||||||
|
import { when } from 'lit-html/directives/when.js';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { extractCommentIdFromDelta } from './utils';
|
||||||
|
|
||||||
|
type InlineCommendId = `comment-${CommentId}`;
|
||||||
|
function isInlineCommendId(key: string): key is InlineCommendId {
|
||||||
|
return key.startsWith('comment-');
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CommentInlineSpecExtension =
|
||||||
|
InlineSpecExtension<AffineTextAttributes>({
|
||||||
|
name: 'comment',
|
||||||
|
schema: dynamicSchema(
|
||||||
|
isInlineCommendId,
|
||||||
|
z.boolean().optional().nullable().catch(undefined)
|
||||||
|
),
|
||||||
|
match: delta => {
|
||||||
|
if (!delta.attributes) return false;
|
||||||
|
const comments = Object.entries(delta.attributes).filter(
|
||||||
|
([key, value]) => isInlineCommendId(key) && value === true
|
||||||
|
);
|
||||||
|
return comments.length > 0;
|
||||||
|
},
|
||||||
|
renderer: ({ delta, children }) =>
|
||||||
|
html`<inline-comment .commentIds=${extractCommentIdFromDelta(delta)}
|
||||||
|
>${when(
|
||||||
|
children,
|
||||||
|
() => html`${children}`,
|
||||||
|
() => nothing
|
||||||
|
)}</inline-comment
|
||||||
|
>`,
|
||||||
|
wrapper: true,
|
||||||
|
});
|
||||||
53
blocksuite/affine/inlines/comment/src/utils.ts
Normal file
53
blocksuite/affine/inlines/comment/src/utils.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { getInlineEditorByModel } from '@blocksuite/affine-rich-text';
|
||||||
|
import type { CommentId } from '@blocksuite/affine-shared/services';
|
||||||
|
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
|
||||||
|
import { type BlockStdScope, TextSelection } from '@blocksuite/std';
|
||||||
|
import type { InlineEditor } from '@blocksuite/std/inline';
|
||||||
|
import type { DeltaInsert } from '@blocksuite/store';
|
||||||
|
|
||||||
|
export function findCommentedTexts(std: BlockStdScope, commentId: CommentId) {
|
||||||
|
const selections: [TextSelection, InlineEditor][] = [];
|
||||||
|
std.store.getAllModels().forEach(model => {
|
||||||
|
const inlineEditor = getInlineEditorByModel(std, model);
|
||||||
|
if (!inlineEditor) return;
|
||||||
|
|
||||||
|
inlineEditor.mapDeltasInInlineRange(
|
||||||
|
{
|
||||||
|
index: 0,
|
||||||
|
length: inlineEditor.yTextLength,
|
||||||
|
},
|
||||||
|
(delta, rangeIndex) => {
|
||||||
|
if (
|
||||||
|
delta.attributes &&
|
||||||
|
Object.keys(delta.attributes).some(
|
||||||
|
key => key === `comment-${commentId}`
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
selections.push([
|
||||||
|
new TextSelection({
|
||||||
|
from: {
|
||||||
|
blockId: model.id,
|
||||||
|
index: rangeIndex,
|
||||||
|
length: delta.insert.length,
|
||||||
|
},
|
||||||
|
to: null,
|
||||||
|
}),
|
||||||
|
inlineEditor,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return selections;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractCommentIdFromDelta(
|
||||||
|
delta: DeltaInsert<AffineTextAttributes>
|
||||||
|
) {
|
||||||
|
if (!delta.attributes) return [];
|
||||||
|
|
||||||
|
return Object.keys(delta.attributes)
|
||||||
|
.filter(key => key.startsWith('comment-'))
|
||||||
|
.map(key => key.replace('comment-', ''));
|
||||||
|
}
|
||||||
22
blocksuite/affine/inlines/comment/src/view.ts
Normal file
22
blocksuite/affine/inlines/comment/src/view.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import {
|
||||||
|
type ViewExtensionContext,
|
||||||
|
ViewExtensionProvider,
|
||||||
|
} from '@blocksuite/affine-ext-loader';
|
||||||
|
|
||||||
|
import { effects } from './effects';
|
||||||
|
import { InlineCommentManager } from './inline-comment-manager';
|
||||||
|
import { CommentInlineSpecExtension } from './inline-spec';
|
||||||
|
|
||||||
|
export class InlineCommentViewExtension extends ViewExtensionProvider {
|
||||||
|
override name = 'affine-inline-comment';
|
||||||
|
|
||||||
|
override effect(): void {
|
||||||
|
super.effect();
|
||||||
|
effects();
|
||||||
|
}
|
||||||
|
|
||||||
|
override setup(context: ViewExtensionContext) {
|
||||||
|
super.setup(context);
|
||||||
|
context.register([CommentInlineSpecExtension, InlineCommentManager]);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
blocksuite/affine/inlines/comment/tsconfig.json
Normal file
18
blocksuite/affine/inlines/comment/tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "./src",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
|
||||||
|
},
|
||||||
|
"include": ["./src"],
|
||||||
|
"references": [
|
||||||
|
{ "path": "../../ext-loader" },
|
||||||
|
{ "path": "../../model" },
|
||||||
|
{ "path": "../../rich-text" },
|
||||||
|
{ "path": "../../shared" },
|
||||||
|
{ "path": "../../../framework/global" },
|
||||||
|
{ "path": "../../../framework/std" },
|
||||||
|
{ "path": "../../../framework/store" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@blocksuite/affine-components": "workspace:*",
|
"@blocksuite/affine-components": "workspace:*",
|
||||||
"@blocksuite/affine-ext-loader": "workspace:*",
|
"@blocksuite/affine-ext-loader": "workspace:*",
|
||||||
|
"@blocksuite/affine-inline-comment": "workspace:*",
|
||||||
"@blocksuite/affine-inline-footnote": "workspace:*",
|
"@blocksuite/affine-inline-footnote": "workspace:*",
|
||||||
"@blocksuite/affine-inline-latex": "workspace:*",
|
"@blocksuite/affine-inline-latex": "workspace:*",
|
||||||
"@blocksuite/affine-inline-link": "workspace:*",
|
"@blocksuite/affine-inline-link": "workspace:*",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { CommentInlineSpecExtension } from '@blocksuite/affine-inline-comment';
|
||||||
import { FootNoteInlineSpecExtension } from '@blocksuite/affine-inline-footnote';
|
import { FootNoteInlineSpecExtension } from '@blocksuite/affine-inline-footnote';
|
||||||
import { LatexInlineSpecExtension } from '@blocksuite/affine-inline-latex';
|
import { LatexInlineSpecExtension } from '@blocksuite/affine-inline-latex';
|
||||||
import { LinkInlineSpecExtension } from '@blocksuite/affine-inline-link';
|
import { LinkInlineSpecExtension } from '@blocksuite/affine-inline-link';
|
||||||
@@ -32,5 +33,6 @@ export const DefaultInlineManagerExtension =
|
|||||||
LinkInlineSpecExtension.identifier,
|
LinkInlineSpecExtension.identifier,
|
||||||
FootNoteInlineSpecExtension.identifier,
|
FootNoteInlineSpecExtension.identifier,
|
||||||
MentionInlineSpecExtension.identifier,
|
MentionInlineSpecExtension.identifier,
|
||||||
|
CommentInlineSpecExtension.identifier,
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"references": [
|
"references": [
|
||||||
{ "path": "../../components" },
|
{ "path": "../../components" },
|
||||||
{ "path": "../../ext-loader" },
|
{ "path": "../../ext-loader" },
|
||||||
|
{ "path": "../comment" },
|
||||||
{ "path": "../footnote" },
|
{ "path": "../footnote" },
|
||||||
{ "path": "../latex" },
|
{ "path": "../latex" },
|
||||||
{ "path": "../link" },
|
{ "path": "../link" },
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { createIdentifier } from '@blocksuite/global/di';
|
||||||
|
import type { DisposableMember } from '@blocksuite/global/disposable';
|
||||||
|
import type { BaseSelection, ExtensionType } from '@blocksuite/store';
|
||||||
|
|
||||||
|
export type CommentId = string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `CommentProvider` is an interface used to connect external comment services
|
||||||
|
* with in-editor comment operations and rendering.
|
||||||
|
* All comment-related actions within the editor are routed through
|
||||||
|
* this interface to make external requests, and the editor is notified via callbacks.
|
||||||
|
* In essence, it follows the flow: BlockSuite -> AFFiNE -> BlockSuite.
|
||||||
|
*/
|
||||||
|
export interface CommentProvider {
|
||||||
|
addComment: (selections: BaseSelection[]) => void;
|
||||||
|
resolveComment: (id: CommentId) => void;
|
||||||
|
highlightComment: (id: CommentId | null) => void;
|
||||||
|
getComments: () => CommentId[];
|
||||||
|
|
||||||
|
onCommentAdded: (
|
||||||
|
callback: (id: CommentId, selections: BaseSelection[]) => void
|
||||||
|
) => DisposableMember;
|
||||||
|
onCommentResolved: (callback: (id: CommentId) => void) => DisposableMember;
|
||||||
|
onCommentDeleted: (callback: (id: CommentId) => void) => DisposableMember;
|
||||||
|
onCommentHighlighted: (
|
||||||
|
callback: (id: CommentId | null) => void
|
||||||
|
) => DisposableMember;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CommentProviderIdentifier =
|
||||||
|
createIdentifier<CommentProvider>('comment-provider');
|
||||||
|
|
||||||
|
export const CommentProviderExtension = (
|
||||||
|
provider: CommentProvider
|
||||||
|
): ExtensionType => {
|
||||||
|
return {
|
||||||
|
setup: di => {
|
||||||
|
di.addImpl(CommentProviderIdentifier, provider);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './comment-provider';
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { Store } from '@blocksuite/store';
|
||||||
|
|
||||||
|
import type { CommentId } from './comment-provider';
|
||||||
|
|
||||||
|
export function findCommentedBlocks(store: Store, commentId: CommentId) {
|
||||||
|
return store.getAllModels().filter(block => {
|
||||||
|
return 'comment' in block.props && block.props.comment === commentId;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
export * from './auto-clear-selection-service';
|
export * from './auto-clear-selection-service';
|
||||||
export * from './block-meta-service';
|
export * from './block-meta-service';
|
||||||
export * from './citation-service';
|
export * from './citation-service';
|
||||||
|
export * from './comment-service';
|
||||||
export * from './doc-display-meta-service';
|
export * from './doc-display-meta-service';
|
||||||
export * from './doc-mode-service';
|
export * from './doc-mode-service';
|
||||||
export * from './drag-handle-config';
|
export * from './drag-handle-config';
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export type AffineTextAttributes = AffineTextStyleAttributes & {
|
|||||||
member: string;
|
member: string;
|
||||||
notification?: string;
|
notification?: string;
|
||||||
} | null;
|
} | null;
|
||||||
|
[key: `comment-${string}`]: boolean | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AffineInlineEditor = InlineEditor<AffineTextAttributes>;
|
export type AffineInlineEditor = InlineEditor<AffineTextAttributes>;
|
||||||
|
|||||||
@@ -32,12 +32,29 @@ export class InlineManager<TextAttributes extends BaseTextAttributes> {
|
|||||||
|
|
||||||
const renderer: AttributeRenderer<TextAttributes> = props => {
|
const renderer: AttributeRenderer<TextAttributes> = props => {
|
||||||
// Priority increases from front to back
|
// Priority increases from front to back
|
||||||
for (const spec of this.specs.toReversed()) {
|
const specs = this.specs.toReversed();
|
||||||
|
const wrapperSpecs = specs.filter(spec => spec.wrapper);
|
||||||
|
const normalSpecs = specs.filter(spec => !spec.wrapper);
|
||||||
|
|
||||||
|
let result = defaultRenderer(props);
|
||||||
|
|
||||||
|
for (const spec of normalSpecs) {
|
||||||
if (spec.match(props.delta)) {
|
if (spec.match(props.delta)) {
|
||||||
return spec.renderer(props);
|
result = spec.renderer(props);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return defaultRenderer(props);
|
|
||||||
|
for (const spec of wrapperSpecs) {
|
||||||
|
if (spec.match(props.delta)) {
|
||||||
|
result = spec.renderer({
|
||||||
|
...props,
|
||||||
|
children: result,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
};
|
};
|
||||||
return renderer;
|
return renderer;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export type InlineSpecs<
|
|||||||
match: (delta: DeltaInsert<TextAttributes>) => boolean;
|
match: (delta: DeltaInsert<TextAttributes>) => boolean;
|
||||||
renderer: AttributeRenderer<TextAttributes>;
|
renderer: AttributeRenderer<TextAttributes>;
|
||||||
embed?: boolean;
|
embed?: boolean;
|
||||||
|
wrapper?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type InlineMarkdownMatchAction<
|
export type InlineMarkdownMatchAction<
|
||||||
|
|||||||
@@ -279,7 +279,10 @@ export class InlineEditor<
|
|||||||
this._isReadonly = isReadonly;
|
this._isReadonly = isReadonly;
|
||||||
}
|
}
|
||||||
|
|
||||||
transact(fn: () => void): void {
|
/**
|
||||||
|
* @param withoutTransact Execute a transaction without capturing the history.
|
||||||
|
*/
|
||||||
|
transact(fn: () => void, withoutTransact = false): void {
|
||||||
const doc = this.yText.doc;
|
const doc = this.yText.doc;
|
||||||
if (!doc) {
|
if (!doc) {
|
||||||
throw new BlockSuiteError(
|
throw new BlockSuiteError(
|
||||||
@@ -288,6 +291,6 @@ export class InlineEditor<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
doc.transact(fn, doc.clientID);
|
doc.transact(fn, withoutTransact ? null : doc.clientID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,11 +19,16 @@ export class InlineTextService<TextAttributes extends BaseTextAttributes> {
|
|||||||
options: {
|
options: {
|
||||||
match?: (delta: DeltaInsert, deltaInlineRange: InlineRange) => boolean;
|
match?: (delta: DeltaInsert, deltaInlineRange: InlineRange) => boolean;
|
||||||
mode?: 'replace' | 'merge';
|
mode?: 'replace' | 'merge';
|
||||||
|
withoutTransact?: boolean;
|
||||||
} = {}
|
} = {}
|
||||||
): void => {
|
): void => {
|
||||||
if (this.editor.isReadonly) return;
|
if (this.editor.isReadonly) return;
|
||||||
|
|
||||||
const { match = () => true, mode = 'merge' } = options;
|
const {
|
||||||
|
match = () => true,
|
||||||
|
mode = 'merge',
|
||||||
|
withoutTransact = false,
|
||||||
|
} = options;
|
||||||
const deltas = this.editor.deltaService.getDeltasByInlineRange(inlineRange);
|
const deltas = this.editor.deltaService.getDeltasByInlineRange(inlineRange);
|
||||||
|
|
||||||
deltas
|
deltas
|
||||||
@@ -49,7 +54,7 @@ export class InlineTextService<TextAttributes extends BaseTextAttributes> {
|
|||||||
targetInlineRange.length,
|
targetInlineRange.length,
|
||||||
normalizedAttributes
|
normalizedAttributes
|
||||||
);
|
);
|
||||||
});
|
}, withoutTransact);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export type AttributeRenderer<
|
|||||||
startOffset: number;
|
startOffset: number;
|
||||||
endOffset: number;
|
endOffset: number;
|
||||||
lineIndex: number;
|
lineIndex: number;
|
||||||
|
children?: TemplateResult<1>;
|
||||||
}) => TemplateResult<1>;
|
}) => TemplateResult<1>;
|
||||||
|
|
||||||
export interface InlineRange {
|
export interface InlineRange {
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import {
|
|||||||
type ReferenceParams,
|
type ReferenceParams,
|
||||||
} from '@blocksuite/affine/model';
|
} from '@blocksuite/affine/model';
|
||||||
import {
|
import {
|
||||||
|
type CommentId,
|
||||||
|
type CommentProvider,
|
||||||
type DocModeProvider,
|
type DocModeProvider,
|
||||||
type EditorSetting,
|
type EditorSetting,
|
||||||
GeneralSettingSchema,
|
GeneralSettingSchema,
|
||||||
@@ -13,7 +15,7 @@ import {
|
|||||||
type ParseDocUrlService,
|
type ParseDocUrlService,
|
||||||
type ThemeExtension,
|
type ThemeExtension,
|
||||||
} from '@blocksuite/affine/shared/services';
|
} from '@blocksuite/affine/shared/services';
|
||||||
import { type Workspace } from '@blocksuite/affine/store';
|
import type { BaseSelection, Workspace } from '@blocksuite/affine/store';
|
||||||
import type { TestAffineEditorContainer } from '@blocksuite/integration-test';
|
import type { TestAffineEditorContainer } from '@blocksuite/integration-test';
|
||||||
import { Signal, signal } from '@preact/signals-core';
|
import { Signal, signal } from '@preact/signals-core';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
@@ -191,6 +193,86 @@ export function mockEditorSetting() {
|
|||||||
return signal;
|
return signal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mockCommentProvider() {
|
||||||
|
class MockCommentProvider implements CommentProvider {
|
||||||
|
commentId = 0;
|
||||||
|
|
||||||
|
comments = new Map<
|
||||||
|
CommentId,
|
||||||
|
{
|
||||||
|
selections: BaseSelection[];
|
||||||
|
resolved: boolean;
|
||||||
|
}
|
||||||
|
>();
|
||||||
|
|
||||||
|
commentAddSubject = new Subject<{
|
||||||
|
id: CommentId;
|
||||||
|
selections: BaseSelection[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
commentResolveSubject = new Subject<CommentId>();
|
||||||
|
|
||||||
|
commentHighlightSubject = new Subject<CommentId | null>();
|
||||||
|
|
||||||
|
commentDeleteSubject = new Subject<CommentId>();
|
||||||
|
|
||||||
|
addComment(selections: BaseSelection[]) {
|
||||||
|
const id: CommentId = `${this.commentId++}`;
|
||||||
|
this.comments.set(id, {
|
||||||
|
selections,
|
||||||
|
resolved: false,
|
||||||
|
});
|
||||||
|
this.commentAddSubject.next({
|
||||||
|
id,
|
||||||
|
selections,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveComment(id: CommentId) {
|
||||||
|
const comment = this.comments.get(id);
|
||||||
|
if (!comment) return;
|
||||||
|
comment.resolved = true;
|
||||||
|
this.commentResolveSubject.next(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteComment(id: CommentId) {
|
||||||
|
this.comments.delete(id);
|
||||||
|
this.commentDeleteSubject.next(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
highlightComment(id: CommentId | null) {
|
||||||
|
this.commentHighlightSubject.next(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
getComments() {
|
||||||
|
return Array.from(this.comments.keys());
|
||||||
|
}
|
||||||
|
|
||||||
|
onCommentAdded(
|
||||||
|
callback: (id: CommentId, selections: BaseSelection[]) => void
|
||||||
|
) {
|
||||||
|
return this.commentAddSubject.subscribe(({ id, selections }) => {
|
||||||
|
callback(id, selections);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onCommentResolved(callback: (id: CommentId) => void) {
|
||||||
|
return this.commentResolveSubject.subscribe(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
onCommentDeleted(callback: (id: CommentId) => void) {
|
||||||
|
return this.commentDeleteSubject.subscribe(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
onCommentHighlighted(callback: (id: CommentId | null) => void) {
|
||||||
|
return this.commentHighlightSubject.subscribe(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = new MockCommentProvider();
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
editorSetting$: Signal<EditorSetting>;
|
editorSetting$: Signal<EditorSetting>;
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ const usePatchSpecs = (mode: DocMode) => {
|
|||||||
featureFlagService.flags.enable_pdf_embed_preview.$
|
featureFlagService.flags.enable_pdf_embed_preview.$
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const enableComment = useLiveData(featureFlagService.flags.enable_comment.$);
|
||||||
|
|
||||||
const patchedSpecs = useMemo(() => {
|
const patchedSpecs = useMemo(() => {
|
||||||
const manager = getViewManager()
|
const manager = getViewManager()
|
||||||
.config.init()
|
.config.init()
|
||||||
@@ -106,7 +108,8 @@ const usePatchSpecs = (mode: DocMode) => {
|
|||||||
.mobile(framework)
|
.mobile(framework)
|
||||||
.electron(framework)
|
.electron(framework)
|
||||||
.linkPreview(framework)
|
.linkPreview(framework)
|
||||||
.codeBlockHtmlPreview(framework).value;
|
.codeBlockHtmlPreview(framework)
|
||||||
|
.comment(enableComment).value;
|
||||||
|
|
||||||
if (BUILD_CONFIG.isMobileEdition) {
|
if (BUILD_CONFIG.isMobileEdition) {
|
||||||
if (mode === 'page') {
|
if (mode === 'page') {
|
||||||
@@ -122,6 +125,7 @@ const usePatchSpecs = (mode: DocMode) => {
|
|||||||
enableAI,
|
enableAI,
|
||||||
enablePDFEmbedPreview,
|
enablePDFEmbedPreview,
|
||||||
enableTurboRenderer,
|
enableTurboRenderer,
|
||||||
|
enableComment,
|
||||||
framework,
|
framework,
|
||||||
isInPeekView,
|
isInPeekView,
|
||||||
isCloud,
|
isCloud,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { ReactToLit } from '@affine/component';
|
|||||||
import { AIViewExtension } from '@affine/core/blocksuite/view-extensions/ai';
|
import { AIViewExtension } from '@affine/core/blocksuite/view-extensions/ai';
|
||||||
import { CloudViewExtension } from '@affine/core/blocksuite/view-extensions/cloud';
|
import { CloudViewExtension } from '@affine/core/blocksuite/view-extensions/cloud';
|
||||||
import { CodeBlockPreviewViewExtension } from '@affine/core/blocksuite/view-extensions/code-block-preview';
|
import { CodeBlockPreviewViewExtension } from '@affine/core/blocksuite/view-extensions/code-block-preview';
|
||||||
|
import { CommentViewExtension } from '@affine/core/blocksuite/view-extensions/comment';
|
||||||
import { AffineDatabaseViewExtension } from '@affine/core/blocksuite/view-extensions/database';
|
import { AffineDatabaseViewExtension } from '@affine/core/blocksuite/view-extensions/database';
|
||||||
import {
|
import {
|
||||||
EdgelessBlockHeaderConfigViewExtension,
|
EdgelessBlockHeaderConfigViewExtension,
|
||||||
@@ -56,6 +57,7 @@ type Configure = {
|
|||||||
electron: (framework?: FrameworkProvider) => Configure;
|
electron: (framework?: FrameworkProvider) => Configure;
|
||||||
linkPreview: (framework?: FrameworkProvider) => Configure;
|
linkPreview: (framework?: FrameworkProvider) => Configure;
|
||||||
codeBlockHtmlPreview: (framework?: FrameworkProvider) => Configure;
|
codeBlockHtmlPreview: (framework?: FrameworkProvider) => Configure;
|
||||||
|
comment: (enableComment?: boolean) => Configure;
|
||||||
|
|
||||||
value: ViewExtensionManager;
|
value: ViewExtensionManager;
|
||||||
};
|
};
|
||||||
@@ -116,6 +118,7 @@ class ViewProvider {
|
|||||||
electron: this._configureElectron,
|
electron: this._configureElectron,
|
||||||
linkPreview: this._configureLinkPreview,
|
linkPreview: this._configureLinkPreview,
|
||||||
codeBlockHtmlPreview: this._configureCodeBlockHtmlPreview,
|
codeBlockHtmlPreview: this._configureCodeBlockHtmlPreview,
|
||||||
|
comment: this._configureComment,
|
||||||
value: this._manager,
|
value: this._manager,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -137,7 +140,8 @@ class ViewProvider {
|
|||||||
.ai()
|
.ai()
|
||||||
.electron()
|
.electron()
|
||||||
.linkPreview()
|
.linkPreview()
|
||||||
.codeBlockHtmlPreview();
|
.codeBlockHtmlPreview()
|
||||||
|
.comment();
|
||||||
|
|
||||||
return this.config;
|
return this.config;
|
||||||
};
|
};
|
||||||
@@ -323,6 +327,11 @@ class ViewProvider {
|
|||||||
this._manager.configure(CodeBlockPreviewViewExtension, { framework });
|
this._manager.configure(CodeBlockPreviewViewExtension, { framework });
|
||||||
return this.config;
|
return this.config;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private readonly _configureComment = (enableComment?: boolean) => {
|
||||||
|
this._manager.configure(CommentViewExtension, { enableComment });
|
||||||
|
return this.config;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getViewManager() {
|
export function getViewManager() {
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { noop } from '@blocksuite/affine/global/utils';
|
||||||
|
import { CommentProviderExtension } from '@blocksuite/affine/shared/services';
|
||||||
|
|
||||||
|
export const AffineCommentProvider = CommentProviderExtension({
|
||||||
|
addComment: noop,
|
||||||
|
resolveComment: noop,
|
||||||
|
highlightComment: noop,
|
||||||
|
getComments: () => [],
|
||||||
|
|
||||||
|
onCommentAdded: () => noop,
|
||||||
|
onCommentResolved: () => noop,
|
||||||
|
onCommentDeleted: () => noop,
|
||||||
|
onCommentHighlighted: () => noop,
|
||||||
|
});
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import {
|
||||||
|
type ViewExtensionContext,
|
||||||
|
ViewExtensionProvider,
|
||||||
|
} from '@blocksuite/affine/ext-loader';
|
||||||
|
import z from 'zod';
|
||||||
|
|
||||||
|
import { AffineCommentProvider } from './comment-provider';
|
||||||
|
|
||||||
|
const optionsSchema = z.object({
|
||||||
|
enableComment: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export class CommentViewExtension extends ViewExtensionProvider {
|
||||||
|
override name = 'comment';
|
||||||
|
|
||||||
|
override schema = optionsSchema;
|
||||||
|
|
||||||
|
override setup(
|
||||||
|
context: ViewExtensionContext,
|
||||||
|
options?: z.infer<typeof optionsSchema>
|
||||||
|
) {
|
||||||
|
super.setup(context, options);
|
||||||
|
if (!options?.enableComment) return;
|
||||||
|
|
||||||
|
context.register([AffineCommentProvider]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -264,6 +264,13 @@ export const AFFINE_FLAGS = {
|
|||||||
configurable: isCanaryBuild,
|
configurable: isCanaryBuild,
|
||||||
defaultState: false,
|
defaultState: false,
|
||||||
},
|
},
|
||||||
|
enable_comment: {
|
||||||
|
category: 'affine',
|
||||||
|
displayName: 'Enable Comment',
|
||||||
|
description: 'Enable comment',
|
||||||
|
configurable: isCanaryBuild,
|
||||||
|
defaultState: true,
|
||||||
|
},
|
||||||
} satisfies { [key in string]: FlagInfo };
|
} satisfies { [key in string]: FlagInfo };
|
||||||
|
|
||||||
// oxlint-disable-next-line no-redeclare
|
// oxlint-disable-next-line no-redeclare
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ test.describe('Embed synced doc in edgeless mode', () => {
|
|||||||
},
|
},
|
||||||
{ embedDocId, height }
|
{ embedDocId, height }
|
||||||
);
|
);
|
||||||
|
await waitNextFrame(page);
|
||||||
};
|
};
|
||||||
|
|
||||||
const embedSyncedBlockInNote = page.locator(
|
const embedSyncedBlockInNote = page.locator(
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export const PackageList = [
|
|||||||
'blocksuite/affine/gfx/template',
|
'blocksuite/affine/gfx/template',
|
||||||
'blocksuite/affine/gfx/text',
|
'blocksuite/affine/gfx/text',
|
||||||
'blocksuite/affine/gfx/turbo-renderer',
|
'blocksuite/affine/gfx/turbo-renderer',
|
||||||
|
'blocksuite/affine/inlines/comment',
|
||||||
'blocksuite/affine/inlines/footnote',
|
'blocksuite/affine/inlines/footnote',
|
||||||
'blocksuite/affine/inlines/latex',
|
'blocksuite/affine/inlines/latex',
|
||||||
'blocksuite/affine/inlines/link',
|
'blocksuite/affine/inlines/link',
|
||||||
@@ -130,6 +131,7 @@ export const PackageList = [
|
|||||||
'blocksuite/affine/components',
|
'blocksuite/affine/components',
|
||||||
'blocksuite/affine/ext-loader',
|
'blocksuite/affine/ext-loader',
|
||||||
'blocksuite/affine/gfx/turbo-renderer',
|
'blocksuite/affine/gfx/turbo-renderer',
|
||||||
|
'blocksuite/affine/inlines/comment',
|
||||||
'blocksuite/affine/inlines/latex',
|
'blocksuite/affine/inlines/latex',
|
||||||
'blocksuite/affine/inlines/link',
|
'blocksuite/affine/inlines/link',
|
||||||
'blocksuite/affine/inlines/preset',
|
'blocksuite/affine/inlines/preset',
|
||||||
@@ -724,6 +726,19 @@ export const PackageList = [
|
|||||||
'blocksuite/framework/store',
|
'blocksuite/framework/store',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
location: 'blocksuite/affine/inlines/comment',
|
||||||
|
name: '@blocksuite/affine-inline-comment',
|
||||||
|
workspaceDependencies: [
|
||||||
|
'blocksuite/affine/ext-loader',
|
||||||
|
'blocksuite/affine/model',
|
||||||
|
'blocksuite/affine/rich-text',
|
||||||
|
'blocksuite/affine/shared',
|
||||||
|
'blocksuite/framework/global',
|
||||||
|
'blocksuite/framework/std',
|
||||||
|
'blocksuite/framework/store',
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
location: 'blocksuite/affine/inlines/footnote',
|
location: 'blocksuite/affine/inlines/footnote',
|
||||||
name: '@blocksuite/affine-inline-footnote',
|
name: '@blocksuite/affine-inline-footnote',
|
||||||
@@ -786,6 +801,7 @@ export const PackageList = [
|
|||||||
workspaceDependencies: [
|
workspaceDependencies: [
|
||||||
'blocksuite/affine/components',
|
'blocksuite/affine/components',
|
||||||
'blocksuite/affine/ext-loader',
|
'blocksuite/affine/ext-loader',
|
||||||
|
'blocksuite/affine/inlines/comment',
|
||||||
'blocksuite/affine/inlines/footnote',
|
'blocksuite/affine/inlines/footnote',
|
||||||
'blocksuite/affine/inlines/latex',
|
'blocksuite/affine/inlines/latex',
|
||||||
'blocksuite/affine/inlines/link',
|
'blocksuite/affine/inlines/link',
|
||||||
@@ -1505,6 +1521,7 @@ export type PackageName =
|
|||||||
| '@blocksuite/affine-gfx-template'
|
| '@blocksuite/affine-gfx-template'
|
||||||
| '@blocksuite/affine-gfx-text'
|
| '@blocksuite/affine-gfx-text'
|
||||||
| '@blocksuite/affine-gfx-turbo-renderer'
|
| '@blocksuite/affine-gfx-turbo-renderer'
|
||||||
|
| '@blocksuite/affine-inline-comment'
|
||||||
| '@blocksuite/affine-inline-footnote'
|
| '@blocksuite/affine-inline-footnote'
|
||||||
| '@blocksuite/affine-inline-latex'
|
| '@blocksuite/affine-inline-latex'
|
||||||
| '@blocksuite/affine-inline-link'
|
| '@blocksuite/affine-inline-link'
|
||||||
|
|||||||
@@ -90,6 +90,7 @@
|
|||||||
{ "path": "./blocksuite/affine/gfx/template" },
|
{ "path": "./blocksuite/affine/gfx/template" },
|
||||||
{ "path": "./blocksuite/affine/gfx/text" },
|
{ "path": "./blocksuite/affine/gfx/text" },
|
||||||
{ "path": "./blocksuite/affine/gfx/turbo-renderer" },
|
{ "path": "./blocksuite/affine/gfx/turbo-renderer" },
|
||||||
|
{ "path": "./blocksuite/affine/inlines/comment" },
|
||||||
{ "path": "./blocksuite/affine/inlines/footnote" },
|
{ "path": "./blocksuite/affine/inlines/footnote" },
|
||||||
{ "path": "./blocksuite/affine/inlines/latex" },
|
{ "path": "./blocksuite/affine/inlines/latex" },
|
||||||
{ "path": "./blocksuite/affine/inlines/link" },
|
{ "path": "./blocksuite/affine/inlines/link" },
|
||||||
|
|||||||
28
yarn.lock
28
yarn.lock
@@ -2514,6 +2514,7 @@ __metadata:
|
|||||||
"@blocksuite/affine-components": "workspace:*"
|
"@blocksuite/affine-components": "workspace:*"
|
||||||
"@blocksuite/affine-ext-loader": "workspace:*"
|
"@blocksuite/affine-ext-loader": "workspace:*"
|
||||||
"@blocksuite/affine-gfx-turbo-renderer": "workspace:*"
|
"@blocksuite/affine-gfx-turbo-renderer": "workspace:*"
|
||||||
|
"@blocksuite/affine-inline-comment": "workspace:*"
|
||||||
"@blocksuite/affine-inline-latex": "workspace:*"
|
"@blocksuite/affine-inline-latex": "workspace:*"
|
||||||
"@blocksuite/affine-inline-link": "workspace:*"
|
"@blocksuite/affine-inline-link": "workspace:*"
|
||||||
"@blocksuite/affine-inline-preset": "workspace:*"
|
"@blocksuite/affine-inline-preset": "workspace:*"
|
||||||
@@ -3509,6 +3510,31 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
|
"@blocksuite/affine-inline-comment@workspace:*, @blocksuite/affine-inline-comment@workspace:blocksuite/affine/inlines/comment":
|
||||||
|
version: 0.0.0-use.local
|
||||||
|
resolution: "@blocksuite/affine-inline-comment@workspace:blocksuite/affine/inlines/comment"
|
||||||
|
dependencies:
|
||||||
|
"@blocksuite/affine-ext-loader": "workspace:*"
|
||||||
|
"@blocksuite/affine-model": "workspace:*"
|
||||||
|
"@blocksuite/affine-rich-text": "workspace:*"
|
||||||
|
"@blocksuite/affine-shared": "workspace:*"
|
||||||
|
"@blocksuite/global": "workspace:*"
|
||||||
|
"@blocksuite/std": "workspace:*"
|
||||||
|
"@blocksuite/store": "workspace:*"
|
||||||
|
"@lit/context": "npm:^1.1.2"
|
||||||
|
"@preact/signals-core": "npm:^1.8.0"
|
||||||
|
"@toeverything/theme": "npm:^1.1.15"
|
||||||
|
"@types/lodash-es": "npm:^4.17.12"
|
||||||
|
lit: "npm:^3.2.0"
|
||||||
|
lit-html: "npm:^3.2.1"
|
||||||
|
lodash-es: "npm:^4.17.21"
|
||||||
|
rxjs: "npm:^7.8.1"
|
||||||
|
vitest: "npm:3.1.3"
|
||||||
|
yjs: "npm:^13.6.21"
|
||||||
|
zod: "npm:^3.23.8"
|
||||||
|
languageName: unknown
|
||||||
|
linkType: soft
|
||||||
|
|
||||||
"@blocksuite/affine-inline-footnote@workspace:*, @blocksuite/affine-inline-footnote@workspace:blocksuite/affine/inlines/footnote":
|
"@blocksuite/affine-inline-footnote@workspace:*, @blocksuite/affine-inline-footnote@workspace:blocksuite/affine/inlines/footnote":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@blocksuite/affine-inline-footnote@workspace:blocksuite/affine/inlines/footnote"
|
resolution: "@blocksuite/affine-inline-footnote@workspace:blocksuite/affine/inlines/footnote"
|
||||||
@@ -3637,6 +3663,7 @@ __metadata:
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@blocksuite/affine-components": "workspace:*"
|
"@blocksuite/affine-components": "workspace:*"
|
||||||
"@blocksuite/affine-ext-loader": "workspace:*"
|
"@blocksuite/affine-ext-loader": "workspace:*"
|
||||||
|
"@blocksuite/affine-inline-comment": "workspace:*"
|
||||||
"@blocksuite/affine-inline-footnote": "workspace:*"
|
"@blocksuite/affine-inline-footnote": "workspace:*"
|
||||||
"@blocksuite/affine-inline-latex": "workspace:*"
|
"@blocksuite/affine-inline-latex": "workspace:*"
|
||||||
"@blocksuite/affine-inline-link": "workspace:*"
|
"@blocksuite/affine-inline-link": "workspace:*"
|
||||||
@@ -4217,6 +4244,7 @@ __metadata:
|
|||||||
"@blocksuite/affine-gfx-template": "workspace:*"
|
"@blocksuite/affine-gfx-template": "workspace:*"
|
||||||
"@blocksuite/affine-gfx-text": "workspace:*"
|
"@blocksuite/affine-gfx-text": "workspace:*"
|
||||||
"@blocksuite/affine-gfx-turbo-renderer": "workspace:*"
|
"@blocksuite/affine-gfx-turbo-renderer": "workspace:*"
|
||||||
|
"@blocksuite/affine-inline-comment": "workspace:*"
|
||||||
"@blocksuite/affine-inline-footnote": "workspace:*"
|
"@blocksuite/affine-inline-footnote": "workspace:*"
|
||||||
"@blocksuite/affine-inline-latex": "workspace:*"
|
"@blocksuite/affine-inline-latex": "workspace:*"
|
||||||
"@blocksuite/affine-inline-link": "workspace:*"
|
"@blocksuite/affine-inline-link": "workspace:*"
|
||||||
|
|||||||
Reference in New Issue
Block a user