From 9a42db4cb130188a09c7578694833b1d1fec49c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Let=C3=ADcia=20Coelho?= Date: Sat, 11 Apr 2026 20:49:06 +0100 Subject: [PATCH] added feature --- .../document/presentation/editor_page.dart | 2 + .../delta/text_delta_extension.dart | 4 + .../mention/mention_all_members_block.dart | 44 ++++++ .../editor_plugins/mention/mention_block.dart | 30 +++- .../document/presentation/editor_style.dart | 5 +- .../handlers/all_members_notification.dart | 138 ++++++++++++++++++ 6 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_all_members_block.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/all_members_notification.dart diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index 404f087c59153..032b786716d63 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -9,6 +9,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/i18n/edito import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy/plugins/inline_actions/handlers/child_page.dart'; +import 'package:appflowy/plugins/inline_actions/handlers/all_members_notification.dart'; import 'package:appflowy/plugins/inline_actions/handlers/date_reference.dart'; import 'package:appflowy/plugins/inline_actions/handlers/inline_page_reference.dart'; import 'package:appflowy/plugins/inline_actions/handlers/reminder_reference.dart'; @@ -83,6 +84,7 @@ class _AppFlowyEditorPageState extends State if (FeatureFlag.inlineSubPageMention.isOn) InlineChildPageService(currentViewId: documentBloc.documentId), InlinePageReferenceService(currentViewId: documentBloc.documentId), + AllMembersNotificationService(context), DateReferenceService(context), ReminderReferenceService(context), ], diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/delta/text_delta_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/delta/text_delta_extension.dart index 905c033bda979..d04d7b76f806c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/delta/text_delta_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/delta/text_delta_extension.dart @@ -32,6 +32,10 @@ extension TextDeltaExtension on Delta { if (mentionPageId != null) { text += await getMentionPageName(mentionPageId); continue; + } else if (mentionType == MentionType.allMembers.name) { + final label = mention?[MentionBlockKeys.label] ?? 'all'; + text += '@$label'; + continue; } else if (mentionType == MentionType.externalLink.name) { final url = mention?[MentionBlockKeys.url] ?? ''; final info = await LinkInfoCache.get(url); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_all_members_block.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_all_members_block.dart new file mode 100644 index 0000000000000..0ac7656342ec6 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_all_members_block.dart @@ -0,0 +1,44 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +class MentionAllMembersBlock extends StatelessWidget { + const MentionAllMembersBlock({ + super.key, + required this.mention, + this.textStyle, + }); + + final Map mention; + final TextStyle? textStyle; + + @override + Widget build(BuildContext context) { + final label = mention[MentionBlockKeys.label] as String? ?? 'all'; + final effectiveStyle = textStyle?.copyWith( + leadingDistribution: TextLeadingDistribution.even, + ); + + final iconSize = (textStyle?.fontSize ?? 14.0) / 14.0 * 16.0; + + return Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Text( + '@$label', + style: effectiveStyle, + strutStyle: effectiveStyle != null + ? StrutStyle.fromTextStyle(effectiveStyle) + : null, + ), + const HSpace(4), + FlowySvg( + FlowySvgs.settings_members_m, + size: Size.square(iconSize), + color: effectiveStyle?.color, + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart index 0060d65bb7208..738c4c22d6291 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart @@ -1,4 +1,5 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_all_members_block.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -12,13 +13,15 @@ enum MentionType { page, date, externalLink, - childPage; + childPage, + allMembers; static MentionType fromString(String value) => switch (value) { 'page' => page, 'date' => date, 'externalLink' => externalLink, 'childPage' => childPage, + 'allMembers' => allMembers, // Backwards compatibility 'reminder' => date, _ => throw UnimplementedError(), @@ -58,6 +61,10 @@ class MentionBlockKeys { static const includeTime = 'include_time'; static const reminderId = 'reminder_id'; // ReminderID static const reminderOption = 'reminder_option'; + static const workspaceId = 'workspace_id'; + static const memberCount = 'member_count'; + static const memberEmails = 'member_emails'; + static const label = 'label'; static const mentionChar = '\$'; @@ -92,6 +99,22 @@ class MentionBlockKeys { }, }; } + + static Map buildMentionAllMembersAttributes({ + required String workspaceId, + required List memberEmails, + String label = 'all', + }) { + return { + MentionBlockKeys.mention: { + MentionBlockKeys.type: MentionType.allMembers.name, + MentionBlockKeys.workspaceId: workspaceId, + MentionBlockKeys.memberCount: memberEmails.length, + MentionBlockKeys.memberEmails: memberEmails, + MentionBlockKeys.label: label, + }, + }; + } } class MentionBlock extends StatelessWidget { @@ -173,6 +196,11 @@ class MentionBlock extends StatelessWidget { node: node, index: index, ); + case MentionType.allMembers: + return MentionAllMembersBlock( + mention: mention, + textStyle: textStyle, + ); } } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart index b8ec8495f2b91..154481e611dde 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart @@ -399,8 +399,9 @@ class EditorStyleCustomizer { child: MentionBlock( key: ValueKey( switch (type) { - MentionType.page => mention[MentionBlockKeys.pageId], - MentionType.date => mention[MentionBlockKeys.date], + 'page' => mention[MentionBlockKeys.pageId], + 'date' => mention[MentionBlockKeys.date], + 'allMembers' => mention[MentionBlockKeys.label], _ => MentionBlockKeys.mention, }, ), diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/all_members_notification.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/all_members_notification.dart new file mode 100644 index 0000000000000..65871d7e4f066 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/all_members_notification.dart @@ -0,0 +1,138 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; +import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; +import 'package:appflowy/plugins/inline_actions/service_handler.dart'; +import 'package:appflowy/user/application/user_service.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class AllMembersNotificationService extends InlineActionsDelegate { + AllMembersNotificationService(this.context); + + final BuildContext context; + + static const _keywords = ['all', 'everyone', 'members', 'notify']; + + List? _members; + String? _workspaceId; + + @override + Future search([ + String? search, + ]) async { + final workspace = + context.read()?.state.currentWorkspace; + if (workspace == null || + workspace.workspaceType == WorkspaceTypePB.LocalW) { + return InlineActionsResult(title: 'Notifications', results: const []); + } + + if (_workspaceId != workspace.workspaceId || _members == null) { + _workspaceId = workspace.workspaceId; + _members = await _fetchWorkspaceMembers(workspace.workspaceId); + } + + final members = _members ?? const []; + if (members.isEmpty) { + return InlineActionsResult(title: 'Notifications', results: const []); + } + + if (!_matchesSearch(search)) { + return InlineActionsResult(title: 'Notifications', results: const []); + } + + return InlineActionsResult( + title: 'Notifications', + startsWithKeywords: _keywords, + results: [ + InlineActionsMenuItem( + label: '@all', + keywords: _keywords, + iconBuilder: (_) => const FlowySvg( + FlowySvgs.settings_members_m, + size: Size.square(16), + ), + onSelected: (context, editorState, menuService, replace) => + _insertMention( + editorState, + workspace.workspaceId, + members, + replace.$1, + replace.$2, + ), + ), + ], + ); + } + + bool _matchesSearch(String? search) { + if (search == null || search.isEmpty) { + return true; + } + + final normalized = search.toLowerCase(); + return _keywords.any((keyword) => keyword.contains(normalized)); + } + + Future> _fetchWorkspaceMembers( + String workspaceId, + ) async { + final userProfile = context.read()?.state.userProfile; + if (userProfile == null) { + return const []; + } + + final result = await UserBackendService( + userId: userProfile.id, + ).getWorkspaceMembers(workspaceId); + + return result.fold( + (success) => success.items, + (_) => const [], + ); + } + + Future _insertMention( + EditorState editorState, + String workspaceId, + List members, + int start, + int end, + ) async { + final selection = editorState.selection; + if (selection == null || !selection.isCollapsed) { + return; + } + + final node = editorState.getNodeAtPath(selection.end.path); + final delta = node?.delta; + if (node == null || delta == null) { + return; + } + + final memberEmails = members + .map((member) => member.email) + .where((email) => email.isNotEmpty) + .toList(); + if (memberEmails.isEmpty) { + return; + } + + final transaction = editorState.transaction + ..replaceText( + node, + start, + end, + MentionBlockKeys.mentionChar, + attributes: MentionBlockKeys.buildMentionAllMembersAttributes( + workspaceId: workspaceId, + memberEmails: memberEmails, + ), + ); + + await editorState.apply(transaction); + } +}