mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-25 10:22:55 +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:
@@ -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 ||
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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[]
|
||||||
|
|||||||
@@ -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``,
|
||||||
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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 ? (
|
||||||
|
|||||||
@@ -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',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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`
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user