fix(editor): time issues of comment initialization (#13031)

#### PR Dependency Tree


* **PR #13031** 👈

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**
* Added the ability to filter comments by their resolution status
(resolved, unresolved, or all) when viewing or managing comments.

* **Refactor**
* Improved the way commented text is identified and retrieved, resulting
in more reliable comment selection and filtering.
* Enhanced comment retrieval to support asynchronous data loading,
ensuring up-to-date comment information.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
L-Sun
2025-07-04 16:19:00 +08:00
committed by GitHub
parent a485ad5c45
commit eb56adea46
7 changed files with 111 additions and 68 deletions
@@ -38,7 +38,7 @@ export class InlineCommentManager extends LifeCycleWatcher {
const provider = this._provider; const provider = this._provider;
if (!provider) return; if (!provider) return;
this._init(); this._init().catch(console.error);
this._disposables.add(provider.onCommentAdded(this._handleAddComment)); this._disposables.add(provider.onCommentAdded(this._handleAddComment));
this._disposables.add( this._disposables.add(
@@ -59,20 +59,12 @@ export class InlineCommentManager extends LifeCycleWatcher {
this._disposables.dispose(); this._disposables.dispose();
} }
private _init() { private async _init() {
const provider = this._provider; const provider = this._provider;
if (!provider) return; if (!provider) return;
const commentsInProvider = provider.getComments(); const commentsInProvider = await provider.getComments('unresolved');
const inlineComments = findAllCommentedTexts(this.std).flatMap( const inlineComments = [...findAllCommentedTexts(this.std.store).values()];
([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( const blockComments = findAllCommentedBlocks(this.std.store).flatMap(
block => Object.keys(block.props.comments) block => Object.keys(block.props.comments)
@@ -165,12 +157,17 @@ export class InlineCommentManager extends LifeCycleWatcher {
}; };
private readonly _handleDeleteAndResolve = (id: CommentId) => { private readonly _handleDeleteAndResolve = (id: CommentId) => {
const commentedTexts = findCommentedTexts(this.std, id); const commentedTexts = findCommentedTexts(this.std.store, id);
if (commentedTexts.length === 0) return; if (commentedTexts.length === 0) return;
this.std.store.withoutTransact(() => { this.std.store.withoutTransact(() => {
commentedTexts.forEach(([selection, inlineEditor]) => { commentedTexts.forEach(selection => {
inlineEditor.formatText( const inlineEditor = getInlineEditorByModel(
this.std,
selection.from.blockId
);
inlineEditor?.formatText(
selection.from, selection.from,
{ {
[`comment-${id}`]: null, [`comment-${id}`]: null,
+44 -43
View File
@@ -1,55 +1,56 @@
import { getInlineEditorByModel } from '@blocksuite/affine-rich-text';
import type { CommentId } from '@blocksuite/affine-shared/services'; import type { CommentId } from '@blocksuite/affine-shared/services';
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types'; import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import { type BlockStdScope, TextSelection } from '@blocksuite/std'; import { TextSelection } from '@blocksuite/std';
import type { InlineEditor } from '@blocksuite/std/inline'; import type { DeltaInsert, Store } from '@blocksuite/store';
import type { DeltaInsert } from '@blocksuite/store';
export function findAllCommentedTexts(std: BlockStdScope) { export function findAllCommentedTexts(
const selections: [TextSelection, InlineEditor<AffineTextAttributes>][] = []; store: Store
std.store.getAllModels().forEach(model => { ): Map<TextSelection, CommentId> {
const inlineEditor = getInlineEditorByModel(std, model); const result = new Map<TextSelection, CommentId>();
if (!inlineEditor) return;
inlineEditor.mapDeltasInInlineRange( store.getAllModels().forEach(model => {
{ if (!model.text) return;
index: 0,
length: inlineEditor.yTextLength, let index = 0;
}, model.text.toDelta().forEach(delta => {
(delta, rangeIndex) => { if (!delta.insert) return;
if (
delta.attributes && const length = delta.insert.length;
Object.keys(delta.attributes).some(key => key.startsWith('comment-'))
) { if (!delta.attributes) {
selections.push([ index += length;
new TextSelection({ return;
from: {
blockId: model.id,
index: rangeIndex,
length: delta.insert.length,
},
to: null,
}),
inlineEditor,
]);
}
} }
);
Object.keys(delta.attributes)
.filter(key => key.startsWith('comment-'))
.forEach(key => {
const commentId = key.replace('comment-', '');
const selection = new TextSelection({
from: {
blockId: model.id,
index,
length,
},
to: null,
});
result.set(selection, commentId);
});
index += length;
});
}); });
return selections; return result;
} }
export function findCommentedTexts(std: BlockStdScope, commentId: CommentId) { export function findCommentedTexts(
return findAllCommentedTexts(std).filter(([selection, inlineEditor]) => { store: Store,
const deltas = inlineEditor.getDeltasByInlineRange({ commentId: CommentId
index: selection.from.index, ): TextSelection[] {
length: selection.from.length, return [...findAllCommentedTexts(store).entries()]
}); .filter(([_, id]) => id === commentId)
return deltas .map(([selection]) => selection);
.flatMap(([delta]) => extractCommentIdFromDelta(delta))
.includes(commentId);
});
} }
export function extractCommentIdFromDelta( export function extractCommentIdFromDelta(
@@ -15,7 +15,10 @@ export interface CommentProvider {
addComment: (selections: BaseSelection[]) => void; addComment: (selections: BaseSelection[]) => void;
resolveComment: (id: CommentId) => void; resolveComment: (id: CommentId) => void;
highlightComment: (id: CommentId | null) => void; highlightComment: (id: CommentId | null) => void;
getComments: () => CommentId[];
getComments: (
type: 'resolved' | 'unresolved' | 'all'
) => Promise<CommentId[]> | CommentId[];
onCommentAdded: ( onCommentAdded: (
callback: (id: CommentId, selections: BaseSelection[]) => void callback: (id: CommentId, selections: BaseSelection[]) => void
@@ -244,8 +244,14 @@ export function mockCommentProvider() {
this.commentHighlightSubject.next(id); this.commentHighlightSubject.next(id);
} }
getComments() { getComments(type: 'resolved' | 'unresolved' | 'all' = 'all') {
return Array.from(this.comments.keys()); return Array.from(this.comments.entries())
.filter(([_, comment]) => {
if (type === 'all') return true;
if (type === 'resolved') return comment.resolved;
return !comment.resolved;
})
.map(([id]) => id);
} }
onCommentAdded( onCommentAdded(
@@ -138,8 +138,10 @@ class AffineCommentService implements CommentProvider {
this.commentEntity.highlightComment(id); this.commentEntity.highlightComment(id);
} }
getComments(): string[] { async getComments(
return this.commentEntity.getComments(); type: 'resolved' | 'unresolved' | 'all' = 'all'
): Promise<string[]> {
return this.commentEntity.getComments(type);
} }
onCommentAdded(callback: (id: string, selections: BaseSelection[]) => void) { onCommentAdded(callback: (id: string, selections: BaseSelection[]) => void) {
@@ -14,7 +14,16 @@ import {
onStart, onStart,
} from '@toeverything/infra'; } from '@toeverything/infra';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { catchError, of, Subject, switchMap, tap, timer } from 'rxjs'; import {
catchError,
filter,
first,
of,
Subject,
switchMap,
tap,
timer,
} from 'rxjs';
import { type DocDisplayMetaService } from '../../doc-display-meta'; import { type DocDisplayMetaService } from '../../doc-display-meta';
import { GlobalContextService } from '../../global-context'; import { GlobalContextService } from '../../global-context';
@@ -280,8 +289,30 @@ export class DocCommentEntity extends Entity<{
this.commentHighlighted$.next(id); this.commentHighlighted$.next(id);
} }
getComments(): CommentId[] { async getComments(
return this.comments$.value.map(comment => comment.id); type: 'resolved' | 'unresolved' | 'all' = 'all'
): Promise<CommentId[]> {
return new Promise<CommentId[]>(resolve => {
this.revalidate();
this.loading$
.pipe(
filter(loading => !loading),
first()
)
.subscribe(() => {
resolve(
this.comments$.value
.filter(comment =>
type === 'all'
? true
: type === 'resolved'
? comment.resolved
: !comment.resolved
)
.map(comment => comment.id)
);
});
});
} }
onCommentAdded( onCommentAdded(
@@ -214,9 +214,12 @@ export class Editor extends Entity {
const std = editorContainer.host.std; const std = editorContainer.host.std;
// First try to find inline commented texts // First try to find inline commented texts
const inlineCommentedSelections = findCommentedTexts(std, commentId); const inlineCommentedSelections = findCommentedTexts(
std.store,
commentId
);
if (inlineCommentedSelections.length > 0) { if (inlineCommentedSelections.length > 0) {
const firstSelection = inlineCommentedSelections[0][0]; const firstSelection = inlineCommentedSelections[0];
finalId = firstSelection.from.blockId; finalId = firstSelection.from.blockId;
finalKey = 'blockIds'; finalKey = 'blockIds';
} else { } else {