From 381e7cdea575754699fc70d646b165dc2db9ad9d Mon Sep 17 00:00:00 2001 From: aditya-likeminds <141997698+aditya-likeminds@users.noreply.github.com> Date: Sun, 8 Oct 2023 23:26:17 +0530 Subject: [PATCH 01/19] added lint rules --- analysis_options.yaml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/analysis_options.yaml b/analysis_options.yaml index a5744c1..ea22b62 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -2,3 +2,30 @@ include: package:flutter_lints/flutter.yaml # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # Style rules + - camel_case_types + - library_names + - avoid_catches_without_on_clauses + - avoid_catching_errors + - avoid_empty_else + - unnecessary_brace_in_string_interps + - avoid_redundant_argument_values + - leading_newlines_in_multiline_strings + # formatting + - lines_longer_than_80_chars + - curly_braces_in_flow_control_structures + # doc comments + - slash_for_doc_comments \ No newline at end of file From a459a7a2243d2b6e20129176eb4293392a5276e3 Mon Sep 17 00:00:00 2001 From: aditya-likeminds <141997698+aditya-likeminds@users.noreply.github.com> Date: Sun, 8 Oct 2023 23:26:26 +0530 Subject: [PATCH 02/19] added pull-request workflow --- .github/workflows/pull_request.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/pull_request.yml diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 0000000..8cec717 --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,27 @@ +name: Feed SDK test on every PR + +on: + pull_request: + branches-ignore: + - master + +jobs: + run-flutter-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: subosito/flutter-action@v2 + with: + flutter-version: ${{ env.FLUTTER_VERSION }} + - run: flutter pub get + + - name: Lint + run: flutter analyze > lint-results.txt + + - name: Upload the lint results as an artifact + if: always() + uses: actions/upload-artifact@v2 + with: + name: lint-results + path: lint-results.txt \ No newline at end of file From 3e758e902d19b52c6c0ff2e6966ac08027c4358a Mon Sep 17 00:00:00 2001 From: aditya-likeminds <141997698+aditya-likeminds@users.noreply.github.com> Date: Sun, 8 Oct 2023 23:26:34 +0530 Subject: [PATCH 03/19] added release workflow --- .github/workflows/release.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..087bafb --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,20 @@ +name: Create Release + +on: + push: + tags: + - 'v*' + +jobs: + create-github-release: + name: Create GitHub Release + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Create Release + run: gh release create ${{ github.ref }} --generate-notes + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file From bf0c4b852ebaedb3bdb377e390de2338181a4115 Mon Sep 17 00:00:00 2001 From: aditya-likeminds Date: Wed, 11 Oct 2023 20:50:07 +0530 Subject: [PATCH 04/19] fix: git tag and release --- .github/workflows/release.yml | 55 +++++++++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 087bafb..99cc23e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,20 +1,65 @@ -name: Create Release +name: Create Tag and Release on Version Change on: push: - tags: - - 'v*' + branches: + - master jobs: + create_tag: + name: Create Git Tag + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Check for version changes + run: | + # Fetch all tags from the remote repository + git fetch --tags + + # Get the previous version from the last release tag + export previous_version=$(git describe --tags --abbrev=0) + + # Get the current version from pubspec.yaml + export current_version=$(cat pubspec.yaml | grep 'version:' | awk '{print $2}') + + if [[ "$previous_version" != "v$current_version" ]]; then + echo "Version has changed from $previous_version to v$current_version." + else + echo "Version has not changed." + exit 1 + fi + + - name: Push Git Tag + run: | + # Git login + git config --global user.name "$(git log -n 1 --pretty=format:%an)" + git config --global user.email "$(git log -n 1 --pretty=format:%ae)" + + # Push a Git tag with the new version + export current_version=$(cat pubspec.yaml | grep 'version:' | awk '{print $2}') + git tag -a "v$current_version" -m "Version $current_version" + git push origin "v$current_version" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + create-github-release: name: Create GitHub Release runs-on: ubuntu-latest + needs: create_tag permissions: contents: write steps: - name: Checkout code uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Create Release - run: gh release create ${{ github.ref }} --generate-notes + run: gh release create "$(git describe --tags --abbrev=0)" --generate-notes env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 770d8a0d2eafc4ec2a4987c31f6124c1d90c23bd Mon Sep 17 00:00:00 2001 From: aditya-likeminds Date: Wed, 25 Oct 2023 13:09:04 +0530 Subject: [PATCH 05/19] fix(workflows): write permissions --- .github/workflows/release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 99cc23e..3ab308e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,6 +5,8 @@ on: branches: - master +permissions: write-all + jobs: create_tag: name: Create Git Tag From 2033d806dd14aa16832fe41d2db8b18a6a5d331b Mon Sep 17 00:00:00 2001 From: Anuj Kumar <144224503+AnujLM@users.noreply.github.com> Date: Thu, 2 Nov 2023 16:13:04 +0530 Subject: [PATCH 06/19] fix menu overflow --- lib/src/widgets/conversation/chat_bubble.dart | 72 +++++++------------ pubspec.yaml | 1 + 2 files changed, 25 insertions(+), 48 deletions(-) diff --git a/lib/src/widgets/conversation/chat_bubble.dart b/lib/src/widgets/conversation/chat_bubble.dart index cdef301..802c374 100644 --- a/lib/src/widgets/conversation/chat_bubble.dart +++ b/lib/src/widgets/conversation/chat_bubble.dart @@ -1,3 +1,4 @@ +import 'package:contextmenu/contextmenu.dart'; import 'package:custom_pop_up_menu/custom_pop_up_menu.dart'; import 'package:flutter/material.dart'; import 'package:likeminds_chat_fl/likeminds_chat_fl.dart'; @@ -189,54 +190,29 @@ class _LMChatBubbleState extends State { const SizedBox(width: 4), AbsorbPointer( absorbing: isDeleted, - child: CustomPopupMenu( - controller: widget.menuController ?? _menuController, - pressType: PressType.longPress, - showArrow: false, - verticalMargin: 2.h, - horizontalMargin: isSent ? 2.w : 18.w, - menuBuilder: () => !isDeleted - ? GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - debugPrint("Menu object tapped"); - }, - child: widget.menu ?? - ClipRRect( - child: Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: - BorderRadius.circular(6), - ), - constraints: BoxConstraints( - minWidth: 42.w, - maxWidth: 60.w, - ), - child: IntrinsicWidth( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: [ - for (LMMenuItemUI menuItem - in widget.menuItems!) - ListTile( - onTap: () { - menuItem.onTap(); - _menuController.hideMenu(); - }, - leading: menuItem.leading, - title: menuItem.title, - splashColor: Colors.grey - .withOpacity(0.5), - ), - ], - ), - ), - ), - ), - ) - : const SizedBox(), + child: ContextMenuArea( + width: 60.w, + verticalPadding: 2.h, + // controller: widget.menuController ?? _menuController, + // pressType: PressType.longPress, + // showArrow: false, + // verticalMargin: 2.h, + // horizontalMargin: isSent ? 2.w : 18.w, + builder: (context) => !isDeleted + ? [ + for (LMMenuItemUI menuItem in widget.menuItems!) + ListTile( + onTap: () { + menuItem.onTap(); + Navigator.pop(context); + }, + leading: menuItem.leading, + title: menuItem.title, + splashColor: Colors.grey.withOpacity(0.5), + ), + kVerticalPaddingSmall, + ] + : [const SizedBox()], child: Container( constraints: BoxConstraints( minHeight: 4.h, diff --git a/pubspec.yaml b/pubspec.yaml index 4681321..69cc615 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,6 +26,7 @@ dependencies: swipe_to_action: video_thumbnail: custom_pop_up_menu: + contextmenu: ^3.0.0 dev_dependencies: flutter_test: From c93a8eecebfd5b8cb95172f8b791b028f83f2950 Mon Sep 17 00:00:00 2001 From: Anuj Kumar <144224503+AnujLM@users.noreply.github.com> Date: Fri, 3 Nov 2023 17:39:25 +0530 Subject: [PATCH 07/19] create LMMenu --- example/lib/views/chatroom_page.dart | 1 - lib/likeminds_chat_ui_fl.dart | 1 + lib/src/models/menu_decoration_model.dart | 26 ++++++++ lib/src/widgets/chatroom/chatroom_topic.dart | 14 ++--- lib/src/widgets/common/menu/menu.dart | 60 +++++++++++++++++++ lib/src/widgets/conversation/chat_bubble.dart | 39 ++---------- lib/src/widgets/widgets.dart | 2 + pubspec.yaml | 2 - 8 files changed, 101 insertions(+), 44 deletions(-) create mode 100644 lib/src/models/menu_decoration_model.dart create mode 100644 lib/src/widgets/common/menu/menu.dart diff --git a/example/lib/views/chatroom_page.dart b/example/lib/views/chatroom_page.dart index 69e12f2..10fcc89 100644 --- a/example/lib/views/chatroom_page.dart +++ b/example/lib/views/chatroom_page.dart @@ -1,4 +1,3 @@ -import 'package:custom_pop_up_menu/custom_pop_up_menu.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/lib/likeminds_chat_ui_fl.dart b/lib/likeminds_chat_ui_fl.dart index d7a2b9d..2bf9fb8 100644 --- a/lib/likeminds_chat_ui_fl.dart +++ b/lib/likeminds_chat_ui_fl.dart @@ -4,3 +4,4 @@ export 'src/widgets/widgets.dart'; export 'src/utils/utils.dart'; export 'src/models/media_model.dart'; export 'src/models/menu_item_model.dart'; +export 'src/models/menu_decoration_model.dart'; diff --git a/lib/src/models/menu_decoration_model.dart b/lib/src/models/menu_decoration_model.dart new file mode 100644 index 0000000..4b088af --- /dev/null +++ b/lib/src/models/menu_decoration_model.dart @@ -0,0 +1,26 @@ + +import 'package:flutter/material.dart'; + +class LMMenuDecoration { + final double? elevation; + final Color? shadowColor; + final Color? surfaceTintColor; + final String? semanticLabel; + final ShapeBorder? shape; + final Color? color; + final BoxConstraints? constraints; + final Clip? clipBehavior; + final EdgeInsets? itemPadding; + + LMMenuDecoration({ + this.elevation, + this.shadowColor, + this.surfaceTintColor, + this.semanticLabel, + this.shape, + this.color, + this.constraints, + this.clipBehavior, + this.itemPadding, + }); +} diff --git a/lib/src/widgets/chatroom/chatroom_topic.dart b/lib/src/widgets/chatroom/chatroom_topic.dart index 4570ead..3899575 100644 --- a/lib/src/widgets/chatroom/chatroom_topic.dart +++ b/lib/src/widgets/chatroom/chatroom_topic.dart @@ -84,13 +84,13 @@ class LMChatRoomTopic extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - height: 65, - decoration: BoxDecoration( - color: backGroundColor ?? kWhiteColor, - ), - child: GestureDetector( - onTap: onTap, + return GestureDetector( + onTap: onTap, + child: Container( + height: 65, + decoration: BoxDecoration( + color: backGroundColor ?? kWhiteColor, + ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), child: Row(children: [ diff --git a/lib/src/widgets/common/menu/menu.dart b/lib/src/widgets/common/menu/menu.dart new file mode 100644 index 0000000..e27226c --- /dev/null +++ b/lib/src/widgets/common/menu/menu.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:likeminds_chat_ui_fl/likeminds_chat_ui_fl.dart'; + +class LMMenu extends StatelessWidget { + const LMMenu({ + super.key, + required this.menuItems, + required this.child, + this.menuDecoration, + }); + final List menuItems; + final Widget child; + final LMMenuDecoration? menuDecoration; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onLongPressStart: (details) { + if (menuItems.isNotEmpty) { + final RenderBox overlay = + Overlay.of(context).context.findRenderObject() as RenderBox; + + final RelativeRect position = RelativeRect.fromRect( + Rect.fromPoints( + details.globalPosition, + details.globalPosition.translate(0, 0), + ), + overlay.localToGlobal(Offset.zero) & overlay.size, + ); + showMenu( + context: context, + position: position, + elevation: menuDecoration?.elevation, + shadowColor: menuDecoration?.shadowColor, + surfaceTintColor: menuDecoration?.surfaceTintColor, + semanticLabel: menuDecoration?.semanticLabel, + shape: menuDecoration?.shape, + color: menuDecoration?.color, + constraints: menuDecoration?.constraints, + clipBehavior: menuDecoration?.clipBehavior ?? Clip.none, + items: [ + for (LMMenuItemUI menuItem in menuItems) + PopupMenuItem( + onTap: () { + menuItem.onTap(); + }, + padding: menuDecoration?.itemPadding ?? EdgeInsets.zero, + child: ListTile( + leading: menuItem.leading, + title: menuItem.title, + ), + ), + // kVerticalPaddingSmall, + ]); + } + }, + child: child, + ); + } +} diff --git a/lib/src/widgets/conversation/chat_bubble.dart b/lib/src/widgets/conversation/chat_bubble.dart index 802c374..8b640d2 100644 --- a/lib/src/widgets/conversation/chat_bubble.dart +++ b/lib/src/widgets/conversation/chat_bubble.dart @@ -1,10 +1,7 @@ -import 'package:contextmenu/contextmenu.dart'; -import 'package:custom_pop_up_menu/custom_pop_up_menu.dart'; import 'package:flutter/material.dart'; import 'package:likeminds_chat_fl/likeminds_chat_fl.dart'; import 'package:likeminds_chat_ui_fl/likeminds_chat_ui_fl.dart'; import 'package:likeminds_chat_ui_fl/src/utils/theme.dart'; -import 'package:likeminds_chat_ui_fl/src/models/menu_item_model.dart'; import 'package:swipe_to_action/swipe_to_action.dart'; class LMChatBubble extends StatefulWidget { @@ -22,9 +19,8 @@ class LMChatBubble extends StatefulWidget { this.reactionButton, this.outsideTitle, this.outsideFooter, - this.menu, - this.menuController, this.menuItems, + this.menuDecoration, this.onReply, this.onEdit, this.onLongPress, @@ -58,10 +54,8 @@ class LMChatBubble extends StatefulWidget { final LMTextView? outsideTitle; final Widget? outsideFooter; final Widget? mediaWidget; - - final CustomPopupMenuController? menuController; - final Widget? menu; final List? menuItems; + final LMMenuDecoration? menuDecoration; final Function(Conversation replyingTo)? onReply; final Function(Conversation editConversation)? onEdit; final Function(Conversation conversation)? onLongPress; @@ -91,8 +85,6 @@ class _LMChatBubbleState extends State { Conversation? replyingTo; User? sender; User? currentUser; - late CustomPopupMenuController _menuController; - bool isSent = false; bool isDeleted = false; bool isEdited = false; @@ -107,7 +99,6 @@ class _LMChatBubbleState extends State { replyingTo = widget.replyingTo; isEdited = widget.conversation.isEdited ?? false; isDeleted = widget.conversation.deletedByUserId != null; - _menuController = CustomPopupMenuController(); } @override @@ -190,29 +181,9 @@ class _LMChatBubbleState extends State { const SizedBox(width: 4), AbsorbPointer( absorbing: isDeleted, - child: ContextMenuArea( - width: 60.w, - verticalPadding: 2.h, - // controller: widget.menuController ?? _menuController, - // pressType: PressType.longPress, - // showArrow: false, - // verticalMargin: 2.h, - // horizontalMargin: isSent ? 2.w : 18.w, - builder: (context) => !isDeleted - ? [ - for (LMMenuItemUI menuItem in widget.menuItems!) - ListTile( - onTap: () { - menuItem.onTap(); - Navigator.pop(context); - }, - leading: menuItem.leading, - title: menuItem.title, - splashColor: Colors.grey.withOpacity(0.5), - ), - kVerticalPaddingSmall, - ] - : [const SizedBox()], + child: LMMenu( + menuItems: widget.menuItems ?? [], + menuDecoration: widget.menuDecoration, child: Container( constraints: BoxConstraints( minHeight: 4.h, diff --git a/lib/src/widgets/widgets.dart b/lib/src/widgets/widgets.dart index 9f7cdfb..bebd9f2 100644 --- a/lib/src/widgets/widgets.dart +++ b/lib/src/widgets/widgets.dart @@ -18,6 +18,8 @@ export 'common/sheets/group_detail_bottom_sheet.dart'; export 'common/profile_picture.dart'; +export 'common/menu/menu.dart'; + export 'chatroom/chatroom_topic.dart'; // export 'media/document.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 69cc615..38d7ece 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,8 +25,6 @@ dependencies: url_launcher: swipe_to_action: video_thumbnail: - custom_pop_up_menu: - contextmenu: ^3.0.0 dev_dependencies: flutter_test: From 916a4ab4da8b9cf84366bd8057eb4d742f42bbb9 Mon Sep 17 00:00:00 2001 From: Anuj Kumar <144224503+AnujLM@users.noreply.github.com> Date: Wed, 8 Nov 2023 18:00:22 +0530 Subject: [PATCH 08/19] LMTagViewData implementation --- .../lib/utils/constants/string_constants.dart | 2 +- lib/likeminds_chat_ui_fl.dart | 1 + lib/src/models/tag_view_data_model.dart | 137 ++++++++++++++++++ lib/src/utils/constants.dart | 2 +- lib/src/utils/helpers.dart | 56 ++++--- 5 files changed, 177 insertions(+), 21 deletions(-) create mode 100644 lib/src/models/tag_view_data_model.dart diff --git a/example/lib/utils/constants/string_constants.dart b/example/lib/utils/constants/string_constants.dart index 6455148..e814a02 100644 --- a/example/lib/utils/constants/string_constants.dart +++ b/example/lib/utils/constants/string_constants.dart @@ -2,7 +2,7 @@ const String kStringLike = "Like"; const String kStringLikes = "Likes"; const String kStringAddComment = "Add Comment"; 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]+)>>'; + 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]+)>>|<<([^<>]+)\|route://participants>>'; // Attachment Type Constants const String kAttachmentTypeImage = "image"; diff --git a/lib/likeminds_chat_ui_fl.dart b/lib/likeminds_chat_ui_fl.dart index d7a2b9d..f60281f 100644 --- a/lib/likeminds_chat_ui_fl.dart +++ b/lib/likeminds_chat_ui_fl.dart @@ -4,3 +4,4 @@ export 'src/widgets/widgets.dart'; export 'src/utils/utils.dart'; export 'src/models/media_model.dart'; export 'src/models/menu_item_model.dart'; +export 'src/models/tag_view_data_model.dart'; \ No newline at end of file diff --git a/lib/src/models/tag_view_data_model.dart b/lib/src/models/tag_view_data_model.dart new file mode 100644 index 0000000..c4ed254 --- /dev/null +++ b/lib/src/models/tag_view_data_model.dart @@ -0,0 +1,137 @@ +import 'package:likeminds_chat_fl/likeminds_chat_fl.dart'; + +enum LMTagType { groupTag, userTag } + +class LMTagViewData { + //common values + final String name; + final String imageUrl; + final LMTagType tagType; + + // groupTag specific values + final String? description; + final String? route; + final String? tag; + + // userTag specific values + final String? customTitle; + final int? id; + final bool? isGuest; + final String? userUniqueId; + + LMTagViewData._({ + required this.name, + required this.imageUrl, + required this.tagType, + this.description, + this.route, + this.tag, + this.customTitle, + this.id, + this.isGuest, + this.userUniqueId, + }); + + //factory constructor for groupTag + factory LMTagViewData.fromGroupTag(GroupTag groupTag) { + return (LMTagViewDataBuilder() + ..name(groupTag.name!) + ..imageUrl(groupTag.imageUrl!) + ..tagType(LMTagType.groupTag) + ..description(groupTag.description) + ..route(groupTag.route) + ..tag(groupTag.tag)) + .build(); + } + + //factory constructor for userTag + factory LMTagViewData.fromUserTag(UserTag userTag) { + return (LMTagViewDataBuilder() + ..name(userTag.name!) + ..imageUrl(userTag.imageUrl!) + ..tagType(LMTagType.userTag) + ..customTitle(userTag.customTitle) + ..id(userTag.id) + ..isGuest(userTag.isGuest) + ..userUniqueId(userTag.userUniqueId)) + .build(); + } +} + +// Builder class +class LMTagViewDataBuilder { + String? _name; + String? _imageUrl; + LMTagType? _tagType; + String? _description; + String? _route; + String? _tag; + String? _customTitle; + int? _id; + bool? _isGuest; + String? _userUniqueId; + + void name(String name) { + _name = name; + } + + void imageUrl(String imageUrl) { + _imageUrl = imageUrl; + } + void tagType(LMTagType tagType) { + _tagType = tagType; + } + + void description(String? description) { + _description = description; + } + + void route(String? route) { + _route = route; + } + + void tag(String? tag) { + _tag = tag; + } + + void customTitle(String? customTitle) { + _customTitle = customTitle; + } + + void id(int? id) { + _id = id; + } + + void isGuest(bool? isGuest) { + _isGuest = isGuest; + } + + void userUniqueId(String? userUniqueId) { + _userUniqueId = userUniqueId; + } + + LMTagViewData build() { + if (_name == null) { + throw StateError("name is required"); + } + if (_imageUrl == null) { + throw StateError("imageUrl is required"); + } + if (_tagType == null) { + throw StateError("lmTagType is required"); + } + + return LMTagViewData._( + name: _name!, + imageUrl: _imageUrl!, + tagType: _tagType!, + description: _description, + route: _route, + tag: _tag, + customTitle: _customTitle, + id: _id, + isGuest: _isGuest, + userUniqueId: _userUniqueId, + ); + } +} diff --git a/lib/src/utils/constants.dart b/lib/src/utils/constants.dart index c5010f1..caee574 100644 --- a/lib/src/utils/constants.dart +++ b/lib/src/utils/constants.dart @@ -1,5 +1,5 @@ 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]+)>>'; + 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]+)>>|<<@participants\|route://participants>>'; // Attachment Type Constants const String kAttachmentTypeImage = "image"; diff --git a/lib/src/utils/helpers.dart b/lib/src/utils/helpers.dart index 560fe92..6a8ebec 100644 --- a/lib/src/utils/helpers.dart +++ b/lib/src/utils/helpers.dart @@ -8,9 +8,9 @@ import 'package:likeminds_chat_ui_fl/src/utils/theme.dart'; class TaggingHelper { static final RegExp tagRegExp = RegExp(r'@([^<>~]+)~'); static const String notificationTagRoute = - r'<<([^<>]+)\|route://([^<>]+)/([a-zA-Z-0-9]+)>>'; + r'<<([^<>]+)\|route://([^<>]+)/([a-zA-Z-0-9]+)>>|<<([^<>]+)\|route://participants>>'; static const String tagRoute = - r'<<([^<>]+)\|route://member/([a-zA-Z-0-9]+)>>'; + r'<<([^<>]+)\|route://member/([a-zA-Z-0-9]+)>>|<<([^<>]+)\|route://participants>>'; static const String linkRoute = r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+'; @@ -36,13 +36,17 @@ class TaggingHelper { /// Decodes the string with the user tags and returns the decoded string static Map decodeString(String string) { Map result = {}; - final Iterable matches = - RegExp(notificationTagRoute).allMatches(string); + final Iterable matches = RegExp(tagRoute).allMatches(string); for (final match in matches) { - final String tag = match.group(1)!; - final String id = match.group(3)!; - string = string.replaceAll('<<$tag|route://member/$id>>', '@$tag'); - result.addAll({tag: id}); + final String tag = match.group(1) ?? match.group(3)!; + final String? id = match.group(2); + if (id != null) { + string = string.replaceAll('<<$tag|route://member/$id>>', '@$tag'); + } else { + string = + string.replaceAll('<<@participants|route://participants>', '@$tag'); + } + result.addAll({tag: id ?? ''}); } return result; } @@ -52,11 +56,16 @@ class TaggingHelper { final Iterable matches = RegExp(notificationTagRoute).allMatches(string); for (final match in matches) { - final String tag = match.group(1)!; - final String mid = match.group(2)!; - final String id = match.group(3)!; - string = string.replaceAll('<<$tag|route://$mid/$id>>', '@$tag'); - result.addAll({tag: id}); + final String tag = match.group(1) ?? match.group(4)!; + final String? mid = match.group(2); + final String? id = match.group(3); + if (id != null) { + string = string.replaceAll('<<$tag|route://$mid/$id>>', '@$tag'); + } else { + string = + string.replaceAll('<<@participants|route://participants>', '@$tag'); + } + result.addAll({tag: id ?? ''}); } return result; } @@ -82,10 +91,14 @@ class TaggingHelper { for (final match in matches) { final String tag = match.group(1)!; - final String mid = match.group(2)!; - final String id = match.group(3)!; - text = text.replaceAll( + final String? mid = match.group(2); + final String? id = match.group(3); + if(id!=null) { + text = text.replaceAll( '<<$tag|route://$mid/$id>>', withTilde ? '@$tag~' : '@$tag'); + } else { + text = text.replaceAll('<<@participants|route://participants>', '@$tag'); + } } return text; } @@ -97,9 +110,14 @@ class TaggingHelper { for (final match in matches) { final String tag = match.group(1)!; - final String mid = match.group(2)!; - final String id = match.group(3)!; - text = text.replaceAll('<<$tag|route://$mid/$id>>', '@$tag~'); + final String? mid = match.group(2); + final String? id = match.group(3); + if(id!=null) { + text = text.replaceAll('<<$tag|route://$mid/$id>>', '@$tag~'); + } else { + + text = text.replaceAll('<<@participants|route://participants>', '@$tag'); + } } return text; } From 4d89afc1068fb9d1c0a7d3ff75d85b7a4d3587a2 Mon Sep 17 00:00:00 2001 From: Anuj Kumar <144224503+AnujLM@users.noreply.github.com> Date: Fri, 10 Nov 2023 17:30:54 +0530 Subject: [PATCH 09/19] fix link regex --- .../expandable_text/expandable_text.dart | 32 ++++++++++++++++--- lib/src/utils/helpers.dart | 30 ++++++++++++++--- pubspec.yaml | 1 + 3 files changed, 54 insertions(+), 9 deletions(-) diff --git a/lib/packages/expandable_text/expandable_text.dart b/lib/packages/expandable_text/expandable_text.dart index b62d3bf..fc876c8 100644 --- a/lib/packages/expandable_text/expandable_text.dart +++ b/lib/packages/expandable_text/expandable_text.dart @@ -8,6 +8,7 @@ import 'package:likeminds_chat_fl/likeminds_chat_fl.dart'; import 'package:likeminds_chat_ui_fl/src/utils/constants.dart'; import 'package:likeminds_chat_ui_fl/src/utils/helpers.dart'; import 'package:likeminds_chat_ui_fl/src/utils/theme.dart'; +import 'package:linkify/linkify.dart'; import 'package:url_launcher/url_launcher.dart'; import './text_parser.dart'; @@ -399,8 +400,9 @@ class ExpandableTextState extends State } List extractLinksAndTags(String text) { - // final regExpression = RegExp( - // r'<<([a-z\sA-Z]+)\|route://member/([a-zA-Z\0-9]+)>>|<<([a-z\sA-Z\s0-9]+)\|route://member/([0-9]+)>>'); + 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); List textSpans = []; int lastIndex = 0; for (Match match in regExp.allMatches(text)) { @@ -416,6 +418,17 @@ class ExpandableTextState extends State )); } 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 && + TaggingHelper.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, @@ -429,10 +442,19 @@ class ExpandableTextState extends State recognizer: TapGestureRecognizer() ..onTap = () async { if (!isTag) { - String checkLink = getFirstValidLinkFromString(link ?? ''); - if (Uri.parse(checkLink).isAbsolute) { + final checkLink = + TaggingHelper.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 560fe92..debe5cb 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_chat_fl/likeminds_chat_fl.dart'; import 'package:likeminds_chat_ui_fl/src/utils/theme.dart'; +import 'package:linkify/linkify.dart'; class TaggingHelper { static final RegExp tagRegExp = RegExp(r'@([^<>~]+)~'); @@ -180,10 +181,10 @@ class TaggingHelper { return textSpans; } -} -List extractLinkFromString(String text) { - RegExp exp = RegExp(TaggingHelper.linkRoute); + +static List extractLinkFromString(String text) { + RegExp exp = RegExp(linkRoute); Iterable matches = exp.allMatches(text); List links = []; for (var match in matches) { @@ -199,7 +200,7 @@ List extractLinkFromString(String text) { } } -String getFirstValidLinkFromString(String text) { +static String getFirstValidLinkFromString(String text) { try { List links = extractLinkFromString(text); List validLinks = []; @@ -225,6 +226,27 @@ String getFirstValidLinkFromString(String text) { } } +static LinkifyElement? extractLinkAndEmailFromString(String text) { + debugPrint("text: $text"); + final links = linkify( + text, + options: const LinkifyOptions( + looseUrl: true, + excludeLastPeriod: 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 4681321..97a0c68 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,6 +26,7 @@ dependencies: swipe_to_action: video_thumbnail: custom_pop_up_menu: + linkify: dev_dependencies: flutter_test: From b46fb4f602ff45e870b2d6b5036d9d663594a26d Mon Sep 17 00:00:00 2001 From: Anuj Kumar <144224503+AnujLM@users.noreply.github.com> Date: Mon, 13 Nov 2023 14:49:34 +0530 Subject: [PATCH 10/19] change tagRoute to notificationTagRoute --- lib/src/utils/helpers.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/utils/helpers.dart b/lib/src/utils/helpers.dart index 6a8ebec..df51df4 100644 --- a/lib/src/utils/helpers.dart +++ b/lib/src/utils/helpers.dart @@ -36,9 +36,9 @@ class TaggingHelper { /// Decodes the string with the user tags and returns the decoded string static Map decodeString(String string) { Map result = {}; - final Iterable matches = RegExp(tagRoute).allMatches(string); + final Iterable matches = RegExp(notificationTagRoute).allMatches(string); for (final match in matches) { - final String tag = match.group(1) ?? match.group(3)!; + final String tag = match.group(1) ?? match.group(4)!; final String? id = match.group(2); if (id != null) { string = string.replaceAll('<<$tag|route://member/$id>>', '@$tag'); From ee9b271eb3dbfad0b73b0af9e89ad39e35f72c6a Mon Sep 17 00:00:00 2001 From: Anuj Kumar <144224503+AnujLM@users.noreply.github.com> Date: Mon, 13 Nov 2023 17:54:27 +0530 Subject: [PATCH 11/19] resolved merge issues --- lib/packages/expandable_text/expandable_text.dart | 4 +--- lib/src/utils/constants.dart | 2 +- lib/src/utils/helpers.dart | 1 - 3 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 fc876c8..78e4059 100644 --- a/lib/packages/expandable_text/expandable_text.dart +++ b/lib/packages/expandable_text/expandable_text.dart @@ -400,9 +400,7 @@ class ExpandableTextState extends State } List extractLinksAndTags(String text) { - 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); + List textSpans = []; int lastIndex = 0; for (Match match in regExp.allMatches(text)) { diff --git a/lib/src/utils/constants.dart b/lib/src/utils/constants.dart index caee574..bd77a80 100644 --- a/lib/src/utils/constants.dart +++ b/lib/src/utils/constants.dart @@ -1,5 +1,5 @@ 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]+)>>|<<@participants\|route://participants>>'; + 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]+)>>|<<@participants\|route://participants>>'; // Attachment Type Constants const String kAttachmentTypeImage = "image"; diff --git a/lib/src/utils/helpers.dart b/lib/src/utils/helpers.dart index c98b5df..08767d8 100644 --- a/lib/src/utils/helpers.dart +++ b/lib/src/utils/helpers.dart @@ -245,7 +245,6 @@ static String getFirstValidLinkFromString(String text) { } static LinkifyElement? extractLinkAndEmailFromString(String text) { - debugPrint("text: $text"); final links = linkify( text, options: const LinkifyOptions( From ddc8cf4616687771fc1e315626216189265084ff Mon Sep 17 00:00:00 2001 From: Anuj Kumar <144224503+AnujLM@users.noreply.github.com> Date: Tue, 14 Nov 2023 11:20:22 +0530 Subject: [PATCH 12/19] implement menuBuilder --- lib/likeminds_chat_ui_fl.dart | 1 - lib/src/models/menu_decoration_model.dart | 26 -- lib/src/widgets/common/menu/menu.dart | 39 ++- lib/src/widgets/conversation/chat_bubble.dart | 328 ++++++++---------- 4 files changed, 179 insertions(+), 215 deletions(-) delete mode 100644 lib/src/models/menu_decoration_model.dart diff --git a/lib/likeminds_chat_ui_fl.dart b/lib/likeminds_chat_ui_fl.dart index 2bf9fb8..d7a2b9d 100644 --- a/lib/likeminds_chat_ui_fl.dart +++ b/lib/likeminds_chat_ui_fl.dart @@ -4,4 +4,3 @@ export 'src/widgets/widgets.dart'; export 'src/utils/utils.dart'; export 'src/models/media_model.dart'; export 'src/models/menu_item_model.dart'; -export 'src/models/menu_decoration_model.dart'; diff --git a/lib/src/models/menu_decoration_model.dart b/lib/src/models/menu_decoration_model.dart deleted file mode 100644 index 4b088af..0000000 --- a/lib/src/models/menu_decoration_model.dart +++ /dev/null @@ -1,26 +0,0 @@ - -import 'package:flutter/material.dart'; - -class LMMenuDecoration { - final double? elevation; - final Color? shadowColor; - final Color? surfaceTintColor; - final String? semanticLabel; - final ShapeBorder? shape; - final Color? color; - final BoxConstraints? constraints; - final Clip? clipBehavior; - final EdgeInsets? itemPadding; - - LMMenuDecoration({ - this.elevation, - this.shadowColor, - this.surfaceTintColor, - this.semanticLabel, - this.shape, - this.color, - this.constraints, - this.clipBehavior, - this.itemPadding, - }); -} diff --git a/lib/src/widgets/common/menu/menu.dart b/lib/src/widgets/common/menu/menu.dart index e27226c..c428b0d 100644 --- a/lib/src/widgets/common/menu/menu.dart +++ b/lib/src/widgets/common/menu/menu.dart @@ -6,11 +6,28 @@ class LMMenu extends StatelessWidget { super.key, required this.menuItems, required this.child, - this.menuDecoration, + this.elevation, + this.shadowColor, + this.surfaceTintColor, + this.semanticLabel, + this.shape, + this.color, + this.constraints, + this.clipBehavior, + this.itemPadding, }); + final List menuItems; final Widget child; - final LMMenuDecoration? menuDecoration; + final double? elevation; + final Color? shadowColor; + final Color? surfaceTintColor; + final String? semanticLabel; + final ShapeBorder? shape; + final Color? color; + final BoxConstraints? constraints; + final Clip? clipBehavior; + final EdgeInsets? itemPadding; @override Widget build(BuildContext context) { @@ -30,21 +47,21 @@ class LMMenu extends StatelessWidget { showMenu( context: context, position: position, - elevation: menuDecoration?.elevation, - shadowColor: menuDecoration?.shadowColor, - surfaceTintColor: menuDecoration?.surfaceTintColor, - semanticLabel: menuDecoration?.semanticLabel, - shape: menuDecoration?.shape, - color: menuDecoration?.color, - constraints: menuDecoration?.constraints, - clipBehavior: menuDecoration?.clipBehavior ?? Clip.none, + elevation: elevation, + shadowColor: shadowColor, + surfaceTintColor: surfaceTintColor, + semanticLabel: semanticLabel, + shape: shape, + color: color, + constraints: constraints, + clipBehavior: clipBehavior ?? Clip.none, items: [ for (LMMenuItemUI menuItem in menuItems) PopupMenuItem( onTap: () { menuItem.onTap(); }, - padding: menuDecoration?.itemPadding ?? EdgeInsets.zero, + padding: itemPadding ?? EdgeInsets.zero, child: ListTile( leading: menuItem.leading, title: menuItem.title, diff --git a/lib/src/widgets/conversation/chat_bubble.dart b/lib/src/widgets/conversation/chat_bubble.dart index 8b640d2..8535844 100644 --- a/lib/src/widgets/conversation/chat_bubble.dart +++ b/lib/src/widgets/conversation/chat_bubble.dart @@ -19,8 +19,7 @@ class LMChatBubble extends StatefulWidget { this.reactionButton, this.outsideTitle, this.outsideFooter, - this.menuItems, - this.menuDecoration, + this.menuBuilder, this.onReply, this.onEdit, this.onLongPress, @@ -54,8 +53,7 @@ class LMChatBubble extends StatefulWidget { final LMTextView? outsideTitle; final Widget? outsideFooter; final Widget? mediaWidget; - final List? menuItems; - final LMMenuDecoration? menuDecoration; + final LMMenu Function(Widget child)? menuBuilder; final Function(Conversation replyingTo)? onReply; final Function(Conversation editConversation)? onEdit; final Function(Conversation conversation)? onLongPress; @@ -181,179 +179,9 @@ class _LMChatBubbleState extends State { const SizedBox(width: 4), AbsorbPointer( absorbing: isDeleted, - child: LMMenu( - menuItems: widget.menuItems ?? [], - menuDecoration: widget.menuDecoration, - child: Container( - constraints: BoxConstraints( - minHeight: 4.h, - minWidth: 10.w, - maxWidth: 60.w, - ), - padding: EdgeInsets.all( - widget.mediaWidget != null ? 8.0 : 12.0, - ), - decoration: BoxDecoration( - color: widget.backgroundColor ?? Colors.white, - borderRadius: widget.borderRadius ?? - BorderRadius.circular( - widget.borderRadiusNum ?? 6, - ), - ), - child: Column( - crossAxisAlignment: isSent - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, - children: [ - widget.replyItem == null - ? replyingTo != null - ? isDeleted - ? const SizedBox.shrink() - : LMReplyItem( - replyToConversation: replyingTo!, - backgroundColor: Colors.white, - highlightColor: Theme.of(context) - .primaryColor, - title: LMTextView( - text: replyingTo!.member!.name, - textStyle: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - ), - ), - subtitle: LMTextView( - text: replyingTo!.answer, - textStyle: const TextStyle( - fontSize: 12, - color: Colors.black, - fontWeight: FontWeight.w400, - ), - ), - ) - : const SizedBox.shrink() - : isDeleted - ? const SizedBox.shrink() - : widget.replyItem!, - replyingTo != null - ? const SizedBox(height: 8) - : const SizedBox.shrink(), - isSent - ? const SizedBox() - : widget.title ?? const SizedBox.shrink(), - isDeleted - ? const SizedBox.shrink() - : ((widget.mediaWidget != null && - widget.content != null) || - widget.conversation.hasFiles!) - ? Padding( - padding: const EdgeInsets.symmetric( - vertical: 0, horizontal: 0), - child: widget.mediaWidget, - ) - : const SizedBox(), - isDeleted - ? widget.deletedText != null - ? widget.deletedText! - : conversation!.deletedByUserId == - conversation!.userId - ? LMTextView( - text: currentUser!.id == - conversation! - .deletedByUserId - ? "You deleted this message" - : "This message was deleted", - textStyle: widget - .content!.textStyle! - .copyWith( - fontStyle: FontStyle.italic, - ), - ) - : LMTextView( - text: - "This message was deleted by a community managers", - textStyle: widget - .content!.textStyle! - .copyWith( - fontStyle: FontStyle.italic, - ), - ) - : replyingTo != null - ? Align( - alignment: Alignment.topLeft, - child: Padding( - padding: EdgeInsets.only( - top: widget.mediaWidget != null - ? 4.0 - : 0, - ), - child: widget.content ?? - LMChatContent( - conversation: - widget.conversation, - linkStyle: TextStyle( - color: Theme.of(context) - .primaryColor, - fontSize: 14, - fontWeight: FontWeight.w400, - ), - textStyle: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - ), - ), - ), - ) - : Padding( - padding: EdgeInsets.only( - top: widget.mediaWidget != null - ? 4.0 - : 0), - child: widget.content ?? - LMChatContent( - conversation: - widget.conversation, - linkStyle: TextStyle( - color: Theme.of(context) - .primaryColor, - fontSize: 14, - fontWeight: FontWeight.w400, - ), - textStyle: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - ), - visibleLines: 4, - animation: true, - ), - ), - if (widget.footer != null && - widget.footer!.isNotEmpty && - !isDeleted) - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: widget.footer!, - ), - ((widget.conversation.hasFiles == null || - !widget.conversation.hasFiles!) || - (widget.conversation - .attachmentsUploaded != - null && - widget.conversation - .attachmentsUploaded!) || - isDeleted) - ? const SizedBox() - : const LMIcon( - type: LMIconType.icon, - icon: Icons.timer_outlined, - size: 12, - boxSize: 18, - boxPadding: 6, - ), - ], - ), - ), - ), + child: widget.menuBuilder != null + ? widget.menuBuilder!(chatBubbleContent()) + : chatBubbleContent(), ), const SizedBox(width: 4), isSent @@ -376,4 +204,150 @@ class _LMChatBubbleState extends State { ); // ); } + + Container chatBubbleContent() { + return Container( + constraints: BoxConstraints( + minHeight: 4.h, + minWidth: 10.w, + maxWidth: 60.w, + ), + padding: EdgeInsets.all( + widget.mediaWidget != null ? 8.0 : 12.0, + ), + decoration: BoxDecoration( + color: widget.backgroundColor ?? Colors.white, + borderRadius: widget.borderRadius ?? + BorderRadius.circular( + widget.borderRadiusNum ?? 6, + ), + ), + child: Column( + crossAxisAlignment: + isSent ? CrossAxisAlignment.end : CrossAxisAlignment.start, + children: [ + widget.replyItem == null + ? replyingTo != null + ? isDeleted + ? const SizedBox.shrink() + : LMReplyItem( + replyToConversation: replyingTo!, + backgroundColor: Colors.white, + highlightColor: Theme.of(context).primaryColor, + title: LMTextView( + text: replyingTo!.member!.name, + textStyle: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + subtitle: LMTextView( + text: replyingTo!.answer, + textStyle: const TextStyle( + fontSize: 12, + color: Colors.black, + fontWeight: FontWeight.w400, + ), + ), + ) + : const SizedBox.shrink() + : isDeleted + ? const SizedBox.shrink() + : widget.replyItem!, + replyingTo != null + ? const SizedBox(height: 8) + : const SizedBox.shrink(), + isSent ? const SizedBox() : widget.title ?? const SizedBox.shrink(), + isDeleted + ? const SizedBox.shrink() + : ((widget.mediaWidget != null && widget.content != null) || + widget.conversation.hasFiles!) + ? Padding( + padding: const EdgeInsets.symmetric( + vertical: 0, horizontal: 0), + child: widget.mediaWidget, + ) + : const SizedBox(), + isDeleted + ? widget.deletedText != null + ? widget.deletedText! + : conversation!.deletedByUserId == conversation!.userId + ? LMTextView( + text: currentUser!.id == conversation!.deletedByUserId + ? "You deleted this message" + : "This message was deleted", + textStyle: widget.content!.textStyle!.copyWith( + fontStyle: FontStyle.italic, + ), + ) + : LMTextView( + text: + "This message was deleted by a community managers", + textStyle: widget.content!.textStyle!.copyWith( + fontStyle: FontStyle.italic, + ), + ) + : replyingTo != null + ? Align( + alignment: Alignment.topLeft, + child: Padding( + padding: EdgeInsets.only( + top: widget.mediaWidget != null ? 4.0 : 0, + ), + child: widget.content ?? + LMChatContent( + conversation: widget.conversation, + linkStyle: TextStyle( + color: Theme.of(context).primaryColor, + fontSize: 14, + fontWeight: FontWeight.w400, + ), + textStyle: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + ), + ) + : Padding( + padding: EdgeInsets.only( + top: widget.mediaWidget != null ? 4.0 : 0), + child: widget.content ?? + LMChatContent( + conversation: widget.conversation, + linkStyle: TextStyle( + color: Theme.of(context).primaryColor, + fontSize: 14, + fontWeight: FontWeight.w400, + ), + textStyle: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + ), + visibleLines: 4, + animation: true, + ), + ), + if (widget.footer != null && widget.footer!.isNotEmpty && !isDeleted) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: widget.footer!, + ), + ((widget.conversation.hasFiles == null || + !widget.conversation.hasFiles!) || + (widget.conversation.attachmentsUploaded != null && + widget.conversation.attachmentsUploaded!) || + isDeleted) + ? const SizedBox() + : const LMIcon( + type: LMIconType.icon, + icon: Icons.timer_outlined, + size: 12, + boxSize: 18, + boxPadding: 6, + ), + ], + ), + ); + } } From 961e12e32e6ebb45a3c9766f07225bbdd076fb71 Mon Sep 17 00:00:00 2001 From: Anuj Kumar <144224503+AnujLM@users.noreply.github.com> Date: Tue, 14 Nov 2023 11:25:20 +0530 Subject: [PATCH 13/19] changes in example app --- example/lib/views/chatroom_page.dart | 367 ++++++++++++++------------- 1 file changed, 184 insertions(+), 183 deletions(-) diff --git a/example/lib/views/chatroom_page.dart b/example/lib/views/chatroom_page.dart index 10fcc89..0cdf235 100644 --- a/example/lib/views/chatroom_page.dart +++ b/example/lib/views/chatroom_page.dart @@ -510,132 +510,132 @@ class _ChatRoomPageState extends State { item.replyId.toString()] : null : null; - + return item.userId == user!.id ? LMChatBubble( currentUser: user!, key: Key(item.id.toString()), isSent: item.userId == user!.id, backgroundColor: primary.shade500, - - menuItems: [ - LMMenuItemUI( - onTap: () { - int userId = item.userId ?? - item.memberId!; - if (userId == user!.id) { - item.member = user!; - } - if (item.deletedByUserId != - null) { - return; - } - _convActionBloc.add( - ReplyConversation( - chatroomId: chatroom!.id, - conversationId: item.id, - replyConversation: item, - ), - ); - - }, - leading: const LMIcon( - type: LMIconType.svg, - assetPath: ssReplyIcon, - size: 24, - ), - title: const LMTextView( - text: "Reply", - textStyle: TextStyle( - fontSize: 14, - ), - ), - ), - LMMenuItemUI( - leading: const LMIcon( - type: LMIconType.svg, - assetPath: ssCopyIcon, - size: 24, - ), - title: const LMTextView( - text: "Copy", - textStyle: TextStyle( - fontSize: 14, - ), - ), - onTap: () { - Clipboard.setData( - ClipboardData( - text: TaggingHelper - .convertRouteToTag( - item.answer) ?? - '', - ), - ).then((value) { - toast("Copied to clipboard"); - - }); - }, - ), - if (checkEditPermissions(item)) + menuBuilder: (child) => LMMenu( + menuItems: [ LMMenuItemUI( - onTap: () async { + onTap: () { + int userId = item.userId ?? + item.memberId!; + if (userId == user!.id) { + item.member = user!; + } + if (item.deletedByUserId != + null) { + return; + } _convActionBloc.add( - EditingConversation( + ReplyConversation( chatroomId: chatroom!.id, conversationId: item.id, - editConversation: item, + replyConversation: item, ), ); - }, leading: const LMIcon( type: LMIconType.svg, - assetPath: ssEditIcon, + assetPath: ssReplyIcon, size: 24, ), title: const LMTextView( - text: "Edit", + text: "Reply", textStyle: TextStyle( fontSize: 14, ), ), ), - if (checkDeletePermissions(item)) LMMenuItemUI( - onTap: () async { - final response = await locator< - LikeMindsService>() - .deleteConversation( - (DeleteConversationRequestBuilder() - ..conversationIds( - [item.id]) - ..reason( - "Delete")) - .build()); - if (response.success) { - - rebuildConversationList - .value = - !rebuildConversationList - .value; - toast("Message deleted"); - } - }, leading: const LMIcon( type: LMIconType.svg, - assetPath: ssDeleteIcon, - color: Colors.red, + assetPath: ssCopyIcon, size: 24, ), title: const LMTextView( - text: "Delete", + text: "Copy", textStyle: TextStyle( fontSize: 14, ), ), - ) - ], + onTap: () { + Clipboard.setData( + ClipboardData( + text: TaggingHelper + .convertRouteToTag( + item.answer) ?? + '', + ), + ).then((value) { + toast( + "Copied to clipboard"); + }); + }, + ), + if (checkEditPermissions(item)) + LMMenuItemUI( + onTap: () async { + _convActionBloc.add( + EditingConversation( + chatroomId: + chatroom!.id, + conversationId: item.id, + editConversation: item, + ), + ); + }, + leading: const LMIcon( + type: LMIconType.svg, + assetPath: ssEditIcon, + size: 24, + ), + title: const LMTextView( + text: "Edit", + textStyle: TextStyle( + fontSize: 14, + ), + ), + ), + if (checkDeletePermissions(item)) + LMMenuItemUI( + onTap: () async { + final response = await locator< + LikeMindsService>() + .deleteConversation( + (DeleteConversationRequestBuilder() + ..conversationIds( + [item.id]) + ..reason( + "Delete")) + .build()); + if (response.success) { + rebuildConversationList + .value = + !rebuildConversationList + .value; + toast("Message deleted"); + } + }, + leading: const LMIcon( + type: LMIconType.svg, + assetPath: ssDeleteIcon, + color: Colors.red, + size: 24, + ), + title: const LMTextView( + text: "Delete", + textStyle: TextStyle( + fontSize: 14, + ), + ), + ) + ], + child: child, + ), onReply: (replyingTo) { int userId = item.userId ?? item.memberId!; @@ -736,124 +736,125 @@ class _ChatRoomPageState extends State { key: Key(item.id.toString()), isSent: item.userId == user!.id, backgroundColor: secondary.shade100, + menuBuilder: (child) => LMMenu( menuItems: [ - LMMenuItemUI( - onTap: () { - int userId = item.userId ?? - item.memberId!; - if (userId == user!.id) { - item.member = user!; - } - if (item.deletedByUserId != - null) { - return; - } - _convActionBloc.add( - ReplyConversation( - chatroomId: chatroom!.id, - conversationId: item.id, - replyConversation: item, - ), - ); - - }, - leading: const LMIcon( - type: LMIconType.svg, - assetPath: ssReplyIcon, - size: 24, - ), - title: const LMTextView( - text: "Reply", - textStyle: TextStyle( - fontSize: 14, - ), - ), - ), - LMMenuItemUI( - leading: const LMIcon( - type: LMIconType.svg, - assetPath: ssCopyIcon, - size: 24, - ), - title: const LMTextView( - text: "Copy", - textStyle: TextStyle( - fontSize: 14, - ), - ), - onTap: () { - Clipboard.setData( - ClipboardData( - text: TaggingHelper - .convertRouteToTag( - item.answer) ?? - '', - ), - ).then((value) { - toast("Copied to clipboard"); - - }); - }, - ), - if (checkEditPermissions(item)) LMMenuItemUI( - onTap: () async { + onTap: () { + int userId = item.userId ?? + item.memberId!; + if (userId == user!.id) { + item.member = user!; + } + if (item.deletedByUserId != + null) { + return; + } _convActionBloc.add( - EditingConversation( + ReplyConversation( chatroomId: chatroom!.id, conversationId: item.id, - editConversation: item, + replyConversation: item, ), ); - }, leading: const LMIcon( type: LMIconType.svg, - assetPath: ssEditIcon, + assetPath: ssReplyIcon, size: 24, ), title: const LMTextView( - text: "Edit", + text: "Reply", textStyle: TextStyle( fontSize: 14, ), ), ), - if (checkDeletePermissions(item)) LMMenuItemUI( - onTap: () async { - final response = await locator< - LikeMindsService>() - .deleteConversation( - (DeleteConversationRequestBuilder() - ..conversationIds( - [item.id]) - ..reason( - "Delete")) - .build()); - if (response.success) { - - rebuildConversationList - .value = - !rebuildConversationList - .value; - toast("Message deleted"); - } - }, leading: const LMIcon( type: LMIconType.svg, - assetPath: ssDeleteIcon, - color: Colors.red, + assetPath: ssCopyIcon, size: 24, ), title: const LMTextView( - text: "Delete", + text: "Copy", textStyle: TextStyle( fontSize: 14, ), ), - ) - ], + onTap: () { + Clipboard.setData( + ClipboardData( + text: TaggingHelper + .convertRouteToTag( + item.answer) ?? + '', + ), + ).then((value) { + toast( + "Copied to clipboard"); + }); + }, + ), + if (checkEditPermissions(item)) + LMMenuItemUI( + onTap: () async { + _convActionBloc.add( + EditingConversation( + chatroomId: + chatroom!.id, + conversationId: item.id, + editConversation: item, + ), + ); + }, + leading: const LMIcon( + type: LMIconType.svg, + assetPath: ssEditIcon, + size: 24, + ), + title: const LMTextView( + text: "Edit", + textStyle: TextStyle( + fontSize: 14, + ), + ), + ), + if (checkDeletePermissions(item)) + LMMenuItemUI( + onTap: () async { + final response = await locator< + LikeMindsService>() + .deleteConversation( + (DeleteConversationRequestBuilder() + ..conversationIds( + [item.id]) + ..reason( + "Delete")) + .build()); + if (response.success) { + rebuildConversationList + .value = + !rebuildConversationList + .value; + toast("Message deleted"); + } + }, + leading: const LMIcon( + type: LMIconType.svg, + assetPath: ssDeleteIcon, + color: Colors.red, + size: 24, + ), + title: const LMTextView( + text: "Delete", + textStyle: TextStyle( + fontSize: 14, + ), + ), + ) + ], + child: child, + ), onReply: (replyingTo) { int userId = item.userId ?? item.memberId!; From 18fd49565b4523d762d6b2022dd84e3ec36c8014 Mon Sep 17 00:00:00 2001 From: Anuj Kumar <144224503+AnujLM@users.noreply.github.com> Date: Tue, 14 Nov 2023 14:35:03 +0530 Subject: [PATCH 14/19] add all models export to models.dart --- lib/likeminds_chat_ui_fl.dart | 4 +--- lib/src/models/models.dart | 3 +++ 2 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 lib/src/models/models.dart diff --git a/lib/likeminds_chat_ui_fl.dart b/lib/likeminds_chat_ui_fl.dart index f60281f..d774f65 100644 --- a/lib/likeminds_chat_ui_fl.dart +++ b/lib/likeminds_chat_ui_fl.dart @@ -2,6 +2,4 @@ library likeminds_chat_ui_fl; export 'src/widgets/widgets.dart'; export 'src/utils/utils.dart'; -export 'src/models/media_model.dart'; -export 'src/models/menu_item_model.dart'; -export 'src/models/tag_view_data_model.dart'; \ No newline at end of file +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..2d9affe --- /dev/null +++ b/lib/src/models/models.dart @@ -0,0 +1,3 @@ +export 'media_model.dart'; +export 'menu_item_model.dart'; +export 'tag_view_data_model.dart'; \ No newline at end of file From 95f9ad91b0a346b4feef4d207f13fb2514eae05d Mon Sep 17 00:00:00 2001 From: Anuj Kumar <144224503+AnujLM@users.noreply.github.com> Date: Tue, 14 Nov 2023 17:42:08 +0530 Subject: [PATCH 15/19] fix link regex --- lib/src/utils/helpers.dart | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/src/utils/helpers.dart b/lib/src/utils/helpers.dart index 08767d8..c76d117 100644 --- a/lib/src/utils/helpers.dart +++ b/lib/src/utils/helpers.dart @@ -251,12 +251,13 @@ static LinkifyElement? extractLinkAndEmailFromString(String text) { looseUrl: true, excludeLastPeriod: 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 e88a4c5df63a139f93bbb4523f81394cf3b0e9b9 Mon Sep 17 00:00:00 2001 From: Anuj Kumar <144224503+AnujLM@users.noreply.github.com> Date: Thu, 16 Nov 2023 18:11:57 +0530 Subject: [PATCH 16/19] fixed link regex 2 char tld issue --- example/pubspec.lock | 6 +- .../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 | 7 +- pubspec.yaml | 1 - 8 files changed, 321 insertions(+), 7 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/example/pubspec.lock b/example/pubspec.lock index 625dd8d..570fba4 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -707,17 +707,17 @@ packages: dependency: transitive description: name: likeminds_chat_fl - sha256: "77156d2826474fe06feb07af18b1609e3a5ff7a4fc2fed8836befac3b9315c0a" + sha256: "5a63ab50488a907523ac9534e5e9e64522955bb0373f6ca3348f2f10c9b5d467" url: "https://pub.dev" source: hosted - version: "1.5.1" + version: "1.5.2" likeminds_chat_ui_fl: dependency: "direct main" description: path: ".." relative: true source: path - version: "1.1.0" + version: "1.1.1" lints: dependency: transitive description: diff --git a/lib/packages/expandable_text/expandable_text.dart b/lib/packages/expandable_text/expandable_text.dart index 78e4059..1959dee 100644 --- a/lib/packages/expandable_text/expandable_text.dart +++ b/lib/packages/expandable_text/expandable_text.dart @@ -5,10 +5,10 @@ import 'dart:math'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:likeminds_chat_fl/likeminds_chat_fl.dart'; +import 'package:likeminds_chat_ui_fl/packages/linkify/linkify.dart'; import 'package:likeminds_chat_ui_fl/src/utils/constants.dart'; import 'package:likeminds_chat_ui_fl/src/utils/helpers.dart'; import 'package:likeminds_chat_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..c3d8b1f --- /dev/null +++ b/lib/packages/linkify/src/email.dart @@ -0,0 +1,71 @@ +import 'package:likeminds_chat_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..5aa1e8f --- /dev/null +++ b/lib/packages/linkify/src/url.dart @@ -0,0 +1,115 @@ +import 'package:likeminds_chat_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 bd77a80..cea37ca 100644 --- a/lib/src/utils/constants.dart +++ b/lib/src/utils/constants.dart @@ -1,5 +1,5 @@ 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]+)>>|<<@participants\|route://participants>>'; + 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]+)>>|<<@participants\|route://participants>>'; // Attachment Type Constants const String kAttachmentTypeImage = "image"; diff --git a/lib/src/utils/helpers.dart b/lib/src/utils/helpers.dart index c76d117..0a6be24 100644 --- a/lib/src/utils/helpers.dart +++ b/lib/src/utils/helpers.dart @@ -3,8 +3,9 @@ import 'dart:math'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:likeminds_chat_fl/likeminds_chat_fl.dart'; +import 'package:likeminds_chat_ui_fl/packages/linkify/linkify.dart'; import 'package:likeminds_chat_ui_fl/src/utils/theme.dart'; -import 'package:linkify/linkify.dart'; + class TaggingHelper { static final RegExp tagRegExp = RegExp(r'@([^<>~]+)~'); @@ -245,6 +246,7 @@ static String getFirstValidLinkFromString(String text) { } static LinkifyElement? extractLinkAndEmailFromString(String text) { + debugPrint("=======> $text<======="); final links = linkify( text, options: const LinkifyOptions( @@ -257,6 +259,9 @@ static LinkifyElement? extractLinkAndEmailFromString(String text) { ] ); if (links.isNotEmpty) { + for(var link in links) { + debugPrint("=======> $link<======="); + } if(links.first is EmailElement || links.first is UrlElement) { return links.first; } diff --git a/pubspec.yaml b/pubspec.yaml index cda4eb8..38d7ece 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,7 +25,6 @@ dependencies: url_launcher: swipe_to_action: video_thumbnail: - linkify: dev_dependencies: flutter_test: From 46c009517753cf776f9a799d4c19b9c8e5b8faca Mon Sep 17 00:00:00 2001 From: Anuj Kumar <144224503+AnujLM@users.noreply.github.com> Date: Thu, 16 Nov 2023 18:48:10 +0530 Subject: [PATCH 17/19] fix link regex for https:// or 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 5aa1e8f..1f629a5 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 cea37ca..2a70ac5 100644 --- a/lib/src/utils/constants.dart +++ b/lib/src/utils/constants.dart @@ -1,5 +1,5 @@ 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]+)>>|<<@participants\|route://participants>>'; + 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]+)>>|<<@participants\|route://participants>>'; // Attachment Type Constants const String kAttachmentTypeImage = "image"; From 6680007a1b5f043708f740928f85c749c2a1a94b Mon Sep 17 00:00:00 2001 From: Anuj Kumar <144224503+AnujLM@users.noreply.github.com> Date: Thu, 16 Nov 2023 22:38:07 +0530 Subject: [PATCH 18/19] fix link regex --- lib/packages/linkify/src/url.dart | 2 +- lib/src/utils/helpers.dart | 125 +++++++++++++++--------------- 2 files changed, 65 insertions(+), 62 deletions(-) diff --git a/lib/packages/linkify/src/url.dart b/lib/packages/linkify/src/url.dart index 1f629a5..7110c79 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 0a6be24..f0e6402 100644 --- a/lib/src/utils/helpers.dart +++ b/lib/src/utils/helpers.dart @@ -6,7 +6,6 @@ import 'package:likeminds_chat_fl/likeminds_chat_fl.dart'; import 'package:likeminds_chat_ui_fl/packages/linkify/linkify.dart'; import 'package:likeminds_chat_ui_fl/src/utils/theme.dart'; - class TaggingHelper { static final RegExp tagRegExp = RegExp(r'@([^<>~]+)~'); static const String notificationTagRoute = @@ -38,7 +37,8 @@ class TaggingHelper { /// Decodes the string with the user tags and returns the decoded string static Map decodeString(String string) { Map result = {}; - final Iterable matches = RegExp(notificationTagRoute).allMatches(string); + final Iterable matches = + RegExp(notificationTagRoute).allMatches(string); for (final match in matches) { final String tag = match.group(1) ?? match.group(4)!; final String? id = match.group(2); @@ -95,11 +95,12 @@ class TaggingHelper { final String tag = match.group(1)!; final String? mid = match.group(2); final String? id = match.group(3); - if(id!=null) { + if (id != null) { text = text.replaceAll( - '<<$tag|route://$mid/$id>>', withTilde ? '@$tag~' : '@$tag'); + '<<$tag|route://$mid/$id>>', withTilde ? '@$tag~' : '@$tag'); } else { - text = text.replaceAll('<<@participants|route://participants>', '@$tag'); + text = + text.replaceAll('<<@participants|route://participants>', '@$tag'); } } return text; @@ -114,11 +115,11 @@ class TaggingHelper { final String tag = match.group(1)!; final String? mid = match.group(2); final String? id = match.group(3); - if(id!=null) { + if (id != null) { text = text.replaceAll('<<$tag|route://$mid/$id>>', '@$tag~'); } else { - - text = text.replaceAll('<<@participants|route://participants>', '@$tag'); + text = + text.replaceAll('<<@participants|route://participants>', '@$tag'); } } return text; @@ -201,73 +202,75 @@ class TaggingHelper { return textSpans; } - -static List extractLinkFromString(String text) { - RegExp exp = RegExp(linkRoute); - Iterable matches = exp.allMatches(text); - List links = []; - for (var match in matches) { - String link = text.substring(match.start, match.end); - if (link.isNotEmpty) { - links.add(link); + static List extractLinkFromString(String text) { + RegExp exp = RegExp(linkRoute); + Iterable matches = exp.allMatches(text); + List links = []; + for (var match in matches) { + String link = text.substring(match.start, match.end); + if (link.isNotEmpty) { + links.add(link); + } + } + if (links.isNotEmpty) { + return links; + } else { + return []; } } - if (links.isNotEmpty) { - return links; - } else { - return []; - } -} -static String getFirstValidLinkFromString(String text) { - try { - List links = extractLinkFromString(text); - List validLinks = []; - String validLink = ''; - if (links.isNotEmpty) { - for (String link in links) { - if (Uri.parse(link).isAbsolute) { - validLinks.add(link); - } else { - link = "https://$link"; + static String getFirstValidLinkFromString(String text) { + try { + List links = extractLinkFromString(text); + List validLinks = []; + String validLink = ''; + if (links.isNotEmpty) { + for (String link in links) { if (Uri.parse(link).isAbsolute) { validLinks.add(link); + } else { + link = "https://$link"; + if (Uri.parse(link).isAbsolute) { + validLinks.add(link); + } } } } + if (validLinks.isNotEmpty) { + validLink = validLinks.first; + } + return validLink; + } catch (e) { + return ''; } - if (validLinks.isNotEmpty) { - validLink = validLinks.first; - } - return validLink; - } catch (e) { - return ''; } -} -static LinkifyElement? extractLinkAndEmailFromString(String text) { - debugPrint("=======> $text<======="); - final links = linkify( - text, - options: const LinkifyOptions( - looseUrl: true, - excludeLastPeriod: true, - ), - linkifiers: [ - EmailLinkifier(), - UrlLinkifier(), - ] - ); - if (links.isNotEmpty) { - for(var link in links) { - debugPrint("=======> $link<======="); + static LinkifyElement? extractLinkAndEmailFromString(String 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; + } } - if(links.first is EmailElement || links.first is UrlElement) { - return links.first; + final links = linkify(text, + options: const LinkifyOptions( + looseUrl: true, + excludeLastPeriod: true, + ), + linkifiers: [ + const EmailLinkifier(), + const UrlLinkifier(), + ]); + if (links.isNotEmpty) { + if (links.first is EmailElement || links.first is UrlElement) { + return links.first; + } } + return null; } - return null; -} } class PostHelper { From a9c4178057d9e6153e3e3d9ed8c541d5ab0042fa Mon Sep 17 00:00:00 2001 From: Anuj Kumar <144224503+AnujLM@users.noreply.github.com> Date: Thu, 16 Nov 2023 22:56:36 +0530 Subject: [PATCH 19/19] version update --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 38d7ece..88dc009 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: likeminds_chat_ui_fl description: A new Flutter package project. -version: 1.1.1 +version: 1.1.2 publish_to: none environment: