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:
Peng Xiao
2025-07-09 12:49:46 +08:00
committed by GitHub
parent ce7fffda08
commit a50270fc03
17 changed files with 194 additions and 31 deletions

View File

@@ -305,7 +305,10 @@ export class PageRootBlockComponent extends BlockComponent<RootBlockModel> {
); );
// make sure there is a block can be focused // make sure there is a block can be focused
if (notes.length === 0 || notes[notes.length - 1].children.length === 0) { if (
!this.store.readonly$.value &&
(notes.length === 0 || notes[notes.length - 1].children.length === 0)
) {
this.std.command.exec(appendParagraphCommand); this.std.command.exec(appendParagraphCommand);
return; return;
} }
@@ -322,7 +325,7 @@ export class PageRootBlockComponent extends BlockComponent<RootBlockModel> {
parseFloat(paddingLeft), parseFloat(paddingLeft),
parseFloat(paddingRight) parseFloat(paddingRight)
); );
if (!isClickOnBlankArea) { if (!isClickOnBlankArea && !this.store.readonly$.value) {
const lastBlock = notes[notes.length - 1].lastChild(); const lastBlock = notes[notes.length - 1].lastChild();
if ( if (
!lastBlock || !lastBlock ||

View File

@@ -1,2 +1,4 @@
export { InlineCommentManager } from './inline-comment-manager';
export * from './inline-spec'; export * from './inline-spec';
export * from './utils'; export * from './utils';
export * from './view';

View File

@@ -5,6 +5,7 @@ import {
type CommentId, type CommentId,
CommentProviderIdentifier, CommentProviderIdentifier,
findAllCommentedBlocks, findAllCommentedBlocks,
findAllCommentedElements,
} from '@blocksuite/affine-shared/services'; } from '@blocksuite/affine-shared/services';
import type { AffineInlineEditor } from '@blocksuite/affine-shared/types'; import type { AffineInlineEditor } from '@blocksuite/affine-shared/types';
import { DisposableGroup } from '@blocksuite/global/disposable'; import { DisposableGroup } from '@blocksuite/global/disposable';
@@ -64,15 +65,8 @@ export class InlineCommentManager extends LifeCycleWatcher {
if (!provider) return; if (!provider) return;
const commentsInProvider = await provider.getComments('unresolved'); const commentsInProvider = await provider.getComments('unresolved');
const inlineComments = [...findAllCommentedTexts(this.std.store).values()];
const blockComments = findAllCommentedBlocks(this.std.store).flatMap( const commentsInEditor = this.getCommentsInEditor();
block => Object.keys(block.props.comments)
);
const commentsInEditor = [
...new Set([...inlineComments, ...blockComments]),
];
// remove comments that are in editor but not in provider // remove comments that are in editor but not in provider
// which means the comment may be removed or resolved in provider side // which means the comment may be removed or resolved in provider side
@@ -82,6 +76,24 @@ export class InlineCommentManager extends LifeCycleWatcher {
}); });
} }
getCommentsInEditor() {
const inlineComments = [...findAllCommentedTexts(this.std.store).values()];
const blockComments = findAllCommentedBlocks(this.std.store).flatMap(
block => Object.keys(block.props.comments)
);
const surfaceComments = findAllCommentedElements(this.std.store).flatMap(
element => Object.keys(element.comments)
);
const commentsInEditor = [
...new Set([...inlineComments, ...blockComments, ...surfaceComments]),
];
return commentsInEditor;
}
private readonly _handleAddComment = ( private readonly _handleAddComment = (
id: CommentId, id: CommentId,
selections: BaseSelection[] selections: BaseSelection[]

View File

@@ -36,3 +36,14 @@ export const CommentInlineSpecExtension =
>`, >`,
wrapper: true, wrapper: true,
}); });
export const NullCommentInlineSpecExtension =
InlineSpecExtension<AffineTextAttributes>({
name: 'comment',
schema: dynamicSchema(
isInlineCommendId,
z.boolean().optional().nullable().catch(undefined)
),
match: () => false,
renderer: () => html``,
});

View File

@@ -2,21 +2,41 @@ import {
type ViewExtensionContext, type ViewExtensionContext,
ViewExtensionProvider, ViewExtensionProvider,
} from '@blocksuite/affine-ext-loader'; } from '@blocksuite/affine-ext-loader';
import z from 'zod';
import { effects } from './effects'; import { effects } from './effects';
import { InlineCommentManager } from './inline-comment-manager'; import { InlineCommentManager } from './inline-comment-manager';
import { CommentInlineSpecExtension } from './inline-spec'; import {
CommentInlineSpecExtension,
NullCommentInlineSpecExtension,
} from './inline-spec';
export class InlineCommentViewExtension extends ViewExtensionProvider { const optionsSchema = z.object({
enabled: z.boolean().optional().default(true),
});
export class InlineCommentViewExtension extends ViewExtensionProvider<
z.infer<typeof optionsSchema>
> {
override name = 'affine-inline-comment'; override name = 'affine-inline-comment';
override schema = optionsSchema;
override effect(): void { override effect(): void {
super.effect(); super.effect();
effects(); effects();
} }
override setup(context: ViewExtensionContext) { override setup(
super.setup(context); context: ViewExtensionContext,
context.register([CommentInlineSpecExtension, InlineCommentManager]); options?: z.infer<typeof optionsSchema>
) {
super.setup(context, options);
context.register([
options?.enabled
? CommentInlineSpecExtension
: NullCommentInlineSpecExtension,
InlineCommentManager,
]);
} }
} }

View File

