diff --git a/lib/flutter_chat_ui.dart b/lib/flutter_chat_ui.dart index a86f90f68..18e7930a3 100644 --- a/lib/flutter_chat_ui.dart +++ b/lib/flutter_chat_ui.dart @@ -5,6 +5,7 @@ export 'src/chat_theme.dart'; export 'src/models/bubble_rtl_alignment.dart'; export 'src/models/emoji_enlargement_behavior.dart'; export 'src/models/input_clear_mode.dart'; +export 'src/models/matchers.dart'; export 'src/models/pattern_style.dart'; export 'src/models/send_button_visibility_mode.dart'; export 'src/models/typing_indicator_mode.dart'; diff --git a/lib/src/models/matchers.dart b/lib/src/models/matchers.dart new file mode 100644 index 000000000..16ba32e17 --- /dev/null +++ b/lib/src/models/matchers.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_link_previewer/flutter_link_previewer.dart' + show regexEmail, regexLink; +import 'package:flutter_parsed_text/flutter_parsed_text.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../../flutter_chat_ui.dart'; + +MatchText mailToMatcher({ + final TextStyle? style, +}) => + MatchText( + onTap: (mail) async { + final url = Uri(scheme: 'mailto', path: mail); + if (await canLaunchUrl(url)) { + await launchUrl(url); + } + }, + pattern: regexEmail, + style: style, + ); + +MatchText urlMatcher({ + final TextStyle? style, + final Function(String url)? onLinkPressed, +}) => + MatchText( + onTap: (urlText) async { + final protocolIdentifierRegex = RegExp( + r'^((http|ftp|https):\/\/)', + caseSensitive: false, + ); + if (!urlText.startsWith(protocolIdentifierRegex)) { + urlText = 'https://$urlText'; + } + if (onLinkPressed != null) { + onLinkPressed(urlText); + } else { + final url = Uri.tryParse(urlText); + if (url != null && await canLaunchUrl(url)) { + await launchUrl( + url, + mode: LaunchMode.externalApplication, + ); + } + } + }, + pattern: regexLink, + style: style, + ); + +MatchText _patternStyleMatcher({ + required final PatternStyle patternStyle, + final TextStyle? style, +}) => + MatchText( + pattern: patternStyle.pattern, + style: style, + renderText: ({required String str, required String pattern}) => { + 'display': str.replaceAll( + patternStyle.from, + patternStyle.replace, + ), + }, + ); + +MatchText boldMatcher({ + final TextStyle? style, +}) => + _patternStyleMatcher( + patternStyle: PatternStyle.bold, + style: style, + ); + +MatchText italicMatcher({ + final TextStyle? style, +}) => + _patternStyleMatcher( + patternStyle: PatternStyle.italic, + style: style, + ); + +MatchText lineThroughMatcher({ + final TextStyle? style, +}) => + _patternStyleMatcher( + patternStyle: PatternStyle.lineThrough, + style: style, + ); + +MatchText codeMatcher({ + final TextStyle? style, +}) => + _patternStyleMatcher( + patternStyle: PatternStyle.code, + style: style, + ); diff --git a/lib/src/widgets/message/system_message.dart b/lib/src/widgets/message/system_message.dart index 1eecc6a2a..1467164fb 100644 --- a/lib/src/widgets/message/system_message.dart +++ b/lib/src/widgets/message/system_message.dart @@ -1,25 +1,43 @@ import 'package:flutter/material.dart'; +import '../../../flutter_chat_ui.dart'; import '../state/inherited_chat_theme.dart'; /// A class that represents system message widget. class SystemMessage extends StatelessWidget { const SystemMessage({ - super.key, required this.message, + this.options = const TextMessageOptions(), + super.key, }); /// System message. final String message; + /// See [TextMessage.options]. + final TextMessageOptions options; + @override Widget build(BuildContext context) => Container( alignment: Alignment.center, margin: InheritedChatTheme.of(context).theme.systemMessageTheme.margin, - child: Text( - message, - style: + child: TextMessageText( + bodyLinkTextStyle: InheritedChatTheme.of(context) + .theme + .systemMessageTheme + .linkTextStyle, + bodyTextStyle: InheritedChatTheme.of(context).theme.systemMessageTheme.textStyle, + boldTextStyle: InheritedChatTheme.of(context) + .theme + .systemMessageTheme + .boldTextStyle, + codeTextStyle: InheritedChatTheme.of(context) + .theme + .systemMessageTheme + .codeTextStyle, + options: options, + text: message, ), ); } @@ -28,12 +46,24 @@ class SystemMessage extends StatelessWidget { class SystemMessageTheme { const SystemMessageTheme({ required this.margin, + this.linkTextStyle, required this.textStyle, + this.boldTextStyle, + this.codeTextStyle, }); /// Margin around the system message. final EdgeInsets margin; - /// Text style for the system message. + /// Style to apply to anything that matches a link. + final TextStyle? linkTextStyle; + + /// Regular style to use for any unmatched text. Also used as basis for the fallback options. final TextStyle textStyle; + + /// Style to apply to anything that matches bold markdown. + final TextStyle? boldTextStyle; + + /// Style to apply to anything that matches code markdown. + final TextStyle? codeTextStyle; } diff --git a/lib/src/widgets/message/text_message.dart b/lib/src/widgets/message/text_message.dart index 7bded9b18..770ba7a4d 100644 --- a/lib/src/widgets/message/text_message.dart +++ b/lib/src/widgets/message/text_message.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_chat_types/flutter_chat_types.dart' as types; import 'package:flutter_link_previewer/flutter_link_previewer.dart' - show LinkPreview, regexEmail, regexLink; + show LinkPreview, regexLink; import 'package:flutter_parsed_text/flutter_parsed_text.dart'; -import 'package:url_launcher/url_launcher.dart'; import '../../models/emoji_enlargement_behavior.dart'; +import '../../models/matchers.dart'; import '../../models/pattern_style.dart'; import '../../util.dart'; import '../state/inherited_chat_theme.dart'; @@ -217,87 +217,32 @@ class TextMessageText extends StatelessWidget { Widget build(BuildContext context) => ParsedText( parse: [ ...options.matchers, - MatchText( - onTap: (mail) async { - final url = Uri(scheme: 'mailto', path: mail); - if (await canLaunchUrl(url)) { - await launchUrl(url); - } - }, - pattern: regexEmail, + mailToMatcher( style: bodyLinkTextStyle ?? bodyTextStyle.copyWith( decoration: TextDecoration.underline, ), ), - MatchText( - onTap: (urlText) async { - final protocolIdentifierRegex = RegExp( - r'^((http|ftp|https):\/\/)', - caseSensitive: false, - ); - if (!urlText.startsWith(protocolIdentifierRegex)) { - urlText = 'https://$urlText'; - } - if (options.onLinkPressed != null) { - options.onLinkPressed!(urlText); - } else { - final url = Uri.tryParse(urlText); - if (url != null && await canLaunchUrl(url)) { - await launchUrl( - url, - mode: LaunchMode.externalApplication, - ); - } - } - }, - pattern: regexLink, + urlMatcher( + onLinkPressed: options.onLinkPressed, style: bodyLinkTextStyle ?? bodyTextStyle.copyWith( decoration: TextDecoration.underline, ), ), - MatchText( - pattern: PatternStyle.bold.pattern, + boldMatcher( style: boldTextStyle ?? bodyTextStyle.merge(PatternStyle.bold.textStyle), - renderText: ({required String str, required String pattern}) => { - 'display': str.replaceAll( - PatternStyle.bold.from, - PatternStyle.bold.replace, - ), - }, ), - MatchText( - pattern: PatternStyle.italic.pattern, + italicMatcher( style: bodyTextStyle.merge(PatternStyle.italic.textStyle), - renderText: ({required String str, required String pattern}) => { - 'display': str.replaceAll( - PatternStyle.italic.from, - PatternStyle.italic.replace, - ), - }, ), - MatchText( - pattern: PatternStyle.lineThrough.pattern, + lineThroughMatcher( style: bodyTextStyle.merge(PatternStyle.lineThrough.textStyle), - renderText: ({required String str, required String pattern}) => { - 'display': str.replaceAll( - PatternStyle.lineThrough.from, - PatternStyle.lineThrough.replace, - ), - }, ), - MatchText( - pattern: PatternStyle.code.pattern, + codeMatcher( style: codeTextStyle ?? bodyTextStyle.merge(PatternStyle.code.textStyle), - renderText: ({required String str, required String pattern}) => { - 'display': str.replaceAll( - PatternStyle.code.from, - PatternStyle.code.replace, - ), - }, ), ], maxLines: maxLines,