mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-21 00:07:01 +08:00
fix(core): comment mention filters (#13062)
#### PR Dependency Tree * **PR #13062** 👈 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 * **New Features** * Replies in comment threads are now collapsed when there are more than four, with an option to expand and view all replies. * Mentions within comments and replies are automatically detected and tracked. * "Show more replies" indicator is now localized for English users. * **Improvements** * Filtering for "only my replies" now includes replies where you are mentioned. * Enhanced focus behavior in the comment editor for improved usability. * Updated styling for active and collapsed reply states in comment threads. * **Bug Fixes** * Ensured consistent handling of mentions and reply associations in comment data. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -108,6 +108,15 @@ export const CommentEditor = forwardRef<CommentEditorRef, CommentEditorProps>(
|
|||||||
[doc, snapshotHelper]
|
[doc, snapshotHelper]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const focusEditor = useCallback(() => {
|
||||||
|
if (editorRef.current) {
|
||||||
|
const lastChild = editorRef.current.std.store.root?.lastChild();
|
||||||
|
if (lastChild) {
|
||||||
|
focusTextModel(editorRef.current.std, lastChild.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [editorRef]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancel = false;
|
let cancel = false;
|
||||||
if (autoFocus && editorRef.current && doc) {
|
if (autoFocus && editorRef.current && doc) {
|
||||||
@@ -119,25 +128,20 @@ export const CommentEditor = forwardRef<CommentEditorRef, CommentEditorProps>(
|
|||||||
'rich-text'
|
'rich-text'
|
||||||
) as unknown as RichText;
|
) as unknown as RichText;
|
||||||
if (!richText) return;
|
if (!richText) return;
|
||||||
|
|
||||||
// Finally focus the inline editor
|
|
||||||
const inlineEditor = richText.inlineEditor;
|
|
||||||
richText.focus();
|
|
||||||
|
|
||||||
richText.scrollIntoView({
|
richText.scrollIntoView({
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
block: 'center',
|
block: 'center',
|
||||||
});
|
});
|
||||||
|
// Finally focus the inline editor
|
||||||
// fixme: the following does not work
|
richText.focus();
|
||||||
inlineEditor?.focusEnd();
|
focusEditor();
|
||||||
})
|
})
|
||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
cancel = true;
|
cancel = true;
|
||||||
};
|
};
|
||||||
}, [autoFocus, doc]);
|
}, [autoFocus, doc, focusEditor]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (doc) {
|
if (doc) {
|
||||||
@@ -179,14 +183,9 @@ export const CommentEditor = forwardRef<CommentEditorRef, CommentEditorProps>(
|
|||||||
const handleClickEditor = useCallback(
|
const handleClickEditor = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (editorRef.current) {
|
focusEditor();
|
||||||
const lastChild = editorRef.current.std.store.root?.lastChild();
|
|
||||||
if (lastChild) {
|
|
||||||
focusTextModel(editorRef.current.std, lastChild.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[editorRef]
|
[focusEditor]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const container = style({
|
|||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
padding: '0 8px',
|
padding: '0 8px',
|
||||||
},
|
},
|
||||||
'&[data-readonly="false"]:focus-within': {
|
'&[data-readonly="false"]:is(:focus-within, :active)': {
|
||||||
borderColor: cssVarV2('layer/insideBorder/primaryBorder'),
|
borderColor: cssVarV2('layer/insideBorder/primaryBorder'),
|
||||||
boxShadow: cssVar('activeShadow'),
|
boxShadow: cssVar('activeShadow'),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -293,6 +293,74 @@ const CommentItem = ({
|
|||||||
|
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
// When the comment item is rendered the first time, the replies will be collapsed by default
|
||||||
|
// The replies will be collapsed when replies length > 4, that is, the comment, first reply and the last 2 replies
|
||||||
|
// will be shown
|
||||||
|
// When new reply is added either by clicking the reply button or synced remotely, we will NOT collapse the replies
|
||||||
|
const [collapsed, setCollapsed] = useState(
|
||||||
|
(comment.replies?.length ?? 0) > 4
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderedReplies = useMemo(() => {
|
||||||
|
// Sort replies ascending by createdAt
|
||||||
|
const sortedReplies =
|
||||||
|
comment.replies?.toSorted((a, b) => a.createdAt - b.createdAt) ?? [];
|
||||||
|
if (sortedReplies.length === 0) return null;
|
||||||
|
|
||||||
|
// If not collapsed or replies are fewer than threshold, render all
|
||||||
|
if (!collapsed || sortedReplies.length <= 4) {
|
||||||
|
return sortedReplies.map(reply => (
|
||||||
|
<ReadonlyCommentRenderer
|
||||||
|
key={reply.id}
|
||||||
|
avatarUrl={reply.user.avatarUrl}
|
||||||
|
name={reply.user.name}
|
||||||
|
time={reply.createdAt}
|
||||||
|
snapshot={reply.content.snapshot}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collapsed state: first reply + collapsed indicator + last two replies
|
||||||
|
const firstReply = sortedReplies[0];
|
||||||
|
const tailReplies = sortedReplies.slice(-2);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{firstReply && (
|
||||||
|
<ReadonlyCommentRenderer
|
||||||
|
key={firstReply.id}
|
||||||
|
avatarUrl={firstReply.user.avatarUrl}
|
||||||
|
name={firstReply.user.name}
|
||||||
|
time={firstReply.createdAt}
|
||||||
|
snapshot={firstReply.content.snapshot}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={styles.collapsedReplies}
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setCollapsed(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={styles.collapsedRepliesTitle}>
|
||||||
|
{t['com.affine.comment.reply.show-more']({
|
||||||
|
count: (sortedReplies.length - 4).toString(),
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{tailReplies.map(reply => (
|
||||||
|
<ReadonlyCommentRenderer
|
||||||
|
key={reply.id}
|
||||||
|
avatarUrl={reply.user.avatarUrl}
|
||||||
|
name={reply.user.name}
|
||||||
|
time={reply.createdAt}
|
||||||
|
snapshot={reply.content.snapshot}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}, [collapsed, comment.replies, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={handleClickPreview}
|
onClick={handleClickPreview}
|
||||||
@@ -353,19 +421,7 @@ const CommentItem = ({
|
|||||||
time={comment.createdAt}
|
time={comment.createdAt}
|
||||||
snapshot={comment.content.snapshot}
|
snapshot={comment.content.snapshot}
|
||||||
/>
|
/>
|
||||||
|
{renderedReplies}
|
||||||
{/* unlike comment, replies are sorted by createdAt in ascending order */}
|
|
||||||
{comment.replies
|
|
||||||
?.toSorted((a, b) => a.createdAt - b.createdAt)
|
|
||||||
.map(reply => (
|
|
||||||
<ReadonlyCommentRenderer
|
|
||||||
key={reply.id}
|
|
||||||
avatarUrl={reply.user.avatarUrl}
|
|
||||||
name={reply.user.name}
|
|
||||||
time={reply.createdAt}
|
|
||||||
snapshot={reply.content.snapshot}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{highlighting &&
|
{highlighting &&
|
||||||
@@ -421,13 +477,31 @@ const CommentList = ({ entity }: { entity: DocCommentEntity }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Filter by only my replies and mentions
|
// Filter by only my replies and mentions
|
||||||
if (filterState.onlyMyReplies) {
|
if (filterState.onlyMyReplies && account) {
|
||||||
filteredComments = filteredComments.filter(comment => {
|
filteredComments = filteredComments.filter(comment => {
|
||||||
return (
|
return (
|
||||||
comment.user.id === account?.id ||
|
comment.user.id === account.id ||
|
||||||
comment.replies?.some(reply => reply.user.id === account?.id)
|
comment.mentions.includes(account.id) ||
|
||||||
|
comment.replies?.some(reply => {
|
||||||
|
return (
|
||||||
|
reply.user.id === account.id ||
|
||||||
|
reply.mentions.includes(account.id)
|
||||||
|
);
|
||||||
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
filteredComments = filteredComments.map(comment => {
|
||||||
|
return {
|
||||||
|
...comment,
|
||||||
|
replies: comment.replies?.filter(reply => {
|
||||||
|
return (
|
||||||
|
reply.user.id === account.id ||
|
||||||
|
reply.mentions.includes(account.id)
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter by only current mode
|
// Filter by only current mode
|
||||||
@@ -444,8 +518,8 @@ const CommentList = ({ entity }: { entity: DocCommentEntity }) => {
|
|||||||
filterState.showResolvedComments,
|
filterState.showResolvedComments,
|
||||||
filterState.onlyMyReplies,
|
filterState.onlyMyReplies,
|
||||||
filterState.onlyCurrentMode,
|
filterState.onlyCurrentMode,
|
||||||
account?.id,
|
|
||||||
docMode,
|
docMode,
|
||||||
|
account,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const newPendingComment = useLiveData(entity.pendingComment$);
|
const newPendingComment = useLiveData(entity.pendingComment$);
|
||||||
|
|||||||
@@ -189,3 +189,24 @@ export const time = style({
|
|||||||
color: cssVarV2('text/secondary'),
|
color: cssVarV2('text/secondary'),
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const collapsedReplies = style({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
cursor: 'pointer',
|
||||||
|
height: '28px',
|
||||||
|
paddingLeft: '42px',
|
||||||
|
borderRadius: 8,
|
||||||
|
selectors: {
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: cssVarV2('layer/background/hoverOverlay'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const collapsedRepliesTitle = style({
|
||||||
|
color: cssVarV2('text/emphasis'),
|
||||||
|
fontSize: cssVar('fontXs'),
|
||||||
|
fontWeight: '500',
|
||||||
|
});
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import type {
|
|||||||
DocCommentListResult,
|
DocCommentListResult,
|
||||||
DocCommentReply,
|
DocCommentReply,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
import { findMentions } from './utils';
|
||||||
|
|
||||||
type GQLCommentType =
|
type GQLCommentType =
|
||||||
ListCommentsQuery['workspace']['comments']['edges'][number]['node'];
|
ListCommentsQuery['workspace']['comments']['edges'][number]['node'];
|
||||||
@@ -38,10 +39,12 @@ const normalizeUser = (user: GQLUserType) => ({
|
|||||||
|
|
||||||
const normalizeReply = (reply: GQLReplyType): DocCommentReply => ({
|
const normalizeReply = (reply: GQLReplyType): DocCommentReply => ({
|
||||||
id: reply.id,
|
id: reply.id,
|
||||||
|
commentId: reply.commentId,
|
||||||
content: reply.content as DocCommentContent,
|
content: reply.content as DocCommentContent,
|
||||||
createdAt: new Date(reply.createdAt).getTime(),
|
createdAt: new Date(reply.createdAt).getTime(),
|
||||||
updatedAt: new Date(reply.updatedAt).getTime(),
|
updatedAt: new Date(reply.updatedAt).getTime(),
|
||||||
user: normalizeUser(reply.user),
|
user: normalizeUser(reply.user),
|
||||||
|
mentions: findMentions(reply.content.snapshot.blocks),
|
||||||
});
|
});
|
||||||
|
|
||||||
const normalizeComment = (comment: GQLCommentType): DocComment => ({
|
const normalizeComment = (comment: GQLCommentType): DocComment => ({
|
||||||
@@ -57,6 +60,7 @@ const normalizeComment = (comment: GQLCommentType): DocComment => ({
|
|||||||
name: '',
|
name: '',
|
||||||
avatarUrl: '',
|
avatarUrl: '',
|
||||||
},
|
},
|
||||||
|
mentions: findMentions(comment.content.snapshot.blocks),
|
||||||
replies: comment.replies?.map(normalizeReply) ?? [],
|
replies: comment.replies?.map(normalizeReply) ?? [],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -172,13 +176,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 response = await graphql.gql({
|
const response = await graphql.gql({
|
||||||
query: createCommentMutation,
|
query: createCommentMutation,
|
||||||
variables: {
|
variables: {
|
||||||
@@ -188,7 +193,7 @@ export class DocCommentStore extends Entity<{
|
|||||||
docMode: this.props.getDocMode(),
|
docMode: this.props.getDocMode(),
|
||||||
docTitle: this.props.getDocTitle(),
|
docTitle: this.props.getDocTitle(),
|
||||||
content: commentInput.content,
|
content: commentInput.content,
|
||||||
mentions: commentInput.mentions,
|
mentions,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -257,7 +262,6 @@ 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;
|
||||||
@@ -265,6 +269,8 @@ 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: {
|
||||||
@@ -273,7 +279,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: replyInput.mentions,
|
mentions: mentions,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
import { type CommentChangeAction, DocMode } from '@affine/graphql';
|
import { type CommentChangeAction, DocMode } from '@affine/graphql';
|
||||||
import type {
|
import type { BaseSelection } from '@blocksuite/affine/store';
|
||||||
BaseSelection,
|
|
||||||
BaseTextAttributes,
|
|
||||||
BlockSnapshot,
|
|
||||||
DeltaInsert,
|
|
||||||
} from '@blocksuite/affine/store';
|
|
||||||
import {
|
import {
|
||||||
effect,
|
effect,
|
||||||
Entity,
|
Entity,
|
||||||
@@ -34,19 +29,13 @@ import type {
|
|||||||
DocCommentChangeListResult,
|
DocCommentChangeListResult,
|
||||||
DocCommentContent,
|
DocCommentContent,
|
||||||
DocCommentListResult,
|
DocCommentListResult,
|
||||||
|
DocCommentReply,
|
||||||
PendingComment,
|
PendingComment,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { DocCommentStore } from './doc-comment-store';
|
import { DocCommentStore } from './doc-comment-store';
|
||||||
|
|
||||||
type DisposeCallback = () => void;
|
type DisposeCallback = () => void;
|
||||||
|
|
||||||
const MentionAttribute = 'mention';
|
|
||||||
type ExtendedTextAttributes = BaseTextAttributes & {
|
|
||||||
[MentionAttribute]: {
|
|
||||||
member: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export class DocCommentEntity extends Entity<{
|
export class DocCommentEntity extends Entity<{
|
||||||
docId: string;
|
docId: string;
|
||||||
}> {
|
}> {
|
||||||
@@ -136,31 +125,6 @@ export class DocCommentEntity extends Entity<{
|
|||||||
return this.framework.get(GlobalContextService).globalContext.docMode.$;
|
return this.framework.get(GlobalContextService).globalContext.docMode.$;
|
||||||
}
|
}
|
||||||
|
|
||||||
findMentions(snapshot: BlockSnapshot): string[] {
|
|
||||||
const mentionedUserIds = new Set<string>();
|
|
||||||
if (
|
|
||||||
snapshot.props.type === 'text' &&
|
|
||||||
snapshot.props.text &&
|
|
||||||
'delta' in (snapshot.props.text as any)
|
|
||||||
) {
|
|
||||||
const delta = (snapshot.props.text as any)
|
|
||||||
.delta as DeltaInsert<ExtendedTextAttributes>[];
|
|
||||||
for (const op of delta) {
|
|
||||||
if (op.attributes?.[MentionAttribute]) {
|
|
||||||
mentionedUserIds.add(op.attributes[MentionAttribute].member);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const block of snapshot.children) {
|
|
||||||
this.findMentions(block).forEach(userId => {
|
|
||||||
mentionedUserIds.add(userId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array.from(mentionedUserIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
async commitComment(id: string): Promise<void> {
|
async commitComment(id: string): Promise<void> {
|
||||||
const pendingComment = this.pendingComment$.value;
|
const pendingComment = this.pendingComment$.value;
|
||||||
if (!pendingComment || pendingComment.id !== id) {
|
if (!pendingComment || pendingComment.id !== id) {
|
||||||
@@ -172,9 +136,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 = this.findMentions(snapshot.blocks);
|
|
||||||
const comment = await this.store.createComment({
|
const comment = await this.store.createComment({
|
||||||
mentions: mentions,
|
|
||||||
content: {
|
content: {
|
||||||
snapshot,
|
snapshot,
|
||||||
preview,
|
preview,
|
||||||
@@ -207,9 +169,7 @@ export class DocCommentEntity extends Entity<{
|
|||||||
throw new Error('Pending reply has no commentId');
|
throw new Error('Pending reply has no commentId');
|
||||||
}
|
}
|
||||||
|
|
||||||
const mentions = this.findMentions(snapshot.blocks);
|
|
||||||
const reply = await this.store.createReply(pendingReply.commentId, {
|
const reply = await this.store.createReply(pendingReply.commentId, {
|
||||||
mentions,
|
|
||||||
content: {
|
content: {
|
||||||
snapshot,
|
snapshot,
|
||||||
},
|
},
|
||||||
@@ -434,7 +394,12 @@ export class DocCommentEntity extends Entity<{
|
|||||||
|
|
||||||
if (commentId) {
|
if (commentId) {
|
||||||
// This is a reply change - handle separately
|
// This is a reply change - handle separately
|
||||||
this.handleReplyChange(currentComments, action, comment, commentId);
|
const reply = {
|
||||||
|
...comment,
|
||||||
|
id: id,
|
||||||
|
commentId: commentId,
|
||||||
|
};
|
||||||
|
this.handleReplyChange(currentComments, action, reply, commentId);
|
||||||
commentsUpdated = true;
|
commentsUpdated = true;
|
||||||
} else {
|
} else {
|
||||||
// This is a top-level comment change
|
// This is a top-level comment change
|
||||||
@@ -479,7 +444,7 @@ export class DocCommentEntity extends Entity<{
|
|||||||
private handleReplyChange(
|
private handleReplyChange(
|
||||||
currentComments: DocComment[],
|
currentComments: DocComment[],
|
||||||
action: CommentChangeAction,
|
action: CommentChangeAction,
|
||||||
reply: DocComment,
|
reply: DocCommentReply,
|
||||||
parentCommentId: string
|
parentCommentId: string
|
||||||
): void {
|
): void {
|
||||||
const parentIndex = currentComments.findIndex(
|
const parentIndex = currentComments.findIndex(
|
||||||
|
|||||||
37
packages/frontend/core/src/modules/comment/entities/utils.ts
Normal file
37
packages/frontend/core/src/modules/comment/entities/utils.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import type {
|
||||||
|
BaseTextAttributes,
|
||||||
|
BlockSnapshot,
|
||||||
|
DeltaInsert,
|
||||||
|
} from '@blocksuite/affine/store';
|
||||||
|
|
||||||
|
const MentionAttribute = 'mention';
|
||||||
|
type ExtendedTextAttributes = BaseTextAttributes & {
|
||||||
|
[MentionAttribute]: {
|
||||||
|
member: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function findMentions(snapshot: BlockSnapshot): string[] {
|
||||||
|
const mentionedUserIds = new Set<string>();
|
||||||
|
if (
|
||||||
|
snapshot.props.type === 'text' &&
|
||||||
|
snapshot.props.text &&
|
||||||
|
'delta' in (snapshot.props.text as any)
|
||||||
|
) {
|
||||||
|
const delta = (snapshot.props.text as any)
|
||||||
|
.delta as DeltaInsert<ExtendedTextAttributes>[];
|
||||||
|
for (const op of delta) {
|
||||||
|
if (op.attributes?.[MentionAttribute]) {
|
||||||
|
mentionedUserIds.add(op.attributes[MentionAttribute].member);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const block of snapshot.children) {
|
||||||
|
findMentions(block).forEach(userId => {
|
||||||
|
mentionedUserIds.add(userId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(mentionedUserIds);
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ export interface BaseComment {
|
|||||||
|
|
||||||
export interface DocComment extends BaseComment {
|
export interface DocComment extends BaseComment {
|
||||||
resolved: boolean;
|
resolved: boolean;
|
||||||
|
mentions: string[];
|
||||||
replies?: DocCommentReply[];
|
replies?: DocCommentReply[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,7 +30,10 @@ export type PendingComment = {
|
|||||||
commentId?: CommentId; // only for replies, points to the parent comment
|
commentId?: CommentId; // only for replies, points to the parent comment
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DocCommentReply = BaseComment;
|
export interface DocCommentReply extends BaseComment {
|
||||||
|
commentId: CommentId;
|
||||||
|
mentions: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export type DocCommentContent = {
|
export type DocCommentContent = {
|
||||||
snapshot: DocSnapshot; // blocksuite snapshot
|
snapshot: DocSnapshot; // blocksuite snapshot
|
||||||
|
|||||||
@@ -8254,6 +8254,12 @@ export function useAFFiNEI18N(): {
|
|||||||
* `Delete this reply? This action cannot be undone.`
|
* `Delete this reply? This action cannot be undone.`
|
||||||
*/
|
*/
|
||||||
["com.affine.comment.reply.delete.confirm.description"](): string;
|
["com.affine.comment.reply.delete.confirm.description"](): string;
|
||||||
|
/**
|
||||||
|
* `Show {{count}} more replies`
|
||||||
|
*/
|
||||||
|
["com.affine.comment.reply.show-more"](options: {
|
||||||
|
readonly count: string;
|
||||||
|
}): string;
|
||||||
/**
|
/**
|
||||||
* `Show resolved comments`
|
* `Show resolved comments`
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2071,6 +2071,7 @@
|
|||||||
"com.affine.comment.delete.confirm.description": "All comments will also be deleted, and this action cannot be undone.",
|
"com.affine.comment.delete.confirm.description": "All comments will also be deleted, and this action cannot be undone.",
|
||||||
"com.affine.comment.reply.delete.confirm.title": "Delete this reply?",
|
"com.affine.comment.reply.delete.confirm.title": "Delete this reply?",
|
||||||
"com.affine.comment.reply.delete.confirm.description": "Delete this reply? This action cannot be undone.",
|
"com.affine.comment.reply.delete.confirm.description": "Delete this reply? This action cannot be undone.",
|
||||||
|
"com.affine.comment.reply.show-more": "Show {{count}} more replies",
|
||||||
"com.affine.comment.filter.show-resolved": "Show resolved comments",
|
"com.affine.comment.filter.show-resolved": "Show resolved comments",
|
||||||
"com.affine.comment.filter.only-my-replies": "Only my replies and mentions",
|
"com.affine.comment.filter.only-my-replies": "Only my replies and mentions",
|
||||||
"com.affine.comment.filter.only-current-mode": "Only current mode",
|
"com.affine.comment.filter.only-current-mode": "Only current mode",
|
||||||
|
|||||||
Reference in New Issue
Block a user