mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-15 17:16:16 +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:
@@ -23,6 +23,7 @@ import type {
|
||||
DocCommentListResult,
|
||||
DocCommentReply,
|
||||
} from '../types';
|
||||
import { findMentions } from './utils';
|
||||
|
||||
type GQLCommentType =
|
||||
ListCommentsQuery['workspace']['comments']['edges'][number]['node'];
|
||||
@@ -38,10 +39,12 @@ const normalizeUser = (user: GQLUserType) => ({
|
||||
|
||||
const normalizeReply = (reply: GQLReplyType): DocCommentReply => ({
|
||||
id: reply.id,
|
||||
commentId: reply.commentId,
|
||||
content: reply.content as DocCommentContent,
|
||||
createdAt: new Date(reply.createdAt).getTime(),
|
||||
updatedAt: new Date(reply.updatedAt).getTime(),
|
||||
user: normalizeUser(reply.user),
|
||||
mentions: findMentions(reply.content.snapshot.blocks),
|
||||
});
|
||||
|
||||
const normalizeComment = (comment: GQLCommentType): DocComment => ({
|
||||
@@ -57,6 +60,7 @@ const normalizeComment = (comment: GQLCommentType): DocComment => ({
|
||||
name: '',
|
||||
avatarUrl: '',
|
||||
},
|
||||
mentions: findMentions(comment.content.snapshot.blocks),
|
||||
replies: comment.replies?.map(normalizeReply) ?? [],
|
||||
});
|
||||
|
||||
@@ -172,13 +176,14 @@ export class DocCommentStore extends Entity<{
|
||||
|
||||
async createComment(commentInput: {
|
||||
content: DocCommentContent;
|
||||
mentions?: string[];
|
||||
}): Promise<DocComment> {
|
||||
const graphql = this.graphqlService;
|
||||
if (!graphql) {
|
||||
throw new Error('GraphQL service not found');
|
||||
}
|
||||
|
||||
const mentions = findMentions(commentInput.content.snapshot.blocks);
|
||||
|
||||
const response = await graphql.gql({
|
||||
query: createCommentMutation,
|
||||
variables: {
|
||||
@@ -188,7 +193,7 @@ export class DocCommentStore extends Entity<{
|
||||
docMode: this.props.getDocMode(),
|
||||
docTitle: this.props.getDocTitle(),
|
||||
content: commentInput.content,
|
||||
mentions: commentInput.mentions,
|
||||
mentions,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -257,7 +262,6 @@ export class DocCommentStore extends Entity<{
|
||||
commentId: string,
|
||||
replyInput: {
|
||||
content: DocCommentContent;
|
||||
mentions?: string[];
|
||||
}
|
||||
): Promise<DocCommentReply> {
|
||||
const graphql = this.graphqlService;
|
||||
@@ -265,6 +269,8 @@ export class DocCommentStore extends Entity<{
|
||||
throw new Error('GraphQL service not found');
|
||||
}
|
||||
|
||||
const mentions = findMentions(replyInput.content.snapshot.blocks);
|
||||
|
||||
const response = await graphql.gql({
|
||||
query: createReplyMutation,
|
||||
variables: {
|
||||
@@ -273,7 +279,7 @@ export class DocCommentStore extends Entity<{
|
||||
content: replyInput.content,
|
||||
docMode: this.props.getDocMode(),
|
||||
docTitle: this.props.getDocTitle(),
|
||||
mentions: replyInput.mentions,
|
||||
mentions: mentions,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { type CommentChangeAction, DocMode } from '@affine/graphql';
|
||||
import type {
|
||||
BaseSelection,
|
||||
BaseTextAttributes,
|
||||
BlockSnapshot,
|
||||
DeltaInsert,
|
||||
} from '@blocksuite/affine/store';
|
||||
import type { BaseSelection } from '@blocksuite/affine/store';
|
||||
import {
|
||||
effect,
|
||||
Entity,
|
||||
@@ -34,19 +29,13 @@ import type {
|
||||
DocCommentChangeListResult,
|
||||
DocCommentContent,
|
||||
DocCommentListResult,
|
||||
DocCommentReply,
|
||||
PendingComment,
|
||||
} from '../types';
|
||||
import { DocCommentStore } from './doc-comment-store';
|
||||
|
||||
type DisposeCallback = () => void;
|
||||
|
||||
const MentionAttribute = 'mention';
|
||||
type ExtendedTextAttributes = BaseTextAttributes & {
|
||||
[MentionAttribute]: {
|
||||
member: string;
|
||||
};
|
||||
};
|
||||
|
||||
export class DocCommentEntity extends Entity<{
|
||||
docId: string;
|
||||
}> {
|
||||
@@ -136,31 +125,6 @@ export class DocCommentEntity extends Entity<{
|
||||
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> {
|
||||
const pendingComment = this.pendingComment$.value;
|
||||
if (!pendingComment || pendingComment.id !== id) {
|
||||
@@ -172,9 +136,7 @@ export class DocCommentEntity extends Entity<{
|
||||
if (!snapshot) {
|
||||
throw new Error('Failed to get snapshot');
|
||||
}
|
||||
const mentions = this.findMentions(snapshot.blocks);
|
||||
const comment = await this.store.createComment({
|
||||
mentions: mentions,
|
||||
content: {
|
||||
snapshot,
|
||||
preview,
|
||||
@@ -207,9 +169,7 @@ export class DocCommentEntity extends Entity<{
|
||||
throw new Error('Pending reply has no commentId');
|
||||
}
|
||||
|
||||
const mentions = this.findMentions(snapshot.blocks);
|
||||
const reply = await this.store.createReply(pendingReply.commentId, {
|
||||
mentions,
|
||||
content: {
|
||||
snapshot,
|
||||
},
|
||||
@@ -434,7 +394,12 @@ export class DocCommentEntity extends Entity<{
|
||||
|
||||
if (commentId) {
|
||||
// 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;
|
||||
} else {
|
||||
// This is a top-level comment change
|
||||
@@ -479,7 +444,7 @@ export class DocCommentEntity extends Entity<{
|
||||
private handleReplyChange(
|
||||
currentComments: DocComment[],
|
||||
action: CommentChangeAction,
|
||||
reply: DocComment,
|
||||
reply: DocCommentReply,
|
||||
parentCommentId: string
|
||||
): void {
|
||||
const parentIndex = currentComments.findIndex(
|
||||
|
||||
@@ -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 {
|
||||
resolved: boolean;
|
||||
mentions: string[];
|
||||
replies?: DocCommentReply[];
|
||||
}
|
||||
|
||||
@@ -29,7 +30,10 @@ export type PendingComment = {
|
||||
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 = {
|
||||
snapshot: DocSnapshot; // blocksuite snapshot
|
||||
|
||||
Reference in New Issue
Block a user