diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dcdb7a5..d49a8885 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## [3.0.0] +* **Feat**: [374](https://github.com/SimformSolutionsPvtLtd/chatview/issues/374) + Add support for displaying and selecting text and links in a single message view. * **Breaking**: [411](https://github.com/SimformSolutionsPvtLtd/chatview/pull/411) Update the example iOS deployment target to 13, as the example depends on `audiowaveform`, which requires iOS 13 or later. * **Feat**: [401](https://github.com/SimformSolutionsPvtLtd/chatview/pull/401) diff --git a/lib/src/extensions/extensions.dart b/lib/src/extensions/extensions.dart index a108aff5..77d39dd7 100644 --- a/lib/src/extensions/extensions.dart +++ b/lib/src/extensions/extensions.dart @@ -112,6 +112,12 @@ extension ValidateString on String { bool get isUrl => Uri.tryParse(this)?.isAbsolute ?? false; + /// Regular expression pattern to match URLs. + static final _urlRegex = RegExp(urlRegex, caseSensitive: false); + + /// Extracts the first URL found in the string. + String? get extractedUrl => _urlRegex.firstMatch(this)?.group(0); + Widget getUserProfilePicture({ required ChatUser? Function(String) getChatUser, double? profileCircleRadius, diff --git a/lib/src/utils/constants/constants.dart b/lib/src/utils/constants/constants.dart index d60f5e2b..d0c38a34 100644 --- a/lib/src/utils/constants/constants.dart +++ b/lib/src/utils/constants/constants.dart @@ -65,6 +65,11 @@ applicationDateFormatter(DateTime inputTime) { } } +/// Regular expression to identify URLs in a text. +const String urlRegex = + r'((https?://)?(www\.)?[a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b' + r'([-a-zA-Z0-9@:%_\+.~#?&//=]*))'; + /// Default widget that appears on receipts at [MessageStatus.pending] when a message /// is not sent or at the pending state. A custom implementation can have different /// widgets for different states. diff --git a/lib/src/widgets/link_preview.dart b/lib/src/widgets/link_preview.dart index 36f3dfc6..0d66ce48 100644 --- a/lib/src/widgets/link_preview.dart +++ b/lib/src/widgets/link_preview.dart @@ -30,12 +30,16 @@ import '../utils/constants/constants.dart'; class LinkPreview extends StatelessWidget { const LinkPreview({ Key? key, - required this.url, + required this.textMessage, + required this.extractedUrl, this.linkPreviewConfig, }) : super(key: key); + /// Provides the whole text message to show. + final String textMessage; + /// Provides url which is passed in message. - final String url; + final String extractedUrl; /// Provides configuration of chat bubble appearance when link/URL is passed /// in message. @@ -43,18 +47,19 @@ class LinkPreview extends StatelessWidget { @override Widget build(BuildContext context) { + final isImageUrl = extractedUrl.isImageUrl; return Padding( padding: linkPreviewConfig?.padding ?? const EdgeInsets.symmetric(horizontal: 6, vertical: verticalPadding), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (!url.isImageUrl && + if (!isImageUrl && !(context.chatBubbleConfig?.disableLinkPreview ?? false)) ...{ Padding( padding: const EdgeInsets.symmetric(vertical: verticalPadding), child: AnyLinkPreview( - link: url, + link: extractedUrl, removeElevation: true, errorBody: linkPreviewConfig?.errorBody, proxyUrl: linkPreviewConfig?.proxyUrl, @@ -77,13 +82,13 @@ class LinkPreview extends StatelessWidget { titleStyle: linkPreviewConfig?.titleStyle, ), ), - } else if (url.isImageUrl) ...{ + } else if (isImageUrl) ...{ Padding( padding: const EdgeInsets.symmetric(vertical: verticalPadding), child: InkWell( onTap: _onLinkTap, child: Image.network( - url, + extractedUrl, height: 120, width: double.infinity, fit: BoxFit.fitWidth, @@ -95,7 +100,7 @@ class LinkPreview extends StatelessWidget { InkWell( onTap: _onLinkTap, child: Text( - url, + textMessage, style: linkPreviewConfig?.linkStyle ?? const TextStyle( color: Colors.white, @@ -109,15 +114,15 @@ class LinkPreview extends StatelessWidget { } void _onLinkTap() { - if (linkPreviewConfig?.onUrlDetect != null) { - linkPreviewConfig?.onUrlDetect!(url); + if (linkPreviewConfig?.onUrlDetect case final onUrlDetect?) { + onUrlDetect(extractedUrl); } else { _launchURL(); } } void _launchURL() async { - final parsedUrl = Uri.parse(url); + final parsedUrl = Uri.parse(extractedUrl); await canLaunchUrl(parsedUrl) ? await launchUrl(parsedUrl) : throw couldNotLaunch; diff --git a/lib/src/widgets/text_message_view.dart b/lib/src/widgets/text_message_view.dart index a20aa52b..d0af03cf 100644 --- a/lib/src/widgets/text_message_view.dart +++ b/lib/src/widgets/text_message_view.dart @@ -78,14 +78,21 @@ class TextMessageView extends StatelessWidget { final textSelectionConfig = isMessageBySender ? outgoingChatBubbleConfig?.textSelectionConfig : inComingChatBubbleConfig?.textSelectionConfig; - final baseWidget = Text( - textMessage, - style: _textStyle ?? - textTheme.bodyMedium!.copyWith( - color: Colors.white, - fontSize: 16, - ), - ); + final extractedUrl = textMessage.extractedUrl; + final baseWidget = extractedUrl != null + ? LinkPreview( + linkPreviewConfig: _linkPreviewConfig, + textMessage: textMessage, + extractedUrl: extractedUrl, + ) + : Text( + textMessage, + style: _textStyle ?? + textTheme.bodyMedium!.copyWith( + color: Colors.white, + fontSize: 16, + ), + ); return Stack( clipBehavior: Clip.none, children: [ @@ -106,17 +113,12 @@ class TextMessageView extends StatelessWidget { border: border, borderRadius: _borderRadius(textMessage), ), - child: textMessage.isUrl - ? LinkPreview( - linkPreviewConfig: _linkPreviewConfig, - url: textMessage, + child: isSelectable + ? CustomSelectionArea( + config: textSelectionConfig, + child: baseWidget, ) - : isSelectable - ? CustomSelectionArea( - config: textSelectionConfig, - child: baseWidget, - ) - : baseWidget, + : baseWidget, ), if (message.reaction.reactions.isNotEmpty) ReactionWidget(