From 953ae84f0e2a437f9c448640e8bf1ef05b89350a Mon Sep 17 00:00:00 2001 From: g0dzillaa <127401074+g0dzillaa@users.noreply.github.com> Date: Sun, 16 Apr 2023 07:28:36 +0930 Subject: [PATCH 01/24] added `autocorrect` and `enableSuggestions` props (#422) * added `autocorrect` and `enableSuggestions` props * added `enabled` props to `InputOptions` --- lib/src/widgets/input/input.dart | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/src/widgets/input/input.dart b/lib/src/widgets/input/input.dart index 8802d13ab..e0459a882 100644 --- a/lib/src/widgets/input/input.dart +++ b/lib/src/widgets/input/input.dart @@ -181,6 +181,9 @@ class _InputState extends State { child: Padding( padding: textPadding, child: TextField( + enabled: widget.options.enabled, + autocorrect: widget.options.autocorrect, + enableSuggestions: widget.options.enableSuggestions, controller: _textController, cursorColor: InheritedChatTheme.of(context) .theme @@ -248,6 +251,9 @@ class InputOptions { this.onTextFieldTap, this.sendButtonVisibilityMode = SendButtonVisibilityMode.editing, this.textEditingController, + this.autocorrect = true, + this.enableSuggestions = true, + this.enabled = true, }); /// Controls the [Input] clear behavior. Defaults to [InputClearMode.always]. @@ -271,4 +277,13 @@ class InputOptions { /// you can create your own [InputTextFieldController] (imported from this lib) /// and pass it here. final TextEditingController? textEditingController; + + /// Controls the [TextInput] autocorrect behavior. Defaults to [true]. + final bool autocorrect; + + /// Controls the [TextInput] enableSuggestions behavior. Defaults to [true]. + final bool enableSuggestions; + + /// Controls the [TextInput] enabled behavior. Defaults to [true]. + final bool enabled; } From 3472f9aba10902f692085f26fd06da553e4312ed Mon Sep 17 00:00:00 2001 From: LearningBot Date: Sun, 16 Apr 2023 05:59:03 +0800 Subject: [PATCH 02/24] fix example build (#415) --- example/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/pubspec.yaml b/example/pubspec.yaml index a5d864bfd..2d29e3964 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -70,7 +70,7 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - - messages.json + - assets/messages.json # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware From c20677f49cdc0bf94d57b43db2f302bd6041d437 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sun, 16 Apr 2023 00:59:30 +0300 Subject: [PATCH 03/24] Update dependencies and pubspec assets for example project (#402) --- example/pubspec.yaml | 7 +++---- pubspec.yaml | 6 +++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 2d29e3964..04b39dd9d 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -3,7 +3,7 @@ description: A new Flutter project. # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +publish_to: "none" # Remove this line if you wish to publish to pub.dev # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 @@ -20,7 +20,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: '>=2.18.0 <3.0.0' + sdk: ">=2.18.0 <3.0.0" # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions @@ -40,7 +40,7 @@ dependencies: path: ../ http: ^0.13.5 image_picker: ^0.8.6 - intl: ^0.17.0 + intl: ^0.18.0 mime: ^1.0.2 open_filex: ^4.2.2 path_provider: ^2.0.11 @@ -62,7 +62,6 @@ dev_dependencies: # The following section is specific to Flutter packages. flutter: - # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. diff --git a/pubspec.yaml b/pubspec.yaml index c57da4ab9..178c0704b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,8 +7,8 @@ homepage: https://flyer.chat repository: https://github.com/flyerhq/flutter_chat_ui environment: - sdk: '>=2.18.0 <3.0.0' - flutter: '>=2.0.0' + sdk: ">=2.18.0 <3.0.0" + flutter: ">=2.0.0" dependencies: diffutil_dart: ^3.0.0 @@ -18,7 +18,7 @@ dependencies: flutter_chat_types: ^3.6.0 flutter_link_previewer: ^3.2.0 flutter_parsed_text: ^2.2.1 - intl: ^0.17.0 + intl: ^0.18.0 meta: ^1.8.0 photo_view: ^0.14.0 scroll_to_index: ^3.0.1 From 9ef2e2a54815a3232f92320864597699144eb082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A5=98=EC=A7=80=EC=8A=B9?= <8514199@gmail.com> Date: Sun, 16 Apr 2023 07:02:16 +0900 Subject: [PATCH 04/24] update (#399) From 6c4e68396a7aac4128c6bf6cb2fb5f96544ba82c Mon Sep 17 00:00:00 2001 From: Sergei Date: Sun, 16 Apr 2023 01:04:33 +0300 Subject: [PATCH 05/24] Add "keyboardType" to the InputOptions (#376) Add an ability to control a text input field's keyboard type (multiline/number/email/etc.). --- lib/src/widgets/input/input.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/src/widgets/input/input.dart b/lib/src/widgets/input/input.dart index e0459a882..1c6573866 100644 --- a/lib/src/widgets/input/input.dart +++ b/lib/src/widgets/input/input.dart @@ -205,7 +205,7 @@ class _InputState extends State { InheritedL10n.of(context).l10n.inputPlaceholder, ), focusNode: _inputFocusNode, - keyboardType: TextInputType.multiline, + keyboardType: widget.options.keyboardType, maxLines: 5, minLines: 1, onChanged: widget.options.onTextChanged, @@ -247,6 +247,7 @@ class _InputState extends State { class InputOptions { const InputOptions({ this.inputClearMode = InputClearMode.always, + this.keyboardType = TextInputType.multiline, this.onTextChanged, this.onTextFieldTap, this.sendButtonVisibilityMode = SendButtonVisibilityMode.editing, @@ -258,6 +259,9 @@ class InputOptions { /// Controls the [Input] clear behavior. Defaults to [InputClearMode.always]. final InputClearMode inputClearMode; + + /// Controls the [Input] keyboard type. Defaults to [TextInputType.multiline]. + final TextInputType keyboardType; /// Will be called whenever the text inside [TextField] changes. final void Function(String)? onTextChanged; From 84c8b15b031420249b0bf055cfd33b46cf7152b3 Mon Sep 17 00:00:00 2001 From: Mark Gil Libres Date: Wed, 3 May 2023 07:14:35 +1000 Subject: [PATCH 06/24] Upgrade packages (#426) * upgrade deps to latest * update packages --- example/ios/Podfile.lock | 19 ++++++++++--------- example/ios/Runner.xcodeproj/project.pbxproj | 4 +++- example/pubspec.yaml | 12 ++++++------ pubspec.yaml | 4 ++-- 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 330b0b313..214c3887b 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -38,8 +38,9 @@ PODS: - Flutter - open_filex (0.0.2): - Flutter - - path_provider_ios (0.0.1): + - path_provider_foundation (0.0.1): - Flutter + - FlutterMacOS - SDWebImage (5.13.4): - SDWebImage/Core (= 5.13.4) - SDWebImage/Core (5.13.4) @@ -52,7 +53,7 @@ DEPENDENCIES: - Flutter (from `Flutter`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - open_filex (from `.symlinks/plugins/open_filex/ios`) - - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) SPEC REPOS: @@ -71,23 +72,23 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/image_picker_ios/ios" open_filex: :path: ".symlinks/plugins/open_filex/ios" - path_provider_ios: - :path: ".symlinks/plugins/path_provider_ios/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 - file_picker: 817ab1d8cd2da9d2da412a417162deee3500fc95 + file_picker: ce3938a0df3cc1ef404671531facef740d03f920 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 - image_picker_ios: b786a5dcf033a8336a657191401bfdf12017dabb + image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5 open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4 - path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 + path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9 SDWebImage: e5cc87bf736e60f49592f307bdf9e157189298a3 SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 - url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de + url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3 -COCOAPODS: 1.11.3 +COCOAPODS: 1.12.0 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 77728927a..2e0a30033 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -200,6 +200,7 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -236,6 +237,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 04b39dd9d..b45b24660 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -32,19 +32,19 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.5 - file_picker: ^5.2.2 + file_picker: ^5.2.9 flutter: sdk: flutter flutter_chat_types: ^3.6.0 flutter_chat_ui: path: ../ http: ^0.13.5 - image_picker: ^0.8.6 + image_picker: ^0.8.7+3 intl: ^0.18.0 - mime: ^1.0.2 - open_filex: ^4.2.2 - path_provider: ^2.0.11 - uuid: ^3.0.6 + mime: ^1.0.4 + open_filex: ^4.3.2 + path_provider: ^2.0.14 + uuid: ^3.0.7 dev_dependencies: dart_code_metrics: ^4.21.3 diff --git a/pubspec.yaml b/pubspec.yaml index 178c0704b..16a9224b3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,8 +22,8 @@ dependencies: meta: ^1.8.0 photo_view: ^0.14.0 scroll_to_index: ^3.0.1 - url_launcher: ^6.1.6 - visibility_detector: ^0.3.3 + url_launcher: ^6.1.10 + visibility_detector: ^0.4.0+2 dev_dependencies: dart_code_metrics: ^4.21.3 From deb05bd0ada9f8a275f0a77e34e997c4f098e583 Mon Sep 17 00:00:00 2001 From: Olle Ekberg <31688577+OlleEkberg@users.noreply.github.com> Date: Tue, 16 May 2023 13:26:29 +0200 Subject: [PATCH 07/24] added swedish localizations (#435) --- lib/src/chat_l10n.dart | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/src/chat_l10n.dart b/lib/src/chat_l10n.dart index 7e90cd210..7c6556b0f 100644 --- a/lib/src/chat_l10n.dart +++ b/lib/src/chat_l10n.dart @@ -224,3 +224,19 @@ class ChatL10nZhTW extends ChatL10n { super.unreadMessagesLabel = '未讀訊息', }); } + +/// Swedish l10n which extends [ChatL10n]. +@immutable +class ChatL10nSe extends ChatL10n { + /// Creates Swedish l10n. Use this constructor if you want to + /// override only a couple of properties, otherwise create a new class + /// which extends [ChatL10n] + const ChatL10nSe({ + super.attachmentButtonAccessibilityLabel = 'Skicka media', + super.emptyChatPlaceholder = 'Här finns inga meddelanden', + super.fileButtonAccessibilityLabel = 'Fil', + super.inputPlaceholder = 'Meddelande', + super.sendButtonAccessibilityLabel = 'Skicka', + super.unreadMessagesLabel = 'Olästa meddelanden', + }); +} From b27b2ea52bfeb6d3e03e76974fb477746b031d98 Mon Sep 17 00:00:00 2001 From: 4ehealthcare <85117969+Ghazanfarrajpoot@users.noreply.github.com> Date: Tue, 16 May 2023 13:27:41 +0200 Subject: [PATCH 08/24] Update pubspec.yaml (#374) Fix Issues with latest flutter 3.3.10 and upgrade packages to support apps Co-authored-by: Alex --- pubspec.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 16a9224b3..38f2406c2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,8 +7,8 @@ homepage: https://flyer.chat repository: https://github.com/flyerhq/flutter_chat_ui environment: - sdk: ">=2.18.0 <3.0.0" - flutter: ">=2.0.0" + sdk: '>=2.18.6 <3.0.0' + flutter: '>=2.0.0' dependencies: diffutil_dart: ^3.0.0 @@ -26,7 +26,7 @@ dependencies: visibility_detector: ^0.4.0+2 dev_dependencies: - dart_code_metrics: ^4.21.3 + dart_code_metrics: ^5.3.0 flutter_lints: ^2.0.1 flutter_test: sdk: flutter From 1b2db18c4c1b58e55e51532276dc92f9724100e4 Mon Sep 17 00:00:00 2001 From: tuoku <70937274+tuoku@users.noreply.github.com> Date: Tue, 16 May 2023 14:28:44 +0300 Subject: [PATCH 09/24] added Finnish L10n (#350) Co-authored-by: Alex --- lib/src/chat_l10n.dart | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/src/chat_l10n.dart b/lib/src/chat_l10n.dart index 7c6556b0f..812628498 100644 --- a/lib/src/chat_l10n.dart +++ b/lib/src/chat_l10n.dart @@ -225,6 +225,22 @@ class ChatL10nZhTW extends ChatL10n { }); } +/// Finnish l10n which extends [ChatL10n]. +@immutable +class ChatL10nFi extends ChatL10n { + /// Creates Finnish l10n. Use this constructor if you want to + /// override only a couple of properties, otherwise create a new class + /// which extends [ChatL10n] + const ChatL10nFi({ + super.attachmentButtonAccessibilityLabel = 'Lähetä mediaa', + super.emptyChatPlaceholder = 'Ei viestejä täällä vielä', + super.fileButtonAccessibilityLabel = 'Tiedosto', + super.inputPlaceholder = 'Viesti', + super.sendButtonAccessibilityLabel = 'Lähetä', + super.unreadMessagesLabel = 'Lukemattomat viestit', + }); +} + /// Swedish l10n which extends [ChatL10n]. @immutable class ChatL10nSe extends ChatL10n { From 45d0b42ca3e3838fc5aaf14f1e4e7eac7d382f69 Mon Sep 17 00:00:00 2001 From: Kamil Rykowski Date: Tue, 16 May 2023 13:44:08 +0200 Subject: [PATCH 10/24] Make the nameBuilder use the User not string (#401) Co-authored-by: Kamil Rykowski --- lib/src/widgets/chat.dart | 2 +- lib/src/widgets/message/message.dart | 2 +- lib/src/widgets/message/text_message.dart | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/widgets/chat.dart b/lib/src/widgets/chat.dart index a382192cd..579a73018 100644 --- a/lib/src/widgets/chat.dart +++ b/lib/src/widgets/chat.dart @@ -218,7 +218,7 @@ class Chat extends StatefulWidget { final List messages; /// See [Message.nameBuilder]. - final Widget Function(String userId)? nameBuilder; + final Widget Function(types.User)? nameBuilder; /// See [Input.onAttachmentPressed]. final VoidCallback? onAttachmentPressed; diff --git a/lib/src/widgets/message/message.dart b/lib/src/widgets/message/message.dart index ffd8a5544..c3b0bb640 100644 --- a/lib/src/widgets/message/message.dart +++ b/lib/src/widgets/message/message.dart @@ -111,7 +111,7 @@ class Message extends StatelessWidget { final int messageWidth; /// See [TextMessage.nameBuilder]. - final Widget Function(String userId)? nameBuilder; + final Widget Function(types.User)? nameBuilder; /// See [UserAvatar.onAvatarTap]. final void Function(types.User)? onAvatarTap; diff --git a/lib/src/widgets/message/text_message.dart b/lib/src/widgets/message/text_message.dart index 1fd231a6b..a83047aa1 100644 --- a/lib/src/widgets/message/text_message.dart +++ b/lib/src/widgets/message/text_message.dart @@ -39,7 +39,7 @@ class TextMessage extends StatelessWidget { /// This is to allow custom user name builder /// By using this we can fetch newest user info based on id - final Widget Function(String userId)? nameBuilder; + final Widget Function(types.User)? nameBuilder; /// See [LinkPreview.onPreviewDataFetched]. final void Function(types.TextMessage, types.PreviewData)? @@ -155,7 +155,7 @@ class TextMessage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ if (showName) - nameBuilder?.call(message.author.id) ?? + nameBuilder?.call(message.author) ?? UserName(author: message.author), if (enlargeEmojis) if (options.isTextSelectable) From 8396139b325112d1849b73bea6e078b5a9d6f516 Mon Sep 17 00:00:00 2001 From: Alex Demchenko Date: Tue, 16 May 2023 22:25:46 +0200 Subject: [PATCH 11/24] Update dependencies --- .github/workflows/build.yaml | 2 +- LICENSE | 2 +- example/lib/main.dart | 28 +- example/pubspec.yaml | 16 +- lib/src/chat_l10n.dart | 28 +- lib/src/chat_theme.dart | 24 +- lib/src/conditional/browser_conditional.dart | 2 +- lib/src/conditional/io_conditional.dart | 2 +- lib/src/util.dart | 2 +- lib/src/widgets/chat.dart | 250 +++++++-------- lib/src/widgets/chat_list.dart | 229 +++++++------- lib/src/widgets/image_gallery.dart | 24 +- lib/src/widgets/input/input.dart | 46 +-- .../input/input_text_field_controller.dart | 2 +- lib/src/widgets/message/image_message.dart | 40 +-- lib/src/widgets/message/message.dart | 190 +++++------ lib/src/widgets/message/text_message.dart | 59 ++-- .../widgets/patched_sliver_animated_list.dart | 296 ------------------ .../widgets/state/inherited_chat_theme.dart | 6 +- lib/src/widgets/state/inherited_l10n.dart | 6 +- lib/src/widgets/state/inherited_user.dart | 6 +- lib/src/widgets/typing_indicator.dart | 106 +++---- pubspec.yaml | 16 +- 23 files changed, 531 insertions(+), 851 deletions(-) delete mode 100644 lib/src/widgets/patched_sliver_animated_list.dart diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index a79c0390c..180068d40 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -21,5 +21,5 @@ jobs: - name: Install dependencies run: flutter pub get - - run: flutter format . --set-exit-if-changed + - run: dart format . --set-exit-if-changed - run: flutter analyze --no-pub diff --git a/LICENSE b/LICENSE index cd914dd90..babf35767 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2022 Oleksandr Demchenko + Copyright 2023 Oleksandr Demchenko Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/example/lib/main.dart b/example/lib/main.dart index 37dd60384..1266aceab 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -46,20 +46,6 @@ class _ChatPageState extends State { _loadMessages(); } - @override - Widget build(BuildContext context) => Scaffold( - body: Chat( - messages: _messages, - onAttachmentPressed: _handleAttachmentPressed, - onMessageTap: _handleMessageTap, - onPreviewDataFetched: _handlePreviewDataFetched, - onSendPressed: _handleSendPressed, - showUserAvatars: true, - showUserNames: true, - user: _user, - ), - ); - void _addMessage(types.Message message) { setState(() { _messages.insert(0, message); @@ -235,4 +221,18 @@ class _ChatPageState extends State { _messages = messages; }); } + + @override + Widget build(BuildContext context) => Scaffold( + body: Chat( + messages: _messages, + onAttachmentPressed: _handleAttachmentPressed, + onMessageTap: _handleMessageTap, + onPreviewDataFetched: _handlePreviewDataFetched, + onSendPressed: _handleSendPressed, + showUserAvatars: true, + showUserNames: true, + user: _user, + ), + ); } diff --git a/example/pubspec.yaml b/example/pubspec.yaml index b45b24660..14bb014ae 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -20,7 +20,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: ">=2.18.0 <3.0.0" + sdk: ">=2.18.0 <4.0.0" # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions @@ -32,22 +32,22 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.5 - file_picker: ^5.2.9 + file_picker: ^5.3.0 flutter: sdk: flutter - flutter_chat_types: ^3.6.0 + flutter_chat_types: ^3.6.1 flutter_chat_ui: path: ../ - http: ^0.13.5 - image_picker: ^0.8.7+3 - intl: ^0.18.0 + http: ^0.13.6 + image_picker: ^0.8.7+5 + intl: ^0.18.1 mime: ^1.0.4 open_filex: ^4.3.2 - path_provider: ^2.0.14 + path_provider: ^2.0.15 uuid: ^3.0.7 dev_dependencies: - dart_code_metrics: ^4.21.3 + dart_code_metrics: ^5.7.3 # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is # activated in the `analysis_options.yaml` file located at the root of your diff --git a/lib/src/chat_l10n.dart b/lib/src/chat_l10n.dart index 812628498..f0e87c208 100644 --- a/lib/src/chat_l10n.dart +++ b/lib/src/chat_l10n.dart @@ -38,7 +38,7 @@ abstract class ChatL10n { class ChatL10nAr extends ChatL10n { /// Creates Arabic l10n. Use this constructor if you want to /// override only a couple of properties, otherwise create a new class - /// which extends [ChatL10n] + /// which extends [ChatL10n]. const ChatL10nAr({ super.attachmentButtonAccessibilityLabel = 'إرسال الوسائط', super.emptyChatPlaceholder = 'لا يوجد رسائل هنا بعد', @@ -54,7 +54,7 @@ class ChatL10nAr extends ChatL10n { class ChatL10nDe extends ChatL10n { /// Creates German l10n. Use this constructor if you want to /// override only a couple of variables, otherwise create a new class - /// which extends [ChatL10n] + /// which extends [ChatL10n]. const ChatL10nDe({ super.attachmentButtonAccessibilityLabel = 'Medien senden', super.emptyChatPlaceholder = 'Noch keine Nachrichten', @@ -70,7 +70,7 @@ class ChatL10nDe extends ChatL10n { class ChatL10nEn extends ChatL10n { /// Creates English l10n. Use this constructor if you want to /// override only a couple of properties, otherwise create a new class - /// which extends [ChatL10n] + /// which extends [ChatL10n]. const ChatL10nEn({ super.attachmentButtonAccessibilityLabel = 'Send media', super.emptyChatPlaceholder = 'No messages here yet', @@ -86,7 +86,7 @@ class ChatL10nEn extends ChatL10n { class ChatL10nEs extends ChatL10n { /// Creates Spanish l10n. Use this constructor if you want to /// override only a couple of properties, otherwise create a new class - /// which extends [ChatL10n] + /// which extends [ChatL10n]. const ChatL10nEs({ super.attachmentButtonAccessibilityLabel = 'Enviar multimedia', super.emptyChatPlaceholder = 'Aún no hay mensajes', @@ -102,7 +102,7 @@ class ChatL10nEs extends ChatL10n { class ChatL10nKo extends ChatL10n { /// Creates Korean l10n. Use this constructor if you want to /// override only a couple of properties, otherwise create a new class - /// which extends [ChatL10n] + /// which extends [ChatL10n]. const ChatL10nKo({ super.attachmentButtonAccessibilityLabel = '미디어 보내기', super.emptyChatPlaceholder = '주고받은 메시지가 없습니다', @@ -118,7 +118,7 @@ class ChatL10nKo extends ChatL10n { class ChatL10nPl extends ChatL10n { /// Creates Polish l10n. Use this constructor if you want to /// override only a couple of properties, otherwise create a new class - /// which extends [ChatL10n] + /// which extends [ChatL10n]. const ChatL10nPl({ super.attachmentButtonAccessibilityLabel = 'Wyślij multimedia', super.emptyChatPlaceholder = 'Tu jeszcze nie ma wiadomości', @@ -134,7 +134,7 @@ class ChatL10nPl extends ChatL10n { class ChatL10nPt extends ChatL10n { /// Creates Portuguese l10n. Use this constructor if you want to /// override only a couple of properties, otherwise create a new class - /// which extends [ChatL10n] + /// which extends [ChatL10n]. const ChatL10nPt({ super.attachmentButtonAccessibilityLabel = 'Envia mídia', super.emptyChatPlaceholder = 'Ainda não há mensagens aqui', @@ -150,7 +150,7 @@ class ChatL10nPt extends ChatL10n { class ChatL10nRu extends ChatL10n { /// Creates Russian l10n. Use this constructor if you want to /// override only a couple of properties, otherwise create a new class - /// which extends [ChatL10n] + /// which extends [ChatL10n]. const ChatL10nRu({ super.attachmentButtonAccessibilityLabel = 'Отправить медиа', super.emptyChatPlaceholder = 'Пока что у вас нет сообщений', @@ -166,7 +166,7 @@ class ChatL10nRu extends ChatL10n { class ChatL10nTr extends ChatL10n { /// Creates Turkish l10n. Use this constructor if you want to /// override only a couple of properties, otherwise create a new class - /// which extends [ChatL10n] + /// which extends [ChatL10n]. const ChatL10nTr({ super.attachmentButtonAccessibilityLabel = 'Medya gönder', super.emptyChatPlaceholder = 'Henüz mesaj yok', @@ -182,7 +182,7 @@ class ChatL10nTr extends ChatL10n { class ChatL10nUk extends ChatL10n { /// Creates Ukrainian l10n. Use this constructor if you want to /// override only a couple of properties, otherwise create a new class - /// which extends [ChatL10n] + /// which extends [ChatL10n]. const ChatL10nUk({ super.attachmentButtonAccessibilityLabel = 'Надіслати медіа', super.emptyChatPlaceholder = 'Повідомлень ще немає', @@ -198,7 +198,7 @@ class ChatL10nUk extends ChatL10n { class ChatL10nZhCN extends ChatL10n { /// Creates Simplified Chinese l10n. Use this constructor if you want to /// override only a couple of properties, otherwise create a new class - /// which extends [ChatL10n] + /// which extends [ChatL10n]. const ChatL10nZhCN({ super.attachmentButtonAccessibilityLabel = '发送媒体文件', super.emptyChatPlaceholder = '暂无消息', @@ -214,7 +214,7 @@ class ChatL10nZhCN extends ChatL10n { class ChatL10nZhTW extends ChatL10n { /// Creates Traditional Chinese l10n. Use this constructor if you want to /// override only a couple of properties, otherwise create a new class - /// which extends [ChatL10n] + /// which extends [ChatL10n]. const ChatL10nZhTW({ super.attachmentButtonAccessibilityLabel = '傳送媒體', super.emptyChatPlaceholder = '還沒有訊息在這裡', @@ -230,7 +230,7 @@ class ChatL10nZhTW extends ChatL10n { class ChatL10nFi extends ChatL10n { /// Creates Finnish l10n. Use this constructor if you want to /// override only a couple of properties, otherwise create a new class - /// which extends [ChatL10n] + /// which extends [ChatL10n]. const ChatL10nFi({ super.attachmentButtonAccessibilityLabel = 'Lähetä mediaa', super.emptyChatPlaceholder = 'Ei viestejä täällä vielä', @@ -246,7 +246,7 @@ class ChatL10nFi extends ChatL10n { class ChatL10nSe extends ChatL10n { /// Creates Swedish l10n. Use this constructor if you want to /// override only a couple of properties, otherwise create a new class - /// which extends [ChatL10n] + /// which extends [ChatL10n]. const ChatL10nSe({ super.attachmentButtonAccessibilityLabel = 'Skicka media', super.emptyChatPlaceholder = 'Här finns inga meddelanden', diff --git a/lib/src/chat_theme.dart b/lib/src/chat_theme.dart index cf9e7c137..f42bf2041 100644 --- a/lib/src/chat_theme.dart +++ b/lib/src/chat_theme.dart @@ -6,7 +6,7 @@ import 'widgets/unread_header.dart'; // For internal usage only. Use values from theme itself. -/// See [ChatTheme.userAvatarNameColors] +/// See [ChatTheme.userAvatarNameColors]. const colors = [ Color(0xffff6767), Color(0xff66e0da), @@ -179,7 +179,7 @@ abstract class ChatTheme { final double messageInsetsVertical; /// Primary color of the chat used as a background of sent messages - /// and statuses + /// and statuses. final Color primaryColor; /// Text style used for displaying emojis on text messages. @@ -194,15 +194,14 @@ abstract class ChatTheme { final TextStyle? receivedMessageBodyCodeTextStyle; /// Text style used for displaying link text on received text messages. - /// Defaults to [receivedMessageBodyTextStyle] + /// Defaults to [receivedMessageBodyTextStyle]. final TextStyle? receivedMessageBodyLinkTextStyle; /// Body text style used for displaying text on different types - /// of received messages + /// of received messages. final TextStyle receivedMessageBodyTextStyle; - /// Caption text style used for displaying secondary info (e.g. file size) - /// on different types of received messages + /// Caption text style used for displaying secondary info (e.g. file size) on different types of received messages. final TextStyle receivedMessageCaptionTextStyle; /// Color of the document icon on received messages. Has no effect when @@ -242,15 +241,14 @@ abstract class ChatTheme { final TextStyle? sentMessageBodyCodeTextStyle; /// Text style used for displaying link text on sent text messages. - /// Defaults to [sentMessageBodyTextStyle] + /// Defaults to [sentMessageBodyTextStyle]. final TextStyle? sentMessageBodyLinkTextStyle; /// Body text style used for displaying text on different types - /// of sent messages + /// of sent messages. final TextStyle sentMessageBodyTextStyle; - /// Caption text style used for displaying secondary info (e.g. file size) - /// on different types of sent messages + /// Caption text style used for displaying secondary info (e.g. file size) on different types of sent messages. final TextStyle sentMessageCaptionTextStyle; /// Color of the document icon on sent messages. Has no effect when @@ -286,7 +284,7 @@ abstract class ChatTheme { final List userAvatarNameColors; /// Text style used for displaying initials on user avatar if no - /// image is provided + /// image is provided. final TextStyle userAvatarTextStyle; /// User names text style. Color will be overwritten with [userAvatarNameColors]. @@ -298,7 +296,7 @@ abstract class ChatTheme { class DefaultChatTheme extends ChatTheme { /// Creates a default chat theme. Use this constructor if you want to /// override only a couple of properties, otherwise create a new class - /// which extends [ChatTheme] + /// which extends [ChatTheme]. const DefaultChatTheme({ super.attachmentButtonIcon, super.attachmentButtonMargin, @@ -467,7 +465,7 @@ class DefaultChatTheme extends ChatTheme { class DarkChatTheme extends ChatTheme { /// Creates a dark chat theme. Use this constructor if you want to /// override only a couple of properties, otherwise create a new class - /// which extends [ChatTheme] + /// which extends [ChatTheme]. const DarkChatTheme({ super.attachmentButtonIcon, super.attachmentButtonMargin, diff --git a/lib/src/conditional/browser_conditional.dart b/lib/src/conditional/browser_conditional.dart index 2f46445ac..b047079f7 100644 --- a/lib/src/conditional/browser_conditional.dart +++ b/lib/src/conditional/browser_conditional.dart @@ -12,7 +12,7 @@ BaseConditional createConditional() => BrowserConditional(); /// A conditional for browser. class BrowserConditional extends BaseConditional { /// Returns [NetworkImage] if URI starts with http - /// otherwise returns transparent image + /// otherwise returns transparent image. @override ImageProvider getProvider(String uri, {Map? headers}) { if (uri.startsWith('http') || uri.startsWith('blob')) { diff --git a/lib/src/conditional/io_conditional.dart b/lib/src/conditional/io_conditional.dart index a81ea4107..c9dd7e60a 100644 --- a/lib/src/conditional/io_conditional.dart +++ b/lib/src/conditional/io_conditional.dart @@ -12,7 +12,7 @@ BaseConditional createConditional() => IOConditional(); /// A conditional for anything but browser. class IOConditional extends BaseConditional { /// Returns [NetworkImage] if URI starts with http - /// otherwise uses IO to create File + /// otherwise uses IO to create File. @override ImageProvider getProvider(String uri, {Map? headers}) { if (uri.startsWith('http')) { diff --git a/lib/src/util.dart b/lib/src/util.dart index e0ffd282e..e7128eb0a 100644 --- a/lib/src/util.dart +++ b/lib/src/util.dart @@ -53,7 +53,7 @@ String getUserName(types.User user) => '${user.firstName ?? ''} ${user.lastName ?? ''}'.trim(); /// Returns formatted date used as a divider between different days in the -/// chat history +/// chat history. String getVerboseDateTimeRepresentation( DateTime dateTime, { DateFormat? dateFormat, diff --git a/lib/src/widgets/chat.dart b/lib/src/widgets/chat.dart index 579a73018..a8a992723 100644 --- a/lib/src/widgets/chat.dart +++ b/lib/src/widgets/chat.dart @@ -114,18 +114,10 @@ class Chat extends StatefulWidget { /// See [Message.bubbleRtlAlignment]. final BubbleRtlAlignment? bubbleRtlAlignment; - /// Allows you to replace the default Input widget e.g. if you want to create - /// a channel view. If you're looking for the bottom widget added to the chat - /// list, see [listBottomWidget] instead. + /// Allows you to replace the default Input widget e.g. if you want to create a channel view. If you're looking for the bottom widget added to the chat list, see [listBottomWidget] instead. final Widget? customBottomWidget; - /// If [dateFormat], [dateLocale] and/or [timeFormat] is not enough to - /// customize date headers in your case, use this to return an arbitrary - /// string based on a [DateTime] of a particular message. Can be helpful to - /// return "Today" if [DateTime] is today. IMPORTANT: this will replace - /// all default date headers, so you must handle all cases yourself, like - /// for example today, yesterday and before. Or you can just return the same - /// date header for any message. + /// If [dateFormat], [dateLocale] and/or [timeFormat] is not enough to customize date headers in your case, use this to return an arbitrary string based on a [DateTime] of a particular message. Can be helpful to return "Today" if [DateTime] is today. IMPORTANT: this will replace all default date headers, so you must handle all cases yourself, like for example today, yesterday and before. Or you can just return the same date header for any message. final String Function(DateTime)? customDateHeaderText; /// See [Message.customMessageBuilder]. @@ -136,11 +128,7 @@ class Chat extends StatefulWidget { final Widget Function(types.Message message, {required BuildContext context})? customStatusBuilder; - /// Allows you to customize the date format. IMPORTANT: only for the date, - /// do not return time here. See [timeFormat] to customize the time format. - /// [dateLocale] will be ignored if you use this, so if you want a localized date - /// make sure you initialize your [DateFormat] with a locale. See [customDateHeaderText] - /// for more customization. + /// Allows you to customize the date format. IMPORTANT: only for the date, do not return time here. See [timeFormat] to customize the time format. [dateLocale] will be ignored if you use this, so if you want a localized date make sure you initialize your [DateFormat] with a locale. See [customDateHeaderText] for more customization. final DateFormat? dateFormat; /// Custom date header builder gives ability to customize date header widget. @@ -296,11 +284,7 @@ class Chat extends StatefulWidget { /// properties, see more here [DefaultChatTheme]. final ChatTheme theme; - /// Allows you to customize the time format. IMPORTANT: only for the time, - /// do not return date here. See [dateFormat] to customize the date format. - /// [dateLocale] will be ignored if you use this, so if you want a localized time - /// make sure you initialize your [DateFormat] with a locale. See [customDateHeaderText] - /// for more customization. + /// Allows you to customize the time format. IMPORTANT: only for the time, do not return date here. See [dateFormat] to customize the date format. [dateLocale] will be ignored if you use this, so if you want a localized time make sure you initialize your [DateFormat] with a locale. See [customDateHeaderText] for more customization. final DateFormat? timeFormat; /// Used to show typing users with indicator. See [TypingIndicatorOptions]. @@ -350,40 +334,6 @@ class ChatState extends State { didUpdateWidget(widget); } - @override - void didUpdateWidget(covariant Chat oldWidget) { - super.didUpdateWidget(oldWidget); - - if (widget.messages.isNotEmpty) { - final result = calculateChatMessages( - widget.messages, - widget.user, - customDateHeaderText: widget.customDateHeaderText, - dateFormat: widget.dateFormat, - dateHeaderThreshold: widget.dateHeaderThreshold, - dateIsUtc: widget.dateIsUtc, - dateLocale: widget.dateLocale, - groupMessagesThreshold: widget.groupMessagesThreshold, - lastReadMessageId: widget.scrollToUnreadOptions.lastReadMessageId, - showUserNames: widget.showUserNames, - timeFormat: widget.timeFormat, - ); - - _chatMessages = result[0] as List; - _gallery = result[1] as List; - - _refreshAutoScrollMapping(); - _maybeScrollToFirstUnread(); - } - } - - @override - void dispose() { - _galleryPageController?.dispose(); - _scrollController.dispose(); - super.dispose(); - } - /// Scroll to the unread header. void scrollToUnreadHeader() { final unreadHeaderIndex = _autoScrollIndexById[_unreadHeaderId]; @@ -402,85 +352,6 @@ class ChatState extends State { duration: duration ?? scrollAnimationDuration, ); - @override - Widget build(BuildContext context) => InheritedUser( - user: widget.user, - child: InheritedChatTheme( - theme: widget.theme, - child: InheritedL10n( - l10n: widget.l10n, - child: Stack( - children: [ - Container( - color: widget.theme.backgroundColor, - child: Column( - children: [ - Flexible( - child: widget.messages.isEmpty - ? SizedBox.expand( - child: _emptyStateBuilder(), - ) - : GestureDetector( - onTap: () { - FocusManager.instance.primaryFocus?.unfocus(); - widget.onBackgroundTap?.call(); - }, - child: LayoutBuilder( - builder: ( - BuildContext context, - BoxConstraints constraints, - ) => - ChatList( - bottomWidget: widget.listBottomWidget, - bubbleRtlAlignment: - widget.bubbleRtlAlignment!, - isLastPage: widget.isLastPage, - itemBuilder: (Object item, int? index) => - _messageBuilder( - item, - constraints, - index, - ), - items: _chatMessages, - keyboardDismissBehavior: - widget.keyboardDismissBehavior, - onEndReached: widget.onEndReached, - onEndReachedThreshold: - widget.onEndReachedThreshold, - scrollController: _scrollController, - scrollPhysics: widget.scrollPhysics, - typingIndicatorOptions: - widget.typingIndicatorOptions, - useTopSafeAreaInset: - widget.useTopSafeAreaInset ?? isMobile, - ), - ), - ), - ), - widget.customBottomWidget ?? - Input( - isAttachmentUploading: widget.isAttachmentUploading, - onAttachmentPressed: widget.onAttachmentPressed, - onSendPressed: widget.onSendPressed, - options: widget.inputOptions, - ), - ], - ), - ), - if (_isImageViewVisible) - ImageGallery( - imageHeaders: widget.imageHeaders, - images: _gallery, - pageController: _galleryPageController!, - onClosePressed: _onCloseGalleryPressed, - options: widget.imageGalleryOptions, - ), - ], - ), - ), - ), - ); - Widget _emptyStateBuilder() => widget.emptyState ?? Container( @@ -645,4 +516,117 @@ class ChatState extends State { i++; } } + + @override + void didUpdateWidget(covariant Chat oldWidget) { + super.didUpdateWidget(oldWidget); + + if (widget.messages.isNotEmpty) { + final result = calculateChatMessages( + widget.messages, + widget.user, + customDateHeaderText: widget.customDateHeaderText, + dateFormat: widget.dateFormat, + dateHeaderThreshold: widget.dateHeaderThreshold, + dateIsUtc: widget.dateIsUtc, + dateLocale: widget.dateLocale, + groupMessagesThreshold: widget.groupMessagesThreshold, + lastReadMessageId: widget.scrollToUnreadOptions.lastReadMessageId, + showUserNames: widget.showUserNames, + timeFormat: widget.timeFormat, + ); + + _chatMessages = result[0] as List; + _gallery = result[1] as List; + + _refreshAutoScrollMapping(); + _maybeScrollToFirstUnread(); + } + } + + @override + void dispose() { + _galleryPageController?.dispose(); + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => InheritedUser( + user: widget.user, + child: InheritedChatTheme( + theme: widget.theme, + child: InheritedL10n( + l10n: widget.l10n, + child: Stack( + children: [ + Container( + color: widget.theme.backgroundColor, + child: Column( + children: [ + Flexible( + child: widget.messages.isEmpty + ? SizedBox.expand( + child: _emptyStateBuilder(), + ) + : GestureDetector( + onTap: () { + FocusManager.instance.primaryFocus?.unfocus(); + widget.onBackgroundTap?.call(); + }, + child: LayoutBuilder( + builder: ( + BuildContext context, + BoxConstraints constraints, + ) => + ChatList( + bottomWidget: widget.listBottomWidget, + bubbleRtlAlignment: + widget.bubbleRtlAlignment!, + isLastPage: widget.isLastPage, + itemBuilder: (Object item, int? index) => + _messageBuilder( + item, + constraints, + index, + ), + items: _chatMessages, + keyboardDismissBehavior: + widget.keyboardDismissBehavior, + onEndReached: widget.onEndReached, + onEndReachedThreshold: + widget.onEndReachedThreshold, + scrollController: _scrollController, + scrollPhysics: widget.scrollPhysics, + typingIndicatorOptions: + widget.typingIndicatorOptions, + useTopSafeAreaInset: + widget.useTopSafeAreaInset ?? isMobile, + ), + ), + ), + ), + widget.customBottomWidget ?? + Input( + isAttachmentUploading: widget.isAttachmentUploading, + onAttachmentPressed: widget.onAttachmentPressed, + onSendPressed: widget.onSendPressed, + options: widget.inputOptions, + ), + ], + ), + ), + if (_isImageViewVisible) + ImageGallery( + imageHeaders: widget.imageHeaders, + images: _gallery, + pageController: _galleryPageController!, + onClosePressed: _onCloseGalleryPressed, + options: widget.imageGalleryOptions, + ), + ], + ), + ), + ), + ); } diff --git a/lib/src/widgets/chat_list.dart b/lib/src/widgets/chat_list.dart index 6f06b67e6..8c3ab0df9 100644 --- a/lib/src/widgets/chat_list.dart +++ b/lib/src/widgets/chat_list.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_chat_types/flutter_chat_types.dart' as types; import '../models/bubble_rtl_alignment.dart'; -import 'patched_sliver_animated_list.dart'; import 'state/inherited_chat_theme.dart'; import 'state/inherited_user.dart'; import 'typing_indicator.dart'; @@ -52,11 +51,7 @@ class ChatList extends StatefulWidget { /// A representation of how a [ScrollView] should dismiss the on-screen keyboard. final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; - /// Used for pagination (infinite scroll) together with [onEndReached]. - /// Can be anything from 0 to 1, where 0 is immediate load of the next page - /// as soon as scroll starts, and 1 is load of the next page only if scrolled - /// to the very end of the list. Default value is 0.75, e.g. start loading - /// next page when scrolled through about 3/4 of the available content. + /// Used for pagination (infinite scroll) together with [onEndReached]. Can be anything from 0 to 1, where 0 is immediate load of the next page as soon as scroll starts, and 1 is load of the next page only if scrolled to the very end of the list. Default value is 0.75, e.g. start loading next page when scrolled through about 3/4 of the available content. final double? onEndReachedThreshold; /// Scroll controller for the main [CustomScrollView]. Also used to auto scroll @@ -88,8 +83,8 @@ class _ChatListState extends State bool _indicatorOnScrollStatus = false; bool _isNextPageLoading = false; - final GlobalKey _listKey = - GlobalKey(); + final GlobalKey _listKey = + GlobalKey(); late List _oldData = List.from(widget.items); @override @@ -99,6 +94,114 @@ class _ChatListState extends State didUpdateWidget(widget); } + void _calculateDiffs(List oldList) async { + final diffResult = calculateListDiff( + oldList, + widget.items, + equalityChecker: (item1, item2) { + if (item1 is Map && item2 is Map) { + final message1 = item1['message']! as types.Message; + final message2 = item2['message']! as types.Message; + + return message1.id == message2.id; + } else { + return item1 == item2; + } + }, + ); + + for (final update in diffResult.getUpdates(batch: false)) { + update.when( + insert: (pos, count) { + _listKey.currentState?.insertItem(pos); + }, + remove: (pos, count) { + final item = oldList[pos]; + _listKey.currentState?.removeItem( + pos, + (_, animation) => _removedMessageBuilder(item, animation), + ); + }, + change: (pos, payload) {}, + move: (from, to) {}, + ); + } + + _scrollToBottomIfNeeded(oldList); + + _oldData = List.from(widget.items); + } + + Widget _newMessageBuilder(int index, Animation animation) { + try { + final item = _oldData[index]; + + return SizeTransition( + key: _valueKeyForItem(item), + axisAlignment: -1, + sizeFactor: animation.drive(CurveTween(curve: Curves.easeOutQuad)), + child: widget.itemBuilder(item, index), + ); + } catch (e) { + return const SizedBox(); + } + } + + Widget _removedMessageBuilder(Object item, Animation animation) => + SizeTransition( + key: _valueKeyForItem(item), + axisAlignment: -1, + sizeFactor: animation.drive(CurveTween(curve: Curves.easeInQuad)), + child: FadeTransition( + opacity: animation.drive(CurveTween(curve: Curves.easeInQuad)), + child: widget.itemBuilder(item, null), + ), + ); + + // Hacky solution to reconsider. + void _scrollToBottomIfNeeded(List oldList) { + try { + // Take index 1 because there is always a spacer on index 0. + final oldItem = oldList[1]; + final item = widget.items[1]; + + if (oldItem is Map && item is Map) { + final oldMessage = oldItem['message']! as types.Message; + final message = item['message']! as types.Message; + + // Compare items to fire only on newly added messages. + if (oldMessage.id != message.id) { + // Run only for sent message. + if (message.author.id == InheritedUser.of(context).user.id) { + // Delay to give some time for Flutter to calculate new + // size after new message was added. + Future.delayed(const Duration(milliseconds: 100), () { + if (widget.scrollController.hasClients) { + widget.scrollController.animateTo( + 0, + duration: const Duration(milliseconds: 200), + curve: Curves.easeInQuad, + ); + } + }); + } + } + } + } catch (e) { + // Do nothing if there are no items. + } + } + + Key? _valueKeyForItem(Object item) => + _mapMessage(item, (message) => ValueKey(message.id)); + + T? _mapMessage(Object maybeMessage, T Function(types.Message) f) { + if (maybeMessage is Map) { + return f(maybeMessage['message'] as types.Message); + } + return null; + } + @override void didUpdateWidget(covariant ChatList oldWidget) { super.didUpdateWidget(oldWidget); @@ -178,7 +281,7 @@ class _ChatListState extends State ), SliverPadding( padding: const EdgeInsets.only(bottom: 4), - sliver: PatchedSliverAnimatedList( + sliver: SliverAnimatedList( findChildIndexCallback: (Key key) { if (key is ValueKey) { final newIndex = widget.items.indexWhere( @@ -235,112 +338,4 @@ class _ChatListState extends State ], ), ); - - void _calculateDiffs(List oldList) async { - final diffResult = calculateListDiff( - oldList, - widget.items, - equalityChecker: (item1, item2) { - if (item1 is Map && item2 is Map) { - final message1 = item1['message']! as types.Message; - final message2 = item2['message']! as types.Message; - - return message1.id == message2.id; - } else { - return item1 == item2; - } - }, - ); - - for (final update in diffResult.getUpdates(batch: false)) { - update.when( - insert: (pos, count) { - _listKey.currentState?.insertItem(pos); - }, - remove: (pos, count) { - final item = oldList[pos]; - _listKey.currentState?.removeItem( - pos, - (_, animation) => _removedMessageBuilder(item, animation), - ); - }, - change: (pos, payload) {}, - move: (from, to) {}, - ); - } - - _scrollToBottomIfNeeded(oldList); - - _oldData = List.from(widget.items); - } - - Widget _newMessageBuilder(int index, Animation animation) { - try { - final item = _oldData[index]; - - return SizeTransition( - key: _valueKeyForItem(item), - axisAlignment: -1, - sizeFactor: animation.drive(CurveTween(curve: Curves.easeOutQuad)), - child: widget.itemBuilder(item, index), - ); - } catch (e) { - return const SizedBox(); - } - } - - Widget _removedMessageBuilder(Object item, Animation animation) => - SizeTransition( - key: _valueKeyForItem(item), - axisAlignment: -1, - sizeFactor: animation.drive(CurveTween(curve: Curves.easeInQuad)), - child: FadeTransition( - opacity: animation.drive(CurveTween(curve: Curves.easeInQuad)), - child: widget.itemBuilder(item, null), - ), - ); - - // Hacky solution to reconsider. - void _scrollToBottomIfNeeded(List oldList) { - try { - // Take index 1 because there is always a spacer on index 0. - final oldItem = oldList[1]; - final item = widget.items[1]; - - if (oldItem is Map && item is Map) { - final oldMessage = oldItem['message']! as types.Message; - final message = item['message']! as types.Message; - - // Compare items to fire only on newly added messages. - if (oldMessage.id != message.id) { - // Run only for sent message. - if (message.author.id == InheritedUser.of(context).user.id) { - // Delay to give some time for Flutter to calculate new - // size after new message was added - Future.delayed(const Duration(milliseconds: 100), () { - if (widget.scrollController.hasClients) { - widget.scrollController.animateTo( - 0, - duration: const Duration(milliseconds: 200), - curve: Curves.easeInQuad, - ); - } - }); - } - } - } - } catch (e) { - // Do nothing if there are no items. - } - } - - Key? _valueKeyForItem(Object item) => - _mapMessage(item, (message) => ValueKey(message.id)); - - T? _mapMessage(Object maybeMessage, T Function(types.Message) f) { - if (maybeMessage is Map) { - return f(maybeMessage['message'] as types.Message); - } - return null; - } } diff --git a/lib/src/widgets/image_gallery.dart b/lib/src/widgets/image_gallery.dart index d5371c539..7ce3c7531 100644 --- a/lib/src/widgets/image_gallery.dart +++ b/lib/src/widgets/image_gallery.dart @@ -29,6 +29,18 @@ class ImageGallery extends StatelessWidget { /// Page controller for the image pages. final PageController pageController; + Widget _imageGalleryLoadingBuilder(ImageChunkEvent? event) => Center( + child: SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + value: event == null || event.expectedTotalBytes == null + ? 0 + : event.cumulativeBytesLoaded / event.expectedTotalBytes!, + ), + ), + ); + @override Widget build(BuildContext context) => WillPopScope( onWillPop: () async { @@ -70,18 +82,6 @@ class ImageGallery extends StatelessWidget { ), ), ); - - Widget _imageGalleryLoadingBuilder(ImageChunkEvent? event) => Center( - child: SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - value: event == null || event.expectedTotalBytes == null - ? 0 - : event.cumulativeBytesLoaded / event.expectedTotalBytes!, - ), - ), - ); } class ImageGalleryOptions { diff --git a/lib/src/widgets/input/input.dart b/lib/src/widgets/input/input.dart index 1c6573866..c3106c78b 100644 --- a/lib/src/widgets/input/input.dart +++ b/lib/src/widgets/input/input.dart @@ -76,28 +76,6 @@ class _InputState extends State { _handleSendButtonVisibilityModeChange(); } - @override - void didUpdateWidget(covariant Input oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.options.sendButtonVisibilityMode != - oldWidget.options.sendButtonVisibilityMode) { - _handleSendButtonVisibilityModeChange(); - } - } - - @override - void dispose() { - _inputFocusNode.dispose(); - _textController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) => GestureDetector( - onTap: () => _inputFocusNode.requestFocus(), - child: _inputBuilder(), - ); - void _handleSendButtonVisibilityModeChange() { _textController.removeListener(_handleTextControllerChange); if (widget.options.sendButtonVisibilityMode == @@ -241,6 +219,28 @@ class _InputState extends State { ), ); } + + @override + void didUpdateWidget(covariant Input oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.options.sendButtonVisibilityMode != + oldWidget.options.sendButtonVisibilityMode) { + _handleSendButtonVisibilityModeChange(); + } + } + + @override + void dispose() { + _inputFocusNode.dispose(); + _textController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => GestureDetector( + onTap: () => _inputFocusNode.requestFocus(), + child: _inputBuilder(), + ); } @immutable @@ -259,7 +259,7 @@ class InputOptions { /// Controls the [Input] clear behavior. Defaults to [InputClearMode.always]. final InputClearMode inputClearMode; - + /// Controls the [Input] keyboard type. Defaults to [TextInputType.multiline]. final TextInputType keyboardType; diff --git a/lib/src/widgets/input/input_text_field_controller.dart b/lib/src/widgets/input/input_text_field_controller.dart index e71a5ace9..233abe9fb 100644 --- a/lib/src/widgets/input/input_text_field_controller.dart +++ b/lib/src/widgets/input/input_text_field_controller.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import '../../models/pattern_style.dart'; /// Controller for the [TextField] on [Input] widget -/// To highlighting the matches for pattern +/// To highlighting the matches for pattern. class InputTextFieldController extends TextEditingController { /// A map of style to apply to the text pattern. final List _listPatternStyle = [ diff --git a/lib/src/widgets/message/image_message.dart b/lib/src/widgets/message/image_message.dart index d4564748e..22a4ee073 100644 --- a/lib/src/widgets/message/image_message.dart +++ b/lib/src/widgets/message/image_message.dart @@ -48,6 +48,26 @@ class _ImageMessageState extends State { _size = Size(widget.message.width ?? 0, widget.message.height ?? 0); } + void _getImage() { + final oldImageStream = _stream; + _stream = _image?.resolve(createLocalImageConfiguration(context)); + if (_stream?.key == oldImageStream?.key) { + return; + } + final listener = ImageStreamListener(_updateImage); + oldImageStream?.removeListener(listener); + _stream?.addListener(listener); + } + + void _updateImage(ImageInfo info, bool _) { + setState(() { + _size = Size( + info.image.width.toDouble(), + info.image.height.toDouble(), + ); + }); + } + @override void didChangeDependencies() { super.didChangeDependencies(); @@ -157,24 +177,4 @@ class _ImageMessageState extends State { ); } } - - void _getImage() { - final oldImageStream = _stream; - _stream = _image?.resolve(createLocalImageConfiguration(context)); - if (_stream?.key == oldImageStream?.key) { - return; - } - final listener = ImageStreamListener(_updateImage); - oldImageStream?.removeListener(listener); - _stream?.addListener(listener); - } - - void _updateImage(ImageInfo info, bool _) { - setState(() { - _size = Size( - info.image.width.toDouble(), - info.image.height.toDouble(), - ); - }); - } } diff --git a/lib/src/widgets/message/message.dart b/lib/src/widgets/message/message.dart index c3b0bb640..6f33231b3 100644 --- a/lib/src/widgets/message/message.dart +++ b/lib/src/widgets/message/message.dart @@ -59,14 +59,14 @@ class Message extends StatelessWidget { audioMessageBuilder; /// This is to allow custom user avatar builder - /// By using this we can fetch newest user info based on id + /// By using this we can fetch newest user info based on id. final Widget Function(String userId)? avatarBuilder; /// Customize the default bubble using this function. `child` is a content /// you should render inside your bubble, `message` is a current message /// (contains `author` inside) and `nextMessageInGroup` allows you to see /// if the message is a part of a group (messages are grouped when written - /// in quick succession by the same author) + /// in quick succession by the same author). final Widget Function( Widget child, { required types.Message message, @@ -174,6 +174,99 @@ class Message extends StatelessWidget { final Widget Function(types.VideoMessage, {required int messageWidth})? videoMessageBuilder; + Widget _avatarBuilder() => showAvatar + ? avatarBuilder?.call(message.author.id) ?? + UserAvatar( + author: message.author, + bubbleRtlAlignment: bubbleRtlAlignment, + imageHeaders: imageHeaders, + onAvatarTap: onAvatarTap, + ) + : const SizedBox(width: 40); + + Widget _bubbleBuilder( + BuildContext context, + BorderRadius borderRadius, + bool currentUserIsAuthor, + bool enlargeEmojis, + ) => + bubbleBuilder != null + ? bubbleBuilder!( + _messageBuilder(), + message: message, + nextMessageInGroup: roundBorder, + ) + : enlargeEmojis && hideBackgroundOnEmojiMessages + ? _messageBuilder() + : Container( + decoration: BoxDecoration( + borderRadius: borderRadius, + color: !currentUserIsAuthor || + message.type == types.MessageType.image + ? InheritedChatTheme.of(context).theme.secondaryColor + : InheritedChatTheme.of(context).theme.primaryColor, + ), + child: ClipRRect( + borderRadius: borderRadius, + child: _messageBuilder(), + ), + ); + + Widget _messageBuilder() { + switch (message.type) { + case types.MessageType.audio: + final audioMessage = message as types.AudioMessage; + return audioMessageBuilder != null + ? audioMessageBuilder!(audioMessage, messageWidth: messageWidth) + : const SizedBox(); + case types.MessageType.custom: + final customMessage = message as types.CustomMessage; + return customMessageBuilder != null + ? customMessageBuilder!(customMessage, messageWidth: messageWidth) + : const SizedBox(); + case types.MessageType.file: + final fileMessage = message as types.FileMessage; + return fileMessageBuilder != null + ? fileMessageBuilder!(fileMessage, messageWidth: messageWidth) + : FileMessage(message: fileMessage); + case types.MessageType.image: + final imageMessage = message as types.ImageMessage; + return imageMessageBuilder != null + ? imageMessageBuilder!(imageMessage, messageWidth: messageWidth) + : ImageMessage( + imageHeaders: imageHeaders, + message: imageMessage, + messageWidth: messageWidth, + ); + case types.MessageType.text: + final textMessage = message as types.TextMessage; + return textMessageBuilder != null + ? textMessageBuilder!( + textMessage, + messageWidth: messageWidth, + showName: showName, + ) + : TextMessage( + emojiEnlargementBehavior: emojiEnlargementBehavior, + hideBackgroundOnEmojiMessages: hideBackgroundOnEmojiMessages, + message: textMessage, + nameBuilder: nameBuilder, + onPreviewDataFetched: onPreviewDataFetched, + options: textMessageOptions, + showName: showName, + usePreviewData: usePreviewData, + userAgent: userAgent, + ); + case types.MessageType.video: + final videoMessage = message as types.VideoMessage; + return videoMessageBuilder != null + ? videoMessageBuilder!(videoMessage, messageWidth: messageWidth) + : const SizedBox(); + default: + return const SizedBox(); + } + } + @override Widget build(BuildContext context) { final query = MediaQuery.of(context); @@ -291,97 +384,4 @@ class Message extends StatelessWidget { ), ); } - - Widget _avatarBuilder() => showAvatar - ? avatarBuilder?.call(message.author.id) ?? - UserAvatar( - author: message.author, - bubbleRtlAlignment: bubbleRtlAlignment, - imageHeaders: imageHeaders, - onAvatarTap: onAvatarTap, - ) - : const SizedBox(width: 40); - - Widget _bubbleBuilder( - BuildContext context, - BorderRadius borderRadius, - bool currentUserIsAuthor, - bool enlargeEmojis, - ) => - bubbleBuilder != null - ? bubbleBuilder!( - _messageBuilder(), - message: message, - nextMessageInGroup: roundBorder, - ) - : enlargeEmojis && hideBackgroundOnEmojiMessages - ? _messageBuilder() - : Container( - decoration: BoxDecoration( - borderRadius: borderRadius, - color: !currentUserIsAuthor || - message.type == types.MessageType.image - ? InheritedChatTheme.of(context).theme.secondaryColor - : InheritedChatTheme.of(context).theme.primaryColor, - ), - child: ClipRRect( - borderRadius: borderRadius, - child: _messageBuilder(), - ), - ); - - Widget _messageBuilder() { - switch (message.type) { - case types.MessageType.audio: - final audioMessage = message as types.AudioMessage; - return audioMessageBuilder != null - ? audioMessageBuilder!(audioMessage, messageWidth: messageWidth) - : const SizedBox(); - case types.MessageType.custom: - final customMessage = message as types.CustomMessage; - return customMessageBuilder != null - ? customMessageBuilder!(customMessage, messageWidth: messageWidth) - : const SizedBox(); - case types.MessageType.file: - final fileMessage = message as types.FileMessage; - return fileMessageBuilder != null - ? fileMessageBuilder!(fileMessage, messageWidth: messageWidth) - : FileMessage(message: fileMessage); - case types.MessageType.image: - final imageMessage = message as types.ImageMessage; - return imageMessageBuilder != null - ? imageMessageBuilder!(imageMessage, messageWidth: messageWidth) - : ImageMessage( - imageHeaders: imageHeaders, - message: imageMessage, - messageWidth: messageWidth, - ); - case types.MessageType.text: - final textMessage = message as types.TextMessage; - return textMessageBuilder != null - ? textMessageBuilder!( - textMessage, - messageWidth: messageWidth, - showName: showName, - ) - : TextMessage( - emojiEnlargementBehavior: emojiEnlargementBehavior, - hideBackgroundOnEmojiMessages: hideBackgroundOnEmojiMessages, - message: textMessage, - nameBuilder: nameBuilder, - onPreviewDataFetched: onPreviewDataFetched, - options: textMessageOptions, - showName: showName, - usePreviewData: usePreviewData, - userAgent: userAgent, - ); - case types.MessageType.video: - final videoMessage = message as types.VideoMessage; - return videoMessageBuilder != null - ? videoMessageBuilder!(videoMessage, messageWidth: messageWidth) - : const SizedBox(); - default: - return const SizedBox(); - } - } } diff --git a/lib/src/widgets/message/text_message.dart b/lib/src/widgets/message/text_message.dart index a83047aa1..7bded9b18 100644 --- a/lib/src/widgets/message/text_message.dart +++ b/lib/src/widgets/message/text_message.dart @@ -38,7 +38,7 @@ class TextMessage extends StatelessWidget { final types.TextMessage message; /// This is to allow custom user name builder - /// By using this we can fetch newest user info based on id + /// By using this we can fetch newest user info based on id. final Widget Function(types.User)? nameBuilder; /// See [LinkPreview.onPreviewDataFetched]. @@ -57,33 +57,6 @@ class TextMessage extends StatelessWidget { /// User agent to fetch preview data with. final String? userAgent; - @override - Widget build(BuildContext context) { - final enlargeEmojis = - emojiEnlargementBehavior != EmojiEnlargementBehavior.never && - isConsistsOfEmojis(emojiEnlargementBehavior, message); - final theme = InheritedChatTheme.of(context).theme; - final user = InheritedUser.of(context).user; - final width = MediaQuery.of(context).size.width; - - if (usePreviewData && onPreviewDataFetched != null) { - final urlRegexp = RegExp(regexLink, caseSensitive: false); - final matches = urlRegexp.allMatches(message.text); - - if (matches.isNotEmpty) { - return _linkPreview(user, width, context); - } - } - - return Container( - margin: EdgeInsets.symmetric( - horizontal: theme.messageInsetsHorizontal, - vertical: theme.messageInsetsVertical, - ), - child: _textWidgetBuilder(user, context, enlargeEmojis), - ); - } - Widget _linkPreview( types.User user, double width, @@ -155,8 +128,7 @@ class TextMessage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ if (showName) - nameBuilder?.call(message.author) ?? - UserName(author: message.author), + nameBuilder?.call(message.author) ?? UserName(author: message.author), if (enlargeEmojis) if (options.isTextSelectable) SelectableText(message.text, style: emojiTextStyle) @@ -174,6 +146,33 @@ class TextMessage extends StatelessWidget { ], ); } + + @override + Widget build(BuildContext context) { + final enlargeEmojis = + emojiEnlargementBehavior != EmojiEnlargementBehavior.never && + isConsistsOfEmojis(emojiEnlargementBehavior, message); + final theme = InheritedChatTheme.of(context).theme; + final user = InheritedUser.of(context).user; + final width = MediaQuery.of(context).size.width; + + if (usePreviewData && onPreviewDataFetched != null) { + final urlRegexp = RegExp(regexLink, caseSensitive: false); + final matches = urlRegexp.allMatches(message.text); + + if (matches.isNotEmpty) { + return _linkPreview(user, width, context); + } + } + + return Container( + margin: EdgeInsets.symmetric( + horizontal: theme.messageInsetsHorizontal, + vertical: theme.messageInsetsVertical, + ), + child: _textWidgetBuilder(user, context, enlargeEmojis), + ); + } } /// Widget to reuse the markdown capabilities, e.g., for previews. diff --git a/lib/src/widgets/patched_sliver_animated_list.dart b/lib/src/widgets/patched_sliver_animated_list.dart deleted file mode 100644 index b34bc4355..000000000 --- a/lib/src/widgets/patched_sliver_animated_list.dart +++ /dev/null @@ -1,296 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - -/// NOTE: See here for an explanation of the fix: -/// https://github.com/flutter/flutter/pull/108710 -/// Remove this file and replace with the upstream version once the fix is merged. - -/// Signature for the builder callback used by [AnimatedList]. -typedef AnimatedListItemBuilder = Widget Function( - BuildContext context, - int index, - Animation animation, -); - -/// Signature for the builder callback used by [AnimatedListState.removeItem]. -typedef AnimatedListRemovedItemBuilder = Widget Function( - BuildContext context, - Animation animation, -); - -// The default insert/remove animation duration. -const Duration _kDuration = Duration(milliseconds: 300); - -// Incoming and outgoing AnimatedList items. -class _ActiveItem implements Comparable<_ActiveItem> { - _ActiveItem.incoming(this.controller, this.itemIndex) - : removedItemBuilder = null; - _ActiveItem.outgoing( - this.controller, - this.itemIndex, - this.removedItemBuilder, - ); - _ActiveItem.index(this.itemIndex) - : controller = null, - removedItemBuilder = null; - - final AnimationController? controller; - final AnimatedListRemovedItemBuilder? removedItemBuilder; - int itemIndex; - - @override - int compareTo(_ActiveItem other) => itemIndex - other.itemIndex; -} - -/// A sliver that animates items when they are inserted or removed. -/// -/// This widget's [PatchedSliverAnimatedListState] can be used to dynamically insert or -/// remove items. To refer to the [PatchedSliverAnimatedListState] either provide a -/// [GlobalKey] or use the static [PatchedSliverAnimatedList.of] method from an item's -/// input callback. -/// -/// {@tool dartpad} -/// This sample application uses a [PatchedSliverAnimatedList] to create an animated -/// effect when items are removed or added to the list. -/// -/// ** See code in examples/api/lib/widgets/animated_list/sliver_animated_list.0.dart ** -/// {@end-tool} -/// -/// See also: -/// -/// * [SliverList], which does not animate items when they are inserted or -/// removed. -/// * [AnimatedList], a non-sliver scrolling container that animates items when -/// they are inserted or removed. -class PatchedSliverAnimatedList extends StatefulWidget { - /// Creates a sliver that animates items when they are inserted or removed. - const PatchedSliverAnimatedList({ - super.key, - required this.itemBuilder, - this.findChildIndexCallback, - this.initialItemCount = 0, - }) : assert(initialItemCount >= 0); - - /// Called, as needed, to build list item widgets. - /// - /// List items are only built when they're scrolled into view. - /// - /// The [AnimatedListItemBuilder] index parameter indicates the item's - /// position in the list. The value of the index parameter will be between 0 - /// and [initialItemCount] plus the total number of items that have been - /// inserted with [PatchedSliverAnimatedListState.insertItem] and less the total - /// number of items that have been removed with - /// [PatchedSliverAnimatedListState.removeItem]. - /// - /// Implementations of this callback should assume that - /// [PatchedSliverAnimatedListState.removeItem] removes an item immediately. - final AnimatedListItemBuilder itemBuilder; - - /// {@macro flutter.widgets.SliverChildBuilderDelegate.findChildIndexCallback} - final ChildIndexGetter? findChildIndexCallback; - - /// {@macro flutter.widgets.animatedList.initialItemCount} - final int initialItemCount; - - @override - PatchedSliverAnimatedListState createState() => - PatchedSliverAnimatedListState(); -} - -class PatchedSliverAnimatedListState extends State - with TickerProviderStateMixin { - final List<_ActiveItem> _incomingItems = <_ActiveItem>[]; - final List<_ActiveItem> _outgoingItems = <_ActiveItem>[]; - int _itemsCount = 0; - - @override - void initState() { - super.initState(); - _itemsCount = widget.initialItemCount; - } - - @override - void dispose() { - for (final item in _incomingItems.followedBy(_outgoingItems)) { - item.controller!.dispose(); - } - super.dispose(); - } - - @override - Widget build(BuildContext context) => SliverList( - delegate: _createDelegate(), - ); - - /// Insert an item at [index] and start an animation that will be passed to - /// [PatchedSliverAnimatedList.itemBuilder] when the item is visible. - /// - /// This method's semantics are the same as Dart's [List.insert] method: - /// it increases the length of the list by one and shifts all items at or - /// after [index] towards the end of the list. - void insertItem(int index, {Duration duration = _kDuration}) { - assert(index >= 0); - - final itemIndex = _indexToItemIndex(index); - assert(itemIndex >= 0 && itemIndex <= _itemsCount); - - // Increment the incoming and outgoing item indices to account - // for the insertion. - for (final item in _incomingItems) { - if (item.itemIndex >= itemIndex) { - item.itemIndex += 1; - } - } - for (final item in _outgoingItems) { - if (item.itemIndex >= itemIndex) { - item.itemIndex += 1; - } - } - - final controller = AnimationController( - duration: duration, - vsync: this, - ); - final incomingItem = _ActiveItem.incoming( - controller, - itemIndex, - ); - setState(() { - _incomingItems - ..add(incomingItem) - ..sort(); - _itemsCount += 1; - }); - - controller.forward().then((_) { - _removeActiveItemAt(_incomingItems, incomingItem.itemIndex)! - .controller! - .dispose(); - }); - } - - /// Remove the item at [index] and start an animation that will be passed - /// to [builder] when the item is visible. - /// - /// Items are removed immediately. After an item has been removed, its index - /// will no longer be passed to the [PatchedSliverAnimatedList.itemBuilder]. However - /// the item will still appear in the list for [duration] and during that time - /// [builder] must construct its widget as needed. - /// - /// This method's semantics are the same as Dart's [List.remove] method: - /// it decreases the length of the list by one and shifts all items at or - /// before [index] towards the beginning of the list. - void removeItem( - int index, - AnimatedListRemovedItemBuilder builder, { - Duration duration = _kDuration, - }) { - assert(index >= 0); - - final itemIndex = _indexToItemIndex(index); - assert(itemIndex >= 0 && itemIndex < _itemsCount); - assert(_activeItemAt(_outgoingItems, itemIndex) == null); - - final incomingItem = _removeActiveItemAt(_incomingItems, itemIndex); - final controller = incomingItem?.controller ?? - AnimationController(duration: duration, value: 1.0, vsync: this); - final outgoingItem = _ActiveItem.outgoing(controller, itemIndex, builder); - setState(() { - _outgoingItems - ..add(outgoingItem) - ..sort(); - }); - - controller.reverse().then((void value) { - _removeActiveItemAt(_outgoingItems, outgoingItem.itemIndex)! - .controller! - .dispose(); - - // Decrement the incoming and outgoing item indices to account - // for the removal. - for (final item in _incomingItems) { - if (item.itemIndex > outgoingItem.itemIndex) { - item.itemIndex -= 1; - } - } - for (final item in _outgoingItems) { - if (item.itemIndex > outgoingItem.itemIndex) { - item.itemIndex -= 1; - } - } - - setState(() => _itemsCount -= 1); - }); - } - - _ActiveItem? _removeActiveItemAt(List<_ActiveItem> items, int itemIndex) { - final i = binarySearch(items, _ActiveItem.index(itemIndex)); - return i == -1 ? null : items.removeAt(i); - } - - _ActiveItem? _activeItemAt(List<_ActiveItem> items, int itemIndex) { - final i = binarySearch(items, _ActiveItem.index(itemIndex)); - return i == -1 ? null : items[i]; - } - - // The insertItem() and removeItem() index parameters are defined as if the - // removeItem() operation removed the corresponding list entry immediately. - // The entry is only actually removed from the ListView when the remove animation - // finishes. The entry is added to _outgoingItems when removeItem is called - // and removed from _outgoingItems when the remove animation finishes. - - int _indexToItemIndex(int index) { - var itemIndex = index; - for (final item in _outgoingItems) { - if (item.itemIndex <= itemIndex) { - itemIndex += 1; - } else { - break; - } - } - return itemIndex; - } - - int _itemIndexToIndex(int itemIndex) { - var index = itemIndex; - for (final item in _outgoingItems) { - assert(item.itemIndex != itemIndex); - if (item.itemIndex < itemIndex) { - index -= 1; - } else { - break; - } - } - return index; - } - - SliverChildDelegate _createDelegate() => SliverChildBuilderDelegate( - _itemBuilder, - childCount: _itemsCount, - findChildIndexCallback: widget.findChildIndexCallback == null - ? null - : (Key key) { - final index = widget.findChildIndexCallback!(key); - return index != null ? _indexToItemIndex(index) : null; - }, - ); - - Widget _itemBuilder(BuildContext context, int itemIndex) { - final outgoingItem = _activeItemAt(_outgoingItems, itemIndex); - if (outgoingItem != null) { - return outgoingItem.removedItemBuilder!( - context, - outgoingItem.controller!.view, - ); - } - - final incomingItem = _activeItemAt(_incomingItems, itemIndex); - final animation = - incomingItem?.controller?.view ?? kAlwaysCompleteAnimation; - return widget.itemBuilder( - context, - _itemIndexToIndex(itemIndex), - animation, - ); - } -} diff --git a/lib/src/widgets/state/inherited_chat_theme.dart b/lib/src/widgets/state/inherited_chat_theme.dart index efa0c80e9..d55a00574 100644 --- a/lib/src/widgets/state/inherited_chat_theme.dart +++ b/lib/src/widgets/state/inherited_chat_theme.dart @@ -11,12 +11,12 @@ class InheritedChatTheme extends InheritedWidget { required super.child, }); - /// Represents chat theme. - final ChatTheme theme; - static InheritedChatTheme of(BuildContext context) => context.dependOnInheritedWidgetOfExactType()!; + /// Represents chat theme. + final ChatTheme theme; + @override bool updateShouldNotify(InheritedChatTheme oldWidget) => theme.hashCode != oldWidget.theme.hashCode; diff --git a/lib/src/widgets/state/inherited_l10n.dart b/lib/src/widgets/state/inherited_l10n.dart index a07b5a59c..41cc950ca 100644 --- a/lib/src/widgets/state/inherited_l10n.dart +++ b/lib/src/widgets/state/inherited_l10n.dart @@ -11,12 +11,12 @@ class InheritedL10n extends InheritedWidget { required super.child, }); - /// Represents localized copy. - final ChatL10n l10n; - static InheritedL10n of(BuildContext context) => context.dependOnInheritedWidgetOfExactType()!; + /// Represents localized copy. + final ChatL10n l10n; + @override bool updateShouldNotify(InheritedL10n oldWidget) => l10n.hashCode != oldWidget.l10n.hashCode; diff --git a/lib/src/widgets/state/inherited_user.dart b/lib/src/widgets/state/inherited_user.dart index 3783ea2cf..1fb8ed0ae 100644 --- a/lib/src/widgets/state/inherited_user.dart +++ b/lib/src/widgets/state/inherited_user.dart @@ -10,12 +10,12 @@ class InheritedUser extends InheritedWidget { required super.child, }); - /// Represents current logged in user. Used to determine message's author. - final types.User user; - static InheritedUser of(BuildContext context) => context.dependOnInheritedWidgetOfExactType()!; + /// Represents current logged in user. Used to determine message's author. + final types.User user; + @override bool updateShouldNotify(InheritedUser oldWidget) => user.id != oldWidget.user.id; diff --git a/lib/src/widgets/typing_indicator.dart b/lib/src/widgets/typing_indicator.dart index fa0e26caf..2dac6872b 100644 --- a/lib/src/widgets/typing_indicator.dart +++ b/lib/src/widgets/typing_indicator.dart @@ -80,6 +80,35 @@ class _TypingIndicatorState extends State } } + /// Handler for circles offset. + Animation _circleOffset( + Offset? start, + Offset? end, + Interval animationInterval, + ) => + TweenSequence( + >[ + TweenSequenceItem( + tween: Tween( + begin: start, + end: end, + ), + weight: 50.0, + ), + TweenSequenceItem( + tween: Tween( + begin: end, + end: start, + ), + weight: 50.0, + ), + ], + ).animate(CurvedAnimation( + parent: _animatedCirclesController, + curve: animationInterval, + reverseCurve: animationInterval, + )); + @override void didUpdateWidget(TypingIndicator oldWidget) { super.didUpdateWidget(oldWidget); @@ -176,35 +205,6 @@ class _TypingIndicatorState extends State ], ), ); - - /// Handler for circles offset. - Animation _circleOffset( - Offset? start, - Offset? end, - Interval animationInterval, - ) => - TweenSequence( - >[ - TweenSequenceItem( - tween: Tween( - begin: start, - end: end, - ), - weight: 50.0, - ), - TweenSequenceItem( - tween: Tween( - begin: end, - end: start, - ), - weight: 50.0, - ), - ], - ).animate(CurvedAnimation( - parent: _animatedCirclesController, - curve: animationInterval, - reverseCurve: animationInterval, - )); } /// Typing Widget. @@ -220,6 +220,30 @@ class TypingWidget extends StatelessWidget { final BuildContext context; final TypingIndicatorMode mode; + /// Handler for multi user typing text. + String _multiUserTextBuilder(List author) { + if (author.isEmpty) { + return ''; + } else if (author.length == 1) { + return '${author.first.firstName} is typing'; + } else if (author.length == 2) { + return '${author.first.firstName} and ${author[1].firstName}'; + } else { + return '${author.first.firstName} and ${author.length - 1} others'; + } + } + + /// Used to specify width of stacking avatars based on number of authors. + double _getStackingWidth(List author, double indicatorWidth) { + if (author.length == 1) { + return indicatorWidth * 0.06; + } else if (author.length == 2) { + return indicatorWidth * 0.11; + } else { + return indicatorWidth * 0.15; + } + } + @override Widget build(BuildContext context) { final sWidth = _getStackingWidth( @@ -266,30 +290,6 @@ class TypingWidget extends StatelessWidget { ); } } - - /// Handler for multi user typing text. - String _multiUserTextBuilder(List author) { - if (author.isEmpty) { - return ''; - } else if (author.length == 1) { - return '${author.first.firstName} is typing'; - } else if (author.length == 2) { - return '${author.first.firstName} and ${author[1].firstName}'; - } else { - return '${author.first.firstName} and ${author.length - 1} others'; - } - } - - /// Used to specify width of stacking avatars based on number of authors. - double _getStackingWidth(List author, double indicatorWidth) { - if (author.length == 1) { - return indicatorWidth * 0.06; - } else if (author.length == 2) { - return indicatorWidth * 0.11; - } else { - return indicatorWidth * 0.15; - } - } } /// Multi Avatar Handler Widget. diff --git a/pubspec.yaml b/pubspec.yaml index 38f2406c2..70cd0ecee 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,26 +7,26 @@ homepage: https://flyer.chat repository: https://github.com/flyerhq/flutter_chat_ui environment: - sdk: '>=2.18.6 <3.0.0' - flutter: '>=2.0.0' + sdk: '>=2.18.0 <4.0.0' + flutter: '>=3.0.0' dependencies: diffutil_dart: ^3.0.0 equatable: ^2.0.5 flutter: sdk: flutter - flutter_chat_types: ^3.6.0 - flutter_link_previewer: ^3.2.0 + flutter_chat_types: ^3.6.1 + flutter_link_previewer: ^3.2.1 flutter_parsed_text: ^2.2.1 - intl: ^0.18.0 - meta: ^1.8.0 + intl: ^0.18.1 + meta: ^1.9.1 photo_view: ^0.14.0 scroll_to_index: ^3.0.1 - url_launcher: ^6.1.10 + url_launcher: ^6.1.11 visibility_detector: ^0.4.0+2 dev_dependencies: - dart_code_metrics: ^5.3.0 + dart_code_metrics: ^5.7.3 flutter_lints: ^2.0.1 flutter_test: sdk: flutter From 53b95300ef032c17eb8dcde6d87cb6b2f88f3713 Mon Sep 17 00:00:00 2001 From: Alex Demchenko Date: Tue, 16 May 2023 22:36:24 +0200 Subject: [PATCH 12/24] Update dependencies --- .metadata | 2 +- CHANGELOG.md | 4 + example/.metadata | 30 +-- example/android/app/build.gradle | 3 +- .../android/app/src/debug/AndroidManifest.xml | 3 +- .../android/app/src/main/AndroidManifest.xml | 5 +- .../app/src/profile/AndroidManifest.xml | 3 +- example/android/build.gradle | 6 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- example/ios/Podfile | 3 + example/ios/Podfile.lock | 22 +- example/ios/Runner.xcodeproj/project.pbxproj | 242 +++++++++++++++--- .../xcshareddata/xcschemes/Runner.xcscheme | 11 + .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 564 -> 295 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 1283 -> 406 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 1588 -> 450 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 1025 -> 282 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 1716 -> 462 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 1920 -> 704 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 1283 -> 406 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 1895 -> 586 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 2665 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 2665 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 3831 -> 1674 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 1888 -> 762 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 3294 -> 1226 bytes .../Icon-App-83.5x83.5@2x.png | Bin 3612 -> 1418 bytes example/ios/RunnerTests/RunnerTests.swift | 12 + example/linux/CMakeLists.txt | 1 + example/macos/Podfile | 5 +- .../macos/Runner.xcodeproj/project.pbxproj | 233 ++++++++++++++--- .../xcshareddata/xcschemes/Runner.xcscheme | 11 + example/macos/Runner/Configs/AppInfo.xcconfig | 2 +- example/macos/Runner/MainFlutterWindow.swift | 2 +- example/macos/RunnerTests/RunnerTests.swift | 12 + example/pubspec.yaml | 6 +- example/web/index.html | 9 +- example/windows/CMakeLists.txt | 1 + example/windows/runner/CMakeLists.txt | 1 + example/windows/runner/Runner.rc | 2 +- example/windows/runner/flutter_window.cpp | 5 + example/windows/runner/main.cpp | 2 +- example/windows/runner/utils.cpp | 9 +- example/windows/runner/win32_window.cpp | 55 +++- example/windows/runner/win32_window.h | 20 +- pubspec.yaml | 4 +- 46 files changed, 584 insertions(+), 144 deletions(-) create mode 100644 example/ios/RunnerTests/RunnerTests.swift create mode 100644 example/macos/RunnerTests/RunnerTests.swift diff --git a/.metadata b/.metadata index 87268d19c..40e012c9d 100644 --- a/.metadata +++ b/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a + revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 channel: stable project_type: package diff --git a/CHANGELOG.md b/CHANGELOG.md index 77f6b768f..e1bdbf3a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.6.7 + +- Update dependencies. Requires Dart >= 2.19.0. + ## 1.6.6 - Add `audioMessageBuilder` (no default implementation yet). Thanks @marinkobabic for the PR! diff --git a/example/.metadata b/example/.metadata index 6423aca06..e94891d5f 100644 --- a/example/.metadata +++ b/example/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled. version: - revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a + revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 channel: stable project_type: app @@ -13,26 +13,26 @@ project_type: app migration: platforms: - platform: root - create_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a - base_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a + create_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 + base_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 - platform: android - create_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a - base_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a + create_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 + base_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 - platform: ios - create_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a - base_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a + create_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 + base_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 - platform: linux - create_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a - base_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a + create_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 + base_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 - platform: macos - create_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a - base_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a + create_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 + base_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 - platform: web - create_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a - base_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a + create_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 + base_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 - platform: windows - create_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a - base_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a + create_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 + base_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 # User provided section diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 4ffc37b65..0ce28921a 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -26,6 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { + namespace "com.example" compileSdkVersion flutter.compileSdkVersion ndkVersion flutter.ndkVersion @@ -46,7 +47,7 @@ android { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.example" // You can update the following values to match your application needs. - // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. minSdkVersion flutter.minSdkVersion targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml index d919e08d3..399f6981d 100644 --- a/example/android/app/src/debug/AndroidManifest.xml +++ b/example/android/app/src/debug/AndroidManifest.xml @@ -1,5 +1,4 @@ - +