mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-17 14:27:02 +08:00
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:
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -1 +1,3 @@
|
||||
export * from './block-comment-manager';
|
||||
export * from './comment-provider';
|
||||
export * from './utils';
|
||||
|
||||
@@ -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) {
|
||||
return store.getAllModels().filter(block => {
|
||||
return 'comment' in block.props && block.props.comment === commentId;
|
||||
type CommentedBlock = BlockModel<{ comments: Record<CommentId, boolean> }>;
|
||||
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;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user