mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-27 02:42:25 +08:00
feat(core): support mentions in comments (#13000)
fix AF-2706, PD-2687 <img width="412" alt="image" src="https://github.com/user-attachments/assets/b796f543-1c42-452a-8f65-9dddfa751ab4" /> <img width="384" alt="image" src="https://github.com/user-attachments/assets/7ac3bcc5-6cf1-49bb-9786-1eb33fad7225" /> <img width="347" alt="image" src="https://github.com/user-attachments/assets/02babd37-4740-4770-8be8-e253be18bb5a" /> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Added support for mentions in comments and replies, including detection and notification when users are mentioned. * Introduced new notification types for comments and comment mentions, with dedicated notification components and localized messages. * Enabled navigation directly to specific comments from notifications. * Sidebar comment tab and comment features now depend on both feature flags and server support. * **Improvements** * Comment creation and reply workflows now support optional mentions. * Menu configurations for linked widgets can now selectively include specific menu groups. * Enhanced navigation helper with a function to jump directly to a page comment. * Improved comment entity lifecycle management for proper cleanup. * **Bug Fixes** * Improved lifecycle management for comment entities to ensure proper cleanup. * **Style** * Updated mention styling to use a dynamic font size based on theme variables. * Adjusted comment preview container underline highlight color. * **Localization** * Added English translations for comment and mention notification messages. * **Configuration** * Updated feature flag logic for comment features, making configuration more flexible and environment-aware. <!-- end of auto-generated comment: release notes by coderabbit.ai --> #### PR Dependency Tree * **PR #13000** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal)
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
} from '@affine/core/blocksuite/editors';
|
||||
import { getViewManager } from '@affine/core/blocksuite/manager/view';
|
||||
import { useEnableAI } from '@affine/core/components/hooks/affine/use-enable-ai';
|
||||
import { ServerService } from '@affine/core/modules/cloud';
|
||||
import type { DocCustomPropertyInfo } from '@affine/core/modules/db';
|
||||
import type {
|
||||
DatabaseRow,
|
||||
@@ -21,6 +22,7 @@ import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { JournalService } from '@affine/core/modules/journal';
|
||||
import { useInsidePeekView } from '@affine/core/modules/peek-view';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import { ServerFeature } from '@affine/graphql';
|
||||
import track from '@affine/track';
|
||||
import type { DocTitle } from '@blocksuite/affine/fragments/doc-title';
|
||||
import type { DocMode } from '@blocksuite/affine/model';
|
||||
@@ -80,7 +82,13 @@ const usePatchSpecs = (mode: DocMode) => {
|
||||
featureFlagService.flags.enable_pdf_embed_preview.$
|
||||
);
|
||||
|
||||
const enableComment = useLiveData(featureFlagService.flags.enable_comment.$);
|
||||
const serverService = useService(ServerService);
|
||||
const serverConfig = useLiveData(serverService.server.config$);
|
||||
|
||||
const enableComment =
|
||||
useLiveData(featureFlagService.flags.enable_comment.$) &&
|
||||
// comment may not be supported by the server
|
||||
serverConfig.features.includes(ServerFeature.Comment);
|
||||
|
||||
const patchedSpecs = useMemo(() => {
|
||||
const manager = getViewManager()
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { AtMenuConfigService } from '@affine/core/modules/at-menu-config/services';
|
||||
import {
|
||||
AtMenuConfigService,
|
||||
type LinkedMenuGroupType,
|
||||
} from '@affine/core/modules/at-menu-config/services';
|
||||
import type { LinkedWidgetConfig } from '@blocksuite/affine/widgets/linked-doc';
|
||||
import { type FrameworkProvider } from '@toeverything/infra';
|
||||
|
||||
export function createLinkedWidgetConfig(
|
||||
framework: FrameworkProvider
|
||||
framework: FrameworkProvider,
|
||||
options?: {
|
||||
includedGroups?: LinkedMenuGroupType[];
|
||||
}
|
||||
): Partial<LinkedWidgetConfig> | undefined {
|
||||
const service = framework.getOptional(AtMenuConfigService);
|
||||
if (!service) return;
|
||||
return service.getConfig();
|
||||
return service.getConfig(options?.includedGroups);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { CloudViewExtension } from '@affine/core/blocksuite/view-extensions/cloud';
|
||||
import { createLinkedWidgetConfig } from '@affine/core/blocksuite/view-extensions/editor-config/linked';
|
||||
import { AffineEditorViewExtension } from '@affine/core/blocksuite/view-extensions/editor-view/editor-view';
|
||||
import { AffineThemeViewExtension } from '@affine/core/blocksuite/view-extensions/theme';
|
||||
import { LinkedMenuGroupType } from '@affine/core/modules/at-menu-config/services';
|
||||
import { CodeBlockViewExtension } from '@blocksuite/affine/blocks/code/view';
|
||||
import { DividerViewExtension } from '@blocksuite/affine/blocks/divider/view';
|
||||
import { LatexViewExtension as LatexBlockViewExtension } from '@blocksuite/affine/blocks/latex/view';
|
||||
@@ -145,6 +147,9 @@ export function getCommentEditorViewManager(framework: FrameworkProvider) {
|
||||
// Affine side
|
||||
AffineThemeViewExtension,
|
||||
AffineEditorViewExtension,
|
||||
|
||||
// for rendering mentions
|
||||
CloudViewExtension,
|
||||
]);
|
||||
|
||||
manager.configure(ParagraphViewExtension, {
|
||||
@@ -155,8 +160,15 @@ export function getCommentEditorViewManager(framework: FrameworkProvider) {
|
||||
|
||||
manager.configure(
|
||||
LinkedDocViewExtension,
|
||||
createLinkedWidgetConfig(framework)
|
||||
createLinkedWidgetConfig(framework, {
|
||||
includedGroups: [LinkedMenuGroupType.Mention],
|
||||
})
|
||||
);
|
||||
|
||||
manager.configure(CloudViewExtension, {
|
||||
framework,
|
||||
enableCloud: true,
|
||||
});
|
||||
}
|
||||
return manager;
|
||||
}
|
||||
|
||||
@@ -551,7 +551,6 @@ const useCommentEntity = (docId: string | undefined) => {
|
||||
const entityRef = docCommentManager.get(docId);
|
||||
setEntity(entityRef.obj);
|
||||
entityRef.obj.start();
|
||||
entityRef.obj.revalidate();
|
||||
|
||||
// Set up pending comment watching to auto-open sidebar
|
||||
const unwatchPending = commentPanelService.watchForPendingComments(
|
||||
@@ -560,6 +559,7 @@ const useCommentEntity = (docId: string | undefined) => {
|
||||
|
||||
return () => {
|
||||
unwatchPending();
|
||||
entityRef.obj.stop();
|
||||
entityRef.release();
|
||||
};
|
||||
}, [docCommentManager, commentPanelService, docId]);
|
||||
|
||||
@@ -125,7 +125,7 @@ export const previewContainer = style({
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
top: '0',
|
||||
backgroundColor: cssVarV2('layer/insideBorder/primaryBorder'),
|
||||
backgroundColor: cssVarV2('block/comment/highlightUnderline'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -62,6 +62,26 @@ export function useNavigateHelper() {
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
const jumpToPageComment = useCallback(
|
||||
(
|
||||
workspaceId: string,
|
||||
pageId: string,
|
||||
commentId: string,
|
||||
mode: DocMode,
|
||||
logic: RouteLogic = RouteLogic.PUSH
|
||||
) => {
|
||||
const search = toDocSearchParams({
|
||||
mode,
|
||||
refreshKey: nanoid(),
|
||||
commentId,
|
||||
});
|
||||
const query = search?.size ? `?${search.toString()}` : '';
|
||||
return navigate(`/workspace/${workspaceId}/${pageId}${query}`, {
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
});
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
const jumpToCollections = useCallback(
|
||||
(workspaceId: string, logic: RouteLogic = RouteLogic.PUSH) => {
|
||||
return navigate(`/workspace/${workspaceId}/collection`, {
|
||||
@@ -213,6 +233,7 @@ export function useNavigateHelper() {
|
||||
() => ({
|
||||
jumpToPage,
|
||||
jumpToPageBlock,
|
||||
jumpToPageComment,
|
||||
jumpToIndex,
|
||||
jumpTo404,
|
||||
openPage,
|
||||
@@ -229,6 +250,7 @@ export function useNavigateHelper() {
|
||||
[
|
||||
jumpToPage,
|
||||
jumpToPageBlock,
|
||||
jumpToPageComment,
|
||||
jumpToIndex,
|
||||
jumpTo404,
|
||||
openPage,
|
||||
|
||||
@@ -156,6 +156,10 @@ const NotificationItem = ({ notification }: { notification: Notification }) => {
|
||||
|
||||
return type === NotificationType.Mention ? (
|
||||
<MentionNotificationItem notification={notification} />
|
||||
) : type === NotificationType.Comment ? (
|
||||
<CommentNotificationItem notification={notification} />
|
||||
) : type === NotificationType.CommentMention ? (
|
||||
<CommentMentionNotificationItem notification={notification} />
|
||||
) : type === NotificationType.InvitationAccepted ? (
|
||||
<InvitationAcceptedNotificationItem notification={notification} />
|
||||
) : type === NotificationType.Invitation ? (
|
||||
@@ -771,3 +775,143 @@ const DocNameWithIcon = ({
|
||||
</b>
|
||||
);
|
||||
};
|
||||
|
||||
const CommentNotificationItem = ({
|
||||
notification,
|
||||
}: {
|
||||
notification: Notification;
|
||||
}) => {
|
||||
const notificationListService = useService(NotificationListService);
|
||||
const { jumpToPageComment } = useNavigateHelper();
|
||||
const t = useI18n();
|
||||
const body = notification.body;
|
||||
|
||||
const memberInactived = !body.createdByUser;
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
track.$.sidebar.notifications.clickNotification({
|
||||
type: notification.type,
|
||||
item: 'read',
|
||||
});
|
||||
if (!body.workspaceId || !body.doc?.id) {
|
||||
return;
|
||||
}
|
||||
notificationListService.readNotification(notification.id).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
jumpToPageComment(
|
||||
body.workspaceId,
|
||||
body.doc.id,
|
||||
body.commentId,
|
||||
body.doc.mode
|
||||
);
|
||||
}, [body, jumpToPageComment, notificationListService, notification]);
|
||||
|
||||
return (
|
||||
<div className={styles.itemContainer} onClick={handleClick}>
|
||||
<Avatar
|
||||
size={22}
|
||||
name={body.createdByUser?.name}
|
||||
url={body.createdByUser?.avatarUrl}
|
||||
/>
|
||||
<div className={styles.itemMain}>
|
||||
<span>
|
||||
<Trans
|
||||
i18nKey={'com.affine.notification.comment'}
|
||||
components={{
|
||||
1: (
|
||||
<b
|
||||
className={styles.itemNameLabel}
|
||||
data-inactived={memberInactived}
|
||||
/>
|
||||
),
|
||||
2: <DocNameWithIcon mode={body.doc?.mode || 'page'} />,
|
||||
}}
|
||||
values={{
|
||||
username:
|
||||
body.createdByUser?.name ?? t['com.affine.inactive-member'](),
|
||||
docTitle: body.doc?.title || t['Untitled'](),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<div className={styles.itemDate}>
|
||||
{i18nTime(notification.createdAt, {
|
||||
relative: true,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<DeleteButton notification={notification} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CommentMentionNotificationItem = ({
|
||||
notification,
|
||||
}: {
|
||||
notification: Notification;
|
||||
}) => {
|
||||
const notificationListService = useService(NotificationListService);
|
||||
const { jumpToPageComment } = useNavigateHelper();
|
||||
const t = useI18n();
|
||||
const body = notification.body;
|
||||
|
||||
const memberInactived = !body.createdByUser;
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
track.$.sidebar.notifications.clickNotification({
|
||||
type: notification.type,
|
||||
item: 'read',
|
||||
});
|
||||
if (!body.workspaceId || !body.doc?.id) {
|
||||
return;
|
||||
}
|
||||
notificationListService.readNotification(notification.id).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
jumpToPageComment(
|
||||
body.workspaceId,
|
||||
body.doc.id,
|
||||
body.commentId,
|
||||
body.doc.mode
|
||||
);
|
||||
}, [body, jumpToPageComment, notificationListService, notification]);
|
||||
|
||||
return (
|
||||
<div className={styles.itemContainer} onClick={handleClick}>
|
||||
<Avatar
|
||||
size={22}
|
||||
name={body.createdByUser?.name}
|
||||
url={body.createdByUser?.avatarUrl}
|
||||
/>
|
||||
<div className={styles.itemMain}>
|
||||
<span>
|
||||
<Trans
|
||||
i18nKey={'com.affine.notification.comment-mention'}
|
||||
components={{
|
||||
1: (
|
||||
<b
|
||||
className={styles.itemNameLabel}
|
||||
data-inactived={memberInactived}
|
||||
/>
|
||||
),
|
||||
2: <DocNameWithIcon mode={body.doc?.mode || 'page'} />,
|
||||
}}
|
||||
values={{
|
||||
username:
|
||||
body.createdByUser?.name ?? t['com.affine.inactive-member'](),
|
||||
docTitle: body.doc?.title || t['Untitled'](),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<div className={styles.itemDate}>
|
||||
{i18nTime(notification.createdAt, {
|
||||
relative: true,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<DeleteButton notification={notification} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ import { PageDetailEditor } from '@affine/core/components/page-detail-editor';
|
||||
import { WorkspacePropertySidebar } from '@affine/core/components/properties/sidebar';
|
||||
import { TrashPageFooter } from '@affine/core/components/pure/trash-page-footer';
|
||||
import { TopTip } from '@affine/core/components/top-tip';
|
||||
import { ServerService } from '@affine/core/modules/cloud';
|
||||
import { DocService } from '@affine/core/modules/doc';
|
||||
import { EditorService } from '@affine/core/modules/editor';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
@@ -33,6 +34,7 @@ import {
|
||||
} from '@affine/core/modules/workbench';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import { isNewTabTrigger } from '@affine/core/utils';
|
||||
import { ServerFeature } from '@affine/graphql';
|
||||
import track from '@affine/track';
|
||||
import { DisposableGroup } from '@blocksuite/affine/global/disposable';
|
||||
import { RefNodeSlotsProvider } from '@blocksuite/affine/inlines/reference';
|
||||
@@ -113,6 +115,14 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
featureFlagService.flags.enable_adapter_panel.$
|
||||
);
|
||||
|
||||
const serverService = useService(ServerService);
|
||||
const serverConfig = useLiveData(serverService.server.config$);
|
||||
|
||||
const enableComment =
|
||||
useLiveData(featureFlagService.flags.enable_comment.$) &&
|
||||
// comment may not be supported by the server
|
||||
serverConfig.features.includes(ServerFeature.Comment);
|
||||
|
||||
useEffect(() => {
|
||||
if (isActiveView) {
|
||||
setActiveBlockSuiteEditor(editorContainer);
|
||||
@@ -383,7 +393,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
</ViewSidebarTab>
|
||||
)}
|
||||
|
||||
{workspace.flavour !== 'local' && (
|
||||
{workspace.flavour !== 'local' && enableComment && (
|
||||
<ViewSidebarTab tabId="comment" icon={<CommentIcon />}>
|
||||
<Scrollable.Root className={styles.sidebarScrollArea}>
|
||||
<Scrollable.Viewport>
|
||||
|
||||
@@ -62,6 +62,13 @@ const RESERVED_ITEM_KEYS = {
|
||||
datePicker: 'date-picker',
|
||||
};
|
||||
|
||||
export enum LinkedMenuGroupType {
|
||||
LinkToDoc = 'link-to-doc',
|
||||
Mention = 'mention',
|
||||
Journal = 'journal',
|
||||
NewDoc = 'new-doc',
|
||||
}
|
||||
|
||||
export class AtMenuConfigService extends Service {
|
||||
constructor(
|
||||
private readonly journalService: JournalService,
|
||||
@@ -79,9 +86,11 @@ export class AtMenuConfigService extends Service {
|
||||
|
||||
// todo(@peng17): maybe refactor the config using entity, so that each config
|
||||
// can be reactive to the query, instead of recreating the whole config?
|
||||
getConfig(): Partial<LinkedWidgetConfig> {
|
||||
getConfig(
|
||||
includedGroups?: LinkedMenuGroupType[]
|
||||
): Partial<LinkedWidgetConfig> {
|
||||
return {
|
||||
getMenus: this.getMenusFn(),
|
||||
getMenus: this.getMenusFn(includedGroups),
|
||||
mobile: this.getMobileConfig(),
|
||||
autoFocusedItemKey: this.autoFocusedItemKey,
|
||||
};
|
||||
@@ -102,14 +111,14 @@ export class AtMenuConfigService extends Service {
|
||||
return null;
|
||||
}
|
||||
|
||||
const linkToDocGroup = menus[0];
|
||||
const memberGroup = menus[1];
|
||||
const linkToDocGroup = menus.at(0);
|
||||
const memberGroup = menus.at(1);
|
||||
|
||||
if (resolveSignal(memberGroup.items).length > 1) {
|
||||
if (memberGroup && resolveSignal(memberGroup.items).length > 1) {
|
||||
return resolveSignal(memberGroup.items)[0]?.key;
|
||||
}
|
||||
|
||||
if (resolveSignal(linkToDocGroup.items).length > 0) {
|
||||
if (linkToDocGroup && resolveSignal(linkToDocGroup.items).length > 0) {
|
||||
return resolveSignal(linkToDocGroup.items)[0]?.key;
|
||||
}
|
||||
|
||||
@@ -635,9 +644,7 @@ export class AtMenuConfigService extends Service {
|
||||
return query.length > 0 && !loading && members.length === 0;
|
||||
});
|
||||
|
||||
if (query.length > 0) {
|
||||
this.memberSearchService.search(query);
|
||||
}
|
||||
this.memberSearchService.search(query);
|
||||
|
||||
return {
|
||||
name: I18n.t('com.affine.editor.at-menu.mention-members'),
|
||||
@@ -655,13 +662,28 @@ export class AtMenuConfigService extends Service {
|
||||
};
|
||||
}
|
||||
|
||||
private getMenusFn(): LinkedWidgetConfig['getMenus'] {
|
||||
private getMenusFn(
|
||||
includedGroups: LinkedMenuGroupType[] = [
|
||||
LinkedMenuGroupType.LinkToDoc,
|
||||
LinkedMenuGroupType.Mention,
|
||||
LinkedMenuGroupType.Journal,
|
||||
LinkedMenuGroupType.NewDoc,
|
||||
]
|
||||
): LinkedWidgetConfig['getMenus'] {
|
||||
return (query, close, editorHost, inlineEditor, abortSignal) => {
|
||||
return [
|
||||
this.linkToDocGroup(query, close, inlineEditor, abortSignal),
|
||||
this.memberGroup(query, close, inlineEditor, abortSignal),
|
||||
this.journalGroup(query, close, inlineEditor),
|
||||
this.newDocMenuGroup(query, close, editorHost, inlineEditor),
|
||||
...(includedGroups?.includes(LinkedMenuGroupType.LinkToDoc)
|
||||
? [this.linkToDocGroup(query, close, inlineEditor, abortSignal)]
|
||||
: []),
|
||||
...(includedGroups?.includes(LinkedMenuGroupType.Mention)
|
||||
? [this.memberGroup(query, close, inlineEditor, abortSignal)]
|
||||
: []),
|
||||
...(includedGroups?.includes(LinkedMenuGroupType.Journal)
|
||||
? [this.journalGroup(query, close, inlineEditor)]
|
||||
: []),
|
||||
...(includedGroups?.includes(LinkedMenuGroupType.NewDoc)
|
||||
? [this.newDocMenuGroup(query, close, editorHost, inlineEditor)]
|
||||
: []),
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -162,6 +162,7 @@ export class DocCommentStore extends Entity<{
|
||||
|
||||
async createComment(commentInput: {
|
||||
content: DocCommentContent;
|
||||
mentions?: string[];
|
||||
}): Promise<DocComment> {
|
||||
const graphql = this.graphqlService;
|
||||
if (!graphql) {
|
||||
@@ -177,6 +178,7 @@ export class DocCommentStore extends Entity<{
|
||||
docMode: this.props.getDocMode(),
|
||||
docTitle: this.props.getDocTitle(),
|
||||
content: commentInput.content,
|
||||
mentions: commentInput.mentions,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -245,6 +247,7 @@ export class DocCommentStore extends Entity<{
|
||||
commentId: string,
|
||||
replyInput: {
|
||||
content: DocCommentContent;
|
||||
mentions?: string[];
|
||||
}
|
||||
): Promise<DocCommentReply> {
|
||||
const graphql = this.graphqlService;
|
||||
@@ -260,6 +263,7 @@ export class DocCommentStore extends Entity<{
|
||||
content: replyInput.content,
|
||||
docMode: this.props.getDocMode(),
|
||||
docTitle: this.props.getDocTitle(),
|
||||
mentions: replyInput.mentions,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { type CommentChangeAction, DocMode } from '@affine/graphql';
|
||||
import type { BaseSelection } from '@blocksuite/affine/store';
|
||||
import type {
|
||||
BaseSelection,
|
||||
BaseTextAttributes,
|
||||
BlockSnapshot,
|
||||
DeltaInsert,
|
||||
} from '@blocksuite/affine/store';
|
||||
import {
|
||||
effect,
|
||||
Entity,
|
||||
@@ -26,6 +31,13 @@ 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;
|
||||
}> {
|
||||
@@ -115,6 +127,31 @@ 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) {
|
||||
@@ -126,7 +163,9 @@ 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,
|
||||
@@ -159,7 +198,9 @@ 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,
|
||||
},
|
||||
|
||||
@@ -269,8 +269,8 @@ export const AFFINE_FLAGS = {
|
||||
bsFlag: 'enable_comment',
|
||||
displayName: 'Enable Comment',
|
||||
description: 'Enable comment',
|
||||
configurable: isCanaryBuild,
|
||||
defaultState: true,
|
||||
configurable: true,
|
||||
defaultState: isCanaryBuild,
|
||||
},
|
||||
} satisfies { [key in string]: FlagInfo };
|
||||
|
||||
|
||||
@@ -9438,6 +9438,26 @@ export const TypedTrans: {
|
||||
["1"]: JSX.Element;
|
||||
["2"]: JSX.Element;
|
||||
}>>;
|
||||
/**
|
||||
* `<1>{{username}}</1> commented in <2>{{docTitle}}</2>`
|
||||
*/
|
||||
["com.affine.notification.comment"]: ComponentType<TypedTransProps<Readonly<{
|
||||
username: string;
|
||||
docTitle: string;
|
||||
}>, {
|
||||
["1"]: JSX.Element;
|
||||
["2"]: JSX.Element;
|
||||
}>>;
|
||||
/**
|
||||
* `<1>{{username}}</1> mentioned you in a comment in <2>{{docTitle}}</2>`
|
||||
*/
|
||||
["com.affine.notification.comment-mention"]: ComponentType<TypedTransProps<Readonly<{
|
||||
username: string;
|
||||
docTitle: string;
|
||||
}>, {
|
||||
["1"]: JSX.Element;
|
||||
["2"]: JSX.Element;
|
||||
}>>;
|
||||
/**
|
||||
* `<1>{{username}}</1> has accept your invitation`
|
||||
*/
|
||||
|
||||
@@ -1932,6 +1932,8 @@
|
||||
"com.affine.page-starter-bar.edgeless": "Edgeless",
|
||||
"com.affine.notification.unsupported": "Unsupported message",
|
||||
"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-mention": "<1>{{username}}</1> mentioned you in a comment in <2>{{docTitle}}</2>",
|
||||
"com.affine.notification.empty": "No new notifications",
|
||||
"com.affine.notification.loading-more": "Loading more...",
|
||||
"com.affine.notification.empty.description": "You'll be notified here for @mentions and workspace invites.",
|
||||
|
||||
Reference in New Issue
Block a user