diff --git a/sama_chat_client/lib/src/features/conversation/view/messages_list.dart b/sama_chat_client/lib/src/features/conversation/view/messages_list.dart index 47e8392..dfcaab3 100644 --- a/sama_chat_client/lib/src/features/conversation/view/messages_list.dart +++ b/sama_chat_client/lib/src/features/conversation/view/messages_list.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:sama_sdk/api/api.dart' hide DeleteMessagesStatus; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; @@ -7,6 +8,7 @@ import '../../../db/models/models.dart'; import '../../../shared/ui/colors.dart'; import '../../../shared/utils/date_utils.dart'; import '../../../shared/utils/list_utils.dart'; +import '../../../shared/utils/regexp_utils.dart'; import '../../../shared/utils/screen_factor.dart'; import '../../../shared/utils/string_utils.dart'; import '../bloc/conversation_bloc.dart'; @@ -491,6 +493,75 @@ class MessageItem extends StatelessWidget { ); }); }), + if ((message.body != null) || + message.attachments.firstOrNull != null) + FocusedPopupMenuItem( + leadingIcon: const Icon(Icons.copy_outlined), + title: const Text('Copy'), + onPressed: () { + var text = message.body; + var link = text?.firstUrl; + var email = text?.firstEmail; + var phone = text?.firstPhone; + var url = message.attachments.firstOrNull?.url; + + FocusedPopupMenu( + menuItems: [ + if (link?.isNotEmpty ?? false) + FocusedPopupMenuItem( + title: const Text('Copy URL'), + onPressed: () { + Clipboard.setData( + ClipboardData(text: link!), + ); + }), + if (email?.isNotEmpty ?? false) + FocusedPopupMenuItem( + title: const Text('Copy email'), + onPressed: () { + Clipboard.setData( + ClipboardData(text: email!), + ); + }), + if (phone?.isNotEmpty ?? false) + FocusedPopupMenuItem( + title: const Text('Copy phone'), + onPressed: () { + Clipboard.setData( + ClipboardData(text: phone!), + ); + }), + if (url?.isNotEmpty ?? false) + FocusedPopupMenuItem( + title: + const Text('Copy attachment'), + onPressed: () { + Clipboard.setData( + ClipboardData(text: url!), + ); + }), + if (text?.isNotEmpty ?? false) + FocusedPopupMenuItem( + title: const Text('Copy message'), + onPressed: () { + Clipboard.setData( + ClipboardData(text: text!), + ); + }), + ], + context: context, + child: MultiBlocProvider(providers: [ + BlocProvider.value( + value: BlocProvider.of< + MediaAttachmentBloc>( + context)), + BlocProvider.value( + value: BlocProvider.of< + ConversationBloc>(context)), + ], child: this), + stickToRight: message.isOwn) + .show(); + }), FocusedPopupMenuItem( leadingIcon: const Icon(Icons.check_circle_outline), title: const Text('Select'), diff --git a/sama_chat_client/lib/src/features/conversation/widgets/focused_popup_menu.dart b/sama_chat_client/lib/src/features/conversation/widgets/focused_popup_menu.dart index 504bd36..af7294e 100644 --- a/sama_chat_client/lib/src/features/conversation/widgets/focused_popup_menu.dart +++ b/sama_chat_client/lib/src/features/conversation/widgets/focused_popup_menu.dart @@ -91,14 +91,14 @@ class FocusedMenuDetails extends StatelessWidget { Widget build(BuildContext context) { Size size = MediaQuery.of(context).size; + final itemBottom = childOffset.dy + childSize.height; + final bottomOffset = screenHeight - itemBottom; + final menuHeight = menuItems.length * menuItemHeight; final childPaddingDy = menuHeight + topPaddingHeight; - final needToMove = menuHeight + - childSize.height + - topPaddingHeight - - childOffset.dy.abs() > - size.height; + final needToMove = menuHeight + topPaddingHeight > childOffset.dy && + menuHeight > bottomOffset; final leftOffset = stickToRight ? childOffset.dx - diff --git a/sama_chat_client/lib/src/features/conversation/widgets/text_message.dart b/sama_chat_client/lib/src/features/conversation/widgets/text_message.dart index 324857a..18546c5 100644 --- a/sama_chat_client/lib/src/features/conversation/widgets/text_message.dart +++ b/sama_chat_client/lib/src/features/conversation/widgets/text_message.dart @@ -91,9 +91,7 @@ class TextMessage extends StatelessWidget { List linkify(String text, [bool preview = true]) { final List list = []; - final RegExp linkRegExp = - RegExp('($urlPattern)|($emailPattern)|($phonePattern)'); - final RegExpMatch? match = linkRegExp.firstMatch(text); + final RegExpMatch? match = RegPatterns.any.firstMatch(text); if (match == null) { list.add(TextSpan(text: text)); @@ -105,16 +103,16 @@ class TextMessage extends StatelessWidget { } final String linkText = match.group(0)!; - if (linkText.contains(RegExp(urlPattern))) { + if (linkText.contains(RegPatterns.url)) { if (preview) { preview = false; list.add(buildLinkPreviewComponent(linkText, linkText)); } else { list.add(buildTextComponent(linkText, linkText)); } - } else if (linkText.contains(RegExp(emailPattern))) { + } else if (linkText.contains(RegPatterns.email)) { list.add(buildTextComponent(linkText, 'mailto:$linkText')); - } else if (linkText.contains(RegExp(phonePattern))) { + } else if (linkText.contains(RegPatterns.phone)) { list.add(buildTextComponent(linkText, 'tel:$linkText')); } else { throw 'Unexpected match: $linkText'; diff --git a/sama_chat_client/lib/src/shared/utils/regexp_utils.dart b/sama_chat_client/lib/src/shared/utils/regexp_utils.dart index dc18661..0c493fa 100644 --- a/sama_chat_client/lib/src/shared/utils/regexp_utils.dart +++ b/sama_chat_client/lib/src/shared/utils/regexp_utils.dart @@ -1,3 +1,25 @@ -const String urlPattern = 'https?://\\S+'; -const String emailPattern = r'\S+@\S+'; -const String phonePattern = r'[\d-]{9,}'; +class RegPatterns { + static final RegExp url = RegExp(r'https?://\S+'); + static final RegExp email = RegExp(r'\S+@\S+'); + static final RegExp phone = RegExp(r'\+?\d[\d\s-]{8,}\d'); + + static final RegExp any = RegExp( + '(?:${url.pattern})|' + '(?:${email.pattern})|' + '(?:${phone.pattern})', + ); +} + +extension RegPatternsExtension on String { + String? _firstMatch(RegExp pattern) => pattern.firstMatch(this)?.group(0); + + String? get firstUrl => _firstMatch(RegPatterns.url); + + String? get firstEmail => _firstMatch(RegPatterns.email); + + String? get firstPhone => _firstMatch(RegPatterns.phone); +} + +List matches(String text, RegExp regExp) { + return regExp.allMatches(text).map((m) => m.group(0)!).toList(); +}