feat(editor): block comment extension (#12980)

#### PR Dependency Tree


* **PR #12980** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
This commit is contained in:
L-Sun
2025-07-02 17:42:16 +08:00
committed by GitHub
parent 8ce85f708d
commit d768ad4af0
47 changed files with 432 additions and 38 deletions

View File

@@ -17,6 +17,7 @@ import {
AttachmentBlockStyles, AttachmentBlockStyles,
} from '@blocksuite/affine-model'; } from '@blocksuite/affine-model';
import { import {
BlockCommentManager,
CitationProvider, CitationProvider,
DocModeProvider, DocModeProvider,
FileSizeLimitProvider, FileSizeLimitProvider,
@@ -92,6 +93,12 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
return this.citationService.isCitationModel(this.model); return this.citationService.isCitationModel(this.model);
} }
get isCommentHighlighted() {
return this.std
.get(BlockCommentManager)
.isBlockCommentHighlighted(this.model);
}
convertTo = () => { convertTo = () => {
return this.std return this.std
.get(AttachmentEmbedProvider) .get(AttachmentEmbedProvider)
@@ -499,6 +506,7 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
class=${classMap({ class=${classMap({
'affine-attachment-container': true, 'affine-attachment-container': true,
focused: this.selected$.value, focused: this.selected$.value,
'comment-highlighted': this.isCommentHighlighted,
})} })}
style=${this.containerStyleMap} style=${this.containerStyleMap}
> >

View File

@@ -10,6 +10,7 @@ import {
} from '@blocksuite/affine-shared/consts'; } from '@blocksuite/affine-shared/consts';
import { import {
ActionPlacement, ActionPlacement,
blockCommentToolbarButton,
type ToolbarAction, type ToolbarAction,
type ToolbarActionGroup, type ToolbarActionGroup,
type ToolbarModuleConfig, type ToolbarModuleConfig,
@@ -240,6 +241,10 @@ const builtinToolbarConfig = {
replaceAction, replaceAction,
downloadAction, downloadAction,
captionAction, captionAction,
{
id: 'f.comment',
...blockCommentToolbarButton,
},
{ {
placement: ActionPlacement.More, placement: ActionPlacement.More,
id: 'a.clipboard', id: 'a.clipboard',

View File

@@ -15,6 +15,10 @@ export const styles = css`
} }
} }
.affine-attachment-container.comment-highlighted {
outline: 2px solid ${unsafeCSSVarV2('block/comment/highlightUnderline')};
}
.affine-attachment-card { .affine-attachment-card {
display: flex; display: flex;
gap: 12px; gap: 12px;

View File

@@ -8,6 +8,7 @@ import type {
} from '@blocksuite/affine-model'; } from '@blocksuite/affine-model';
import { ImageProxyService } from '@blocksuite/affine-shared/adapters'; import { ImageProxyService } from '@blocksuite/affine-shared/adapters';
import { import {
BlockCommentManager,
CitationProvider, CitationProvider,
DocModeProvider, DocModeProvider,
LinkPreviewServiceIdentifier, LinkPreviewServiceIdentifier,
@@ -128,6 +129,12 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
return this.std.get(ImageProxyService); return this.std.get(ImageProxyService);
} }
get isCommentHighlighted() {
return this.std
.get(BlockCommentManager)
.isBlockCommentHighlighted(this.model);
}
handleClick = (event: MouseEvent) => { handleClick = (event: MouseEvent) => {
event.stopPropagation(); event.stopPropagation();

View File

@@ -45,6 +45,7 @@ export class BookmarkCard extends SignalWatcher(
[style]: true, [style]: true,
selected: this.bookmark.selected$.value, selected: this.bookmark.selected$.value,
edgeless: isGfxBlockComponent(this.bookmark), edgeless: isGfxBlockComponent(this.bookmark),
'comment-highlighted': this.bookmark.isCommentHighlighted,
}); });
const domainName = url.match( const domainName = url.match(

View File

@@ -17,6 +17,7 @@ import {
} from '@blocksuite/affine-shared/consts'; } from '@blocksuite/affine-shared/consts';
import { import {
ActionPlacement, ActionPlacement,
blockCommentToolbarButton,
EmbedIframeService, EmbedIframeService,
EmbedOptionProvider, EmbedOptionProvider,
type LinkEventType, type LinkEventType,
@@ -288,6 +289,10 @@ const builtinToolbarConfig = {
}, },
} satisfies ToolbarActionGroup<ToolbarAction>, } satisfies ToolbarActionGroup<ToolbarAction>,
captionAction, captionAction,
{
id: 'e.comment',
...blockCommentToolbarButton,
},
{ {
placement: ActionPlacement.More, placement: ActionPlacement.More,
id: 'a.clipboard', id: 'a.clipboard',

View File

@@ -1,4 +1,4 @@
import { unsafeCSSVar } from '@blocksuite/affine-shared/theme'; import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { baseTheme } from '@toeverything/theme'; import { baseTheme } from '@toeverything/theme';
import { css, unsafeCSS } from 'lit'; import { css, unsafeCSS } from 'lit';
@@ -158,6 +158,10 @@ export const styles = css`
border-radius: 4px; border-radius: 4px;
} }
.affine-bookmark-card.comment-highlighted {
outline: 2px solid ${unsafeCSSVarV2('block/comment/highlightUnderline')};
}
.affine-bookmark-card.loading { .affine-bookmark-card.loading {
.affine-bookmark-content-title-text { .affine-bookmark-content-title-text {
color: var(--affine-placeholder-color); color: var(--affine-placeholder-color);

View File

@@ -6,6 +6,7 @@ import {
EDGELESS_TOP_CONTENTEDITABLE_SELECTOR, EDGELESS_TOP_CONTENTEDITABLE_SELECTOR,
} from '@blocksuite/affine-shared/consts'; } from '@blocksuite/affine-shared/consts';
import { import {
BlockCommentManager,
DocModeProvider, DocModeProvider,
NotificationProvider, NotificationProvider,
} from '@blocksuite/affine-shared/services'; } from '@blocksuite/affine-shared/services';
@@ -390,6 +391,12 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
}); });
} }
get isCommentHighlighted() {
return this.std
.get(BlockCommentManager)
.isBlockCommentHighlighted(this.model);
}
override async getUpdateComplete() { override async getUpdateComplete() {
const result = await super.getUpdateComplete(); const result = await super.getUpdateComplete();
await this._richTextElement?.updateComplete; await this._richTextElement?.updateComplete;
@@ -413,6 +420,7 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
<div <div
class=${classMap({ class=${classMap({
'affine-code-block-container': true, 'affine-code-block-container': true,
'highlight-comment': this.isCommentHighlighted,
mobile: IS_MOBILE, mobile: IS_MOBILE,
wrap: this.model.props.wrap, wrap: this.model.props.wrap,
'disable-line-numbers': !showLineNumbers, 'disable-line-numbers': !showLineNumbers,

View File

@@ -7,9 +7,10 @@ import {
WrapIcon, WrapIcon,
} from '@blocksuite/affine-components/icons'; } from '@blocksuite/affine-components/icons';
import type { MenuItemGroup } from '@blocksuite/affine-components/toolbar'; import type { MenuItemGroup } from '@blocksuite/affine-components/toolbar';
import { CommentProviderIdentifier } from '@blocksuite/affine-shared/services';
import { isInsidePageEditor } from '@blocksuite/affine-shared/utils'; import { isInsidePageEditor } from '@blocksuite/affine-shared/utils';
import { noop, sleep } from '@blocksuite/global/utils'; import { noop, sleep } from '@blocksuite/global/utils';
import { NumberedListIcon } from '@blocksuite/icons/lit'; import { CommentIcon, NumberedListIcon } from '@blocksuite/icons/lit';
import { BlockSelection } from '@blocksuite/std'; import { BlockSelection } from '@blocksuite/std';
import { html } from 'lit'; import { html } from 'lit';
import { ifDefined } from 'lit/directives/if-defined.js'; import { ifDefined } from 'lit/directives/if-defined.js';
@@ -113,6 +114,47 @@ export const PRIMARY_GROUPS: MenuItemGroup<CodeBlockToolbarContext>[] = [
}; };
}, },
}, },
{
type: 'comment',
label: 'Comment',
tooltip: 'Comment',
icon: CommentIcon({
width: '20',
height: '20',
}),
when: ({ std }) => !!std.getOptional(CommentProviderIdentifier),
generate: ({ blockComponent }) => {
return {
action: () => {
const commentProvider = blockComponent.std.getOptional(
CommentProviderIdentifier
);
if (!commentProvider) return;
commentProvider.addComment([
new BlockSelection({
blockId: blockComponent.model.id,
}),
]);
},
render: item =>
html`<editor-icon-button
class="code-toolbar-button comment"
aria-label=${ifDefined(item.label)}
.tooltip=${item.label}
.tooltipOffset=${4}
.iconSize=${'16px'}
.iconContainerPadding=${4}
@click=${(e: MouseEvent) => {
e.stopPropagation();
item.action();
}}
>
${item.icon}
</editor-icon-button>`,
};
},
},
], ],
}, },
]; ];

