mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-12 23:56:36 +08:00
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:
@@ -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[]
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user