From 873aead34ad7d1c559f1f8a8aa001288fa52a6dc Mon Sep 17 00:00:00 2001 From: Anuj Kumar <144224503+AnujLM@users.noreply.github.com> Date: Fri, 10 Nov 2023 19:10:59 +0530 Subject: [PATCH 1/8] fix link regex --- .../expandable_text/expandable_text.dart | 30 ++++++++++++++++--- lib/src/utils/helpers.dart | 24 +++++++++++++-- pubspec.yaml | 1 + 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/lib/packages/expandable_text/expandable_text.dart b/lib/packages/expandable_text/expandable_text.dart index b9f5585..9ff0735 100644 --- a/lib/packages/expandable_text/expandable_text.dart +++ b/lib/packages/expandable_text/expandable_text.dart @@ -8,6 +8,7 @@ import 'package:likeminds_feed/likeminds_feed.dart'; import 'package:likeminds_feed_ui_fl/likeminds_feed_ui_fl.dart'; import 'package:likeminds_feed_ui_fl/src/utils/constants.dart'; import 'package:likeminds_feed_ui_fl/src/utils/theme.dart'; +import 'package:linkify/linkify.dart'; import 'package:url_launcher/url_launcher.dart'; import './text_parser.dart'; @@ -88,7 +89,7 @@ class ExpandableText extends StatefulWidget { class ExpandableTextState extends State with TickerProviderStateMixin { bool _expanded = false; - RegExp regExp = RegExp(kRegexLinksAndTags); + // RegExp regExp = RegExp(kRegexLinksAndTags); late TapGestureRecognizer _linkTapGestureRecognizer; late TapGestureRecognizer _prefixTapGestureRecognizer; @@ -393,6 +394,9 @@ class ExpandableTextState extends State List extractLinksAndTags(String text) { List textSpans = []; int lastIndex = 0; + const String regexLinksAndTags = + r'(?:(?:http|https|ftp|www)\:\/\/)?[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(?::[a-zA-Z0-9]*)?\/?[^\s\n]+|[\w\.-]+@[a-zA-Z\d\.-]+\.[a-zA-Z]{2,}|<<([^<>]+)\|route://member/([a-zA-Z-0-9]+)>>'; + RegExp regExp = RegExp(regexLinksAndTags); for (Match match in regExp.allMatches(text)) { int startIndex = match.start; int endIndex = match.end; @@ -412,6 +416,16 @@ class ExpandableTextState extends State )); } else { bool isTag = link != null && link[0] == '<'; + + //if it is a valid link using linkify and if that is not then add normal TextSpan + if (!isTag && extractLinkAndEmailFromString(link ?? '') == null) { + textSpans.add(TextSpan( + text: text.substring(startIndex, endIndex), + style: widget.style, + )); + lastIndex = endIndex; + continue; + } // Add a TextSpan for the URL textSpans.add(TextSpan( text: isTag ? TaggingHelper.decodeString(link).keys.first : link, @@ -419,10 +433,18 @@ class ExpandableTextState extends State recognizer: TapGestureRecognizer() ..onTap = () async { if (!isTag) { - String checkLink = getFirstValidLinkFromString(link ?? ''); - if (Uri.parse(checkLink).isAbsolute) { + final checkLink = extractLinkAndEmailFromString(link ?? ''); + debugPrint('checkLink: $checkLink'); + if (checkLink is UrlElement) { + if (Uri.parse(checkLink.url).isAbsolute) { + launchUrl( + Uri.parse(checkLink.url), + mode: LaunchMode.externalApplication, + ); + } + } else if (checkLink is EmailElement) { launchUrl( - Uri.parse(checkLink), + Uri.parse('mailto:${checkLink.emailAddress}'), mode: LaunchMode.externalApplication, ); } diff --git a/lib/src/utils/helpers.dart b/lib/src/utils/helpers.dart index e2ab51b..7b73163 100644 --- a/lib/src/utils/helpers.dart +++ b/lib/src/utils/helpers.dart @@ -4,6 +4,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:likeminds_feed/likeminds_feed.dart'; import 'package:likeminds_feed_ui_fl/src/utils/theme.dart'; +import 'package:linkify/linkify.dart'; class TaggingHelper { static final RegExp tagRegExp = RegExp(r'@([^<>~]+)~'); @@ -12,7 +13,7 @@ class TaggingHelper { static const String tagRoute = r'<<([^<>]+)\|route://member/([a-zA-Z-0-9]+)>>'; static const String linkRoute = - r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+'; + r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+|(\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b)'; /// Encodes the string with the user tags and returns the encoded string static String encodeString(String string, List userTags) { @@ -191,7 +192,7 @@ List extractLinkFromString(String text) { List links = []; for (var match in matches) { String link = text.substring(match.start, match.end); - if (link.isNotEmpty) { + if (link.isNotEmpty && match.group(1) == null) { links.add(link); } } @@ -228,6 +229,25 @@ String getFirstValidLinkFromString(String text) { } } +LinkifyElement? extractLinkAndEmailFromString(String text) { + debugPrint("text: $text"); + final links = linkify( + text, + options: const LinkifyOptions( + looseUrl: true, + ), + ); + if (links.isNotEmpty) { + final emails = linkify(text); + if (emails.isNotEmpty && emails.first is EmailElement) { + return emails.first; + } else if (links.first is UrlElement) { + return links.first; + } + } + return null; +} + class PostHelper { static String getFileSizeString({required int bytes, int decimals = 0}) { const suffixes = ["b", "kb", "mb", "gb", "tb"]; diff --git a/pubspec.yaml b/pubspec.yaml index ca03023..6eece6a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,6 +29,7 @@ dependencies: visibility_aware_state: 1.0.5 likeminds_feed: 1.6.3 + linkify: # path: ../LikeMinds-Flutter-Feed-SDK From fa9de4080637f0587a962ba50c79c5c43cb1ebc3 Mon Sep 17 00:00:00 2001 From: Anuj Kumar <144224503+AnujLM@users.noreply.github.com> Date: Wed, 15 Nov 2023 15:52:12 +0530 Subject: [PATCH 2/8] fix link regex --- lib/src/utils/helpers.dart | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/lib/src/utils/helpers.dart b/lib/src/utils/helpers.dart index 7b73163..093b55f 100644 --- a/lib/src/utils/helpers.dart +++ b/lib/src/utils/helpers.dart @@ -231,17 +231,16 @@ String getFirstValidLinkFromString(String text) { LinkifyElement? extractLinkAndEmailFromString(String text) { debugPrint("text: $text"); - final links = linkify( - text, - options: const LinkifyOptions( - looseUrl: true, - ), - ); + final links = linkify(text, + options: const LinkifyOptions( + looseUrl: true, + ), + linkifiers: [ + EmailLinkifier(), + UrlLinkifier(), + ]); if (links.isNotEmpty) { - final emails = linkify(text); - if (emails.isNotEmpty && emails.first is EmailElement) { - return emails.first; - } else if (links.first is UrlElement) { + if (links.first is EmailElement || links.first is UrlElement) { return links.first; } } From 11f922d989262dfd357f42cc1698a7149655cdfd Mon Sep 17 00:00:00 2001 From: Anuj Kumar <144224503+AnujLM@users.noreply.github.com> Date: Wed, 15 Nov 2023 15:58:05 +0530 Subject: [PATCH 3/8] fix kRegexLinksAndTags --- lib/packages/expandable_text/expandable_text.dart | 5 +---- lib/src/utils/constants.dart | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/packages/expandable_text/expandable_text.dart b/lib/packages/expandable_text/expandable_text.dart index 9ff0735..badc4f0 100644 --- a/lib/packages/expandable_text/expandable_text.dart +++ b/lib/packages/expandable_text/expandable_text.dart @@ -89,7 +89,7 @@ class ExpandableText extends StatefulWidget { class ExpandableTextState extends State with TickerProviderStateMixin { bool _expanded = false; - // RegExp regExp = RegExp(kRegexLinksAndTags); + RegExp regExp = RegExp(kRegexLinksAndTags); late TapGestureRecognizer _linkTapGestureRecognizer; late TapGestureRecognizer _prefixTapGestureRecognizer; @@ -394,9 +394,6 @@ class ExpandableTextState extends State List extractLinksAndTags(String text) { List textSpans = []; int lastIndex = 0; - const String regexLinksAndTags = - r'(?:(?:http|https|ftp|www)\:\/\/)?[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(?::[a-zA-Z0-9]*)?\/?[^\s\n]+|[\w\.-]+@[a-zA-Z\d\.-]+\.[a-zA-Z]{2,}|<<([^<>]+)\|route://member/([a-zA-Z-0-9]+)>>'; - RegExp regExp = RegExp(regexLinksAndTags); for (Match match in regExp.allMatches(text)) { int startIndex = match.start; int endIndex = match.end; diff --git a/lib/src/utils/constants.dart b/lib/src/utils/constants.dart index d12299f..252b203 100644 --- a/lib/src/utils/constants.dart +++ b/lib/src/utils/constants.dart @@ -1,2 +1,2 @@ const String kRegexLinksAndTags = - r'((?:http|https|ftp|www)\:\/\/)?[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(?::[a-zA-Z0-9]*)?\/?[^\s\n]+|@([^<>~]+)~|<<([^<>]+)\|route://member/([a-zA-Z-0-9]+)>>|#\w+'; + r'(?:(?:http|https|ftp|www)\:\/\/)?[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(?::[a-zA-Z0-9]*)?\/?[^\s\n]+|[\w\.-]+@[a-zA-Z\d\.-]+\.[a-zA-Z]{2,}|<<([^<>]+)\|route://member/([a-zA-Z-0-9]+)>>'; From b55b0acba6d65b1e6f4ad79e58d6f577cb377dd6 Mon Sep 17 00:00:00 2001 From: Anurag Tyagi Date: Wed, 15 Nov 2023 16:50:38 +0530 Subject: [PATCH 4/8] feat(PostUI): added post ui model --- lib/likeminds_feed_ui_fl.dart | 3 +- lib/src/models/models.dart | 3 + lib/src/models/post_ui.dart | 178 ++++++++++++++++++++++ lib/src/widgets/common/extras/loader.dart | 4 +- 4 files changed, 185 insertions(+), 3 deletions(-) create mode 100644 lib/src/models/models.dart create mode 100644 lib/src/models/post_ui.dart diff --git a/lib/likeminds_feed_ui_fl.dart b/lib/likeminds_feed_ui_fl.dart index 6941a8c..ee6dc05 100644 --- a/lib/likeminds_feed_ui_fl.dart +++ b/lib/likeminds_feed_ui_fl.dart @@ -4,5 +4,4 @@ export 'src/widgets/widgets.dart'; export 'src/utils/typedefs.dart'; export 'src/utils/helpers.dart'; export 'src/utils/utils.dart'; -export 'src/models/media_model.dart'; -export 'src/models/topic_ui.dart'; +export 'src/models/models.dart'; diff --git a/lib/src/models/models.dart b/lib/src/models/models.dart new file mode 100644 index 0000000..ff74445 --- /dev/null +++ b/lib/src/models/models.dart @@ -0,0 +1,3 @@ +export 'media_model.dart'; +export 'topic_ui.dart'; +export 'post_ui.dart'; \ No newline at end of file diff --git a/lib/src/models/post_ui.dart b/lib/src/models/post_ui.dart new file mode 100644 index 0000000..3fa01ef --- /dev/null +++ b/lib/src/models/post_ui.dart @@ -0,0 +1,178 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +// import 'package:likeminds_feed/src/models/feed/post.dart'; + +import 'package:likeminds_feed/likeminds_feed.dart'; + +class PostUI { + final String id; + String text; + List topics; + List? attachments; + final int communityId; + bool isPinned; + final String userId; + int likeCount; + int commentCount; + bool isSaved; + bool isLiked; + List menuItems; + final DateTime createdAt; + DateTime updatedAt; + bool isEdited; + + PostUI._({ + required this.id, + required this.text, + required this.attachments, + required this.communityId, + required this.isPinned, + required this.userId, + required this.likeCount, + required this.isSaved, + required this.topics, + required this.menuItems, + required this.createdAt, + required this.updatedAt, + required this.isLiked, + required this.commentCount, + required this.isEdited, + }); + + factory PostUI.fromPost({required Post post}) { + return PostUI._( + id: post.id, + isEdited: post.isEdited, + text: post.text, + attachments: post.attachments, + communityId: post.communityId, + isPinned: post.isPinned, + topics: post.topics ?? [], + userId: post.userId, + likeCount: post.likeCount, + commentCount: post.commentCount, + isSaved: post.isSaved, + isLiked: post.isLiked, + menuItems: post.menuItems, + createdAt: post.createdAt, + updatedAt: post.updatedAt, + ); + } + + Post toPost() { + return Post( + id: id, + text: text, + topics: topics, + isEdited: isEdited, + attachments: attachments, + communityId: communityId, + isPinned: isPinned, + userId: userId, + likeCount: likeCount, + isSaved: isSaved, + isLiked: isLiked, + commentCount: commentCount, + menuItems: menuItems, + createdAt: createdAt, + updatedAt: updatedAt, + ); + } +} + +class PostUIBuilder { + String? _id; + String? _text; + List? _topics; + List? _attachments; + int? _communityId; + bool? _isPinned; + String? _userId; + int? _likeCount; + int? _commentCount; + bool? _isSaved; + bool? _isLiked; + List? _menuItems; + DateTime? _createdAt; + DateTime? _updatedAt; + bool? _isEdited; + + void id(String id) { + _id = id; + } + + void text(String text) { + _text = text; + } + + void topics(List topics) { + _topics = topics; + } + + void attachments(List attachments) { + _attachments = attachments; + } + + void communityId(int communityId) { + _communityId = communityId; + } + + void isPinned(bool isPinned) { + _isPinned = isPinned; + } + + void userId(String userId) { + _userId = userId; + } + + void likeCount(int likeCount) { + _likeCount = likeCount; + } + + void commentCount(int commentCount) { + _commentCount = commentCount; + } + + void isSaved(bool isSaved) { + _isSaved = isSaved; + } + + void isLiked(bool isLiked) { + _isLiked = isLiked; + } + + void menuItems(List menuItems) { + _menuItems = menuItems; + } + + void createdAt(DateTime createdAt) { + _createdAt = createdAt; + } + + void updatedAt(DateTime updatedAt) { + _updatedAt = updatedAt; + } + + void isEdited(bool isEdited) { + _isEdited = isEdited; + } + + PostUI build() { + return PostUI._( + id: _id!, + text: _text!, + topics: _topics!, + attachments: _attachments, + communityId: _communityId!, + isPinned: _isPinned!, + userId: _userId!, + likeCount: _likeCount!, + commentCount: _commentCount!, + isSaved: _isSaved!, + isLiked: _isLiked!, + menuItems: _menuItems!, + createdAt: _createdAt!, + updatedAt: _updatedAt!, + isEdited: _isEdited!, + ); + } +} diff --git a/lib/src/widgets/common/extras/loader.dart b/lib/src/widgets/common/extras/loader.dart index c9c2c76..bc26de2 100644 --- a/lib/src/widgets/common/extras/loader.dart +++ b/lib/src/widgets/common/extras/loader.dart @@ -9,7 +9,9 @@ class LMLoader extends StatelessWidget { @override Widget build(BuildContext context) { return CircularProgressIndicator( - color: color ?? (isPrimary ? Theme.of(context).primaryColor : Colors.white), + color: color ?? (isPrimary ? Theme + .of(context) + .primaryColor : Colors.white), ); } } From e6cc3d3a85de2d7141e0b4c199f2cb15f95b60a6 Mon Sep 17 00:00:00 2001 From: Anurag Tyagi Date: Thu, 16 Nov 2023 16:28:58 +0530 Subject: [PATCH 5/8] chore(PostUI): replaced PostUI with PostViewData --- example/lib/views/feed/feed_page.dart | 4 ++-- .../lib/views/post_detail/post_detail_screen.dart | 3 ++- lib/src/models/post_ui.dart | 14 +++++++------- lib/src/widgets/post/post.dart | 5 +++-- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/example/lib/views/feed/feed_page.dart b/example/lib/views/feed/feed_page.dart index 1d8a25b..2a2a380 100644 --- a/example/lib/views/feed/feed_page.dart +++ b/example/lib/views/feed/feed_page.dart @@ -91,7 +91,7 @@ class _FeedScreenState extends State { children: [ const SizedBox(height: 8), LMPostWidget( - post: item, + post: PostViewData.fromPost(post: item), isFeed: true, user: feedResponse.users[item.userId]!, onTagTap: (String userId) { @@ -157,7 +157,7 @@ class _FeedScreenState extends State { class MyPostWidget extends LMPostWidget { MyPostWidget({ super.key, - required Post post, + required PostViewData post, required User user, required Function() onTap, required bool isFeed, diff --git a/example/lib/views/post_detail/post_detail_screen.dart b/example/lib/views/post_detail/post_detail_screen.dart index 09201a2..584e374 100644 --- a/example/lib/views/post_detail/post_detail_screen.dart +++ b/example/lib/views/post_detail/post_detail_screen.dart @@ -484,7 +484,8 @@ class _PostDetailScreenState extends State { child: postData == null ? const LMPostMediaShimmer() : LMPostWidget( - post: postData!, + post: PostViewData.fromPost( + post: postData!), onTagTap: (String userId) { locator() .routeToProfile(userId); diff --git a/lib/src/models/post_ui.dart b/lib/src/models/post_ui.dart index 3fa01ef..f54b68e 100644 --- a/lib/src/models/post_ui.dart +++ b/lib/src/models/post_ui.dart @@ -3,7 +3,7 @@ import 'package:likeminds_feed/likeminds_feed.dart'; -class PostUI { +class PostViewData { final String id; String text; List topics; @@ -20,7 +20,7 @@ class PostUI { DateTime updatedAt; bool isEdited; - PostUI._({ + PostViewData._({ required this.id, required this.text, required this.attachments, @@ -38,8 +38,8 @@ class PostUI { required this.isEdited, }); - factory PostUI.fromPost({required Post post}) { - return PostUI._( + factory PostViewData.fromPost({required Post post}) { + return PostViewData._( id: post.id, isEdited: post.isEdited, text: post.text, @@ -79,7 +79,7 @@ class PostUI { } } -class PostUIBuilder { +class PostViewDataBuilder { String? _id; String? _text; List? _topics; @@ -156,8 +156,8 @@ class PostUIBuilder { _isEdited = isEdited; } - PostUI build() { - return PostUI._( + PostViewData build() { + return PostViewData._( id: _id!, text: _text!, topics: _topics!, diff --git a/lib/src/widgets/post/post.dart b/lib/src/widgets/post/post.dart index 432b596..f4975ab 100644 --- a/lib/src/widgets/post/post.dart +++ b/lib/src/widgets/post/post.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:likeminds_feed/likeminds_feed.dart'; +import 'package:likeminds_feed_ui_fl/likeminds_feed_ui_fl.dart'; import 'package:likeminds_feed_ui_fl/src/utils/theme.dart'; import 'package:likeminds_feed_ui_fl/src/widgets/post/post_content.dart'; import 'package:likeminds_feed_ui_fl/src/widgets/post/post_footer.dart'; @@ -18,7 +19,7 @@ class LMPostWidget extends StatefulWidget { final LMPostMedia? media; // Required variables - final Post post; + final PostViewData post; final User user; final bool isFeed; final Function() onTap; @@ -100,7 +101,7 @@ class InheritedPostProvider extends InheritedWidget { @override final Widget child; - final Post post; + final PostViewData post; static InheritedPostProvider? of(BuildContext context) { return context.dependOnInheritedWidgetOfExactType(); From b7592081c1406aee9677029d5c4748e7e537c0fd Mon Sep 17 00:00:00 2001 From: Anuj Kumar <144224503+AnujLM@users.noreply.github.com> Date: Thu, 16 Nov 2023 18:14:55 +0530 Subject: [PATCH 6/8] fixed link regex 2 char tld --- .../expandable_text/expandable_text.dart | 2 +- lib/packages/linkify/linkify.dart | 124 ++++++++++++++++++ lib/packages/linkify/src/email.dart | 71 ++++++++++ lib/packages/linkify/src/url.dart | 115 ++++++++++++++++ lib/src/utils/constants.dart | 2 +- lib/src/utils/helpers.dart | 2 +- pubspec.yaml | 1 - 7 files changed, 313 insertions(+), 4 deletions(-) create mode 100644 lib/packages/linkify/linkify.dart create mode 100644 lib/packages/linkify/src/email.dart create mode 100644 lib/packages/linkify/src/url.dart diff --git a/lib/packages/expandable_text/expandable_text.dart b/lib/packages/expandable_text/expandable_text.dart index badc4f0..e78c8f6 100644 --- a/lib/packages/expandable_text/expandable_text.dart +++ b/lib/packages/expandable_text/expandable_text.dart @@ -6,9 +6,9 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:likeminds_feed/likeminds_feed.dart'; import 'package:likeminds_feed_ui_fl/likeminds_feed_ui_fl.dart'; +import 'package:likeminds_feed_ui_fl/packages/linkify/linkify.dart'; import 'package:likeminds_feed_ui_fl/src/utils/constants.dart'; import 'package:likeminds_feed_ui_fl/src/utils/theme.dart'; -import 'package:linkify/linkify.dart'; import 'package:url_launcher/url_launcher.dart'; import './text_parser.dart'; diff --git a/lib/packages/linkify/linkify.dart b/lib/packages/linkify/linkify.dart new file mode 100644 index 0000000..9edca15 --- /dev/null +++ b/lib/packages/linkify/linkify.dart @@ -0,0 +1,124 @@ +library linkify; + +import 'src/email.dart'; +import 'src/url.dart'; +export 'src/email.dart' show EmailLinkifier, EmailElement; +export 'src/url.dart' show UrlLinkifier, UrlElement; + + +abstract class LinkifyElement { + final String text; + final String originText; + + LinkifyElement(this.text, [String? originText]) + : originText = originText ?? text; + + @override + bool operator ==(other) => equals(other); + + @override + int get hashCode => Object.hash(text, originText); + + bool equals(other) => other is LinkifyElement && other.text == text; +} + +class LinkableElement extends LinkifyElement { + final String url; + + LinkableElement(String? text, this.url, [String? originText]) + : super(text ?? url, originText); + + @override + bool operator ==(other) => equals(other); + + @override + int get hashCode => Object.hash(text, originText, url); + + @override + bool equals(other) => + other is LinkableElement && super.equals(other) && other.url == url; +} + +/// Represents an element containing text +class TextElement extends LinkifyElement { + TextElement(String text) : super(text); + + @override + String toString() { + return "TextElement: '$text'"; + } + + @override + bool operator ==(other) => equals(other); + + @override + int get hashCode => Object.hash(text, originText); + + @override + bool equals(other) => other is TextElement && super.equals(other); +} + +abstract class Linkifier { + const Linkifier(); + + List parse( + List elements, LinkifyOptions options); +} + +class LinkifyOptions { + /// Removes http/https from shown URLs. + final bool humanize; + + /// Removes www. from shown URLs. + final bool removeWww; + + /// Enables loose URL parsing (any string with "." is a URL). + final bool looseUrl; + + /// When used with [looseUrl], default to `https` instead of `http`. + final bool defaultToHttps; + + /// Excludes `.` at end of URLs. + final bool excludeLastPeriod; + + const LinkifyOptions({ + this.humanize = true, + this.removeWww = false, + this.looseUrl = false, + this.defaultToHttps = false, + this.excludeLastPeriod = true, + }); +} + +const _urlLinkifier = UrlLinkifier(); +const _emailLinkifier = EmailLinkifier(); +const defaultLinkifiers = [_urlLinkifier, _emailLinkifier]; + +/// Turns [text] into a list of [LinkifyElement] +/// +/// Use [humanize] to remove http/https from the start of the URL shown. +/// Will default to `false` (if `null`) +/// +/// Uses [linkTypes] to enable some types of links (URL, email). +/// Will default to all (if `null`). +List linkify( + String text, { + LinkifyOptions options = const LinkifyOptions(), + List linkifiers = defaultLinkifiers, +}) { + var list = [TextElement(text)]; + + if (text.isEmpty) { + return []; + } + + if (linkifiers.isEmpty) { + return list; + } + + for (var linkifier in linkifiers) { + list = linkifier.parse(list, options); + } + + return list; +} \ No newline at end of file diff --git a/lib/packages/linkify/src/email.dart b/lib/packages/linkify/src/email.dart new file mode 100644 index 0000000..46aede2 --- /dev/null +++ b/lib/packages/linkify/src/email.dart @@ -0,0 +1,71 @@ +import 'package:likeminds_feed_ui_fl/packages/linkify/linkify.dart'; + +final _emailRegex = RegExp( + r'^(.*?)((mailto:)?[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z][A-Z]+)', + caseSensitive: false, + dotAll: true, +); + +class EmailLinkifier extends Linkifier { + const EmailLinkifier(); + + @override + List parse(elements, options) { + final list = []; + + for (var element in elements) { + if (element is TextElement) { + final match = _emailRegex.firstMatch(element.text); + + if (match == null) { + list.add(element); + } else { + final text = element.text.replaceFirst(match.group(0)!, ''); + + if (match.group(1)?.isNotEmpty == true) { + list.add(TextElement(match.group(1)!)); + } + + if (match.group(2)?.isNotEmpty == true) { + // Always humanize emails + list.add(EmailElement( + match.group(2)!.replaceFirst(RegExp(r'mailto:'), ''), + )); + } + + if (text.isNotEmpty) { + list.addAll(parse([TextElement(text)], options)); + } + } + } else { + list.add(element); + } + } + + return list; + } +} + +/// Represents an element containing an email address +class EmailElement extends LinkableElement { + final String emailAddress; + + EmailElement(this.emailAddress) : super(emailAddress, 'mailto:$emailAddress'); + + @override + String toString() { + return "EmailElement: '$emailAddress' ($text)"; + } + + @override + bool operator ==(other) => equals(other); + + @override + int get hashCode => Object.hash(text, originText, url, emailAddress); + + @override + bool equals(other) => + other is EmailElement && + super.equals(other) && + other.emailAddress == emailAddress; +} \ No newline at end of file diff --git a/lib/packages/linkify/src/url.dart b/lib/packages/linkify/src/url.dart new file mode 100644 index 0000000..2e041ab --- /dev/null +++ b/lib/packages/linkify/src/url.dart @@ -0,0 +1,115 @@ +import 'package:likeminds_feed_ui_fl/packages/linkify/linkify.dart'; + +final _urlRegex = RegExp( + r'^(.*?)((?:https?:\/\/|www\.)[^\s/$.?#].[^\s]*)', + caseSensitive: false, + dotAll: true, +); + +final _looseUrlRegex = RegExp( + r"^(.*?)((https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{1,4}\b([-a-zA-Z0-9@:%_\+.~#?&//=']*)?)", + caseSensitive: false, + dotAll: true, +); + + +final _protocolIdentifierRegex = RegExp( + r'^(https?:\/\/)', + caseSensitive: false, +); + +class UrlLinkifier extends Linkifier { + const UrlLinkifier(); + + @override + List parse(elements, options) { + final list = []; + + for (var element in elements) { + if (element is TextElement) { + var match = options.looseUrl + ? _looseUrlRegex.firstMatch(element.text) + : _urlRegex.firstMatch(element.text); + + if (match == null) { + list.add(element); + } else { + final text = element.text.replaceFirst(match.group(0)!, ''); + + if (match.group(1)?.isNotEmpty == true) { + list.add(TextElement(match.group(1)!)); + } + + if (match.group(2)?.isNotEmpty == true) { + var originalUrl = match.group(2)!; + var originText = originalUrl; + String? end; + + if ((options.excludeLastPeriod) && + originalUrl[originalUrl.length - 1] == ".") { + end = "."; + originText = originText.substring(0, originText.length - 1); + originalUrl = originalUrl.substring(0, originalUrl.length - 1); + } + + var url = originalUrl; + + if (!originalUrl.startsWith(_protocolIdentifierRegex)) { + originalUrl = (options.defaultToHttps ? "https://" : "http://") + + originalUrl; + } + + if ((options.humanize) || (options.removeWww)) { + if (options.humanize) { + url = url.replaceFirst(RegExp(r'https?://'), ''); + } + if (options.removeWww) { + url = url.replaceFirst(RegExp(r'www\.'), ''); + } + + list.add(UrlElement( + originalUrl, + url, + originText, + )); + } else { + list.add(UrlElement(originalUrl, null, originText)); + } + + if (end != null) { + list.add(TextElement(end)); + } + } + + if (text.isNotEmpty) { + list.addAll(parse([TextElement(text)], options)); + } + } + } else { + list.add(element); + } + } + + return list; + } +} + +/// Represents an element containing a link +class UrlElement extends LinkableElement { + UrlElement(String url, [String? text, String? originText]) + : super(text, url, originText); + + @override + String toString() { + return "LinkElement: '$url' ($text)"; + } + + @override + bool operator ==(other) => equals(other); + + @override + int get hashCode => Object.hash(text, originText, url); + + @override + bool equals(other) => other is UrlElement && super.equals(other); +} \ No newline at end of file diff --git a/lib/src/utils/constants.dart b/lib/src/utils/constants.dart index 252b203..bd4f7dc 100644 --- a/lib/src/utils/constants.dart +++ b/lib/src/utils/constants.dart @@ -1,2 +1,2 @@ const String kRegexLinksAndTags = - r'(?:(?:http|https|ftp|www)\:\/\/)?[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(?::[a-zA-Z0-9]*)?\/?[^\s\n]+|[\w\.-]+@[a-zA-Z\d\.-]+\.[a-zA-Z]{2,}|<<([^<>]+)\|route://member/([a-zA-Z-0-9]+)>>'; + r'(?:(?:http|https|ftp|www)\:\/\/)?[a-zA-Z0-9\-\.]+\.[a-zA-Z]{1,3}(?::[a-zA-Z0-9]*)?\/?[^\s\n]+|[\w\.-]+@[a-zA-Z\d\.-]+\.[a-zA-Z]{2,}|<<([^<>]+)\|route://member/([a-zA-Z-0-9]+)>>'; diff --git a/lib/src/utils/helpers.dart b/lib/src/utils/helpers.dart index 093b55f..1306314 100644 --- a/lib/src/utils/helpers.dart +++ b/lib/src/utils/helpers.dart @@ -3,8 +3,8 @@ import 'dart:math'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:likeminds_feed/likeminds_feed.dart'; +import 'package:likeminds_feed_ui_fl/packages/linkify/linkify.dart'; import 'package:likeminds_feed_ui_fl/src/utils/theme.dart'; -import 'package:linkify/linkify.dart'; class TaggingHelper { static final RegExp tagRegExp = RegExp(r'@([^<>~]+)~'); diff --git a/pubspec.yaml b/pubspec.yaml index 6eece6a..ca03023 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,7 +29,6 @@ dependencies: visibility_aware_state: 1.0.5 likeminds_feed: 1.6.3 - linkify: # path: ../LikeMinds-Flutter-Feed-SDK From 16a228d0e73a91f5e487280407027a1c53690997 Mon Sep 17 00:00:00 2001 From: Anuj Kumar <144224503+AnujLM@users.noreply.github.com> Date: Thu, 16 Nov 2023 18:48:57 +0530 Subject: [PATCH 7/8] fix link regex for https:// and www. --- lib/packages/linkify/src/url.dart | 2 +- lib/src/utils/constants.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/packages/linkify/src/url.dart b/lib/packages/linkify/src/url.dart index 2e041ab..d97ab97 100644 --- a/lib/packages/linkify/src/url.dart +++ b/lib/packages/linkify/src/url.dart @@ -7,7 +7,7 @@ final _urlRegex = RegExp( ); final _looseUrlRegex = RegExp( - r"^(.*?)((https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{1,4}\b([-a-zA-Z0-9@:%_\+.~#?&//=']*)?)", + r"^(.*?)((?:https?:\/\/|www\.)[^\s/$.?#].[^\s]*)|^(.*?)(https?:\/\/|www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{1,4}\b([-a-zA-Z0-9@:%_\+.~#?&//=']*)?$", caseSensitive: false, dotAll: true, ); diff --git a/lib/src/utils/constants.dart b/lib/src/utils/constants.dart index bd4f7dc..3b53c3b 100644 --- a/lib/src/utils/constants.dart +++ b/lib/src/utils/constants.dart @@ -1,2 +1,2 @@ const String kRegexLinksAndTags = - r'(?:(?:http|https|ftp|www)\:\/\/)?[a-zA-Z0-9\-\.]+\.[a-zA-Z]{1,3}(?::[a-zA-Z0-9]*)?\/?[^\s\n]+|[\w\.-]+@[a-zA-Z\d\.-]+\.[a-zA-Z]{2,}|<<([^<>]+)\|route://member/([a-zA-Z-0-9]+)>>'; + r'(?:(?:http|https|ftp|www)\:\/\/)?[a-zA-Z0-9\-\.]+\.[a-zA-Z]{1,}(?::[a-zA-Z0-9]*)?\/?[^\s\n]+|[\w\.-]+@[a-zA-Z\d\.-]+\.[a-zA-Z]{2,}|<<([^<>]+)\|route://member/([a-zA-Z-0-9]+)>>'; From 6882a1894d4a1f5f96fa225477caebcc62053c94 Mon Sep 17 00:00:00 2001 From: Anuj Kumar <144224503+AnujLM@users.noreply.github.com> Date: Thu, 16 Nov 2023 20:14:32 +0530 Subject: [PATCH 8/8] fiexd link reges issues --- lib/packages/linkify/src/url.dart | 2 +- lib/src/utils/helpers.dart | 14 +++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/packages/linkify/src/url.dart b/lib/packages/linkify/src/url.dart index d97ab97..32d6e8d 100644 --- a/lib/packages/linkify/src/url.dart +++ b/lib/packages/linkify/src/url.dart @@ -7,7 +7,7 @@ final _urlRegex = RegExp( ); final _looseUrlRegex = RegExp( - r"^(.*?)((?:https?:\/\/|www\.)[^\s/$.?#].[^\s]*)|^(.*?)(https?:\/\/|www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{1,4}\b([-a-zA-Z0-9@:%_\+.~#?&//=']*)?$", + r'''^(.*?)((https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\+.~#?&//=/'"`]*))''', caseSensitive: false, dotAll: true, ); diff --git a/lib/src/utils/helpers.dart b/lib/src/utils/helpers.dart index 1306314..797788f 100644 --- a/lib/src/utils/helpers.dart +++ b/lib/src/utils/helpers.dart @@ -230,14 +230,22 @@ String getFirstValidLinkFromString(String text) { } LinkifyElement? extractLinkAndEmailFromString(String text) { - debugPrint("text: $text"); + final urls = linkify(text, linkifiers: [ + const EmailLinkifier(), + const UrlLinkifier(), + ]); + if (urls.isNotEmpty) { + if (urls.first is EmailElement || urls.first is UrlElement) { + return urls.first; + } + } final links = linkify(text, options: const LinkifyOptions( looseUrl: true, ), linkifiers: [ - EmailLinkifier(), - UrlLinkifier(), + const EmailLinkifier(), + const UrlLinkifier(), ]); if (links.isNotEmpty) { if (links.first is EmailElement || links.first is UrlElement) {