diff --git a/CHANGELOG.md b/CHANGELOG.md index ee57c0f2..edfd4ddb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,47 +1,49 @@ ## [2.0.0] (Unreleased) +* **Breaking**: [173](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/pull/173) Added + callback to sort message in chat. * **Breaking**: [177](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/pull/177) Fixed json serializable of models and added copyWith method (Message, Reaction and Reply Message). -* **Fix**: [182](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/issues/182) Fix - send message not working when user start texting after newLine. +* **Breaking**: [181](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/pull/181) Removed + deprecated field `showTypingIndicator` from ChatView. +* **Feat**: [179](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/pull/179) Added reply + suggestions functionality +* **Feat**: [157](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/pull/157) Added onTap + of reacted user from reacted user list. * **Feat**: [156](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/pull/156) Added default avatar, error builder for asset, network and base64 profile image and cached_network_image for network images. -* **Breaking**: [173](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/pull/173) Added - callback to sort message in chat. -* **Fix**: [181](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/pull/181) Removed - deprecated field `showTypingIndicator` from ChatView. +* **Feat**: [121](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/pull/121) Added support + for configuring the audio recording quality. +* **Feat**: [93](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/issues/93) Added support + that provide date pattern to change chat separation. * **Fix**: [139](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/issues/139) Added support to customize view for the reply of any message. -* **Fix**: [174](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/issues/174) Fix - wrong username shown while replying to any messages. -* **Fix**: [134](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/issues/134) - Added a reply message view for custom message type. -* **Feat**: [157](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/pull/157) - Added onTap of reacted user from reacted user list. +* **Fix**: [174](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/issues/174) Fix wrong + username shown while replying to any messages. +* **Fix**: [134](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/issues/134) Added a + reply message view for custom message type. * **Fix**: [137](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/issues/137) Added support for cancel voice recording and field to provide cancel record icon. -* **Feat**: [93](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/issues/93) Added support - that provide date pattern to change chat separation. -* **Fix**: [142](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/issues/142) Added - field to provide base64 string data for profile picture. -* **Fix**: [165](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/issues/165) Fix issue - of user reaction callback provides incorrect message object when user react on any message - with double or from reaction sheet. -* **Fix**: [164](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/issues/164) - Add flag to enable/disable chat text field. -* **Feat**: [121](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/pull/121)Added support - for configuring the audio recording quality. -* **Fix**: [131](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/issues/131) - Fix unsupported operation while running on the web. +* **Fix**: [142](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/issues/142) Added field + to provide base64 string data for profile picture. +* **Fix**: [161](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/pull/161) Added field + to set top padding of chat text field. +* **Fix**: [165](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/issues/165) Fix issue of + user reaction callback provides incorrect message object when user react on any message with + double or from reaction sheet. +* **Fix**: [164](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/issues/164) Add flag to + enable/disable chat text field. +* **Fix**: [131](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/issues/131) Fix + unsupported operation while running on the web. * **Fix**: [160](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/pull/160) Added configuration for emoji picker sheet. -* **Fix**: [130](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/issues/130) Added - report button for receiver message and update onMoreTap, onReportTap callback. -* **Fix**: [126](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/issues/126) Added - flag to hide user name in chat. -* **Feat**: [161](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/pull/161) Added - field to set top padding of chat text field. +* **Fix**: [130](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/issues/130) Added report + button for receiver message and update onMoreTap, onReportTap callback. +* **Fix**: [126](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/issues/126) Added flag + to hide user name in chat. +* **Fix**: [182](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/issues/182) Fix + send message not working when user start texting after newLine. ## [1.3.1] diff --git a/README.md b/README.md index bf0fcd5e..6e83d310 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ![Banner](https://raw.githubusercontent.com/SimformSolutionsPvtLtd/flutter_chat_ui/main/preview/banner.png) # ChatView - [![chatview](https://img.shields.io/pub/v/chatview?label=chatview)](https://pub.dev/packages/chatview) +[![chatview](https://img.shields.io/pub/v/chatview?label=chatview)](https://pub.dev/packages/chatview) A Flutter package that allows you to integrate Chat View with highly customization options such as one on one chat, group chat, message reactions, reply messages, link preview and configurations for overall view. @@ -34,7 +34,7 @@ ChatView( ), ``` -## Installing +## Installing 1. Add dependency to `pubspec.yaml` @@ -434,7 +434,7 @@ ChatView( ``` 17. Callback when a user starts/stops typing in `TextFieldConfiguration` - + ```dart ChatView( ... @@ -459,7 +459,7 @@ ChatView( ``` 18. Passing customReceipts builder or handling stuffs related receipts see `ReceiptsWidgetConfig` in outgoingChatBubbleConfig. - + ```dart ChatView( ... @@ -492,18 +492,7 @@ ChatView( ) ``` -19. Added field `chatTextFieldTopPadding` to set top padding of chat text field. - -```dart -ChatView( - ... - chatTextFieldTopPadding: 10, - ... - -) -``` - -20. Flag `enableOtherUserName` to hide user name in chat. +19. Flag `enableOtherUserName` to hide user name in chat. ```dart ChatView( @@ -516,7 +505,7 @@ ChatView( ) ``` -21. Added report button for receiver message and update `onMoreTap` and `onReportTap` callbacks. +20. Added report button for receiver message and update `onMoreTap` and `onReportTap` callbacks. ```dart ChatView( @@ -533,7 +522,7 @@ ChatView( ) ``` -22. Added `emojiPickerSheetConfig` for configuration of emoji picker sheet. +21. Added `emojiPickerSheetConfig` for configuration of emoji picker sheet. ```dart ChatView( @@ -554,7 +543,7 @@ ChatView( ) ``` -23. Configure the styling & audio recording quality using `VoiceRecordingConfiguration` in sendMessageConfig. +22. Configure the styling & audio recording quality using `VoiceRecordingConfiguration` in sendMessageConfig. ```dart ChatView( @@ -579,7 +568,7 @@ ChatView( ) ``` -24. Added `enabled` to enable/disable chat text field. +23. Added `enabled` to enable/disable chat text field. ```dart ChatView( @@ -595,7 +584,7 @@ ChatView( ) ``` -25. Added flag `isProfilePhotoInBase64` that defines whether provided image is url or base64 data. +24. Added flag `isProfilePhotoInBase64` that defines whether provided image is url or base64 data. ```dart final chatController = ChatController( @@ -621,7 +610,7 @@ ChatView( ) ``` -26. Added `chatSeparatorDatePattern` in `DefaultGroupSeparatorConfiguration` to separate chats with provided pattern. +25. Added `chatSeparatorDatePattern` in `DefaultGroupSeparatorConfiguration` to separate chats with provided pattern. ```dart ChatView( @@ -637,7 +626,7 @@ ChatView( ) ``` -27. Field `cancelRecordConfiguration` to provide an configuration to cancel voice record message. +26. Field `cancelRecordConfiguration` to provide an configuration to cancel voice record message. ```dart ChatView( @@ -660,7 +649,7 @@ ChatView( ) ``` -28. Added callback of onTap on list of reacted users in reaction sheet `reactedUserCallback`. +27. Added callback of onTap on list of reacted users in reaction sheet `reactedUserCallback`. ```dart ChatView( @@ -680,7 +669,7 @@ ChatView( ), ``` -29. Added a `customMessageReplyViewBuilder` to customize reply message view for custom type message. +28. Added a `customMessageReplyViewBuilder` to customize reply message view for custom type message. ```dart ChatView( @@ -702,7 +691,7 @@ ChatView( ) ``` -30. Add default avatar for profile image `defaultAvatarImage`, +29. Add default avatar for profile image `defaultAvatarImage`, error builder for asset and network profile image `assetImageErrorBuilder` `networkImageErrorBuilder`, Enum `ImageType` to define image as asset, network or base64 data. ```dart @@ -727,7 +716,7 @@ ChatView( ``` -31. Added a `customMessageReplyViewBuilder` to customize reply message view for custom type message. +30. Added a `customMessageReplyViewBuilder` to customize reply message view for custom type message. ```dart ChatView( @@ -812,8 +801,42 @@ ChatView( ) ``` +33. Reply Suggestions functionalities. + +* Add reply suggestions +```dart +_chatController.addReplySuggestions([ + SuggestionItemData(text: 'Thanks.'), + SuggestionItemData(text: 'Thank you very much.'), + SuggestionItemData(text: 'Great.') + ]); +``` +* Remove reply suggestions +```dart +_chatController.removeReplySuggestions(); +``` +* Update Sugestions Config +```dart +replySuggestionsConfig: ReplySuggestionsConfig( + itemConfig: SuggestionItemConfig( + decoration: BoxDecoration(), + textStyle: TextStyle(), + padding: EdgetInsets.all(8), + customItemBuilder: (index, suggestionItemData) => Container() + ), + listConfig: SuggestionListConfig( + decoration: BoxDecoration(), + padding: EdgetInsets.all(8), + itemSeparatorWidth: 8, + axisAlignment: SuggestionListAlignment.left + ) + onTap: (item) => + _onSendTap(item.text, const ReplyMessage(), MessageType.text), + autoDismissOnSelection: true +), +``` -33. Added callback `messageSorter` to sort message in `ChatBackgroundConfiguration`. +34. Added callback `messageSorter` to sort message in `ChatBackgroundConfiguration`. ```dart ChatView( @@ -832,7 +855,7 @@ ChatView( ## How to use -Check out [blog](https://medium.com/simform-engineering/chatview-a-cutting-edge-chat-ui-solution-7367b1f9d772) for better understanding and basic implementation. +Check out [blog](https://medium.com/simform-engineering/chatview-a-cutting-edge-chat-ui-solution-7367b1f9d772) for better understanding and basic implementation. Also, for whole example, check out the **example** app in the [example](https://github.com/SimformSolutionsPvtLtd/flutter_chatview/tree/main/example) directory or the 'Example' tab on pub.dartlang.org for a more complete example. @@ -844,6 +867,8 @@ Also, for whole example, check out the **example** app in the [example](https://
Vatsal Tanna