View File

@@ -1,4 +1,5 @@
import { scrollbarStyle } from '@blocksuite/affine-shared/styles'; import { scrollbarStyle } from '@blocksuite/affine-shared/styles';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { css } from 'lit'; import { css } from 'lit';
export const codeBlockStyles = css` export const codeBlockStyles = css`
@@ -20,6 +21,10 @@ export const codeBlockStyles = css`
padding: 12px; padding: 12px;
} }
.affine-code-block-container.highlight-comment {
outline: 2px solid ${unsafeCSSVarV2('block/comment/highlightUnderline')};
}
${scrollbarStyle('.affine-code-block-container rich-text')} ${scrollbarStyle('.affine-code-block-container rich-text')}
.affine-code-block-container .inline-editor { .affine-code-block-container .inline-editor {

View File

@@ -1,3 +1,4 @@
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { stopPropagation } from '@blocksuite/affine-shared/utils'; import { stopPropagation } from '@blocksuite/affine-shared/utils';
import type { DataViewUILogicBase } from '@blocksuite/data-view'; import type { DataViewUILogicBase } from '@blocksuite/data-view';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit'; import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
@@ -72,6 +73,12 @@ export class DatabaseTitle extends SignalWatcher(
.affine-database-title [data-title-focus='true']::before { .affine-database-title [data-title-focus='true']::before {
color: var(--affine-placeholder-color); color: var(--affine-placeholder-color);
} }
.affine-database-title.comment-highlighted {
border-bottom: 2px solid
${unsafeCSSVarV2('block/comment/highlightUnderline')};
background-color: ${unsafeCSSVarV2('block/comment/highlightActive')};
}
`; `;
private readonly compositionEnd = () => { private readonly compositionEnd = () => {
@@ -134,6 +141,7 @@ export class DatabaseTitle extends SignalWatcher(
const classList = classMap({ const classList = classMap({
'affine-database-title': true, 'affine-database-title': true,
ellipsis: !this.isFocus$.value, ellipsis: !this.isFocus$.value,
'comment-highlighted': this.database?.isCommentHighlighted ?? false,
}); });
const untitledStyle = styleMap({ const untitledStyle = styleMap({
height: isEmpty ? 'auto' : 0, height: isEmpty ? 'auto' : 0,

View File

@@ -10,6 +10,8 @@ import { toast } from '@blocksuite/affine-components/toast';
import type { DatabaseBlockModel } from '@blocksuite/affine-model'; import type { DatabaseBlockModel } from '@blocksuite/affine-model';
import { EDGELESS_TOP_CONTENTEDITABLE_SELECTOR } from '@blocksuite/affine-shared/consts'; import { EDGELESS_TOP_CONTENTEDITABLE_SELECTOR } from '@blocksuite/affine-shared/consts';
import { import {
BlockCommentManager,
CommentProviderIdentifier,
DocModeProvider, DocModeProvider,
NotificationProvider, NotificationProvider,
type TelemetryEventMap, type TelemetryEventMap,
@@ -34,11 +36,12 @@ import {
import { widgetPresets } from '@blocksuite/data-view/widget-presets'; import { widgetPresets } from '@blocksuite/data-view/widget-presets';
import { Rect } from '@blocksuite/global/gfx'; import { Rect } from '@blocksuite/global/gfx';
import { import {
CommentIcon,
CopyIcon, CopyIcon,
DeleteIcon, DeleteIcon,
MoreHorizontalIcon, MoreHorizontalIcon,
} from '@blocksuite/icons/lit'; } from '@blocksuite/icons/lit';
import { type BlockComponent } from '@blocksuite/std'; import { type BlockComponent, BlockSelection } from '@blocksuite/std';
import { RANGE_SYNC_EXCLUDE_ATTR } from '@blocksuite/std/inline'; import { RANGE_SYNC_EXCLUDE_ATTR } from '@blocksuite/std/inline';
import { Slice } from '@blocksuite/store'; import { Slice } from '@blocksuite/store';
import { autoUpdate } from '@floating-ui/dom'; import { autoUpdate } from '@floating-ui/dom';
@@ -82,6 +85,18 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
); );
}, },
}), }),
menu.action({
prefix: CommentIcon(),
name: 'Comment',
hide: () => !this.std.getOptional(CommentProviderIdentifier),
select: () => {
this.std.getOptional(CommentProviderIdentifier)?.addComment([
new BlockSelection({
blockId: this.blockId,
}),
]);
},
}),
menu.action({ menu.action({
prefix: CopyIcon(), prefix: CopyIcon(),
name: 'Copy', name: 'Copy',
@@ -297,6 +312,12 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
}; };
} }
get isCommentHighlighted() {
return this.std
.get(BlockCommentManager)
.isBlockCommentHighlighted(this.model);
}
override get topContenteditableElement() { override get topContenteditableElement() {
if (this.std.get(DocModeProvider).getEditorMode() === 'edgeless') { if (this.std.get(DocModeProvider).getEditorMode() === 'edgeless') {
return this.closest<BlockComponent>( return this.closest<BlockComponent>(

View File

@@ -11,6 +11,7 @@ import {
} from '@blocksuite/affine-shared/consts'; } from '@blocksuite/affine-shared/consts';
import { import {
ActionPlacement, ActionPlacement,
blockCommentToolbarButton,
DocDisplayMetaProvider, DocDisplayMetaProvider,
EditorSettingProvider, EditorSettingProvider,
type LinkEventType, type LinkEventType,
@@ -305,6 +306,10 @@ const builtinToolbarConfig = {
}, },
} satisfies ToolbarActionGroup<ToolbarAction>, } satisfies ToolbarActionGroup<ToolbarAction>,
captionAction, captionAction,
{
id: 'e.comment',
...blockCommentToolbarButton,
},
{ {
placement: ActionPlacement.More, placement: ActionPlacement.More,
id: 'a.clipboard', id: 'a.clipboard',

View File

@@ -338,6 +338,7 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
'note-empty': this.isNoteContentEmpty, 'note-empty': this.isNoteContentEmpty,
'in-canvas': inCanvas, 'in-canvas': inCanvas,
[this._cardStyle]: true, [this._cardStyle]: true,
'comment-highlighted': this.isCommentHighlighted,
}); });
const theme = this.std.get(ThemeProvider).theme; const theme = this.std.get(ThemeProvider).theme;

View File

@@ -15,6 +15,10 @@ export const styles = css`
position: relative; position: relative;
} }
.affine-embed-linked-doc-block.comment-highlighted {
outline: 2px solid ${unsafeCSSVarV2('block/comment/highlightUnderline')};
}
.affine-embed-linked-doc-block.in-canvas { .affine-embed-linked-doc-block.in-canvas {
border: 1px solid ${unsafeCSSVarV2('layer/insideBorder/border')}; border: 1px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
background: ${unsafeCSSVarV2('layer/background/linkedDocOnEdgeless')}; background: ${unsafeCSSVarV2('layer/background/linkedDocOnEdgeless')};

View File

@@ -16,6 +16,7 @@ import {
import { REFERENCE_NODE } from '@blocksuite/affine-shared/consts'; import { REFERENCE_NODE } from '@blocksuite/affine-shared/consts';
import { import {
ActionPlacement, ActionPlacement,
blockCommentToolbarButton,
EditorSettingProvider, EditorSettingProvider,
type LinkEventType, type LinkEventType,
type OpenDocMode, type OpenDocMode,
@@ -225,6 +226,10 @@ const builtinToolbarConfig = {
openDocActionGroup, openDocActionGroup,
conversionsActionGroup, conversionsActionGroup,
captionAction, captionAction,
{
id: 'e.comment',
...blockCommentToolbarButton,
},
{ {
placement: ActionPlacement.More, placement: ActionPlacement.More,
id: 'a.clipboard', id: 'a.clipboard',

View File

@@ -232,6 +232,7 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent<EmbedSynce
surface: false, surface: false,
selected: this.selected$.value, selected: this.selected$.value,
'show-hover-border': true, 'show-hover-border': true,
'comment-highlighted': this.isCommentHighlighted,
})} })}
@click=${this._handleClick} @click=${this._handleClick}
style=${containerStyleMap} style=${containerStyleMap}

View File

@@ -57,6 +57,9 @@ export const blockStyles = css`
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
} }
.affine-embed-synced-doc-container.comment-highlighted {
outline: 2px solid ${unsafeCSSVarV2('block/comment/highlightUnderline')};
}
.affine-embed-synced-doc-container.show-hover-border:hover { .affine-embed-synced-doc-container.show-hover-border:hover {
border-color: var(--affine-border-color); border-color: var(--affine-border-color);
} }

View File

@@ -2,17 +2,20 @@ import {
CaptionedBlockComponent, CaptionedBlockComponent,
SelectedStyle, SelectedStyle,
} from '@blocksuite/affine-components/caption'; } from '@blocksuite/affine-components/caption';
import type { EmbedCardStyle } from '@blocksuite/affine-model'; import type { EmbedCardStyle, EmbedProps } from '@blocksuite/affine-model';
import { import {
EMBED_CARD_HEIGHT, EMBED_CARD_HEIGHT,
EMBED_CARD_MIN_WIDTH, EMBED_CARD_MIN_WIDTH,
EMBED_CARD_WIDTH, EMBED_CARD_WIDTH,
} from '@blocksuite/affine-shared/consts'; } from '@blocksuite/affine-shared/consts';
import { DocModeProvider } from '@blocksuite/affine-shared/services'; import {
BlockCommentManager,
DocModeProvider,
} from '@blocksuite/affine-shared/services';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { findAncestorModel } from '@blocksuite/affine-shared/utils'; import { findAncestorModel } from '@blocksuite/affine-shared/utils';
import type { BlockService } from '@blocksuite/std'; import type { BlockService } from '@blocksuite/std';
import { import {
type GfxCompatibleProps,
GfxViewInteractionExtension, GfxViewInteractionExtension,
type ResizeConstraint, type ResizeConstraint,
} from '@blocksuite/std/gfx'; } from '@blocksuite/std/gfx';
@@ -25,7 +28,7 @@ import { type ClassInfo, classMap } from 'lit/directives/class-map.js';
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js'; import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
export class EmbedBlockComponent< export class EmbedBlockComponent<
Model extends BlockModel<GfxCompatibleProps> = BlockModel<GfxCompatibleProps>, Model extends BlockModel<EmbedProps> = BlockModel<EmbedProps>,
Service extends BlockService = BlockService, Service extends BlockService = BlockService,
WidgetName extends string = string, WidgetName extends string = string,
> extends CaptionedBlockComponent<Model, Service, WidgetName> { > extends CaptionedBlockComponent<Model, Service, WidgetName> {
@@ -59,6 +62,12 @@ export class EmbedBlockComponent<
*/ */
protected embedContainerStyle: StyleInfo = {}; protected embedContainerStyle: StyleInfo = {};
get isCommentHighlighted() {
return this.std
.get(BlockCommentManager)
.isBlockCommentHighlighted(this.model);
}
renderEmbed = (content: () => TemplateResult) => { renderEmbed = (content: () => TemplateResult) => {
if ( if (
this._cardStyle === 'horizontal' || this._cardStyle === 'horizontal' ||
@@ -90,6 +99,11 @@ export class EmbedBlockComponent<
style=${styleMap({ style=${styleMap({
height: `${this._cardHeight}px`, height: `${this._cardHeight}px`,
width: '100%', width: '100%',
...(this.isCommentHighlighted
? {
border: `2px solid ${unsafeCSSVarV2('block/comment/highlightUnderline')}`,
}
: {}),
...this.embedContainerStyle, ...this.embedContainerStyle,
})} })}
> >

View File

@@ -1,4 +1,5 @@
import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface'; import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
import type { EmbedProps } from '@blocksuite/affine-model';
import { Bound } from '@blocksuite/global/gfx'; import { Bound } from '@blocksuite/global/gfx';
import { import {
blockComponentSymbol, blockComponentSymbol,
@@ -7,16 +8,13 @@ import {
GfxElementSymbol, GfxElementSymbol,
toGfxBlockComponent, toGfxBlockComponent,
} from '@blocksuite/std'; } from '@blocksuite/std';
import type { import type { GfxBlockElementModel } from '@blocksuite/std/gfx';
GfxBlockElementModel,
GfxCompatibleProps,
} from '@blocksuite/std/gfx';
import type { StyleInfo } from 'lit/directives/style-map.js'; import type { StyleInfo } from 'lit/directives/style-map.js';
import type { EmbedBlockComponent } from './embed-block-element.js'; import type { EmbedBlockComponent } from './embed-block-element.js';
export function toEdgelessEmbedBlock< export function toEdgelessEmbedBlock<
Model extends GfxBlockElementModel<GfxCompatibleProps>, Model extends GfxBlockElementModel<EmbedProps>,
Service extends BlockService, Service extends BlockService,
WidgetName extends string, WidgetName extends string,
B extends typeof EmbedBlockComponent<Model, Service, WidgetName>, B extends typeof EmbedBlockComponent<Model, Service, WidgetName>,

View File

@@ -13,6 +13,7 @@ import {
} from '@blocksuite/affine-shared/consts'; } from '@blocksuite/affine-shared/consts';
import { import {
ActionPlacement, ActionPlacement,
blockCommentToolbarButton,
EmbedOptionProvider, EmbedOptionProvider,
type LinkEventType, type LinkEventType,
type ToolbarAction, type ToolbarAction,
@@ -348,6 +349,10 @@ function createBuiltinToolbarConfigForExternal(
}); });
}, },
}, },
{
id: 'e.comment',
...blockCommentToolbarButton,
},
{ {
placement: ActionPlacement.More, placement: ActionPlacement.More,
id: 'a.clipboard', id: 'a.clipboard',

View File

@@ -18,6 +18,7 @@ import type { BaseSelection } from '@blocksuite/store';
import { computed } from '@preact/signals-core'; import { computed } from '@preact/signals-core';
import { css, html, type PropertyValues } from 'lit'; import { css, html, type PropertyValues } from 'lit';
import { property, query } from 'lit/decorators.js'; import { property, query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { styleMap } from 'lit/directives/style-map.js'; import { styleMap } from 'lit/directives/style-map.js';
import { when } from 'lit/directives/when.js'; import { when } from 'lit/directives/when.js';
@@ -76,6 +77,10 @@ export class ImageBlockPageComponent extends SignalWatcher(
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
affine-page-image .comment-highlighted {
outline: 2px solid ${unsafeCSSVarV2('block/comment/highlightUnderline')};
}
`; `;
resizeable$ = computed(() => this.block.resizeable$.value); resizeable$ = computed(() => this.block.resizeable$.value);
@@ -364,7 +369,13 @@ export class ImageBlockPageComponent extends SignalWatcher(
const { loading, error, icon, description, needUpload } = this.state; const { loading, error, icon, description, needUpload } = this.state;
return html` return html`
<div class="resizable-img" style=${styleMap(imageSize)}> <div
class=${classMap({
'resizable-img': true,
'comment-highlighted': this.block.isCommentHighlighted,
})}
style=${styleMap(imageSize)}
>
<img <img
class="drag-target" class="drag-target"
draggable="false" draggable="false"

View File

@@ -1,6 +1,7 @@
import { ImageBlockModel } from '@blocksuite/affine-model'; import { ImageBlockModel } from '@blocksuite/affine-model';
import { import {
ActionPlacement, ActionPlacement,
blockCommentToolbarButton,
type ToolbarModuleConfig, type ToolbarModuleConfig,
ToolbarModuleExtension, ToolbarModuleExtension,
} from '@blocksuite/affine-shared/services'; } from '@blocksuite/affine-shared/services';
@@ -49,6 +50,10 @@ const builtinToolbarConfig = {
}); });
}, },
}, },
{
id: 'c.comment',
...blockCommentToolbarButton,
},
{ {
placement: ActionPlacement.More, placement: ActionPlacement.More,
id: 'a.clipboard', id: 'a.clipboard',
@@ -141,6 +146,10 @@ const builtinSurfaceToolbarConfig = {
}); });
}, },
}, },
{
id: 'c.comment',
...blockCommentToolbarButton,
},
], ],
when: ctx => ctx.getSurfaceModelsByType(ImageBlockModel).length === 1, when: ctx => ctx.getSurfaceModelsByType(ImageBlockModel).length === 1,

View File

@@ -5,7 +5,10 @@ import { Peekable } from '@blocksuite/affine-components/peek';
import { ResourceController } from '@blocksuite/affine-components/resource'; import { ResourceController } from '@blocksuite/affine-components/resource';
import type { ImageBlockModel } from '@blocksuite/affine-model'; import type { ImageBlockModel } from '@blocksuite/affine-model';
import { ImageSelection } from '@blocksuite/affine-shared/selection'; import { ImageSelection } from '@blocksuite/affine-shared/selection';
import { ToolbarRegistryIdentifier } from '@blocksuite/affine-shared/services'; import {
BlockCommentManager,
ToolbarRegistryIdentifier,
} from '@blocksuite/affine-shared/services';
import { formatSize } from '@blocksuite/affine-shared/utils'; import { formatSize } from '@blocksuite/affine-shared/utils';
import { IS_MOBILE } from '@blocksuite/global/env'; import { IS_MOBILE } from '@blocksuite/global/env';
import { BrokenImageIcon, ImageIcon } from '@blocksuite/icons/lit'; import { BrokenImageIcon, ImageIcon } from '@blocksuite/icons/lit';
@@ -65,6 +68,12 @@ export class ImageBlockComponent extends CaptionedBlockComponent<ImageBlockModel
return this.pageImage?.resizeImg; return this.pageImage?.resizeImg;
} }
get isCommentHighlighted() {
return this.std
.get(BlockCommentManager)
.isBlockCommentHighlighted(this.model);
}
private _handleClick(event: MouseEvent) { private _handleClick(event: MouseEvent) {
// the peek view need handle shift + click // the peek view need handle shift + click
if (event.defaultPrevented) return; if (event.defaultPrevented) return;

View File

@@ -8,6 +8,7 @@ import {
EDGELESS_TOP_CONTENTEDITABLE_SELECTOR, EDGELESS_TOP_CONTENTEDITABLE_SELECTOR,
} from '@blocksuite/affine-shared/consts'; } from '@blocksuite/affine-shared/consts';
import { import {
BlockCommentManager,
CitationProvider, CitationProvider,
DocModeProvider, DocModeProvider,
} from '@blocksuite/affine-shared/services'; } from '@blocksuite/affine-shared/services';
@@ -107,6 +108,12 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
); );
} }
get isCommentHighlighted() {
return this.std
.get(BlockCommentManager)
.isBlockCommentHighlighted(this.model);
}
override get topContenteditableElement() { override get topContenteditableElement() {
if (this.std.get(DocModeProvider).getEditorMode() === 'edgeless') { if (this.std.get(DocModeProvider).getEditorMode() === 'edgeless') {
return this.closest<BlockComponent>( return this.closest<BlockComponent>(
@@ -268,7 +275,10 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
} }
</style> </style>
<div <div
class="affine-paragraph-block-container" class=${classMap({
'affine-paragraph-block-container': true,
'highlight-comment': this.isCommentHighlighted,
})}
data-has-collapsed-siblings="${collapsedSiblings.length > 0}" data-has-collapsed-siblings="${collapsedSiblings.length > 0}"
> >
<div <div

View File

@@ -1,3 +1,4 @@
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { css } from 'lit'; import { css } from 'lit';
export const paragraphBlockStyles = css` export const paragraphBlockStyles = css`
@@ -15,6 +16,11 @@ export const paragraphBlockStyles = css`
position: relative; position: relative;
} }
.affine-paragraph-block-container.highlight-comment {
background-color: ${unsafeCSSVarV2('block/comment/highlightActive')};
outline: 2px solid ${unsafeCSSVarV2('block/comment/highlightUnderline')};
}
affine-paragraph code { affine-paragraph code {
font-size: calc(var(--affine-font-base) - 3px); font-size: calc(var(--affine-font-base) - 3px);
padding: 0px 4px 2px; padding: 0px 4px 2px;

View File

@@ -40,11 +40,10 @@ import type {
} from '@blocksuite/affine-shared/services'; } from '@blocksuite/affine-shared/services';
import { import {
ActionPlacement, ActionPlacement,
CommentProviderIdentifier, blockCommentToolbarButton,
} from '@blocksuite/affine-shared/services'; } from '@blocksuite/affine-shared/services';
import { tableViewMeta } from '@blocksuite/data-view/view-presets'; import { tableViewMeta } from '@blocksuite/data-view/view-presets';
import { import {
CommentIcon,
CopyIcon, CopyIcon,
DatabaseTableViewIcon, DatabaseTableViewIcon,
DeleteIcon, DeleteIcon,
@@ -270,28 +269,17 @@ const turnIntoLinkedDoc = {
}, },
} as const satisfies ToolbarAction; } as const satisfies ToolbarAction;
const commentAction = {
id: 'd.comment',
when: ({ std, chain }) =>
isFormatSupported(chain).run()[0] &&
!!std.getOptional(CommentProviderIdentifier),
icon: CommentIcon(),
run: ({ std }) => {
const commentProvider = std.getOptional(CommentProviderIdentifier);
if (!commentProvider) return;
commentProvider.addComment(std.selection.value);
},
} as const satisfies ToolbarAction;
export const builtinToolbarConfig = { export const builtinToolbarConfig = {
actions: [ actions: [
conversionsActionGroup, conversionsActionGroup,
inlineTextActionGroup, inlineTextActionGroup,
highlightActionGroup, highlightActionGroup,
commentAction,
turnIntoDatabase, turnIntoDatabase,
turnIntoLinkedDoc, turnIntoLinkedDoc,
{
id: 'g.comment',
...blockCommentToolbarButton,
},
{ {
placement: ActionPlacement.More, placement: ActionPlacement.More,
id: 'a.clipboard', id: 'a.clipboard',

View File

@@ -5,6 +5,7 @@ import {
} from '@blocksuite/affine-shared/commands'; } from '@blocksuite/affine-shared/commands';
import { import {
ActionPlacement, ActionPlacement,
blockCommentToolbarButton,
type ToolbarModuleConfig, type ToolbarModuleConfig,
} from '@blocksuite/affine-shared/services'; } from '@blocksuite/affine-shared/services';
import { CaptionIcon, CopyIcon, DeleteIcon } from '@blocksuite/icons/lit'; import { CaptionIcon, CopyIcon, DeleteIcon } from '@blocksuite/icons/lit';
@@ -61,6 +62,10 @@ export const surfaceRefToolbarModuleConfig: ToolbarModuleConfig = {
surfaceRefBlock.captionElement.show(); surfaceRefBlock.captionElement.show();
}, },
}, },
{
id: 'e.comment',
...blockCommentToolbarButton,
},
{ {
id: 'a.clipboard', id: 'a.clipboard',
placement: ActionPlacement.More, placement: ActionPlacement.More,

View File

@@ -13,6 +13,7 @@ import {
type SurfaceRefBlockModel, type SurfaceRefBlockModel,
} from '@blocksuite/affine-model'; } from '@blocksuite/affine-model';
import { import {
BlockCommentManager,
DocModeProvider, DocModeProvider,
EditPropsStore, EditPropsStore,
type OpenDocMode, type OpenDocMode,
@@ -76,6 +77,10 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
border-color: ${unsafeCSSVarV2('edgeless/frame/border/active')}; border-color: ${unsafeCSSVarV2('edgeless/frame/border/active')};
} }
.affine-surface-ref.comment-highlighted {
outline: 2px solid ${unsafeCSSVarV2('block/comment/highlightUnderline')};
}
@media print { @media print {
.affine-surface-ref { .affine-surface-ref {
outline: none !important; outline: none !important;
@@ -137,6 +142,12 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
return this._referencedModel; return this._referencedModel;
} }
get isCommentHighlighted() {
return this.std
.get(BlockCommentManager)
.isBlockCommentHighlighted(this.model);
}
private readonly _handleClick = () => { private readonly _handleClick = () => {
this.selection.update(() => { this.selection.update(() => {
return [this.selection.create(BlockSelection, { blockId: this.blockId })]; return [this.selection.create(BlockSelection, { blockId: this.blockId })];
@@ -456,6 +467,7 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
class=${classMap({ class=${classMap({
'affine-surface-ref': true, 'affine-surface-ref': true,
focused: this.selected$.value, focused: this.selected$.value,
'comment-highlighted': this.isCommentHighlighted,
})} })}
@click=${this._handleClick} @click=${this._handleClick}
> >

View File

@@ -9,6 +9,7 @@ import {
} from '@blocksuite/affine-ext-loader'; } from '@blocksuite/affine-ext-loader';
import { import {
AutoClearSelectionService, AutoClearSelectionService,
BlockCommentManager,
CitationService, CitationService,
DefaultOpenDocExtension, DefaultOpenDocExtension,
DNDAPIExtension, DNDAPIExtension,
@@ -78,6 +79,7 @@ export class FoundationViewExtension extends ViewExtensionProvider<FoundationVie
LinkPreviewCache, LinkPreviewCache,
LinkPreviewService, LinkPreviewService,
CitationService, CitationService,
BlockCommentManager,
]); ]);
context.register(clipboardConfigs); context.register(clipboardConfigs);
if (this.isEdgeless(context.scope)) { if (this.isEdgeless(context.scope)) {

View File

@@ -58,6 +58,8 @@ export type AttachmentBlockProps = {
style?: (typeof AttachmentBlockStyles)[number]; style?: (typeof AttachmentBlockStyles)[number];
footnoteIdentifier: string | null; footnoteIdentifier: string | null;
comments?: Record<string, boolean>;
} & Omit<GfxCommonBlockProps, 'scale'> & } & Omit<GfxCommonBlockProps, 'scale'> &
BlockMeta; BlockMeta;
@@ -78,6 +80,7 @@ export const defaultAttachmentProps: AttachmentBlockProps = {
'meta:createdBy': undefined, 'meta:createdBy': undefined,
'meta:updatedBy': undefined, 'meta:updatedBy': undefined,
footnoteIdentifier: null, footnoteIdentifier: null,
comments: undefined,
}; };
export const AttachmentBlockSchema = defineBlockSchema({ export const AttachmentBlockSchema = defineBlockSchema({

View File

@@ -28,6 +28,7 @@ export type BookmarkBlockProps = {
url: string; url: string;
caption: string | null; caption: string | null;
footnoteIdentifier: string | null; footnoteIdentifier: string | null;
comments?: Record<string, boolean>;
} & LinkPreviewData & } & LinkPreviewData &
Omit<GfxCommonBlockProps, 'scale'> & Omit<GfxCommonBlockProps, 'scale'> &
BlockMeta; BlockMeta;
@@ -52,6 +53,7 @@ const defaultBookmarkProps: BookmarkBlockProps = {
'meta:updatedBy': undefined, 'meta:updatedBy': undefined,
footnoteIdentifier: null, footnoteIdentifier: null,
comments: undefined,
}; };
export const BookmarkBlockSchema = defineBlockSchema({ export const BookmarkBlockSchema = defineBlockSchema({

View File

@@ -14,6 +14,7 @@ type CodeBlockProps = {
caption: string; caption: string;
preview?: boolean; preview?: boolean;
lineNumber?: boolean; lineNumber?: boolean;
comments?: Record<string, boolean>;
} & BlockMeta; } & BlockMeta;
export const CodeBlockSchema = defineBlockSchema({ export const CodeBlockSchema = defineBlockSchema({
@@ -26,6 +27,7 @@ export const CodeBlockSchema = defineBlockSchema({
caption: '', caption: '',
preview: undefined, preview: undefined,
lineNumber: undefined, lineNumber: undefined,
comments: undefined,
'meta:createdAt': undefined, 'meta:createdAt': undefined,
'meta:createdBy': undefined, 'meta:createdBy': undefined,
'meta:updatedAt': undefined, 'meta:updatedAt': undefined,

View File

@@ -16,6 +16,7 @@ export type DatabaseBlockProps = {
title: Text; title: Text;
cells: SerializedCells; cells: SerializedCells;
columns: Array<ColumnDataType>; columns: Array<ColumnDataType>;
comments?: Record<string, boolean>;
}; };
export class DatabaseBlockModel extends BlockModel<DatabaseBlockProps> {} export class DatabaseBlockModel extends BlockModel<DatabaseBlockProps> {}
@@ -27,6 +28,7 @@ export const DatabaseBlockSchema = defineBlockSchema({
title: internal.Text(), title: internal.Text(),
cells: Object.create(null), cells: Object.create(null),
columns: [], columns: [],
comments: undefined,
}), }),
metadata: { metadata: {
role: 'hub', role: 'hub',

View File

@@ -26,6 +26,7 @@ import { DefaultTheme } from '../../themes/default';
type EdgelessTextProps = { type EdgelessTextProps = {
hasMaxWidth: boolean; hasMaxWidth: boolean;
comments?: Record<string, boolean>;
} & Omit<TextStyleProps, 'fontSize'> & } & Omit<TextStyleProps, 'fontSize'> &
GfxCommonBlockProps; GfxCommonBlockProps;
@@ -54,6 +55,7 @@ export const EdgelessTextBlockSchema = defineBlockSchema({
scale: 1, scale: 1,
rotate: 0, rotate: 0,
hasMaxWidth: false, hasMaxWidth: false,
comments: undefined,
...EdgelessTextZodSchema.parse(undefined), ...EdgelessTextZodSchema.parse(undefined),
}), }),
metadata: { metadata: {

View File

@@ -30,6 +30,7 @@ export type FrameBlockProps = {
background: Color; background: Color;
childElementIds?: Record<string, boolean>; childElementIds?: Record<string, boolean>;
presentationIndex?: string; presentationIndex?: string;
comments?: Record<string, boolean>;
} & GfxCompatibleProps; } & GfxCompatibleProps;
export const FrameZodSchema = z export const FrameZodSchema = z
@@ -50,6 +51,7 @@ export const FrameBlockSchema = defineBlockSchema({
childElementIds: Object.create(null), childElementIds: Object.create(null),
presentationIndex: generateKeyBetweenV2(null, null), presentationIndex: generateKeyBetweenV2(null, null),
lockedBySelf: false, lockedBySelf: false,
comments: undefined,
}), }),
metadata: { metadata: {
version: 1, version: 1,

View File

@@ -19,6 +19,7 @@ export type ImageBlockProps = {
height?: number; height?: number;
rotate: number; rotate: number;
size?: number; size?: number;
comments?: Record<string, boolean>;
} & Omit<GfxCommonBlockProps, 'scale'> & } & Omit<GfxCommonBlockProps, 'scale'> &
BlockMeta; BlockMeta;
@@ -32,6 +33,7 @@ const defaultImageProps: ImageBlockProps = {
lockedBySelf: false, lockedBySelf: false,
rotate: 0, rotate: 0,
size: -1, size: -1,
comments: undefined,
'meta:createdAt': undefined, 'meta:createdAt': undefined,
'meta:createdBy': undefined, 'meta:createdBy': undefined,
'meta:updatedAt': undefined, 'meta:updatedAt': undefined,

View File

@@ -11,6 +11,7 @@ import {
export type LatexProps = { export type LatexProps = {
latex: string; latex: string;
comments?: Record<string, boolean>;
} & GfxCommonBlockProps; } & GfxCommonBlockProps;
export const LatexBlockSchema = defineBlockSchema({ export const LatexBlockSchema = defineBlockSchema({
@@ -22,6 +23,7 @@ export const LatexBlockSchema = defineBlockSchema({
scale: 1, scale: 1,
rotate: 0, rotate: 0,
latex: '', latex: '',
comments: undefined,
}), }),
metadata: { metadata: {
version: 1, version: 1,

View File

@@ -16,6 +16,7 @@ export type ListProps = {
checked: boolean; checked: boolean;
collapsed: boolean; collapsed: boolean;
order: number | null; order: number | null;
comments?: Record<string, boolean>;
} & BlockMeta; } & BlockMeta;
export const ListBlockSchema = defineBlockSchema({ export const ListBlockSchema = defineBlockSchema({
@@ -29,6 +30,7 @@ export const ListBlockSchema = defineBlockSchema({
// number type only for numbered list // number type only for numbered list
order: null, order: null,
comments: undefined,
'meta:createdAt': undefined, 'meta:createdAt': undefined,
'meta:createdBy': undefined, 'meta:createdBy': undefined,
'meta:updatedAt': undefined, 'meta:updatedAt': undefined,

View File

@@ -69,6 +69,7 @@ export const NoteBlockSchema = defineBlockSchema({
shadowType: DEFAULT_NOTE_SHADOW, shadowType: DEFAULT_NOTE_SHADOW,
}, },
}, },
comments: undefined,
}), }),
metadata: { metadata: {
version: 1, version: 1,
@@ -91,6 +92,7 @@ export type NoteProps = {
background: Color; background: Color;
displayMode: NoteDisplayMode; displayMode: NoteDisplayMode;
edgeless: NoteEdgelessProps; edgeless: NoteEdgelessProps;
comments?: Record<string, boolean>;
/** /**
* @deprecated * @deprecated
* use `displayMode` instead * use `displayMode` instead

View File

@@ -21,6 +21,7 @@ export type ParagraphProps = {
type: ParagraphType; type: ParagraphType;
text: Text; text: Text;
collapsed: boolean; collapsed: boolean;
comments?: Record<string, boolean>;
} & BlockMeta; } & BlockMeta;
export const ParagraphBlockSchema = defineBlockSchema({ export const ParagraphBlockSchema = defineBlockSchema({
@@ -29,6 +30,7 @@ export const ParagraphBlockSchema = defineBlockSchema({
type: 'text', type: 'text',
text: internal.Text(), text: internal.Text(),
collapsed: false, collapsed: false,
comments: undefined,
'meta:createdAt': undefined, 'meta:createdAt': undefined,
'meta:createdBy': undefined, 'meta:createdBy': undefined,
'meta:updatedAt': undefined, 'meta:updatedAt': undefined,

View File

@@ -8,14 +8,16 @@ export type SurfaceRefProps = {
reference: string; reference: string;
caption: string; caption: string;
refFlavour: string; refFlavour: string;
comments?: Record<string, boolean>;
}; };
export const SurfaceRefBlockSchema = defineBlockSchema({ export const SurfaceRefBlockSchema = defineBlockSchema({
flavour: 'affine:surface-ref', flavour: 'affine:surface-ref',
props: () => ({ props: (): SurfaceRefProps => ({
reference: '', reference: '',
caption: '', caption: '',
refFlavour: '', refFlavour: '',
comments: undefined,
}), }),
metadata: { metadata: {
version: 1, version: 1,

View File

@@ -29,6 +29,7 @@ export interface TableBlockProps extends BlockMeta {
columns: Record<string, TableColumn>; columns: Record<string, TableColumn>;
// key = `${rowId}:${columnId}` // key = `${rowId}:${columnId}`
cells: Record<string, TableCell>; cells: Record<string, TableCell>;
comments?: Record<string, boolean>;
} }
export interface TableCellSerialized { export interface TableCellSerialized {
@@ -51,6 +52,7 @@ export const TableBlockSchema = defineBlockSchema({
rows: {}, rows: {},
columns: {}, columns: {},
cells: {}, cells: {},
comments: undefined,
'meta:createdAt': undefined, 'meta:createdAt': undefined,
'meta:createdBy': undefined, 'meta:createdBy': undefined,
'meta:updatedAt': undefined, 'meta:updatedAt': undefined,

View File

@@ -10,7 +10,11 @@ import {
import type { BlockMeta } from './types'; import type { BlockMeta } from './types';
export type EmbedProps<Props = object> = Props & GfxCompatibleProps & BlockMeta; export type EmbedProps<Props = object> = Props &
GfxCompatibleProps &
BlockMeta & {
comments?: Record<string, boolean>;
};
export function defineEmbedModel< export function defineEmbedModel<
Props extends object, Props extends object,
@@ -52,6 +56,7 @@ export function createEmbedBlockSchema<
xywh: '[0,0,0,0]', xywh: '[0,0,0,0]',
lockedBySelf: false, lockedBySelf: false,
rotate: 0, rotate: 0,
comments: undefined,
'meta:createdAt': undefined, 'meta:createdAt': undefined,
'meta:updatedAt': undefined, 'meta:updatedAt': undefined,
'meta:createdBy': undefined, 'meta:createdBy': undefined,

View File

@@ -0,0 +1,118 @@
import { DividerBlockModel } from '@blocksuite/affine-model';
import { DisposableGroup } from '@blocksuite/global/disposable';
import {
BlockSelection,
LifeCycleWatcher,
TextSelection,
} from '@blocksuite/std';
import type { BaseSelection, BlockModel } from '@blocksuite/store';
import { signal } from '@preact/signals-core';
import { getSelectedBlocksCommand } from '../../commands';
import { ImageSelection } from '../../selection';
import { matchModels } from '../../utils';
import { type CommentId, CommentProviderIdentifier } from './comment-provider';
import { findCommentedBlocks } from './utils';
export class BlockCommentManager extends LifeCycleWatcher {
static override key = 'block-comment-manager';
private readonly _highlightedCommentId$ = signal<CommentId | null>(null);
private readonly _disposables = new DisposableGroup();
private get _provider() {
return this.std.getOptional(CommentProviderIdentifier);
}
isBlockCommentHighlighted(
block: BlockModel<{ comments?: Record<CommentId, boolean> }>
) {
const comments = block.props.comments;
if (!comments) return false;
return (
this._highlightedCommentId$.value !== null &&
Object.keys(comments).includes(this._highlightedCommentId$.value)
);
}
override mounted() {
const provider = this._provider;
if (!provider) return;
this._disposables.add(provider.onCommentAdded(this._handleAddComment));
this._disposables.add(
provider.onCommentDeleted(this._handleDeleteAndResolve)
);
this._disposables.add(
provider.onCommentResolved(this._handleDeleteAndResolve)
);
this._disposables.add(
provider.onCommentHighlighted(this._handleHighlightComment)
);
}
override unmounted() {
this._disposables.dispose();
}
private readonly _handleAddComment = (
id: CommentId,
selections: BaseSelection[]
) => {
const blocksFromTextRange = selections
.filter((s): s is TextSelection => s.is(TextSelection))
.map(s => {
const [_, { selectedBlocks }] = this.std.command.exec(
getSelectedBlocksCommand,
{
textSelection: s,
}
);
if (!selectedBlocks) return [];
return selectedBlocks.map(b => b.model);
});
const needCommentBlocks = [
...blocksFromTextRange.flat(),
...selections
.filter(s => s instanceof BlockSelection || s instanceof ImageSelection)
.map(({ blockId }) => this.std.store.getModelById(blockId))
.filter(
(m): m is BlockModel =>
m !== null && !matchModels(m, [DividerBlockModel])
),
];
if (needCommentBlocks.length === 0) return;
this.std.store.withoutTransact(() => {
needCommentBlocks.forEach(block => {
const comments = (
'comments' in block.props &&
typeof block.props.comments === 'object' &&
block.props.comments !== null
? block.props.comments
: {}
) as Record<CommentId, boolean>;
this.std.store.updateBlock(block, {
comments: { [id]: true, ...comments },
});
});
});
};
private readonly _handleDeleteAndResolve = (id: CommentId) => {
const commentedBlocks = findCommentedBlocks(this.std.store, id);
this.std.store.withoutTransact(() => {
commentedBlocks.forEach(block => {
delete block.props.comments[id];
});
});
};
private readonly _handleHighlightComment = (id: CommentId | null) => {
this._highlightedCommentId$.value = id;
};
}

View File

@@ -1 +1,3 @@
export * from './block-comment-manager';
export * from './comment-provider'; export * from './comment-provider';
export * from './utils';

View File

@@ -1,9 +1,45 @@
import type { Store } from '@blocksuite/store'; import { CommentIcon } from '@blocksuite/icons/lit';
import { BlockSelection } from '@blocksuite/std';
import type { BlockModel, Store } from '@blocksuite/store';
import type { CommentId } from './comment-provider'; import type { ToolbarAction } from '../toolbar-service';
import { type CommentId, CommentProviderIdentifier } from './comment-provider';
export function findCommentedBlocks(store: Store, commentId: CommentId) { export function findCommentedBlocks(store: Store, commentId: CommentId) {
return store.getAllModels().filter(block => { type CommentedBlock = BlockModel<{ comments: Record<CommentId, boolean> }>;
return 'comment' in block.props && block.props.comment === commentId; 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
);
}); });
} }
export const blockCommentToolbarButton: Omit<ToolbarAction, 'id'> = {
tooltip: 'Comment',
when: ({ std }) => !!std.getOptional(CommentProviderIdentifier),
icon: CommentIcon(),
run: ctx => {
const commentProvider = ctx.std.getOptional(CommentProviderIdentifier);
if (!commentProvider) return;
const selections = ctx.selection.value;
const model = ctx.getCurrentModel();
if (selections.length > 1) {
commentProvider.addComment(selections);
} else if (model) {
commentProvider.addComment([
new BlockSelection({
blockId: model.id,
}),
]);
} else if (selections.length === 1) {
commentProvider.addComment(selections);
} else {
return;
}
},
};