From 558279da293c58c2d28fc51a7c50f7b50d35d6ae Mon Sep 17 00:00:00 2001 From: L-Sun Date: Thu, 3 Jul 2025 20:30:07 +0800 Subject: [PATCH] feat(editor): resolve unassociated comments on init (#13008) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### PR Dependency Tree * **PR #13008** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) ## 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. --- .../comment/src/inline-comment-manager.ts | 48 ++++++++++++++++++- .../affine/inlines/comment/src/utils.ts | 20 ++++++-- .../comment-service/block-comment-manager.ts | 6 +-- .../src/services/comment-service/utils.ts | 11 +++-- 4 files changed, 73 insertions(+), 12 deletions(-) diff --git a/blocksuite/affine/inlines/comment/src/inline-comment-manager.ts b/blocksuite/affine/inlines/comment/src/inline-comment-manager.ts index a7367d72fb..b171799aa8 100644 --- a/blocksuite/affine/inlines/comment/src/inline-comment-manager.ts +++ b/blocksuite/affine/inlines/comment/src/inline-comment-manager.ts @@ -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[] diff --git a/blocksuite/affine/inlines/comment/src/utils.ts b/blocksuite/affine/inlines/comment/src/utils.ts index cfc0c40102..2954bac7f0 100644 --- a/blocksuite/affine/inlines/comment/src/utils.ts +++ b/blocksuite/affine/inlines/comment/src/utils.ts @@ -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][] = []; 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 ) { diff --git a/blocksuite/affine/shared/src/services/comment-service/block-comment-manager.ts b/blocksuite/affine/shared/src/services/comment-service/block-comment-manager.ts index 79c47e47bb..b5e33b8cba 100644 --- a/blocksuite/affine/shared/src/services/comment-service/block-comment-manager.ts +++ b/blocksuite/affine/shared/src/services/comment-service/block-comment-manager.ts @@ -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 => { diff --git a/blocksuite/affine/shared/src/services/comment-service/utils.ts b/blocksuite/affine/shared/src/services/comment-service/utils.ts index f7990d5cdd..2aeeb5d9e2 100644 --- a/blocksuite/affine/shared/src/services/comment-service/utils.ts +++ b/blocksuite/affine/shared/src/services/comment-service/utils.ts @@ -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 }>; 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 = { tooltip: 'Comment', when: ({ std }) => !!std.getOptional(CommentProviderIdentifier),