Dhvanit Vaghani

Ujas Majithiya
+
Apurva Kanthraviya
+
diff --git a/example/lib/main.dart b/example/lib/main.dart index bdc02f92..9a706f46 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -71,6 +71,23 @@ class _ChatScreenState extends State { _chatController.setTypingIndicator = !_chatController.showTypingIndicator; } + void receiveMessage() async { + _chatController.addMessage( + Message( + id: DateTime.now().toString(), + message: 'I will schedule the meeting.', + createdAt: DateTime.now(), + sendBy: '2', + ), + ); + await Future.delayed(const Duration(milliseconds: 500)); + _chatController.addReplySuggestions([ + const SuggestionItemData(text: 'Thanks.'), + const SuggestionItemData(text: 'Thank you very much.'), + const SuggestionItemData(text: 'Great.') + ]); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -125,6 +142,14 @@ class _ChatScreenState extends State { color: theme.themeIconColor, ), ), + IconButton( + tooltip: 'Simulate Message receive', + onPressed: receiveMessage, + icon: Icon( + Icons.supervised_user_circle, + color: theme.themeIconColor, + ), + ), ], ), chatBackgroundConfig: ChatBackgroundConfiguration( @@ -266,6 +291,22 @@ class _ChatScreenState extends State { swipeToReplyConfig: SwipeToReplyConfiguration( replyIconColor: theme.swipeToReplyIconColor, ), + replySuggestionsConfig: ReplySuggestionsConfig( + itemConfig: SuggestionItemConfig( + decoration: BoxDecoration( + color: theme.textFieldBackgroundColor, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: theme.outgoingChatBubbleColor ?? Colors.white, + ), + ), + textStyle: TextStyle( + color: isDarkTheme ? Colors.white : Colors.black, + ), + ), + onTap: (item) => + _onSendTap(item.text, const ReplyMessage(), MessageType.text), + ), ), ); } @@ -275,10 +316,9 @@ class _ChatScreenState extends State { ReplyMessage replyMessage, MessageType messageType, ) { - final id = int.parse(Data.messageList.last.id) + 1; _chatController.addMessage( Message( - id: id.toString(), + id: DateTime.now().toString(), createdAt: DateTime.now(), message: message, sendBy: currentUser.id, diff --git a/lib/src/controller/chat_controller.dart b/lib/src/controller/chat_controller.dart index d1c277e6..0046746c 100644 --- a/lib/src/controller/chat_controller.dart +++ b/lib/src/controller/chat_controller.dart @@ -21,6 +21,7 @@ */ import 'dart:async'; +import 'package:chatview/src/widgets/suggestions/suggestion_list.dart'; import 'package:flutter/material.dart'; import '../models/models.dart'; @@ -42,6 +43,19 @@ class ChatController { /// For more functionalities see [ValueNotifier]. ValueNotifier get typingIndicatorNotifier => _showTypingIndicator; + /// Allow user to add reply suggestions defaults to empty. + final ValueNotifier> _replySuggestion = + ValueNotifier([]); + + /// newSuggestions as [ValueNotifier] for [SuggestionList] widget's [ValueListenableBuilder]. + /// Use this to listen when suggestion gets added + /// ```dart + /// chatcontroller.newSuggestions.addListener((){}); + /// ``` + /// For more functionalities see [ValueNotifier]. + ValueNotifier> get newSuggestions => + _replySuggestion; + /// Getter for typingIndicator value instead of accessing [_showTypingIndicator.value] /// for better accessibility. bool get showTypingIndicator => _showTypingIndicator.value; @@ -74,6 +88,16 @@ class ChatController { messageStreamController.sink.add(initialMessageList); } + /// Used to add reply suggestions. + void addReplySuggestions(List suggestions) { + _replySuggestion.value = suggestions; + } + + /// Used to remove reply suggestions. + void removeReplySuggestions() { + _replySuggestion.value = []; + } + /// Function for setting reaction on specific chat bubble void setReaction({ required String emoji, diff --git a/lib/src/extensions/extensions.dart b/lib/src/extensions/extensions.dart index 0e8ebcfc..99c0a53f 100644 --- a/lib/src/extensions/extensions.dart +++ b/lib/src/extensions/extensions.dart @@ -22,6 +22,7 @@ import 'package:chatview/chatview.dart'; import 'package:chatview/src/widgets/chat_view_inherited_widget.dart'; import 'package:chatview/src/widgets/profile_image_widget.dart'; +import 'package:chatview/src/widgets/suggestions/suggestions_config_inherited_widget.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import '../utils/constants/constants.dart'; @@ -129,4 +130,15 @@ extension ChatViewStateTitleExtension on String? { /// Extension on State for accessing inherited widget. extension StatefulWidgetExtension on State { ChatViewInheritedWidget? get provide => ChatViewInheritedWidget.of(context); + + ReplySuggestionsConfig? get suggestionsConfig => + SuggestionsConfigIW.of(context)?.suggestionsConfig; +} + +/// Extension on State for accessing inherited widget. +extension BuildContextExtension on BuildContext { + ChatViewInheritedWidget? get provide => ChatViewInheritedWidget.of(this); + + ReplySuggestionsConfig? get suggestionsConfig => + SuggestionsConfigIW.of(this)?.suggestionsConfig; } diff --git a/lib/src/models/models.dart b/lib/src/models/models.dart index 45bcbbdf..7c7f854f 100644 --- a/lib/src/models/models.dart +++ b/lib/src/models/models.dart @@ -41,3 +41,7 @@ export 'chat_view_states_configuration.dart'; export 'reaction.dart'; export 'replied_msg_auto_scroll_config.dart'; export 'feature_active_config.dart'; +export 'suggestion_item_data.dart'; +export 'reply_suggestions_config.dart'; +export 'suggestion_list_config.dart'; +export 'suggestion_item_config.dart'; diff --git a/lib/src/models/profile_circle.dart b/lib/src/models/profile_circle.dart index 5e805cb9..88614a94 100644 --- a/lib/src/models/profile_circle.dart +++ b/lib/src/models/profile_circle.dart @@ -59,7 +59,7 @@ class ProfileCircleConfiguration { /// Progress indicator builder for network image final NetworkImageProgressIndicatorBuilder? - networkImageProgressIndicatorBuilder; + networkImageProgressIndicatorBuilder; const ProfileCircleConfiguration({ this.onAvatarTap, diff --git a/lib/src/models/reply_suggestions_config.dart b/lib/src/models/reply_suggestions_config.dart new file mode 100644 index 00000000..ab6db431 --- /dev/null +++ b/lib/src/models/reply_suggestions_config.dart @@ -0,0 +1,19 @@ +import 'package:chatview/src/models/suggestion_item_data.dart'; +import 'package:flutter/material.dart'; + +import 'suggestion_item_config.dart'; +import 'suggestion_list_config.dart'; + +class ReplySuggestionsConfig { + final SuggestionItemConfig? itemConfig; + final SuggestionListConfig? listConfig; + final ValueSetter? onTap; + final bool autoDismissOnSelection; + + const ReplySuggestionsConfig({ + this.listConfig, + this.itemConfig, + this.onTap, + this.autoDismissOnSelection = true, + }); +} diff --git a/lib/src/models/suggestion_item_config.dart b/lib/src/models/suggestion_item_config.dart new file mode 100644 index 00000000..1291b8fe --- /dev/null +++ b/lib/src/models/suggestion_item_config.dart @@ -0,0 +1,16 @@ +import '../values/typedefs.dart'; +import 'package:flutter/material.dart'; + +class SuggestionItemConfig { + final BoxDecoration? decoration; + final EdgeInsets? padding; + final TextStyle? textStyle; + final SuggestionItemBuilder? customItemBuilder; + + const SuggestionItemConfig({ + this.decoration, + this.padding, + this.textStyle, + this.customItemBuilder, + }); +} diff --git a/lib/src/models/suggestion_item_data.dart b/lib/src/models/suggestion_item_data.dart new file mode 100644 index 00000000..a1ded751 --- /dev/null +++ b/lib/src/models/suggestion_item_data.dart @@ -0,0 +1,11 @@ +import 'suggestion_item_config.dart'; + +class SuggestionItemData { + final String text; + final SuggestionItemConfig? config; + + const SuggestionItemData({ + required this.text, + this.config, + }); +} diff --git a/lib/src/models/suggestion_list_config.dart b/lib/src/models/suggestion_list_config.dart new file mode 100644 index 00000000..445aafd9 --- /dev/null +++ b/lib/src/models/suggestion_list_config.dart @@ -0,0 +1,18 @@ +import '../values/enumeration.dart'; +import 'package:flutter/cupertino.dart'; + +class SuggestionListConfig { + final BoxDecoration? decoration; + final EdgeInsets? padding; + final EdgeInsets? margin; + final double itemSeparatorWidth; + final SuggestionListAlignment axisAlignment; + + const SuggestionListConfig({ + this.decoration, + this.padding, + this.margin, + this.axisAlignment = SuggestionListAlignment.right, + this.itemSeparatorWidth = 8, + }); +} diff --git a/lib/src/utils/constants/constants.dart b/lib/src/utils/constants/constants.dart index 620239a5..31f8dde8 100644 --- a/lib/src/utils/constants/constants.dart +++ b/lib/src/utils/constants/constants.dart @@ -86,3 +86,5 @@ Widget lastSeenAgoBuilder(Message message, String formattedDate) { ), ); } + +const suggestionListAnimationDuration = Duration(milliseconds: 200); diff --git a/lib/src/values/enumeration.dart b/lib/src/values/enumeration.dart index ea28745d..56c38ab6 100644 --- a/lib/src/values/enumeration.dart +++ b/lib/src/values/enumeration.dart @@ -21,6 +21,9 @@ */ // Different types Message of ChatView + +import 'package:flutter/material.dart'; + enum MessageType { image, text, @@ -103,6 +106,16 @@ enum ImageType { } } +enum SuggestionListAlignment { + left(Alignment.bottomLeft), + center(Alignment.bottomCenter), + right(Alignment.bottomRight); + + const SuggestionListAlignment(this.alignment); + + final Alignment alignment; +} + extension ChatViewStateExtension on ChatViewState { bool get hasMessages => this == ChatViewState.hasMessages; diff --git a/lib/src/values/typedefs.dart b/lib/src/values/typedefs.dart index 966d2c4c..890d3c53 100644 --- a/lib/src/values/typedefs.dart +++ b/lib/src/values/typedefs.dart @@ -83,3 +83,7 @@ typedef NetworkImageProgressIndicatorBuilder = Widget Function( String url, DownloadProgress progress, ); +typedef SuggestionItemBuilder = Widget Function( + int index, + SuggestionItemData suggestionItemData, +); diff --git a/lib/src/widgets/chat_groupedlist_widget.dart b/lib/src/widgets/chat_groupedlist_widget.dart index ab9c7d1e..afc9b785 100644 --- a/lib/src/widgets/chat_groupedlist_widget.dart +++ b/lib/src/widgets/chat_groupedlist_widget.dart @@ -22,6 +22,7 @@ import 'package:chatview/chatview.dart'; import 'package:chatview/src/extensions/extensions.dart'; import 'package:chatview/src/widgets/chat_view_inherited_widget.dart'; +import 'package:chatview/src/widgets/suggestions/suggestion_list.dart'; import 'package:chatview/src/widgets/type_indicator_widget.dart'; import 'package:flutter/material.dart'; @@ -46,7 +47,6 @@ class ChatGroupedListWidget extends StatefulWidget { this.swipeToReplyConfig, this.repliedMessageConfig, this.typeIndicatorConfig, - this.chatTextFieldTopPadding = 0, }) : super(key: key); /// Allow user to swipe to see time while reaction pop is not open. @@ -92,9 +92,6 @@ class ChatGroupedListWidget extends StatefulWidget { /// swipe whole chat. final bool isEnableSwipeToSeeTime; - /// Provides top padding of chat text field - final double chatTextFieldTopPadding; - @override State createState() => _ChatGroupedListWidgetState(); } @@ -124,10 +121,29 @@ class _ChatGroupedListWidgetState extends State bool get isEnableSwipeToSeeTime => widget.isEnableSwipeToSeeTime; + double height = 0; + @override void initState() { super.initState(); _initializeAnimation(); + updateChatTextFieldHeight(); + } + + @override + void didUpdateWidget(covariant ChatGroupedListWidget oldWidget) { + super.didUpdateWidget(oldWidget); + updateChatTextFieldHeight(); + } + + void updateChatTextFieldHeight() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + setState(() { + height = + provide?.chatTextFieldViewKey.currentContext?.size?.height ?? 10; + }); + }); } void _initializeAnimation() { @@ -162,6 +178,8 @@ class _ChatGroupedListWidgetState extends State @override Widget build(BuildContext context) { + final suggestionsListConfig = + suggestionsConfig?.listConfig ?? const SuggestionListConfig(); return SingleChildScrollView( reverse: true, // When reaction popup is being appeared at that user should not scroll. @@ -169,6 +187,7 @@ class _ChatGroupedListWidgetState extends State padding: EdgeInsets.only(bottom: showTypingIndicator ? 50 : 0), controller: widget.scrollController, child: Column( + mainAxisSize: MainAxisSize.min, children: [ GestureDetector( onHorizontalDragUpdate: (details) => isEnableSwipeToSeeTime @@ -202,16 +221,31 @@ class _ChatGroupedListWidgetState extends State .chatController .typingIndicatorNotifier, builder: (context, value, child) => TypingIndicator( - typeIndicatorConfig: widget.typeIndicatorConfig, - chatBubbleConfig: - chatBubbleConfig?.inComingChatBubbleConfig, - showIndicator: value, - )), - SizedBox( - height: (MediaQuery.of(context).size.width * - (widget.replyMessage.message.isNotEmpty ? 0.3 : 0.14)) + - (widget.chatTextFieldTopPadding), + typeIndicatorConfig: widget.typeIndicatorConfig, + chatBubbleConfig: + chatBubbleConfig?.inComingChatBubbleConfig, + showIndicator: value, + ), + ), + Flexible( + child: Align( + alignment: suggestionsListConfig.axisAlignment.alignment, + child: ValueListenableBuilder( + valueListenable: ChatViewInheritedWidget.of(context)! + .chatController + .newSuggestions, + builder: (context, value, child) { + return SuggestionList( + suggestions: value, + ); + }, + ), + ), ), + + // Adds bottom space to the message list, ensuring it is displayed + // above the message text field. + const SizedBox(height: 100), ], ), ); diff --git a/lib/src/widgets/chat_list_widget.dart b/lib/src/widgets/chat_list_widget.dart index 9f707304..b708665f 100644 --- a/lib/src/widgets/chat_list_widget.dart +++ b/lib/src/widgets/chat_list_widget.dart @@ -51,7 +51,6 @@ class ChatListWidget extends StatefulWidget { this.loadMoreData, this.isLastPage, this.onChatListTap, - this.chatTextFieldTopPadding = 0, this.emojiPickerSheetConfig, }) : super(key: key); @@ -110,9 +109,6 @@ class ChatListWidget extends StatefulWidget { /// Provides callback when user tap anywhere on whole chat. final VoidCallBack? onChatListTap; - /// Provides top padding of chat text field - final double chatTextFieldTopPadding; - /// Configuration for emoji picker sheet final Config? emojiPickerSheetConfig; @@ -229,7 +225,6 @@ class _ChatListWidgetState extends State } }, onChatListTap: _onChatListTap, - chatTextFieldTopPadding: widget.chatTextFieldTopPadding, ), if (featureActiveConfig?.enableReactionPopup ?? false) ReactionPopup( diff --git a/lib/src/widgets/chat_view.dart b/lib/src/widgets/chat_view.dart index e206adc8..c9c67eac 100644 --- a/lib/src/widgets/chat_view.dart +++ b/lib/src/widgets/chat_view.dart @@ -20,9 +20,11 @@ * SOFTWARE. */ import 'package:chatview/chatview.dart'; +import 'package:chatview/src/extensions/extensions.dart'; import 'package:chatview/src/widgets/chat_list_widget.dart'; import 'package:chatview/src/widgets/chat_view_inherited_widget.dart'; import 'package:chatview/src/widgets/chatview_state_widget.dart'; +import 'package:chatview/src/widgets/suggestions/suggestions_config_inherited_widget.dart'; import 'package:flutter/material.dart'; import 'package:timeago/timeago.dart'; import '../values/custom_time_messages.dart'; @@ -53,9 +55,9 @@ class ChatView extends StatefulWidget { required this.chatViewState, ChatViewStateConfiguration? chatViewStateConfig, this.featureActiveConfig = const FeatureActiveConfig(), - this.chatTextFieldTopPadding = 0, this.emojiPickerSheetConfig, this.replyMessageBuilder, + this.replySuggestionsConfig, }) : chatBackgroundConfig = chatBackgroundConfig ?? const ChatBackgroundConfiguration(), chatViewStateConfig = @@ -134,12 +136,12 @@ class ChatView extends StatefulWidget { /// Provides callback when user tap on chat list. final VoidCallBack? onChatListTap; - /// Provides top padding of chat text field - final double chatTextFieldTopPadding; - /// Configuration for emoji picker sheet final Config? emojiPickerSheetConfig; + /// Suggestion Item Config + final ReplySuggestionsConfig? replySuggestionsConfig; + /// Provides a callback for the view when replying to message final CustomViewForReplyMessage? replyMessageBuilder; @@ -194,97 +196,113 @@ class _ChatViewState extends State featureActiveConfig: featureActiveConfig, currentUser: widget.currentUser, profileCircleConfiguration: widget.profileCircleConfig, - child: Container( - height: - chatBackgroundConfig.height ?? MediaQuery.of(context).size.height, - width: chatBackgroundConfig.width ?? MediaQuery.of(context).size.width, - decoration: BoxDecoration( - color: chatBackgroundConfig.backgroundColor ?? Colors.white, - image: chatBackgroundConfig.backgroundImage != null - ? DecorationImage( - fit: BoxFit.fill, - image: NetworkImage(chatBackgroundConfig.backgroundImage!), - ) - : null, - ), - padding: chatBackgroundConfig.padding, - margin: chatBackgroundConfig.margin, - child: Column( - children: [ - if (widget.appBar != null) widget.appBar!, - Expanded( - child: Stack( - children: [ - if (chatViewState.isLoading) - ChatViewStateWidget( - chatViewStateWidgetConfig: - chatViewStateConfig?.loadingWidgetConfig, - chatViewState: chatViewState, - ) - else if (chatViewState.noMessages) - ChatViewStateWidget( - chatViewStateWidgetConfig: - chatViewStateConfig?.noMessageWidgetConfig, - chatViewState: chatViewState, - onReloadButtonTap: chatViewStateConfig?.onReloadButtonTap, + child: SuggestionsConfigIW( + suggestionsConfig: widget.replySuggestionsConfig, + child: Builder(builder: (context) { + return Container( + height: chatBackgroundConfig.height ?? + MediaQuery.of(context).size.height, + width: + chatBackgroundConfig.width ?? MediaQuery.of(context).size.width, + decoration: BoxDecoration( + color: chatBackgroundConfig.backgroundColor ?? Colors.white, + image: chatBackgroundConfig.backgroundImage != null + ? DecorationImage( + fit: BoxFit.fill, + image: + NetworkImage(chatBackgroundConfig.backgroundImage!), ) - else if (chatViewState.isError) - ChatViewStateWidget( - chatViewStateWidgetConfig: - chatViewStateConfig?.errorWidgetConfig, - chatViewState: chatViewState, - onReloadButtonTap: chatViewStateConfig?.onReloadButtonTap, - ) - else if (chatViewState.hasMessages) - ValueListenableBuilder( - valueListenable: replyMessage, - builder: (_, state, child) { - return ChatListWidget( - showTypingIndicator: - chatController.showTypingIndicator, - replyMessage: state, - chatController: widget.chatController, - chatBackgroundConfig: widget.chatBackgroundConfig, - reactionPopupConfig: widget.reactionPopupConfig, - typeIndicatorConfig: widget.typeIndicatorConfig, - chatBubbleConfig: widget.chatBubbleConfig, - loadMoreData: widget.loadMoreData, - isLastPage: widget.isLastPage, - replyPopupConfig: widget.replyPopupConfig, - loadingWidget: widget.loadingWidget, + : null, + ), + padding: chatBackgroundConfig.padding, + margin: chatBackgroundConfig.margin, + child: Column( + children: [ + if (widget.appBar != null) widget.appBar!, + Expanded( + child: Stack( + children: [ + if (chatViewState.isLoading) + ChatViewStateWidget( + chatViewStateWidgetConfig: + chatViewStateConfig?.loadingWidgetConfig, + chatViewState: chatViewState, + ) + else if (chatViewState.noMessages) + ChatViewStateWidget( + chatViewStateWidgetConfig: + chatViewStateConfig?.noMessageWidgetConfig, + chatViewState: chatViewState, + onReloadButtonTap: + chatViewStateConfig?.onReloadButtonTap, + ) + else if (chatViewState.isError) + ChatViewStateWidget( + chatViewStateWidgetConfig: + chatViewStateConfig?.errorWidgetConfig, + chatViewState: chatViewState, + onReloadButtonTap: + chatViewStateConfig?.onReloadButtonTap, + ) + else if (chatViewState.hasMessages) + ValueListenableBuilder( + valueListenable: replyMessage, + builder: (_, state, child) { + return ChatListWidget( + showTypingIndicator: + chatController.showTypingIndicator, + replyMessage: state, + chatController: widget.chatController, + chatBackgroundConfig: widget.chatBackgroundConfig, + reactionPopupConfig: widget.reactionPopupConfig, + typeIndicatorConfig: widget.typeIndicatorConfig, + chatBubbleConfig: widget.chatBubbleConfig, + loadMoreData: widget.loadMoreData, + isLastPage: widget.isLastPage, + replyPopupConfig: widget.replyPopupConfig, + loadingWidget: widget.loadingWidget, + messageConfig: widget.messageConfig, + profileCircleConfig: widget.profileCircleConfig, + repliedMessageConfig: widget.repliedMessageConfig, + swipeToReplyConfig: widget.swipeToReplyConfig, + onChatListTap: widget.onChatListTap, + assignReplyMessage: (message) => _sendMessageKey + .currentState + ?.assignReplyMessage(message), + emojiPickerSheetConfig: + widget.emojiPickerSheetConfig, + ); + }, + ), + if (featureActiveConfig.enableTextField) + SendMessageWidget( + key: _sendMessageKey, + chatController: chatController, + sendMessageBuilder: widget.sendMessageBuilder, + sendMessageConfig: widget.sendMessageConfig, + backgroundColor: chatBackgroundConfig.backgroundColor, + onSendTap: (message, replyMessage, messageType) { + if (context.suggestionsConfig + ?.autoDismissOnSelection ?? + true) { + chatController.removeReplySuggestions(); + } + _onSendTap(message, replyMessage, messageType); + }, + onReplyCallback: (reply) => + replyMessage.value = reply, + onReplyCloseCallback: () => + replyMessage.value = const ReplyMessage(), messageConfig: widget.messageConfig, - profileCircleConfig: widget.profileCircleConfig, - repliedMessageConfig: widget.repliedMessageConfig, - swipeToReplyConfig: widget.swipeToReplyConfig, - onChatListTap: widget.onChatListTap, - assignReplyMessage: (message) => _sendMessageKey - .currentState - ?.assignReplyMessage(message), - chatTextFieldTopPadding: - widget.chatTextFieldTopPadding, - emojiPickerSheetConfig: widget.emojiPickerSheetConfig, - ); - }, - ), - if (featureActiveConfig.enableTextField) - SendMessageWidget( - key: _sendMessageKey, - chatController: chatController, - sendMessageBuilder: widget.sendMessageBuilder, - sendMessageConfig: widget.sendMessageConfig, - backgroundColor: chatBackgroundConfig.backgroundColor, - onSendTap: _onSendTap, - onReplyCallback: (reply) => replyMessage.value = reply, - onReplyCloseCallback: () => - replyMessage.value = const ReplyMessage(), - messageConfig: widget.messageConfig, - replyMessageBuilder: widget.replyMessageBuilder, - ), - ], - ), + replyMessageBuilder: widget.replyMessageBuilder, + ), + ], + ), + ), + ], ), - ], - ), + ); + }), ), ); } diff --git a/lib/src/widgets/chat_view_appbar.dart b/lib/src/widgets/chat_view_appbar.dart index fb7cebe6..8da6f4d2 100644 --- a/lib/src/widgets/chat_view_appbar.dart +++ b/lib/src/widgets/chat_view_appbar.dart @@ -104,7 +104,7 @@ class ChatViewAppBar extends StatelessWidget { /// Progress indicator builder for network image final NetworkImageProgressIndicatorBuilder? - networkImageProgressIndicatorBuilder; + networkImageProgressIndicatorBuilder; @override Widget build(BuildContext context) { @@ -142,7 +142,8 @@ class ChatViewAppBar extends StatelessWidget { assetImageErrorBuilder: assetImageErrorBuilder, networkImageErrorBuilder: networkImageErrorBuilder, imageType: imageType, - networkImageProgressIndicatorBuilder: networkImageProgressIndicatorBuilder, + networkImageProgressIndicatorBuilder: + networkImageProgressIndicatorBuilder, ), ), Column( diff --git a/lib/src/widgets/chat_view_inherited_widget.dart b/lib/src/widgets/chat_view_inherited_widget.dart index 0aaea25e..31289b5f 100644 --- a/lib/src/widgets/chat_view_inherited_widget.dart +++ b/lib/src/widgets/chat_view_inherited_widget.dart @@ -4,7 +4,7 @@ import 'package:chatview/chatview.dart'; /// This widget for alternative of excessive amount of passing arguments /// over widgets. class ChatViewInheritedWidget extends InheritedWidget { - const ChatViewInheritedWidget({ + ChatViewInheritedWidget({ Key? key, required Widget child, required this.featureActiveConfig, @@ -16,6 +16,7 @@ class ChatViewInheritedWidget extends InheritedWidget { final ProfileCircleConfiguration? profileCircleConfiguration; final ChatController chatController; final ChatUser currentUser; + final GlobalKey chatTextFieldViewKey = GlobalKey(); static ChatViewInheritedWidget? of(BuildContext context) => context.dependOnInheritedWidgetOfExactType(); diff --git a/lib/src/widgets/send_message_widget.dart b/lib/src/widgets/send_message_widget.dart index 4fa0ab5f..3e41619f 100644 --- a/lib/src/widgets/send_message_widget.dart +++ b/lib/src/widgets/send_message_widget.dart @@ -104,19 +104,17 @@ class SendMessageWidgetState extends State { @override Widget build(BuildContext context) { - return widget.sendMessageBuilder != null - ? Positioned( - right: 0, - left: 0, - bottom: 0, - child: widget.sendMessageBuilder!(replyMessage), - ) - : Align( - alignment: Alignment.bottomCenter, - child: SizedBox( + return Align( + alignment: Alignment.bottomCenter, + child: widget.sendMessageBuilder != null + ? widget.sendMessageBuilder!(replyMessage) + : SizedBox( width: MediaQuery.of(context).size.width, child: Stack( children: [ + // This has been added to prevent messages from being + // displayed below the text field + // when the user scrolls the message list. Positioned( right: 0, left: 0, @@ -128,6 +126,7 @@ class SendMessageWidgetState extends State { ), ), Padding( + key: provide?.chatTextFieldViewKey, padding: EdgeInsets.fromLTRB( bottomPadding4, bottomPadding4, @@ -249,7 +248,7 @@ class SendMessageWidgetState extends State { ], ), ), - ); + ); } void _onRecordingComplete(String? path) { diff --git a/lib/src/widgets/suggestions/suggestion_item.dart b/lib/src/widgets/suggestions/suggestion_item.dart new file mode 100644 index 00000000..025c26ec --- /dev/null +++ b/lib/src/widgets/suggestions/suggestion_item.dart @@ -0,0 +1,47 @@ +import 'package:chatview/src/extensions/extensions.dart'; +import 'package:chatview/src/models/models.dart'; +import 'package:flutter/material.dart'; + +class SuggestionItem extends StatelessWidget { + const SuggestionItem({ + super.key, + required this.suggestionItemData, + }); + + final SuggestionItemData suggestionItemData; + + @override + Widget build(BuildContext context) { + final suggestionsConfig = + context.suggestionsConfig ?? const ReplySuggestionsConfig(); + final suggestionsListConfig = suggestionsConfig.itemConfig; + final theme = Theme.of(context); + return GestureDetector( + onTap: () { + suggestionsConfig.onTap?.call(suggestionItemData); + if (suggestionsConfig.autoDismissOnSelection) { + context.provide?.chatController.removeReplySuggestions(); + } + }, + child: Container( + padding: suggestionItemData.config?.padding ?? + suggestionsListConfig?.padding ?? + const EdgeInsets.all(6), + decoration: suggestionItemData.config?.decoration ?? + suggestionsListConfig?.decoration ?? + BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(8)), + border: Border.all( + color: theme.primaryColor, + ), + ), + child: Text( + suggestionItemData.text, + style: suggestionItemData.config?.textStyle ?? + suggestionsListConfig?.textStyle, + ), + ), + ); + } +} diff --git a/lib/src/widgets/suggestions/suggestion_list.dart b/lib/src/widgets/suggestions/suggestion_list.dart new file mode 100644 index 00000000..1fb0c36b --- /dev/null +++ b/lib/src/widgets/suggestions/suggestion_list.dart @@ -0,0 +1,100 @@ +import 'package:chatview/src/extensions/extensions.dart'; +import 'package:chatview/src/models/models.dart'; +import 'package:chatview/src/utils/constants/constants.dart'; +import 'package:chatview/src/widgets/suggestions/suggestion_item.dart'; +import 'package:flutter/material.dart'; + +class SuggestionList extends StatefulWidget { + const SuggestionList({ + super.key, + required this.suggestions, + this.gap, + }); + + final List suggestions; + final double? gap; + + @override + State createState() => _SuggestionListState(); +} + +class _SuggestionListState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + + @override + void initState() { + super.initState(); + + _controller = AnimationController( + duration: suggestionListAnimationDuration, + vsync: this, + ); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + final newSuggestions = provide?.chatController.newSuggestions; + newSuggestions?.addListener(animateSuggestionList); + }); + } + + void animateSuggestionList() { + final newSuggestions = provide?.chatController.newSuggestions; + if (newSuggestions != null) { + newSuggestions.value.isEmpty + ? _controller.reverse() + : _controller.forward(); + } + } + + @override + Widget build(BuildContext context) { + final suggestionsItemConfig = suggestionsConfig?.itemConfig; + final suggestionsListConfig = + suggestionsConfig?.listConfig ?? const SuggestionListConfig(); + return Container( + decoration: suggestionsListConfig.decoration, + padding: + suggestionsListConfig.padding ?? const EdgeInsets.only(left: 8.0), + margin: suggestionsListConfig.margin, + child: SizeTransition( + sizeFactor: _controller, + axisAlignment: -1.0, + fixedCrossAxisSizeFactor: 1, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: List.generate( + widget.suggestions.length, + (index) { + final suggestion = widget.suggestions[index]; + return suggestion.config?.customItemBuilder + ?.call(index, suggestion) ?? + suggestionsItemConfig?.customItemBuilder + ?.call(index, suggestion) ?? + Padding( + padding: EdgeInsets.only( + right: index == widget.suggestions.length + ? 0 + : (suggestionsListConfig.itemSeparatorWidth), + ), + child: SuggestionItem( + suggestionItemData: suggestion, + ), + ); + }, + ), + ), + ), + ), + ); + } + + @override + void dispose() { + _controller.dispose(); + final newSuggestions = provide?.chatController.newSuggestions; + newSuggestions?.removeListener(animateSuggestionList); + super.dispose(); + } +} diff --git a/lib/src/widgets/suggestions/suggestions_config_inherited_widget.dart b/lib/src/widgets/suggestions/suggestions_config_inherited_widget.dart new file mode 100644 index 00000000..bdd96a92 --- /dev/null +++ b/lib/src/widgets/suggestions/suggestions_config_inherited_widget.dart @@ -0,0 +1,21 @@ +import 'package:chatview/src/models/reply_suggestions_config.dart'; +import 'package:flutter/material.dart'; +import 'package:chatview/chatview.dart'; + +/// This widget for alternative of excessive amount of passing arguments +/// over widgets. +class SuggestionsConfigIW extends InheritedWidget { + const SuggestionsConfigIW({ + super.key, + required super.child, + this.suggestionsConfig, + }); + + final ReplySuggestionsConfig? suggestionsConfig; + + static SuggestionsConfigIW? of(BuildContext context) => + context.dependOnInheritedWidgetOfExactType(); + + @override + bool updateShouldNotify(covariant SuggestionsConfigIW oldWidget) => false; +}