diff --git a/doc/contributors.md b/doc/contributors.md new file mode 100644 index 00000000..1acf2ae2 --- /dev/null +++ b/doc/contributors.md @@ -0,0 +1,12 @@ +## Main Contributors + + + + + + + + + +

Vatsal Tanna

Dhvanit Vaghani

Ujas Majithiya

Apurva Kanthraviya

Aditya Chavda
+
\ No newline at end of file diff --git a/doc/how_to_use.md b/doc/how_to_use.md new file mode 100644 index 00000000..b517e834 --- /dev/null +++ b/doc/how_to_use.md @@ -0,0 +1,5 @@ +## 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. + +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. \ No newline at end of file diff --git a/doc/installation.md b/doc/installation.md new file mode 100644 index 00000000..90d47693 --- /dev/null +++ b/doc/installation.md @@ -0,0 +1,59 @@ +1. Add dependencies to `pubspec.yaml` + + Get the latest version in the 'Installing' tab + on [pub.dev](https://pub.dev/packages/chatview/install) + + ```yaml + dependencies: + chatview: + ``` + +2. Run pub get. + + ```shell + flutter pub get + ``` + +3. Import package. + + ```dart + import 'package:chatview/chatview.dart'; + ``` + +## Platform specific configuration + +### For image Picker +#### iOS +* Add the following keys to your _Info.plist_ file, located in `/ios/Runner/Info.plist`: + +``` + NSCameraUsageDescription + Used to demonstrate image picker plugin + NSMicrophoneUsageDescription + Used to capture audio for image picker plugin + NSPhotoLibraryUsageDescription + Used to demonstrate image picker plugin +``` + +### For voice messages +#### iOS +* Add this two rows in `ios/Runner/Info.plist` +``` + NSMicrophoneUsageDescription + This app requires Mic permission. +``` +* This plugin requires ios 10.0 or higher. So add this line in `Podfile` +``` + platform :ios, '10.0' +``` + +#### Android +* Change the minimum Android sdk version to 21 (or higher) in your android/app/build.gradle file. +``` + minSdkVersion 21 +``` + +* Add RECORD_AUDIO permission in `AndroidManifest.xml` +``` + +``` \ No newline at end of file diff --git a/doc/migration_guide.md b/doc/migration_guide.md new file mode 100644 index 00000000..ad9d9553 --- /dev/null +++ b/doc/migration_guide.md @@ -0,0 +1,171 @@ +## Migration guide for release 2.0.0 + +- Renamed `sendBy` field to `sentBy` in `Message` class. + +- Renamed `chatUsers` field to `otherUsers` in `ChatController` class. + +- Moved `currentUser` field from `ChatView` widget to `ChatController` class + +- Updated `id` value in `copyWith` method of `Message` to have correct value. + +- Removed `showTypingIndicator` field from `ChatView` and replaced it with `ChatController.showTypingIndicator`. + + Before: + ```dart + ChatView( + showTypingIndicator:false, + ), + ``` + + After: + ```dart + /// use it with your [ChatController] instance. + _chatContoller.setTypingIndicator = true; // for showing indicator + _chatContoller.setTypingIndicator = false; // for hiding indicator + ``` + +- Updated `ChatUser`, `Message` and `ReplyMessage` Data Model's `fromJson` and `toJson` methods: + + ##### in `ChatUser.fromJson`: + + Before: + ```dart + ChatUser.fromJson( + { + ... + 'imageType': ImageType.asset, + ... + }, + ), + ``` + + After: + ```dart + ChatUser.fromJson( + { + ... + 'imageType': 'asset', + ... + }, + ), + ``` + + ##### in `ChatUser.toJson`: + + Before: + ```dart + { + ... + imageType: ImageType.asset, + ... + } + ``` + + After: + ```dart + { + ... + imageType: asset, + ... + } + ``` + + ##### in `Message.fromJson`: + + Before: + ```dart + Message.fromJson( + { + ... + 'createdAt': DateTime.now(), + 'message_type': MessageType.text, + 'voice_message_duration': Duration(seconds: 5), + ... + } + ) + ``` + + After: + ```dart + Message.fromJson( + { + ... + 'createdAt': '2024-06-13T17:32:19.586412', + 'message_type': 'text', + 'voice_message_duration': '5000000', + ... + } + ) + ``` + + ##### in `Message.toJson`: + + Before: + ```dart + { + ... + createdAt: 2024-06-13 17:23:19.454789, + message_type: MessageType.text, + voice_message_duration: 0:00:05.000000, + ... + } + ``` + + After: + ```dart + { + ... + createdAt: 2024-06-13T17:32:19.586412, + message_type: text, + voice_message_duration: 5000000, + ... + } + ``` + + ##### in `ReplyMessage.fromJson`: + + Before: + ```dart + ReplyMessage.fromJson( + { + ... + 'message_type': MessageType.text, + 'voiceMessageDuration': Duration(seconds: 5), + ... + } + ) + ``` + + After: + ```dart + ReplyMessage.fromJson( + { + ... + 'message_type': 'text', + 'voiceMessageDuration': '5000000', + ... + } + ) + ``` + + in `ReplyMessage.toJson`: + + Before: + ```dart + { + ... + message_type: MessageType.text, + voiceMessageDuration: 0:00:05.000000, + ... + } + ``` + + After: + ```dart + { + ... + message_type: text, + voiceMessageDuration: 5000000, + ... + } + ``` \ No newline at end of file diff --git a/doc/optional_parameter.md b/doc/optional_parameter.md new file mode 100644 index 00000000..3d80e168 --- /dev/null +++ b/doc/optional_parameter.md @@ -0,0 +1,695 @@ +## Some more optional parameters + +1. Enable and disable specific features with `FeatureActiveConfig`. +```dart +ChatView( + ... + featureActiveConfig: FeatureActiveConfig( + enableSwipeToReply: true, + enableSwipeToSeeTime: false, + ), + ... +) +``` + +2. Adding an appbar with `ChatViewAppBar`. +```dart +ChatView( + ... + appBar: ChatViewAppBar( + profilePicture: profileImage, + chatTitle: "Simform", + userStatus: "online", + actions: [ + Icon(Icons.more_vert), + ], + ), + ... +) +``` + +3. Adding a message list configuration with `ChatBackgroundConfiguration` class. +```dart +ChatView( + ... + chatBackgroundConfig: ChatBackgroundConfiguration( + backgroundColor: Colors.white, + backgroundImage: backgroundImage, + ), + ... +) +``` + +4. Adding a send message configuration with `SendMessageConfiguration` class. +```dart +ChatView( + ... + sendMessageConfig: SendMessageConfiguration( + replyMessageColor: Colors.grey, + replyDialogColor:Colors.blue, + replyTitleColor: Colors.black, + closeIconColor: Colors.black, + ), + ... +) +``` + +5. Adding a chat bubble configuration with `ChatBubbleConfiguration` class. +```dart +ChatView( + ... + chatBubbleConfig: ChatBubbleConfiguration( + onDoubleTap: (){ + // Your code goes here + }, + outgoingChatBubbleConfig: ChatBubble( // Sender's message chat bubble + color: Colors.blue, + borderRadius: const BorderRadius.only( + topRight: Radius.circular(12), + topLeft: Radius.circular(12), + bottomLeft: Radius.circular(12), + ), + ), + inComingChatBubbleConfig: ChatBubble( // Receiver's message chat bubble + color: Colors.grey.shade200, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + bottomRight: Radius.circular(12), + ), + ), + ) + ... +) +``` + +6. Adding swipe to reply configuration with `SwipeToReplyConfiguration` class. +```dart +ChatView( + ... + swipeToReplyConfig: SwipeToReplyConfiguration( + onLeftSwipe: (message, sentBy){ + // Your code goes here + }, + onRightSwipe: (message, sentBy){ + // Your code goes here + }, + ), + ... +) +``` + +7. Adding messages configuration with `MessageConfiguration` class. +```dart +ChatView( + ... + messageConfig: MessageConfiguration( + messageReactionConfig: MessageReactionConfiguration(), // Emoji reaction configuration for single message + imageMessageConfig: ImageMessageConfiguration( + onTap: (){ + // Your code goes here + }, + shareIconConfig: ShareIconConfiguration( + onPressed: (){ + // Your code goes here + }, + ), + ), + ), + ... +) +``` + +8. Adding reaction pop-up configuration with `ReactionPopupConfiguration` class. +```dart +ChatView( + ... + reactionPopupConfig: ReactionPopupConfiguration( + backgroundColor: Colors.white, + userReactionCallback: (message, emoji){ + // Your code goes here + } + padding: EdgeInsets.all(12), + shadow: BoxShadow( + color: Colors.black54, + blurRadius: 20, + ), + ), + ... +) +``` + +9. Adding reply pop-up configuration with `ReplyPopupConfiguration` class. +```dart +ChatView( + ... + replyPopupConfig: ReplyPopupConfiguration( + backgroundColor: Colors.white, + onUnsendTap:(message){ // message is 'Message' class instance + // Your code goes here + }, + onReplyTap:(message){ // message is 'Message' class instance + // Your code goes here + }, + onReportTap:(){ + // Your code goes here + }, + onMoreTap:(){ + // Your code goes here + }, + ), + ... +) +``` + +10. Adding replied message configuration with `RepliedMessageConfiguration` class. +```dart +ChatView( + ... + repliedMessageConfig: RepliedMessageConfiguration( + backgroundColor: Colors.blue, + verticalBarColor: Colors.black, + repliedMsgAutoScrollConfig: RepliedMsgAutoScrollConfig(), + ), + ... +) +``` + +11. For customizing typing indicators use `typeIndicatorConfig` with `TypeIndicatorConfig`. +```dart +ChatView( + ... + + typeIndicatorConfig: TypeIndicatorConfiguration( + flashingCircleBrightColor: Colors.grey, + flashingCircleDarkColor: Colors.black, + ), + ... +) + +``` +12. For showing hiding typeIndicatorwidget use `ChatController.setTypingIndicaor`, for more info see `ChatController`. +```dart +/// use it with your [ChatController] instance. +_chatContoller.setTypingIndicator = true; // for showing indicator +_chatContoller.setTypingIndicator = false; // for hiding indicator +``` + +13. Adding linkpreview configuration with `LinkPreviewConfiguration` class. +```dart +ChatView( + ... + chatBubbleConfig: ChatBubbleConfiguration( + linkPreviewConfig: LinkPreviewConfiguration( + linkStyle: const TextStyle( + color: Colors.white, + decoration: TextDecoration.underline, + ), + backgroundColor: Colors.grey, + bodyStyle: const TextStyle( + color: Colors.grey.shade200, + fontSize:16, + ), + titleStyle: const TextStyle( + color: Colors.black, + fontSize:20, + ), + ), + ) + ... +) +``` + +14. Adding pagination. +```dart +ChatView( + ... + isLastPage: false, + featureActiveConfig: FeatureActiveConfig( + enablePagination: true, + ), + loadMoreData: chatController.loadMoreData, + ... +) +``` + +15. Add image picker configuration. +```dart +ChatView( + ... + sendMessageConfig: SendMessageConfiguration( + enableCameraImagePicker: false, + enableGalleryImagePicker: true, + imagePickerIconsConfig: ImagePickerIconsConfiguration( + cameraIconColor: Colors.black, + galleryIconColor: Colors.black, + ) + ) + ... +) +``` + +16. Add `ChatViewState` customisations. +```dart +ChatView( + ... + chatViewStateConfig: ChatViewStateConfiguration( + loadingWidgetConfig: ChatViewStateWidgetConfiguration( + loadingIndicatorColor: Colors.pink, + ), + onReloadButtonTap: () {}, + ), + ... +) +``` + +17. Setting auto scroll and highlight config with `RepliedMsgAutoScrollConfig` class. +```dart +ChatView( + ... + repliedMsgAutoScrollConfig: RepliedMsgAutoScrollConfig( + enableHighlightRepliedMsg: true, + highlightColor: Colors.grey, + highlightScale: 1.1, + ) + ... +) +``` + +18. Callback when a user starts/stops typing in `TextFieldConfiguration` + +```dart +ChatView( + ... + sendMessageConfig: SendMessageConfiguration( + + textFieldConfig: TextFieldConfiguration( + onMessageTyping: (status) { + // send composing/composed status to other client + // your code goes here + }, + + + /// After typing stopped, the threshold time after which the composing + /// status to be changed to [TypeWriterStatus.typed]. + /// Default is 1 second. + compositionThresholdTime: const Duration(seconds: 1), + + ), + ... + ) +) +``` + +19. Passing customReceipts builder or handling stuffs related receipts see `ReceiptsWidgetConfig` in outgoingChatBubbleConfig. + +```dart +ChatView( + ... + featureActiveConfig: const FeatureActiveConfig( + /// Controls the visibility of message seen ago receipts default is true + lastSeenAgoBuilderVisibility: false, + /// Controls the visibility of the message [receiptsBuilder] + receiptsBuilderVisibility: false), + ChatBubbleConfiguration( + inComingChatBubbleConfig: ChatBubble( + onMessageRead: (message) { + /// send your message reciepts to the other client + debugPrint('Message Read'); + }, + + ), + outgoingChatBubbleConfig: ChatBubble( + receiptsWidgetConfig: ReceiptsWidgetConfig( + /// custom receipts builder + receiptsBuilder: _customReceiptsBuilder, + /// whether to display receipts in all + /// message or just at the last one just like instagram + showReceiptsIn: ShowReceiptsIn.lastMessage + ), + ), + ), + + ... + +) +``` + +20. Flag `enableOtherUserName` to hide user name in chat. + +```dart +ChatView( + ... + featureActiveConfig: const FeatureActiveConfig( + enableOtherUserName: false, + ), + ... + +) +``` + +21. Added report button for receiver message and update `onMoreTap` and `onReportTap` callbacks. + +```dart +ChatView( + ... + replyPopupConfig: ReplyPopupConfiguration( + onReportTap: (Message message) { + debugPrint('Message: $message'); + }, + onMoreTap: (Message message, bool sentByCurrentUser) { + debugPrint('Message : $message'); + }, + ), + ... +) +``` + +22. Added `emojiPickerSheetConfig` for configuration of emoji picker sheet. + +```dart +ChatView( + ... + emojiPickerSheetConfig: Config( + emojiViewConfig: EmojiViewConfig( + columns: 7, + emojiSizeMax: 32, + recentsLimit: 28, + backgroundColor: Colors.white, + ), + categoryViewConfig: const CategoryViewConfig( + initCategory: Category.RECENT, + recentTabBehavior: RecentTabBehavior.NONE, + ), + ... + +) +``` + +23. Configure the styling & audio recording quality using `VoiceRecordingConfiguration` in sendMessageConfig. + +```dart +ChatView( + ... + sendMessageConfig: SendMessageConfiguration( + + voiceRecordingConfiguration: VoiceRecordingConfiguration( + iosEncoder: IosEncoder.kAudioFormatMPEG4AAC, + androidOutputFormat: AndroidOutputFormat.mpeg4, + androidEncoder: AndroidEncoder.aac, + bitRate: 128000, + sampleRate: 44100, + waveStyle: WaveStyle( + showMiddleLine: false, + waveColor: theme.waveColor ?? Colors.white, + extendWaveform: true, + ), + ), + + ... + ) +) +``` + +24. Added `enabled` to enable/disable chat text field. + +```dart +ChatView( + ... + sendMessageConfig: SendMessageConfiguration( + ... + textFieldConfig: TextFieldConfig( + enabled: true // [false] to disable text field. + ), + ... + ), + ... + +) +``` +25. Added flag `isProfilePhotoInBase64` that defines whether provided image is url or base64 data. + +```dart +final chatController = ChatController( + ... + chatUsers: [ + ChatUser( + id: '1', + name: 'Simform', + isProfilePhotoInBase64: false, + profilePhoto: 'ImageNetworkUrl', + ), + ], + ... +); + +ChatView( + ... + profileCircleConfig: const ProfileCircleConfiguration( + isProfilePhotoInBase64: false, + profileImageUrl: 'ImageNetworkUrl', + ), + ... +) +``` + +26. Added `chatSeparatorDatePattern` in `DefaultGroupSeparatorConfiguration` to separate chats with provided pattern. + +```dart +ChatView( + ... + chatBackgroundConfig: ChatBackgroundConfiguration( + ... + defaultGroupSeparatorConfig: DefaultGroupSeparatorConfiguration( + chatSeparatorDatePattern: 'MMM dd, yyyy' + ), + ... + ), + ... +) +``` + +27. Field `cancelRecordConfiguration` to provide an configuration to cancel voice record message. + +```dart +ChatView( + ... + sendMessageConfig: SendMessageConfiguration( + ... + cancelRecordConfiguration: CancelRecordConfiguration( + icon: const Icon( + Icons.cancel_outlined, + ), + onCancel: () { + debugPrint('Voice recording cancelled'); + }, + iconColor: Colors.black, + ), + ... + ), + ... + +) +``` + +28. Added callback of onTap on list of reacted users in reaction sheet `reactedUserCallback`. +```dart + +ChatView( + ... + messageConfig: MessageConfiguration( + ... + messageReactionConfig: MessageReactionConfiguration( + reactionsBottomSheetConfig: ReactionsBottomSheetConfiguration( + reactedUserCallback: (reactedUser, reaction) { + debugPrint(reaction); + }, + ), + ), + ... + ), + ... +), +``` + +29. Added a `customMessageReplyViewBuilder` to customize reply message view for custom type message. + +```dart +ChatView( + ... + messageConfig: MessageConfiguration( + customMessageBuilder: (ReplyMessage state) { + return Text( + state.message, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 12, + color: Colors.black, + ), + ); + }, + ), + ... +) +``` + +30. 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 +ChatView( + ... + appBar: ChatViewAppBar( + defaultAvatarImage: defaultAvatar, + imageType: ImageType.network, + networkImageErrorBuilder: (context, url, error) { + return Center( + child: Text('Error $error'), + ); + }, + assetImageErrorBuilder: (context, error, stackTrace) { + return Center( + child: Text('Error $error'), + ); + }, + ), + ... +), +``` + +31. Added a `customMessageReplyViewBuilder` to customize reply message view for custom type message. + +```dart +ChatView( + ... + messageConfig: MessageConfiguration( + customMessageBuilder: (ReplyMessage state) { + return Text( + state.message, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 12, + color: Colors.black, + ), + ); + }, + ), + ... +) +``` + +32. Added a `replyMessageBuilder` to customize view for the reply. + +```dart +ChatView( + ... + replyMessageBuilder: (context, state) { + return Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical( + top: Radius.circular(14), + ), + ), + margin: const EdgeInsets.only( + bottom: 17, + right: 0.4, + left: 0.4, + ), + padding: const EdgeInsets.fromLTRB(10, 10, 10, 30), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + ), + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + state.message, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ), + IconButton( + constraints: const BoxConstraints(), + padding: EdgeInsets.zero, + icon: const Icon( + Icons.close, + size: 16, + ), + onPressed: () => ChatView.closeReplyMessageView(context), + ), + ], + ), + ], + ), + ), + ); + }, + ... +) +``` + +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 suggestions 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 +), +``` + +34. Added callback `messageSorter` to sort message in `ChatBackgroundConfiguration`. + +```dart +ChatView( + ... + chatBackgroundConfig: ChatBackgroundConfiguration( + ... + messageSorter: (message1, message2) { + return message1.createdAt.compareTo(message2.createdAt); + } + ... + ), + ... +), +``` \ No newline at end of file diff --git a/doc/overview.md b/doc/overview.md new file mode 100644 index 00000000..7f25c88e --- /dev/null +++ b/doc/overview.md @@ -0,0 +1,12 @@ +![Banner](https://raw.githubusercontent.com/SimformSolutionsPvtLtd/flutter_chatview/main/preview/banner.png) + +# 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. + +For web demo visit [Chat View Example](https://simformsolutionspvtltd.github.io/flutter_chatview/). + +## Preview + +![The example app running in iOS](https://raw.githubusercontent.com/SimformSolutionsPvtLtd/flutter_chatview/main/preview/chatview.gif) \ No newline at end of file diff --git a/doc/usage.md b/doc/usage.md new file mode 100644 index 00000000..14562a45 --- /dev/null +++ b/doc/usage.md @@ -0,0 +1,51 @@ +1. Adding a chat controller. +```dart +final chatController = ChatController( + initialMessageList: messageList, + scrollController: ScrollController(), + currentUser: ChatUser(id: '1', name: 'Flutter'), + otherUsers: [ChatUser(id: '2', name: 'Simform')], +); +``` + +2. Adding a `ChatView` widget. +```dart +ChatView( + chatController: chatController, + onSendTap: onSendTap, + chatViewState: ChatViewState.hasMessages, // Add this state once data is available. +) +``` + +3. Adding a messageList with `Message` class. +```dart +List messageList = [ + Message( + id: '1', + message: "Hi", + createdAt: createdAt, + sentBy: userId, + ), + Message( + id: '2', + message: "Hello", + createdAt: createdAt, + sentBy: userId, + ), +]; +``` + +4. Adding a `onSendTap`. +```dart +void onSendTap(String message, ReplyMessage replyMessage, MessageType messageType){ + final message = Message( + id: '3', + message: "How are you", + createdAt: DateTime.now(), + senBy: currentUser.id, + replyMessage: replyMessage, + messageType: messageType, + ); + chatController.addMessage(message); +} +``` \ No newline at end of file diff --git a/lib/src/widgets/revised_group_list_view.dart b/lib/src/widgets/revised_group_list_view.dart new file mode 100644 index 00000000..0b232564 --- /dev/null +++ b/lib/src/widgets/revised_group_list_view.dart @@ -0,0 +1,438 @@ +// import 'dart:async'; +// import 'dart:collection'; +// import 'dart:math' as math; +// +// import 'package:chatview/src/models/message.dart'; +// import 'package:flutter/gestures.dart'; +// import 'package:flutter/material.dart'; +// import 'package:flutter/widgets.dart'; +// +// enum GroupedListOrder { ASC, DESC } +// /// A groupable list of widgets similar to [ListView], execpt that the +// /// items can be sectioned into groups. +// /// +// /// See [ListView.builder] +// /// +// @immutable +// class GroupedListView extends StatefulWidget { +// /// Items of which [itemBuilder] or [indexedItemBuilder] produce the list. +// final List elements; +// +// /// Defines which elements are grouped together. +// /// +// /// Function is called for each element in the list, when equal for two +// /// elements, those two belong to the same group. +// final E Function(T element) groupBy; +// +// /// Can be used to define a custom sorting for the groups. +// /// +// /// If not set groups will be sorted with their natural sorting order or their +// /// specific [Comparable] implementation. +// final int Function(E value1, E value2)? groupComparator; +// +// /// Can be used to define a custom sorting for the elements inside each group. +// /// +// /// If not set elements will be sorted with their natural sorting order or +// /// their specific [Comparable] implementation. +// final int Function(T element1, T element2)? itemComparator; +// +// /// Called to build group separators for each group. +// /// Value is always the groupBy result from the first element of the group. +// /// +// /// Will be ignored if [groupHeaderBuilder] is used. +// final Widget Function(E value)? groupSeparatorBuilder; +// +// /// Same as [groupSeparatorBuilder], will be called to build group separators +// /// for each group. +// /// The passed element is always the first element of the group. +// /// +// /// If defined [groupSeparatorBuilder] wont be used. +// final Widget Function(T element)? groupHeaderBuilder; +// +// /// Called to build children for the list with +// /// 0 <= element < elements.length. +// final Widget Function(BuildContext context, T element)? itemBuilder; +// +// /// Called to build children for the list with +// /// 0 <= element, index < elements.length +// final Widget Function(BuildContext context, T element, int index)? +// indexedItemBuilder; +// +// /// Whether the order of the list is ascending or descending. +// /// +// /// Defaults to ASC. +// final GroupedListOrder order; +// +// /// Whether the elements will be sorted or not. If not it must be done +// /// manually. +// /// +// /// Defauts to true. +// final bool sort; +// +// /// When set to true the group header of the current visible group will stick +// /// on top. +// final bool useStickyGroupSeparators; +// +// /// Called to build separators for between each item in the list. +// final Widget separator; +// +// /// Whether the group headers float over the list or occupy their own space. +// final bool floatingHeader; +// +// /// Background color of the sticky header. +// /// Only used if [floatingHeader] is false. +// final Color stickyHeaderBackgroundColor; +// +// /// An object that can be used to control the position to which this scroll +// /// view is scrolled. +// /// +// /// See [ScrollView.controller] +// final ScrollController? controller; +// +// /// The axis along which the scroll view scrolls. +// /// +// /// Defaults to [Axis.vertical]. +// final Axis scrollDirection; +// +// /// Whether this is the primary scroll view associated with the parent +// /// [PrimaryScrollController]. +// /// +// /// See [ScrollView.primary] +// final bool? primary; +// +// /// How the scroll view should respond to user input. +// /// +// /// See [ScrollView.physics]. +// final ScrollPhysics? physics; +// +// /// Whether the extent of the scroll view in the [scrollDirection] should be +// /// determined by the contents being viewed. +// /// +// /// See [ScrollView.shrinkWrap] +// final bool shrinkWrap; +// +// /// The amount of space by which to inset the children. +// final EdgeInsetsGeometry? padding; +// +// /// Whether the view scrolls in the reading direction. +// /// +// /// Defaults to false. +// /// +// /// See [ScrollView.reverse]. +// final bool reverse; +// +// /// Whether to wrap each child in an [AutomaticKeepAlive]. +// /// +// /// See [SliverChildBuilderDelegate.addAutomaticKeepAlives]. +// final bool addAutomaticKeepAlives; +// +// /// Whether to wrap each child in a [RepaintBoundary]. +// /// +// /// See [SliverChildBuilderDelegate.addRepaintBoundaries]. +// final bool addRepaintBoundaries; +// +// /// Whether to wrap each child in an [IndexedSemantics]. +// /// +// /// See [SliverChildBuilderDelegate.addSemanticIndexes]. +// final bool addSemanticIndexes; +// +// /// Creates a scrollable, linear array of widgets that are created on demand. +// /// +// /// See [ScrollView.cacheExtent] +// final double? cacheExtent; +// +// /// {@macro flutter.widgets.Clip} +// /// +// /// Defaults to [Clip.hardEdge]. +// final Clip clipBehavior; +// +// /// {@macro flutter.widgets.scrollable.dragStartBehavior} +// final DragStartBehavior dragStartBehavior; +// +// /// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will +// /// dismiss the keyboard automatically. +// final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; +// +// /// {@macro flutter.widgets.scrollable.restorationId} +// final String? restorationId; +// +// /// The number of children that will contribute semantic information. +// /// +// /// Some subtypes of [ScrollView] can infer this value automatically. For +// /// example [ListView] will use the number of widgets in the child list, +// /// while the [ListView.separated] constructor will use half that amount. +// /// +// /// For [CustomScrollView] and other types which do not receive a builder +// /// or list of widgets, the child count must be explicitly provided. If the +// /// number is unknown or unbounded this should be left unset or set to null. +// /// +// /// See also: +// /// +// /// * [SemanticsConfiguration.scrollChildCount], the corresponding semantics property. +// final int? semanticChildCount; +// +// /// If non-null, forces the children to have the given extent in the scroll +// /// direction. +// /// +// /// Specifying an [itemExtent] is more efficient than letting the children +// /// determine their own extent because the scrolling machinery can make use of +// /// the foreknowledge of the children's extent to save work, for example when +// /// the scroll position changes drastically. +// final double? itemExtent; +// +// /// Creates a [GroupedListView] +// const GroupedListView({ +// Key? key, +// required this.elements, +// required this.groupBy, +// this.groupComparator, +// this.groupSeparatorBuilder, +// this.groupHeaderBuilder, +// this.itemBuilder, +// this.indexedItemBuilder, +// this.itemComparator, +// this.order = GroupedListOrder.ASC, +// this.sort = true, +// this.useStickyGroupSeparators = false, +// this.separator = const SizedBox.shrink(), +// this.floatingHeader = false, +// this.stickyHeaderBackgroundColor = const Color(0xffF7F7F7), +// this.scrollDirection = Axis.vertical, +// this.controller, +// this.primary, +// this.physics, +// this.shrinkWrap = false, +// this.padding, +// this.reverse = false, +// this.addAutomaticKeepAlives = true, +// this.addRepaintBoundaries = true, +// this.addSemanticIndexes = true, +// this.cacheExtent, +// this.clipBehavior = Clip.hardEdge, +// this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, +// this.dragStartBehavior = DragStartBehavior.start, +// this.restorationId, +// this.semanticChildCount, +// this.itemExtent, +// }) : assert(itemBuilder != null || indexedItemBuilder != null), +// assert(groupSeparatorBuilder != null || groupHeaderBuilder != null), +// super(key: key); +// +// @override +// State createState() => _GroupedListViewState(); +// } +// +// class _GroupedListViewState extends State> { +// final StreamController _streamController = StreamController(); +// final LinkedHashMap _keys = LinkedHashMap(); +// final GlobalKey _key = GlobalKey(); +// late final ScrollController _controller; +// GlobalKey? _groupHeaderKey; +// List _sortedElements = []; +// int _topElementIndex = 0; +// RenderBox? _headerBox; +// RenderBox? _listBox; +// +// /// Fix for backwards compatability +// /// +// /// See: +// /// * https://docs.flutter.dev/development/tools/sdk/release-notes/release-notes-3.0.0#your-code +// I? _ambiguate(I? value) => value; +// +// @override +// void initState() { +// _controller = widget.controller ?? ScrollController(); +// if (widget.useStickyGroupSeparators) { +// _controller.addListener(_scrollListener); +// } +// super.initState(); +// } +// +// @override +// void dispose() { +// if (widget.useStickyGroupSeparators) { +// _controller.removeListener(_scrollListener); +// } +// if (widget.controller == null) { +// _controller.dispose(); +// } +// _streamController.close(); +// super.dispose(); +// } +// +// @override +// Widget build(BuildContext context) { +// _sortedElements = _sortElements(); +// var hiddenIndex = widget.reverse ? _sortedElements.length * 2 - 1 : 0; +// var isSeparator = widget.reverse ? (int i) => i.isOdd : (int i) => i.isEven; +// +// if (widget.reverse) { +// _ambiguate(WidgetsBinding.instance)!.addPostFrameCallback((_) { +// _scrollListener(); +// }); +// } +// +// return Stack( +// key: _key, +// alignment: Alignment.topCenter, +// children: [ +// ListView.builder( +// key: widget.key, +// scrollDirection: widget.scrollDirection, +// controller: _controller, +// primary: widget.primary, +// physics: widget.physics, +// shrinkWrap: widget.shrinkWrap, +// padding: widget.padding, +// reverse: widget.reverse, +// clipBehavior: widget.clipBehavior, +// dragStartBehavior: widget.dragStartBehavior, +// itemExtent: widget.itemExtent, +// restorationId: widget.restorationId, +// keyboardDismissBehavior: widget.keyboardDismissBehavior, +// semanticChildCount: widget.semanticChildCount, +// itemCount: _sortedElements.length * 2, +// addAutomaticKeepAlives: widget.addAutomaticKeepAlives, +// addRepaintBoundaries: widget.addRepaintBoundaries, +// addSemanticIndexes: widget.addSemanticIndexes, +// cacheExtent: widget.cacheExtent, +// itemBuilder: (context, index) { +// var actualIndex = index ~/ 2; +// print('==> Index: $index || hiddenIndex: $hiddenIndex || actualIndex: $actualIndex || sortedElements[actualIndex]: ${(_sortedElements[actualIndex] as Message).message} || isSeparator(index): ${isSeparator(index)}'); +// if (index == hiddenIndex) { +// return Opacity( +// opacity: widget.useStickyGroupSeparators ? 0 : 1, +// child: _buildGroupSeparator(_sortedElements[actualIndex]), +// ); +// } +// if (isSeparator(index)) { +// var curr = widget.groupBy(_sortedElements[actualIndex]); +// var prev = widget.groupBy( +// _sortedElements[actualIndex + (widget.reverse ? 1 : -1)]); +// print('==> prev: $prev and curr $curr'); +// if (prev != curr) { +// return _buildGroupSeparator(_sortedElements[actualIndex]); +// } +// return widget.separator; +// } +// return _buildItem(context, actualIndex); +// }, +// ), +// ], +// ); +// } +// +// Widget _buildItem(context, int index) { +// final key = _keys.putIfAbsent('$index', () => GlobalKey()); +// final value = _sortedElements[index]; +// return KeyedSubtree( +// key: key, +// child: widget.indexedItemBuilder != null +// ? widget.indexedItemBuilder!(context, value, index) +// : widget.itemBuilder!(context, value), +// ); +// } +// +// void _scrollListener() { +// _listBox ??= _key.currentContext?.findRenderObject() as RenderBox?; +// var listPos = _listBox?.localToGlobal(Offset.zero).dy ?? 0; +// _headerBox ??= +// _groupHeaderKey?.currentContext?.findRenderObject() as RenderBox?; +// var headerHeight = _headerBox?.size.height ?? 0; +// var max = double.negativeInfinity; +// var topItemKey = widget.reverse ? '${_sortedElements.length - 1}' : '0'; +// for (var entry in _keys.entries) { +// var key = entry.value; +// if (_isListItemRendered(key)) { +// var itemBox = key.currentContext!.findRenderObject() as RenderBox; +// // position of the item's top border inside the list view +// var y = itemBox.localToGlobal(Offset(0, -listPos - headerHeight)).dy; +// if (y <= headerHeight && y > max) { +// topItemKey = entry.key; +// max = y; +// } +// } +// } +// var index = math.max(int.parse(topItemKey), 0); +// if (index != _topElementIndex) { +// var curr = widget.groupBy(_sortedElements[index]); +// E prev; +// +// try { +// prev = widget.groupBy(_sortedElements[_topElementIndex]); +// } on RangeError catch (_) { +// prev = widget.groupBy(_sortedElements[0]); +// } +// +// if (prev != curr) { +// _topElementIndex = index; +// _streamController.add(_topElementIndex); +// } +// } +// } +// +// List _sortElements() { +// var elements = [...widget.elements]; +// if (widget.sort && elements.isNotEmpty) { +// elements.sort((e1, e2) { +// int? compareResult; +// // compare groups +// if (widget.groupComparator != null) { +// compareResult = +// widget.groupComparator!(widget.groupBy(e1), widget.groupBy(e2)); +// } +// else if (widget.groupBy(e1) is Comparable) { +// compareResult = (widget.groupBy(e1) as Comparable) +// .compareTo(widget.groupBy(e2) as Comparable); +// } +// // compare elements inside group +// if (compareResult == null || compareResult == 0) { +// if (widget.itemComparator != null) { +// compareResult = widget.itemComparator!(e1, e2); +// } else if (e1 is Comparable) { +// compareResult = e1.compareTo(e2); +// } +// } +// return compareResult!; +// }); +// if (widget.order == GroupedListOrder.DESC) { +// elements = elements.reversed.toList(); +// } +// } +// return elements; +// } +// +// Widget _showFixedGroupHeader(int topElementIndex) { +// _groupHeaderKey = GlobalKey(); +// if (widget.useStickyGroupSeparators && _sortedElements.isNotEmpty) { +// T topElement; +// +// try { +// topElement = _sortedElements[topElementIndex]; +// } on RangeError catch (_) { +// topElement = _sortedElements[0]; +// } +// +// return Container( +// key: _groupHeaderKey, +// color: +// widget.floatingHeader ? null : widget.stickyHeaderBackgroundColor, +// width: widget.floatingHeader ? null : MediaQuery.of(context).size.width, +// child: _buildGroupSeparator(topElement), +// ); +// } +// return Container(); +// } +// +// bool _isListItemRendered(GlobalKey> key) { +// return key.currentContext != null && +// key.currentContext!.findRenderObject() != null; +// } +// +// Widget _buildGroupSeparator(T element) { +// if (widget.groupHeaderBuilder == null) { +// return widget.groupSeparatorBuilder!(widget.groupBy(element)); +// } +// return widget.groupHeaderBuilder!(element); +// } +// }