@@ -59,7 +59,7 @@ interface BlocksuiteEditorProps {
defaultOpenProperty?: DefaultOpenProperty; defaultOpenProperty?: DefaultOpenProperty;
} }
const usePatchSpecs = (mode: DocMode) => { const usePatchSpecs = (mode: DocMode, shared?: boolean) => {
const [reactToLit, portals] = useLitPortalFactory(); const [reactToLit, portals] = useLitPortalFactory();
const { workspaceService, featureFlagService } = useServices({ const { workspaceService, featureFlagService } = useServices({
WorkspaceService, WorkspaceService,
@@ -86,7 +86,8 @@ const usePatchSpecs = (mode: DocMode) => {
const serverConfig = useLiveData(serverService.server.config$); const serverConfig = useLiveData(serverService.server.config$);
// comment may not be supported by the server // comment may not be supported by the server
const enableComment = serverConfig.features.includes(ServerFeature.Comment); const enableComment =
serverConfig.features.includes(ServerFeature.Comment) && !shared;
const patchedSpecs = useMemo(() => { const patchedSpecs = useMemo(() => {
const manager = getViewManager() const manager = getViewManager()
@@ -206,7 +207,7 @@ export const BlocksuiteDocEditor = forwardRef<
[externalTitleRef] [externalTitleRef]
); );
const [specs, portals] = usePatchSpecs('page'); const [specs, portals] = usePatchSpecs('page', shared);
const displayBiDirectionalLink = useLiveData( const displayBiDirectionalLink = useLiveData(
editorSettingService.editorSetting.settings$.selector( editorSettingService.editorSetting.settings$.selector(

View File

@@ -33,6 +33,7 @@ import type {
import { ViewExtensionManager } from '@blocksuite/affine/ext-loader'; import { ViewExtensionManager } from '@blocksuite/affine/ext-loader';
import { getInternalViewExtensions } from '@blocksuite/affine/extensions/view'; import { getInternalViewExtensions } from '@blocksuite/affine/extensions/view';
import { FoundationViewExtension } from '@blocksuite/affine/foundation/view'; import { FoundationViewExtension } from '@blocksuite/affine/foundation/view';
import { InlineCommentViewExtension } from '@blocksuite/affine/inlines/comment';
import { AffineCanvasTextFonts } from '@blocksuite/affine/shared/services'; import { AffineCanvasTextFonts } from '@blocksuite/affine/shared/services';
import { LinkedDocViewExtension } from '@blocksuite/affine/widgets/linked-doc/view'; import { LinkedDocViewExtension } from '@blocksuite/affine/widgets/linked-doc/view';
import type { FrameworkProvider } from '@toeverything/infra'; import type { FrameworkProvider } from '@toeverything/infra';
@@ -340,6 +341,11 @@ class ViewProvider {
enableComment, enableComment,
framework, framework,
}); });
this._manager.configure(InlineCommentViewExtension, {
enabled: enableComment,
});
return this.config; return this.config;
}; };
} }

View File

@@ -128,6 +128,7 @@ class AffineCommentService implements CommentProvider {
private readonly framework: FrameworkProvider private readonly framework: FrameworkProvider
) { ) {
this.docCommentManager = framework.get(DocCommentManagerService); this.docCommentManager = framework.get(DocCommentManagerService);
this.docCommentManager.std = std;
} }
private get currentDocId(): string { private get currentDocId(): string {

View File

@@ -1,6 +1,7 @@
import { CloudViewExtension } from '@affine/core/blocksuite/view-extensions/cloud'; import { CloudViewExtension } from '@affine/core/blocksuite/view-extensions/cloud';
import { AffineEditorViewExtension } from '@affine/core/blocksuite/view-extensions/editor-view/editor-view'; import { AffineEditorViewExtension } from '@affine/core/blocksuite/view-extensions/editor-view/editor-view';
import { AffineThemeViewExtension } from '@affine/core/blocksuite/view-extensions/theme'; import { AffineThemeViewExtension } from '@affine/core/blocksuite/view-extensions/theme';
import { I18n } from '@affine/i18n';
import { CodeBlockViewExtension } from '@blocksuite/affine/blocks/code/view'; import { CodeBlockViewExtension } from '@blocksuite/affine/blocks/code/view';
import { DividerViewExtension } from '@blocksuite/affine/blocks/divider/view'; import { DividerViewExtension } from '@blocksuite/affine/blocks/divider/view';
import { LatexViewExtension as LatexBlockViewExtension } from '@blocksuite/affine/blocks/latex/view'; import { LatexViewExtension as LatexBlockViewExtension } from '@blocksuite/affine/blocks/latex/view';
@@ -154,7 +155,7 @@ export function getCommentEditorViewManager(framework: FrameworkProvider) {
manager.configure(ParagraphViewExtension, { manager.configure(ParagraphViewExtension, {
getPlaceholder: () => { getPlaceholder: () => {
return ''; return I18n.t('com.affine.notification.comment-prompt');
}, },
}); });

View File

@@ -377,7 +377,7 @@ const CommentItem = ({
entity.dismissDraftReply(); entity.dismissDraftReply();
}, [entity, pendingReply]); }, [entity, pendingReply]);
const handleClickPreview = useCallback(() => { const handleClick = useCallback(() => {
workbench.workbench.openDoc( workbench.workbench.openDoc(
{ {
docId: entity.props.docId, docId: entity.props.docId,
@@ -470,6 +470,10 @@ const CommentItem = ({
const canDelete = const canDelete =
(isMyComment && canCreateComment) || (!isMyComment && canDeleteComment); (isMyComment && canCreateComment) || (!isMyComment && canDeleteComment);
const isCommentInEditor = useLiveData(entity.commentsInEditor$).includes(
comment.id
);
// invalid comment, should not happen // invalid comment, should not happen
if (!comment.content) { if (!comment.content) {
return null; return null;
@@ -477,7 +481,7 @@ const CommentItem = ({
return ( return (
<div <div
onClick={handleClickPreview} onClick={handleClick}
data-comment-id={comment.id} data-comment-id={comment.id}
data-resolved={comment.resolved} data-resolved={comment.resolved}
data-highlighting={highlighting || menuOpen} data-highlighting={highlighting || menuOpen}
@@ -512,7 +516,12 @@ const CommentItem = ({
onDelete={handleDelete} onDelete={handleDelete}
/> />
</div> </div>
<div className={styles.previewContainer}>{comment.content?.preview}</div> <div
data-deleted={!isCommentInEditor}
className={styles.previewContainer}
>
{comment.content?.preview}
</div>
<div className={styles.repliesContainer}> <div className={styles.repliesContainer}>
{isEditing && editingDoc ? ( {isEditing && editingDoc ? (

View File

@@ -130,6 +130,9 @@ export const previewContainer = style({
top: '0', top: '0',
backgroundColor: cssVarV2('block/comment/highlightUnderline'), backgroundColor: cssVarV2('block/comment/highlightUnderline'),
}, },
'&[data-deleted="true"]': {
textDecoration: 'line-through',
},
}, },
}); });

View File

@@ -179,13 +179,14 @@ export class DocCommentStore extends Entity<{
async createComment(commentInput: { async createComment(commentInput: {
content: DocCommentContent; content: DocCommentContent;
mentions?: string[];
}): Promise<DocComment> { }): Promise<DocComment> {
const graphql = this.graphqlService; const graphql = this.graphqlService;
if (!graphql) { if (!graphql) {
throw new Error('GraphQL service not found'); throw new Error('GraphQL service not found');
} }
const mentions = findMentions(commentInput.content.snapshot.blocks); const mentions = commentInput.mentions;
const response = await graphql.gql({ const response = await graphql.gql({
query: createCommentMutation, query: createCommentMutation,
@@ -265,6 +266,7 @@ export class DocCommentStore extends Entity<{
commentId: string, commentId: string,
replyInput: { replyInput: {
content: DocCommentContent; content: DocCommentContent;
mentions?: string[];
} }
): Promise<DocCommentReply> { ): Promise<DocCommentReply> {
const graphql = this.graphqlService; const graphql = this.graphqlService;
@@ -272,8 +274,6 @@ export class DocCommentStore extends Entity<{
throw new Error('GraphQL service not found'); throw new Error('GraphQL service not found');
} }
const mentions = findMentions(replyInput.content.snapshot.blocks);
const response = await graphql.gql({ const response = await graphql.gql({
query: createReplyMutation, query: createReplyMutation,
variables: { variables: {
@@ -282,7 +282,7 @@ export class DocCommentStore extends Entity<{
content: replyInput.content, content: replyInput.content,
docMode: this.props.getDocMode(), docMode: this.props.getDocMode(),
docTitle: this.props.getDocTitle(), docTitle: this.props.getDocTitle(),
mentions: mentions, mentions: replyInput.mentions,
}, },
}, },
}); });

View File

@@ -1,9 +1,12 @@
import { type CommentChangeAction, DocMode } from '@affine/graphql'; import { type CommentChangeAction, DocMode } from '@affine/graphql';
import { track } from '@affine/track';
import { InlineCommentManager } from '@blocksuite/affine/inlines/comment';
import type { import type {
BaseSelection, BaseSelection,
DocSnapshot, DocSnapshot,
Store, Store,
} from '@blocksuite/affine/store'; } from '@blocksuite/affine/store';
import type { BlockStdScope } from '@blocksuite/std';
import { import {
effect, effect,
Entity, Entity,
@@ -38,6 +41,7 @@ import type {
PendingComment, PendingComment,
} from '../types'; } from '../types';
import { DocCommentStore } from './doc-comment-store'; import { DocCommentStore } from './doc-comment-store';
import { findMentions } from './utils';
type DisposeCallback = () => void; type DisposeCallback = () => void;
@@ -50,6 +54,7 @@ type EditingDraft = {
export class DocCommentEntity extends Entity<{ export class DocCommentEntity extends Entity<{
docId: string; docId: string;
std: BlockStdScope | null;
}> { }> {
constructor( constructor(
private readonly snapshotHelper: SnapshotHelper, private readonly snapshotHelper: SnapshotHelper,
@@ -69,6 +74,8 @@ export class DocCommentEntity extends Entity<{
loading$ = new LiveData<boolean>(false); loading$ = new LiveData<boolean>(false);
comments$ = new LiveData<DocComment[]>([]); comments$ = new LiveData<DocComment[]>([]);
commentsInEditor$ = new LiveData<string[]>([]);
// Only one pending comment at a time (for new comments) // Only one pending comment at a time (for new comments)
readonly pendingComment$ = new LiveData<PendingComment | null>(null); readonly pendingComment$ = new LiveData<PendingComment | null>(null);
@@ -195,7 +202,9 @@ export class DocCommentEntity extends Entity<{
attachments: draft.attachments, attachments: draft.attachments,
}); });
} }
track.$.commentPanel.$.editComment({
type: draft.type === 'comment' ? 'root' : 'node',
});
this.editingDraft$.setValue(null); this.editingDraft$.setValue(null);
this.revalidate(); this.revalidate();
} }
@@ -220,6 +229,7 @@ export class DocCommentEntity extends Entity<{
if (!snapshot) { if (!snapshot) {
throw new Error('Failed to get snapshot'); throw new Error('Failed to get snapshot');
} }
const mentions = findMentions(snapshot.blocks);
const comment = await this.store.createComment({ const comment = await this.store.createComment({
content: { content: {
snapshot, snapshot,
@@ -227,6 +237,7 @@ export class DocCommentEntity extends Entity<{
mode: this.docMode$.value ?? 'page', mode: this.docMode$.value ?? 'page',
attachments, attachments,
}, },
mentions,
}); });
const currentComments = this.comments$.value; const currentComments = this.comments$.value;
this.comments$.setValue([...currentComments, comment]); this.comments$.setValue([...currentComments, comment]);
@@ -234,6 +245,19 @@ export class DocCommentEntity extends Entity<{
id: comment.id, id: comment.id,
selections: pendingComment.selections || [], 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.pendingComment$.setValue(null);
this.revalidate(); this.revalidate();
} }
@@ -254,11 +278,13 @@ export class DocCommentEntity extends Entity<{
throw new Error('Pending reply has no commentId'); throw new Error('Pending reply has no commentId');
} }
const mentions = findMentions(snapshot.blocks);
const reply = await this.store.createReply(pendingReply.commentId, { const reply = await this.store.createReply(pendingReply.commentId, {
content: { content: {
snapshot, snapshot,
attachments, attachments,
}, },
mentions,
}); });
const currentComments = this.comments$.value; const currentComments = this.comments$.value;
const updatedComments = currentComments.map(comment => const updatedComments = currentComments.map(comment =>
@@ -267,6 +293,12 @@ export class DocCommentEntity extends Entity<{
: comment : comment
); );
this.comments$.setValue(updatedComments); 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.pendingReply$.setValue(null);
this.revalidate(); this.revalidate();
} }
@@ -275,6 +307,7 @@ export class DocCommentEntity extends Entity<{
await this.store.deleteComment(id); await this.store.deleteComment(id);
const currentComments = this.comments$.value; const currentComments = this.comments$.value;
this.comments$.setValue(currentComments.filter(c => c.id !== id)); this.comments$.setValue(currentComments.filter(c => c.id !== id));
track.$.commentPanel.$.deleteComment({ type: 'root' });
this.commentDeleted$.next(id); this.commentDeleted$.next(id);
this.revalidate(); this.revalidate();
} }
@@ -289,6 +322,7 @@ export class DocCommentEntity extends Entity<{
}; };
}); });
this.comments$.setValue(updatedComments); this.comments$.setValue(updatedComments);
track.$.commentPanel.$.deleteComment({ type: 'node' });
this.revalidate(); this.revalidate();
} }
@@ -385,6 +419,9 @@ export class DocCommentEntity extends Entity<{
this.comments$.setValue(updatedComments); this.comments$.setValue(updatedComments);
this.commentResolved$.next(id); this.commentResolved$.next(id);
track.$.commentPanel.$.resolveComment({
type: resolved ? 'on' : 'off',
});
this.revalidate(); this.revalidate();
} catch (error) { } catch (error) {
console.error('Failed to resolve comment:', error); console.error('Failed to resolve comment:', error);
@@ -459,6 +496,7 @@ export class DocCommentEntity extends Entity<{
// Initial load // Initial load
this.revalidate(); this.revalidate();
this.revalidateCommentsInEditor();
// Set up polling every 10 seconds // Set up polling every 10 seconds
const polling$ = timer(10000, 10000).pipe( const polling$ = timer(10000, 10000).pipe(
@@ -508,6 +546,7 @@ export class DocCommentEntity extends Entity<{
return allComments; return allComments;
}).pipe( }).pipe(
tap(() => this.revalidateCommentsInEditor()),
catchError(error => { catchError(error => {
console.error('Failed to fetch comments:', error); console.error('Failed to fetch comments:', error);
return of(null); return of(null);
@@ -642,7 +681,7 @@ export class DocCommentEntity extends Entity<{
const allComments: DocComment[] = []; const allComments: DocComment[] = [];
let cursor = ''; let cursor = '';
let firstResult: DocCommentListResult | null = null; let firstResult: DocCommentListResult | null = null;
this.revalidateCommentsInEditor();
// Fetch all pages of comments // Fetch all pages of comments
while (true) { while (true) {
const result = await this.store.listComments({ after: cursor }); const result = await this.store.listComments({ after: cursor });
@@ -661,6 +700,7 @@ export class DocCommentEntity extends Entity<{
return allComments; return allComments;
}).pipe( }).pipe(
tap(allComments => { tap(allComments => {
this.revalidateCommentsInEditor();
// Update state with all comments // Update state with all comments
this.comments$.setValue(allComments); 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 { override dispose(): void {
this.stop(); this.stop();
this.commentAdded$.complete(); this.commentAdded$.complete();

View File

@@ -1,3 +1,4 @@
import type { BlockStdScope } from '@blocksuite/std';
import { ObjectPool, Service } from '@toeverything/infra'; import { ObjectPool, Service } from '@toeverything/infra';
import { DocCommentEntity } from '../entities/doc-comment'; import { DocCommentEntity } from '../entities/doc-comment';
@@ -9,6 +10,8 @@ export class DocCommentManagerService extends Service {
super(); super();
} }
std: BlockStdScope | null = null;
private readonly pool = new ObjectPool<DocId, DocCommentEntity>({ private readonly pool = new ObjectPool<DocId, DocCommentEntity>({
onDelete: entity => { onDelete: entity => {
entity.dispose(); entity.dispose();
@@ -18,9 +21,21 @@ export class DocCommentManagerService extends Service {
get(docId: DocId) { get(docId: DocId) {
let commentRef = this.pool.get(docId); let commentRef = this.pool.get(docId);
if (!commentRef) { if (!commentRef) {
const comment = this.framework.createEntity(DocCommentEntity, { const props = new Proxy(
docId, {
}); 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); commentRef = this.pool.put(docId, comment);
// todo: add LRU cache for the pool? // todo: add LRU cache for the pool?
} }

View File

@@ -7754,6 +7754,10 @@ export function useAFFiNEI18N(): {
* `Unsupported message` * `Unsupported message`
*/ */
["com.affine.notification.unsupported"](): string; ["com.affine.notification.unsupported"](): string;
/**
* `What are your thoughts?`
*/
["com.affine.notification.comment-prompt"](): string;
/** /**
* `No new notifications` * `No new notifications`
*/ */

View File

@@ -1936,6 +1936,7 @@
"com.affine.notification.mention": "<1>{{username}}</1> mentioned you in <2>{{docTitle}}</2>", "com.affine.notification.mention": "<1>{{username}}</1> mentioned you in <2>{{docTitle}}</2>",
"com.affine.notification.comment": "<1>{{username}}</1> commented in <2>{{docTitle}}</2>", "com.affine.notification.comment": "<1>{{username}}</1> commented in <2>{{docTitle}}</2>",
"com.affine.notification.comment-mention": "<1>{{username}}</1> mentioned you in a comment in <2>{{docTitle}}</2>", "com.affine.notification.comment-mention": "<1>{{username}}</1> mentioned you in a comment in <2>{{docTitle}}</2>",
"com.affine.notification.comment-prompt": "What are your thoughts?",
"com.affine.notification.empty": "No new notifications", "com.affine.notification.empty": "No new notifications",
"com.affine.notification.loading-more": "Loading more...", "com.affine.notification.loading-more": "Loading more...",
"com.affine.notification.empty.description": "You'll be notified here for @mentions and workspace invites.", "com.affine.notification.empty.description": "You'll be notified here for @mentions and workspace invites.",

View File

@@ -198,6 +198,15 @@ type WorkspaceEmbeddingEvents =
| 'addIgnoredDocs'; | 'addIgnoredDocs';
// END SECTION // END SECTION
// SECTION: comment events
// Add events for comment actions
type CommentEvents =
| 'createComment'
| 'editComment'
| 'deleteComment'
| 'resolveComment';
// END SECTION
type UserEvents = type UserEvents =
| GeneralEvents | GeneralEvents
| AppEvents | AppEvents
@@ -215,6 +224,7 @@ type UserEvents =
| PaymentEvents | PaymentEvents
| DNDEvents | DNDEvents
| AIEvents | AIEvents
| CommentEvents
| AttachmentEvents | AttachmentEvents
| TemplateEvents | TemplateEvents
| NotificationEvents | NotificationEvents
@@ -421,6 +431,9 @@ interface PageEvents extends PageDivision {
chatPanel: { chatPanel: {
chatPanelInput: ['addEmbeddingDoc']; chatPanelInput: ['addEmbeddingDoc'];
}; };
commentPanel: {
$: ['createComment', 'editComment', 'deleteComment', 'resolveComment'];
};
attachment: { attachment: {
$: [ $: [
'openAttachmentInFullscreen', 'openAttachmentInFullscreen',
@@ -807,6 +820,15 @@ export type EventArgs = {
navigatePinedCollectionRouter: { navigatePinedCollectionRouter: {
control: 'all' | 'user-custom-collection'; control: 'all' | 'user-custom-collection';
}; };
resolveComment: { type: 'on' | 'off' };
createComment: {
type: 'root' | 'node';
withAttachment: boolean;
withMention: boolean;
category: string;
};
editComment: { type: 'root' | 'node' };
deleteComment: { type: 'root' | 'node' };
}; };
// for type checking // for type checking