Skip to content

Commit

Permalink
Make text matchers reusable (#343)
Browse files Browse the repository at this point in the history
* Make text matchers reusable

* Make SystemMessage use TextMessageText internally in order to use the same matchers
  • Loading branch information
provokateurin authored Nov 6, 2023
1 parent 23c48eb commit ab50f41
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 69 deletions.
1 change: 1 addition & 0 deletions lib/flutter_chat_ui.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
97 changes: 97 additions & 0 deletions lib/src/models/matchers.dart
Original file line number Diff line number Diff line change
@@ -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,
);
40 changes: 35 additions & 5 deletions lib/src/widgets/message/system_message.dart
Original file line number Diff line number Diff line change
@@ -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,
),
);
}
Expand All @@ -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;
}
73 changes: 9 additions & 64 deletions lib/src/widgets/message/text_message.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit ab50f41

Please sign in to comment.