Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:universal_platform/universal_platform.dart';

import '../../inline_actions/handlers/hashtag_reference.dart';
import 'editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart';
import 'editor_plugins/toolbar_item/custom_format_toolbar_items.dart';
import 'editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart';
Expand Down Expand Up @@ -88,6 +89,13 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage>
],
);

late final InlineActionsService hashtagActionsService = InlineActionsService(
context: context,
handlers: [
HashtagReferenceService(),
],
);

late final List<CommandShortcutEvent> commandShortcuts = [
...commandShortcutEvents,
..._buildFindAndReplaceCommands(),
Expand Down Expand Up @@ -121,6 +129,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage>
documentBloc,
styleCustomizer,
inlineActionsService,
hashtagActionsService,
(editorState, node) => _customSlashMenuItems(
editorState: editorState,
node: node,
Expand Down Expand Up @@ -329,6 +338,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage>
effectiveScrollController.dispose();
}
inlineActionsService.dispose();
hashtagActionsService.dispose();
editorScrollController.dispose();

super.dispose();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/hashtag/hashtag_block_keys.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
Expand All @@ -22,13 +23,14 @@ extension TextDeltaExtension on Delta {
while (ops.moveNext()) {
final op = ops.current;
final attributes = op.attributes;

if (op is TextInsert) {
// if the text is '\$', it means the block text is empty,
// the real data is in the attributes
// Mentions
if (op.text == MentionBlockKeys.mentionChar) {
final mention = attributes?[MentionBlockKeys.mention];
final mentionPageId = mention?[MentionBlockKeys.pageId];
final mentionType = mention?[MentionBlockKeys.type];

if (mentionPageId != null) {
text += await getMentionPageName(mentionPageId);
continue;
Expand All @@ -40,14 +42,23 @@ extension TextDeltaExtension on Delta {
}
}

// Hashtags
if (op.text == HashtagBlockKeys.hashtagChar) {
final hashtag = attributes?[HashtagBlockKeys.hashtag];
final name = hashtag?[HashtagBlockKeys.name];

if (name is String && name.isNotEmpty) {
text += '#$name';
continue;
}
}

text += op.text;
} else {
// if the delta contains other types of operations,
// return the default plain text
return defaultPlainText;
}
}

return text;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';

import 'hashtag_block_keys.dart';

class HashtagBlock extends StatelessWidget {
const HashtagBlock({
super.key,
required this.data,
required this.textStyle,
});

final Map<String, dynamic> data;
final TextStyle? textStyle;

@override
Widget build(BuildContext context) {
final tag = data[HashtagBlockKeys.name] as String? ?? '';

if (tag.isEmpty) {
return const SizedBox.shrink();
}

return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.blue.withValues(alpha: 0.10),
borderRadius: BorderRadius.circular(6),
),
child: Text(
'#$tag',
style: textStyle?.copyWith(
fontWeight: FontWeight.w600,
),
),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
class HashtagBlockKeys {
const HashtagBlockKeys._();

static const hashtag = 'hashtag';
static const name = 'name';

// caractere interno invisível/placeholder, tal como o mention usa '$'
static const hashtagChar = '%';

static Map<String, dynamic> buildHashtagAttributes({
required String name,
}) {
return {
hashtag: {
HashtagBlockKeys.name: name,
},
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/transaction_handler/editor_transaction_handler.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';

import 'hashtag_block_keys.dart';

class HashtagTransactionHandler extends EditorTransactionHandler<Map<String, dynamic>> {
HashtagTransactionHandler()
: super(
type: HashtagBlockKeys.hashtag,
livesInDelta: true,
);

@override
Future<void> onTransaction(
BuildContext context,
String viewId,
EditorState editorState,
List<Map<String, dynamic>> added,
List<Map<String, dynamic>> removed, {
bool isCut = false,
bool isUndoRedo = false,
bool isPaste = false,
bool isDraggingNode = false,
bool isTurnInto = false,
String? parentViewId,
}) async {
// Versão inicial:
// não faz sync com backend nem efeitos laterais.
// Serve apenas para o editor reconhecer oficialmente este tipo
// e para termos um ponto de extensão preparado.
return;
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/hashtag/hashtag_block_keys.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
Expand All @@ -11,27 +12,52 @@ class CustomParagraphNodeParser extends NodeParser {
@override
String transform(Node node, DocumentMarkdownEncoder? encoder) {
final delta = node.delta;
if (delta != null) {
for (final o in delta) {
final attribute = o.attributes ?? {};
final Map? mention = attribute[MentionBlockKeys.mention] ?? {};
if (mention == null) continue;
if (delta == null) {
return const TextNodeParser().transform(node, encoder);
}

String text = '';

for (final o in delta) {
final attributes = o.attributes ?? {};

// Mentions
final Map? mention = attributes[MentionBlockKeys.mention];
if (mention != null) {
/// filter date reminder node, and return it
final String date = mention[MentionBlockKeys.date] ?? '';
if (date.isNotEmpty) {
final dateTime = DateTime.tryParse(date);
if (dateTime == null) continue;
return '${DateFormat.yMMMd().format(dateTime)}\n';
if (dateTime != null) {
text += DateFormat.yMMMd().format(dateTime);
continue;
}
}

/// filter reference page
final String pageId = mention[MentionBlockKeys.pageId] ?? '';
if (pageId.isNotEmpty) {
return '[]($pageId)\n';
text += '[]($pageId)';
continue;
}
}

// Hashtags
final Map? hashtag = attributes[HashtagBlockKeys.hashtag];
if (hashtag != null) {
final String name = hashtag[HashtagBlockKeys.name] ?? '';
if (name.isNotEmpty) {
text += '#$name';
continue;
}
}

// Plain text
if (o is TextInsert) {
Comment on lines +55 to +56
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Non-TextInsert ops in paragraph delta are silently dropped, which may lose content.

This only appends TextInsert text and ignores other op types, so embeds or other inserts are dropped from the markdown export and effectively replaced by a newline. Consider either delegating to TextNodeParser when encountering non-TextInsert ops, or explicitly handling the other op types that should appear in the exported text.

text += o.text;
}
}
return const TextNodeParser().transform(node, encoder);

return '$text\n';
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/inline_actions/hashtag_actions_command.dart';


List<CharacterShortcutEvent> buildCharacterShortcutEvents(
BuildContext context,
DocumentBloc documentBloc,
EditorStyleCustomizer styleCustomizer,
InlineActionsService inlineActionsService,
InlineActionsService hashtagActionsService,
SlashMenuItemsBuilder slashMenuItemsBuilder,
) {
return [
Expand Down Expand Up @@ -69,6 +72,10 @@ List<CharacterShortcutEvent> buildCharacterShortcutEvents(
style: styleCustomizer.inlineActionsMenuStyleBuilder(),
),

hashtagActionsCommand(
hashtagActionsService,
style: styleCustomizer.inlineActionsMenuStyleBuilder(),
),
/// Inline page menu
/// - Using `[[`
pageReferenceShortcutBrackets(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/hashtag/hashtag_transaction_handler.dart';

import 'mention_transaction_handler.dart';

Expand All @@ -19,6 +20,7 @@ final _transactionHandlers = <EditorTransactionHandler>[
ChildPageTransactionHandler(),
],
DateTransactionHandler(),
HashtagTransactionHandler(),
];

/// Handles delegating transactions to appropriate handlers.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import 'package:google_fonts/google_fonts.dart';
import 'package:universal_platform/universal_platform.dart';

import 'editor_plugins/desktop_toolbar/link/link_hover_menu.dart';
import 'editor_plugins/hashtag/hashtag_block.dart';
import 'editor_plugins/hashtag/hashtag_block_keys.dart';
import 'editor_plugins/toolbar_item/more_option_toolbar_item.dart';

class EditorStyleCustomizer {
Expand Down Expand Up @@ -412,6 +414,20 @@ class EditorStyleCustomizer {
);
}

final hashtag =
attributes[HashtagBlockKeys.hashtag] as Map<String, dynamic>?;
if (hashtag != null) {
return WidgetSpan(
alignment: PlaceholderAlignment.middle,
style: newStyle,
child: HashtagBlock(
key: ValueKey(hashtag[HashtagBlockKeys.name]),
data: hashtag,
textStyle: newStyle,
),
);
}

// customize the inline math equation block
final formula = attributes[InlineMathEquationKeys.formula];
if (formula is String) {
Expand Down
Loading
Loading