mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-15 09:06:19 +08:00
fix(core): some ux enhancements on comments (#13105)
fix PD-2688 #### PR Dependency Tree * **PR #13105** 👈 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 ## Summary by CodeRabbit * **New Features** * Added configurable support for enabling or disabling inline comments. * Introduced visual indication (strikethrough) for deleted comments in the comment sidebar. * Sidebar now shows when a comment is no longer present in the editor. * Added a localized placeholder prompt ("What are your thoughts?") in the comment editor. * Integrated detailed event tracking for comment actions: create, edit, delete, and resolve. * **Improvements** * Inline comments are now disabled in shared mode. * Enhanced synchronization between editor comments and provider state to remove stale comments. * Inline comment features now respect the document’s read-only state. * Improved mention handling and tracking in comment creation and editing. * Comment manager and entities now dynamically track comments present in the editor. * Comment configuration updated to enable or disable inline comments based on settings. * **Bug Fixes** * Prevented comment block creation when in read-only mode. * **Localization** * Added English localization for the comment prompt. <!-- end of auto-generated comment: release notes by coderabbit.ai --> #### PR Dependency Tree * **PR #13105** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal)
This commit is contained in:
@@ -179,13 +179,14 @@ export class DocCommentStore extends Entity<{
|
||||
|
||||
async createComment(commentInput: {
|
||||
content: DocCommentContent;
|
||||
mentions?: string[];
|
||||
}): Promise<DocComment> {
|
||||
const graphql = this.graphqlService;
|
||||
if (!graphql) {
|
||||
throw new Error('GraphQL service not found');
|
||||
}
|
||||
|
||||
const mentions = findMentions(commentInput.content.snapshot.blocks);
|
||||
const mentions = commentInput.mentions;
|
||||
|
||||
const response = await graphql.gql({
|
||||
query: createCommentMutation,
|
||||
@@ -265,6 +266,7 @@ export class DocCommentStore extends Entity<{
|
||||
commentId: string,
|
||||
replyInput: {
|
||||
content: DocCommentContent;
|
||||
mentions?: string[];
|
||||
}
|
||||
): Promise<DocCommentReply> {
|
||||
const graphql = this.graphqlService;
|
||||
@@ -272,8 +274,6 @@ export class DocCommentStore extends Entity<{
|
||||
throw new Error('GraphQL service not found');
|
||||
}
|
||||
|
||||
const mentions = findMentions(replyInput.content.snapshot.blocks);
|
||||
|
||||
const response = await graphql.gql({
|
||||
query: createReplyMutation,
|
||||
variables: {
|
||||
@@ -282,7 +282,7 @@ export class DocCommentStore extends Entity<{
|
||||
content: replyInput.content,
|
||||
docMode: this.props.getDocMode(),
|
||||
docTitle: this.props.getDocTitle(),
|
||||
mentions: mentions,
|
||||
mentions: replyInput.mentions,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { type CommentChangeAction, DocMode } from '@affine/graphql';
|
||||
import { track } from '@affine/track';
|
||||
import { InlineCommentManager } from '@blocksuite/affine/inlines/comment';
|
||||
import type {
|
||||
BaseSelection,
|
||||
DocSnapshot,
|
||||
Store,
|
||||
} from '@blocksuite/affine/store';
|
||||
import type { BlockStdScope } from '@blocksuite/std';
|
||||
import {
|
||||
effect,
|
||||
Entity,
|
||||
@@ -38,6 +41,7 @@ import type {
|
||||
PendingComment,
|
||||
} from '../types';
|
||||
import { DocCommentStore } from './doc-comment-store';
|
||||
import { findMentions } from './utils';
|
||||
|
||||
type DisposeCallback = () => void;
|
||||
|
||||
@@ -50,6 +54,7 @@ type EditingDraft = {
|
||||
|
||||
export class DocCommentEntity extends Entity<{
|
||||
docId: string;
|
||||
std: BlockStdScope | null;
|
||||
}> {
|
||||
constructor(
|
||||
private readonly snapshotHelper: SnapshotHelper,
|
||||
@@ -69,6 +74,8 @@ export class DocCommentEntity extends Entity<{
|
||||
loading$ = new LiveData<boolean>(false);
|
||||
comments$ = new LiveData<DocComment[]>([]);
|
||||
|
||||
commentsInEditor$ = new LiveData<string[]>([]);
|
||||
|
||||
// Only one pending comment at a time (for new comments)
|
||||
readonly pendingComment$ = new LiveData<PendingComment | null>(null);
|
||||
|
||||
@@ -195,7 +202,9 @@ export class DocCommentEntity extends Entity<{
|
||||
attachments: draft.attachments,
|
||||
});
|
||||
}
|
||||
|
||||
track.$.commentPanel.$.editComment({
|
||||
type: draft.type === 'comment' ? 'root' : 'node',
|
||||
});
|
||||
this.editingDraft$.setValue(null);
|
||||
this.revalidate();
|
||||
}
|
||||
@@ -220,6 +229,7 @@ export class DocCommentEntity extends Entity<{
|
||||
if (!snapshot) {
|
||||
throw new Error('Failed to get snapshot');
|
||||
}
|
||||
const mentions = findMentions(snapshot.blocks);
|
||||
const comment = await this.store.createComment({
|
||||
content: {
|
||||
snapshot,
|
||||
@@ -227,6 +237,7 @@ export class DocCommentEntity extends Entity<{
|
||||
mode: this.docMode$.value ?? 'page',
|
||||
attachments,
|
||||
},
|
||||
mentions,
|
||||
});
|
||||
const currentComments = this.comments$.value;
|
||||
this.comments$.setValue([...currentComments, comment]);
|
||||
@@ -234,6 +245,19 @@ export class DocCommentEntity extends Entity<{
|
||||
id: comment.id,
|
||||
selections: pendingComment.selections || [],
|
||||
});
|
||||
// for block's preview, it will be something like <Paragraph>
|
||||
// extract the block type from the preview
|
||||
const blockType = preview?.match(/<([^>]+)>/)?.[1];
|
||||
track.$.commentPanel.$.createComment({
|
||||
type: 'root',
|
||||
withAttachment: (attachments?.length ?? 0) > 0,
|
||||
withMention: mentions.length > 0,
|
||||
category: blockType
|
||||
? blockType
|
||||
: (this.docMode$.value ?? 'page') === 'page'
|
||||
? 'Page'
|
||||
: 'Note',
|
||||
});
|
||||
this.pendingComment$.setValue(null);
|
||||
this.revalidate();
|
||||
}
|
||||
@@ -254,11 +278,13 @@ export class DocCommentEntity extends Entity<{
|
||||
throw new Error('Pending reply has no commentId');
|
||||
}
|
||||
|
||||
const mentions = findMentions(snapshot.blocks);
|
||||
const reply = await this.store.createReply(pendingReply.commentId, {
|
||||
content: {
|
||||
snapshot,
|
||||
attachments,
|
||||
},
|
||||
mentions,
|
||||
});
|
||||
const currentComments = this.comments$.value;
|
||||
const updatedComments = currentComments.map(comment =>
|
||||
@@ -267,6 +293,12 @@ export class DocCommentEntity extends Entity<{
|
||||
: comment
|
||||
);
|
||||
this.comments$.setValue(updatedComments);
|
||||
track.$.commentPanel.$.createComment({
|
||||
type: 'node',
|
||||
withAttachment: (attachments?.length ?? 0) > 0,
|
||||
withMention: mentions.length > 0,
|
||||
category: (this.docMode$.value ?? 'page') === 'page' ? 'Page' : 'Note',
|
||||
});
|
||||
this.pendingReply$.setValue(null);
|
||||
this.revalidate();
|
||||
}
|
||||
@@ -275,6 +307,7 @@ export class DocCommentEntity extends Entity<{
|
||||
await this.store.deleteComment(id);
|
||||
const currentComments = this.comments$.value;
|
||||
this.comments$.setValue(currentComments.filter(c => c.id !== id));
|
||||
track.$.commentPanel.$.deleteComment({ type: 'root' });
|
||||
this.commentDeleted$.next(id);
|
||||
this.revalidate();
|
||||
}
|
||||
@@ -289,6 +322,7 @@ export class DocCommentEntity extends Entity<{
|
||||
};
|
||||
});
|
||||
this.comments$.setValue(updatedComments);
|
||||
track.$.commentPanel.$.deleteComment({ type: 'node' });
|
||||
this.revalidate();
|
||||
}
|
||||
|
||||
@@ -385,6 +419,9 @@ export class DocCommentEntity extends Entity<{
|
||||
this.comments$.setValue(updatedComments);
|
||||
|
||||
this.commentResolved$.next(id);
|
||||
track.$.commentPanel.$.resolveComment({
|
||||
type: resolved ? 'on' : 'off',
|
||||
});
|
||||
this.revalidate();
|
||||
} catch (error) {
|
||||
console.error('Failed to resolve comment:', error);
|
||||
@@ -459,6 +496,7 @@ export class DocCommentEntity extends Entity<{
|
||||
|
||||
// Initial load
|
||||
this.revalidate();
|
||||
this.revalidateCommentsInEditor();
|
||||
|
||||
// Set up polling every 10 seconds
|
||||
const polling$ = timer(10000, 10000).pipe(
|
||||
@@ -508,6 +546,7 @@ export class DocCommentEntity extends Entity<{
|
||||
|
||||
return allComments;
|
||||
}).pipe(
|
||||
tap(() => this.revalidateCommentsInEditor()),
|
||||
catchError(error => {
|
||||
console.error('Failed to fetch comments:', error);
|
||||
return of(null);
|
||||
@@ -642,7 +681,7 @@ export class DocCommentEntity extends Entity<{
|
||||
const allComments: DocComment[] = [];
|
||||
let cursor = '';
|
||||
let firstResult: DocCommentListResult | null = null;
|
||||
|
||||
this.revalidateCommentsInEditor();
|
||||
// Fetch all pages of comments
|
||||
while (true) {
|
||||
const result = await this.store.listComments({ after: cursor });
|
||||
@@ -661,6 +700,7 @@ export class DocCommentEntity extends Entity<{
|
||||
return allComments;
|
||||
}).pipe(
|
||||
tap(allComments => {
|
||||
this.revalidateCommentsInEditor();
|
||||
// Update state with all comments
|
||||
this.comments$.setValue(allComments);
|
||||
}),
|
||||
@@ -675,6 +715,18 @@ export class DocCommentEntity extends Entity<{
|
||||
})
|
||||
);
|
||||
|
||||
private readonly revalidateCommentsInEditor = () => {
|
||||
this.commentsInEditor$.setValue(this.getCommentsInEditor());
|
||||
};
|
||||
|
||||
private getCommentsInEditor(): string[] {
|
||||
const inlineCommentManager = this.props.std?.get(InlineCommentManager);
|
||||
if (!inlineCommentManager) {
|
||||
return [];
|
||||
}
|
||||
return inlineCommentManager.getCommentsInEditor();
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this.stop();
|
||||
this.commentAdded$.complete();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { BlockStdScope } from '@blocksuite/std';
|
||||
import { ObjectPool, Service } from '@toeverything/infra';
|
||||
|
||||
import { DocCommentEntity } from '../entities/doc-comment';
|
||||
@@ -9,6 +10,8 @@ export class DocCommentManagerService extends Service {
|
||||
super();
|
||||
}
|
||||
|
||||
std: BlockStdScope | null = null;
|
||||
|
||||
private readonly pool = new ObjectPool<DocId, DocCommentEntity>({
|
||||
onDelete: entity => {
|
||||
entity.dispose();
|
||||
@@ -18,9 +21,21 @@ export class DocCommentManagerService extends Service {
|
||||
get(docId: DocId) {
|
||||
let commentRef = this.pool.get(docId);
|
||||
if (!commentRef) {
|
||||
const comment = this.framework.createEntity(DocCommentEntity, {
|
||||
docId,
|
||||
});
|
||||
const props = new Proxy(
|
||||
{
|
||||
docId,
|
||||
std: this.std,
|
||||
},
|
||||
{
|
||||
get: (target, prop) => {
|
||||
if (prop === 'std') {
|
||||
return this.std;
|
||||
}
|
||||
return target[prop as keyof typeof target];
|
||||
},
|
||||
}
|
||||
);
|
||||
const comment = this.framework.createEntity(DocCommentEntity, props);
|
||||
commentRef = this.pool.put(docId, comment);
|
||||
// todo: add LRU cache for the pool?
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user