feat(editor): resolve unassociated comments on init (#13008)

#### PR Dependency Tree


* **PR #13008** 👈

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

* **Bug Fixes**
* Improved synchronization between comment states in the editor and the
comment provider to ensure consistency of inline and block comments.

* **Refactor**
* Separated and modularized utility functions for finding commented
texts and blocks, enhancing maintainability and reusability.

* **Style**
* Updated styles to remove background color and border from inline
comments within embedded note content.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
L-Sun
2025-07-03 20:30:07 +08:00
committed by GitHub
parent c5b442225f
commit 558279da29
4 changed files with 73 additions and 12 deletions
@@ -1,8 +1,10 @@
import { getInlineEditorByModel } from '@blocksuite/affine-rich-text';
import { getSelectedBlocksCommand } from '@blocksuite/affine-shared/commands';
import {
BlockCommentManager,
type CommentId,
CommentProviderIdentifier,
findAllCommentedBlocks,
} from '@blocksuite/affine-shared/services';
import type { AffineInlineEditor } from '@blocksuite/affine-shared/types';
import { DisposableGroup } from '@blocksuite/global/disposable';
@@ -13,8 +15,13 @@ import {
} from '@blocksuite/std';
import type { BaseSelection, BlockModel } from '@blocksuite/store';
import { signal } from '@preact/signals-core';
import difference from 'lodash-es/difference';
import { extractCommentIdFromDelta, findCommentedTexts } from './utils';
import {
extractCommentIdFromDelta,
findAllCommentedTexts,
findCommentedTexts,
} from './utils';
export class InlineCommentManager extends LifeCycleWatcher {
static override key = 'inline-comment-manager';
@@ -31,6 +38,8 @@ export class InlineCommentManager extends LifeCycleWatcher {
const provider = this._provider;
if (!provider) return;
this._init();
this._disposables.add(provider.onCommentAdded(this._handleAddComment));
this._disposables.add(
provider.onCommentDeleted(this._handleDeleteAndResolve)
@@ -50,6 +59,43 @@ export class InlineCommentManager extends LifeCycleWatcher {
this._disposables.dispose();
}
private _init() {
const provider = this._provider;
if (!provider) return;
const commentsInProvider = provider.getComments();
const inlineComments = findAllCommentedTexts(this.std).flatMap(
([selection, inlineEditor]) => {
const deltas = inlineEditor.getDeltasByInlineRange({
index: selection.from.index,
length: selection.from.length,
});
return deltas.flatMap(([delta]) => extractCommentIdFromDelta(delta));
}
);
const blockComments = findAllCommentedBlocks(this.std.store).flatMap(
block => Object.keys(block.props.comments)
);
const commentsInEditor = [
...new Set([...inlineComments, ...blockComments]),
];
// resolve comments that are in provider but not in editor
// which means the commented content may be deleted
difference(commentsInProvider, commentsInEditor).forEach(comment => {
provider.resolveComment(comment);
});
// remove comments that are in editor but not in provider
// which means the comment may be removed or resolved in provider side
difference(commentsInEditor, commentsInProvider).forEach(comment => {
this._handleDeleteAndResolve(comment);
this.std.get(BlockCommentManager).handleDeleteAndResolve(comment);
});
}
private readonly _handleAddComment = (
id: CommentId,
selections: BaseSelection[]
+15 -5
View File
@@ -5,8 +5,8 @@ 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][] = [];
export function findAllCommentedTexts(std: BlockStdScope) {
const selections: [TextSelection, InlineEditor<AffineTextAttributes>][] = [];
std.store.getAllModels().forEach(model => {
const inlineEditor = getInlineEditorByModel(std, model);
if (!inlineEditor) return;
@@ -19,9 +19,7 @@ export function findCommentedTexts(std: BlockStdScope, commentId: CommentId) {
(delta, rangeIndex) => {
if (
delta.attributes &&
Object.keys(delta.attributes).some(
key => key === `comment-${commentId}`
)
Object.keys(delta.attributes).some(key => key.startsWith('comment-'))
) {
selections.push([
new TextSelection({
@@ -42,6 +40,18 @@ export function findCommentedTexts(std: BlockStdScope, commentId: CommentId) {
return selections;
}
export function findCommentedTexts(std: BlockStdScope, commentId: CommentId) {
return findAllCommentedTexts(std).filter(([selection, inlineEditor]) => {
const deltas = inlineEditor.getDeltasByInlineRange({
index: selection.from.index,
length: selection.from.length,
});
return deltas
.flatMap(([delta]) => extractCommentIdFromDelta(delta))
.includes(commentId);
});
}
export function extractCommentIdFromDelta(
delta: DeltaInsert<AffineTextAttributes>
) {
@@ -42,10 +42,10 @@ export class BlockCommentManager extends LifeCycleWatcher {
this._disposables.add(provider.onCommentAdded(this._handleAddComment));
this._disposables.add(
provider.onCommentDeleted(this._handleDeleteAndResolve)
provider.onCommentDeleted(this.handleDeleteAndResolve)
);
this._disposables.add(
provider.onCommentResolved(this._handleDeleteAndResolve)
provider.onCommentResolved(this.handleDeleteAndResolve)
);
this._disposables.add(
provider.onCommentHighlighted(this._handleHighlightComment)
@@ -103,7 +103,7 @@ export class BlockCommentManager extends LifeCycleWatcher {
});
};
private readonly _handleDeleteAndResolve = (id: CommentId) => {
readonly handleDeleteAndResolve = (id: CommentId) => {
const commentedBlocks = findCommentedBlocks(this.std.store, id);
this.std.store.withoutTransact(() => {
commentedBlocks.forEach(block => {
@@ -5,18 +5,23 @@ import type { BlockModel, Store } from '@blocksuite/store';
import type { ToolbarAction } from '../toolbar-service';
import { type CommentId, CommentProviderIdentifier } from './comment-provider';
export function findCommentedBlocks(store: Store, commentId: CommentId) {
export function findAllCommentedBlocks(store: Store) {
type CommentedBlock = BlockModel<{ comments: Record<CommentId, boolean> }>;
return store.getAllModels().filter((block): block is CommentedBlock => {
return (
'comments' in block.props &&
typeof block.props.comments === 'object' &&
block.props.comments !== null &&
commentId in block.props.comments
block.props.comments !== null
);
});
}
export function findCommentedBlocks(store: Store, commentId: CommentId) {
return findAllCommentedBlocks(store).filter(block => {
return block.props.comments[commentId];
});
}
export const blockCommentToolbarButton: Omit<ToolbarAction, 'id'> = {
tooltip: 'Comment',
when: ({ std }) => !!std.getOptional(CommentProviderIdentifier),