From 90c4dae5b162639a07f57c61449cebc4f8471b2f Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 26 Apr 2023 17:50:06 +0530 Subject: [PATCH 01/51] feat: add support for url attachment theming. Signed-off-by: xsahil03x --- packages/stream_chat_flutter/CHANGELOG.md | 11 +++ .../lib/src/attachment/url_attachment.dart | 12 +-- .../message_input/quoted_message_widget.dart | 2 +- .../lib/src/message_widget/message_card.dart | 2 +- .../lib/src/theme/message_theme.dart | 95 ++++++++++++++++--- .../lib/src/theme/stream_chat_theme.dart | 14 ++- .../test/src/theme/message_theme_test.dart | 4 +- 7 files changed, 114 insertions(+), 26 deletions(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index dea279237..1292a2023 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,3 +1,14 @@ +## Upcoming + +โœ… Added + +- Added `MessageTheme.urlAttachmentHostStyle`, `MessageTheme.urlAttachmentTitleStyle`, and + `MessageTheme.urlAttachmentTextStyle` to customize the style of the url attachment. + +๐Ÿ”„ Changed + +- Deprecated `MessageTheme.linkBackgroundColor` in favor of `MessageTheme.urlAttachmentBackgroundColor`. + ## 6.0.0 ๐Ÿž Fixed diff --git a/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart index 10170bb15..4da662fa7 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart @@ -79,7 +79,7 @@ class StreamUrlAttachment extends StatelessWidget { borderRadius: const BorderRadius.only( topRight: Radius.circular(16), ), - color: messageTheme.linkBackgroundColor, + color: messageTheme.urlAttachmentBackgroundColor, ), child: Padding( padding: const EdgeInsets.only( @@ -89,9 +89,7 @@ class StreamUrlAttachment extends StatelessWidget { ), child: Text( hostDisplayName, - style: chatThemeData.textTheme.bodyBold.copyWith( - color: chatThemeData.colorTheme.accentPrimary, - ), + style: messageTheme.urlAttachmentHostStyle, ), ), ), @@ -109,14 +107,12 @@ class StreamUrlAttachment extends StatelessWidget { urlAttachment.title!.trim(), maxLines: messageTheme.urlAttachmentTitleMaxLine ?? 1, overflow: TextOverflow.ellipsis, - style: chatThemeData.textTheme.body - .copyWith(fontWeight: FontWeight.w700), + style: messageTheme.urlAttachmentTitleStyle, ), if (urlAttachment.text != null) Text( urlAttachment.text!, - style: chatThemeData.textTheme.body - .copyWith(fontWeight: FontWeight.w400), + style: messageTheme.urlAttachmentTextStyle, ), ], ), diff --git a/packages/stream_chat_flutter/lib/src/message_input/quoted_message_widget.dart b/packages/stream_chat_flutter/lib/src/message_input/quoted_message_widget.dart index 8de663e17..08152a587 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/quoted_message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/quoted_message_widget.dart @@ -205,7 +205,7 @@ class _QuotedMessage extends StatelessWidget { Color? _getBackgroundColor(BuildContext context) { if (_containsLinkAttachment) { - return messageTheme.linkBackgroundColor; + return messageTheme.urlAttachmentBackgroundColor; } return messageTheme.messageBackgroundColor; } diff --git a/packages/stream_chat_flutter/lib/src/message_widget/message_card.dart b/packages/stream_chat_flutter/lib/src/message_widget/message_card.dart index 2ad2cede2..0f59d7fee 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/message_card.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/message_card.dart @@ -215,7 +215,7 @@ class _MessageCardState extends State { } if (widget.hasUrlAttachments) { - return widget.messageTheme.linkBackgroundColor; + return widget.messageTheme.urlAttachmentBackgroundColor; } if (widget.isOnlyEmoji) { diff --git a/packages/stream_chat_flutter/lib/src/theme/message_theme.dart b/packages/stream_chat_flutter/lib/src/theme/message_theme.dart index d95ed7bba..6ae2cf884 100644 --- a/packages/stream_chat_flutter/lib/src/theme/message_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/message_theme.dart @@ -20,9 +20,15 @@ class StreamMessageThemeData with Diagnosticable { this.reactionsMaskColor, this.avatarTheme, this.createdAtStyle, - this.linkBackgroundColor, + @Deprecated('Use urlAttachmentBackgroundColor instead') + Color? linkBackgroundColor, + Color? urlAttachmentBackgroundColor, + this.urlAttachmentHostStyle, + this.urlAttachmentTitleStyle, + this.urlAttachmentTextStyle, this.urlAttachmentTitleMaxLine, - }); + }) : urlAttachmentBackgroundColor = + urlAttachmentBackgroundColor ?? linkBackgroundColor; /// Text style for message text final TextStyle? messageTextStyle; @@ -58,9 +64,22 @@ class StreamMessageThemeData with Diagnosticable { final StreamAvatarThemeData? avatarTheme; /// Background color for messages with url attachments. - final Color? linkBackgroundColor; + @Deprecated('Use urlAttachmentBackgroundColor instead') + Color? get linkBackgroundColor => urlAttachmentBackgroundColor; - /// Max number of lines in Url link title + /// Background color for messages with url attachments. + final Color? urlAttachmentBackgroundColor; + + /// Color for url attachment host. + final TextStyle? urlAttachmentHostStyle; + + /// Color for url attachment title. + final TextStyle? urlAttachmentTitleStyle; + + /// Color for url attachment text. + final TextStyle? urlAttachmentTextStyle; + + /// Max number of lines in Url link title. final int? urlAttachmentTitleMaxLine; /// Copy with a theme @@ -76,7 +95,12 @@ class StreamMessageThemeData with Diagnosticable { Color? reactionsBackgroundColor, Color? reactionsBorderColor, Color? reactionsMaskColor, - Color? linkBackgroundColor, + @Deprecated('Use urlAttachmentBackgroundColor instead') + Color? linkBackgroundColor, + Color? urlAttachmentBackgroundColor, + TextStyle? urlAttachmentHostStyle, + TextStyle? urlAttachmentTitleStyle, + TextStyle? urlAttachmentTextStyle, int? urlAttachmentTitleMaxLine, }) { return StreamMessageThemeData( @@ -93,7 +117,15 @@ class StreamMessageThemeData with Diagnosticable { reactionsBackgroundColor ?? this.reactionsBackgroundColor, reactionsBorderColor: reactionsBorderColor ?? this.reactionsBorderColor, reactionsMaskColor: reactionsMaskColor ?? this.reactionsMaskColor, - linkBackgroundColor: linkBackgroundColor ?? this.linkBackgroundColor, + urlAttachmentBackgroundColor: urlAttachmentBackgroundColor ?? + linkBackgroundColor ?? + this.urlAttachmentBackgroundColor, + urlAttachmentHostStyle: + urlAttachmentHostStyle ?? this.urlAttachmentHostStyle, + urlAttachmentTitleStyle: + urlAttachmentTitleStyle ?? this.urlAttachmentTitleStyle, + urlAttachmentTextStyle: + urlAttachmentTextStyle ?? this.urlAttachmentTextStyle, urlAttachmentTitleMaxLine: urlAttachmentTitleMaxLine ?? this.urlAttachmentTitleMaxLine, ); @@ -129,8 +161,23 @@ class StreamMessageThemeData with Diagnosticable { reactionsMaskColor: Color.lerp(a.reactionsMaskColor, b.reactionsMaskColor, t), repliesStyle: TextStyle.lerp(a.repliesStyle, b.repliesStyle, t), - linkBackgroundColor: - Color.lerp(a.linkBackgroundColor, b.linkBackgroundColor, t), + urlAttachmentBackgroundColor: Color.lerp( + a.urlAttachmentBackgroundColor, + b.urlAttachmentBackgroundColor, + t, + ), + urlAttachmentHostStyle: + TextStyle.lerp(a.urlAttachmentHostStyle, b.urlAttachmentHostStyle, t), + urlAttachmentTextStyle: TextStyle.lerp( + a.urlAttachmentTextStyle, + b.urlAttachmentTextStyle, + t, + ), + urlAttachmentTitleStyle: TextStyle.lerp( + a.urlAttachmentTitleStyle, + b.urlAttachmentTitleStyle, + t, + ), ); } @@ -154,7 +201,10 @@ class StreamMessageThemeData with Diagnosticable { reactionsBackgroundColor: other.reactionsBackgroundColor, reactionsBorderColor: other.reactionsBorderColor, reactionsMaskColor: other.reactionsMaskColor, - linkBackgroundColor: other.linkBackgroundColor, + urlAttachmentBackgroundColor: other.urlAttachmentBackgroundColor, + urlAttachmentHostStyle: other.urlAttachmentHostStyle, + urlAttachmentTitleStyle: other.urlAttachmentTitleStyle, + urlAttachmentTextStyle: other.urlAttachmentTextStyle, urlAttachmentTitleMaxLine: other.urlAttachmentTitleMaxLine, ); } @@ -175,7 +225,10 @@ class StreamMessageThemeData with Diagnosticable { reactionsBorderColor == other.reactionsBorderColor && reactionsMaskColor == other.reactionsMaskColor && avatarTheme == other.avatarTheme && - linkBackgroundColor == other.linkBackgroundColor && + urlAttachmentBackgroundColor == other.urlAttachmentBackgroundColor && + urlAttachmentHostStyle == other.urlAttachmentHostStyle && + urlAttachmentTitleStyle == other.urlAttachmentTitleStyle && + urlAttachmentTextStyle == other.urlAttachmentTextStyle && urlAttachmentTitleMaxLine == other.urlAttachmentTitleMaxLine; @override @@ -191,7 +244,10 @@ class StreamMessageThemeData with Diagnosticable { reactionsBorderColor.hashCode ^ reactionsMaskColor.hashCode ^ avatarTheme.hashCode ^ - linkBackgroundColor.hashCode ^ + urlAttachmentBackgroundColor.hashCode ^ + urlAttachmentHostStyle.hashCode ^ + urlAttachmentTitleStyle.hashCode ^ + urlAttachmentTextStyle.hashCode ^ urlAttachmentTitleMaxLine.hashCode; @override @@ -209,7 +265,22 @@ class StreamMessageThemeData with Diagnosticable { ..add(ColorProperty('reactionsBackgroundColor', reactionsBackgroundColor)) ..add(ColorProperty('reactionsBorderColor', reactionsBorderColor)) ..add(ColorProperty('reactionsMaskColor', reactionsMaskColor)) - ..add(ColorProperty('linkBackgroundColor', linkBackgroundColor)) + ..add(ColorProperty( + 'urlAttachmentBackgroundColor', + urlAttachmentBackgroundColor, + )) + ..add(DiagnosticsProperty( + 'urlAttachmentHostStyle', + urlAttachmentHostStyle, + )) + ..add(DiagnosticsProperty( + 'urlAttachmentTitleStyle', + urlAttachmentTitleStyle, + )) + ..add(DiagnosticsProperty( + 'urlAttachmentTextStyle', + urlAttachmentTextStyle, + )) ..add(DiagnosticsProperty( 'urlAttachmentTitleMaxLine', urlAttachmentTitleMaxLine, diff --git a/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.dart index 8021406f9..01993efb1 100644 --- a/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.dart @@ -199,7 +199,12 @@ class StreamChatThemeData { messageLinksStyle: TextStyle( color: accentColor, ), - linkBackgroundColor: colorTheme.linkBg, + urlAttachmentBackgroundColor: colorTheme.linkBg, + urlAttachmentHostStyle: textTheme.bodyBold.copyWith(color: accentColor), + urlAttachmentTitleStyle: + textTheme.body.copyWith(fontWeight: FontWeight.w700), + urlAttachmentTextStyle: + textTheme.body.copyWith(fontWeight: FontWeight.w400), ), otherMessageTheme: StreamMessageThemeData( reactionsBackgroundColor: colorTheme.disabled, @@ -223,7 +228,12 @@ class StreamChatThemeData { width: 32, ), ), - linkBackgroundColor: colorTheme.linkBg, + urlAttachmentBackgroundColor: colorTheme.linkBg, + urlAttachmentHostStyle: textTheme.bodyBold.copyWith(color: accentColor), + urlAttachmentTitleStyle: + textTheme.body.copyWith(fontWeight: FontWeight.w700), + urlAttachmentTextStyle: + textTheme.body.copyWith(fontWeight: FontWeight.w400), ), messageInputTheme: StreamMessageInputThemeData( borderRadius: BorderRadius.circular(20), diff --git a/packages/stream_chat_flutter/test/src/theme/message_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/message_theme_test.dart index 3b6c3a6fa..246d9d907 100644 --- a/packages/stream_chat_flutter/test/src/theme/message_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/message_theme_test.dart @@ -60,7 +60,7 @@ final _messageThemeControl = StreamMessageThemeData( messageLinksStyle: TextStyle( color: StreamColorTheme.light().accentPrimary, ), - linkBackgroundColor: StreamColorTheme.light().linkBg, + urlAttachmentBackgroundColor: StreamColorTheme.light().linkBg, ); final _messageThemeControlDark = StreamMessageThemeData( @@ -89,5 +89,5 @@ final _messageThemeControlDark = StreamMessageThemeData( messageLinksStyle: TextStyle( color: StreamColorTheme.dark().accentPrimary, ), - linkBackgroundColor: StreamColorTheme.dark().linkBg, + urlAttachmentBackgroundColor: StreamColorTheme.dark().linkBg, ); From 90759cc36f54aa3b5e2f73d30204d1d582166d9f Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 26 Apr 2023 17:57:06 +0530 Subject: [PATCH 02/51] chore: fix analysis Signed-off-by: xsahil03x --- .../stream_chat_flutter/lib/src/attachment/url_attachment.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart index 4da662fa7..a8ed00c66 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart @@ -36,8 +36,6 @@ class StreamUrlAttachment extends StatelessWidget { @override Widget build(BuildContext context) { - final chatThemeData = StreamChatTheme.of(context); - return ConstrainedBox( constraints: const BoxConstraints( maxWidth: 400, From 03e584f59103c8dda960c64637bd7ee40e8186f7 Mon Sep 17 00:00:00 2001 From: kanat <> Date: Wed, 26 Apr 2023 15:32:51 -0700 Subject: [PATCH 03/51] [1505] fix emoji regexp to exclude non-emoji chars --- .../stream_chat_flutter/lib/src/utils/extensions.dart | 2 +- .../test/src/utils/extension_test.dart | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/utils/extensions.dart b/packages/stream_chat_flutter/lib/src/utils/extensions.dart index 67b622dba..06b02aae1 100644 --- a/packages/stream_chat_flutter/lib/src/utils/extensions.dart +++ b/packages/stream_chat_flutter/lib/src/utils/extensions.dart @@ -34,7 +34,7 @@ extension StringExtension on String { if (trimmedString.isEmpty) return false; if (trimmedString.characters.length > 3) return false; final emojiRegex = RegExp( - r'^(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])+$', + r'^(\u00a9|\u00ae|\u200d|[\ufe00-\ufe0f]|[\u2600-\u27FF]|[\u2300-\u2bFF]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])+$', multiLine: true, caseSensitive: false, ); diff --git a/packages/stream_chat_flutter/test/src/utils/extension_test.dart b/packages/stream_chat_flutter/test/src/utils/extension_test.dart index 269807274..c8e75f816 100644 --- a/packages/stream_chat_flutter/test/src/utils/extension_test.dart +++ b/packages/stream_chat_flutter/test/src/utils/extension_test.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; @@ -81,6 +82,16 @@ void main() { expect('๐ŸŒถ1'.isOnlyEmoji, false); expect('๐Ÿ‘จโ€๐Ÿ‘จ๐Ÿ‘จโ€๐Ÿ‘จ'.isOnlyEmoji, true); expect('๐Ÿ‘จโ€๐Ÿ‘จ๐Ÿ‘จโ€๐Ÿ‘จ '.isOnlyEmoji, true); + expect('๐Ÿ‘จ๐Ÿ‘จ๐Ÿ‘จ๐Ÿ‘จ'.isOnlyEmoji, false); + expect('โญโญโญ'.isOnlyEmoji, true); + expect('โญ•โญ•โญ'.isOnlyEmoji, true); + expect('โœ…'.isOnlyEmoji, true); + expect('โ˜บ๏ธ'.isOnlyEmoji, true); + }); + + test('korean symbols', () { + expect('ใ…Žใ…Žใ…Ž'.isOnlyEmoji, false); + expect('ใ…Žใ…Žใ…Žใ…Ž'.isOnlyEmoji, false); }); }); } From 79b260d3c56b46635a65a086290cef6791273f7a Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 27 Apr 2023 14:36:21 +0530 Subject: [PATCH 04/51] test: add more tests Signed-off-by: xsahil03x --- .../test/src/utils/extension_test.dart | 58 ++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_flutter/test/src/utils/extension_test.dart b/packages/stream_chat_flutter/test/src/utils/extension_test.dart index c8e75f816..eef7677e1 100644 --- a/packages/stream_chat_flutter/test/src/utils/extension_test.dart +++ b/packages/stream_chat_flutter/test/src/utils/extension_test.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; @@ -89,7 +88,62 @@ void main() { expect('โ˜บ๏ธ'.isOnlyEmoji, true); }); - test('korean symbols', () { + test('Korean vowels', () { + expect('ใ…'.isOnlyEmoji, false); + expect('ใ…‘'.isOnlyEmoji, false); + expect('ใ…“'.isOnlyEmoji, false); + expect('ใ…•'.isOnlyEmoji, false); + expect('ใ…—'.isOnlyEmoji, false); + expect('ใ…›'.isOnlyEmoji, false); + expect('ใ…œ'.isOnlyEmoji, false); + expect('ใ… '.isOnlyEmoji, false); + expect('ใ…ก'.isOnlyEmoji, false); + expect('ใ…ฃ'.isOnlyEmoji, false); + }); + + test('Korean consonants', () { + expect('ใ„ฑ'.isOnlyEmoji, false); + expect('ใ„ด'.isOnlyEmoji, false); + expect('ใ„ท'.isOnlyEmoji, false); + expect('ใ„น'.isOnlyEmoji, false); + expect('ใ…'.isOnlyEmoji, false); + expect('ใ…‚'.isOnlyEmoji, false); + expect('ใ……'.isOnlyEmoji, false); + expect('ใ…‡'.isOnlyEmoji, false); + expect('ใ…ˆ'.isOnlyEmoji, false); + expect('ใ…Š'.isOnlyEmoji, false); + expect('ใ…‹'.isOnlyEmoji, false); + expect('ใ…Œ'.isOnlyEmoji, false); + expect('ใ…'.isOnlyEmoji, false); + expect('ใ…Ž'.isOnlyEmoji, false); + }); + + test('Korean syllables', () { + expect('๊ฐ€'.isOnlyEmoji, false); + expect('๋‚˜'.isOnlyEmoji, false); + expect('๋‹ค'.isOnlyEmoji, false); + expect('๋ผ'.isOnlyEmoji, false); + expect('๋งˆ'.isOnlyEmoji, false); + expect('๋ฐ”'.isOnlyEmoji, false); + expect('์‚ฌ'.isOnlyEmoji, false); + expect('์•„'.isOnlyEmoji, false); + expect('์ž'.isOnlyEmoji, false); + expect('์ฐจ'.isOnlyEmoji, false); + expect('์นด'.isOnlyEmoji, false); + expect('ํƒ€'.isOnlyEmoji, false); + expect('ํŒŒ'.isOnlyEmoji, false); + expect('ํ•˜'.isOnlyEmoji, false); + }); + + // https://github.com/GetStream/stream-chat-flutter/issues/1502 + test('Issue:#1502', () { + expect('ใ„ด'.isOnlyEmoji, false); + expect('ใ„ดใ…‡'.isOnlyEmoji, false); + expect('ใ…‡ใ…‹'.isOnlyEmoji, false); + }); + + // https://github.com/GetStream/stream-chat-flutter/issues/1505 + test('Issue:#1505', () { expect('ใ…Žใ…Žใ…Ž'.isOnlyEmoji, false); expect('ใ…Žใ…Žใ…Žใ…Ž'.isOnlyEmoji, false); }); From 3ed48fafba116d02e3bdf72ae2a49f9af13cef6a Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 27 Apr 2023 14:41:04 +0530 Subject: [PATCH 05/51] chore: update CHANGELOG.md Signed-off-by: xsahil03x --- packages/stream_chat_flutter/CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index dea279237..277d24844 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,3 +1,12 @@ +## Upcoming + +๐Ÿž Fixed + +- [#1502](https://github.com/GetStream/stream-chat-flutter/issues/1502) Fixed `isOnlyEmoji` method Detects Single Hangul + Consonants as Emoji. +- [#1505](https://github.com/GetStream/stream-chat-flutter/issues/1505) Fixed Message bubble disappears for Hangul + Consonants. + ## 6.0.0 ๐Ÿž Fixed From e4e6fca3111b79b57f5750e269baeb416be1f140 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 1 May 2023 17:53:48 +0530 Subject: [PATCH 06/51] fix(ui): `editMessageInputBuilder` property not used in message edit widget. Signed-off-by: xsahil03x --- packages/stream_chat_flutter/CHANGELOG.md | 48 +++++++++++++------ .../src/message_widget/message_widget.dart | 1 + 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 277d24844..1a04f07c9 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -2,31 +2,38 @@ ๐Ÿž Fixed -- [#1502](https://github.com/GetStream/stream-chat-flutter/issues/1502) Fixed `isOnlyEmoji` method Detects Single Hangul +- [[#1502]](https://github.com/GetStream/stream-chat-flutter/issues/1502) Fixed `isOnlyEmoji` method Detects Single + Hangul Consonants as Emoji. -- [#1505](https://github.com/GetStream/stream-chat-flutter/issues/1505) Fixed Message bubble disappears for Hangul +- [[#1505]](https://github.com/GetStream/stream-chat-flutter/issues/1505) Fixed Message bubble disappears for Hangul Consonants. +- [[#1490]](https://github.com/GetStream/stream-chat-flutter/issues/1490) Fixed `editMessageInputBuilder` property not + used in message edit widget. ## 6.0.0 ๐Ÿž Fixed -- [#1456](https://github.com/GetStream/stream-chat-flutter/issues/1456) Fixed logic for showing that a message was read using sending indicator. -- [#1462](https://github.com/GetStream/stream-chat-flutter/issues/1462) Fixed support for iPad in the share button for images. -- [#1475](https://github.com/GetStream/stream-chat-flutter/issues/1475) Fixed typo to fix compilation. +- [[#1456]](https://github.com/GetStream/stream-chat-flutter/issues/1456) Fixed logic for showing that a message was + read using sending indicator. +- [[#1462]](https://github.com/GetStream/stream-chat-flutter/issues/1462) Fixed support for iPad in the share button for + images. +- [[#1475]](https://github.com/GetStream/stream-chat-flutter/issues/1475) Fixed typo to fix compilation. โœ… Added - Now it is possible to customize the max lines of the title of a url attachment. Before it was always 1 line. -- Added `attachmentActionsModalBuilder` parameter to `StreamMessageWidget` that allows to customize `AttachmentActionsModal`. -- Added `StreamMessageInput.sendMessageKeyPredicate` and `StreamMessageInput.clearQuotedMessageKeyPredicate` to customize the keys used to send and clear the quoted message. +- Added `attachmentActionsModalBuilder` parameter to `StreamMessageWidget` that allows to + customize `AttachmentActionsModal`. +- Added `StreamMessageInput.sendMessageKeyPredicate` and `StreamMessageInput.clearQuotedMessageKeyPredicate` to + customize the keys used to send and clear the quoted message. ๐Ÿ”„ Changed - Updated dependencies to resolvable versions. ๐Ÿš€ Improved -- + - Improved draw of reaction options. [#1455](https://github.com/GetStream/stream-chat-flutter/pull/1455) ## 5.3.0 @@ -36,17 +43,22 @@ - Updated `photo_manager` dependency to `^2.5.2` ๐Ÿž Fixed -- [[#1424]](https://github.com/GetStream/stream-chat-flutter/issues/1424) Fixed a render issue when showing messages starting with 4 whitespaces. + +- [[#1424]](https://github.com/GetStream/stream-chat-flutter/issues/1424) Fixed a render issue when showing messages + starting with 4 whitespaces. - Fixed a bug where the `AttachmentPickerBottomSheet` was not able to identify the mobile browser. - Fixed uploading files on Windows - fixed temp file path. โœ… Added + - New `noPhotoOrVideoLabel` displayed when there is no files to choose. ## 5.2.0 โœ… Added -- Added a new `bottomRowBuilderWithDefaultWidget` parameter to `StreamMessageWidget` which contains a third parameter (default `BottomRow` widget with `copyWith` method available) to allow easier customization. + +- Added a new `bottomRowBuilderWithDefaultWidget` parameter to `StreamMessageWidget` which contains a third parameter ( + default `BottomRow` widget with `copyWith` method available) to allow easier customization. ๐Ÿ”„ Changed @@ -56,14 +68,20 @@ - Updated `dart_vlc` dependency to `^0.4.0` - Updated `file_picker` dependency to `^5.2.4` - Deprecated `StreamMessageWidget.bottomRowBuilder` in favor of `StreamMessageWidget.bottomRowBuilderWithDefaultWidget`. -- Deprecated `StreamMessageWidget.deletedBottomRowBuilder` in favor of `StreamMessageWidget.bottomRowBuilderWithDefaultWidget`. +- Deprecated `StreamMessageWidget.deletedBottomRowBuilder` in favor + of `StreamMessageWidget.bottomRowBuilderWithDefaultWidget`. - Deprecated `StreamMessageWidget.usernameBuilder` in favor of `StreamMessageWidget.bottomRowBuilderWithDefaultWidget`. ๐Ÿž Fixed -- [[#1379]](https://github.com/GetStream/stream-chat-flutter/issues/1379) Fixed "Issues with photo attachments on web", where the cached image attachment would not render while uploading. -- Fix render overflow issue with `MessageSearchListTileTitle`. It now uses `Text.rich` instead of `Row`. Better default behaviour and allows `TextOverflow`. -- [[1346]](https://github.com/GetStream/stream-chat-flutter/issues/1346) Fixed a render issue while uploading video on web. -- [[#1347]](https://github.com/GetStream/stream-chat-flutter/issues/1347) `onReply` not working in `AttachmentActionsModal` which is used by `StreamImageAttachment` and `StreamImageGroup`. + +- [[#1379]](https://github.com/GetStream/stream-chat-flutter/issues/1379) Fixed "Issues with photo attachments on web", + where the cached image attachment would not render while uploading. +- Fix render overflow issue with `MessageSearchListTileTitle`. It now uses `Text.rich` instead of `Row`. Better default + behaviour and allows `TextOverflow`. +- [[1346]](https://github.com/GetStream/stream-chat-flutter/issues/1346) Fixed a render issue while uploading video on + web. +- [[#1347]](https://github.com/GetStream/stream-chat-flutter/issues/1347) `onReply` not working + in `AttachmentActionsModal` which is used by `StreamImageAttachment` and `StreamImageGroup`. ## 5.1.0 diff --git a/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart index 7d51eb8be..267c96d49 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart @@ -1035,6 +1035,7 @@ class _StreamMessageWidgetState extends State builder: (_) => EditMessageSheet( message: widget.message, channel: StreamChannel.of(context).channel, + editMessageInputBuilder: widget.editMessageInputBuilder, ), ); }, From 82d96a1569b25573e63de5aac216be526bc7ac27 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 1 May 2023 18:05:17 +0530 Subject: [PATCH 07/51] fix(ui): `UserAvatarTransform.userAvatarBuilder` works only for otherUser. Signed-off-by: xsahil03x --- packages/stream_chat_flutter/CHANGELOG.md | 40 ++++++++++++++----- .../message_widget_content.dart | 2 + 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 277d24844..e9d71847b 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -6,20 +6,26 @@ Consonants as Emoji. - [#1505](https://github.com/GetStream/stream-chat-flutter/issues/1505) Fixed Message bubble disappears for Hangul Consonants. +- [[#1476]](https://github.com/GetStream/stream-chat-flutter/issues/1476) Fixed `UserAvatarTransform.userAvatarBuilder` + works only for otherUser. ## 6.0.0 ๐Ÿž Fixed -- [#1456](https://github.com/GetStream/stream-chat-flutter/issues/1456) Fixed logic for showing that a message was read using sending indicator. -- [#1462](https://github.com/GetStream/stream-chat-flutter/issues/1462) Fixed support for iPad in the share button for images. +- [#1456](https://github.com/GetStream/stream-chat-flutter/issues/1456) Fixed logic for showing that a message was read + using sending indicator. +- [#1462](https://github.com/GetStream/stream-chat-flutter/issues/1462) Fixed support for iPad in the share button for + images. - [#1475](https://github.com/GetStream/stream-chat-flutter/issues/1475) Fixed typo to fix compilation. โœ… Added - Now it is possible to customize the max lines of the title of a url attachment. Before it was always 1 line. -- Added `attachmentActionsModalBuilder` parameter to `StreamMessageWidget` that allows to customize `AttachmentActionsModal`. -- Added `StreamMessageInput.sendMessageKeyPredicate` and `StreamMessageInput.clearQuotedMessageKeyPredicate` to customize the keys used to send and clear the quoted message. +- Added `attachmentActionsModalBuilder` parameter to `StreamMessageWidget` that allows to + customize `AttachmentActionsModal`. +- Added `StreamMessageInput.sendMessageKeyPredicate` and `StreamMessageInput.clearQuotedMessageKeyPredicate` to + customize the keys used to send and clear the quoted message. ๐Ÿ”„ Changed @@ -27,6 +33,7 @@ ๐Ÿš€ Improved - + - Improved draw of reaction options. [#1455](https://github.com/GetStream/stream-chat-flutter/pull/1455) ## 5.3.0 @@ -36,17 +43,22 @@ - Updated `photo_manager` dependency to `^2.5.2` ๐Ÿž Fixed -- [[#1424]](https://github.com/GetStream/stream-chat-flutter/issues/1424) Fixed a render issue when showing messages starting with 4 whitespaces. + +- [[#1424]](https://github.com/GetStream/stream-chat-flutter/issues/1424) Fixed a render issue when showing messages + starting with 4 whitespaces. - Fixed a bug where the `AttachmentPickerBottomSheet` was not able to identify the mobile browser. - Fixed uploading files on Windows - fixed temp file path. โœ… Added + - New `noPhotoOrVideoLabel` displayed when there is no files to choose. ## 5.2.0 โœ… Added -- Added a new `bottomRowBuilderWithDefaultWidget` parameter to `StreamMessageWidget` which contains a third parameter (default `BottomRow` widget with `copyWith` method available) to allow easier customization. + +- Added a new `bottomRowBuilderWithDefaultWidget` parameter to `StreamMessageWidget` which contains a third parameter ( + default `BottomRow` widget with `copyWith` method available) to allow easier customization. ๐Ÿ”„ Changed @@ -56,14 +68,20 @@ - Updated `dart_vlc` dependency to `^0.4.0` - Updated `file_picker` dependency to `^5.2.4` - Deprecated `StreamMessageWidget.bottomRowBuilder` in favor of `StreamMessageWidget.bottomRowBuilderWithDefaultWidget`. -- Deprecated `StreamMessageWidget.deletedBottomRowBuilder` in favor of `StreamMessageWidget.bottomRowBuilderWithDefaultWidget`. +- Deprecated `StreamMessageWidget.deletedBottomRowBuilder` in favor + of `StreamMessageWidget.bottomRowBuilderWithDefaultWidget`. - Deprecated `StreamMessageWidget.usernameBuilder` in favor of `StreamMessageWidget.bottomRowBuilderWithDefaultWidget`. ๐Ÿž Fixed -- [[#1379]](https://github.com/GetStream/stream-chat-flutter/issues/1379) Fixed "Issues with photo attachments on web", where the cached image attachment would not render while uploading. -- Fix render overflow issue with `MessageSearchListTileTitle`. It now uses `Text.rich` instead of `Row`. Better default behaviour and allows `TextOverflow`. -- [[1346]](https://github.com/GetStream/stream-chat-flutter/issues/1346) Fixed a render issue while uploading video on web. -- [[#1347]](https://github.com/GetStream/stream-chat-flutter/issues/1347) `onReply` not working in `AttachmentActionsModal` which is used by `StreamImageAttachment` and `StreamImageGroup`. + +- [[#1379]](https://github.com/GetStream/stream-chat-flutter/issues/1379) Fixed "Issues with photo attachments on web", + where the cached image attachment would not render while uploading. +- Fix render overflow issue with `MessageSearchListTileTitle`. It now uses `Text.rich` instead of `Row`. Better default + behaviour and allows `TextOverflow`. +- [[1346]](https://github.com/GetStream/stream-chat-flutter/issues/1346) Fixed a render issue while uploading video on + web. +- [[#1347]](https://github.com/GetStream/stream-chat-flutter/issues/1347) `onReply` not working + in `AttachmentActionsModal` which is used by `StreamImageAttachment` and `StreamImageGroup`. ## 5.1.0 diff --git a/packages/stream_chat_flutter/lib/src/message_widget/message_widget_content.dart b/packages/stream_chat_flutter/lib/src/message_widget/message_widget_content.dart index d02a43646..e502463b2 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/message_widget_content.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/message_widget_content.dart @@ -379,6 +379,8 @@ class MessageWidgetContent extends StatelessWidget { showUserAvatar == DisplayWidget.show && message.user != null) ...[ UserAvatarTransform( + onUserAvatarTap: onUserAvatarTap, + userAvatarBuilder: userAvatarBuilder, translateUserAvatar: translateUserAvatar, messageTheme: messageTheme, message: message, From 6411fee02eead55c60d140c54ddf7060be4db82e Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 1 May 2023 18:10:17 +0530 Subject: [PATCH 08/51] feat(llc): expose `ChannelMute` class. Signed-off-by: xsahil03x --- packages/stream_chat/CHANGELOG.md | 6 ++++++ packages/stream_chat/lib/stream_chat.dart | 1 + 2 files changed, 7 insertions(+) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 9a4f508d7..d0b8ad326 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,3 +1,9 @@ +## Upcoming + +โœ… Added + +- Expose `ChannelMute` class. [#1473](https://github.com/GetStream/stream-chat-flutter/issues/1473) + ## 6.0.0 ๐Ÿž Fixed diff --git a/packages/stream_chat/lib/stream_chat.dart b/packages/stream_chat/lib/stream_chat.dart index fa5e727e4..f214c852d 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -24,6 +24,7 @@ export 'src/core/models/attachment.dart'; export 'src/core/models/attachment_file.dart'; export 'src/core/models/channel_config.dart'; export 'src/core/models/channel_model.dart'; +export 'src/core/models/channel_mute.dart'; export 'src/core/models/channel_state.dart'; export 'src/core/models/command.dart'; export 'src/core/models/device.dart'; From 0abacb37694ba6a96150096db1e1415dbc548bd7 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 1 May 2023 19:37:58 +0530 Subject: [PATCH 09/51] feat(ui): Added `StreamMessageInput.ogPreviewFilter` to allow users to filter out the og preview links. Signed-off-by: xsahil03x --- packages/stream_chat_flutter/CHANGELOG.md | 56 +++++++++++++++---- .../message_input/stream_message_input.dart | 32 +++++++++-- 2 files changed, 72 insertions(+), 16 deletions(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 277d24844..1a5443c48 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -7,19 +7,41 @@ - [#1505](https://github.com/GetStream/stream-chat-flutter/issues/1505) Fixed Message bubble disappears for Hangul Consonants. +โœ… Added + +- Added `StreamMessageInput.ogPreviewFilter` to allow users to filter out the og preview + links. [#1338](https://github.com/GetStream/stream-chat-flutter/issues/1338) + + ```dart + StreamMessageInput( + ogPreviewFilter: (matchedUri, messageText) { + final url = matchedUri.toString(); + if (url.contains('giphy.com')) { + // Return false to prevent the OG preview from being built. + return false; + } + // Return true to build the OG preview. + return true; + ), + ``` + ## 6.0.0 ๐Ÿž Fixed -- [#1456](https://github.com/GetStream/stream-chat-flutter/issues/1456) Fixed logic for showing that a message was read using sending indicator. -- [#1462](https://github.com/GetStream/stream-chat-flutter/issues/1462) Fixed support for iPad in the share button for images. +- [#1456](https://github.com/GetStream/stream-chat-flutter/issues/1456) Fixed logic for showing that a message was read + using sending indicator. +- [#1462](https://github.com/GetStream/stream-chat-flutter/issues/1462) Fixed support for iPad in the share button for + images. - [#1475](https://github.com/GetStream/stream-chat-flutter/issues/1475) Fixed typo to fix compilation. โœ… Added - Now it is possible to customize the max lines of the title of a url attachment. Before it was always 1 line. -- Added `attachmentActionsModalBuilder` parameter to `StreamMessageWidget` that allows to customize `AttachmentActionsModal`. -- Added `StreamMessageInput.sendMessageKeyPredicate` and `StreamMessageInput.clearQuotedMessageKeyPredicate` to customize the keys used to send and clear the quoted message. +- Added `attachmentActionsModalBuilder` parameter to `StreamMessageWidget` that allows to + customize `AttachmentActionsModal`. +- Added `StreamMessageInput.sendMessageKeyPredicate` and `StreamMessageInput.clearQuotedMessageKeyPredicate` to + customize the keys used to send and clear the quoted message. ๐Ÿ”„ Changed @@ -27,6 +49,7 @@ ๐Ÿš€ Improved - + - Improved draw of reaction options. [#1455](https://github.com/GetStream/stream-chat-flutter/pull/1455) ## 5.3.0 @@ -36,17 +59,22 @@ - Updated `photo_manager` dependency to `^2.5.2` ๐Ÿž Fixed -- [[#1424]](https://github.com/GetStream/stream-chat-flutter/issues/1424) Fixed a render issue when showing messages starting with 4 whitespaces. + +- [[#1424]](https://github.com/GetStream/stream-chat-flutter/issues/1424) Fixed a render issue when showing messages + starting with 4 whitespaces. - Fixed a bug where the `AttachmentPickerBottomSheet` was not able to identify the mobile browser. - Fixed uploading files on Windows - fixed temp file path. โœ… Added + - New `noPhotoOrVideoLabel` displayed when there is no files to choose. ## 5.2.0 โœ… Added -- Added a new `bottomRowBuilderWithDefaultWidget` parameter to `StreamMessageWidget` which contains a third parameter (default `BottomRow` widget with `copyWith` method available) to allow easier customization. + +- Added a new `bottomRowBuilderWithDefaultWidget` parameter to `StreamMessageWidget` which contains a third parameter ( + default `BottomRow` widget with `copyWith` method available) to allow easier customization. ๐Ÿ”„ Changed @@ -56,14 +84,20 @@ - Updated `dart_vlc` dependency to `^0.4.0` - Updated `file_picker` dependency to `^5.2.4` - Deprecated `StreamMessageWidget.bottomRowBuilder` in favor of `StreamMessageWidget.bottomRowBuilderWithDefaultWidget`. -- Deprecated `StreamMessageWidget.deletedBottomRowBuilder` in favor of `StreamMessageWidget.bottomRowBuilderWithDefaultWidget`. +- Deprecated `StreamMessageWidget.deletedBottomRowBuilder` in favor + of `StreamMessageWidget.bottomRowBuilderWithDefaultWidget`. - Deprecated `StreamMessageWidget.usernameBuilder` in favor of `StreamMessageWidget.bottomRowBuilderWithDefaultWidget`. ๐Ÿž Fixed -- [[#1379]](https://github.com/GetStream/stream-chat-flutter/issues/1379) Fixed "Issues with photo attachments on web", where the cached image attachment would not render while uploading. -- Fix render overflow issue with `MessageSearchListTileTitle`. It now uses `Text.rich` instead of `Row`. Better default behaviour and allows `TextOverflow`. -- [[1346]](https://github.com/GetStream/stream-chat-flutter/issues/1346) Fixed a render issue while uploading video on web. -- [[#1347]](https://github.com/GetStream/stream-chat-flutter/issues/1347) `onReply` not working in `AttachmentActionsModal` which is used by `StreamImageAttachment` and `StreamImageGroup`. + +- [[#1379]](https://github.com/GetStream/stream-chat-flutter/issues/1379) Fixed "Issues with photo attachments on web", + where the cached image attachment would not render while uploading. +- Fix render overflow issue with `MessageSearchListTileTitle`. It now uses `Text.rich` instead of `Row`. Better default + behaviour and allows `TextOverflow`. +- [[1346]](https://github.com/GetStream/stream-chat-flutter/issues/1346) Fixed a render issue while uploading video on + web. +- [[#1347]](https://github.com/GetStream/stream-chat-flutter/issues/1347) `onReply` not working + in `AttachmentActionsModal` which is used by `StreamImageAttachment` and `StreamImageGroup`. ## 5.1.0 diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart index b479b5cea..d7768a097 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart @@ -25,6 +25,13 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; const _kCommandTrigger = '/'; const _kMentionTrigger = '@'; +/// Signature for the function that determines if a [matchedUri] should be +/// previewed as an OG Attachment. +typedef OgPreviewFilter = bool Function( + Uri matchedUri, + String messageText, +); + /// Inactive state: /// /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_input.png) @@ -114,6 +121,7 @@ class StreamMessageInput extends StatefulWidget { this.sendMessageKeyPredicate = _defaultSendMessageKeyPredicate, this.clearQuotedMessageKeyPredicate = _defaultClearQuotedMessageKeyPredicate, + this.ogPreviewFilter = _defaultOgPreviewFilter, }); /// The predicate used to send a message on desktop/web @@ -259,6 +267,18 @@ class StreamMessageInput extends StatefulWidget { /// Callback for when the quoted message is cleared final VoidCallback? onQuotedMessageCleared; + /// The filter used to determine if a link should be shown as an OpenGraph + /// preview. + final OgPreviewFilter ogPreviewFilter; + + static bool _defaultOgPreviewFilter( + Uri matchedUri, + String messageText, + ) { + // Show the preview for all links + return true; + } + static bool _defaultValidator(Message message) => message.text?.isNotEmpty == true || message.attachments.isNotEmpty; @@ -990,11 +1010,13 @@ class StreamMessageInputState extends State if (_lastSearchedContainsUrlText == value) return; _lastSearchedContainsUrlText = value; - final matchedUrls = _urlRegex.allMatches(value).toList() - ..removeWhere((it) { - final _parsedMatch = Uri.tryParse(it.group(0) ?? '')?.withScheme; - return _parsedMatch?.host.split('.').last.isValidTLD() == false; - }); + final matchedUrls = _urlRegex.allMatches(value).where((it) { + final _parsedMatch = Uri.tryParse(it.group(0) ?? '')?.withScheme; + if (_parsedMatch == null) return false; + + return _parsedMatch.host.split('.').last.isValidTLD() && + widget.ogPreviewFilter.call(_parsedMatch, value); + }).toList(); // Reset the og attachment if the text doesn't contain any url if (matchedUrls.isEmpty || From 85ad3e541e51ceb60360f742d0cb7528449e3230 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 1 May 2023 19:44:35 +0530 Subject: [PATCH 10/51] chore: fix analysis Signed-off-by: xsahil03x --- packages/stream_chat/lib/src/core/models/own_user.dart | 1 - packages/stream_chat/test/src/core/models/own_user_test.dart | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/stream_chat/lib/src/core/models/own_user.dart b/packages/stream_chat/lib/src/core/models/own_user.dart index 60d152780..cfa7f79a5 100644 --- a/packages/stream_chat/lib/src/core/models/own_user.dart +++ b/packages/stream_chat/lib/src/core/models/own_user.dart @@ -1,5 +1,4 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:stream_chat/src/core/models/channel_mute.dart'; import 'package:stream_chat/src/core/util/serializer.dart'; import 'package:stream_chat/stream_chat.dart'; diff --git a/packages/stream_chat/test/src/core/models/own_user_test.dart b/packages/stream_chat/test/src/core/models/own_user_test.dart index e0dc7801e..73df4b4ea 100644 --- a/packages/stream_chat/test/src/core/models/own_user_test.dart +++ b/packages/stream_chat/test/src/core/models/own_user_test.dart @@ -1,5 +1,4 @@ import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat/src/core/models/channel_mute.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:test/test.dart'; From 2bd8defe6d8e07d2887e605f6bd5ea3ff7477514 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 2 May 2023 18:51:29 +0530 Subject: [PATCH 11/51] fix(llc): Error while hiding channel and clearing message history. Signed-off-by: xsahil03x --- packages/stream_chat/CHANGELOG.md | 5 +++++ packages/stream_chat/lib/src/client/channel.dart | 10 +--------- packages/stream_chat/lib/src/client/client.dart | 8 ++++---- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index d0b8ad326..f7094a534 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,5 +1,10 @@ ## Upcoming +๐Ÿž Fixed + +- [[#1355]](https://github.com/GetStream/stream-chat-flutter/issues/1355) Fixed error while hiding channel and clearing + message history. + โœ… Added - Expose `ChannelMute` class. [#1473](https://github.com/GetStream/stream-chat-flutter/issues/1473) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 7d8515c73..1d419cbb2 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -1485,19 +1485,11 @@ class Channel { /// will be removed for the user. Future hide({bool clearHistory = false}) async { _checkInitialized(); - final response = await _client.hideChannel( + return _client.hideChannel( id!, type, clearHistory: clearHistory, ); - if (clearHistory) { - state!.truncate(); - final cid = _cid; - if (cid != null) { - await _client.chatPersistenceClient?.deleteMessageByCid(cid); - } - } - return response; } /// Removes the hidden status for the channel. diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 4f2dbda3d..6feb16d69 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -1567,7 +1567,7 @@ class ClientState { _client.on(EventType.channelHidden).listen((event) async { final eventChannel = event.channel!; await _client.chatPersistenceClient?.deleteChannels([eventChannel.cid]); - channels[eventChannel.cid]?.dispose(); + channels.remove(eventChannel.cid)?.dispose(); }), ); } @@ -1606,7 +1606,7 @@ class ClientState { .listen((Event event) async { final eventChannel = event.channel!; await _client.chatPersistenceClient?.deleteChannels([eventChannel.cid]); - channels[eventChannel.cid]?.dispose(); + channels.remove(eventChannel.cid)?.dispose(); }), ); } @@ -1713,9 +1713,9 @@ class ClientState { _unreadChannelsController.close(); _totalUnreadCountController.close(); - final channels = this.channels.values.toList(); + final channels = this.channels.keys; for (final channel in channels) { - channel.dispose(); + this.channels.remove(channel)?.dispose(); } _channelsController.close(); } From 42e1112c504a374a317f550dbc163519b37cfd10 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 2 May 2023 18:52:13 +0530 Subject: [PATCH 12/51] fix(core): Channel doesn't auto display again after being hidden. Signed-off-by: xsahil03x --- .../stream_chat_flutter_core/CHANGELOG.md | 5 ++++ .../lib/src/stream_channel.dart | 26 ++++++++++++++----- .../stream_channel_list_event_handler.dart | 9 +++++-- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index 29b6a27fd..5a560da6e 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,3 +1,8 @@ +## Upcoming + +- [[#1356]](https://github.com/GetStream/stream-chat-flutter/issues/1356) Channel doesn't auto display again after being + hidden. + ## 6.0.0 - Updated dependencies to resolvable versions. diff --git a/packages/stream_chat_flutter_core/lib/src/stream_channel.dart b/packages/stream_chat_flutter_core/lib/src/stream_channel.dart index 9842c3e2a..bbfdea5bc 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_channel.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel.dart @@ -387,12 +387,20 @@ class StreamChannelState extends State { (it) => it.user.id == channel.client.state.currentUser?.id, ); - if (read != null && - !(channel.state!.messages - .any((it) => it.createdAt.compareTo(read.lastRead) > 0) && - channel.state!.messages - .any((it) => it.createdAt.compareTo(read.lastRead) <= 0))) { - _futures.add(_loadChannelAtTimestamp(read.lastRead)); + if (read == null) return; + + final messages = channel.state!.messages; + final lastRead = read.lastRead; + + final hasNewMessages = + messages.any((it) => it.createdAt.isAfter(lastRead)); + final hasOldMessages = + messages.any((it) => it.createdAt.isBeforeOrEqualTo(lastRead)); + + // Only load messages if the unread message is in-between the messages. + // Otherwise, we can just load the channel normally. + if (hasNewMessages && hasOldMessages) { + _futures.add(_loadChannelAtTimestamp(lastRead)); } } } @@ -449,3 +457,9 @@ class StreamChannelState extends State { return child; } } + +extension on DateTime { + bool isBeforeOrEqualTo(DateTime other) { + return isBefore(other) || isAtSameMomentAs(other); + } +} diff --git a/packages/stream_chat_flutter_core/lib/src/stream_channel_list_event_handler.dart b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_event_handler.dart index 92b5650bc..0fc34dfdf 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_channel_list_event_handler.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_event_handler.dart @@ -101,14 +101,19 @@ class StreamChannelListEventHandler { /// we are currently watching. /// /// By default, this moves the channel to the top of the list. - void onMessageNew(Event event, StreamChannelListController controller) { + void onMessageNew(Event event, StreamChannelListController controller) async { final channelCid = event.cid; if (channelCid == null) return; final channels = [...controller.currentItems]; final channelIndex = channels.indexWhere((it) => it.cid == channelCid); - if (channelIndex <= 0) return; + if (channelIndex <= 0) { + // If the channel is not in the list, It might be hidden. + // So, we just refresh the list. + await controller.refresh(resetValue: false); + return; + } final channel = channels.removeAt(channelIndex); channels.insert(0, channel); From d344355e911c0b18f007b6cbab7a5069a8fd1a1c Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 2 May 2023 19:41:41 +0530 Subject: [PATCH 13/51] feat(ui): add support for customizing messageInput hint. Signed-off-by: xsahil03x --- packages/stream_chat_flutter/CHANGELOG.md | 22 ++++++- .../message_input/stream_message_input.dart | 60 ++++++++++++++++--- 2 files changed, 72 insertions(+), 10 deletions(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index a5ec731e2..c735532b6 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -31,7 +31,27 @@ return true; ), ``` - + +- Added `StreamMessageInput.hintGetter` to allow users to customize the hint text of the message + input. [#1401](https://github.com/GetStream/stream-chat-flutter/issues/1401) + + ```dart + StreamMessageInput( + hintGetter: (context, hintType) { + switch (hintType) { + case HintType.searchGif: + return 'Custom Search Giphy'; + case HintType.addACommentOrSend: + return 'Custom Add a comment or send'; + case HintType.slowModeOn: + return 'Custom Slow mode is on'; + case HintType.writeAMessage: + return 'Custom Write a message'; + } + }, + ), + ``` + ๐Ÿ”„ Changed - Deprecated `MessageTheme.linkBackgroundColor` in favor of `MessageTheme.urlAttachmentBackgroundColor`. diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart index d7768a097..724535766 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart @@ -32,6 +32,26 @@ typedef OgPreviewFilter = bool Function( String messageText, ); +/// Different types of hints that can be shown in [StreamMessageInput]. +enum HintType { + /// Hint for [StreamMessageInput] when the command is enabled and the command + /// is 'giphy'. + searchGif, + + /// Hint for [StreamMessageInput] when there are attachments. + addACommentOrSend, + + /// Hint for [StreamMessageInput] when slow mode is enabled. + slowModeOn, + + /// Hint for [StreamMessageInput] when other conditions are not met. + writeAMessage, +} + +/// Function that returns the hint text for [StreamMessageInput] based on +/// [type]. +typedef HintGetter = String? Function(BuildContext context, HintType type); + /// Inactive state: /// /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_input.png) @@ -122,6 +142,7 @@ class StreamMessageInput extends StatefulWidget { this.clearQuotedMessageKeyPredicate = _defaultClearQuotedMessageKeyPredicate, this.ogPreviewFilter = _defaultOgPreviewFilter, + this.hintGetter = _defaultHintGetter, }); /// The predicate used to send a message on desktop/web @@ -271,6 +292,25 @@ class StreamMessageInput extends StatefulWidget { /// preview. final OgPreviewFilter ogPreviewFilter; + /// Returns the hint text for the message input. + final HintGetter hintGetter; + + static String? _defaultHintGetter( + BuildContext context, + HintType type, + ) { + switch (type) { + case HintType.searchGif: + return context.translations.searchGifLabel; + case HintType.addACommentOrSend: + return context.translations.addACommentOrSendLabel; + case HintType.slowModeOn: + return context.translations.slowModeOnLabel; + case HintType.writeAMessage: + return context.translations.writeAMessageLabel; + } + } + static bool _defaultOgPreviewFilter( Uri matchedUri, String messageText, @@ -982,18 +1022,20 @@ class StreamMessageInputState extends State leading: true, ); - String _getHint(BuildContext context) { + String? _getHint(BuildContext context) { + HintType hintType; + if (_commandEnabled && _effectiveController.message.command == 'giphy') { - return context.translations.searchGifLabel; - } - if (_effectiveController.attachments.isNotEmpty) { - return context.translations.addACommentOrSendLabel; - } - if (_timeOut != 0) { - return context.translations.slowModeOnLabel; + hintType = HintType.searchGif; + } else if (_effectiveController.attachments.isNotEmpty) { + hintType = HintType.addACommentOrSend; + } else if (_timeOut != 0) { + hintType = HintType.slowModeOn; + } else { + hintType = HintType.writeAMessage; } - return context.translations.writeAMessageLabel; + return widget.hintGetter.call(context, hintType); } String? _lastSearchedContainsUrlText; From dc02c497e5b664a6dc5acfb3e8ef430f2a7adbc6 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 2 May 2023 19:55:59 +0530 Subject: [PATCH 14/51] fix(llc): fix concurrent modification error. Signed-off-by: xsahil03x --- packages/stream_chat/lib/src/client/client.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 6feb16d69..26d229b43 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -1713,7 +1713,7 @@ class ClientState { _unreadChannelsController.close(); _totalUnreadCountController.close(); - final channels = this.channels.keys; + final channels = [...this.channels.keys]; for (final channel in channels) { this.channels.remove(channel)?.dispose(); } From b4413be47dc5e3920a083b076dbbd9e8885b0247 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 3 May 2023 15:47:53 +0530 Subject: [PATCH 15/51] feat(ui): update scrollable_positioned_list with the latest changes. Signed-off-by: xsahil03x --- .../src/positioned_list.dart | 93 +- .../src/scroll_view.dart | 28 +- .../src/scrollable_positioned_list.dart | 225 ++-- .../src/viewport.dart | 47 +- .../src/wrapping.dart | 1066 +++++++++++++++++ .../message_list_view/message_list_view.dart | 12 +- ...ontal_scrollable_positioned_list_test.dart | 37 +- .../positioned_list_test.dart | 51 + ...ersed_scrollable_positioned_list_test.dart | 3 +- .../scrollable_positioned_list_test.dart | 385 +++--- ...rated_scrollable_positioned_list_test.dart | 61 +- ...ontal_scrollable_positioned_list_test.dart | 3 +- .../shrink_wrap_position_list_test.dart | 473 ++++++++ ...nk_wrap_scrollable_position_list_test.dart | 246 ++++ 14 files changed, 2333 insertions(+), 397 deletions(-) create mode 100644 packages/stream_chat_flutter/lib/scrollable_positioned_list/src/wrapping.dart create mode 100644 packages/stream_chat_flutter/test/scrollable_positioned_list/shrink_wrap_position_list_test.dart create mode 100644 packages/stream_chat_flutter/test/scrollable_positioned_list/shrink_wrap_scrollable_position_list_test.dart diff --git a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/positioned_list.dart b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/positioned_list.dart index c62472caa..1ea7733e5 100644 --- a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/positioned_list.dart +++ b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/positioned_list.dart @@ -11,6 +11,7 @@ import 'package:stream_chat_flutter/scrollable_positioned_list/src/indexed_key.d import 'package:stream_chat_flutter/scrollable_positioned_list/src/item_positions_listener.dart'; import 'package:stream_chat_flutter/scrollable_positioned_list/src/item_positions_notifier.dart'; import 'package:stream_chat_flutter/scrollable_positioned_list/src/scroll_view.dart'; +import 'package:stream_chat_flutter/scrollable_positioned_list/src/wrapping.dart'; /// A list of widgets similar to [ListView], except scroll control /// and position reporting is based on index rather than pixel offset. @@ -35,28 +36,20 @@ class PositionedList extends StatefulWidget { this.alignment = 0, this.scrollDirection = Axis.vertical, this.reverse = false, + this.shrinkWrap = false, this.physics, this.padding, this.cacheExtent, this.semanticChildCount, - this.findChildIndexCallback, this.addSemanticIndexes = true, this.addRepaintBoundaries = true, this.addAutomaticKeepAlives = true, - this.keyboardDismissBehavior, - }) : assert((positionedIndex == 0) || (positionedIndex < itemCount), - 'positionedIndex cannot be 0 and must be smaller than itemCount'); - - /// Called to find the new index of a child based on its key in case of - /// reordering. - /// - /// If not provided, a child widget may not map to its existing [RenderObject] - /// when the order in which children are returned from [builder] changes. - /// This may result in state-loss. - /// - /// This callback should take an input [Key], and it should return the - /// index of the child element with that associated key, or null if not found. - final ChildIndexGetter? findChildIndexCallback; + this.findChildIndexCallback, + this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, + }) : assert( + (positionedIndex == 0) || (positionedIndex < itemCount), + 'positionedIndex must be 0 or a value less than itemCount', + ); /// Number of items the [itemBuilder] can produce. final int itemCount; @@ -98,6 +91,15 @@ class PositionedList extends StatefulWidget { /// See [ScrollView.reverse]. final bool reverse; + /// {@template flutter.widgets.scroll_view.shrinkWrap} + /// Whether the extent of the scroll view in the [scrollDirection] should be + /// determined by the contents being viewed. + /// + /// Defaults to false. + /// + /// See [ScrollView.shrinkWrap]. + final bool shrinkWrap; + /// How the scroll view should respond to user input. /// /// For example, determines how the scroll view continues to animate after the @@ -132,9 +134,22 @@ class PositionedList extends StatefulWidget { /// See [SliverChildBuilderDelegate.addAutomaticKeepAlives]. final bool addAutomaticKeepAlives; - /// [ScrollViewKeyboardDismissBehavior] the defines how this [PositionedList] will - /// dismiss the keyboard automatically. - final ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior; + /// Called to find the new index of a child based on its key in case of reordering. + /// + /// If not provided, a child widget may not map to its existing [RenderObject] + /// when the order of children returned from the children builder changes. + /// This may result in state-loss. + /// + /// This callback should take an input [Key], and it should return the + /// index of the child element with that associated key, or null if not found. + /// + /// See [SliverChildBuilderDelegate.findChildIndexCallback]. + final ChildIndexGetter? findChildIndexCallback; + + /// Defines how this [ScrollView] will dismiss the keyboard automatically. + /// + /// See [ScrollView.keyboardDismissBehavior]. + final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; @override State createState() => _PositionedListState(); @@ -175,12 +190,13 @@ class _PositionedListState extends State { anchor: widget.alignment, center: _centerKey, controller: scrollController, - keyboardDismissBehavior: widget.keyboardDismissBehavior, scrollDirection: widget.scrollDirection, reverse: widget.reverse, cacheExtent: widget.cacheExtent, physics: widget.physics, + shrinkWrap: widget.shrinkWrap, semanticChildCount: widget.semanticChildCount ?? widget.itemCount, + keyboardDismissBehavior: widget.keyboardDismissBehavior, slivers: [ if (widget.positionedIndex > 0) SliverPadding( @@ -196,9 +212,9 @@ class _PositionedListState extends State { ? widget.positionedIndex : widget.positionedIndex * 2, addSemanticIndexes: false, - findChildIndexCallback: widget.findChildIndexCallback, addRepaintBoundaries: widget.addRepaintBoundaries, addAutomaticKeepAlives: widget.addAutomaticKeepAlives, + findChildIndexCallback: widget.findChildIndexCallback, ), ), ), @@ -213,10 +229,10 @@ class _PositionedListState extends State { index + widget.positionedIndex * 2, ), childCount: widget.itemCount != 0 ? 1 : 0, - findChildIndexCallback: widget.findChildIndexCallback, addSemanticIndexes: false, addRepaintBoundaries: widget.addRepaintBoundaries, addAutomaticKeepAlives: widget.addAutomaticKeepAlives, + findChildIndexCallback: widget.findChildIndexCallback, ), ), ), @@ -234,10 +250,10 @@ class _PositionedListState extends State { childCount: widget.separatorBuilder == null ? widget.itemCount - widget.positionedIndex - 1 : 2 * (widget.itemCount - widget.positionedIndex - 1), - findChildIndexCallback: widget.findChildIndexCallback, addSemanticIndexes: false, addRepaintBoundaries: widget.addRepaintBoundaries, addAutomaticKeepAlives: widget.addAutomaticKeepAlives, + findChildIndexCallback: widget.findChildIndexCallback, ), ), ), @@ -319,25 +335,33 @@ class _PositionedListState extends State { if (!updateScheduled) { updateScheduled = true; SchedulerBinding.instance.addPostFrameCallback((_) { - if (registeredElements.value == null) { + final elements = registeredElements.value; + if (elements == null) { updateScheduled = false; return; } final positions = []; - RenderViewport? viewport; - for (final element in registeredElements.value!) { - final box = element.renderObject as RenderBox?; - viewport ??= RenderAbstractViewport.of(box) as RenderViewport?; - if (viewport == null || box == null) { - break; + RenderViewportBase? viewport; + for (final element in elements) { + final box = element.renderObject! as RenderBox; + viewport ??= RenderAbstractViewport.of(box) as RenderViewportBase?; + var anchor = 0.0; + if (viewport is RenderViewport) { + anchor = viewport.anchor; } - final key = element.widget.key as IndexedKey; + + if (viewport is CustomRenderViewport) { + anchor = viewport.anchor; + } + + final key = element.widget.key! as IndexedKey; + // Skip this element if `box` has never been laid out. + if (!box.hasSize) continue; if (widget.scrollDirection == Axis.vertical) { - final reveal = viewport.getOffsetToReveal(box, 0).offset; + final reveal = viewport!.getOffsetToReveal(box, 0).offset; if (!reveal.isFinite) continue; - final itemOffset = reveal - - viewport.offset.pixels + - viewport.anchor * viewport.size.height; + final itemOffset = + reveal - viewport.offset.pixels + anchor * viewport.size.height; positions.add(ItemPosition( index: key.index, itemLeadingEdge: itemOffset.round() / @@ -348,6 +372,7 @@ class _PositionedListState extends State { } else { final itemOffset = box.localToGlobal(Offset.zero, ancestor: viewport).dx; + if (!itemOffset.isFinite) continue; positions.add(ItemPosition( index: key.index, itemLeadingEdge: (widget.reverse diff --git a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scroll_view.dart b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scroll_view.dart index 13a7fdd39..911512495 100644 --- a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scroll_view.dart +++ b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scroll_view.dart @@ -5,13 +5,14 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:stream_chat_flutter/scrollable_positioned_list/src/viewport.dart'; +import 'package:stream_chat_flutter/scrollable_positioned_list/src/wrapping.dart'; -/// {@template custom_scroll_view} -/// A version of [CustomScrollView] that does not constrict the extents +/// {@template unbounded_custom_scroll_view} +/// A version of [CustomScrollView] that allows does not constrict the extents /// to be within 0 and 1. See [CustomScrollView] for more information. /// {@endtemplate} class UnboundedCustomScrollView extends CustomScrollView { - /// {@macro custom_scroll_view} + /// {@macro unbounded_custom_scroll_view} const UnboundedCustomScrollView({ super.key, super.scrollDirection, @@ -19,19 +20,19 @@ class UnboundedCustomScrollView extends CustomScrollView { super.controller, super.primary, super.physics, - super.shrinkWrap, + bool shrinkWrap = false, super.center, double anchor = 0.0, super.cacheExtent, super.slivers, super.semanticChildCount, super.dragStartBehavior, - ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior, - }) : _anchor = anchor, - super( - keyboardDismissBehavior: keyboardDismissBehavior ?? - ScrollViewKeyboardDismissBehavior.manual, - ); + super.keyboardDismissBehavior, + }) : _shrinkWrap = shrinkWrap, + _anchor = anchor, + super(shrinkWrap: false); + + final bool _shrinkWrap; // [CustomScrollView] enforces constraints on [CustomScrollView.anchor], so // we need our own version. @@ -49,11 +50,14 @@ class UnboundedCustomScrollView extends CustomScrollView { AxisDirection axisDirection, List slivers, ) { - if (shrinkWrap) { - return ShrinkWrappingViewport( + if (_shrinkWrap) { + return CustomShrinkWrappingViewport( axisDirection: axisDirection, offset: offset, slivers: slivers, + cacheExtent: cacheExtent, + center: center, + anchor: anchor, ); } return UnboundedViewport( diff --git a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scrollable_positioned_list.dart b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scrollable_positioned_list.dart index 5d307158b..b3785154c 100644 --- a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scrollable_positioned_list.dart +++ b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scrollable_positioned_list.dart @@ -37,6 +37,7 @@ class ScrollablePositionedList extends StatefulWidget { required this.itemBuilder, super.key, this.itemScrollController, + this.shrinkWrap = false, ItemPositionsListener? itemPositionsListener, this.initialScrollIndex = 0, this.initialAlignment = 0, @@ -50,7 +51,7 @@ class ScrollablePositionedList extends StatefulWidget { this.addRepaintBoundaries = true, this.minCacheExtent, this.findChildIndexCallback, - this.keyboardDismissBehavior, + this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, }) : itemPositionsNotifier = itemPositionsListener as ItemPositionsNotifier?, separatorBuilder = null; @@ -61,6 +62,7 @@ class ScrollablePositionedList extends StatefulWidget { required this.itemBuilder, required IndexedWidgetBuilder this.separatorBuilder, super.key, + this.shrinkWrap = false, this.itemScrollController, ItemPositionsListener? itemPositionsListener, this.initialScrollIndex = 0, @@ -75,24 +77,9 @@ class ScrollablePositionedList extends StatefulWidget { this.addRepaintBoundaries = true, this.minCacheExtent, this.findChildIndexCallback, - this.keyboardDismissBehavior, + this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, }) : itemPositionsNotifier = itemPositionsListener as ItemPositionsNotifier?; - /// Called to find the new index of a child based on its key in case of - /// reordering. - /// - /// If not provided, a child widget may not map to its existing [RenderObject] - /// when the order in which children are returned from [builder] changes. - /// This may result in state-loss. - /// - /// This callback should take an input [Key], and it should return the - /// index of the child element with that associated key, or null if not found. - final ChildIndexGetter? findChildIndexCallback; - - /// [ScrollViewKeyboardDismissBehavior] the defines how this [PositionedList] will - /// dismiss the keyboard automatically. - final ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior; - /// Number of items the [itemBuilder] can produce. final int itemCount; @@ -131,6 +118,15 @@ class ScrollablePositionedList extends StatefulWidget { /// See [ScrollView.reverse]. final bool reverse; + /// {@template flutter.widgets.scroll_view.shrinkWrap} + /// Whether the extent of the scroll view in the [scrollDirection] should be + /// determined by the contents being viewed. + /// + /// Defaults to false. + /// + /// See [ScrollView.shrinkWrap]. + final bool shrinkWrap; + /// How the scroll view should respond to user input. /// /// For example, determines how the scroll view continues to animate after the @@ -171,6 +167,23 @@ class ScrollablePositionedList extends StatefulWidget { /// cache extent. final double? minCacheExtent; + /// Called to find the new index of a child based on its key in case of reordering. + /// + /// If not provided, a child widget may not map to its existing [RenderObject] + /// when the order of children returned from the children builder changes. + /// This may result in state-loss. + /// + /// This callback should take an input [Key], and it should return the + /// index of the child element with that associated key, or null if not found. + /// + /// See [SliverChildBuilderDelegate.findChildIndexCallback]. + final ChildIndexGetter? findChildIndexCallback; + + /// Defines how this [ScrollView] will dismiss the keyboard automatically. + /// + /// See [ScrollView.keyboardDismissBehavior]. + final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; + @override State createState() => _ScrollablePositionedListState(); } @@ -233,11 +246,15 @@ class ItemScrollController { Curve curve = Curves.linear, List opacityAnimationWeights = const [40, 20, 40], }) { - assert(_scrollableListState != null, '_scrollableListState cannot be null'); - assert(opacityAnimationWeights.length == 3, - 'opacityAnimationWeights.length is not equal to 3'); - assert(duration > Duration.zero, - 'duration needs to be bigger than Duration.zero'); + assert( + _scrollableListState != null, + '''ScrollController must be attached to a ScrollablePositionedList to scroll.''', + ); + assert( + opacityAnimationWeights.length == 3, + 'opacityAnimationWeights must have exactly three elements.', + ); + assert(duration > Duration.zero, 'Duration must be greater than zero.'); return _scrollableListState!._scrollTo( index: index, alignment: alignment, @@ -249,7 +266,9 @@ class ItemScrollController { void _attach(_ScrollablePositionedListState scrollableListState) { assert( - _scrollableListState == null, '_scrollableListState needs to be null'); + _scrollableListState == null, + '''ScrollController must not be attached to multiple ScrollablePositionedLists.''', + ); _scrollableListState = scrollableListState; } @@ -273,11 +292,12 @@ class _ScrollablePositionedListState extends State bool _isTransitioning = false; + AnimationController? _animationController; + @override void initState() { super.initState(); - final ItemPosition? initialPosition = - PageStorage.of(context).readState(context); + final initialPosition = PageStorage.of(context).readState(context); primary ..target = initialPosition?.index ?? widget.initialScrollIndex ..alignment = initialPosition?.itemLeadingEdge ?? widget.initialAlignment; @@ -301,6 +321,7 @@ class _ScrollablePositionedListState extends State .removeListener(_updatePositions); secondary.itemPositionsNotifier.itemPositions .removeListener(_updatePositions); + _animationController?.dispose(); super.dispose(); } @@ -329,84 +350,90 @@ class _ScrollablePositionedListState extends State } @override - Widget build(BuildContext context) => LayoutBuilder( - builder: (context, constraints) { - final cacheExtent = _cacheExtent(constraints); - return GestureDetector( - onPanDown: (_) => _stopScroll(canceled: true), - excludeFromSemantics: true, - child: Stack( - children: [ + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final cacheExtent = _cacheExtent(constraints); + return GestureDetector( + onPanDown: (_) => _stopScroll(canceled: true), + excludeFromSemantics: true, + child: Stack( + children: [ + PostMountCallback( + key: primary.key, + callback: startAnimationCallback, + child: FadeTransition( + opacity: ReverseAnimation(opacity), + child: NotificationListener( + onNotification: (_) => _isTransitioning, + child: PositionedList( + itemBuilder: widget.itemBuilder, + separatorBuilder: widget.separatorBuilder, + itemCount: widget.itemCount, + positionedIndex: primary.target, + controller: primary.scrollController, + itemPositionsNotifier: primary.itemPositionsNotifier, + scrollDirection: widget.scrollDirection, + reverse: widget.reverse, + cacheExtent: cacheExtent, + alignment: primary.alignment, + physics: widget.physics, + shrinkWrap: widget.shrinkWrap, + addSemanticIndexes: widget.addSemanticIndexes, + semanticChildCount: widget.semanticChildCount, + padding: widget.padding, + addAutomaticKeepAlives: widget.addAutomaticKeepAlives, + addRepaintBoundaries: widget.addRepaintBoundaries, + findChildIndexCallback: widget.findChildIndexCallback, + keyboardDismissBehavior: widget.keyboardDismissBehavior, + ), + ), + ), + ), + if (_isTransitioning) PostMountCallback( - key: primary.key, + key: secondary.key, callback: startAnimationCallback, child: FadeTransition( - opacity: ReverseAnimation(opacity), + opacity: opacity, child: NotificationListener( - onNotification: (_) => _isTransitioning, + onNotification: (_) => false, child: PositionedList( - keyboardDismissBehavior: widget.keyboardDismissBehavior, itemBuilder: widget.itemBuilder, separatorBuilder: widget.separatorBuilder, itemCount: widget.itemCount, - positionedIndex: primary.target, - controller: primary.scrollController, - itemPositionsNotifier: primary.itemPositionsNotifier, + itemPositionsNotifier: secondary.itemPositionsNotifier, + positionedIndex: secondary.target, + controller: secondary.scrollController, scrollDirection: widget.scrollDirection, reverse: widget.reverse, cacheExtent: cacheExtent, - alignment: primary.alignment, + alignment: secondary.alignment, physics: widget.physics, + shrinkWrap: widget.shrinkWrap, addSemanticIndexes: widget.addSemanticIndexes, semanticChildCount: widget.semanticChildCount, padding: widget.padding, addAutomaticKeepAlives: widget.addAutomaticKeepAlives, addRepaintBoundaries: widget.addRepaintBoundaries, findChildIndexCallback: widget.findChildIndexCallback, + keyboardDismissBehavior: widget.keyboardDismissBehavior, ), ), ), ), - if (_isTransitioning) - PostMountCallback( - key: secondary.key, - callback: startAnimationCallback, - child: FadeTransition( - opacity: opacity, - child: NotificationListener( - onNotification: (_) => false, - child: PositionedList( - keyboardDismissBehavior: - widget.keyboardDismissBehavior, - itemBuilder: widget.itemBuilder, - separatorBuilder: widget.separatorBuilder, - itemCount: widget.itemCount, - itemPositionsNotifier: - secondary.itemPositionsNotifier, - positionedIndex: secondary.target, - controller: secondary.scrollController, - scrollDirection: widget.scrollDirection, - reverse: widget.reverse, - cacheExtent: cacheExtent, - alignment: secondary.alignment, - physics: widget.physics, - addSemanticIndexes: widget.addSemanticIndexes, - semanticChildCount: widget.semanticChildCount, - padding: widget.padding, - addAutomaticKeepAlives: widget.addAutomaticKeepAlives, - addRepaintBoundaries: widget.addRepaintBoundaries, - ), - ), - ), - ), - ], - ), - ); - }, - ); + ], + ), + ); + }, + ); + } double _cacheExtent(BoxConstraints constraints) => max( - constraints.maxHeight * _screenScrollCount, + (widget.scrollDirection == Axis.vertical + ? constraints.maxHeight + : constraints.maxWidth) * + _screenScrollCount, widget.minCacheExtent ?? 0, ); @@ -434,16 +461,19 @@ class _ScrollablePositionedListState extends State index = widget.itemCount - 1; } if (_isTransitioning) { + final scrollCompleter = Completer(); _stopScroll(canceled: true); - SchedulerBinding.instance.addPostFrameCallback((_) { - _startScroll( + SchedulerBinding.instance.addPostFrameCallback((_) async { + await _startScroll( index: index, alignment: alignment, duration: duration, curve: curve, opacityAnimationWeights: opacityAnimationWeights, ); + scrollCompleter.complete(); }); + await scrollCompleter.future; } else { await _startScroll( index: index, @@ -486,10 +516,11 @@ class _ScrollablePositionedListState extends State startAnimationCallback = () { SchedulerBinding.instance.addPostFrameCallback((_) { startAnimationCallback = () {}; - - opacity.parent = _opacityAnimation(opacityAnimationWeights).animate( - AnimationController(vsync: this, duration: duration)..forward(), - ); + _animationController?.dispose(); + _animationController = + AnimationController(vsync: this, duration: duration)..forward(); + opacity.parent = _opacityAnimation(opacityAnimationWeights) + .animate(_animationController!); secondary.scrollController.jumpTo(-direction * (_screenScrollCount * primary.scrollController.position.viewportDimension - @@ -532,17 +563,19 @@ class _ScrollablePositionedListState extends State } } - setState(() { - if (opacity.value >= 0.5) { - // Secondary [ListView] is more visible than the primary; make it the - // new primary. - final temp = primary; - primary = secondary; - secondary = temp; - } - _isTransitioning = false; - opacity.parent = const AlwaysStoppedAnimation(0); - }); + if (mounted) { + setState(() { + if (opacity.value >= 0.5) { + // Secondary [ListView] is more visible than the primary; make it the + // new primary. + final temp = primary; + primary = secondary; + secondary = temp; + } + _isTransitioning = false; + opacity.parent = const AlwaysStoppedAnimation(0); + }); + } } Animatable _opacityAnimation(List opacityAnimationWeights) { diff --git a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/viewport.dart b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/viewport.dart index 7d2d6b9fd..aac9acc9e 100644 --- a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/viewport.dart +++ b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/viewport.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// ignore_for_file: lines_longer_than_80_chars - import 'dart:math' as math; import 'package:flutter/rendering.dart'; @@ -15,7 +13,7 @@ import 'package:flutter/widgets.dart'; /// Version of [Viewport] with some modifications to how extents are /// computed to allow scroll extents outside 0 to 1. See [Viewport] /// for more information. -/// description +/// {@endtemplate} class UnboundedViewport extends Viewport { /// {@macro unbounded_viewport} UnboundedViewport({ @@ -37,15 +35,16 @@ class UnboundedViewport extends Viewport { double get anchor => _anchor; @override - RenderViewport createRenderObject(BuildContext context) => - UnboundedRenderViewport( - axisDirection: axisDirection, - crossAxisDirection: crossAxisDirection ?? - Viewport.getDefaultCrossAxisDirection(context, axisDirection), - anchor: anchor, - offset: offset, - cacheExtent: cacheExtent, - ); + RenderViewport createRenderObject(BuildContext context) { + return UnboundedRenderViewport( + axisDirection: axisDirection, + crossAxisDirection: crossAxisDirection ?? + Viewport.getDefaultCrossAxisDirection(context, axisDirection), + anchor: anchor, + offset: offset, + cacheExtent: cacheExtent, + ); + } } /// A render object that is bigger on the inside. @@ -137,14 +136,20 @@ class UnboundedRenderViewport extends RenderViewport { @override void performLayout() { if (center == null) { - assert(firstChild == null, 'firstChild cannot be null'); + assert( + firstChild == null, + 'A RenderViewport with no center render object must have no children.', + ); _minScrollExtent = 0.0; _maxScrollExtent = 0.0; _hasVisualOverflow = false; offset.applyContentDimensions(0, 0); return; } - assert(center!.parent == this, 'center.parent cannot be equal to this'); + assert( + center!.parent == this, + '''The "center" property of a RenderViewport must be a child of the viewport.''', + ); late double mainAxisExtent; late double crossAxisExtent; @@ -186,7 +191,7 @@ class UnboundedRenderViewport extends RenderViewport { } while (count < _maxLayoutCycles); assert(() { if (count >= _maxLayoutCycles) { - assert(count != 1, 'count not equal to 1'); + assert(count != 1); throw FlutterError( 'A RenderViewport exceeded its maximum number of layout cycles.\n' 'RenderViewport render objects, during layout, can retry if either their ' @@ -207,7 +212,7 @@ class UnboundedRenderViewport extends RenderViewport { ); } return true; - }(), 'count needs to be bigger than _maxLayoutCycles'); + }()); } double _attemptLayout( @@ -215,11 +220,11 @@ class UnboundedRenderViewport extends RenderViewport { double crossAxisExtent, double correctedOffset, ) { - assert(!mainAxisExtent.isNaN, 'assert mainAxisExtent.isNaN'); - assert(mainAxisExtent >= 0.0, 'assert mainAxisExtent >= 0.0'); - assert(crossAxisExtent.isFinite, 'assert crossAxisExtent.isFinite'); - assert(crossAxisExtent >= 0.0, 'assert crossAxisExtent >= 0.0'); - assert(correctedOffset.isFinite, 'assert correctedOffset.isFinite'); + assert(!mainAxisExtent.isNaN, 'The main axis extent cannot be NaN.'); + assert(mainAxisExtent >= 0.0, 'The main axis extent cannot be negative.'); + assert(crossAxisExtent.isFinite, 'The cross axis extent must be finite.'); + assert(crossAxisExtent >= 0.0, 'The cross axis extent cannot be negative.'); + assert(correctedOffset.isFinite, 'The corrected offset must be finite.'); _minScrollExtent = 0.0; _maxScrollExtent = 0.0; _hasVisualOverflow = false; diff --git a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/wrapping.dart b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/wrapping.dart new file mode 100644 index 000000000..c3a8129ce --- /dev/null +++ b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/wrapping.dart @@ -0,0 +1,1066 @@ +import 'dart:math' as math; + +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +/// A widget that is bigger on the inside and shrink wraps its children in the +/// main axis. +/// +/// [ShrinkWrappingViewport] displays a subset of its children according to its +/// own dimensions and the given [offset]. As the offset varies, different +/// children are visible through the viewport. +/// +/// [ShrinkWrappingViewport] differs from [Viewport] in that [Viewport] expands +/// to fill the main axis whereas [ShrinkWrappingViewport] sizes itself to match +/// its children in the main axis. This shrink wrapping behavior is expensive +/// because the children, and hence the viewport, could potentially change size +/// whenever the [offset] changes (e.g., because of a collapsing header). +/// +/// [ShrinkWrappingViewport] cannot contain box children directly. Instead, use +/// a [SliverList], [SliverFixedExtentList], [SliverGrid], or a +/// [SliverToBoxAdapter], for example. +/// +/// See also: +/// +/// * [ListView], [PageView], [GridView], and [CustomScrollView], which combine +/// [Scrollable] and [ShrinkWrappingViewport] into widgets that are easier to +/// use. +/// * [SliverToBoxAdapter], which allows a box widget to be placed inside a +/// sliver context (the opposite of this widget). +/// * [Viewport], a viewport that does not shrink-wrap its contents. +class CustomShrinkWrappingViewport extends CustomViewport { + /// Creates a widget that is bigger on the inside and shrink wraps its + /// children in the main axis. + /// + /// The viewport listens to the [offset], which means you do not need to + /// rebuild this widget when the [offset] changes. + /// + /// The [offset] argument must not be null. + CustomShrinkWrappingViewport({ + super.key, + super.axisDirection, + super.crossAxisDirection, + double anchor = 0.0, + required super.offset, + List? children, + super.center, + super.cacheExtent, + super.slivers, + }) : _anchor = anchor; + + // [Viewport] enforces constraints on [Viewport.anchor], so we need our own + // version. + final double _anchor; + + @override + double get anchor => _anchor; + + @override + CustomRenderShrinkWrappingViewport createRenderObject(BuildContext context) { + return CustomRenderShrinkWrappingViewport( + axisDirection: axisDirection, + crossAxisDirection: crossAxisDirection ?? + Viewport.getDefaultCrossAxisDirection(context, axisDirection), + offset: offset, + anchor: anchor, + cacheExtent: cacheExtent, + ); + } + + @override + void updateRenderObject( + BuildContext context, + CustomRenderShrinkWrappingViewport renderObject, + ) { + renderObject + ..axisDirection = axisDirection + ..crossAxisDirection = crossAxisDirection ?? + Viewport.getDefaultCrossAxisDirection(context, axisDirection) + ..anchor = anchor + ..offset = offset + ..cacheExtent = cacheExtent + ..cacheExtentStyle = cacheExtentStyle + ..clipBehavior = clipBehavior; + } +} + +/// A render object that is bigger on the inside and shrink wraps its children +/// in the main axis. +/// +/// [RenderShrinkWrappingViewport] displays a subset of its children according +/// to its own dimensions and the given [offset]. As the offset varies, different +/// children are visible through the viewport. +/// +/// [RenderShrinkWrappingViewport] differs from [RenderViewport] in that +/// [RenderViewport] expands to fill the main axis whereas +/// [RenderShrinkWrappingViewport] sizes itself to match its children in the +/// main axis. This shrink wrapping behavior is expensive because the children, +/// and hence the viewport, could potentially change size whenever the [offset] +/// changes (e.g., because of a collapsing header). +/// +/// [RenderShrinkWrappingViewport] cannot contain [RenderBox] children directly. +/// Instead, use a [RenderSliverList], [RenderSliverFixedExtentList], +/// [RenderSliverGrid], or a [RenderSliverToBoxAdapter], for example. +/// +/// See also: +/// +/// * [RenderViewport], a viewport that does not shrink-wrap its contents. +/// * [RenderSliver], which explains more about the Sliver protocol. +/// * [RenderBox], which explains more about the Box protocol. +/// * [RenderSliverToBoxAdapter], which allows a [RenderBox] object to be +/// placed inside a [RenderSliver] (the opposite of this class). +class CustomRenderShrinkWrappingViewport extends CustomRenderViewport { + /// Creates a viewport (for [RenderSliver] objects) that shrink-wraps its + /// contents. + /// + /// The [offset] must be specified. For testing purposes, consider passing a + /// [ViewportOffset.zero] or [ViewportOffset.fixed]. + CustomRenderShrinkWrappingViewport({ + super.axisDirection, + required super.crossAxisDirection, + required super.offset, + double anchor = 0.0, + super.children, + super.center, + super.cacheExtent, + }) : _anchor = anchor; + + double _anchor; + + @override + double get anchor => _anchor; + + @override + bool get sizedByParent => false; + + double lastMainAxisExtent = -1; + + @override + set anchor(double value) { + if (value == _anchor) return; + _anchor = value; + markNeedsLayout(); + } + + late double _shrinkWrapExtent; + + /// This value is set during layout based on the [CacheExtentStyle]. + /// + /// When the style is [CacheExtentStyle.viewport], it is the main axis extent + /// of the viewport multiplied by the requested cache extent, which is still + /// expressed in pixels. + double? _calculatedCacheExtent; + + /// While List in a wrapping container, eg. ListView๏ผŒthe mainAxisExtent will + /// be infinite. This time need to change mainAxisExtent to this value. + final double _maxMainAxisExtent = double.maxFinite; + + @override + void performLayout() { + if (center == null) { + assert(firstChild == null, 'center must be null if children are present'); + _minScrollExtent = 0.0; + _maxScrollExtent = 0.0; + _hasVisualOverflow = false; + offset.applyContentDimensions(0, 0); + return; + } + + assert(center!.parent == this, 'center must be a child of the viewport'); + + final constraints = this.constraints; + if (firstChild == null) { + switch (axis) { + case Axis.vertical: + assert( + constraints.hasBoundedWidth, + 'Vertical viewport was given ' + 'unbounded width.\n' + 'Viewports expand in the cross axis to fill their container and ' + 'constrain their children to match their extent in the cross axis. ' + 'In this case, a vertical viewport was given an unlimited amount ' + 'of horizontal space in which to expand.', + ); + size = Size(constraints.maxWidth, constraints.minHeight); + break; + case Axis.horizontal: + assert( + constraints.hasBoundedHeight, + 'Horizontal viewport was given ' + 'unbounded height.\n' + 'Viewports expand in the cross axis to fill their container and ' + 'constrain their children to match their extent in the cross axis. ' + 'In this case, a horizontal viewport was given an unlimited amount ' + 'of vertical space in which to expand.', + ); + size = Size(constraints.minWidth, constraints.maxHeight); + break; + } + offset.applyViewportDimension(0); + _maxScrollExtent = 0.0; + _shrinkWrapExtent = 0.0; + _hasVisualOverflow = false; + offset.applyContentDimensions(0, 0); + return; + } + + double mainAxisExtent; + final double crossAxisExtent; + switch (axis) { + case Axis.vertical: + assert( + constraints.hasBoundedWidth, + 'Vertical viewport was given ' + 'unbounded width.\n' + 'Viewports expand in the cross axis to fill their container and ' + 'constrain their children to match their extent in the cross axis. ' + 'In this case, a vertical viewport was given an unlimited amount ' + 'of horizontal space in which to expand.', + ); + mainAxisExtent = constraints.maxHeight; + crossAxisExtent = constraints.maxWidth; + break; + case Axis.horizontal: + assert( + constraints.hasBoundedHeight, + 'Horizontal viewport was given ' + 'unbounded height.\n' + 'Viewports expand in the cross axis to fill their container and ' + 'constrain their children to match their extent in the cross axis. ' + 'In this case, a horizontal viewport was given an unlimited amount ' + 'of vertical space in which to expand.', + ); + mainAxisExtent = constraints.maxWidth; + crossAxisExtent = constraints.maxHeight; + break; + } + + if (mainAxisExtent.isInfinite) { + mainAxisExtent = _maxMainAxisExtent; + } + + final centerOffsetAdjustment = center!.centerOffsetAdjustment; + + double correction; + double effectiveExtent; + do { + correction = _attemptLayout( + mainAxisExtent, + crossAxisExtent, + offset.pixels + centerOffsetAdjustment, + ); + if (correction != 0.0) { + offset.correctBy(correction); + } else { + switch (axis) { + case Axis.vertical: + effectiveExtent = constraints.constrainHeight(_shrinkWrapExtent); + break; + case Axis.horizontal: + effectiveExtent = constraints.constrainWidth(_shrinkWrapExtent); + break; + } + // *** Difference from [RenderViewport]. + final top = _minScrollExtent + mainAxisExtent * anchor; + final bottom = _maxScrollExtent - mainAxisExtent * (1.0 - anchor); + + final maxScrollOffset = math.max(math.min(0, top), bottom); + final minScrollOffset = math.min(top, maxScrollOffset); + + final didAcceptViewportDimension = + offset.applyViewportDimension(effectiveExtent); + final didAcceptContentDimension = + offset.applyContentDimensions(minScrollOffset, maxScrollOffset); + if (didAcceptViewportDimension && didAcceptContentDimension) { + break; + } + } + } while (true); + switch (axis) { + case Axis.vertical: + size = + constraints.constrainDimensions(crossAxisExtent, effectiveExtent); + break; + case Axis.horizontal: + size = + constraints.constrainDimensions(effectiveExtent, crossAxisExtent); + break; + } + } + + double _attemptLayout( + double mainAxisExtent, + double crossAxisExtent, + double correctedOffset, + ) { + assert(!mainAxisExtent.isNaN, 'The maxExtent of $this has not been set.'); + assert(mainAxisExtent >= 0.0, 'The maxExtent of $this is negative.'); + assert( + crossAxisExtent.isFinite, + 'The crossAxisExtent of $this is not finite.', + ); + assert(crossAxisExtent >= 0.0, 'The crossAxisExtent of $this is negative.'); + assert( + correctedOffset.isFinite, + 'The correctedOffset of $this is not finite.', + ); + _minScrollExtent = 0.0; + _maxScrollExtent = 0.0; + _hasVisualOverflow = false; + _shrinkWrapExtent = 0.0; + + // centerOffset is the offset from the leading edge of the RenderViewport + // to the zero scroll offset (the line between the forward slivers and the + // reverse slivers). + final centerOffset = mainAxisExtent * anchor - correctedOffset; + final reverseDirectionRemainingPaintExtent = + centerOffset.clamp(0.0, mainAxisExtent); + final forwardDirectionRemainingPaintExtent = + (mainAxisExtent - centerOffset).clamp(0.0, mainAxisExtent); + + switch (cacheExtentStyle) { + case CacheExtentStyle.pixel: + _calculatedCacheExtent = cacheExtent; + break; + case CacheExtentStyle.viewport: + _calculatedCacheExtent = mainAxisExtent * cacheExtent!; + break; + } + + final fullCacheExtent = mainAxisExtent + 2 * _calculatedCacheExtent!; + final centerCacheOffset = centerOffset + _calculatedCacheExtent!; + final reverseDirectionRemainingCacheExtent = + centerCacheOffset.clamp(0.0, fullCacheExtent); + final forwardDirectionRemainingCacheExtent = + (fullCacheExtent - centerCacheOffset).clamp(0.0, fullCacheExtent); + + final leadingNegativeChild = childBefore(center!); + + if (leadingNegativeChild != null) { + // negative scroll offsets + final result = layoutChildSequence( + child: leadingNegativeChild, + scrollOffset: math.max(mainAxisExtent, centerOffset) - mainAxisExtent, + overlap: 0, + layoutOffset: forwardDirectionRemainingPaintExtent, + remainingPaintExtent: reverseDirectionRemainingPaintExtent, + mainAxisExtent: mainAxisExtent, + crossAxisExtent: crossAxisExtent, + growthDirection: GrowthDirection.reverse, + advance: childBefore, + remainingCacheExtent: reverseDirectionRemainingCacheExtent, + cacheOrigin: (mainAxisExtent - centerOffset) + .clamp(-_calculatedCacheExtent!, 0.0), + ); + if (result != 0.0) return -result; + } + + // positive scroll offsets + return layoutChildSequence( + child: center, + scrollOffset: math.max(0, -centerOffset), + overlap: leadingNegativeChild == null ? math.min(0, -centerOffset) : 0.0, + layoutOffset: centerOffset >= mainAxisExtent + ? centerOffset + : reverseDirectionRemainingPaintExtent, + remainingPaintExtent: forwardDirectionRemainingPaintExtent, + mainAxisExtent: mainAxisExtent, + crossAxisExtent: crossAxisExtent, + growthDirection: GrowthDirection.forward, + advance: childAfter, + remainingCacheExtent: forwardDirectionRemainingCacheExtent, + cacheOrigin: centerOffset.clamp(-_calculatedCacheExtent!, 0.0), + ); + } + + @override + bool get hasVisualOverflow => _hasVisualOverflow; + + @override + void updateOutOfBandData( + GrowthDirection growthDirection, + SliverGeometry childLayoutGeometry, + ) { + switch (growthDirection) { + case GrowthDirection.forward: + _maxScrollExtent += childLayoutGeometry.scrollExtent; + break; + case GrowthDirection.reverse: + _minScrollExtent -= childLayoutGeometry.scrollExtent; + break; + } + if (childLayoutGeometry.hasVisualOverflow) _hasVisualOverflow = true; + _shrinkWrapExtent += childLayoutGeometry.maxPaintExtent; + growSize = _shrinkWrapExtent; + } + + @override + String labelForChild(int index) => 'child $index'; +} + +/// A widget that is bigger on the inside. +/// +/// [Viewport] is the visual workhorse of the scrolling machinery. It displays a +/// subset of its children according to its own dimensions and the given +/// [offset]. As the offset varies, different children are visible through +/// the viewport. +/// +/// [Viewport] hosts a bidirectional list of slivers, anchored on a [center] +/// sliver, which is placed at the zero scroll offset. The center widget is +/// displayed in the viewport according to the [anchor] property. +/// +/// Slivers that are earlier in the child list than [center] are displayed in +/// reverse order in the reverse [axisDirection] starting from the [center]. For +/// example, if the [axisDirection] is [AxisDirection.down], the first sliver +/// before [center] is placed above the [center]. The slivers that are later in +/// the child list than [center] are placed in order in the [axisDirection]. For +/// example, in the preceding scenario, the first sliver after [center] is +/// placed below the [center]. +/// +/// [Viewport] cannot contain box children directly. Instead, use a +/// [SliverList], [SliverFixedExtentList], [SliverGrid], or a +/// [SliverToBoxAdapter], for example. +/// +/// See also: +/// +/// * [ListView], [PageView], [GridView], and [CustomScrollView], which combine +/// [Scrollable] and [Viewport] into widgets that are easier to use. +/// * [SliverToBoxAdapter], which allows a box widget to be placed inside a +/// sliver context (the opposite of this widget). +/// * [ShrinkWrappingViewport], a variant of [Viewport] that shrink-wraps its +/// contents along the main axis. +abstract class CustomViewport extends MultiChildRenderObjectWidget { + /// Creates a widget that is bigger on the inside. + /// + /// The viewport listens to the [offset], which means you do not need to + /// rebuild this widget when the [offset] changes. + /// + /// The [offset] argument must not be null. + /// + /// The [cacheExtent] must be specified if the [cacheExtentStyle] is + /// not [CacheExtentStyle.pixel]. + CustomViewport({ + super.key, + this.axisDirection = AxisDirection.down, + this.crossAxisDirection, + this.anchor = 0.0, + required this.offset, + this.center, + this.cacheExtent, + this.cacheExtentStyle = CacheExtentStyle.pixel, + this.clipBehavior = Clip.hardEdge, + List slivers = const [], + }) : assert( + center == null || + slivers.where((Widget child) => child.key == center).length == 1, + 'There should be at most one child with the same key as the center child: $center', + ), + assert( + cacheExtentStyle != CacheExtentStyle.viewport || cacheExtent != null, + 'A cacheExtent is required when using cacheExtentStyle.viewport', + ), + super(children: slivers); + + /// The direction in which the [offset]'s [ViewportOffset.pixels] increases. + /// + /// For example, if the [axisDirection] is [AxisDirection.down], a scroll + /// offset of zero is at the top of the viewport and increases towards the + /// bottom of the viewport. + final AxisDirection axisDirection; + + /// The direction in which child should be laid out in the cross axis. + /// + /// If the [axisDirection] is [AxisDirection.down] or [AxisDirection.up], this + /// property defaults to [AxisDirection.left] if the ambient [Directionality] + /// is [TextDirection.rtl] and [AxisDirection.right] if the ambient + /// [Directionality] is [TextDirection.ltr]. + /// + /// If the [axisDirection] is [AxisDirection.left] or [AxisDirection.right], + /// this property defaults to [AxisDirection.down]. + final AxisDirection? crossAxisDirection; + + /// The relative position of the zero scroll offset. + /// + /// For example, if [anchor] is 0.5 and the [axisDirection] is + /// [AxisDirection.down] or [AxisDirection.up], then the zero scroll offset is + /// vertically centered within the viewport. If the [anchor] is 1.0, and the + /// [axisDirection] is [AxisDirection.right], then the zero scroll offset is + /// on the left edge of the viewport. + final double anchor; + + /// Which part of the content inside the viewport should be visible. + /// + /// The [ViewportOffset.pixels] value determines the scroll offset that the + /// viewport uses to select which part of its content to display. As the user + /// scrolls the viewport, this value changes, which changes the content that + /// is displayed. + /// + /// Typically a [ScrollPosition]. + final ViewportOffset offset; + + /// The first child in the [GrowthDirection.forward] growth direction. + /// + /// Children after [center] will be placed in the [axisDirection] relative to + /// the [center]. Children before [center] will be placed in the opposite of + /// the [axisDirection] relative to the [center]. + /// + /// The [center] must be the key of a child of the viewport. + final Key? center; + + /// {@macro flutter.rendering.RenderViewportBase.cacheExtent} + /// + /// See also: + /// + /// * [cacheExtentStyle], which controls the units of the [cacheExtent]. + final double? cacheExtent; + + /// {@macro flutter.rendering.RenderViewportBase.cacheExtentStyle} + final CacheExtentStyle cacheExtentStyle; + + /// {@macro flutter.material.Material.clipBehavior} + /// + /// Defaults to [Clip.hardEdge]. + final Clip clipBehavior; + + /// Given a [BuildContext] and an [AxisDirection], determine the correct cross + /// axis direction. + /// + /// This depends on the [Directionality] if the `axisDirection` is vertical; + /// otherwise, the default cross axis direction is downwards. + static AxisDirection getDefaultCrossAxisDirection( + BuildContext context, + AxisDirection axisDirection, + ) { + switch (axisDirection) { + case AxisDirection.up: + assert(debugCheckHasDirectionality( + context, + why: + "to determine the cross-axis direction when the viewport has an 'up' axisDirection", + alternative: + "Alternatively, consider specifying the 'crossAxisDirection' argument on the Viewport.", + )); + return textDirectionToAxisDirection(Directionality.of(context)); + case AxisDirection.right: + return AxisDirection.down; + case AxisDirection.down: + assert(debugCheckHasDirectionality( + context, + why: + "to determine the cross-axis direction when the viewport has a 'down' axisDirection", + alternative: + "Alternatively, consider specifying the 'crossAxisDirection' argument on the Viewport.", + )); + return textDirectionToAxisDirection(Directionality.of(context)); + case AxisDirection.left: + return AxisDirection.down; + } + } + + @override + CustomRenderViewport createRenderObject(BuildContext context); + + @override + _ViewportElement createElement() => _ViewportElement(this); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(EnumProperty('axisDirection', axisDirection)) + ..add(EnumProperty( + 'crossAxisDirection', + crossAxisDirection, + defaultValue: null, + )) + ..add(DoubleProperty('anchor', anchor)) + ..add(DiagnosticsProperty('offset', offset)); + if (center != null) { + properties.add(DiagnosticsProperty('center', center)); + } else if (children.isNotEmpty && children.first.key != null) { + properties.add(DiagnosticsProperty( + 'center', + children.first.key, + tooltip: 'implicit', + )); + } + properties + ..add(DiagnosticsProperty('cacheExtent', cacheExtent)) + ..add(DiagnosticsProperty( + 'cacheExtentStyle', + cacheExtentStyle, + )); + } +} + +class _ViewportElement extends MultiChildRenderObjectElement { + /// Creates an element that uses the given widget as its configuration. + _ViewportElement(CustomViewport super.widget); + + @override + CustomViewport get widget => super.widget as CustomViewport; + + @override + CustomRenderViewport get renderObject => + super.renderObject as CustomRenderViewport; + + @override + void mount(Element? parent, dynamic newSlot) { + super.mount(parent, newSlot); + _updateCenter(); + } + + @override + void update(MultiChildRenderObjectWidget newWidget) { + super.update(newWidget); + _updateCenter(); + } + + void _updateCenter() { + if (widget.center != null) { + renderObject.center = children + .singleWhere((Element element) => element.widget.key == widget.center) + .renderObject as RenderSliver?; + } else if (children.isNotEmpty) { + renderObject.center = children.first.renderObject as RenderSliver?; + } else { + renderObject.center = null; + } + } + + @override + void debugVisitOnstageChildren(ElementVisitor visitor) { + children.where((Element e) { + final renderSliver = e.renderObject! as RenderSliver; + return renderSliver.geometry!.visible; + }).forEach(visitor); + } +} + +class CustomSliverPhysicalContainerParentData + extends SliverPhysicalContainerParentData { + /// The position of the child relative to the zero scroll offset. + /// + /// The number of pixels from from the zero scroll offset of the parent sliver + /// (the line at which its [SliverConstraints.scrollOffset] is zero) to the + /// side of the child closest to that offset. A [layoutOffset] can be null + /// when it cannot be determined. The value will be set after layout. + /// + /// In a typical list, this does not change as the parent is scrolled. + /// + /// Defaults to null. + double? layoutOffset; + + GrowthDirection? growthDirection; +} + +/// A render object that is bigger on the inside. +/// +/// [RenderViewport] is the visual workhorse of the scrolling machinery. It +/// displays a subset of its children according to its own dimensions and the +/// given [offset]. As the offset varies, different children are visible through +/// the viewport. +/// +/// [RenderViewport] hosts a bidirectional list of slivers, anchored on a +/// [center] sliver, which is placed at the zero scroll offset. The center +/// widget is displayed in the viewport according to the [anchor] property. +/// +/// Slivers that are earlier in the child list than [center] are displayed in +/// reverse order in the reverse [axisDirection] starting from the [center]. For +/// example, if the [axisDirection] is [AxisDirection.down], the first sliver +/// before [center] is placed above the [center]. The slivers that are later in +/// the child list than [center] are placed in order in the [axisDirection]. For +/// example, in the preceding scenario, the first sliver after [center] is +/// placed below the [center]. +/// +/// [RenderViewport] cannot contain [RenderBox] children directly. Instead, use +/// a [RenderSliverList], [RenderSliverFixedExtentList], [RenderSliverGrid], or +/// a [RenderSliverToBoxAdapter], for example. +/// +/// See also: +/// +/// * [RenderSliver], which explains more about the Sliver protocol. +/// * [RenderBox], which explains more about the Box protocol. +/// * [RenderSliverToBoxAdapter], which allows a [RenderBox] object to be +/// placed inside a [RenderSliver] (the opposite of this class). +/// * [RenderShrinkWrappingViewport], a variant of [RenderViewport] that +/// shrink-wraps its contents along the main axis. +abstract class CustomRenderViewport + extends RenderViewportBase { + /// Creates a viewport for [RenderSliver] objects. + /// + /// If the [center] is not specified, then the first child in the `children` + /// list, if any, is used. + /// + /// The [offset] must be specified. For testing purposes, consider passing a + /// [ViewportOffset.zero] or [ViewportOffset.fixed]. + CustomRenderViewport({ + super.axisDirection, + required super.crossAxisDirection, + required super.offset, + double anchor = 0.0, + List? children, + RenderSliver? center, + super.cacheExtent, + super.cacheExtentStyle, + super.clipBehavior, + }) : assert( + anchor >= 0.0 && anchor <= 1.0, + 'Anchor must be between 0.0 and 1.0.', + ), + assert( + cacheExtentStyle != CacheExtentStyle.viewport || cacheExtent != null, + 'A cacheExtent is required when using CacheExtentStyle.viewport.', + ), + _center = center { + addAll(children); + if (center == null && firstChild != null) _center = firstChild; + } + + /// If a [RenderAbstractViewport] overrides + /// [RenderObject.describeSemanticsConfiguration] to add the [SemanticsTag] + /// [useTwoPaneSemantics] to its [SemanticsConfiguration], two semantics nodes + /// will be used to represent the viewport with its associated scrolling + /// actions in the semantics tree. + /// + /// Two semantics nodes (an inner and an outer node) are necessary to exclude + /// certain child nodes (via the [excludeFromScrolling] tag) from the + /// scrollable area for semantic purposes: The [SemanticsNode]s of children + /// that should be excluded from scrolling will be attached to the outer node. + /// The semantic scrolling actions and the [SemanticsNode]s of scrollable + /// children will be attached to the inner node, which itself is a child of + /// the outer node. + /// + /// See also: + /// + /// * [RenderViewportBase.describeSemanticsConfiguration], which adds this + /// tag to its [SemanticsConfiguration]. + static const SemanticsTag useTwoPaneSemantics = + SemanticsTag('RenderViewport.twoPane'); + + /// When a top-level [SemanticsNode] below a [RenderAbstractViewport] is + /// tagged with [excludeFromScrolling] it will not be part of the scrolling + /// area for semantic purposes. + /// + /// This behavior is only active if the [RenderAbstractViewport] + /// tagged its [SemanticsConfiguration] with [useTwoPaneSemantics]. + /// Otherwise, the [excludeFromScrolling] tag is ignored. + /// + /// As an example, a [RenderSliver] that stays on the screen within a + /// [Scrollable] even though the user has scrolled past it (e.g. a pinned app + /// bar) can tag its [SemanticsNode] with [excludeFromScrolling] to indicate + /// that it should no longer be considered for semantic actions related to + /// scrolling. + static const SemanticsTag excludeFromScrolling = + SemanticsTag('RenderViewport.excludeFromScrolling'); + + @override + void setupParentData(RenderObject child) { + if (child.parentData is! CustomSliverPhysicalContainerParentData) { + child.parentData = CustomSliverPhysicalContainerParentData(); + } + } + + /// The relative position of the zero scroll offset. + /// + /// For example, if [anchor] is 0.5 and the [axisDirection] is + /// [AxisDirection.down] or [AxisDirection.up], then the zero scroll offset is + /// vertically centered within the viewport. If the [anchor] is 1.0, and the + /// [axisDirection] is [AxisDirection.right], then the zero scroll offset is + /// on the left edge of the viewport. + double get anchor; + + set anchor(double value); + + /// The first child in the [GrowthDirection.forward] growth direction. + /// + /// This child that will be at the position defined by [anchor] when the + /// [ViewportOffset.pixels] of [offset] is `0`. + /// + /// Children after [center] will be placed in the [axisDirection] relative to + /// the [center]. Children before [center] will be placed in the opposite of + /// the [axisDirection] relative to the [center]. + /// + /// The [center] must be a child of the viewport. + RenderSliver? get center => _center; + RenderSliver? _center; + + set center(RenderSliver? value) { + if (value == _center) return; + _center = value; + markNeedsLayout(); + } + + @override + bool get sizedByParent => true; + + @override + Size computeDryLayout(BoxConstraints constraints) { + assert(() { + if (!constraints.hasBoundedHeight || !constraints.hasBoundedWidth) { + switch (axis) { + case Axis.vertical: + if (!constraints.hasBoundedHeight) { + throw FlutterError.fromParts([ + ErrorSummary('Vertical viewport was given unbounded height.'), + ErrorDescription( + 'Viewports expand in the scrolling direction to fill their container. ' + 'In this case, a vertical viewport was given an unlimited amount of ' + 'vertical space in which to expand. This situation typically happens ' + 'when a scrollable widget is nested inside another scrollable widget.', + ), + ErrorHint( + 'If this widget is always nested in a scrollable widget there ' + 'is no need to use a viewport because there will always be enough ' + 'vertical space for the children. In this case, consider using a ' + 'Column instead. Otherwise, consider using the "shrinkWrap" property ' + '(or a ShrinkWrappingViewport) to size the height of the viewport ' + 'to the sum of the heights of its children.', + ), + ]); + } + if (!constraints.hasBoundedWidth) { + throw FlutterError( + 'Vertical viewport was given unbounded width.\n' + 'Viewports expand in the cross axis to fill their container and ' + 'constrain their children to match their extent in the cross axis. ' + 'In this case, a vertical viewport was given an unlimited amount of ' + 'horizontal space in which to expand.', + ); + } + break; + case Axis.horizontal: + if (!constraints.hasBoundedWidth) { + throw FlutterError.fromParts([ + ErrorSummary( + 'Horizontal viewport was given unbounded width.', + ), + ErrorDescription( + 'Viewports expand in the scrolling direction to fill their container. ' + 'In this case, a horizontal viewport was given an unlimited amount of ' + 'horizontal space in which to expand. This situation typically happens ' + 'when a scrollable widget is nested inside another scrollable widget.', + ), + ErrorHint( + 'If this widget is always nested in a scrollable widget there ' + 'is no need to use a viewport because there will always be enough ' + 'horizontal space for the children. In this case, consider using a ' + 'Row instead. Otherwise, consider using the "shrinkWrap" property ' + '(or a ShrinkWrappingViewport) to size the width of the viewport ' + 'to the sum of the widths of its children.', + ), + ]); + } + if (!constraints.hasBoundedHeight) { + throw FlutterError( + 'Horizontal viewport was given unbounded height.\n' + 'Viewports expand in the cross axis to fill their container and ' + 'constrain their children to match their extent in the cross axis. ' + 'In this case, a horizontal viewport was given an unlimited amount of ' + 'vertical space in which to expand.', + ); + } + break; + } + } + return true; + }()); + return constraints.biggest; + } + + // Out-of-band data computed during layout. + late double _minScrollExtent; + late double _maxScrollExtent; + bool _hasVisualOverflow = false; + + double growSize = 0; + + @override + bool get hasVisualOverflow => _hasVisualOverflow; + + @override + void updateOutOfBandData( + GrowthDirection growthDirection, + SliverGeometry childLayoutGeometry, + ) { + switch (growthDirection) { + case GrowthDirection.forward: + _maxScrollExtent += childLayoutGeometry.scrollExtent; + break; + case GrowthDirection.reverse: + _minScrollExtent -= childLayoutGeometry.scrollExtent; + break; + } + if (childLayoutGeometry.hasVisualOverflow) _hasVisualOverflow = true; + } + + @override + void updateChildLayoutOffset( + RenderSliver child, + double layoutOffset, + GrowthDirection growthDirection, + ) { + final childParentData = + child.parentData! as CustomSliverPhysicalContainerParentData; + childParentData + ..layoutOffset = layoutOffset + ..growthDirection = growthDirection; + } + + @override + Offset paintOffsetOf(RenderSliver child) { + final childParentData = + child.parentData! as CustomSliverPhysicalContainerParentData; + return computeAbsolutePaintOffset( + child, + childParentData.layoutOffset!, + childParentData.growthDirection!, + ); + } + + @override + double scrollOffsetOf(RenderSliver child, double scrollOffsetWithinChild) { + assert( + child.parent == this, + 'The "child" argument must be a child of this RenderViewport.', + ); + final growthDirection = child.constraints.growthDirection; + switch (growthDirection) { + case GrowthDirection.forward: + var scrollOffsetToChild = 0.0; + var current = center; + while (current != child) { + scrollOffsetToChild += current!.geometry!.scrollExtent; + current = childAfter(current); + } + return scrollOffsetToChild + scrollOffsetWithinChild; + case GrowthDirection.reverse: + var scrollOffsetToChild = 0.0; + var current = childBefore(center!); + while (current != child) { + scrollOffsetToChild -= current!.geometry!.scrollExtent; + current = childBefore(current); + } + return scrollOffsetToChild - scrollOffsetWithinChild; + } + } + + @override + double maxScrollObstructionExtentBefore(RenderSliver child) { + assert( + child.parent == this, + 'The "child" argument must be a child of this RenderViewport.', + ); + final growthDirection = child.constraints.growthDirection; + switch (growthDirection) { + case GrowthDirection.forward: + var pinnedExtent = 0.0; + var current = center; + while (current != child) { + pinnedExtent += current!.geometry!.maxScrollObstructionExtent; + current = childAfter(current); + } + return pinnedExtent; + case GrowthDirection.reverse: + var pinnedExtent = 0.0; + var current = childBefore(center!); + while (current != child) { + pinnedExtent += current!.geometry!.maxScrollObstructionExtent; + current = childBefore(current); + } + return pinnedExtent; + } + } + + @override + void applyPaintTransform(RenderObject child, Matrix4 transform) { + final offset = paintOffsetOf(child as RenderSliver); + transform.translate(offset.dx, offset.dy); + } + + @override + double computeChildMainAxisPosition( + RenderSliver child, + double parentMainAxisPosition, + ) { + final childParentData = + child.parentData! as CustomSliverPhysicalContainerParentData; + switch (applyGrowthDirectionToAxisDirection( + child.constraints.axisDirection, + child.constraints.growthDirection, + )) { + case AxisDirection.down: + case AxisDirection.right: + return parentMainAxisPosition - childParentData.layoutOffset!; + case AxisDirection.up: + return (size.height - parentMainAxisPosition) - + childParentData.layoutOffset!; + case AxisDirection.left: + return (size.width - parentMainAxisPosition) - + childParentData.layoutOffset!; + } + } + + @override + int get indexOfFirstChild { + assert(center != null, 'RenderViewport does not have any children.'); + assert( + center!.parent == this, + 'center is not a child of this RenderViewport', + ); + assert( + firstChild != null, + 'center is the only child of this RenderViewport', + ); + var count = 0; + var child = center; + while (child != firstChild) { + count -= 1; + child = childBefore(child!); + } + return count; + } + + @override + String labelForChild(int index) { + if (index == 0) return 'center child'; + return 'child $index'; + } + + @override + Iterable get childrenInPaintOrder sync* { + if (firstChild == null) return; + var child = firstChild; + while (child != center) { + yield child!; + child = childAfter(child); + } + child = lastChild; + while (true) { + yield child!; + if (child == center) return; + child = childBefore(child); + } + } + + @override + Iterable get childrenInHitTestOrder sync* { + if (firstChild == null) return; + var child = center; + while (child != null) { + yield child; + child = childAfter(child); + } + child = childBefore(center!); + while (child != null) { + yield child; + child = childBefore(child); + } + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DoubleProperty('anchor', anchor)); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart index b6aa25b5e..c66c88f86 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart @@ -71,7 +71,7 @@ enum SpacingType { /// A [StreamChannel] ancestor widget is required in order to provide the /// information about the channels. /// -/// Uses a [ListView.custom] to render the list of channels. +/// Uses a [ScrollablePositionedList] to render the list of channels. /// /// The UI is rendered based on the first ancestor of type [StreamChatTheme]. /// Modify it to change the widget's appearance. @@ -88,8 +88,10 @@ class StreamMessageListView extends StatefulWidget { this.threadBuilder, this.onThreadTap, this.dateDividerBuilder, - this.scrollPhysics = - const ClampingScrollPhysics(), // we need to use ClampingScrollPhysics to avoid the list view to animate and break while loading + // we need to use ClampingScrollPhysics to avoid the list view to bounce + // when we are at the either end of the list view and try to use 'animateTo' + // to animate in the same direction. + this.scrollPhysics = const ClampingScrollPhysics(), this.initialScrollIndex, this.initialAlignment, this.scrollController, @@ -555,6 +557,10 @@ class _StreamMessageListViewState extends State { if (valueKey != null) { final index = messagesIndex[valueKey.value]; if (index != null) { + // The calculation is as follows: + // * Add 2 to the index retrieved to account for the footer and the bottom loader. + // * Multiply the result by 2 to account for the separators between each pair of items. + // * Subtract 1 to adjust for the 0-based indexing of the list view. return ((index + 2) * 2) - 1; } } diff --git a/packages/stream_chat_flutter/test/scrollable_positioned_list/horizontal_scrollable_positioned_list_test.dart b/packages/stream_chat_flutter/test/scrollable_positioned_list/horizontal_scrollable_positioned_list_test.dart index fb5c9fc27..73568c193 100644 --- a/packages/stream_chat_flutter/test/scrollable_positioned_list/horizontal_scrollable_positioned_list_test.dart +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/horizontal_scrollable_positioned_list_test.dart @@ -2,13 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:pedantic/pedantic.dart'; import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart'; -const screenHeight = 400.0; +const screenHeight = 100.0; const screenWidth = 400.0; const itemWidth = screenWidth / 10.0; const itemCount = 500; @@ -46,6 +45,11 @@ void main() { ); } + final fadeTransitionFinder = find.descendant( + of: find.byType(ScrollablePositionedList), + matching: find.byType(FadeTransition), + ); + testWidgets('List positioned with 0 at left', (WidgetTester tester) async { final itemPositionsListener = ItemPositionsListener.create(); await setUpWidgetTest(tester, itemPositionsListener: itemPositionsListener); @@ -172,7 +176,7 @@ void main() { await tester.pumpAndSettle(); expect(tester.getTopLeft(find.text('Item 100')).dx, 0); - expect(tester.getBottomRight(find.text('Item 109')).dy, screenWidth); + expect(tester.getBottomRight(find.text('Item 109')).dy, screenHeight); expect( itemPositionsListener.itemPositions.value @@ -196,6 +200,31 @@ void main() { 1); }); + testWidgets('Scroll to 20 without fading', (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + final itemPositionsListener = ItemPositionsListener.create(); + await setUpWidgetTest(tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener); + + var fadeTransition = tester.widget(fadeTransitionFinder); + final initialOpacity = fadeTransition.opacity; + + unawaited( + itemScrollController.scrollTo(index: 20, duration: scrollDuration)); + await tester.pump(); + await tester.pump(); + await tester.pump(scrollDuration ~/ 2); + + fadeTransition = tester.widget(fadeTransitionFinder); + expect(fadeTransition.opacity, initialOpacity); + + await tester.pumpAndSettle(); + + expect(find.text('Item 14'), findsNothing); + expect(find.text('Item 20'), findsOneWidget); + }); + testWidgets('padding test - centered sliver at left', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); diff --git a/packages/stream_chat_flutter/test/scrollable_positioned_list/positioned_list_test.dart b/packages/stream_chat_flutter/test/scrollable_positioned_list/positioned_list_test.dart index 9888249cb..5dadbda00 100644 --- a/packages/stream_chat_flutter/test/scrollable_positioned_list/positioned_list_test.dart +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/positioned_list_test.dart @@ -360,4 +360,55 @@ void main() { .itemTrailingEdge, 1); }); + + testWidgets('Does not crash when updated offscreen', + (WidgetTester tester) async { + late StateSetter setState; + var updated = false; + + // There's 0 relayout boundaries in this subtree. + final widget = StatefulBuilder(builder: (context, stateSetter) { + setState = stateSetter; + return Positioned( + left: 0, + right: 0, + child: PositionedList( + shrinkWrap: true, + itemCount: 1, + // When `updated` becomes true this line inserts a + // RenderIndexedSemantics to the render tree. + addSemanticIndexes: updated, + itemBuilder: (context, index) => const SizedBox(height: itemHeight), + )); + }); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: Overlay( + initialEntries: [ + OverlayEntry(builder: (context) => widget, maintainState: true), + ], + ), + )); + + // Insert a new opaque OverlayEntry that would prevent the first OverlayEntry + // from doing re-layout. Since there's no relayout boundaries in the first + // OverlayEntry, no dirty RenderObjects in its render subtree can update + // layout. + final newOverlay = + OverlayEntry(builder: (context) => const SizedBox.expand(), opaque: true); + tester.state(find.byType(Overlay)).insert(newOverlay); + await tester.pump(); + + // Update the list item's render tree. A new RenderObjectElement is + // inflated, registeredElement.renderObject will point to this new + // RenderObjectElement's RenderObject (RenderIndexedSemantics), which has + // never been laid out. + setState(() { + updated = true; + }); + + await tester.pump(); + expect(tester.takeException(), isNull); + }); } diff --git a/packages/stream_chat_flutter/test/scrollable_positioned_list/reversed_scrollable_positioned_list_test.dart b/packages/stream_chat_flutter/test/scrollable_positioned_list/reversed_scrollable_positioned_list_test.dart index a5af56a81..cb8d07e3a 100644 --- a/packages/stream_chat_flutter/test/scrollable_positioned_list/reversed_scrollable_positioned_list_test.dart +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/reversed_scrollable_positioned_list_test.dart @@ -2,10 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:pedantic/pedantic.dart'; import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart'; const screenHeight = 400.0; diff --git a/packages/stream_chat_flutter/test/scrollable_positioned_list/scrollable_positioned_list_test.dart b/packages/stream_chat_flutter/test/scrollable_positioned_list/scrollable_positioned_list_test.dart index ef7157438..6419e492a 100644 --- a/packages/stream_chat_flutter/test/scrollable_positioned_list/scrollable_positioned_list_test.dart +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/scrollable_positioned_list_test.dart @@ -2,12 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:pedantic/pedantic.dart'; import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:stream_chat_flutter/scrollable_positioned_list/src/scroll_view.dart'; @@ -48,8 +48,7 @@ void main() { itemCount: itemCount, itemScrollController: itemScrollController, itemBuilder: (context, index) { - assert(index >= 0 && index <= itemCount - 1, - '''index needs to be bigger or equal to 0 and smallert than itemCount -1'''); + assert(index >= 0 && index <= itemCount - 1); return SizedBox( height: variableHeight ? (itemHeight + (index % 13) * 5) : itemHeight, @@ -71,6 +70,11 @@ void main() { ); } + final fadeTransitionFinder = find.descendant( + of: find.byType(ScrollablePositionedList), + matching: find.byType(FadeTransition), + ); + testWidgets('List positioned with 0 at top', (WidgetTester tester) async { final itemPositionsListener = ItemPositionsListener.create(); await setUpWidgetTest(tester, itemPositionsListener: itemPositionsListener); @@ -394,11 +398,7 @@ void main() { itemScrollController: itemScrollController, itemPositionsListener: itemPositionsListener); - var fadeTransition = tester.widget(find - .descendant( - of: find.byType(ScrollablePositionedList), - matching: find.byType(FadeTransition)) - .last); + var fadeTransition = tester.widget(fadeTransitionFinder); final initialOpacity = fadeTransition.opacity; unawaited( @@ -407,11 +407,7 @@ void main() { await tester.pump(); await tester.pump(scrollDuration ~/ 2); - fadeTransition = tester.widget(find - .descendant( - of: find.byType(ScrollablePositionedList), - matching: find.byType(FadeTransition)) - .last); + fadeTransition = tester.widget(fadeTransitionFinder); expect(fadeTransition.opacity, initialOpacity); await tester.pumpAndSettle(); @@ -456,10 +452,6 @@ void main() { final itemScrollController = ItemScrollController(); await setUpWidgetTest(tester, itemScrollController: itemScrollController); - final fadeTransitionFinder = find.descendant( - of: find.byType(ScrollablePositionedList), - matching: find.byType(FadeTransition)); - unawaited( itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pump(); @@ -533,26 +525,14 @@ void main() { await tester.pump(); await tester.pump(); expect( - tester - .widget(find - .descendant( - of: find.byType(ScrollablePositionedList), - matching: find.byType(FadeTransition)) - .last) - .opacity - .value, - closeTo(0, 0.01)); + tester.widget(fadeTransitionFinder.last).opacity.value, + closeTo(0, 0.01), + ); await tester.pump(scrollDuration + scrollDurationTolerance); expect( - tester - .widget(find - .descendant( - of: find.byType(ScrollablePositionedList), - matching: find.byType(FadeTransition)) - .last) - .opacity - .value, - closeTo(1, 0.01)); + tester.widget(fadeTransitionFinder.last).opacity.value, + closeTo(1, 0.01), + ); expect(find.text('Item 0'), findsOneWidget); expect(tester.getTopLeft(find.text('Item 0')).dy, 0); @@ -610,15 +590,9 @@ void main() { expect(tester.getTopLeft(find.text('Item 10')).dy, 0); expect(tester.getBottomLeft(find.text('Item 19')).dy, screenHeight); expect( - tester - .widget(find - .descendant( - of: find.byType(ScrollablePositionedList), - matching: find.byType(FadeTransition)) - .last) - .opacity - .value, - closeTo(0.5, 0.01)); + tester.widget(fadeTransitionFinder.last).opacity.value, + closeTo(0.5, 0.01), + ); await tester.pumpAndSettle(); }); @@ -899,11 +873,7 @@ void main() { await tester.pump(); expect(tester.getTopLeft(find.text('Item 9')).dy, 0); - final fadeTransition = tester.widget(find - .descendant( - of: find.byType(ScrollablePositionedList), - matching: find.byType(FadeTransition)) - .last); + final fadeTransition = tester.widget(fadeTransitionFinder); expect(fadeTransition.opacity.value, 1.0); await tester.pumpAndSettle(); @@ -923,21 +893,12 @@ void main() { await tester.pump(); expect(tester.getTopLeft(find.text('Item 10')).dy, 0); - final fadeTransition = tester.widget(find - .descendant( - of: find.byType(ScrollablePositionedList), - matching: find.byType(FadeTransition)) - .last); + final fadeTransition = tester.widget(fadeTransitionFinder); expect(fadeTransition.opacity.value, 1.0); await tester.pumpAndSettle(); }); - final fadeTransitionFinder = find.descendant( - of: find.byType(ScrollablePositionedList), - matching: find.byType(FadeTransition), - ); - testWidgets('Scroll to 0 stop before half way', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); await setUpWidgetTest(tester, itemScrollController: itemScrollController); @@ -1022,14 +983,13 @@ void main() { itemScrollController.scrollTo(index: 0, duration: scrollDuration)); await tester.pump(); await tester.pump(); - await tester.pump(scrollDuration ~/ 2 + scrollDuration ~/ 20); + await tester.pump(scrollDuration ~/ 2); await tester.tap(find.byType(ScrollablePositionedList)); await tester.pump(); - expect(tester.getTopLeft(find.text('Item 9')).dy, closeTo(0, tolerance)); - final fadeTransition = tester.widget(fadeTransitionFinder); - expect(fadeTransition.opacity.value, 1.0); + expect(tester.getTopLeft(find.text('Item 90')).dy, 0); + expect(fadeTransitionFinder, findsNWidgets(1)); await tester.pumpAndSettle(); }); @@ -1098,6 +1058,34 @@ void main() { expect(find.text('Item 100'), findsNothing); }); + testWidgets("Second scroll future doesn't complete until scroll is done", + (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + await setUpWidgetTest(tester, itemScrollController: itemScrollController); + + unawaited( + itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + await tester.pump(); + await tester.pump(); + await tester.pump(scrollDuration ~/ 2); + + final scrollFuture2 = + itemScrollController.scrollTo(index: 250, duration: scrollDuration); + + var futureComplete = false; + unawaited(scrollFuture2.then((_) => futureComplete = true)); + + await tester.pump(); + await tester.pump(); + await tester.pump(scrollDuration ~/ 2); + + expect(futureComplete, isFalse); + + await tester.pumpAndSettle(); + + expect(futureComplete, isTrue); + }); + testWidgets('Scroll to 250, scroll to 100, scroll to 0 half way', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); @@ -1145,7 +1133,7 @@ void main() { }, skip: true); testWidgets( - '''Jump to 400 at bottom, manually scroll, scroll to 100 at bottom and back''', + 'Jump to 400 at bottom, manually scroll, scroll to 100 at bottom and back', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); final itemPositionsListener = ItemPositionsListener.create(); @@ -1664,7 +1652,7 @@ void main() { }); testWidgets( - '''Maintain programmatic and user position (9 half way off top) in page view''', + 'Maintain programmatic and user position (9 half way off top) in page view', (WidgetTester tester) async { final itemPositionsListener = ItemPositionsListener.create(); final itemScrollController = ItemScrollController(); @@ -1751,21 +1739,21 @@ void main() { MaterialApp( home: ValueListenableBuilder( valueListenable: itemCount, - builder: (context, itemCount, child) => - ScrollablePositionedList.builder( - initialScrollIndex: min(100, itemCount), - itemCount: itemCount, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener, - itemBuilder: (context, index) { - assert(index >= 0 && index <= itemCount - 1, - 'index not bigger than 0 and smaller than itemCount - 1'); - return SizedBox( - height: itemHeight, - child: Text('Item $index'), - ); - }, - ), + builder: (context, itemCount, child) { + return ScrollablePositionedList.builder( + initialScrollIndex: min(100, itemCount), + itemCount: itemCount, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + itemBuilder: (context, index) { + assert(index >= 0 && index <= itemCount - 1); + return SizedBox( + height: itemHeight, + child: Text('Item $index'), + ); + }, + ); + }, ), ), ); @@ -1795,19 +1783,19 @@ void main() { MaterialApp( home: ValueListenableBuilder( valueListenable: itemCount, - builder: (context, itemCount, child) => - ScrollablePositionedList.builder( - initialScrollIndex: min(100, itemCount - 1), - itemCount: itemCount, - itemBuilder: (context, index) { - assert(index >= 0 && index <= itemCount - 1, - 'index not bigger than 0 and smaller than itemCount -1'); - return SizedBox( - height: itemHeight, - child: Text('Item $index'), - ); - }, - ), + builder: (context, itemCount, child) { + return ScrollablePositionedList.builder( + initialScrollIndex: min(100, itemCount - 1), + itemCount: itemCount, + itemBuilder: (context, index) { + assert(index >= 0 && index <= itemCount - 1); + return SizedBox( + height: itemHeight, + child: Text('Item $index'), + ); + }, + ); + }, ), ), ); @@ -1834,19 +1822,19 @@ void main() { MaterialApp( home: ValueListenableBuilder( valueListenable: itemCount, - builder: (context, itemCount, child) => - ScrollablePositionedList.builder( - initialScrollIndex: itemCount - 1, - itemCount: itemCount, - itemBuilder: (context, index) { - assert(index >= 0 && index <= itemCount - 1, - 'index not bigger than 0 and smaller than itemCount -1'); - return SizedBox( - height: itemHeight, - child: Text('Item $index'), - ); - }, - ), + builder: (context, itemCount, child) { + return ScrollablePositionedList.builder( + initialScrollIndex: itemCount - 1, + itemCount: itemCount, + itemBuilder: (context, index) { + assert(index >= 0 && index <= itemCount - 1); + return SizedBox( + height: itemHeight, + child: Text('Item $index'), + ); + }, + ); + }, ), ), ); @@ -1878,11 +1866,7 @@ void main() { minCacheExtent: 10, ); - var fadeTransition = tester.widget(find - .descendant( - of: find.byType(ScrollablePositionedList), - matching: find.byType(FadeTransition)) - .last); + var fadeTransition = tester.widget(fadeTransitionFinder); final initialOpacity = fadeTransition.opacity; unawaited( @@ -1891,11 +1875,7 @@ void main() { await tester.pump(); await tester.pump(scrollDuration ~/ 2); - fadeTransition = tester.widget(find - .descendant( - of: find.byType(ScrollablePositionedList), - matching: find.byType(FadeTransition)) - .last); + fadeTransition = tester.widget(fadeTransitionFinder); expect(fadeTransition.opacity, initialOpacity); await tester.pumpAndSettle(); @@ -1914,11 +1894,9 @@ void main() { minCacheExtent: itemHeight * 200, ); - var fadeTransition = tester.widget(find - .descendant( - of: find.byType(ScrollablePositionedList), - matching: find.byType(FadeTransition)) - .last); + var fadeTransition = tester.widget( + fadeTransitionFinder, + ); final initialOpacity = fadeTransition.opacity; unawaited( @@ -1927,11 +1905,7 @@ void main() { await tester.pump(); await tester.pump(scrollDuration ~/ 2); - fadeTransition = tester.widget(find - .descendant( - of: find.byType(ScrollablePositionedList), - matching: find.byType(FadeTransition)) - .last); + fadeTransition = tester.widget(fadeTransitionFinder); expect(fadeTransition.opacity, initialOpacity); await tester.pumpAndSettle(); @@ -1965,17 +1939,21 @@ void main() { MaterialApp( home: ValueListenableBuilder( valueListenable: key, - builder: (context, key, child) => Container( - key: key, - child: ScrollablePositionedList.builder( - itemCount: 200, - itemScrollController: itemScrollController, - itemBuilder: (context, index) => SizedBox( - height: itemHeight, - child: Text('Item $index'), + builder: (context, key, child) { + return Container( + key: key, + child: ScrollablePositionedList.builder( + itemCount: 200, + itemScrollController: itemScrollController, + itemBuilder: (context, index) { + return SizedBox( + height: itemHeight, + child: Text('Item $index'), + ); + }, ), - ), - ), + ); + }, ), ), ); @@ -2054,15 +2032,19 @@ void main() { MaterialApp( home: ValueListenableBuilder( valueListenable: key, - builder: (context, key, child) => ScrollablePositionedList.builder( - key: key, - itemCount: 10, - itemScrollController: itemScrollController, - itemBuilder: (context, index) => SizedBox( - height: itemHeight, - child: Text('Item $index'), - ), - ), + builder: (context, key, child) { + return ScrollablePositionedList.builder( + key: key, + itemCount: 10, + itemScrollController: itemScrollController, + itemBuilder: (context, index) { + return SizedBox( + height: itemHeight, + child: Text('Item $index'), + ); + }, + ); + }, ), ), ); @@ -2084,17 +2066,21 @@ void main() { MaterialApp( home: ValueListenableBuilder( valueListenable: key, - builder: (context, key, child) => Container( - key: key, - child: ScrollablePositionedList.builder( - itemCount: 100, - itemScrollController: itemScrollController, - itemBuilder: (context, index) => SizedBox( - height: itemHeight, - child: Text('Item $index'), + builder: (context, key, child) { + return Container( + key: key, + child: ScrollablePositionedList.builder( + itemCount: 100, + itemScrollController: itemScrollController, + itemBuilder: (context, index) { + return SizedBox( + height: itemHeight, + child: Text('Item $index'), + ); + }, ), - ), - ), + ); + }, ), ), ); @@ -2124,18 +2110,22 @@ void main() { MaterialApp( home: ValueListenableBuilder( valueListenable: containerKey, - builder: (context, key, child) => Container( - key: key, - child: ScrollablePositionedList.builder( - key: scrollKey, - itemCount: 100, - itemScrollController: itemScrollController, - itemBuilder: (context, index) => SizedBox( - height: itemHeight, - child: Text('Item $index'), + builder: (context, key, child) { + return Container( + key: key, + child: ScrollablePositionedList.builder( + key: scrollKey, + itemCount: 100, + itemScrollController: itemScrollController, + itemBuilder: (context, index) { + return SizedBox( + height: itemHeight, + child: Text('Item $index'), + ); + }, ), - ), - ), + ); + }, ), ), ); @@ -2166,15 +2156,18 @@ void main() { MaterialApp( home: ValueListenableBuilder( valueListenable: itemScrollControllerListenable, - builder: (context, itemScrollController, child) => - ScrollablePositionedList.builder( - itemCount: 100, - itemScrollController: itemScrollController, - itemBuilder: (context, index) => SizedBox( - height: itemHeight, - child: Text('Item $index'), - ), - ), + builder: (context, itemScrollController, child) { + return ScrollablePositionedList.builder( + itemCount: 100, + itemScrollController: itemScrollController, + itemBuilder: (context, index) { + return SizedBox( + height: itemHeight, + child: Text('Item $index'), + ); + }, + ); + }, ), ), ); @@ -2215,29 +2208,35 @@ void main() { Expanded( child: ValueListenableBuilder( valueListenable: topItemScrollControllerListenable, - builder: (context, itemScrollController, child) => - ScrollablePositionedList.builder( - itemCount: 100, - itemScrollController: itemScrollController, - itemBuilder: (context, index) => SizedBox( - height: itemHeight, - child: Text('Item $index'), - ), - ), + builder: (context, itemScrollController, child) { + return ScrollablePositionedList.builder( + itemCount: 100, + itemScrollController: itemScrollController, + itemBuilder: (context, index) { + return SizedBox( + height: itemHeight, + child: Text('Item $index'), + ); + }, + ); + }, ), ), Expanded( child: ValueListenableBuilder( valueListenable: bottomItemScrollControllerListenable, - builder: (context, itemScrollController, child) => - ScrollablePositionedList.builder( - itemCount: 100, - itemScrollController: itemScrollController, - itemBuilder: (context, index) => SizedBox( - height: itemHeight, - child: Text('Item $index'), - ), - ), + builder: (context, itemScrollController, child) { + return ScrollablePositionedList.builder( + itemCount: 100, + itemScrollController: itemScrollController, + itemBuilder: (context, index) { + return SizedBox( + height: itemHeight, + child: Text('Item $index'), + ); + }, + ); + }, ), ), ], diff --git a/packages/stream_chat_flutter/test/scrollable_positioned_list/separated_scrollable_positioned_list_test.dart b/packages/stream_chat_flutter/test/scrollable_positioned_list/separated_scrollable_positioned_list_test.dart index df1035e29..c7439d5cc 100644 --- a/packages/stream_chat_flutter/test/scrollable_positioned_list/separated_scrollable_positioned_list_test.dart +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/separated_scrollable_positioned_list_test.dart @@ -2,10 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:pedantic/pedantic.dart'; import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:stream_chat_flutter/scrollable_positioned_list/src/scroll_view.dart'; @@ -497,20 +496,21 @@ void main() { MaterialApp( home: ValueListenableBuilder( valueListenable: itemCount, - builder: (context, itemCount, child) => - ScrollablePositionedList.separated( - itemCount: itemCount, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener, - itemBuilder: (context, index) => SizedBox( - height: itemHeight, - child: Text('Item $index'), - ), - separatorBuilder: (context, index) => SizedBox( - height: separatorHeight, - child: Text('Separator $index'), - ), - ), + builder: (context, itemCount, child) { + return ScrollablePositionedList.separated( + itemCount: itemCount, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + itemBuilder: (context, index) => SizedBox( + height: itemHeight, + child: Text('Item $index'), + ), + separatorBuilder: (context, index) => SizedBox( + height: separatorHeight, + child: Text('Separator $index'), + ), + ); + }, ), ), ); @@ -538,20 +538,21 @@ void main() { MaterialApp( home: ValueListenableBuilder( valueListenable: itemCount, - builder: (context, itemCount, child) => - ScrollablePositionedList.separated( - itemCount: itemCount, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener, - itemBuilder: (context, index) => SizedBox( - height: itemHeight, - child: Text('Item $index'), - ), - separatorBuilder: (context, index) => SizedBox( - height: separatorHeight, - child: Text('Separator $index'), - ), - ), + builder: (context, itemCount, child) { + return ScrollablePositionedList.separated( + itemCount: itemCount, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + itemBuilder: (context, index) => SizedBox( + height: itemHeight, + child: Text('Item $index'), + ), + separatorBuilder: (context, index) => SizedBox( + height: separatorHeight, + child: Text('Separator $index'), + ), + ); + }, ), ), ); diff --git a/packages/stream_chat_flutter/test/scrollable_positioned_list/seperated_horizontal_scrollable_positioned_list_test.dart b/packages/stream_chat_flutter/test/scrollable_positioned_list/seperated_horizontal_scrollable_positioned_list_test.dart index c26ee43fd..f92c9903a 100644 --- a/packages/stream_chat_flutter/test/scrollable_positioned_list/seperated_horizontal_scrollable_positioned_list_test.dart +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/seperated_horizontal_scrollable_positioned_list_test.dart @@ -2,10 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:pedantic/pedantic.dart'; import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart'; const screenHeight = 400.0; diff --git a/packages/stream_chat_flutter/test/scrollable_positioned_list/shrink_wrap_position_list_test.dart b/packages/stream_chat_flutter/test/scrollable_positioned_list/shrink_wrap_position_list_test.dart new file mode 100644 index 000000000..c28397179 --- /dev/null +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/shrink_wrap_position_list_test.dart @@ -0,0 +1,473 @@ +// Copyright 2019 The Fuchsia Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart'; +import 'package:stream_chat_flutter/scrollable_positioned_list/src/item_positions_notifier.dart'; +import 'package:stream_chat_flutter/scrollable_positioned_list/src/positioned_list.dart'; + +const screenHeight = 400.0; +const screenWidth = 400.0; +const itemHeight = screenHeight / 10.0; +const defaultItemCount = 500; + +void main() { + final itemPositionsNotifier = ItemPositionsListener.create(); + + Future setUpWidgetTest( + WidgetTester tester, { + int topItem = 0, + Key? key, + ScrollController? scrollController, + double anchor = 0, + int itemCount = defaultItemCount, + bool reverse = false, + }) async { + tester.binding.window.devicePixelRatioTestValue = 1.0; + tester.binding.window.physicalSizeTestValue = + const Size(screenWidth, screenHeight); + + await tester.pumpWidget( + MaterialApp( + // Use flex layout to ensure that the minimum height is not limited to screenHeight + home: Column(children: [ + // Use Constrained to make max height not more than screenHeight + ConstrainedBox( + constraints: + const BoxConstraints(maxHeight: screenHeight, maxWidth: screenWidth), + child: PositionedList( + key: key, + itemCount: itemCount, + positionedIndex: topItem, + alignment: anchor, + controller: scrollController, + itemBuilder: (context, index) => SizedBox( + height: itemHeight, + child: Text('Item $index'), + ), + itemPositionsNotifier: + itemPositionsNotifier as ItemPositionsNotifier, + shrinkWrap: true, + reverse: reverse, + ), + ), + ]), + ), + ); + } + + testWidgets('short list with shrink wrap', (WidgetTester tester) async { + const itemCount = 5; + const key = Key('short_list'); + await setUpWidgetTest(tester, itemCount: itemCount, key: key); + await tester.pump(); + + expect( + tester.getBottomRight(find.text('Item 4')).dy, itemHeight * itemCount); + expect(find.text('Item 4'), findsOneWidget); + expect(find.text('Item 5'), findsNothing); + + final positionList = find.byKey(key); + expect(tester.getBottomRight(positionList).dy, itemHeight * itemCount); + + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 0) + .itemLeadingEdge, + 0); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 4) + .itemTrailingEdge, + 1.0); + }); + + testWidgets('List positioned with 0 at top and shrink wrap', + (WidgetTester tester) async { + await setUpWidgetTest(tester); + await tester.pump(); + + expect(find.text('Item 0'), findsOneWidget); + expect(find.text('Item 9'), findsOneWidget); + expect(find.text('Item 10'), findsNothing); + + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 0) + .itemLeadingEdge, + 0); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 9) + .itemTrailingEdge, + 1); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 10) + .itemLeadingEdge, + 1); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 10) + .itemTrailingEdge, + 11 / 10); + }); + + testWidgets('List positioned with 5 at top and shrink wrap', + (WidgetTester tester) async { + await setUpWidgetTest(tester, topItem: 5); + await tester.pump(); + + expect(find.text('Item 4'), findsNothing); + expect(find.text('Item 5'), findsOneWidget); + expect(find.text('Item 14'), findsOneWidget); + expect(find.text('Item 15'), findsNothing); + + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 4) + .itemLeadingEdge, + -1 / 10); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 4) + .itemTrailingEdge, + 0); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 5) + .itemLeadingEdge, + 0); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 14) + .itemTrailingEdge, + 1); + }); + + testWidgets('List positioned with 20 at bottom and shrink wrap', + (WidgetTester tester) async { + await setUpWidgetTest(tester, topItem: 20, anchor: 1); + await tester.pump(); + + expect(find.text('Item 20'), findsNothing); + expect(find.text('Item 19'), findsOneWidget); + expect(find.text('Item 10'), findsOneWidget); + + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 10) + .itemLeadingEdge, + 0); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 19) + .itemLeadingEdge, + 9 / 10); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 19) + .itemTrailingEdge, + 1); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 20) + .itemLeadingEdge, + 1); + }); + + testWidgets('List positioned with 20 at halfway and shrink wrap', + (WidgetTester tester) async { + await setUpWidgetTest(tester, topItem: 20, anchor: 0.5); + await tester.pump(); + + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 20) + .itemLeadingEdge, + 0.5); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 20) + .itemTrailingEdge, + 0.5 + itemHeight / screenHeight); + }); + + testWidgets('List positioned with 20 half off top of screen and shrink wrap', + (WidgetTester tester) async { + await setUpWidgetTest(tester, + topItem: 20, anchor: -(itemHeight / screenHeight) / 2); + await tester.pump(); + + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 20) + .itemLeadingEdge, + -(itemHeight / screenHeight) / 2); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 20) + .itemTrailingEdge, + (itemHeight / screenHeight) / 2); + }); + + testWidgets('List positioned with 5 at top then scroll up 2 and shrink wrap', + (WidgetTester tester) async { + await setUpWidgetTest(tester, topItem: 5); + + await tester.drag( + find.byType(PositionedList), const Offset(0, itemHeight * 2)); + await tester.pump(); + + expect(find.text('Item 2'), findsNothing); + expect(find.text('Item 3'), findsOneWidget); + expect(find.text('Item 12'), findsOneWidget); + expect(find.text('Item 13'), findsNothing); + + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 2) + .itemLeadingEdge, + -1 / 10); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 3) + .itemLeadingEdge, + 0); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 12) + .itemTrailingEdge, + 1); + }); + + testWidgets( + 'List positioned with 5 at top then scroll down 1/2 and shrink wrap', + (WidgetTester tester) async { + await setUpWidgetTest(tester, topItem: 5); + + await tester.drag( + find.byType(PositionedList), const Offset(0, -1 / 2 * itemHeight)); + await tester.pump(); + + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 5) + .itemTrailingEdge, + 1 / 20); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 14) + .itemLeadingEdge, + 17 / 20); + }); + + testWidgets('List positioned with 0 at top scroll up 5 and shrink wrap', + (WidgetTester tester) async { + final scrollController = ScrollController(); + await setUpWidgetTest(tester, scrollController: scrollController); + await tester.pump(); + + scrollController.jumpTo(itemHeight * 5); + await tester.pump(); + await tester.pumpAndSettle(); + + expect(find.text('Item 4'), findsNothing); + expect(find.text('Item 5'), findsOneWidget); + expect(find.text('Item 14'), findsOneWidget); + expect(find.text('Item 15'), findsNothing); + + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 5) + .itemLeadingEdge, + 0); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 4) + .itemLeadingEdge, + -1 / 10); + }); + + testWidgets( + 'List positioned with 5 at top then scroll up 2 programatically and shrink wrap', + (WidgetTester tester) async { + final scrollController = ScrollController(); + await setUpWidgetTest(tester, + topItem: 5, scrollController: scrollController); + + scrollController.jumpTo(-2 * itemHeight); + await tester.pump(); + + expect(find.text('Item 2'), findsNothing); + expect(find.text('Item 3'), findsOneWidget); + expect(find.text('Item 12'), findsOneWidget); + expect(find.text('Item 13'), findsNothing); + + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 2) + .itemLeadingEdge, + -1 / 10); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 3) + .itemLeadingEdge, + 0); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 12) + .itemTrailingEdge, + 1); + }); + + testWidgets( + 'List positioned with 5 at top then scroll down 20 programatically and shrink wrap', + (WidgetTester tester) async { + final scrollController = ScrollController(); + await setUpWidgetTest(tester, + topItem: 5, scrollController: scrollController); + + scrollController.jumpTo(itemHeight * 20); + await tester.pump(); + + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 23) + .itemLeadingEdge, + -2 / 10); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 24) + .itemLeadingEdge, + -1 / 10); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 25) + .itemLeadingEdge, + 0); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 4) + .itemLeadingEdge, + -21 / 10); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 5) + .itemLeadingEdge, + -20 / 10); + }); + + testWidgets( + 'List positioned with 5 at top and initial scroll offset and shrink wrap', + (WidgetTester tester) async { + final scrollController = + ScrollController(initialScrollOffset: -2 * itemHeight); + await setUpWidgetTest(tester, + topItem: 5, scrollController: scrollController); + + expect(find.text('Item 2'), findsNothing); + expect(find.text('Item 3'), findsOneWidget); + expect(find.text('Item 12'), findsOneWidget); + expect(find.text('Item 13'), findsNothing); + + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 3) + .itemLeadingEdge, + 0); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 12) + .itemTrailingEdge, + 1); + }); + + testWidgets('short List with reverse and shrink wrap', + (WidgetTester tester) async { + const itemCount = 5; + const key = Key('short_list'); + await setUpWidgetTest(tester, + itemCount: itemCount, key: key, reverse: true); + await tester.pump(); + + expect(find.text('Item 4'), findsOneWidget); + expect(find.text('Item 5'), findsNothing); + expect( + tester.getBottomRight(find.text('Item 0')).dy, itemHeight * itemCount); + expect(tester.getTopLeft(find.text('Item 4')).dy, 0); + + final positionList = find.byKey(key); + expect(tester.getBottomRight(positionList).dy, itemHeight * itemCount); + expect(tester.getTopLeft(positionList).dy, 0); + + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 0) + .itemLeadingEdge, + 0); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 4) + .itemTrailingEdge, + 1.0); + }); + + testWidgets('test nested positioned list', (WidgetTester tester) async { + const itemCount = 50; + const key = Key('short_list'); + tester.binding.window.devicePixelRatioTestValue = 1.0; + tester.binding.window.physicalSizeTestValue = + const Size(screenWidth, screenHeight); + + await tester.pumpWidget( + MaterialApp( + // Use flex layout to ensure that the minimum height is not limited to screenHeight + home: PositionedList( + itemCount: 5, + itemBuilder: (context, index) { + if (index == 0) { + return PositionedList( + key: key, + itemCount: itemCount, + shrinkWrap: true, + itemBuilder: (context, idx) => SizedBox( + height: itemHeight, + child: Text('Item $idx'), + )); + } else { + return SizedBox( + height: itemHeight, + child: Text('Item ${itemCount + index - 1}'), + ); + } + }, + itemPositionsNotifier: itemPositionsNotifier as ItemPositionsNotifier, + ), + ), + ); + await tester.pump(); + + expect(find.text('Item 0'), findsOneWidget); + expect(find.text('Item 50'), findsNothing); + expect(tester.getTopLeft(find.text('Item 0')).dy, 0); + expect(tester.getBottomRight(find.text('Item 9')).dy, screenHeight); + + final positionList = find.byKey(key); + expect(tester.getBottomRight(positionList).dy, itemHeight * itemCount); + expect(tester.getTopLeft(positionList).dy, 0); + + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 0) + .itemLeadingEdge, + 0); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 0) + .itemTrailingEdge, + 5.0); + }); +} diff --git a/packages/stream_chat_flutter/test/scrollable_positioned_list/shrink_wrap_scrollable_position_list_test.dart b/packages/stream_chat_flutter/test/scrollable_positioned_list/shrink_wrap_scrollable_position_list_test.dart new file mode 100644 index 000000000..c9a0b06ce --- /dev/null +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/shrink_wrap_scrollable_position_list_test.dart @@ -0,0 +1,246 @@ +// Copyright 2019 The Fuchsia Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pedantic/pedantic.dart'; +import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart'; + +const screenHeight = 400.0; +const screenWidth = 400.0; +const itemHeight = screenHeight / 10.0; +const itemCount = 500; +const scrollDuration = Duration(seconds: 1); + +void main() { + Future setUpWidgetTest( + WidgetTester tester, { + ItemScrollController? itemScrollController, + ItemPositionsListener? itemPositionsListener, + EdgeInsets? padding, + int initialIndex = 0, + }) async { + tester.binding.window.devicePixelRatioTestValue = 1.0; + tester.binding.window.physicalSizeTestValue = + const Size(screenWidth, screenHeight); + + await tester.pumpWidget( + MaterialApp( + // Use flex layout to ensure that the minimum height is not limited to screenHeight + home: Column(children: [ + // Use Constrained to make max height not more than screenHeight + ConstrainedBox( + constraints: + const BoxConstraints(maxHeight: screenHeight, maxWidth: screenWidth), + child: ScrollablePositionedList.builder( + itemCount: itemCount, + initialScrollIndex: initialIndex, + itemScrollController: itemScrollController, + itemBuilder: (context, index) => SizedBox( + height: itemHeight, + child: Text('Item $index'), + ), + itemPositionsListener: itemPositionsListener, + shrinkWrap: true, + padding: padding, + ), + ), + ]), + ), + ); + } + + testWidgets('List positioned with 0 at top and shrink wrap', + (WidgetTester tester) async { + final itemPositionsListener = ItemPositionsListener.create(); + await setUpWidgetTest(tester, itemPositionsListener: itemPositionsListener); + + expect(tester.getTopLeft(find.text('Item 0')).dy, 0); + expect(tester.getBottomRight(find.text('Item 9')).dy, screenHeight); + expect(find.text('Item 10'), findsNothing); + + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 0) + .itemLeadingEdge, + 0); + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 9) + .itemTrailingEdge, + 1); + }); + + testWidgets('Scroll to 1 then 2 (both already on screen) with shrink wrap', + (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + final itemPositionsListener = ItemPositionsListener.create(); + await setUpWidgetTest(tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener); + + unawaited( + itemScrollController.scrollTo(index: 1, duration: scrollDuration)); + await tester.pump(); + await tester.pump(scrollDuration); + expect(find.text('Item 0'), findsNothing); + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 1) + .itemLeadingEdge, + 0); + expect(tester.getTopLeft(find.text('Item 1')).dy, 0); + + unawaited( + itemScrollController.scrollTo(index: 2, duration: scrollDuration)); + await tester.pump(); + await tester.pump(scrollDuration); + + expect(find.text('Item 1'), findsNothing); + expect(tester.getTopLeft(find.text('Item 2')).dy, 0); + + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 2) + .itemLeadingEdge, + 0); + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 11) + .itemTrailingEdge, + 1); + }); + + testWidgets( + 'Scroll to 5 (already on screen) and then back to 0 with shrink wrap', + (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + final itemPositionsListener = ItemPositionsListener.create(); + await setUpWidgetTest(tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener); + + unawaited( + itemScrollController.scrollTo(index: 5, duration: scrollDuration)); + await tester.pumpAndSettle(); + unawaited( + itemScrollController.scrollTo(index: 0, duration: scrollDuration)); + await tester.pumpAndSettle(); + + expect(find.text('Item 0'), findsOneWidget); + expect(find.text('Item 9'), findsOneWidget); + expect(find.text('Item 10'), findsNothing); + + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 0) + .itemLeadingEdge, + 0); + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 9) + .itemTrailingEdge, + 1); + }); + + testWidgets('Scroll to 100 (not already on screen) with shrink wrap', + (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + final itemPositionsListener = ItemPositionsListener.create(); + await setUpWidgetTest(tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener); + + unawaited( + itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + await tester.pumpAndSettle(); + + expect(find.text('Item 99'), findsNothing); + expect(find.text('Item 100'), findsOneWidget); + + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 100) + .itemLeadingEdge, + 0); + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 109) + .itemTrailingEdge, + 1); + }); + + testWidgets('Jump to 100 with shrink wrap', (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + final itemPositionsListener = ItemPositionsListener.create(); + await setUpWidgetTest(tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener); + + itemScrollController.jumpTo(index: 100); + await tester.pumpAndSettle(); + + expect(tester.getTopLeft(find.text('Item 100')).dy, 0); + expect(tester.getBottomRight(find.text('Item 109')).dy, screenHeight); + + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 100) + .itemLeadingEdge, + 0); + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 109) + .itemTrailingEdge, + 1); + }); + + testWidgets('padding test - centered sliver at bottom with shrink wrap', + (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + padding: const EdgeInsets.all(10), + ); + + expect(tester.getTopLeft(find.text('Item 0')), const Offset(10, 10)); + expect(tester.getTopLeft(find.text('Item 1')), + const Offset(10, itemHeight + 10)); + expect(tester.getBottomRight(find.text('Item 1')), + const Offset(screenWidth - 10, 10 + itemHeight * 2)); + + unawaited( + itemScrollController.scrollTo(index: 490, duration: scrollDuration)); + await tester.pumpAndSettle(); + + await tester.drag( + find.byType(ScrollablePositionedList), const Offset(0, -100)); + await tester.pumpAndSettle(); + + expect(tester.getTopLeft(find.text('Item 499')), + const Offset(10, screenHeight - itemHeight - 10)); + }); + + testWidgets('padding test - centered sliver not at bottom', + (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + initialIndex: 2, + padding: const EdgeInsets.all(10), + ); + + await tester.drag( + find.byType(ScrollablePositionedList), const Offset(0, 200)); + await tester.pumpAndSettle(); + + expect(tester.getTopLeft(find.text('Item 0')), const Offset(10, 10)); + expect(tester.getTopLeft(find.text('Item 2')), + const Offset(10, 10 + itemHeight * 2)); + expect(tester.getTopLeft(find.text('Item 3')), + const Offset(10, 10 + itemHeight * 3)); + }); +} From fbf9191795cd80737cd169e15fdc254e1911946a Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 3 May 2023 15:53:30 +0530 Subject: [PATCH 16/51] feat(ui): add support for `StreamMessageListView.shrinkWrap`. Signed-off-by: xsahil03x --- packages/stream_chat_flutter/CHANGELOG.md | 2 ++ .../lib/src/message_list_view/message_list_view.dart | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index c735532b6..624577e0b 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -52,6 +52,8 @@ ), ``` +- Added `StreamMessageListView.shrinkWrap` to allow users to shrink wrap the message list view. + ๐Ÿ”„ Changed - Deprecated `MessageTheme.linkBackgroundColor` in favor of `MessageTheme.urlAttachmentBackgroundColor`. diff --git a/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart index c66c88f86..d11260639 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart @@ -115,6 +115,7 @@ class StreamMessageListView extends StatefulWidget { this.unreadMessagesSeparatorBuilder, this.messageListController, this.reverse = true, + this.shrinkWrap = false, this.paginationLimit = 20, this.paginationLoadingIndicatorBuilder, this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.onDrag, @@ -135,6 +136,14 @@ class StreamMessageListView extends StatefulWidget { /// See [ScrollView.reverse]. final bool reverse; + /// Whether the extent of the scroll view in the [scrollDirection] should be + /// determined by the contents being viewed. + /// + /// Defaults to false. + /// + /// See [ScrollView.shrinkWrap]. + final bool shrinkWrap; + /// Limit used during pagination final int paginationLimit; @@ -550,6 +559,7 @@ class _StreamMessageListViewState extends State { physics: widget.scrollPhysics, itemScrollController: _scrollController, reverse: widget.reverse, + shrinkWrap: widget.shrinkWrap, itemCount: itemCount, findChildIndexCallback: (Key key) { final indexedKey = key as IndexedKey; From a3bfedc8bfa42f8f7c6a0c706769f22b6d6d9208 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 3 May 2023 16:27:28 +0530 Subject: [PATCH 17/51] chore: fix analysis and format Signed-off-by: xsahil03x --- .../positioned_list_test.dart | 14 +- .../scrollable_positioned_list_test.dart | 174 ++++++++++-------- .../shrink_wrap_position_list_test.dart | 142 +++++++------- ...nk_wrap_scrollable_position_list_test.dart | 7 +- 4 files changed, 179 insertions(+), 158 deletions(-) diff --git a/packages/stream_chat_flutter/test/scrollable_positioned_list/positioned_list_test.dart b/packages/stream_chat_flutter/test/scrollable_positioned_list/positioned_list_test.dart index 5dadbda00..96e10a4c1 100644 --- a/packages/stream_chat_flutter/test/scrollable_positioned_list/positioned_list_test.dart +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/positioned_list_test.dart @@ -391,12 +391,14 @@ void main() { ), )); - // Insert a new opaque OverlayEntry that would prevent the first OverlayEntry - // from doing re-layout. Since there's no relayout boundaries in the first - // OverlayEntry, no dirty RenderObjects in its render subtree can update - // layout. - final newOverlay = - OverlayEntry(builder: (context) => const SizedBox.expand(), opaque: true); + // Insert a new opaque OverlayEntry that would prevent the first + // OverlayEntry from doing re-layout. Since there's no relayout boundaries + // in the first OverlayEntry, no dirty RenderObjects in its render subtree + // can update layout. + final newOverlay = OverlayEntry( + builder: (context) => const SizedBox.expand(), + opaque: true, + ); tester.state(find.byType(Overlay)).insert(newOverlay); await tester.pump(); diff --git a/packages/stream_chat_flutter/test/scrollable_positioned_list/scrollable_positioned_list_test.dart b/packages/stream_chat_flutter/test/scrollable_positioned_list/scrollable_positioned_list_test.dart index 6419e492a..55fe0c7ec 100644 --- a/packages/stream_chat_flutter/test/scrollable_positioned_list/scrollable_positioned_list_test.dart +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/scrollable_positioned_list_test.dart @@ -48,7 +48,10 @@ void main() { itemCount: itemCount, itemScrollController: itemScrollController, itemBuilder: (context, index) { - assert(index >= 0 && index <= itemCount - 1); + assert( + index >= 0 && index <= itemCount - 1, + 'index must be in the range of 0 to itemCount - 1', + ); return SizedBox( height: variableHeight ? (itemHeight + (index % 13) * 5) : itemHeight, @@ -1133,34 +1136,35 @@ void main() { }, skip: true); testWidgets( - 'Jump to 400 at bottom, manually scroll, scroll to 100 at bottom and back', - (WidgetTester tester) async { - final itemScrollController = ItemScrollController(); - final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener); + 'Jump to 400 at bottom, manually scroll, scroll to 100 at bottom and back', + (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + final itemPositionsListener = ItemPositionsListener.create(); + await setUpWidgetTest(tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener); - itemScrollController.jumpTo(index: 400, alignment: 1); - await tester.pumpAndSettle(); + itemScrollController.jumpTo(index: 400, alignment: 1); + await tester.pumpAndSettle(); - final listFinder = find.byType(ScrollablePositionedList); + final listFinder = find.byType(ScrollablePositionedList); - await tester.drag(listFinder, const Offset(0, -screenHeight)); - await tester.pumpAndSettle(); + await tester.drag(listFinder, const Offset(0, -screenHeight)); + await tester.pumpAndSettle(); - unawaited(itemScrollController.scrollTo( - index: 100, alignment: 1, duration: scrollDuration)); - await tester.pumpAndSettle(); + unawaited(itemScrollController.scrollTo( + index: 100, alignment: 1, duration: scrollDuration)); + await tester.pumpAndSettle(); - unawaited(itemScrollController.scrollTo( - index: 400, alignment: 1, duration: scrollDuration)); - await tester.pumpAndSettle(); + unawaited(itemScrollController.scrollTo( + index: 400, alignment: 1, duration: scrollDuration)); + await tester.pumpAndSettle(); - final itemFinder = find.text('Item 399'); - expect(itemFinder, findsOneWidget); - expect(tester.getBottomLeft(itemFinder).dy, screenHeight); - }); + final itemFinder = find.text('Item 399'); + expect(itemFinder, findsOneWidget); + expect(tester.getBottomLeft(itemFinder).dy, screenHeight); + }, + ); testWidgets('physics', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); @@ -1652,70 +1656,71 @@ void main() { }); testWidgets( - 'Maintain programmatic and user position (9 half way off top) in page view', - (WidgetTester tester) async { - final itemPositionsListener = ItemPositionsListener.create(); - final itemScrollController = ItemScrollController(); - - tester.binding.window.devicePixelRatioTestValue = 1.0; - tester.binding.window.physicalSizeTestValue = - const Size(screenWidth, screenHeight); - - await tester.pumpWidget( - MaterialApp( - home: PageView( - children: [ - KeyedSubtree( - key: const PageStorageKey('key'), - child: ScrollablePositionedList.builder( - itemCount: defaultItemCount, - itemScrollController: itemScrollController, - itemBuilder: (context, index) => SizedBox( - height: itemHeight, - child: Text('Item $index'), + 'Maintain programmatic and user position (9 half way off top) in page view', + (WidgetTester tester) async { + final itemPositionsListener = ItemPositionsListener.create(); + final itemScrollController = ItemScrollController(); + + tester.binding.window.devicePixelRatioTestValue = 1.0; + tester.binding.window.physicalSizeTestValue = + const Size(screenWidth, screenHeight); + + await tester.pumpWidget( + MaterialApp( + home: PageView( + children: [ + KeyedSubtree( + key: const PageStorageKey('key'), + child: ScrollablePositionedList.builder( + itemCount: defaultItemCount, + itemScrollController: itemScrollController, + itemBuilder: (context, index) => SizedBox( + height: itemHeight, + child: Text('Item $index'), + ), + itemPositionsListener: itemPositionsListener, ), - itemPositionsListener: itemPositionsListener, ), - ), - const Center( - child: Text('Test'), - ) - ], + const Center( + child: Text('Test'), + ) + ], + ), ), - ), - ); - - itemScrollController.jumpTo(index: 9); - await tester.pump(); + ); - expect(tester.getBottomRight(find.text('Item 9')).dy, itemHeight); + itemScrollController.jumpTo(index: 9); + await tester.pump(); - await tester.drag( - find.byType(ScrollablePositionedList), const Offset(0, -itemHeight)); - await tester.pumpAndSettle(); + expect(tester.getBottomRight(find.text('Item 9')).dy, itemHeight); - final item9Bottom = tester.getBottomRight(find.text('Item 9')).dy; - expect(item9Bottom, lessThan(itemHeight)); + await tester.drag( + find.byType(ScrollablePositionedList), const Offset(0, -itemHeight)); + await tester.pumpAndSettle(); - await tester.drag(find.byType(PageView), const Offset(-500, 0)); - await tester.pumpAndSettle(); + final item9Bottom = tester.getBottomRight(find.text('Item 9')).dy; + expect(item9Bottom, lessThan(itemHeight)); - await tester.drag(find.byType(PageView), const Offset(500, 0)); - await tester.pumpAndSettle(); + await tester.drag(find.byType(PageView), const Offset(-500, 0)); + await tester.pumpAndSettle(); - expect(tester.getBottomRight(find.text('Item 9')).dy, item9Bottom); + await tester.drag(find.byType(PageView), const Offset(500, 0)); + await tester.pumpAndSettle(); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 9) - .itemLeadingEdge, - -(itemHeight / screenHeight) / 2); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 9) - .itemTrailingEdge, - (itemHeight / screenHeight) / 2); - }); + expect(tester.getBottomRight(find.text('Item 9')).dy, item9Bottom); + + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 9) + .itemLeadingEdge, + -(itemHeight / screenHeight) / 2); + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 9) + .itemTrailingEdge, + (itemHeight / screenHeight) / 2); + }, + ); testWidgets('List with no items', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); @@ -1746,7 +1751,10 @@ void main() { itemScrollController: itemScrollController, itemPositionsListener: itemPositionsListener, itemBuilder: (context, index) { - assert(index >= 0 && index <= itemCount - 1); + assert( + index >= 0 && index <= itemCount - 1, + 'index must be in the range of 0 to itemCount - 1', + ); return SizedBox( height: itemHeight, child: Text('Item $index'), @@ -1788,7 +1796,10 @@ void main() { initialScrollIndex: min(100, itemCount - 1), itemCount: itemCount, itemBuilder: (context, index) { - assert(index >= 0 && index <= itemCount - 1); + assert( + index >= 0 && index <= itemCount - 1, + 'index must be in the range of 0 to itemCount - 1', + ); return SizedBox( height: itemHeight, child: Text('Item $index'), @@ -1827,7 +1838,10 @@ void main() { initialScrollIndex: itemCount - 1, itemCount: itemCount, itemBuilder: (context, index) { - assert(index >= 0 && index <= itemCount - 1); + assert( + index >= 0 && index <= itemCount - 1, + 'index must be in the range of 0 to itemCount - 1', + ); return SizedBox( height: itemHeight, child: Text('Item $index'), diff --git a/packages/stream_chat_flutter/test/scrollable_positioned_list/shrink_wrap_position_list_test.dart b/packages/stream_chat_flutter/test/scrollable_positioned_list/shrink_wrap_position_list_test.dart index c28397179..c5d94670a 100644 --- a/packages/stream_chat_flutter/test/scrollable_positioned_list/shrink_wrap_position_list_test.dart +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/shrink_wrap_position_list_test.dart @@ -31,12 +31,13 @@ void main() { await tester.pumpWidget( MaterialApp( - // Use flex layout to ensure that the minimum height is not limited to screenHeight + // Use flex layout to ensure that the minimum height is not limited to + // screenHeight. home: Column(children: [ // Use Constrained to make max height not more than screenHeight ConstrainedBox( - constraints: - const BoxConstraints(maxHeight: screenHeight, maxWidth: screenWidth), + constraints: const BoxConstraints( + maxHeight: screenHeight, maxWidth: screenWidth), child: PositionedList( key: key, itemCount: itemCount, @@ -292,73 +293,75 @@ void main() { }); testWidgets( - 'List positioned with 5 at top then scroll up 2 programatically and shrink wrap', - (WidgetTester tester) async { - final scrollController = ScrollController(); - await setUpWidgetTest(tester, - topItem: 5, scrollController: scrollController); - - scrollController.jumpTo(-2 * itemHeight); - await tester.pump(); - - expect(find.text('Item 2'), findsNothing); - expect(find.text('Item 3'), findsOneWidget); - expect(find.text('Item 12'), findsOneWidget); - expect(find.text('Item 13'), findsNothing); - - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 2) - .itemLeadingEdge, - -1 / 10); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 3) - .itemLeadingEdge, - 0); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 12) - .itemTrailingEdge, - 1); - }); + '''List positioned with 5 at top then scroll up 2 programatically and shrink wrap''', + (WidgetTester tester) async { + final scrollController = ScrollController(); + await setUpWidgetTest(tester, + topItem: 5, scrollController: scrollController); + + scrollController.jumpTo(-2 * itemHeight); + await tester.pump(); + + expect(find.text('Item 2'), findsNothing); + expect(find.text('Item 3'), findsOneWidget); + expect(find.text('Item 12'), findsOneWidget); + expect(find.text('Item 13'), findsNothing); + + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 2) + .itemLeadingEdge, + -1 / 10); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 3) + .itemLeadingEdge, + 0); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 12) + .itemTrailingEdge, + 1); + }, + ); testWidgets( - 'List positioned with 5 at top then scroll down 20 programatically and shrink wrap', - (WidgetTester tester) async { - final scrollController = ScrollController(); - await setUpWidgetTest(tester, - topItem: 5, scrollController: scrollController); - - scrollController.jumpTo(itemHeight * 20); - await tester.pump(); - - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 23) - .itemLeadingEdge, - -2 / 10); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 24) - .itemLeadingEdge, - -1 / 10); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 25) - .itemLeadingEdge, - 0); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 4) - .itemLeadingEdge, - -21 / 10); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 5) - .itemLeadingEdge, - -20 / 10); - }); + '''List positioned with 5 at top then scroll down 20 programatically and shrink wrap''', + (WidgetTester tester) async { + final scrollController = ScrollController(); + await setUpWidgetTest(tester, + topItem: 5, scrollController: scrollController); + + scrollController.jumpTo(itemHeight * 20); + await tester.pump(); + + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 23) + .itemLeadingEdge, + -2 / 10); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 24) + .itemLeadingEdge, + -1 / 10); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 25) + .itemLeadingEdge, + 0); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 4) + .itemLeadingEdge, + -21 / 10); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 5) + .itemLeadingEdge, + -20 / 10); + }, + ); testWidgets( 'List positioned with 5 at top and initial scroll offset and shrink wrap', @@ -424,7 +427,8 @@ void main() { await tester.pumpWidget( MaterialApp( - // Use flex layout to ensure that the minimum height is not limited to screenHeight + // Use flex layout to ensure that the minimum height is not limited to + // screenHeight. home: PositionedList( itemCount: 5, itemBuilder: (context, index) { diff --git a/packages/stream_chat_flutter/test/scrollable_positioned_list/shrink_wrap_scrollable_position_list_test.dart b/packages/stream_chat_flutter/test/scrollable_positioned_list/shrink_wrap_scrollable_position_list_test.dart index c9a0b06ce..cc3725aa9 100644 --- a/packages/stream_chat_flutter/test/scrollable_positioned_list/shrink_wrap_scrollable_position_list_test.dart +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/shrink_wrap_scrollable_position_list_test.dart @@ -27,12 +27,13 @@ void main() { await tester.pumpWidget( MaterialApp( - // Use flex layout to ensure that the minimum height is not limited to screenHeight + // Use flex layout to ensure that the minimum height is not limited to + // screenHeight. home: Column(children: [ // Use Constrained to make max height not more than screenHeight ConstrainedBox( - constraints: - const BoxConstraints(maxHeight: screenHeight, maxWidth: screenWidth), + constraints: const BoxConstraints( + maxHeight: screenHeight, maxWidth: screenWidth), child: ScrollablePositionedList.builder( itemCount: itemCount, initialScrollIndex: initialIndex, From abab2e0a0606b9ef7c6fef9af12e173a2b9773f6 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 4 May 2023 16:54:12 +0530 Subject: [PATCH 18/51] feat(llc): added synchronization to the `StreamChatClient.sync` api. Signed-off-by: xsahil03x --- packages/stream_chat/CHANGELOG.md | 2 + .../stream_chat/lib/src/client/client.dart | 58 ++++++++++--------- packages/stream_chat/pubspec.yaml | 1 + 3 files changed, 34 insertions(+), 27 deletions(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index f7094a534..f28d7e659 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -8,6 +8,8 @@ โœ… Added - Expose `ChannelMute` class. [#1473](https://github.com/GetStream/stream-chat-flutter/issues/1473) +- Added synchronization to the `StreamChatClient.sync` + api. [#1392](https://github.com/GetStream/stream-chat-flutter/issues/1392) ## 6.0.0 diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 26d229b43..72dfe9893 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -32,6 +32,7 @@ import 'package:stream_chat/src/event_type.dart'; import 'package:stream_chat/src/ws/connection_status.dart'; import 'package:stream_chat/src/ws/websocket.dart'; import 'package:stream_chat/version.dart'; +import 'package:synchronized/extension.dart'; /// Handler function used for logging records. Function requires a single /// [LogRecord] as the only parameter. @@ -488,37 +489,40 @@ class StreamChatClient { /// Get the events missed while offline to sync the offline storage /// Will automatically fetch [cids] and [lastSyncedAt] if [persistenceEnabled] - Future sync({List? cids, DateTime? lastSyncAt}) async { - cids ??= await _chatPersistenceClient?.getChannelCids(); - if (cids == null || cids.isEmpty) { - return; - } + Future sync({List? cids, DateTime? lastSyncAt}) { + return synchronized(() async { + final channels = cids ?? await _chatPersistenceClient?.getChannelCids(); + if (channels == null || channels.isEmpty) { + return; + } - lastSyncAt ??= await _chatPersistenceClient?.getLastSyncAt(); - if (lastSyncAt == null) { - return; - } + final syncAt = + lastSyncAt ?? await _chatPersistenceClient?.getLastSyncAt(); + if (syncAt == null) { + return; + } - try { - final res = await _chatApi.general.sync(cids, lastSyncAt); - final events = res.events - ..sort((a, b) => a.createdAt.compareTo(b.createdAt)); - - for (final event in events) { - logger.fine('event.type: ${event.type}'); - final messageText = event.message?.text; - if (messageText != null) { - logger.fine('event.message.text: $messageText'); + try { + final res = await _chatApi.general.sync(channels, syncAt); + final events = res.events + ..sort((a, b) => a.createdAt.compareTo(b.createdAt)); + + for (final event in events) { + logger.fine('event.type: ${event.type}'); + final messageText = event.message?.text; + if (messageText != null) { + logger.fine('event.message.text: $messageText'); + } + handleEvent(event); } - handleEvent(event); - } - final now = DateTime.now(); - _lastSyncedAt = now; - _chatPersistenceClient?.updateLastSyncAt(now); - } catch (e, stk) { - logger.severe('Error during sync', e, stk); - } + final now = DateTime.now(); + _lastSyncedAt = now; + _chatPersistenceClient?.updateLastSyncAt(now); + } catch (e, stk) { + logger.severe('Error during sync', e, stk); + } + }); } final _queryChannelsStreams = >>{}; diff --git a/packages/stream_chat/pubspec.yaml b/packages/stream_chat/pubspec.yaml index f0bbc763d..61c1ff97c 100644 --- a/packages/stream_chat/pubspec.yaml +++ b/packages/stream_chat/pubspec.yaml @@ -22,6 +22,7 @@ dependencies: mime: ^1.0.4 rate_limiter: ^1.0.0 rxdart: ^0.27.7 + synchronized: ^3.0.0 uuid: ^3.0.7 web_socket_channel: ^2.3.0 From 91a378f720c0030c8b8af958e8d54b1fb4968e5c Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 5 May 2023 01:26:40 +0530 Subject: [PATCH 19/51] refactor: remove mutex as drift already uses one internally. Signed-off-by: xsahil03x --- .../src/stream_chat_persistence_client.dart | 279 ++++++++---------- packages/stream_chat_persistence/pubspec.yaml | 1 - 2 files changed, 124 insertions(+), 156 deletions(-) diff --git a/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart b/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart index 48e563dc9..9d2cb955b 100644 --- a/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart +++ b/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart @@ -1,7 +1,5 @@ import 'package:flutter/foundation.dart'; -import 'package:mutex/mutex.dart'; import 'package:stream_chat/stream_chat.dart'; - import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; /// Various connection modes on which [StreamChatPersistenceClient] can work @@ -48,7 +46,6 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { final Logger _logger; final ConnectionMode _connectionMode; final bool _webUseIndexedDbIfSupported; - final _mutex = ReadWriteMutex(); void _defaultLogHandler(LogRecord record) { print( @@ -59,9 +56,6 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { if (record.stackTrace != null) print(record.stackTrace); } - Future _readProtected(AsyncValueGetter func) => - _mutex.protectRead(func); - bool get _debugIsConnected { assert(() { if (db == null) { @@ -104,90 +98,84 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { Future getConnectionInfo() { assert(_debugIsConnected, ''); _logger.info('getConnectionInfo'); - return _readProtected(() => db!.connectionEventDao.connectionEvent); + return db!.connectionEventDao.connectionEvent; } @override Future updateConnectionInfo(Event event) { assert(_debugIsConnected, ''); _logger.info('updateConnectionInfo'); - return _readProtected( - () => db!.connectionEventDao.updateConnectionEvent(event), - ); + return db!.connectionEventDao.updateConnectionEvent(event); } @override Future updateLastSyncAt(DateTime lastSyncAt) { assert(_debugIsConnected, ''); _logger.info('updateLastSyncAt'); - return _readProtected( - () => db!.connectionEventDao.updateLastSyncAt(lastSyncAt), - ); + return db!.connectionEventDao.updateLastSyncAt(lastSyncAt); } @override Future getLastSyncAt() { assert(_debugIsConnected, ''); _logger.info('getLastSyncAt'); - return _readProtected(() => db!.connectionEventDao.lastSyncAt); + return db!.connectionEventDao.lastSyncAt; } @override Future deleteChannels(List cids) { assert(_debugIsConnected, ''); _logger.info('deleteChannels'); - return _readProtected(() => db!.channelDao.deleteChannelByCids(cids)); + return db!.channelDao.deleteChannelByCids(cids); } @override Future> getChannelCids() { assert(_debugIsConnected, ''); _logger.info('getChannelCids'); - return _readProtected(() => db!.channelDao.cids); + return db!.channelDao.cids; } @override Future deleteMessageByIds(List messageIds) { assert(_debugIsConnected, ''); _logger.info('deleteMessageByIds'); - return _readProtected(() => db!.messageDao.deleteMessageByIds(messageIds)); + return db!.messageDao.deleteMessageByIds(messageIds); } @override Future deletePinnedMessageByIds(List messageIds) { assert(_debugIsConnected, ''); _logger.info('deletePinnedMessageByIds'); - return _readProtected( - () => db!.pinnedMessageDao.deleteMessageByIds(messageIds), - ); + return db!.pinnedMessageDao.deleteMessageByIds(messageIds); } @override Future deleteMessageByCids(List cids) { assert(_debugIsConnected, ''); _logger.info('deleteMessageByCids'); - return _readProtected(() => db!.messageDao.deleteMessageByCids(cids)); + return db!.messageDao.deleteMessageByCids(cids); } @override Future deletePinnedMessageByCids(List cids) { assert(_debugIsConnected, ''); _logger.info('deletePinnedMessageByCids'); - return _readProtected(() => db!.pinnedMessageDao.deleteMessageByCids(cids)); + return db!.pinnedMessageDao.deleteMessageByCids(cids); } @override Future> getMembersByCid(String cid) { assert(_debugIsConnected, ''); _logger.info('getMembersByCid'); - return _readProtected(() => db!.memberDao.getMembersByCid(cid)); + return db!.memberDao.getMembersByCid(cid); } @override Future getChannelByCid(String cid) { assert(_debugIsConnected, ''); _logger.info('getChannelByCid'); - return _readProtected(() => db!.channelDao.getChannelByCid(cid)); + return db!.channelDao.getChannelByCid(cid); } @override @@ -197,11 +185,9 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { }) { assert(_debugIsConnected, ''); _logger.info('getMessagesByCid'); - return _readProtected( - () => db!.messageDao.getMessagesByCid( - cid, - messagePagination: messagePagination, - ), + return db!.messageDao.getMessagesByCid( + cid, + messagePagination: messagePagination, ); } @@ -212,37 +198,34 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { }) { assert(_debugIsConnected, ''); _logger.info('getPinnedMessagesByCid'); - return _readProtected( - () => db!.pinnedMessageDao.getMessagesByCid( - cid, - messagePagination: messagePagination, - ), + return db!.pinnedMessageDao.getMessagesByCid( + cid, + messagePagination: messagePagination, ); } @override - Future> getReadsByCid(String cid) { + Future> getReadsByCid(String cid) async { assert(_debugIsConnected, ''); _logger.info('getReadsByCid'); - return _readProtected(() => db!.readDao.getReadsByCid(cid)); + return db!.readDao.getReadsByCid(cid); } @override - Future>> getChannelThreads(String cid) { + Future>> getChannelThreads(String cid) async { assert(_debugIsConnected, ''); _logger.info('getChannelThreads'); - return _readProtected(() async { - final messages = await db!.messageDao.getThreadMessages(cid); - final messageByParentIdDictionary = >{}; - for (final message in messages) { - final parentId = message.parentId!; - messageByParentIdDictionary[parentId] = [ - ...messageByParentIdDictionary[parentId] ?? [], - message, - ]; - } - return messageByParentIdDictionary; - }); + final messages = await db!.messageDao.getThreadMessages(cid); + final messageByParentIdDictionary = >{}; + for (final message in messages) { + final parentId = message.parentId!; + messageByParentIdDictionary[parentId] = [ + ...messageByParentIdDictionary[parentId] ?? [], + message, + ]; + } + + return messageByParentIdDictionary; } @override @@ -252,11 +235,9 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { }) { assert(_debugIsConnected, ''); _logger.info('getReplies'); - return _readProtected( - () => db!.messageDao.getThreadMessagesByParentId( - parentId, - options: options, - ), + return db!.messageDao.getThreadMessagesByParentId( + parentId, + options: options, ); } @@ -268,73 +249,69 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { Please use channelStateSort instead.''') List>? sort, List>? channelStateSort, PaginationParams? paginationParams, - }) { + }) async { assert(_debugIsConnected, ''); assert( sort == null || channelStateSort == null, 'sort and channelStateSort cannot be used together', ); _logger.info('getChannelStates'); - return _readProtected( - () async { - final channels = await db!.channelQueryDao.getChannels( - filter: filter, - sort: sort, - ); - - final channelStates = await Future.wait( - channels.map((e) => getChannelStateByCid(e.cid)), - ); - - // Only sort the channel states if the channels are not already sorted. - if (sort == null) { - var chainedComparator = (ChannelState a, ChannelState b) { - final dateA = a.channel?.lastMessageAt ?? a.channel?.createdAt; - final dateB = b.channel?.lastMessageAt ?? b.channel?.createdAt; - - if (dateA == null && dateB == null) { - return 0; - } else if (dateA == null) { - return 1; - } else if (dateB == null) { - return -1; - } else { - return dateB.compareTo(dateA); + + final channels = await db!.channelQueryDao.getChannels( + filter: filter, + sort: sort, + ); + + final channelStates = await Future.wait( + channels.map((e) => getChannelStateByCid(e.cid)), + ); + + // Only sort the channel states if the channels are not already sorted. + if (sort == null) { + var chainedComparator = (ChannelState a, ChannelState b) { + final dateA = a.channel?.lastMessageAt ?? a.channel?.createdAt; + final dateB = b.channel?.lastMessageAt ?? b.channel?.createdAt; + + if (dateA == null && dateB == null) { + return 0; + } else if (dateA == null) { + return 1; + } else if (dateB == null) { + return -1; + } else { + return dateB.compareTo(dateA); + } + }; + + if (channelStateSort != null && channelStateSort.isNotEmpty) { + chainedComparator = (a, b) { + int result; + for (final comparator + in channelStateSort.map((it) => it.comparator).withNullifyer) { + try { + result = comparator(a, b); + } catch (e) { + result = 0; } - }; - - if (channelStateSort != null && channelStateSort.isNotEmpty) { - chainedComparator = (a, b) { - int result; - for (final comparator in channelStateSort - .map((it) => it.comparator) - .withNullifyer) { - try { - result = comparator(a, b); - } catch (e) { - result = 0; - } - if (result != 0) return result; - } - return 0; - }; + if (result != 0) return result; } + return 0; + }; + } - channelStates.sort(chainedComparator); - } + channelStates.sort(chainedComparator); + } - final offset = paginationParams?.offset; - if (offset != null && offset > 0 && channelStates.isNotEmpty) { - channelStates.removeRange(0, offset); - } + final offset = paginationParams?.offset; + if (offset != null && offset > 0 && channelStates.isNotEmpty) { + channelStates.removeRange(0, offset); + } - if (paginationParams?.limit != null) { - return channelStates.take(paginationParams!.limit).toList(); - } + if (paginationParams?.limit != null) { + return channelStates.take(paginationParams!.limit).toList(); + } - return channelStates; - }, - ); + return channelStates; } @override @@ -345,12 +322,10 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { }) { assert(_debugIsConnected, ''); _logger.info('updateChannelQueries'); - return _readProtected( - () => db!.channelQueryDao.updateChannelQueries( - filter, - cids, - clearQueryCache: clearQueryCache, - ), + return db!.channelQueryDao.updateChannelQueries( + filter, + cids, + clearQueryCache: clearQueryCache, ); } @@ -358,60 +333,56 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { Future updateChannels(List channels) { assert(_debugIsConnected, ''); _logger.info('updateChannels'); - return _readProtected(() => db!.channelDao.updateChannels(channels)); + return db!.channelDao.updateChannels(channels); } @override Future bulkUpdateMembers(Map?> members) { assert(_debugIsConnected, ''); _logger.info('bulkUpdateMembers'); - return _readProtected(() => db!.memberDao.bulkUpdateMembers(members)); + return db!.memberDao.bulkUpdateMembers(members); } @override Future bulkUpdateMessages(Map?> messages) { assert(_debugIsConnected, ''); _logger.info('bulkUpdateMessages'); - return _readProtected(() => db!.messageDao.bulkUpdateMessages(messages)); + return db!.messageDao.bulkUpdateMessages(messages); } @override Future bulkUpdatePinnedMessages(Map?> messages) { assert(_debugIsConnected, ''); _logger.info('bulkUpdatePinnedMessages'); - return _readProtected( - () => db!.pinnedMessageDao.bulkUpdateMessages(messages), - ); + return db!.pinnedMessageDao.bulkUpdateMessages(messages); } @override Future updatePinnedMessageReactions(List reactions) { assert(_debugIsConnected, ''); _logger.info('updatePinnedMessageReactions'); - return _readProtected( - () => db!.pinnedMessageReactionDao.updateReactions(reactions), - ); + return db!.pinnedMessageReactionDao.updateReactions(reactions); } @override Future updateReactions(List reactions) { assert(_debugIsConnected, ''); _logger.info('updateReactions'); - return _readProtected(() => db!.reactionDao.updateReactions(reactions)); + return db!.reactionDao.updateReactions(reactions); } @override Future bulkUpdateReads(Map?> reads) { assert(_debugIsConnected, ''); _logger.info('bulkUpdateReads'); - return _readProtected(() => db!.readDao.bulkUpdateReads(reads)); + return db!.readDao.bulkUpdateReads(reads); } @override Future updateUsers(List users) { assert(_debugIsConnected, ''); _logger.info('updateUsers'); - return _readProtected(() => db!.userDao.updateUsers(users)); + return db!.userDao.updateUsers(users); } @override @@ -420,53 +391,51 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { ) { assert(_debugIsConnected, ''); _logger.info('deletePinnedMessageReactionsByMessageId'); - return _readProtected( - () => - db!.pinnedMessageReactionDao.deleteReactionsByMessageIds(messageIds), - ); + return db!.pinnedMessageReactionDao.deleteReactionsByMessageIds(messageIds); } @override Future deleteReactionsByMessageId(List messageIds) { assert(_debugIsConnected, ''); _logger.info('deleteReactionsByMessageId'); - return _readProtected( - () => db!.reactionDao.deleteReactionsByMessageIds(messageIds), - ); + return db!.reactionDao.deleteReactionsByMessageIds(messageIds); } @override Future deleteMembersByCids(List cids) { assert(_debugIsConnected, ''); _logger.info('deleteMembersByCids'); - return _readProtected(() => db!.memberDao.deleteMemberByCids(cids)); + return db!.memberDao.deleteMemberByCids(cids); + } + + @override + Future updateChannelThreads( + String cid, + Map> threads, + ) { + assert(_debugIsConnected, ''); + _logger.info('updateChannelThreads'); + return db!.transaction(() => super.updateChannelThreads(cid, threads)); } @override Future updateChannelStates(List channelStates) { assert(_debugIsConnected, ''); _logger.info('updateChannelStates'); - return _readProtected( - () async => db!.transaction( - () async { - await super.updateChannelStates(channelStates); - }, - ), - ); + return db!.transaction(() => super.updateChannelStates(channelStates)); } @override - Future disconnect({bool flush = false}) async => - _mutex.protectWrite(() async { - _logger.info('disconnect'); - if (db != null) { - _logger.info('Disconnecting'); - if (flush) { - _logger.info('Flushing'); - await db!.flush(); - } - await db!.disconnect(); - db = null; - } - }); + Future disconnect({bool flush = false}) async { + _logger.info('disconnect'); + if (db != null) { + _logger.info('Disconnecting'); + if (flush) { + _logger.info('Flushing'); + await db!.flush(); + } + await db!.disconnect(); + db = null; + } + } } diff --git a/packages/stream_chat_persistence/pubspec.yaml b/packages/stream_chat_persistence/pubspec.yaml index 232753961..83fa7a111 100644 --- a/packages/stream_chat_persistence/pubspec.yaml +++ b/packages/stream_chat_persistence/pubspec.yaml @@ -15,7 +15,6 @@ dependencies: sdk: flutter logging: ^1.0.1 meta: ^1.8.0 - mutex: ^3.0.0 path: ^1.8.2 path_provider: ^2.0.1 sqlite3_flutter_libs: ^0.5.0 From 167d60dcacae417573de40f0ab98b46ae6c58ed7 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 5 May 2023 01:27:06 +0530 Subject: [PATCH 20/51] doc: advice to use `regular` as the default connectionMode Signed-off-by: xsahil03x --- packages/stream_chat_persistence/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_persistence/README.md b/packages/stream_chat_persistence/README.md index 0851335b8..1d7f8bafe 100644 --- a/packages/stream_chat_persistence/README.md +++ b/packages/stream_chat_persistence/README.md @@ -37,7 +37,7 @@ The usage is pretty simple. ```dart final chatPersistentClient = StreamChatPersistenceClient( logLevel: Level.INFO, - connectionMode: ConnectionMode.background, + connectionMode: ConnectionMode.regular, ); ``` 2. Pass the instance to the official Stream chat client. From 9e9d2dfc18f97783b91f4b9257986f03ab73cab5 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 5 May 2023 01:55:03 +0530 Subject: [PATCH 21/51] chore: improve comparator. Signed-off-by: xsahil03x --- .../src/stream_chat_persistence_client.dart | 66 +++++++++++-------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart b/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart index 9d2cb955b..b19977dba 100644 --- a/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart +++ b/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart @@ -268,38 +268,14 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { // Only sort the channel states if the channels are not already sorted. if (sort == null) { - var chainedComparator = (ChannelState a, ChannelState b) { - final dateA = a.channel?.lastMessageAt ?? a.channel?.createdAt; - final dateB = b.channel?.lastMessageAt ?? b.channel?.createdAt; - - if (dateA == null && dateB == null) { - return 0; - } else if (dateA == null) { - return 1; - } else if (dateB == null) { - return -1; - } else { - return dateB.compareTo(dateA); - } - }; - + var comparator = _defaultChannelStateComparator; if (channelStateSort != null && channelStateSort.isNotEmpty) { - chainedComparator = (a, b) { - int result; - for (final comparator - in channelStateSort.map((it) => it.comparator).withNullifyer) { - try { - result = comparator(a, b); - } catch (e) { - result = 0; - } - if (result != 0) return result; - } - return 0; - }; + comparator = _combineComparators( + channelStateSort.map((it) => it.comparator).withNullifyer, + ); } - channelStates.sort(chainedComparator); + channelStates.sort(comparator); } final offset = paginationParams?.offset; @@ -439,3 +415,35 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { } } } + +// Creates a new combined [Comparator] which sorts items +// by the given [comparators]. +Comparator _combineComparators(Iterable> comparators) { + return (T a, T b) { + for (final comparator in comparators) { + try { + final result = comparator(a, b); + if (result != 0) return result; + } catch (e) { + // If the comparator throws an exception, we ignore it and + // continue with the next comparator. + continue; + } + } + return 0; + }; +} + +// The default [Comparator] used to sort [ChannelState]s. +int _defaultChannelStateComparator(ChannelState a, ChannelState b) { + final dateA = a.channel?.lastMessageAt ?? a.channel?.createdAt; + final dateB = b.channel?.lastMessageAt ?? b.channel?.createdAt; + + if (dateA == null && dateB == null) return 0; + if (dateA == null) return 1; + if (dateB == null) { + return -1; + } else { + return dateB.compareTo(dateA); + } +} From 7cc8e9ea5a8a4914e6668df56527d6cb9e976e58 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 5 May 2023 01:55:26 +0530 Subject: [PATCH 22/51] ci: run action when draft status changes. Signed-off-by: xsahil03x --- .github/workflows/stream_flutter_workflow.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml index af57f49f6..b63933bb7 100644 --- a/.github/workflows/stream_flutter_workflow.yml +++ b/.github/workflows/stream_flutter_workflow.yml @@ -8,6 +8,11 @@ on: pull_request: paths: - 'packages/**' + types: + - opened + - reopened + - synchronize + - ready_for_review push: branches: - master From ee62abdbc40c8bd6368f5456589276c6ae6710d7 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 5 May 2023 02:21:06 +0530 Subject: [PATCH 23/51] test: add test. Signed-off-by: xsahil03x --- .../lib/src/stream_chat_persistence_client.dart | 1 + .../test/stream_chat_persistence_client_test.dart | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart b/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart index b19977dba..3d5a5bdbc 100644 --- a/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart +++ b/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart @@ -90,6 +90,7 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { 'disconnect the previous instance before connecting again.', ); } + _logger.info('connect'); db = databaseProvider?.call(userId, _connectionMode) ?? await _defaultDatabaseProvider(userId, _connectionMode); } diff --git a/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart b/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart index 8d7ab8be2..c99668fd9 100644 --- a/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart +++ b/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart @@ -55,6 +55,15 @@ void main() { expect(client.db, isNull); }); + test('client function throws stateError if db is not yet connected', () { + final client = StreamChatPersistenceClient(logLevel: Level.ALL); + expect( + // Running a function that requires db connection. + () => client.getReplies('testParentId'), + throwsA(isA()), + ); + }); + group('client functions', () { const userId = 'testUserId'; final mockDatabase = MockChatDatabase(); @@ -66,6 +75,10 @@ void main() { await client.connect(userId, databaseProvider: _mockDatabaseProvider); }); + tearDown(() async { + await client.disconnect(); + }); + test('getReplies', () async { const parentId = 'testParentId'; final replies = List.generate(3, (index) => Message(id: 'testId$index')); From e791fe4c1ed9d27688272794a053ce3a8c90956e Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 5 May 2023 17:39:26 +0530 Subject: [PATCH 24/51] fix: logging interceptor formatting. Signed-off-by: xsahil03x --- .../lib/src/core/http/interceptor/logging_interceptor.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/stream_chat/lib/src/core/http/interceptor/logging_interceptor.dart b/packages/stream_chat/lib/src/core/http/interceptor/logging_interceptor.dart index 6b07495de..72fd70d03 100644 --- a/packages/stream_chat/lib/src/core/http/interceptor/logging_interceptor.dart +++ b/packages/stream_chat/lib/src/core/http/interceptor/logging_interceptor.dart @@ -101,8 +101,7 @@ class LoggingInterceptor extends Interceptor { options.data as Map?, header: 'Body', ); - } - if (data is FormData) { + } else if (data is FormData) { final formDataMap = {} ..addEntries(data.fields) ..addEntries(data.files); @@ -163,7 +162,7 @@ class LoggingInterceptor extends Interceptor { _logPrintResponse('โ•‘'); _printResponse(_logPrintResponse, response); _logPrintResponse('โ•‘'); - _logPrintResponse('โ•š'); + _printLine(_logPrintResponse, 'โ•š'); } super.onResponse(response, handler); } From 89bf63df05dcc6b983e95d840d4c34b98f0d7a61 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 5 May 2023 17:39:49 +0530 Subject: [PATCH 25/51] feat: add support for external interceptors. Signed-off-by: xsahil03x --- .../stream_chat/lib/src/client/client.dart | 2 + .../lib/src/core/api/stream_chat_api.dart | 5 +- .../lib/src/core/http/stream_http_client.dart | 34 ++++--- packages/stream_chat/lib/stream_chat.dart | 2 + .../core/http/stream_http_client_test.dart | 88 +++++++++++++------ 5 files changed, 88 insertions(+), 43 deletions(-) diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 72dfe9893..7627cf1df 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -73,6 +73,7 @@ class StreamChatClient { WebSocket? ws, AttachmentFileUploaderProvider attachmentFileUploaderProvider = StreamAttachmentFileUploader.new, + Iterable? chatApiInterceptors, }) { logger.info('Initiating new StreamChatClient'); @@ -91,6 +92,7 @@ class StreamChatClient { connectionIdManager: _connectionIdManager, attachmentFileUploaderProvider: attachmentFileUploaderProvider, logger: detachedLogger('๐Ÿ•ธ๏ธ'), + interceptors: chatApiInterceptors, ); _ws = ws ?? diff --git a/packages/stream_chat/lib/src/core/api/stream_chat_api.dart b/packages/stream_chat/lib/src/core/api/stream_chat_api.dart index 7e725204d..b6850bac4 100644 --- a/packages/stream_chat/lib/src/core/api/stream_chat_api.dart +++ b/packages/stream_chat/lib/src/core/api/stream_chat_api.dart @@ -1,3 +1,4 @@ +import 'package:dio/dio.dart'; import 'package:logging/logging.dart'; import 'package:stream_chat/src/core/api/attachment_file_uploader.dart'; import 'package:stream_chat/src/core/api/call_api.dart'; @@ -14,7 +15,7 @@ import 'package:stream_chat/src/core/http/token_manager.dart'; export 'device_api.dart' show PushProvider; -/// ApiClient that wraps every other specific api +/// Api_client that wraps every other specific api class StreamChatApi { /// Initialize a new stream chat api StreamChatApi( @@ -26,6 +27,7 @@ class StreamChatApi { AttachmentFileUploaderProvider attachmentFileUploaderProvider = StreamAttachmentFileUploader.new, Logger? logger, + Iterable? interceptors, }) : _fileUploaderProvider = attachmentFileUploaderProvider, _client = client ?? StreamHttpClient( @@ -34,6 +36,7 @@ class StreamChatApi { tokenManager: tokenManager, connectionIdManager: connectionIdManager, logger: logger, + interceptors: interceptors, ); final StreamHttpClient _client; diff --git a/packages/stream_chat/lib/src/core/http/stream_http_client.dart b/packages/stream_chat/lib/src/core/http/stream_http_client.dart index 4a13fbaf5..99e72339f 100644 --- a/packages/stream_chat/lib/src/core/http/stream_http_client.dart +++ b/packages/stream_chat/lib/src/core/http/stream_http_client.dart @@ -25,6 +25,7 @@ class StreamHttpClient { TokenManager? tokenManager, ConnectionIdManager? connectionIdManager, Logger? logger, + Iterable? interceptors, }) : _options = options ?? const StreamHttpClientOptions(), httpClient = dio ?? Dio() { httpClient @@ -45,20 +46,25 @@ class StreamHttpClient { if (tokenManager != null) AuthInterceptor(this, tokenManager), if (connectionIdManager != null) ConnectionIdInterceptor(connectionIdManager), - if (logger != null && logger.level != Level.OFF) - LoggingInterceptor( - requestHeader: true, - logPrint: (step, message) { - switch (step) { - case InterceptStep.request: - return logger.info(message); - case InterceptStep.response: - return logger.info(message); - case InterceptStep.error: - return logger.severe(message); - } - }, - ), + ...interceptors ?? + [ + // Add a default logging interceptor if no interceptors are + // provided. + if (logger != null && logger.level != Level.OFF) + LoggingInterceptor( + requestHeader: true, + logPrint: (step, message) { + switch (step) { + case InterceptStep.request: + return logger.info(message); + case InterceptStep.response: + return logger.info(message); + case InterceptStep.error: + return logger.severe(message); + } + }, + ), + ], ]); } diff --git a/packages/stream_chat/lib/stream_chat.dart b/packages/stream_chat/lib/stream_chat.dart index f214c852d..1e0df28c6 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -3,6 +3,7 @@ library stream_chat; export 'package:async/async.dart'; export 'package:dio/src/cancel_token.dart'; export 'package:dio/src/dio_error.dart'; +export 'package:dio/src/dio_mixin.dart' show Interceptor, InterceptorsWrapper; export 'package:dio/src/multipart_file.dart'; export 'package:dio/src/options.dart'; export 'package:dio/src/options.dart' show ProgressCallback; @@ -19,6 +20,7 @@ export 'src/core/api/responses.dart'; export 'src/core/api/stream_chat_api.dart' show PushProvider; export 'src/core/api/stream_chat_api.dart'; export 'src/core/error/error.dart'; +export 'src/core/http/interceptor/logging_interceptor.dart'; export 'src/core/models/action.dart'; export 'src/core/models/attachment.dart'; export 'src/core/models/attachment_file.dart'; diff --git a/packages/stream_chat/test/src/core/http/stream_http_client_test.dart b/packages/stream_chat/test/src/core/http/stream_http_client_test.dart index 4854b98f8..51533b115 100644 --- a/packages/stream_chat/test/src/core/http/stream_http_client_test.dart +++ b/packages/stream_chat/test/src/core/http/stream_http_client_test.dart @@ -86,41 +86,73 @@ void main() { }, ); - test('loggingInterceptor should be added if logger is provided', () { - const apiKey = 'api-key'; - final client = StreamHttpClient( - apiKey, - logger: Logger('test-logger'), - ); + group('loggingInterceptor', () { + test('should be added if logger is provided', () { + const apiKey = 'api-key'; + final client = StreamHttpClient( + apiKey, + logger: Logger('test-logger'), + ); - expect( - client.httpClient.interceptors.whereType().length, - 1, - ); - }); + expect( + client.httpClient.interceptors.whereType().length, + 1, + ); + }); - test('loggingInterceptor should log requests', () async { - const apiKey = 'api-key'; - final logger = MockLogger(); - final client = StreamHttpClient(apiKey, logger: logger); + test('should not be added if logger.level is OFF', () { + const apiKey = 'api-key'; + final client = StreamHttpClient( + apiKey, + logger: Logger.detached('test-logger')..level = Level.OFF, + ); - try { - await client.get('path'); - } catch (_) {} + expect( + client.httpClient.interceptors.whereType().length, + 0, + ); + }); - verify(() => logger.info(any())).called(greaterThan(0)); - }); + test('should not be added if `interceptors` are provided', () { + const apiKey = 'api-key'; + final client = StreamHttpClient( + apiKey, + logger: Logger.detached('test-logger'), + interceptors: [ + // Sample Interceptor. + InterceptorsWrapper(), + ], + ); - test('loggingInterceptor should log error', () async { - const apiKey = 'api-key'; - final logger = MockLogger(); - final client = StreamHttpClient(apiKey, logger: logger); + expect( + client.httpClient.interceptors.whereType().length, + 0, + ); + }); - try { - await client.get('path'); - } catch (_) {} + test('should log requests', () async { + const apiKey = 'api-key'; + final logger = MockLogger(); + final client = StreamHttpClient(apiKey, logger: logger); + + try { + await client.get('path'); + } catch (_) {} + + verify(() => logger.info(any())).called(greaterThan(0)); + }); + + test('should log error', () async { + const apiKey = 'api-key'; + final logger = MockLogger(); + final client = StreamHttpClient(apiKey, logger: logger); + + try { + await client.get('path'); + } catch (_) {} - verify(() => logger.severe(any())).called(greaterThan(0)); + verify(() => logger.severe(any())).called(greaterThan(0)); + }); }); test('`.close` should close the dio client', () async { From e6ca754b9857da4a228611d4bffa308b9e15f11e Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 5 May 2023 17:48:12 +0530 Subject: [PATCH 26/51] chore: update changelog. Signed-off-by: xsahil03x --- packages/stream_chat/CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index f28d7e659..df72ed55d 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -10,6 +10,29 @@ - Expose `ChannelMute` class. [#1473](https://github.com/GetStream/stream-chat-flutter/issues/1473) - Added synchronization to the `StreamChatClient.sync` api. [#1392](https://github.com/GetStream/stream-chat-flutter/issues/1392) +- Added support for `StreamChatClient.chatApiInterceptors` to add custom interceptors to the API client. + [#1265](https://github.com/GetStream/stream-chat-flutter/issues/1265). + + ```dart + final client = StreamChatClient( + chatApiInterceptors: [ + InterceptorsWrapper( + onRequest: (options, handler) { + // Do something before request is sent. + return handler.next(options); + }, + onResponse: (response, handler) { + // Do something with response data + return handler.next(response); + }, + onError: (DioError e, handler) { + // Do something with response error + return handler.next(e); + }, + ), + ], + ); + ``` ## 6.0.0 From fb5a753ecf51548f76ecbee5998d93c42e9efdf2 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 5 May 2023 18:04:40 +0530 Subject: [PATCH 27/51] Update packages/stream_chat/lib/src/core/api/stream_chat_api.dart --- packages/stream_chat/lib/src/core/api/stream_chat_api.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat/lib/src/core/api/stream_chat_api.dart b/packages/stream_chat/lib/src/core/api/stream_chat_api.dart index b6850bac4..73b6a23a8 100644 --- a/packages/stream_chat/lib/src/core/api/stream_chat_api.dart +++ b/packages/stream_chat/lib/src/core/api/stream_chat_api.dart @@ -15,7 +15,7 @@ import 'package:stream_chat/src/core/http/token_manager.dart'; export 'device_api.dart' show PushProvider; -/// Api_client that wraps every other specific api +/// ApiClient that wraps every other specific api class StreamChatApi { /// Initialize a new stream chat api StreamChatApi( From e9769f1e9797942025157c197bb8607071f938d0 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 8 May 2023 18:22:54 +0530 Subject: [PATCH 28/51] fix(ui): Fix message theme not applied correctly. Signed-off-by: xsahil03x --- .../message_list_view/message_list_view.dart | 12 ++-- .../src/message_widget/deleted_message.dart | 5 +- .../lib/src/message_widget/message_card.dart | 3 +- .../reactions/desktop_reactions_builder.dart | 62 +++++++++++-------- .../reactions/message_reactions_modal.dart | 7 ++- .../lib/src/theme/stream_chat_theme.dart | 8 +-- .../lib/src/utils/device_segmentation.dart | 4 +- 7 files changed, 56 insertions(+), 45 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart index d11260639..5435e95cf 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart @@ -893,6 +893,11 @@ class _StreamMessageListViewState extends State { final currentUserMember = members.firstWhereOrNull((e) => e.user!.id == currentUser!.id); + final hasUrlAttachment = + message.attachments.any((it) => it.ogScrapeUrl != null); + + final borderSide = isOnlyEmoji || hasUrlAttachment ? BorderSide.none : null; + final defaultMessageWidget = StreamMessageWidget( showReplyMessage: false, showResendMessage: false, @@ -917,7 +922,7 @@ class _StreamMessageListViewState extends State { vertical: 8, horizontal: isOnlyEmoji ? 0 : 16.0, ), - borderSide: isMyMessage || isOnlyEmoji ? BorderSide.none : null, + borderSide: borderSide, showUserAvatar: isMyMessage ? DisplayWidget.gone : DisplayWidget.show, messageTheme: isMyMessage ? _streamTheme.ownMessageTheme @@ -1096,10 +1101,7 @@ class _StreamMessageListViewState extends State { final hasUrlAttachment = message.attachments.any((it) => it.ogScrapeUrl != null); - final borderSide = - isOnlyEmoji || hasUrlAttachment || (isMyMessage && !hasFileAttachment) - ? BorderSide.none - : null; + final borderSide = isOnlyEmoji || hasUrlAttachment ? BorderSide.none : null; final currentUser = StreamChat.of(context).currentUser; final members = StreamChannel.of(context).channel.state?.members ?? []; diff --git a/packages/stream_chat_flutter/lib/src/message_widget/deleted_message.dart b/packages/stream_chat_flutter/lib/src/message_widget/deleted_message.dart index 8cd16165c..ca82b5b83 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/deleted_message.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/deleted_message.dart @@ -32,7 +32,6 @@ class StreamDeletedMessage extends StatelessWidget { @override Widget build(BuildContext context) { - final chatThemeData = StreamChatTheme.of(context); return Material( color: messageTheme.messageBackgroundColor, shape: shape ?? @@ -40,9 +39,7 @@ class StreamDeletedMessage extends StatelessWidget { borderRadius: borderRadiusGeometry ?? BorderRadius.zero, side: borderSide ?? BorderSide( - color: Theme.of(context).brightness == Brightness.dark - ? chatThemeData.colorTheme.barsBg.withAlpha(24) - : chatThemeData.colorTheme.textHighEmphasis.withAlpha(24), + color: messageTheme.messageBorderColor ?? Colors.transparent, ), ), child: Padding( diff --git a/packages/stream_chat_flutter/lib/src/message_widget/message_card.dart b/packages/stream_chat_flutter/lib/src/message_widget/message_card.dart index 0f59d7fee..e7f1d5069 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/message_card.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/message_card.dart @@ -139,7 +139,8 @@ class _MessageCardState extends State { RoundedRectangleBorder( side: widget.borderSide ?? BorderSide( - color: widget.messageTheme.messageBorderColor ?? Colors.grey, + color: widget.messageTheme.messageBorderColor ?? + Colors.transparent, ), borderRadius: widget.borderRadiusGeometry ?? BorderRadius.zero, ), diff --git a/packages/stream_chat_flutter/lib/src/message_widget/reactions/desktop_reactions_builder.dart b/packages/stream_chat_flutter/lib/src/message_widget/reactions/desktop_reactions_builder.dart index bdc10a340..b681fff54 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/reactions/desktop_reactions_builder.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/reactions/desktop_reactions_builder.dart @@ -163,23 +163,28 @@ class _DesktopReactionsBuilderState extends State { onExit: (event) { setState(() => _showReactionsPopup = !_showReactionsPopup); }, - child: Wrap( - children: [ - ...reactionsList.map((reaction) { - final reactionIcon = reactionIcons.firstWhereOrNull( - (r) => r.type == reaction.type, - ); + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Wrap( + spacing: 4, + runSpacing: 4, + children: [ + ...reactionsList.map((reaction) { + final reactionIcon = reactionIcons.firstWhereOrNull( + (r) => r.type == reaction.type, + ); - return _BottomReaction( - reaction: reaction, - message: widget.message, - borderSide: widget.borderSide, - messageTheme: widget.messageTheme, - reactionIcon: reactionIcon, - streamChatTheme: streamChatTheme, - ); - }).toList(), - ], + return _BottomReaction( + reaction: reaction, + message: widget.message, + borderSide: widget.borderSide, + messageTheme: widget.messageTheme, + reactionIcon: reactionIcon, + streamChatTheme: streamChatTheme, + ); + }).toList(), + ], + ), ), ), ); @@ -206,6 +211,9 @@ class _BottomReaction extends StatelessWidget { @override Widget build(BuildContext context) { final userId = StreamChat.of(context).currentUser?.id; + + final backgroundColor = messageTheme?.reactionsBackgroundColor; + return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { @@ -225,33 +233,35 @@ class _BottomReaction extends StatelessWidget { } }, child: Card( - shape: StadiumBorder( + margin: EdgeInsets.zero, + // Setting elevation as null when background color is transparent. + // This is done to avoid shadow when background color is transparent. + elevation: backgroundColor == Colors.transparent ? 0 : null, + shape: RoundedRectangleBorder( side: borderSide ?? BorderSide( - color: messageTheme?.messageBorderColor ?? Colors.grey, + color: messageTheme?.reactionsBorderColor ?? Colors.transparent, ), + borderRadius: BorderRadius.circular(10), ), - color: messageTheme?.messageBackgroundColor, + color: backgroundColor, child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), child: Row( mainAxisSize: MainAxisSize.min, children: [ ConstrainedBox( constraints: BoxConstraints.tight( - const Size.square(16), + const Size.square(14), ), child: reactionIcon?.builder( context, reaction.user?.id == userId, - 16, + 14, ) ?? Icon( Icons.help_outline_rounded, - size: 16, + size: 14, color: reaction.user?.id == userId ? streamChatTheme.colorTheme.accentPrimary : streamChatTheme.colorTheme.textHighEmphasis diff --git a/packages/stream_chat_flutter/lib/src/message_widget/reactions/message_reactions_modal.dart b/packages/stream_chat_flutter/lib/src/message_widget/reactions/message_reactions_modal.dart index 0703ce19f..04c749cc4 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/reactions/message_reactions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/reactions/message_reactions_modal.dart @@ -174,6 +174,7 @@ class StreamMessageReactionsModal extends StatelessWidget { ) { final isCurrentUser = reaction.user?.id == currentUser.id; final chatThemeData = StreamChatTheme.of(context); + final reverse = !isCurrentUser; return ConstrainedBox( constraints: BoxConstraints.loose( const Size(64, 100), @@ -199,13 +200,14 @@ class StreamMessageReactionsModal extends StatelessWidget { ), Positioned( bottom: 6, - left: isCurrentUser ? -3 : null, - right: isCurrentUser ? -3 : null, + left: !reverse ? -3 : null, + right: reverse ? -3 : null, child: Align( alignment: reverse ? Alignment.centerRight : Alignment.centerLeft, child: StreamReactionBubble( reactions: [reaction], + reverse: !reverse, flipTail: !reverse, borderColor: messageTheme.reactionsBorderColor ?? Colors.transparent, @@ -213,7 +215,6 @@ class StreamMessageReactionsModal extends StatelessWidget { Colors.transparent, maskColor: chatThemeData.colorTheme.barsBg, tailCirclesSpacing: 1, - highlightOwnReactions: false, ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.dart index 01993efb1..02ac6f4c3 100644 --- a/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.dart @@ -184,11 +184,11 @@ class StreamChatThemeData { createdAtStyle: textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis), repliesStyle: textTheme.footnoteBold.copyWith(color: accentColor), - messageBackgroundColor: colorTheme.disabled, + messageBackgroundColor: colorTheme.borders, + messageBorderColor: colorTheme.borders, reactionsBackgroundColor: colorTheme.barsBg, reactionsBorderColor: colorTheme.borders, reactionsMaskColor: colorTheme.appBg, - messageBorderColor: colorTheme.disabled, avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( @@ -207,8 +207,8 @@ class StreamChatThemeData { textTheme.body.copyWith(fontWeight: FontWeight.w400), ), otherMessageTheme: StreamMessageThemeData( - reactionsBackgroundColor: colorTheme.disabled, - reactionsBorderColor: colorTheme.barsBg, + reactionsBackgroundColor: colorTheme.borders, + reactionsBorderColor: colorTheme.borders, reactionsMaskColor: colorTheme.appBg, messageTextStyle: textTheme.body, createdAtStyle: diff --git a/packages/stream_chat_flutter/lib/src/utils/device_segmentation.dart b/packages/stream_chat_flutter/lib/src/utils/device_segmentation.dart index bd65820c5..3f815d0c5 100644 --- a/packages/stream_chat_flutter/lib/src/utils/device_segmentation.dart +++ b/packages/stream_chat_flutter/lib/src/utils/device_segmentation.dart @@ -4,7 +4,7 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; bool get isWeb => CurrentPlatform.isWeb; /// Returns true if the app is running in a mobile device. -bool get isMobileDevice => CurrentPlatform.isIos || CurrentPlatform.isAndroid; +bool get isMobileDevice => true; /// Returns true if the app is running in a desktop device. bool get isDesktopDevice => @@ -22,7 +22,7 @@ bool get isDesktopVideoPlayerSupported => bool get isMobileDeviceOrWeb => isWeb || isMobileDevice; /// Returns true if the app is running in a desktop or web. -bool get isDesktopDeviceOrWeb => isWeb || isDesktopDevice; +bool get isDesktopDeviceOrWeb => false; /// Returns true if the app is running in a flutter test environment. bool get isTestEnvironment => CurrentPlatform.isFlutterTest; From 117a764345ea42aef7de113c241ff4c8b47ede92 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 8 May 2023 18:24:19 +0530 Subject: [PATCH 29/51] chore: update changelog. Signed-off-by: xsahil03x --- packages/stream_chat_flutter/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 624577e0b..778c162d3 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -11,6 +11,8 @@ works only for otherUser. - [[#1490]](https://github.com/GetStream/stream-chat-flutter/issues/1490) Fixed `editMessageInputBuilder` property not used in message edit widget. +- [[#1523]](https://github.com/GetStream/stream-chat-flutter/issues/1523) Fixed `StreamMessageThemeData` not being + applied correctly. โœ… Added From 5ace429216a508e122466101271a247a5f4e63d2 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 8 May 2023 20:26:15 +0530 Subject: [PATCH 30/51] fix(llc): fix cannot modify immutable list. Signed-off-by: xsahil03x --- .../stream_chat/lib/src/client/channel.dart | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 1d419cbb2..8152753de 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -997,26 +997,24 @@ class Channel { ) async { final type = reaction.type; - final reactionCounts = {...message.reactionCounts ?? {}}; + final reactionCounts = {...?message.reactionCounts}; if (reactionCounts.containsKey(type)) { reactionCounts.update(type, (value) => value - 1); } - final reactionScores = {...message.reactionScores ?? {}}; + final reactionScores = {...?message.reactionScores}; if (reactionScores.containsKey(type)) { reactionScores.update(type, (value) => value - 1); } - final latestReactions = [...message.latestReactions ?? []] - ..removeWhere((r) => - r.userId == reaction.userId && - r.type == reaction.type && - r.messageId == reaction.messageId); - - final ownReactions = message.ownReactions - ?..removeWhere((r) => - r.userId == reaction.userId && - r.type == reaction.type && - r.messageId == reaction.messageId); + final latestReactions = [...?message.latestReactions]..removeWhere((r) => + r.userId == reaction.userId && + r.type == reaction.type && + r.messageId == reaction.messageId); + + final ownReactions = [...?message.ownReactions]..removeWhere((r) => + r.userId == reaction.userId && + r.type == reaction.type && + r.messageId == reaction.messageId); final newMessage = message.copyWith( reactionCounts: reactionCounts..removeWhere((_, value) => value == 0), From 161057ef4cba6e8ea9f6302db8f7106bd385de3f Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 8 May 2023 20:27:39 +0530 Subject: [PATCH 31/51] fix: fix reaction alignment and theming. Signed-off-by: xsahil03x --- .../reactions/desktop_reactions_builder.dart | 145 ++---------------- .../reactions/message_reactions_modal.dart | 115 +------------- .../reactions/reaction_bubble.dart | 14 +- .../reactions/reactions_card.dart | 142 +++++++++++++++++ .../lib/src/misc/reaction_icon.dart | 17 +- .../lib/src/stream_chat_configuration.dart | 10 +- .../lib/src/theme/stream_chat_theme.dart | 3 +- 7 files changed, 187 insertions(+), 259 deletions(-) create mode 100644 packages/stream_chat_flutter/lib/src/message_widget/reactions/reactions_card.dart diff --git a/packages/stream_chat_flutter/lib/src/message_widget/reactions/desktop_reactions_builder.dart b/packages/stream_chat_flutter/lib/src/message_widget/reactions/desktop_reactions_builder.dart index b681fff54..cbc55bdbb 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/reactions/desktop_reactions_builder.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/reactions/desktop_reactions_builder.dart @@ -2,6 +2,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_portal/flutter_portal.dart'; +import 'package:stream_chat_flutter/src/message_widget/reactions/reactions_card.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; // ignore_for_file: cascade_invocations @@ -75,6 +76,7 @@ class _DesktopReactionsBuilderState extends State { @override Widget build(BuildContext context) { final streamChat = StreamChat.of(context); + final currentUser = streamChat.currentUser!; final reactionIcons = StreamChatConfiguration.of(context).reactionIcons; final streamChatTheme = StreamChatTheme.of(context); @@ -83,13 +85,13 @@ class _DesktopReactionsBuilderState extends State { if (widget.shouldShowReactions) { widget.message.latestReactions?.forEach((element) { if (!reactionsMap.containsKey(element.type) || - element.user!.id == streamChat.currentUser?.id) { + element.user!.id == currentUser.id) { reactionsMap[element.type] = element; } }); reactionsList = reactionsMap.values.toList() - ..sort((a, b) => a.user!.id == streamChat.currentUser?.id ? 1 : -1); + ..sort((a, b) => a.user!.id == currentUser.id ? 1 : -1); } return PortalTarget( @@ -114,44 +116,10 @@ class _DesktopReactionsBuilderState extends State { maxWidth: 336, maxHeight: 342, ), - child: Card( - color: streamChatTheme.colorTheme.barsBg, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.all(16), - child: Text( - '''${widget.message.latestReactions!.length} ${context.translations.messageReactionsLabel}''', - style: streamChatTheme.textTheme.headlineBold, - ), - ), - Flexible( - child: SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Wrap( - spacing: 16, - runSpacing: 16, - children: [ - ...widget.message.latestReactions!.map((reaction) { - final reactionIcon = reactionIcons.firstWhereOrNull( - (r) => r.type == reaction.type, - ); - return _StackedReaction( - reaction: reaction, - streamChatTheme: streamChatTheme, - reactionIcon: reactionIcon, - ); - }).toList(), - ], - ), - ), - ), - ], - ), + child: ReactionsCard( + currentUser: currentUser, + message: widget.message, + messageTheme: widget.messageTheme, ), ), ), @@ -164,7 +132,10 @@ class _DesktopReactionsBuilderState extends State { setState(() => _showReactionsPopup = !_showReactionsPopup); }, child: Padding( - padding: const EdgeInsets.symmetric(vertical: 2), + padding: EdgeInsets.symmetric( + vertical: 2, + horizontal: widget.reverse ? 0 : 4, + ), child: Wrap( spacing: 4, runSpacing: 4, @@ -175,6 +146,7 @@ class _DesktopReactionsBuilderState extends State { ); return _BottomReaction( + currentUser: currentUser, reaction: reaction, message: widget.message, borderSide: widget.borderSide, @@ -193,6 +165,7 @@ class _DesktopReactionsBuilderState extends State { class _BottomReaction extends StatelessWidget { const _BottomReaction({ + required this.currentUser, required this.reaction, required this.message, required this.borderSide, @@ -201,6 +174,7 @@ class _BottomReaction extends StatelessWidget { required this.streamChatTheme, }); + final User currentUser; final Reaction reaction; final Message message; final BorderSide? borderSide; @@ -210,7 +184,7 @@ class _BottomReaction extends StatelessWidget { @override Widget build(BuildContext context) { - final userId = StreamChat.of(context).currentUser?.id; + final userId = currentUser.id; final backgroundColor = messageTheme?.reactionsBackgroundColor; @@ -264,8 +238,7 @@ class _BottomReaction extends StatelessWidget { size: 14, color: reaction.user?.id == userId ? streamChatTheme.colorTheme.accentPrimary - : streamChatTheme.colorTheme.textHighEmphasis - .withOpacity(0.5), + : streamChatTheme.colorTheme.textLowEmphasis, ), ), const SizedBox(width: 4), @@ -290,87 +263,3 @@ class _BottomReaction extends StatelessWidget { properties.add(DiagnosticsProperty('message', message)); } } - -class _StackedReaction extends StatelessWidget { - const _StackedReaction({ - required this.reaction, - required this.streamChatTheme, - required this.reactionIcon, - }); - - final Reaction reaction; - final StreamChatThemeData streamChatTheme; - final StreamReactionIcon? reactionIcon; - - @override - Widget build(BuildContext context) { - final userId = StreamChat.of(context).currentUser?.id; - return SizedBox( - width: 80, - child: Column( - children: [ - Stack( - children: [ - StreamUserAvatar( - user: reaction.user!, - constraints: const BoxConstraints.tightFor( - height: 64, - width: 64, - ), - borderRadius: BorderRadius.circular(32), - ), - Positioned( - bottom: 0, - right: 0, - child: DecoratedBox( - decoration: BoxDecoration( - color: streamChatTheme.colorTheme.inputBg, - border: Border.all( - color: streamChatTheme.colorTheme.barsBg, - width: 2, - ), - shape: BoxShape.circle, - ), - child: Padding( - padding: const EdgeInsets.all(8), - child: reactionIcon?.builder( - context, - reaction.userId == userId, - 16, - ) ?? - Icon( - Icons.help_outline_rounded, - size: 16, - color: reaction.user?.id == userId - ? streamChatTheme.colorTheme.accentPrimary - : streamChatTheme.colorTheme.textHighEmphasis - .withOpacity(0.5), - ), - ), - ), - ), - ], - ), - Text( - userId == reaction.user!.name ? 'You' : reaction.user!.name, - textAlign: TextAlign.center, - ), - ], - ), - ); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add( - DiagnosticsProperty('reaction', reaction), - ); - properties.add( - DiagnosticsProperty( - 'reactionIcon', - reactionIcon, - ), - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/reactions/message_reactions_modal.dart b/packages/stream_chat_flutter/lib/src/message_widget/reactions/message_reactions_modal.dart index 04c749cc4..cd66d08de 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/reactions/message_reactions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/reactions/message_reactions_modal.dart @@ -1,8 +1,8 @@ import 'dart:ui'; import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/message_widget/reactions/reaction_bubble.dart'; import 'package:stream_chat_flutter/src/message_widget/reactions/reactions_align.dart'; +import 'package:stream_chat_flutter/src/message_widget/reactions/reactions_card.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// {@template streamMessageReactionsModal} @@ -83,9 +83,10 @@ class StreamMessageReactionsModal extends StatelessWidget { ), if (message.latestReactions?.isNotEmpty == true) ...[ const SizedBox(height: 8), - _buildReactionCard( - context, - user, + ReactionsCard( + currentUser: user!, + message: message, + messageTheme: messageTheme, ), ], ], @@ -126,110 +127,4 @@ class StreamMessageReactionsModal extends StatelessWidget { ), ); } - - Widget _buildReactionCard(BuildContext context, User? user) { - final chatThemeData = StreamChatTheme.of(context); - return Card( - color: chatThemeData.colorTheme.barsBg, - clipBehavior: Clip.hardEdge, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - margin: EdgeInsets.zero, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - context.translations.messageReactionsLabel, - style: chatThemeData.textTheme.headlineBold, - ), - const SizedBox(height: 16), - Flexible( - child: SingleChildScrollView( - child: Wrap( - spacing: 16, - runSpacing: 16, - children: message.latestReactions! - .map((e) => _buildReaction( - e, - user!, - context, - )) - .toList(), - ), - ), - ), - ], - ), - ), - ); - } - - Widget _buildReaction( - Reaction reaction, - User currentUser, - BuildContext context, - ) { - final isCurrentUser = reaction.user?.id == currentUser.id; - final chatThemeData = StreamChatTheme.of(context); - final reverse = !isCurrentUser; - return ConstrainedBox( - constraints: BoxConstraints.loose( - const Size(64, 100), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Stack( - clipBehavior: Clip.none, - children: [ - StreamUserAvatar( - onTap: onUserAvatarTap, - user: reaction.user!, - constraints: const BoxConstraints.tightFor( - height: 64, - width: 64, - ), - onlineIndicatorConstraints: const BoxConstraints.tightFor( - height: 12, - width: 12, - ), - borderRadius: BorderRadius.circular(32), - ), - Positioned( - bottom: 6, - left: !reverse ? -3 : null, - right: reverse ? -3 : null, - child: Align( - alignment: - reverse ? Alignment.centerRight : Alignment.centerLeft, - child: StreamReactionBubble( - reactions: [reaction], - reverse: !reverse, - flipTail: !reverse, - borderColor: - messageTheme.reactionsBorderColor ?? Colors.transparent, - backgroundColor: messageTheme.reactionsBackgroundColor ?? - Colors.transparent, - maskColor: chatThemeData.colorTheme.barsBg, - tailCirclesSpacing: 1, - ), - ), - ), - ], - ), - const SizedBox(height: 8), - Text( - reaction.user!.name.split(' ')[0], - style: chatThemeData.textTheme.footnoteBold, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - ], - ), - ); - } } diff --git a/packages/stream_chat_flutter/lib/src/message_widget/reactions/reaction_bubble.dart b/packages/stream_chat_flutter/lib/src/message_widget/reactions/reaction_bubble.dart index c3c3c7627..82e5b34d0 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/reactions/reaction_bubble.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/reactions/reaction_bubble.dart @@ -124,24 +124,22 @@ class StreamReactionBubble extends StatelessWidget { final chatThemeData = StreamChatTheme.of(context); final userId = StreamChat.of(context).currentUser?.id; return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 4, - ), + padding: const EdgeInsets.symmetric(horizontal: 4), child: reactionIcon != null ? ConstrainedBox( - constraints: BoxConstraints.tight(const Size.square(16)), + constraints: BoxConstraints.tight(const Size.square(14)), child: reactionIcon.builder( context, - !highlightOwnReactions || reaction.user?.id == userId, + highlightOwnReactions && reaction.user?.id == userId, 16, ), ) : Icon( Icons.help_outline_rounded, - size: 16, - color: (!highlightOwnReactions || reaction.user?.id == userId) + size: 14, + color: (highlightOwnReactions && reaction.user?.id == userId) ? chatThemeData.colorTheme.accentPrimary - : chatThemeData.colorTheme.textHighEmphasis.withOpacity(0.5), + : chatThemeData.colorTheme.textLowEmphasis, ), ); } diff --git a/packages/stream_chat_flutter/lib/src/message_widget/reactions/reactions_card.dart b/packages/stream_chat_flutter/lib/src/message_widget/reactions/reactions_card.dart new file mode 100644 index 000000000..727b44aff --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_widget/reactions/reactions_card.dart @@ -0,0 +1,142 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/avatars/user_avatar.dart'; +import 'package:stream_chat_flutter/src/message_widget/reactions/reaction_bubble.dart'; +import 'package:stream_chat_flutter/src/theme/message_theme.dart'; +import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// {@template reactionsCard} +/// A card that displays the reactions to a message. +/// +/// Used in [StreamMessageReactionsModal] and [DesktopReactionsBuilder]. +/// {@endtemplate} +class ReactionsCard extends StatelessWidget { + /// {@macro reactionsCard} + const ReactionsCard({ + super.key, + required this.currentUser, + required this.message, + required this.messageTheme, + this.onUserAvatarTap, + }); + + /// Current logged in user. + final User currentUser; + + /// Message to display reactions of. + final Message message; + + /// [StreamMessageThemeData] to apply to [message]. + final StreamMessageThemeData messageTheme; + + /// {@macro onUserAvatarTap} + final void Function(User)? onUserAvatarTap; + + @override + Widget build(BuildContext context) { + final chatThemeData = StreamChatTheme.of(context); + return Card( + color: chatThemeData.colorTheme.barsBg, + clipBehavior: Clip.hardEdge, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + context.translations.messageReactionsLabel, + style: chatThemeData.textTheme.headlineBold, + ), + const SizedBox(height: 16), + Flexible( + child: SingleChildScrollView( + child: Wrap( + spacing: 16, + runSpacing: 16, + children: message.latestReactions! + .map((e) => _buildReaction( + e, + currentUser, + context, + )) + .toList(), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildReaction( + Reaction reaction, + User currentUser, + BuildContext context, + ) { + final isCurrentUser = reaction.user?.id == currentUser.id; + final chatThemeData = StreamChatTheme.of(context); + final reverse = !isCurrentUser; + return ConstrainedBox( + constraints: BoxConstraints.loose( + const Size(64, 100), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Stack( + clipBehavior: Clip.none, + children: [ + StreamUserAvatar( + onTap: onUserAvatarTap, + user: reaction.user!, + constraints: const BoxConstraints.tightFor( + height: 64, + width: 64, + ), + onlineIndicatorConstraints: const BoxConstraints.tightFor( + height: 12, + width: 12, + ), + borderRadius: BorderRadius.circular(32), + ), + Positioned( + bottom: 6, + left: !reverse ? -3 : null, + right: reverse ? -3 : null, + child: Align( + alignment: + reverse ? Alignment.centerRight : Alignment.centerLeft, + child: StreamReactionBubble( + reactions: [reaction], + reverse: !reverse, + flipTail: !reverse, + borderColor: + messageTheme.reactionsBorderColor ?? Colors.transparent, + backgroundColor: messageTheme.reactionsBackgroundColor ?? + Colors.transparent, + maskColor: chatThemeData.colorTheme.barsBg, + tailCirclesSpacing: 1, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + reaction.user!.name.split(' ')[0], + style: chatThemeData.textTheme.footnoteBold, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ], + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/misc/reaction_icon.dart b/packages/stream_chat_flutter/lib/src/misc/reaction_icon.dart index ebccc153b..c1f6c144d 100644 --- a/packages/stream_chat_flutter/lib/src/misc/reaction_icon.dart +++ b/packages/stream_chat_flutter/lib/src/misc/reaction_icon.dart @@ -1,5 +1,14 @@ import 'package:flutter/material.dart'; +/// {@template reactionIconBuilder} +/// Signature for a function that builds a reaction icon. +/// {@endtemplate} +typedef ReactionIconBuilder = Widget Function( + BuildContext context, + bool isHighlighted, + double iconSize, +); + /// {@template streamReactionIcon} /// Reaction icon data /// {@endtemplate} @@ -13,10 +22,6 @@ class StreamReactionIcon { /// Type of reaction final String type; - /// Asset to display for reaction - final Widget Function( - BuildContext, - bool highlighted, - double size, - ) builder; + /// {@macro reactionIconBuilder} + final ReactionIconBuilder builder; } diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_configuration.dart b/packages/stream_chat_flutter/lib/src/stream_chat_configuration.dart index 76e4120f4..379c1c140 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_configuration.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_configuration.dart @@ -165,7 +165,7 @@ class StreamChatConfigurationData { return StreamSvgIcon.loveReaction( color: highlighted ? theme.colorTheme.accentPrimary - : theme.primaryIconTheme.color!.withOpacity(0.5), + : theme.primaryIconTheme.color, size: size, ); }, @@ -177,7 +177,7 @@ class StreamChatConfigurationData { return StreamSvgIcon.thumbsUpReaction( color: highlighted ? theme.colorTheme.accentPrimary - : theme.primaryIconTheme.color!.withOpacity(0.5), + : theme.primaryIconTheme.color, size: size, ); }, @@ -189,7 +189,7 @@ class StreamChatConfigurationData { return StreamSvgIcon.thumbsDownReaction( color: highlighted ? theme.colorTheme.accentPrimary - : theme.primaryIconTheme.color!.withOpacity(0.5), + : theme.primaryIconTheme.color, size: size, ); }, @@ -201,7 +201,7 @@ class StreamChatConfigurationData { return StreamSvgIcon.lolReaction( color: highlighted ? theme.colorTheme.accentPrimary - : theme.primaryIconTheme.color!.withOpacity(0.5), + : theme.primaryIconTheme.color, size: size, ); }, @@ -213,7 +213,7 @@ class StreamChatConfigurationData { return StreamSvgIcon.wutReaction( color: highlighted ? theme.colorTheme.accentPrimary - : theme.primaryIconTheme.color!.withOpacity(0.5), + : theme.primaryIconTheme.color, size: size, ); }, diff --git a/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.dart index 02ac6f4c3..04efaea6d 100644 --- a/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.dart @@ -126,8 +126,7 @@ class StreamChatThemeData { StreamTextTheme textTheme, ) { final accentColor = colorTheme.accentPrimary; - final iconTheme = - IconThemeData(color: colorTheme.textHighEmphasis.withOpacity(0.5)); + final iconTheme = IconThemeData(color: colorTheme.textLowEmphasis); final channelHeaderTheme = StreamChannelHeaderThemeData( avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), From 4b9bad168421ebf0c0b406d89f95889d80f82cd6 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 8 May 2023 20:46:15 +0530 Subject: [PATCH 32/51] chore: update golden files. Signed-off-by: xsahil03x --- .../test/src/goldens/command_button_0.png | Bin 23274 -> 23312 bytes .../src/goldens/deleted_message_custom.png | Bin 2936 -> 3090 bytes .../test/src/goldens/deleted_message_dark.png | Bin 2785 -> 2890 bytes .../src/goldens/deleted_message_light.png | Bin 2750 -> 2841 bytes .../test/src/goldens/gallery_header_0.png | Bin 36757 -> 36343 bytes .../test/src/goldens/reaction_bubble_2.png | Bin 2920 -> 2783 bytes .../src/goldens/reaction_bubble_3_dark.png | Bin 1963 -> 1793 bytes .../src/goldens/reaction_bubble_3_light.png | Bin 2123 -> 2042 bytes .../src/goldens/reaction_bubble_like_dark.png | Bin 1517 -> 1406 bytes .../goldens/reaction_bubble_like_light.png | Bin 1296 -> 1230 bytes 10 files changed, 0 insertions(+), 0 deletions(-) diff --git a/packages/stream_chat_flutter/test/src/goldens/command_button_0.png b/packages/stream_chat_flutter/test/src/goldens/command_button_0.png index 72719d491cc4415e63ad752d5d917c560e7c2b14..c294b279eae99d3f3a92964295b048de7f9701f4 100644 GIT binary patch delta 1487 zcmY*ZdrVVT7;jm(iM)|lkkM>T5+fiEg^foQ<}?itUyOnqLLth8VY|wsLfczq5f$0Q zih(T_CJ0#yw6#2pP+CF=QwJSGMh%n#9Uuc*p}cBK***8%#q9pk({q2{`F-E_`+n#C z(@MPEMwHa~)58BQpYOB@;$-$kjh`C*rDA)|OG%AC(Ux_c>e&=k?0q7k!rjfy&7HP; zcgXdV_C9Vg`$eLF7M~~g|J2|4GQYm2;^B*q@v3&6d}P^JQx&e=%;hOX!Lsgy7m3J& zy%|mf@MI=aio!Tsqp6IQfrXjH$ztu+Qa~IDKw=ru4qf=vGn7KfQn*oWX3$*$@M$#x z1D+-aIF?Lz8_UM4G@X$PV^56t{A|DpnXLrGRr-vI!OEs>gZq2B+R2rty0Wu~baOij z@TE9#ce-6`-?KD$)S%|Qivr3dQW7{B`)sbZWas^M7`-2-y@lqauWCxm#&MdrfIu7) z#xT2svl%Ue17C2uIeL@-x(?jiDyZ`+#oV6_b=X7q8$*hRrjIH~onQ ztfo9Tn#Z|cX?S_YHOZyF4QBYmD3_u zkjG4Os&5rgNtr+(7*>A{qF!5*d?Qi~6I09$tCONgSMFrBFe_8Gn0BW@*@rKkTL3G8 zt!V6!W?g1>N0PZ8bzwBQ)~n7af_gYbvM0N%9ia+9B%(Co1{+#Z3~XMo8oBtuNN`|+ z7LI9Zf!wTjG^+F|`XF$K=?Jl*6;*H5kFx~^hn`_gjPH8@8}Ne$xPw|{MA+Y~!EHo! zfhPRydGTx}rP{CvU4$s>aOVqkwIjZ0yjMQOn)^K+a7}P*{K4v%xw_n&t(qw^?}`P@ z5Eyt87Mg_2sn&GnK=%nghRIphP~}es4fz6gOWDDAE3!9c?OCo6UeVyG11J+hU{dgA zSe%t&F^#g7UKa&&Or+z<&zY8cMXGQz_mkgw7J*9xP?#Tz->HDo9vXQTEzA)s%oBgT zN7Q@h8hyjCCYmSz5a9s$I&sHSlYK3DUZ|cR)dF%7U=z_WSN-$`yHYC;wWk&I39(O4 zAl}g_v{SnErCizv;`yz(Xtz)wRsN@?GCU)n#d_pjv<<{~UE3lh${UZKIb9CC(e*yE`Hn0Ey literal 23274 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 D5*uQE diff --git a/packages/stream_chat_flutter/test/src/goldens/deleted_message_custom.png b/packages/stream_chat_flutter/test/src/goldens/deleted_message_custom.png index c464bc01de48fcef06c538bd284ed284370f55aa..36f80c62bfb6aaa4ea2858f1180d58d2ba9d474d 100644 GIT binary patch delta 2556 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 A>i_@% delta 2243 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 diff --git a/packages/stream_chat_flutter/test/src/goldens/gallery_header_0.png b/packages/stream_chat_flutter/test/src/goldens/gallery_header_0.png index a0b780e9357d01474a9e14a7aedd6e7a214abdab..3c8e020f8bb562d587a8eb66c8478eaf3348658d 100644 GIT binary patch literal 36343 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 diff --git a/packages/stream_chat_flutter/test/src/goldens/reaction_bubble_2.png b/packages/stream_chat_flutter/test/src/goldens/reaction_bubble_2.png index 05cfdbbef8eabea8bfeed0bdd0181993718b1149..9a4aac364b25ac1ea29d8ed19d774b07f4014ff7 100644 GIT binary patch delta 2252 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 diff --git a/packages/stream_chat_flutter/test/src/goldens/reaction_bubble_3_light.png b/packages/stream_chat_flutter/test/src/goldens/reaction_bubble_3_light.png index 8caefa4f674cb29e2070c5e2743698fa0de5b275..d67e928d2eebe3a05e8b2f19ecad195dc7610cb6 100644 GIT binary patch delta 1695 zcmV;Q24MNi5c&_0L4Tx4L_t(|obBDuYZHGS$MM%pGLuQfCVszO+yxualM9_-U0C>n z6?*JKp*`)z;9mB)r#-ep@lWt5N(-L8D(Yq7`=u;ig#Ci0$w66AzAd=)Vms4sZGOy5 z=Cg;kS*uO$duGx!>FWV=vF-G2csetgd}1^J3PeYw_j&;8FOyLL6_L=Be@MEUJHK(` z1|}ybQ7V-%JUonIvDnRD*E5w$1+MF2d3hNN3k#@JD!m@5YczD>gU)Z=x`o-<*?|q@ zTUJ(9FgG`cg@uJ)kCM1VgRyNJ4<0;#ZQB5VFbom+K78Lts1#a3kkqd_QBvtNbWMX{ z7)ToibbaScU#V0uJw1(;f0dOUPnevdZP$(1-h9R1b20YJ0#c`!{A8OwsM>u_Bc)6>&<`SNA2$4o>b!HkWKVR3O001)^- zYPA|NmW6CK3r*AF?!K=mrBJWe(QGu3%jJ+Z3;@9R`1n9X6FV5&f3~r>xQIfb0N?Xa ztJN@+&m*1g>h<)A7X$%*+uTGhmxE!Na9tPIuV3%`(^KqVUcY_~+qT=Wocr#(Q+HJ9 zL@5PlV*_W-oI%v3y3;Gz{hQsMTr+ z!w|M@qg*ccdaPJte;r}oy?cjZv50!D20fKRE|(MiyyG~?X0xy?E9wXb`mhyZ$kZ^D zNjBc7^VVuLgsm2`xg1tjR&eFYl|<({PLYo=V`F0|7K;eO5T55Do6U;%asU2(JbwH* z>IfbE<#`?d_*XOW{YITPo6W-WJcMD0VzJo1?a+@R2QxW2e+gjcSy9Ha#HXS+Z{EOl zT{Ievs3RQcr(Xm7cT3^7X0rDqFs*h z@oQJlNB?(`)Vx6uAhml`saC6z=jpZh!K6|M1i>8U(xpp@?B`2=`dxQk^pwmShGF}e zr0cp}nddk}4rY0I8314!2K?Qt|D)*iEW@%ajE|2;9S3L4gP#?hH8uSGk7nfkCN;0u zY{J}qe>S=I*!82x!90Kd8~~6u3^W^!_A`KQnV6VBp-|ZOXO?AQVqzldC;)(&A58rI z?EYxZni^()h#m`&%)Ck|c%IjO<$8X8KGsaV6zipd_wV0hcz8I{O9PJMv=7Zmo{Bzf zh4`?w^H)mpR1|04&tDo?U0ucH%a;?Is~1$%f2~|Ds~`xJ=Xq*leO>v!uV_Et_tnPw zy7D|v1wo+7<#Oc5`y_0!mp3deEn#G21cC2kdwZM98$P4HSl&>rR&njxwO)@EOYHkF zw{PFZ=H@2ShJjoz2WMlWeN*o=3xWX7#zuS9-R9;drlzKPJyvX8TcT-M7OYdcM6*$^ zf1~lm63x-k(SdmTNurCupFVwxd_KQ#5xAMjz%)(hy56_9!@ng6g7z{Gui4zU2z>9z zG6?H=fiAqw@c*}&c)kxc;BPb0G!5n#Z!@V@tC*Ua>ic!PpXMG{u=KA`6ODA0kC1%gw8rD*&ghHXvKJpBjYPH&atL*&z{6Ji* zbkfyeXz=iVZy=G-kbmSz!Q8xg^Q%vXr_<}u54J9qO3HB@rhG|Y_*_i8VtjL@B2ul pQpo0Vc=6%|rlzL8+yxD&{{bh`_)p{z=zstK002ovPDHLkV1hTJaL@n% delta 1815 zcmV+y2k7|v56cjcL4Wm0L_t(|obBDsPaF9i$MKIJ7!w}Ctl)sURYa9Yn^FmUStU{r zph{assuV}rKLC~7$*ISZD$1<_2~JTY=zY}#;kY>zE>U({MTEnyBP9wswBms6-`Eov z&y44{2gfe3i{WdJ8SH#s(wxBKpT}>;<9T3W05pisC?53yr~$K+5dl||PXQT|PXQT| zPXQT!2!kP!WIJYh`}S>2O-&)4PQx@!OioU=<7qCOSFc_{DTS@AEo^RXVtsu*>LGFp^>Y)Td0MB*d z`#vf`fS^)o^(i_+Jdr>m9!D~nM2}%09 z@B6y!INDN5=W{u2+qQOHS9_kPKNX(mY1ehNZQDAZ%V|p~U3Q%M@>y6|XlYqrkg53x zudJ-p7e=XA)cIUaI|m0*zi$_Yb8w*ZxtuN)i}mHSva%BO8y-zCi;Iie_kHboo-P!B z3c6@nItYTO-=hmB2m)QSENv;JJ3 zFv9i`tzj~m3{2BR5Co`HDow?b&1SK?yNeeuUWgxi7T><}@nbH)KeoNL%)ixt@uzxw zQK?iA1OZIbL?)97d4%mH3TA3*3IO1_E{s%jf4$jk7696|H~85*9{}+AV5}|k4`cjB zDh1!G23=dGokA2$I-Ldpc)kzAFvL&hI8J?mkB*LtAAg!+S>xGGb+7XHNFwz4hc$k~ zFyMX+rnXEwh3Mepl`B^ORCoPwPoj-p->}cM{Q3eEzU zLf`$by+^~t9>4GVNPJwYY;SLeI`4LCE|^3Dl^{6t<7%Jtja*_s!y<1yb0Dzuk60Y0$BjB*!Q})-S)9IGJbzDw0{tEr|7fFnd zH2;wl_V``bh0)Uk;Q!0CQ;3%0=H?~~o5eR}n=kxjcS$OmF^O2vfAf1{zz2WWKw-_24!omJNwAL8Bd|5J!PuAhn z8!XGh)vH&#d!rYBsBYEI&dve=F7@@H5(KbqJM@8d8{4)KXpKvKeE@*j+1ZYZ1ppEN z{w96U>({SgnkH`CxBvy{T|vI3Kt34xa?Ry1E+qc1VkVQ)@G*oeW;v5LQa*d_K?1 zO!9fHj$LNbi5t1cr%*0r%VaWGU0tmod97W@=J`If)~I|uXxZ%&$s}U27G(=^*X7U~cU@9~#T zr*)xF(1k)lKX~v!o2D80eFr&3IyL@-a^5WtM?rsIl0`wAcUICm23+pQFHAd# z3?LE8JluKqNckDKp&Ru+qrg|B{GB4RugkmYtotDvKz$TxatHL`kvs$`EZ1( zcj(p1KJ#=0eT>GkfAsd1PaZ1ya6k|~__*r8&J{Tgu6*4G@^t&-Jjx3_^yxgEL&%PY zBNY&Aq6XsfB+>?(ujkuby;I`*4U{fhZ>!{(qTZ2*~{2y3+Y@4$w-qG1L?U3u`H#al@>Yq; zy(aYLC#^XnGq@1_6nGF#`BzWpjvwKky!1yW>iBU(LcWVD@miQ47}M0U%#mlVsyqgp z8U5;){F`2Mi_U8C65^kLvW+pYRCT{d8%|@HXWB!LmWUoq^lS(3N}Ej7%PyfX`r#H^ z?9_!#s4xfY(a!C1Hk-ZdVVx#*;QPiIhtLzrhbk?fOANTr-Bkr;(6yRD?h1d78GsCM zc*-EDxwu&Tn%3X=pEi<-%*}|{Mu*6;Fz;7e@WBep#>LP_uv5aI%F=g*&9-eY^zPM^ ztxFkBPu-~2pw*Ujj?}>HMrN#r*SxuwoJ3*X?YL4NSAK_wtRK<3;5ib2K>0~90Xj1* zEUoqP1fZ5SV0K`vm$o&MS#KmLj80j)dL}F+U;kOP-HBAHK89Ub=^a9JyAd-W_XeNT zvPpbb2y!HZG|01y_#zw6uP1Whu21OEpYF4Fe<7BcIC4|hRQTyW&#Aw^on6jCpWoN2 z9OGbxYqSfYIU9!nlRcX~{y3p8vllQFNfH`KHHwQr?-dUE^ri2RlgN@=R@1(c(7=Ln<_@@6hKI~fAb&w9zSu1lq)gr|34$rc0ljoNdHQEV$IMEo3@9g&{;%jhG8UF)~ C*noim delta 1378 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 delta 1102 zcmV-U1hMqaevL{aokz`A>uuf6xR6hkfKt%$0sP~m>CiFbKPEsg^EX$ZqCMb#mX_}(Ze{Amc{LETRCKDKAaFnJ1>LZ0t$Z}JY zWf`h0`;fPI?~#p0@ZKYBwQAFRQ4M>SYetr3h_y!N`1r#zLR_8WV<@FC8IRw^ z&kCIo;H8<0wMM(s`Es10c16?^DzxkCYuw)6?iF{Mrm)toq{Rq@kWfI?d~UJNXsX}uV=x%t{{DWiG#bq&%CbaN zRV!(-K_Mhi)Eht|&g0xBm+YA>nN;oI{c%JMXvFE`)Tu z-M!OEk_7o&2x|?YS~{u95?bwkM{aIze-`~gx7&ra7K6b60C0A8w(+?z7h#5CVyNa4 zhL4Z<=f{t&_uk#z;qLBkV|cIE!`0Q*#_)fC{=~`YDF7_`6DBB{tbOVC`&(rIAL~;X zp%S8$TCCu%^|p?>@;pak%u1SUPzVW_Btbr#Z4BS`_5;Ah#l_a}d^STo7s3ifgBqF= zr4*Fbm|44aw%+UY0KCr9hr=O&m!a?F<>l7eF|!s*>&1wUwT4g(dx6)b@zObr$77tF zo*t~d_ugYT9HNz`&~Z%tU4*8|e+mVp;~2&mWTVl+;?71Rm^fbfq{|A85a6|-j?xrm zRUyl=FUOgUMyR}B`IO5Hosdvq?RFc^IgFm4KUIj{dyJl+QC1aNt=7BvS)u7emU*|y z!MT@B4tuTy&#c8{I)#a2+2n92-fjb$T8pZxkR-|WZ9sVrr4){28_?m|K(tY=m zWOW=PiXtS&AU0+(v<2!V10WX?_{?sRx`_x-ObmAs3O7jEBqs={qliE)garPC0^bGI zuvZWvbd&xAHj^C$4}a_>g#B#ypTy3+z02n?mj5>;d=EQo$nseWtu@S>^B2Nl`H+x+ zi1|LYmS|a)aPw12>(E*sJVSpw8-*yPV2r`j{BH^Cc>MM2TQc=GvsTFRs47iUJU>0X zJ0vsCG90Dp;>Y*GVOV>{F!T-MEJLIey!WWe5>XVv7=xlHpdYoyQJU8FEFUZX0WdZX UDv4_?R{#J207*qoM6N<$f?h= Date: Tue, 9 May 2023 17:01:02 +0530 Subject: [PATCH 33/51] fix(llc, ui): fix removing message not removing quoted reference. Signed-off-by: xsahil03x --- .../stream_chat/lib/src/client/channel.dart | 52 ++++++++++--- .../message_input/quoted_message_widget.dart | 77 +++++++++++-------- .../lib/src/message_widget/bottom_row.dart | 2 +- 3 files changed, 89 insertions(+), 42 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 8152753de..3b249fcaa 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -746,6 +746,7 @@ class Channel { state!.deleteMessage( message.copyWith( type: 'deleted', + deletedAt: message.deletedAt ?? DateTime.now(), status: MessageSendingStatus.sent, ), hardDelete: hardDelete, @@ -1928,11 +1929,9 @@ class ChannelClientState { void _listenMessageDeleted() { _subscriptions.add(_channel.on(EventType.messageDeleted).listen((event) { final message = event.message!; - if (event.hardDelete == true) { - removeMessage(message); - } else { - updateMessage(message); - } + final hardDelete = event.hardDelete ?? false; + + deleteMessage(message, hardDelete: hardDelete); })); } @@ -1957,18 +1956,35 @@ class ChannelClientState { /// Updates the [message] in the state if it exists. Adds it otherwise. void updateMessage(Message message) { + // Regular messages, which are shown in channel. if (message.parentId == null || message.showInChannel == true) { - final newMessages = [...messages]; + var newMessages = [...messages]; final oldIndex = newMessages.indexWhere((m) => m.id == message.id); if (oldIndex != -1) { - Message? m; + var updatedMessage = message; + // Add quoted message to the message if it is not present. if (message.quotedMessageId != null && message.quotedMessage == null) { final oldMessage = newMessages[oldIndex]; - m = message.copyWith( + updatedMessage = updatedMessage.copyWith( quotedMessage: oldMessage.quotedMessage, ); } - newMessages[oldIndex] = m ?? message; + newMessages[oldIndex] = updatedMessage; + + // Update quoted message reference for every message if available. + newMessages = [...newMessages].map((it) { + // Early return if the message doesn't have a quoted message. + if (it.quotedMessageId != message.id) return it; + + // Setting it to null will remove the quoted message from the message + // So, we are setting the same message but with the deleted state. + return it.copyWith( + quotedMessage: updatedMessage.copyWith( + type: 'deleted', + deletedAt: updatedMessage.deletedAt ?? DateTime.now(), + ), + ); + }).toList(); } else { newMessages.add(message); } @@ -1997,6 +2013,7 @@ class ChannelClientState { ); } + // Thread messages, which are shown in thread page. if (message.parentId != null) { updateThreadInfo(message.parentId!, [message]); } @@ -2026,9 +2043,22 @@ class ChannelClientState { } // Remove regular message, thread message shown in channel - final allMessages = [...messages]; + var updatedMessages = [...messages]..removeWhere((e) => e.id == message.id); + + // Remove quoted message reference from every message if available. + updatedMessages = [...updatedMessages].map((it) { + // Early return if the message doesn't have a quoted message. + if (it.quotedMessageId != message.id) return it; + + // Setting it to null will remove the quoted message from the message. + return it.copyWith( + quotedMessage: null, + quotedMessageId: null, + ); + }).toList(); + _channelState = _channelState.copyWith( - messages: allMessages..removeWhere((e) => e.id == message.id), + messages: updatedMessages, ); } diff --git a/packages/stream_chat_flutter/lib/src/message_input/quoted_message_widget.dart b/packages/stream_chat_flutter/lib/src/message_input/quoted_message_widget.dart index 08152a587..795154caf 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/quoted_message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/quoted_message_widget.dart @@ -134,6 +134,8 @@ class _QuotedMessage extends StatelessWidget { bool get _isGiphy => message.attachments.any((element) => element.type == 'giphy'); + bool get _isDeleted => message.isDeleted || message.deletedAt != null; + @override Widget build(BuildContext context) { final isOnlyEmoji = message.text!.isOnlyEmoji; @@ -144,39 +146,54 @@ class _QuotedMessage extends StatelessWidget { msg = msg.copyWith(text: '${msg.text!.substring(0, textLimit - 3)}...'); } - final children = [ - if (composing) - PlatformWidgetBuilder( - web: (context, child) => child, - desktop: (context, child) => child, - child: ClearInputItemButton( - onTap: onQuotedMessageClear, + List children; + if (_isDeleted) { + // Show deleted message text + children = [ + Text( + context.translations.messageDeletedLabel, + style: messageTheme.messageTextStyle?.copyWith( + fontStyle: FontStyle.italic, + color: messageTheme.createdAtStyle?.color, ), ), - if (_hasAttachments) - _ParseAttachments( - message: message, - messageTheme: messageTheme, - attachmentThumbnailBuilders: attachmentThumbnailBuilders, - ), - if (msg.text!.isNotEmpty && !_isGiphy) - Flexible( - child: StreamMessageText( - message: msg, - messageTheme: isOnlyEmoji && _containsText - ? messageTheme.copyWith( - messageTextStyle: messageTheme.messageTextStyle?.copyWith( - fontSize: 32, - ), - ) - : messageTheme.copyWith( - messageTextStyle: messageTheme.messageTextStyle?.copyWith( - fontSize: 12, + ]; + } else { + // Show quoted message + children = [ + if (composing) + PlatformWidgetBuilder( + web: (context, child) => child, + desktop: (context, child) => child, + child: ClearInputItemButton( + onTap: onQuotedMessageClear, + ), + ), + if (_hasAttachments) + _ParseAttachments( + message: message, + messageTheme: messageTheme, + attachmentThumbnailBuilders: attachmentThumbnailBuilders, + ), + if (msg.text!.isNotEmpty && !_isGiphy) + Flexible( + child: StreamMessageText( + message: msg, + messageTheme: isOnlyEmoji && _containsText + ? messageTheme.copyWith( + messageTextStyle: messageTheme.messageTextStyle?.copyWith( + fontSize: 32, + ), + ) + : messageTheme.copyWith( + messageTextStyle: messageTheme.messageTextStyle?.copyWith( + fontSize: 12, + ), ), - ), + ), ), - ), - ].insertBetween(const SizedBox(width: 8)); + ].insertBetween(const SizedBox(width: 8)); + } return Container( decoration: BoxDecoration( @@ -204,7 +221,7 @@ class _QuotedMessage extends StatelessWidget { } Color? _getBackgroundColor(BuildContext context) { - if (_containsLinkAttachment) { + if (_containsLinkAttachment && !_isDeleted) { return messageTheme.urlAttachmentBackgroundColor; } return messageTheme.messageBackgroundColor; diff --git a/packages/stream_chat_flutter/lib/src/message_widget/bottom_row.dart b/packages/stream_chat_flutter/lib/src/message_widget/bottom_row.dart index 289088afb..04cb4f33b 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/bottom_row.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/bottom_row.dart @@ -151,7 +151,7 @@ class BottomRow extends StatelessWidget { context, message, ) ?? - const Offstage(); + const StreamVisibleFootnote(); } final children = []; From f5f475567f189034a1001f0781f8aaecf04f3553 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 9 May 2023 17:03:02 +0530 Subject: [PATCH 34/51] chore: update CHANGELOG.md Signed-off-by: xsahil03x --- packages/stream_chat/CHANGELOG.md | 2 ++ packages/stream_chat_flutter/CHANGELOG.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index df72ed55d..4177cd157 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -4,6 +4,8 @@ - [[#1355]](https://github.com/GetStream/stream-chat-flutter/issues/1355) Fixed error while hiding channel and clearing message history. +- [[#1525]](https://github.com/GetStream/stream-chat-flutter/issues/1525) Fixed removing message not removing quoted + message reference. โœ… Added diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 778c162d3..f3ea52da9 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -13,6 +13,8 @@ used in message edit widget. - [[#1523]](https://github.com/GetStream/stream-chat-flutter/issues/1523) Fixed `StreamMessageThemeData` not being applied correctly. +- [[#1525]](https://github.com/GetStream/stream-chat-flutter/issues/1525) Fixed removing message not removing quoted + message reference. โœ… Added From b607fd9733d5367a58eee96749ed9c92ab0212f8 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 9 May 2023 17:09:50 +0530 Subject: [PATCH 35/51] chore: update CHANGELOG.md Signed-off-by: xsahil03x --- packages/stream_chat_flutter/CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index f3ea52da9..fc8b653f3 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -13,8 +13,8 @@ used in message edit widget. - [[#1523]](https://github.com/GetStream/stream-chat-flutter/issues/1523) Fixed `StreamMessageThemeData` not being applied correctly. -- [[#1525]](https://github.com/GetStream/stream-chat-flutter/issues/1525) Fixed removing message not removing quoted - message reference. +- [[#1525]](https://github.com/GetStream/stream-chat-flutter/issues/1525) Fixed `StreamQuotedMessageWidget` message for + deleted messages not being shown correctly. โœ… Added From 89b9e957b9cb343852c373f45f655ff7c194664e Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 10 May 2023 17:57:57 +0530 Subject: [PATCH 36/51] fix: visible footnote spacing and visibility. Signed-off-by: xsahil03x --- .../src/message_input/quoted_message_widget.dart | 13 +++---------- .../src/message_list_view/message_list_view.dart | 14 ++++++++++---- .../lib/src/message_widget/bottom_row.dart | 14 +++++++++----- .../lib/src/message_widget/message_widget.dart | 3 +-- .../lib/src/message_widget/quoted_message.dart | 1 - 5 files changed, 23 insertions(+), 22 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input/quoted_message_widget.dart b/packages/stream_chat_flutter/lib/src/message_input/quoted_message_widget.dart index 795154caf..33145852c 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/quoted_message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/quoted_message_widget.dart @@ -22,7 +22,6 @@ class StreamQuotedMessageWidget extends StatelessWidget { this.padding = const EdgeInsets.all(8), this.onTap, this.onQuotedMessageClear, - this.composing = true, }); /// The message @@ -53,9 +52,6 @@ class StreamQuotedMessageWidget extends StatelessWidget { /// Callback for clearing quoted messages. final VoidCallback? onQuotedMessageClear; - /// True if the message is being composed - final bool composing; - @override Widget build(BuildContext context) { final children = [ @@ -63,11 +59,10 @@ class StreamQuotedMessageWidget extends StatelessWidget { child: _QuotedMessage( message: message, textLimit: textLimit, - composing: composing, - onQuotedMessageClear: onQuotedMessageClear, messageTheme: messageTheme, showBorder: showBorder, reverse: reverse, + onQuotedMessageClear: onQuotedMessageClear, attachmentThumbnailBuilders: attachmentThumbnailBuilders, ), ), @@ -104,17 +99,15 @@ class _QuotedMessage extends StatelessWidget { const _QuotedMessage({ required this.message, required this.textLimit, - required this.composing, - required this.onQuotedMessageClear, required this.messageTheme, required this.showBorder, required this.reverse, + this.onQuotedMessageClear, this.attachmentThumbnailBuilders, }); final Message message; final int textLimit; - final bool composing; final VoidCallback? onQuotedMessageClear; final StreamMessageThemeData messageTheme; final bool showBorder; @@ -161,7 +154,7 @@ class _QuotedMessage extends StatelessWidget { } else { // Show quoted message children = [ - if (composing) + if (onQuotedMessageClear != null) PlatformWidgetBuilder( web: (context, child) => child, desktop: (context, child) => child, diff --git a/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart index 5435e95cf..89db51dde 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart @@ -282,9 +282,14 @@ class StreamMessageListView extends StatefulWidget { BuildContext context, List spacingTypes, ) { - if (!spacingTypes.contains(SpacingType.defaultSpacing)) { + if (spacingTypes.contains(SpacingType.otherUser)) { + return const SizedBox(height: 8); + } else if (spacingTypes.contains(SpacingType.thread)) { + return const SizedBox(height: 8); + } else if (spacingTypes.contains(SpacingType.timeDiff)) { return const SizedBox(height: 8); } + return const SizedBox(height: 2); } @@ -644,7 +649,8 @@ class _StreamMessageListViewState extends State { Widget separator; - final isThread = message.replyCount! > 0; + final isPartOfThread = message.replyCount! > 0 || + message.showInChannel == true; if (!Jiffy(message.createdAt.toLocal()).isSame( nextMessage.createdAt.toLocal(), @@ -666,7 +672,7 @@ class _StreamMessageListViewState extends State { final spacingRules = [ if (hasTimeDiff) SpacingType.timeDiff, if (!isNextUserSame) SpacingType.otherUser, - if (isThread) SpacingType.thread, + if (isPartOfThread) SpacingType.thread, if (isDeleted) SpacingType.deleted, ]; @@ -680,7 +686,7 @@ class _StreamMessageListViewState extends State { ); } - if (!isThread && + if (!isPartOfThread && unreadCount > 0 && _oldestUnreadMessage?.id == nextMessage.id) { final unreadMessagesSeparator = diff --git a/packages/stream_chat_flutter/lib/src/message_widget/bottom_row.dart b/packages/stream_chat_flutter/lib/src/message_widget/bottom_row.dart index 04cb4f33b..5d5bbc0d9 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/bottom_row.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/bottom_row.dart @@ -147,11 +147,15 @@ class BottomRow extends StatelessWidget { @override Widget build(BuildContext context) { if (isDeleted) { - return deletedBottomRowBuilder?.call( - context, - message, - ) ?? - const StreamVisibleFootnote(); + final deletedBottomRowBuilder = this.deletedBottomRowBuilder; + if (deletedBottomRowBuilder != null) { + return deletedBottomRowBuilder(context, message); + } + + // Only show visible footnote for ownUser. + final currentUser = streamChat.currentUser; + final isOwnUser = message.user?.id == currentUser?.id; + if (isOwnUser) return const StreamVisibleFootnote(); } final children = []; diff --git a/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart index 267c96d49..a2a3b55fa 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart @@ -794,8 +794,7 @@ class _StreamMessageWidgetState extends State showUsername || showTimeStamp || showInChannel || - showSendingIndicator || - isDeleted; + showSendingIndicator; /// {@template isPinned} /// Whether [StreamMessageWidget.message] is pinned or not. diff --git a/packages/stream_chat_flutter/lib/src/message_widget/quoted_message.dart b/packages/stream_chat_flutter/lib/src/message_widget/quoted_message.dart index 028cc443b..f75bdbe74 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/quoted_message.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/quoted_message.dart @@ -65,7 +65,6 @@ class _QuotedMessageState extends State { top: 8, bottom: widget.hasNonUrlAttachments ? 8 : 0, ), - composing: false, ); } } From bc6283e30c52b9a381e03b68f28f16ffc3da6957 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 10 May 2023 23:57:59 +0530 Subject: [PATCH 37/51] chore: don't use a fallback for deleteBottomRowBuilder Signed-off-by: xsahil03x --- .../lib/src/message_widget/bottom_row.dart | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_widget/bottom_row.dart b/packages/stream_chat_flutter/lib/src/message_widget/bottom_row.dart index 5d5bbc0d9..8d650f5c6 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/bottom_row.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/bottom_row.dart @@ -151,11 +151,6 @@ class BottomRow extends StatelessWidget { if (deletedBottomRowBuilder != null) { return deletedBottomRowBuilder(context, message); } - - // Only show visible footnote for ownUser. - final currentUser = streamChat.currentUser; - final isOwnUser = message.user?.id == currentUser?.id; - if (isOwnUser) return const StreamVisibleFootnote(); } final children = []; From 002438c77163e64189dd3d838f4505a58cf038d2 Mon Sep 17 00:00:00 2001 From: ChrisElliotUK Date: Thu, 11 May 2023 10:22:22 +0100 Subject: [PATCH 38/51] fix message widget --- .../lib/src/message_widget/message_widget.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart index a2a3b55fa..d67365676 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart @@ -1009,7 +1009,7 @@ class _StreamMessageWidgetState extends State title: Text(context.translations.copyMessageLabel), onClick: () { Navigator.of(context, rootNavigator: true).pop(); - Clipboard.setData(ClipboardData(text: widget.message.text)); + Clipboard.setData(ClipboardData(text: widget.message.text ?? '')); }, ), if (shouldShowEditAction) ...[ @@ -1169,7 +1169,7 @@ class _StreamMessageWidgetState extends State : DisplayWidget.show, ), onCopyTap: (message) => - Clipboard.setData(ClipboardData(text: message.text)), + Clipboard.setData(ClipboardData(text: message.text ?? '')), messageTheme: widget.messageTheme, reverse: widget.reverse, showDeleteMessage: shouldShowDeleteAction, From 247197a15d973f856c04e29c1fd3b9315e772659 Mon Sep 17 00:00:00 2001 From: Chris Elliot <102792401+ChrisElliotUK@users.noreply.github.com> Date: Thu, 11 May 2023 10:54:36 +0100 Subject: [PATCH 39/51] Update packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart Co-authored-by: Sahil Kumar --- .../lib/src/message_widget/message_widget.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart index d67365676..3a4ca1b4f 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart @@ -1009,7 +1009,8 @@ class _StreamMessageWidgetState extends State title: Text(context.translations.copyMessageLabel), onClick: () { Navigator.of(context, rootNavigator: true).pop(); - Clipboard.setData(ClipboardData(text: widget.message.text ?? '')); + final text = message.text; + if (text != null) Clipboard.setData(ClipboardData(text: text)); }, ), if (shouldShowEditAction) ...[ From 49c05ad70fed53a72da538bd0464d157a415f277 Mon Sep 17 00:00:00 2001 From: Chris Elliot <102792401+ChrisElliotUK@users.noreply.github.com> Date: Thu, 11 May 2023 10:56:32 +0100 Subject: [PATCH 40/51] Update CHANGELOG.md --- packages/stream_chat_flutter/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index fc8b653f3..c6e131b7c 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -15,6 +15,7 @@ applied correctly. - [[#1525]](https://github.com/GetStream/stream-chat-flutter/issues/1525) Fixed `StreamQuotedMessageWidget` message for deleted messages not being shown correctly. +- [[#1529]](https://github.com/GetStream/stream-chat-flutter/issues/1529) Fixed fix `ClipboardData` requires non-nullable string as text on Flutter 2.10. โœ… Added From 0c2edac3deb46aa62d3520279356ee0ad03ef5fe Mon Sep 17 00:00:00 2001 From: Chris Elliot <102792401+ChrisElliotUK@users.noreply.github.com> Date: Thu, 11 May 2023 10:59:20 +0100 Subject: [PATCH 41/51] Update message_widget.dart --- .../lib/src/message_widget/message_widget.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart index 3a4ca1b4f..f77fcedb2 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart @@ -1169,8 +1169,10 @@ class _StreamMessageWidgetState extends State ? DisplayWidget.gone : DisplayWidget.show, ), - onCopyTap: (message) => - Clipboard.setData(ClipboardData(text: message.text ?? '')), + onCopyTap: (message) { + final text = message.text; + if (text != null) Clipboard.setData(ClipboardData(text: text)); + }, messageTheme: widget.messageTheme, reverse: widget.reverse, showDeleteMessage: shouldShowDeleteAction, From f8c2bee3aff30095b99ed5a3cdc0799ebafa8639 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 11 May 2023 16:19:55 +0530 Subject: [PATCH 42/51] Update packages/stream_chat_flutter/CHANGELOG.md --- packages/stream_chat_flutter/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index c6e131b7c..d253743d6 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -15,7 +15,7 @@ applied correctly. - [[#1525]](https://github.com/GetStream/stream-chat-flutter/issues/1525) Fixed `StreamQuotedMessageWidget` message for deleted messages not being shown correctly. -- [[#1529]](https://github.com/GetStream/stream-chat-flutter/issues/1529) Fixed fix `ClipboardData` requires non-nullable string as text on Flutter 2.10. +- [[#1529]](https://github.com/GetStream/stream-chat-flutter/issues/1529) Fixed fix `ClipboardData` requires non-nullable string as text on Flutter 3.10. โœ… Added From 2cc7fb5bdf08f55b14607a01479347f02d2225c3 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 11 May 2023 16:20:52 +0530 Subject: [PATCH 43/51] Update packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart --- .../lib/src/message_widget/message_widget.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart index f77fcedb2..c5e9538c2 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart @@ -1170,7 +1170,7 @@ class _StreamMessageWidgetState extends State : DisplayWidget.show, ), onCopyTap: (message) { - final text = message.text; + final text = message.text; if (text != null) Clipboard.setData(ClipboardData(text: text)); }, messageTheme: widget.messageTheme, From 61eba92c8530c786a4f17412ab1ad43130f19b79 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 11 May 2023 16:26:07 +0530 Subject: [PATCH 44/51] Update packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart --- .../lib/src/message_widget/message_widget.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart index c5e9538c2..cd30aad7e 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart @@ -1009,7 +1009,7 @@ class _StreamMessageWidgetState extends State title: Text(context.translations.copyMessageLabel), onClick: () { Navigator.of(context, rootNavigator: true).pop(); - final text = message.text; + final text = widget.message.text; if (text != null) Clipboard.setData(ClipboardData(text: text)); }, ), From 7c609d80b11ae7630901b96713cdc83805fe2da0 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 11 May 2023 21:38:51 +0530 Subject: [PATCH 45/51] fix(ui): fix messages grouped incorrectly w.r.t. timestamp. Signed-off-by: xsahil03x --- .../message_list_view/message_list_view.dart | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart index 89db51dde..63fe25a19 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart @@ -652,22 +652,19 @@ class _StreamMessageListViewState extends State { final isPartOfThread = message.replyCount! > 0 || message.showInChannel == true; - if (!Jiffy(message.createdAt.toLocal()).isSame( - nextMessage.createdAt.toLocal(), - Units.DAY, - )) { + final createdAt = message.createdAt.toLocal(); + final nextCreatedAt = nextMessage.createdAt.toLocal(); + if (!Jiffy(createdAt).isSame(nextCreatedAt, Units.DAY)) { separator = _buildDateDivider(nextMessage); } else { - final timeDiff = - Jiffy(nextMessage.createdAt.toLocal()).diff( - message.createdAt.toLocal(), + final hasTimeDiff = !Jiffy(createdAt).isSame( + nextCreatedAt, Units.MINUTE, ); final isNextUserSame = message.user!.id == nextMessage.user?.id; final isDeleted = message.isDeleted; - final hasTimeDiff = timeDiff >= 1; final spacingRules = [ if (hasTimeDiff) SpacingType.timeDiff, @@ -1064,10 +1061,10 @@ class _StreamMessageListViewState extends State { final isNextUserSame = nextMessage != null && message.user!.id == nextMessage.user!.id; - num timeDiff = 0; + var hasTimeDiff = false; if (nextMessage != null) { - timeDiff = Jiffy(nextMessage.createdAt.toLocal()).diff( - message.createdAt.toLocal(), + hasTimeDiff = !Jiffy(message.createdAt.toLocal()).isSame( + nextMessage.createdAt.toLocal(), Units.MINUTE, ); } @@ -1084,21 +1081,21 @@ class _StreamMessageListViewState extends State { final showTimeStamp = (!isThreadMessage || _isThreadConversation) && !hasReplies && - (timeDiff >= 1 || !isNextUserSame); + (hasTimeDiff || !isNextUserSame); final showUsername = !isMyMessage && (!isThreadMessage || _isThreadConversation) && !hasReplies && - (timeDiff >= 1 || !isNextUserSame); + (hasTimeDiff || !isNextUserSame); final showUserAvatar = isMyMessage ? DisplayWidget.gone - : (timeDiff >= 1 || !isNextUserSame) + : (hasTimeDiff || !isNextUserSame) ? DisplayWidget.show : DisplayWidget.hide; final showSendingIndicator = - isMyMessage && (index == 0 || timeDiff >= 1 || !isNextUserSame); + isMyMessage && (index == 0 || hasTimeDiff || !isNextUserSame); final showInChannelIndicator = !_isThreadConversation && isThreadMessage; final showThreadReplyIndicator = !_isThreadConversation && hasReplies; @@ -1157,7 +1154,7 @@ class _StreamMessageListViewState extends State { bottomLeft: isMyMessage ? Radius.circular(attachmentBorderRadius) : Radius.circular( - (timeDiff >= 1 || !isNextUserSame) && + (hasTimeDiff || !isNextUserSame) && !(hasReplies || isThreadMessage || hasFileAttachment) ? 0 : attachmentBorderRadius, @@ -1165,7 +1162,7 @@ class _StreamMessageListViewState extends State { topRight: Radius.circular(attachmentBorderRadius), bottomRight: isMyMessage ? Radius.circular( - (timeDiff >= 1 || !isNextUserSame) && + (hasTimeDiff || !isNextUserSame) && !(hasReplies || isThreadMessage || hasFileAttachment) ? 0 : attachmentBorderRadius, @@ -1178,7 +1175,7 @@ class _StreamMessageListViewState extends State { bottomLeft: isMyMessage ? const Radius.circular(16) : Radius.circular( - (timeDiff >= 1 || !isNextUserSame) && + (hasTimeDiff || !isNextUserSame) && !(hasReplies || isThreadMessage) ? 0 : 16, @@ -1186,7 +1183,7 @@ class _StreamMessageListViewState extends State { topRight: const Radius.circular(16), bottomRight: isMyMessage ? Radius.circular( - (timeDiff >= 1 || !isNextUserSame) && + (hasTimeDiff || !isNextUserSame) && !(hasReplies || isThreadMessage) ? 0 : 16, From c25ccebce32f8bdb3bef80c2945cefdf45c75f87 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 11 May 2023 21:40:03 +0530 Subject: [PATCH 46/51] chore: update CHANGELOG.md Signed-off-by: xsahil03x --- packages/stream_chat_flutter/CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index d253743d6..2650c9924 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -15,7 +15,10 @@ applied correctly. - [[#1525]](https://github.com/GetStream/stream-chat-flutter/issues/1525) Fixed `StreamQuotedMessageWidget` message for deleted messages not being shown correctly. -- [[#1529]](https://github.com/GetStream/stream-chat-flutter/issues/1529) Fixed fix `ClipboardData` requires non-nullable string as text on Flutter 3.10. +- [[#1529]](https://github.com/GetStream/stream-chat-flutter/issues/1529) Fixed `ClipboardData` requires non-nullable + string as text on Flutter 3.10. +- [[#1533]](https://github.com/GetStream/stream-chat-flutter/issues/1533) Fixed `StreamMessageListView` messages grouped + incorrectly w.r.t. timestamp. โœ… Added From d7282bbd6a36afb22b93224eb383dcb46b7e25a0 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 12 May 2023 15:26:53 +0530 Subject: [PATCH 47/51] fix(ui): fix `StreamMessageWidget` actions dialog backdrop filter is cut off by safe area. Signed-off-by: xsahil03x --- .../lib/src/message_widget/message_widget.dart | 1 + .../lib/src/message_widget/message_widget_content.dart | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart index cd30aad7e..0eb30cfbf 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart @@ -1144,6 +1144,7 @@ class _StreamMessageWidgetState extends State showDialog( useRootNavigator: false, context: context, + useSafeArea: false, barrierColor: _streamChatTheme.colorTheme.overlay, builder: (context) => StreamChannel( channel: channel, diff --git a/packages/stream_chat_flutter/lib/src/message_widget/message_widget_content.dart b/packages/stream_chat_flutter/lib/src/message_widget/message_widget_content.dart index e502463b2..c9e403ad5 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/message_widget_content.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/message_widget_content.dart @@ -433,6 +433,7 @@ class MessageWidgetContent extends StatelessWidget { showDialog( useRootNavigator: false, context: context, + useSafeArea: false, barrierColor: streamChatTheme.colorTheme.overlay, builder: (context) => StreamChannel( channel: channel, From ea20bfa23923a7baed4779f65cccb1fc94cf39d0 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 12 May 2023 15:27:45 +0530 Subject: [PATCH 48/51] chore: update CHANGELOG.md Signed-off-by: xsahil03x --- packages/stream_chat_flutter/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 2650c9924..44b9be45d 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -19,6 +19,8 @@ string as text on Flutter 3.10. - [[#1533]](https://github.com/GetStream/stream-chat-flutter/issues/1533) Fixed `StreamMessageListView` messages grouped incorrectly w.r.t. timestamp. +- [[#1532]](https://github.com/GetStream/stream-chat-flutter/issues/1532) Fixed `StreamMessageWidget` actions dialog + backdrop filter is cut off by safe area. โœ… Added From 2df29bf05dae28ad031dac695d46af82d864d2ef Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 12 May 2023 16:26:12 +0530 Subject: [PATCH 49/51] chore(repo): Update dart sdk environment range to support `3.0.0` Signed-off-by: xsahil03x --- packages/stream_chat/pubspec.yaml | 2 +- packages/stream_chat_flutter/pubspec.yaml | 2 +- packages/stream_chat_flutter_core/pubspec.yaml | 2 +- packages/stream_chat_localizations/pubspec.yaml | 2 +- packages/stream_chat_persistence/pubspec.yaml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/stream_chat/pubspec.yaml b/packages/stream_chat/pubspec.yaml index 61c1ff97c..11b278af7 100644 --- a/packages/stream_chat/pubspec.yaml +++ b/packages/stream_chat/pubspec.yaml @@ -6,7 +6,7 @@ repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues environment: - sdk: '>=2.17.0 <3.0.0' + sdk: '>=2.17.0 <4.0.0' dependencies: async: ^2.10.0 diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index 5425efb6f..9651a62e3 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -6,7 +6,7 @@ repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues environment: - sdk: ">=2.17.0 <3.0.0" + sdk: ">=2.17.0 <4.0.0" flutter: ">=1.20.0" dependencies: diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml index 35a6d0671..b62e4f70e 100644 --- a/packages/stream_chat_flutter_core/pubspec.yaml +++ b/packages/stream_chat_flutter_core/pubspec.yaml @@ -6,7 +6,7 @@ repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues environment: - sdk: '>=2.17.0 <3.0.0' + sdk: '>=2.17.0 <4.0.0' flutter: ">=1.17.0" dependencies: diff --git a/packages/stream_chat_localizations/pubspec.yaml b/packages/stream_chat_localizations/pubspec.yaml index 1590d7413..0eaa7171d 100644 --- a/packages/stream_chat_localizations/pubspec.yaml +++ b/packages/stream_chat_localizations/pubspec.yaml @@ -6,7 +6,7 @@ repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues environment: - sdk: '>=2.17.0 <3.0.0' + sdk: '>=2.17.0 <4.0.0' flutter: ">=1.20.0" dependencies: diff --git a/packages/stream_chat_persistence/pubspec.yaml b/packages/stream_chat_persistence/pubspec.yaml index 83fa7a111..a1693dbd0 100644 --- a/packages/stream_chat_persistence/pubspec.yaml +++ b/packages/stream_chat_persistence/pubspec.yaml @@ -6,7 +6,7 @@ repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues environment: - sdk: '>=2.17.0 <3.0.0' + sdk: '>=2.17.0 <4.0.0' flutter: ">=1.17.0" dependencies: From d2c6834a44c4adca26cc384c8f8312177779b5f5 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 12 May 2023 16:28:29 +0530 Subject: [PATCH 50/51] chore: update CHANGELOG.md Signed-off-by: xsahil03x --- packages/stream_chat/CHANGELOG.md | 4 ++ packages/stream_chat_flutter/CHANGELOG.md | 1 + .../stream_chat_flutter_core/CHANGELOG.md | 1 + .../stream_chat_localizations/CHANGELOG.md | 37 ++++++++++++++----- packages/stream_chat_persistence/CHANGELOG.md | 7 +++- 5 files changed, 40 insertions(+), 10 deletions(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 4177cd157..bc96bd2c2 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -36,6 +36,10 @@ ); ``` +๐Ÿ”„ Changed + +- Updated `dart` sdk environment range to support `3.0.0`. + ## 6.0.0 ๐Ÿž Fixed diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 44b9be45d..f7ae3a469 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -66,6 +66,7 @@ ๐Ÿ”„ Changed +- Updated `dart` sdk environment range to support `3.0.0`. - Deprecated `MessageTheme.linkBackgroundColor` in favor of `MessageTheme.urlAttachmentBackgroundColor`. ## 6.0.0 diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index 5a560da6e..8e7a8ef58 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,5 +1,6 @@ ## Upcoming +- Updated `dart` sdk environment range to support `3.0.0`. - [[#1356]](https://github.com/GetStream/stream-chat-flutter/issues/1356) Channel doesn't auto display again after being hidden. diff --git a/packages/stream_chat_localizations/CHANGELOG.md b/packages/stream_chat_localizations/CHANGELOG.md index dc42f2448..d6b35c690 100644 --- a/packages/stream_chat_localizations/CHANGELOG.md +++ b/packages/stream_chat_localizations/CHANGELOG.md @@ -1,3 +1,7 @@ +## Upcoming + +* Updated `dart` sdk environment range to support `3.0.0`. + ## 5.0.0 * Updated `stream_chat_flutter` dependency to [`6.0.0`](https://pub.dev/packages/stream_chat_flutter/changelog). @@ -6,9 +10,12 @@ โœ… Added -* Added support for [Catalan](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ca.dart) locale. +* Added support + for [Catalan](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ca.dart) + locale. * Added translations for new `noPhotoOrVideoLabel` label. -* Changed text in New messages separator. Now is doesn't count the new messages and only shows "New messages". All the translations were updated. +* Changed text in New messages separator. Now is doesn't count the new messages and only shows "New messages". All the + translations were updated. ๐Ÿ”„ Changed @@ -39,7 +46,9 @@ ## 3.3.0 -* Added support for [Norwegian](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dart) locale. +* Added support + for [Norwegian](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dart) + locale. ## 3.2.0 @@ -49,7 +58,9 @@ ## 3.1.0 -* Added support for [German](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_de.dart) locale. +* Added support + for [German](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_de.dart) + locale. ## 3.0.0 @@ -63,7 +74,9 @@ โœ… Added -* Added support for [Portuguese](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart) locale. +* Added support + for [Portuguese](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart) + locale. ๐Ÿ”„ Changed @@ -81,9 +94,15 @@ โœ… Added -* Added support for [Spanish](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart) locale. -* Added support for [Korean](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart) locale. -* Added support for [Japanese](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart) locale. +* Added support + for [Spanish](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart) + locale. +* Added support + for [Korean](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart) + locale. +* Added support + for [Japanese](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart) + locale. * Added translations for cooldown mode. * Added translations for attachmentLimitExceed. @@ -94,7 +113,7 @@ - 'เคคเคธเฅเคตเฅ€เคฐเฅ‡เค‚' -> 'เฅžเฅ‹เคŸเฅ‹เคœ' - 'เคฌเคฟเคคเคพ เคนเฅเค† เค•เคฒ' -> 'เค•เคฒ' - 'เคšเฅˆเคจเคฒ เคฎเฅŒเคจ เคนเฅˆ' -> 'เคšเฅˆเคจเคฒ เคฎเฅเคฏเฅ‚เคŸ เคนเฅˆ' - + ## 1.0.2 * Updated `stream_chat_flutter` dependency diff --git a/packages/stream_chat_persistence/CHANGELOG.md b/packages/stream_chat_persistence/CHANGELOG.md index d980edcc8..307080c6a 100644 --- a/packages/stream_chat_persistence/CHANGELOG.md +++ b/packages/stream_chat_persistence/CHANGELOG.md @@ -1,3 +1,7 @@ +## Upcoming + +- Updated `dart` sdk environment range to support `3.0.0`. + ## 6.0.0 - Updated `drift` to `^2.7.0`. @@ -23,7 +27,8 @@ ## 4.4.0 -- Allowed experimental use of indexedDb on web with `webUseExperimentalIndexedDb` parameter on `StreamChatPersistenceClient`. +- Allowed experimental use of indexedDb on web with `webUseExperimentalIndexedDb` parameter + on `StreamChatPersistenceClient`. Thanks [geweald](https://github.com/geweald). ## 4.3.0 From 693adca7b230f11923e0dfa4bb6cefe98ef2a760 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 12 May 2023 16:37:35 +0530 Subject: [PATCH 51/51] chore(repo): prepare for next release. Signed-off-by: xsahil03x --- packages/stream_chat/CHANGELOG.md | 2 +- packages/stream_chat/lib/version.dart | 2 +- packages/stream_chat/pubspec.yaml | 2 +- packages/stream_chat_flutter/CHANGELOG.md | 4 +++- packages/stream_chat_flutter/pubspec.yaml | 4 ++-- packages/stream_chat_flutter_core/CHANGELOG.md | 3 ++- packages/stream_chat_flutter_core/pubspec.yaml | 4 ++-- packages/stream_chat_localizations/CHANGELOG.md | 3 ++- packages/stream_chat_localizations/pubspec.yaml | 4 ++-- packages/stream_chat_persistence/CHANGELOG.md | 3 ++- packages/stream_chat_persistence/pubspec.yaml | 4 ++-- 11 files changed, 20 insertions(+), 15 deletions(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index bc96bd2c2..3abd5f22f 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,4 +1,4 @@ -## Upcoming +## 6.1.0 ๐Ÿž Fixed diff --git a/packages/stream_chat/lib/version.dart b/packages/stream_chat/lib/version.dart index 19a6844b0..a2567eda3 100644 --- a/packages/stream_chat/lib/version.dart +++ b/packages/stream_chat/lib/version.dart @@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart'; /// Current package version /// Used in [StreamChatClient] to build the `x-stream-client` header // ignore: constant_identifier_names -const PACKAGE_VERSION = '6.0.0'; +const PACKAGE_VERSION = '6.1.0'; diff --git a/packages/stream_chat/pubspec.yaml b/packages/stream_chat/pubspec.yaml index 11b278af7..1f2ae6a50 100644 --- a/packages/stream_chat/pubspec.yaml +++ b/packages/stream_chat/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat homepage: https://getstream.io/ description: The official Dart client for Stream Chat, a service for building chat applications. -version: 6.0.0 +version: 6.1.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index f7ae3a469..f32ef2d2b 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,4 +1,4 @@ -## Upcoming +## 6.1.0 ๐Ÿž Fixed @@ -68,6 +68,8 @@ - Updated `dart` sdk environment range to support `3.0.0`. - Deprecated `MessageTheme.linkBackgroundColor` in favor of `MessageTheme.urlAttachmentBackgroundColor`. +- Updated `stream_chat_flutter_core` dependency + to [`6.1.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog). ## 6.0.0 diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index 9651a62e3..3a0d5dd17 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter. -version: 6.0.0 +version: 6.1.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -38,7 +38,7 @@ dependencies: rxdart: ^0.27.0 share_plus: ^6.3.0 shimmer: ^2.0.0 - stream_chat_flutter_core: ^6.0.0 + stream_chat_flutter_core: ^6.1.0 synchronized: ^3.0.0 thumblr: ^0.0.4 url_launcher: ^6.1.0 diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index 8e7a8ef58..3359bf8c5 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,6 +1,7 @@ -## Upcoming +## 6.1.0 - Updated `dart` sdk environment range to support `3.0.0`. +- Updated `stream_chat` dependency to [`6.1.0`](https://pub.dev/packages/stream_chat/changelog). - [[#1356]](https://github.com/GetStream/stream-chat-flutter/issues/1356) Channel doesn't auto display again after being hidden. diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml index b62e4f70e..031099b86 100644 --- a/packages/stream_chat_flutter_core/pubspec.yaml +++ b/packages/stream_chat_flutter_core/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter_core homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter. -version: 6.0.0 +version: 6.1.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -17,7 +17,7 @@ dependencies: freezed_annotation: ^2.0.3 meta: ^1.8.0 rxdart: ^0.27.0 - stream_chat: ^6.0.0 + stream_chat: ^6.1.0 dev_dependencies: build_runner: ^2.3.3 diff --git a/packages/stream_chat_localizations/CHANGELOG.md b/packages/stream_chat_localizations/CHANGELOG.md index d6b35c690..b435fd724 100644 --- a/packages/stream_chat_localizations/CHANGELOG.md +++ b/packages/stream_chat_localizations/CHANGELOG.md @@ -1,6 +1,7 @@ -## Upcoming +## 5.1.0 * Updated `dart` sdk environment range to support `3.0.0`. +* Updated `stream_chat_flutter` dependency to [`6.1.0`](https://pub.dev/packages/stream_chat_flutter/changelog). ## 5.0.0 diff --git a/packages/stream_chat_localizations/pubspec.yaml b/packages/stream_chat_localizations/pubspec.yaml index 0eaa7171d..e55f63010 100644 --- a/packages/stream_chat_localizations/pubspec.yaml +++ b/packages/stream_chat_localizations/pubspec.yaml @@ -1,6 +1,6 @@ name: stream_chat_localizations description: The Official localizations for Stream Chat Flutter, a service for building chat applications -version: 5.0.0 +version: 5.1.0 homepage: https://github.com/GetStream/stream-chat-flutter repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -14,7 +14,7 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter - stream_chat_flutter: ^6.0.0 + stream_chat_flutter: ^6.1.0 dev_dependencies: dart_code_metrics: ^5.7.2 diff --git a/packages/stream_chat_persistence/CHANGELOG.md b/packages/stream_chat_persistence/CHANGELOG.md index 307080c6a..3e56f2ec3 100644 --- a/packages/stream_chat_persistence/CHANGELOG.md +++ b/packages/stream_chat_persistence/CHANGELOG.md @@ -1,6 +1,7 @@ -## Upcoming +## 6.1.0 - Updated `dart` sdk environment range to support `3.0.0`. +- Updated `stream_chat` dependency to [`6.1.0`](https://pub.dev/packages/stream_chat/changelog). ## 6.0.0 diff --git a/packages/stream_chat_persistence/pubspec.yaml b/packages/stream_chat_persistence/pubspec.yaml index a1693dbd0..341faf1fe 100644 --- a/packages/stream_chat_persistence/pubspec.yaml +++ b/packages/stream_chat_persistence/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_persistence homepage: https://github.com/GetStream/stream-chat-flutter description: Official Stream Chat Persistence library. Build your own chat experience using Dart and Flutter. -version: 6.0.0 +version: 6.1.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -18,7 +18,7 @@ dependencies: path: ^1.8.2 path_provider: ^2.0.1 sqlite3_flutter_libs: ^0.5.0 - stream_chat: ^6.0.0 + stream_chat: ^6.1.0 dev_dependencies: build_runner: ^2.3.3