diff --git a/analysis_options.yaml b/analysis_options.yaml index a1fa9388..e5eaf7ca 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -4,6 +4,7 @@ analyzer: exclude: - packages/*/lib/**/*.g.dart - packages/*/lib/src/emoji/** + - packages/*/lib/scrollable_positioned_list/** - packages/*/lib/**/*.freezed.dart linter: diff --git a/docusaurus/docs/Flutter/assets/end_to_end_encryption.png b/docusaurus/docs/Flutter/assets/end_to_end_encryption.png new file mode 100644 index 00000000..b41e11ce Binary files /dev/null and b/docusaurus/docs/Flutter/assets/end_to_end_encryption.png differ diff --git a/docusaurus/docs/Flutter/guides/end_to_end_chat_encryption.mdx b/docusaurus/docs/Flutter/guides/end_to_end_chat_encryption.mdx new file mode 100644 index 00000000..8a85c6a5 --- /dev/null +++ b/docusaurus/docs/Flutter/guides/end_to_end_chat_encryption.mdx @@ -0,0 +1,258 @@ +--- +id: end_to_end_chat_encryption +sidebar_position: 12 +title: End To End Chat Encryption +--- + +## Introduction + +When you communicate over a chat application with another person or group, +you may exchange sensitive information, like personally identifiable information, financial details, or passwords. +A chat application should use end-to-end encryption to ensure that users' data stays secure. + +:::note +Before you start, keep in mind that this guide is a basic example intended for educational purposes only. +If you want to implement end-to-end encryption in your production app, please consult a security professional first. +There’s a lot more to consider from a security perspective that isn’t covered here. +::: + +## What is End-to-End Encryption? + +End-to-end encryption (E2EE) is the process of securing a message from third parties so that only the sender and receiver can access the message. +E2EE provides security by storing the message in an encrypted form on the application's server or database. + +You can only access the message by decrypting and signing it using a known public key (distributed freely) +and a corresponding private key (only known by the owner). + +Each user in the application has their own public-private key pair. +Public keys are distributed publicly and encrypt the sender’s messages. +The receiver can only decrypt the sender’s message with the matching private key. + +Check out the diagram below for an example: + +![](../assets/end_to_end_encryption.png) + +## Setup + +### Dependencies + +Add the [webcrypto](https://pub.dev/packages/webcrypto) package in your `pubspec.yaml` file. + +```yaml +dependencies: + webcrypto: ^0.5.2 # latest version +``` + +### Generate Key Pair + +Write a function that generates a key pair using the **ECDH** algorithm and the **P-256** elliptic curve (**P-256** is well-supported and +offers the right balance of security and performance). + +The pair will consist of two keys: +- **PublicKey**: The key that is linked to a user to encrypt messages. +- **PrivateKey**: The key that is stored locally to decrypt messages. + +```dart +Future generateKeys() async { + final keyPair = await EcdhPrivateKey.generateKey(EllipticCurve.p256); + final publicKeyJwk = await keyPair.publicKey.exportJsonWebKey(); + final privateKeyJwk = await keyPair.privateKey.exportJsonWebKey(); + + return JsonWebKeyPair( + privateKey: json.encode(privateKeyJwk), + publicKey: json.encode(publicKeyJwk), + ); +} + +// Model class for storing keys +class JsonWebKeyPair { + const JsonWebKeyPair({ + required this.privateKey, + required this.publicKey, + }); + + final String privateKey; + final String publicKey; +} +``` + +### Generate a Crypto Key + +Next, create a symmetric **Crypto Key** using the keys generated in the previous step. +You will use those keys to encrypt and decrypt messages. + +```dart +// SendersJwk -> sender.privateKey +// ReceiverJwk -> receiver.publicKey +Future> deriveKey(String senderJwk, String receiverJwk) async { + // Sender's key + final senderPrivateKey = json.decode(senderJwk); + final senderEcdhKey = await EcdhPrivateKey.importJsonWebKey( + senderPrivateKey, + EllipticCurve.p256, + ); + + // Receiver's key + final receiverPublicKey = json.decode(receiverJwk); + final receiverEcdhKey = await EcdhPublicKey.importJsonWebKey( + receiverPublicKey, + EllipticCurve.p256, + ); + + // Generating CryptoKey + final derivedBits = await senderEcdhKey.deriveBits(256, receiverEcdhKey); + return derivedBits; +} +``` + +### Encrypting Messages + +Once you have generated the **Crypto Key**, you're ready to encrypt the message. +You can use the **AES-GCM** algorithm for its known security and performance balance and good browser availability. + +```dart +// The "iv" stands for initialization vector (IV). To ensure the encryption’s strength, +// each encryption process must use a random and distinct IV. +// It’s included in the message so that the decryption procedure can use it. +final Uint8List iv = Uint8List.fromList('Initialization Vector'.codeUnits); +``` + +```dart +Future encryptMessage(String message, List deriveKey) async { + // Importing cryptoKey + final aesGcmSecretKey = await AesGcmSecretKey.importRawKey(deriveKey); + + // Converting message into bytes + final messageBytes = Uint8List.fromList(message.codeUnits); + + // Encrypting the message + final encryptedMessageBytes = + await aesGcmSecretKey.encryptBytes(messageBytes, iv); + + // Converting encrypted message into String + final encryptedMessage = String.fromCharCodes(encryptedMessageBytes); + return encryptedMessage; +} +``` + +### Decrypting Messages + +Decrypting a message is the opposite of encrypting one. +To decrypt a message to a human-readable format, use the code snippet below: + +```dart +Future decryptMessage(String encryptedMessage, List deriveKey) async { + // Importing cryptoKey + final aesGcmSecretKey = await AesGcmSecretKey.importRawKey(deriveKey); + + // Converting message into bytes + final messageBytes = Uint8List.fromList(encryptedMessage.codeUnits); + + // Decrypting the message + final decryptedMessageBytes = + await aesGcmSecretKey.decryptBytes(messageBytes, iv); + + // Converting decrypted message into String + final decryptedMessage = String.fromCharCodes(decryptedMessageBytes); + return decryptedMessage; +} +``` + +## Implement as a Stream Chat Feature + +Now that your setup is complete you can use it to implement end-to-end encryption in your app. + +### Store User's Public Key + +The first thing you need to do is store the generated `publicKey` as an `extraData` property, in order +for other users to encrypt messages. + +```dart +// Generating keyPair using the function defined in above steps +final keyPair = generateKeys(); +``` + +```dart +await client.connectUser( + User( + id: 'cool-shadow-7', + name: 'Cool Shadow', + image: 'https://getstream.io/cool-shadow', + + // set publicKey as a extraData property + extraData: { 'publicKey': keyPair.publicKey }, + ), + client.devToken('cool-shadow-7').rawValue, +); +``` + +### Sending Encrypted Messages + +Now you will use the `encryptMessage()` function created in the previous steps to encrypt the message. + +To do that, you need to make some minor changes to the **MessageInput** widget. + +```dart +final receiverJwk = receiver.extraData['publicKey']; + +// Generating derivedKey using user's privateKey and receiver's publicKey +final derivedKey = await deriveKey(keyPair.privateKey, receiverJwk); +``` + +```dart +MessageInput( + + ... + + preMessageSending: (message) async { + // Encrypting the message text using derivedKey + final encryptedMessage = await encryptMessage(message.text, derivedKey); + + // Creating a new message with the encrypted message text + final newMessage = message.copyWith(text: encryptedMessage); + + return newMessage; + }, +), +``` + +`preMessageSending` is a parameter that allows your app to process the message before it goes to Stream’s server. +Here, you have used it to encrypt the message before sending it to Stream’s backend. + +### Showing Decrypted Messages + +Now, it’s time to decrypt the message and present it in a human-readable format to the receiver. + +You can customize the **MessageListView** widget to have a custom `messagebuilder`, that can decrypt the message. + +```dart +MessageListView( + ... + messageBuilder: (context, messageDetails, currentMessages, defaultWidget) { + // Retrieving the message from details + final message = messageDetails.message; + + // Decrypting the message text using the derivedKey + final decryptedMessageFuture = decryptMessage(message.text, derivedKey); + return FutureBuilder( + future: decryptedMessageFuture, + builder: (context, snapshot) { + if (snapshot.hasError) return Text('Error: ${snapshot.error}'); + if (!snapshot.hasData) return Container(); + + // Updating the original message with the decrypted text + final decryptedMessage = message.copyWith(text: snapshot.data); + + // Returning defaultWidget with updated message + return defaultWidget.copyWith( + message: decryptedMessage, + ); + }, + ); + }, +), +``` + +That's it! That's all you need to implement E2EE in a Stream powered chat app. + +For more details, check out our [end-to-end encrypted chat article](https://getstream.io/blog/end-to-end-encrypted-chat-in-flutter/#whats-end-to-end-encryption). diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index de445890..4b409536 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,8 +1,9 @@ -## Upcoming +## 3.2.0 🐞 Fixed -- markAllRead() now updates local channel states. +- `markAllRead()` now updates local channel states. +- [[#744]](https://github.com/GetStream/stream-chat-flutter/issues/744) Fixed unread count not updating correctly ## 3.1.1 diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index e3f1139f..e7c2d459 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -1934,8 +1934,6 @@ class ChannelClientState { read: newReads, pinnedMessages: updatedState.pinnedMessages, ); - - _computeUnread(); } int _sortByCreatedAt(Message a, Message b) => diff --git a/packages/stream_chat/lib/version.dart b/packages/stream_chat/lib/version.dart index 680b2d9f..c839a929 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 = '3.1.1'; +const PACKAGE_VERSION = '3.2.0'; diff --git a/packages/stream_chat/pubspec.yaml b/packages/stream_chat/pubspec.yaml index e2f0356a..96096fd3 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: 3.1.1 +version: 3.2.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -13,7 +13,7 @@ dependencies: collection: ^1.15.0 dio: ^4.0.0 equatable: ^2.0.0 - freezed_annotation: ^0.14.0 + freezed_annotation: ^0.15.0 http_parser: ^4.0.0 jose: ^0.3.2 json_annotation: ^4.0.1 @@ -28,7 +28,7 @@ dependencies: dev_dependencies: build_runner: ^2.0.1 dart_code_metrics: ^4.4.0 - freezed: ^0.14.1+3 - json_serializable: ^5.0.2 - mocktail: ^0.1.1 + freezed: ^0.15.0+1 + json_serializable: ^6.0.1 + mocktail: ^0.2.0 test: ^1.17.12 \ No newline at end of file diff --git a/packages/stream_chat/test/src/client/channel_test.dart b/packages/stream_chat/test/src/client/channel_test.dart index c2ea57b5..c217431a 100644 --- a/packages/stream_chat/test/src/client/channel_test.dart +++ b/packages/stream_chat/test/src/client/channel_test.dart @@ -118,9 +118,9 @@ void main() { setUpAll(() { // Fallback values - registerFallbackValue(FakeMessage()); - registerFallbackValue>([]); - registerFallbackValue(FakeAttachmentFile()); + registerFallbackValue(FakeMessage()); + registerFallbackValue([]); + registerFallbackValue(FakeAttachmentFile()); // detached loggers when(() => client.detachedLogger(any())).thenAnswer((invocation) { @@ -176,9 +176,9 @@ void main() { setUpAll(() { // Fallback values - registerFallbackValue(FakeMessage()); - registerFallbackValue(FakeAttachmentFile()); - registerFallbackValue(FakeEvent()); + registerFallbackValue(FakeMessage()); + registerFallbackValue(FakeAttachmentFile()); + registerFallbackValue(FakeEvent()); // detached loggers when(() => client.detachedLogger(any())).thenAnswer((invocation) { diff --git a/packages/stream_chat/test/src/client/client_test.dart b/packages/stream_chat/test/src/client/client_test.dart index b59faa38..36c5919e 100644 --- a/packages/stream_chat/test/src/client/client_test.dart +++ b/packages/stream_chat/test/src/client/client_test.dart @@ -31,7 +31,7 @@ void main() { setUpAll(() { // fallback values - registerFallbackValue(FakeUser()); + registerFallbackValue(FakeUser()); }); setUp(() { @@ -230,7 +230,7 @@ void main() { setUpAll(() { // fallback values - registerFallbackValue(FakeUser()); + registerFallbackValue(FakeUser()); }); setUp(() { @@ -311,7 +311,7 @@ void main() { setUpAll(() { // fallback values - registerFallbackValue(FakeUser()); + registerFallbackValue(FakeUser()); }); setUp(() { @@ -399,7 +399,7 @@ void main() { setUpAll(() { // fallback values - registerFallbackValue(FakeUser()); + registerFallbackValue(FakeUser()); }); setUp(() { @@ -523,9 +523,9 @@ void main() { setUpAll(() { // fallback values - registerFallbackValue(FakeEvent()); - registerFallbackValue(const PaginationParams()); - registerFallbackValue(FakeChannelState()); + registerFallbackValue(FakeEvent()); + registerFallbackValue(const PaginationParams()); + registerFallbackValue(FakeChannelState()); }); setUp(() async { @@ -827,9 +827,9 @@ void main() { setUpAll(() { // fallback values - registerFallbackValue(FakeEvent()); - registerFallbackValue(FakeMessage()); - registerFallbackValue(const PaginationParams()); + registerFallbackValue(FakeEvent()); + registerFallbackValue(FakeMessage()); + registerFallbackValue(const PaginationParams()); }); setUp(() async { diff --git a/packages/stream_chat/test/src/core/api/attachment_file_uploader_test.dart b/packages/stream_chat/test/src/core/api/attachment_file_uploader_test.dart index 27f4ce6a..58ffdefb 100644 --- a/packages/stream_chat/test/src/core/api/attachment_file_uploader_test.dart +++ b/packages/stream_chat/test/src/core/api/attachment_file_uploader_test.dart @@ -15,7 +15,7 @@ void main() { setUp(() { fileUploader = StreamAttachmentFileUploader(client); - registerFallbackValue(FakeMultiPartFile()); + registerFallbackValue(FakeMultiPartFile()); }); Response successResponse(String path, {Object? data}) => Response( diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 8d4faca8..00f8412b 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,10 +1,15 @@ -## Upcoming +## 3.2.0 - Updated Dart SDK constraints to `>=2.14.0 <3.0.0` +- Updated `stream_chat_flutter_core` dependency to [`3.2.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog). 🐞 Fixed +- Fixed message highlight animation alignment in `MessageListView` - [[#491]](https://github.com/GetStream/stream-chat-flutter/issues/491): Fix `MediaListView` showing media in wrong order. +- Fixed `MessageListView` initialIndex not working in some cases. +- Improved `MessageListView` rendering in case of reordering. +- Fix image thumbnail generation when using Stream CDN ✅ Added diff --git a/packages/stream_chat_flutter/lib/scrollable_positioned_list/LICENSE b/packages/stream_chat_flutter/lib/scrollable_positioned_list/LICENSE new file mode 100644 index 00000000..7cc8ade1 --- /dev/null +++ b/packages/stream_chat_flutter/lib/scrollable_positioned_list/LICENSE @@ -0,0 +1,26 @@ +Copyright 2018 the Dart project authors, Inc. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/packages/stream_chat_flutter/lib/scrollable_positioned_list/scrollable_positioned_list.dart b/packages/stream_chat_flutter/lib/scrollable_positioned_list/scrollable_positioned_list.dart new file mode 100644 index 00000000..c275b611 --- /dev/null +++ b/packages/stream_chat_flutter/lib/scrollable_positioned_list/scrollable_positioned_list.dart @@ -0,0 +1,7 @@ +// 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. + +export 'src/indexed_key.dart'; +export 'src/item_positions_listener.dart'; +export 'src/scrollable_positioned_list.dart'; diff --git a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/element_registry.dart b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/element_registry.dart new file mode 100644 index 00000000..218e2ca0 --- /dev/null +++ b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/element_registry.dart @@ -0,0 +1,98 @@ +// 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/widgets.dart'; + +/// A registry to track some [Element]s in the tree. +class RegistryWidget extends StatefulWidget { + /// Creates a [RegistryWidget]. + const RegistryWidget({Key? key, this.elementNotifier, required this.child}) + : super(key: key); + + /// The widget below this widget in the tree. + final Widget child; + + /// Contains the current set of all [Element]s created by + /// [RegisteredElementWidget]s in the tree below this widget. + /// + /// Note that if there is another [RegistryWidget] in this widget's subtree + /// that registry, and not this one, will collect elements in its subtree. + final ValueNotifier?>? elementNotifier; + + @override + State createState() => _RegistryWidgetState(); +} + +/// A widget whose [Element] will be added its nearest ancestor +/// [RegistryWidget]. +class RegisteredElementWidget extends ProxyWidget { + /// Creates a [RegisteredElementWidget]. + const RegisteredElementWidget({Key? key, required Widget child}) + : super(key: key, child: child); + + @override + Element createElement() => _RegisteredElement(this); +} + +class _RegistryWidgetState extends State { + final Set registeredElements = {}; + + @override + Widget build(BuildContext context) => _InheritedRegistryWidget( + state: this, + child: widget.child, + ); +} + +class _InheritedRegistryWidget extends InheritedWidget { + const _InheritedRegistryWidget({ + Key? key, + required this.state, + required Widget child, + }) : super(key: key, child: child); + + final _RegistryWidgetState state; + + @override + bool updateShouldNotify(InheritedWidget oldWidget) => true; +} + +class _RegisteredElement extends ProxyElement { + _RegisteredElement(ProxyWidget widget) : super(widget); + + @override + void notifyClients(ProxyWidget oldWidget) {} + + late _RegistryWidgetState _registryWidgetState; + + @override + void mount(Element? parent, dynamic newSlot) { + super.mount(parent, newSlot); + final _inheritedRegistryWidget = + dependOnInheritedWidgetOfExactType<_InheritedRegistryWidget>()!; + _registryWidgetState = _inheritedRegistryWidget.state; + _registryWidgetState.registeredElements.add(this); + _registryWidgetState.widget.elementNotifier?.value = + _registryWidgetState.registeredElements; + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final _inheritedRegistryWidget = + dependOnInheritedWidgetOfExactType<_InheritedRegistryWidget>()!; + _registryWidgetState = _inheritedRegistryWidget.state; + _registryWidgetState.registeredElements.add(this); + _registryWidgetState.widget.elementNotifier?.value = + _registryWidgetState.registeredElements; + } + + @override + void unmount() { + _registryWidgetState.registeredElements.remove(this); + _registryWidgetState.widget.elementNotifier?.value = + _registryWidgetState.registeredElements; + super.unmount(); + } +} diff --git a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/indexed_key.dart b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/indexed_key.dart new file mode 100644 index 00000000..bbff45c2 --- /dev/null +++ b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/indexed_key.dart @@ -0,0 +1,31 @@ +import 'dart:ui' show hashValues; + +import 'package:flutter/foundation.dart'; + +/// {@template indexed_key} +/// Creates an indexed key that delegates its [operator==] to the given key. +/// +/// It contains an index used in [ScrollablePositionedList]. +/// {@endtemplate} +class IndexedKey extends LocalKey { + /// {@macro indexed_key} + const IndexedKey(this.key, this.index); + + /// The key to which this this delegates its [operator==]. + final Key? key; + + /// Index used to show position in a list. + final int index; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) return false; + return other is IndexedKey && other.key == key; + } + + @override + int get hashCode => hashValues(runtimeType, key); + + @override + String toString() => '(IndexedKey) index: $index, key: $key'; +} diff --git a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/item_positions_listener.dart b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/item_positions_listener.dart new file mode 100644 index 00000000..1b611393 --- /dev/null +++ b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/item_positions_listener.dart @@ -0,0 +1,62 @@ +// 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/foundation.dart'; + +import 'package:stream_chat_flutter/scrollable_positioned_list/src/item_positions_notifier.dart'; +import 'package:stream_chat_flutter/scrollable_positioned_list/src/scrollable_positioned_list.dart'; + +/// Provides a listenable iterable of [itemPositions] of items that are on +/// screen and their locations. +abstract class ItemPositionsListener { + /// Creates an [ItemPositionsListener] that can be used by a + /// [ScrollablePositionedList] to return the current position of items. + factory ItemPositionsListener.create() => ItemPositionsNotifier(); + + /// The position of items that are at least partially visible in the viewport. + ValueListenable> get itemPositions; +} + +/// Position information for an item in the list. +class ItemPosition { + /// Create an [ItemPosition]. + const ItemPosition({ + required this.index, + required this.itemLeadingEdge, + required this.itemTrailingEdge, + }); + + /// Index of the item. + final int index; + + /// Distance in proportion of the viewport's main axis length from the leading + /// edge of the viewport to the leading edge of the item. + /// + /// May be negative if the item is partially visible. + final double itemLeadingEdge; + + /// Distance in proportion of the viewport's main axis length from the leading + /// edge of the viewport to the trailing edge of the item. + /// + /// May be greater than one if the item is partially visible. + final double itemTrailingEdge; + + @override + bool operator ==(dynamic other) { + if (other.runtimeType != runtimeType) return false; + final ItemPosition otherPosition = other; + return otherPosition.index == index && + otherPosition.itemLeadingEdge == itemLeadingEdge && + otherPosition.itemTrailingEdge == itemTrailingEdge; + } + + @override + int get hashCode => + 31 * (31 * (index.hashCode + 7) + itemLeadingEdge.hashCode) + + itemTrailingEdge.hashCode; + + @override + String toString() => + '''ItemPosition(index: $index, itemLeadingEdge: $itemLeadingEdge, itemTrailingEdge: $itemTrailingEdge)'''; +} diff --git a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/item_positions_notifier.dart b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/item_positions_notifier.dart new file mode 100644 index 00000000..3b3645cb --- /dev/null +++ b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/item_positions_notifier.dart @@ -0,0 +1,13 @@ +// 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/foundation.dart'; + +import 'package:stream_chat_flutter/scrollable_positioned_list/src/item_positions_listener.dart'; + +/// Internal implementation of [ItemPositionsListener]. +class ItemPositionsNotifier implements ItemPositionsListener { + @override + final ValueNotifier> itemPositions = ValueNotifier([]); +} 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 new file mode 100644 index 00000000..2ce9adb5 --- /dev/null +++ b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/positioned_list.dart @@ -0,0 +1,370 @@ +// 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/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter/widgets.dart'; + +import 'package:stream_chat_flutter/scrollable_positioned_list/src/element_registry.dart'; +import 'package:stream_chat_flutter/scrollable_positioned_list/src/indexed_key.dart'; +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'; + +/// A list of widgets similar to [ListView], except scroll control +/// and position reporting is based on index rather than pixel offset. +/// +/// [PositionedList] lays out children in the same way as [ListView]. +/// +/// The list can be displayed with the item at [positionIndex] positioned at a +/// particular [alignment]. See [ItemScrollController.jumpTo] for an +/// explanation of alignment. +/// +/// All other parameters are the same as specified in [ListView]. +class PositionedList extends StatefulWidget { + /// Create a [PositionedList]. + const PositionedList({ + Key? key, + required this.itemCount, + required this.itemBuilder, + this.separatorBuilder, + this.controller, + this.itemPositionsNotifier, + this.positionedIndex = 0, + this.alignment = 0, + this.scrollDirection = Axis.vertical, + this.reverse = false, + this.physics, + this.padding, + this.cacheExtent, + this.semanticChildCount, + this.findChildIndexCallback, + this.addSemanticIndexes = true, + this.addRepaintBoundaries = true, + this.addAutomaticKeepAlives = true, + }) : assert((positionedIndex == 0) || (positionedIndex < itemCount), + 'positionedIndex cannot be 0 and must be smaller than itemCount'), + super(key: key); + + /// 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; + + /// Number of items the [itemBuilder] can produce. + final int itemCount; + + /// Called to build children for the list with + /// 0 <= index < itemCount. + final IndexedWidgetBuilder itemBuilder; + + /// If not null, called to build separators for between each item in the list. + /// Called with 0 <= index < itemCount - 1. + final IndexedWidgetBuilder? separatorBuilder; + + /// An object that can be used to control the position to which this scroll + /// view is scrolled. + final ScrollController? controller; + + /// Notifier that reports the items laid out in the list after each frame. + final ItemPositionsNotifier? itemPositionsNotifier; + + /// Index of an item to initially align to a position within the viewport + /// defined by [alignment]. + final int positionedIndex; + + /// Determines where the leading edge of the item at [positionedIndex] + /// should be placed. + /// + /// See [ItemScrollController.jumpTo] for an explanation of alignment. + final double alignment; + + /// The axis along which the scroll view scrolls. + /// + /// Defaults to [Axis.vertical]. + final Axis scrollDirection; + + /// Whether the view scrolls in the reading direction. + /// + /// Defaults to false. + /// + /// See [ScrollView.reverse]. + final bool reverse; + + /// How the scroll view should respond to user input. + /// + /// For example, determines how the scroll view continues to animate after the + /// user stops dragging the scroll view. + /// + /// See [ScrollView.physics]. + final ScrollPhysics? physics; + + /// {@macro flutter.widgets.scrollable.cacheExtent} + final double? cacheExtent; + + /// The number of children that will contribute semantic information. + /// + /// See [ScrollView.semanticChildCount] for more information. + final int? semanticChildCount; + + /// Whether to wrap each child in an [IndexedSemantics]. + /// + /// See [SliverChildBuilderDelegate.addSemanticIndexes]. + final bool addSemanticIndexes; + + /// The amount of space by which to inset the children. + final EdgeInsets? padding; + + /// Whether to wrap each child in a [RepaintBoundary]. + /// + /// See [SliverChildBuilderDelegate.addRepaintBoundaries]. + final bool addRepaintBoundaries; + + /// Whether to wrap each child in an [AutomaticKeepAlive]. + /// + /// See [SliverChildBuilderDelegate.addAutomaticKeepAlives]. + final bool addAutomaticKeepAlives; + + @override + State createState() => _PositionedListState(); +} + +class _PositionedListState extends State { + final Key _centerKey = UniqueKey(); + + final registeredElements = ValueNotifier?>(null); + late final ScrollController scrollController; + + bool updateScheduled = false; + + @override + void initState() { + super.initState(); + scrollController = widget.controller ?? ScrollController(); + scrollController.addListener(_schedulePositionNotificationUpdate); + _schedulePositionNotificationUpdate(); + } + + @override + void dispose() { + scrollController.removeListener(_schedulePositionNotificationUpdate); + super.dispose(); + } + + @override + void didUpdateWidget(PositionedList oldWidget) { + super.didUpdateWidget(oldWidget); + _schedulePositionNotificationUpdate(); + } + + @override + Widget build(BuildContext context) => RegistryWidget( + elementNotifier: registeredElements, + child: UnboundedCustomScrollView( + anchor: widget.alignment, + center: _centerKey, + controller: scrollController, + scrollDirection: widget.scrollDirection, + reverse: widget.reverse, + cacheExtent: widget.cacheExtent, + physics: widget.physics, + semanticChildCount: widget.semanticChildCount ?? widget.itemCount, + slivers: [ + if (widget.positionedIndex > 0) + SliverPadding( + padding: _leadingSliverPadding, + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) => widget.separatorBuilder == null + ? _buildItem(widget.positionedIndex - (index + 1)) + : _buildSeparatedListElement( + widget.positionedIndex * 2 - (index + 1), + ), + childCount: widget.separatorBuilder == null + ? widget.positionedIndex + : widget.positionedIndex * 2, + addSemanticIndexes: false, + findChildIndexCallback: widget.findChildIndexCallback, + addRepaintBoundaries: widget.addRepaintBoundaries, + addAutomaticKeepAlives: widget.addAutomaticKeepAlives, + ), + ), + ), + SliverPadding( + key: _centerKey, + padding: _centerSliverPadding, + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) => widget.separatorBuilder == null + ? _buildItem(index + widget.positionedIndex) + : _buildSeparatedListElement( + index + widget.positionedIndex * 2, + ), + childCount: widget.itemCount != 0 ? 1 : 0, + findChildIndexCallback: widget.findChildIndexCallback, + addSemanticIndexes: false, + addRepaintBoundaries: widget.addRepaintBoundaries, + addAutomaticKeepAlives: widget.addAutomaticKeepAlives, + ), + ), + ), + if (widget.positionedIndex >= 0 && + widget.positionedIndex < widget.itemCount - 1) + SliverPadding( + padding: _trailingSliverPadding, + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) => widget.separatorBuilder == null + ? _buildItem(index + widget.positionedIndex + 1) + : _buildSeparatedListElement( + index + widget.positionedIndex * 2 + 1, + ), + 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, + ), + ), + ), + ], + ), + ); + + Widget _buildSeparatedListElement(int index) { + if (index.isEven) { + return _buildItem(index ~/ 2); + } else { + return widget.separatorBuilder!(context, index ~/ 2); + } + } + + Widget _buildItem(int index) { + final child = widget.itemBuilder(context, index); + return RegisteredElementWidget( + key: IndexedKey(child.key, index), + child: widget.addSemanticIndexes + ? IndexedSemantics(index: index, child: child) + : child, + ); + } + + EdgeInsets get _leadingSliverPadding => + (widget.scrollDirection == Axis.vertical + ? widget.reverse + ? widget.padding?.copyWith(top: 0) + : widget.padding?.copyWith(bottom: 0) + : widget.reverse + ? widget.padding?.copyWith(left: 0) + : widget.padding?.copyWith(right: 0)) ?? + const EdgeInsets.all(0); + + EdgeInsets get _centerSliverPadding => widget.scrollDirection == Axis.vertical + ? widget.reverse + ? widget.padding?.copyWith( + top: widget.positionedIndex == widget.itemCount - 1 + ? widget.padding!.top + : 0, + bottom: + widget.positionedIndex == 0 ? widget.padding!.bottom : 0, + ) ?? + const EdgeInsets.all(0) + : widget.padding?.copyWith( + top: widget.positionedIndex == 0 ? widget.padding!.top : 0, + bottom: widget.positionedIndex == widget.itemCount - 1 + ? widget.padding!.bottom + : 0, + ) ?? + const EdgeInsets.all(0) + : widget.reverse + ? widget.padding?.copyWith( + left: widget.positionedIndex == widget.itemCount - 1 + ? widget.padding!.left + : 0, + right: widget.positionedIndex == 0 ? widget.padding!.right : 0, + ) ?? + const EdgeInsets.all(0) + : widget.padding?.copyWith( + left: widget.positionedIndex == 0 ? widget.padding!.left : 0, + right: widget.positionedIndex == widget.itemCount - 1 + ? widget.padding!.right + : 0, + ) ?? + const EdgeInsets.all(0); + + EdgeInsets get _trailingSliverPadding => + widget.scrollDirection == Axis.vertical + ? widget.reverse + ? widget.padding?.copyWith(bottom: 0) ?? const EdgeInsets.all(0) + : widget.padding?.copyWith(top: 0) ?? const EdgeInsets.all(0) + : widget.reverse + ? widget.padding?.copyWith(right: 0) ?? const EdgeInsets.all(0) + : widget.padding?.copyWith(left: 0) ?? const EdgeInsets.all(0); + + void _schedulePositionNotificationUpdate() { + if (!updateScheduled) { + updateScheduled = true; + SchedulerBinding.instance!.addPostFrameCallback((_) { + if (registeredElements.value == 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; + } + final key = element.widget.key as IndexedKey; + if (widget.scrollDirection == Axis.vertical) { + final reveal = viewport.getOffsetToReveal(box, 0).offset; + if (!reveal.isFinite) continue; + final itemOffset = reveal - + viewport.offset.pixels + + viewport.anchor * viewport.size.height; + positions.add(ItemPosition( + index: key.index, + itemLeadingEdge: itemOffset.round() / + scrollController.position.viewportDimension, + itemTrailingEdge: (itemOffset + box.size.height).round() / + scrollController.position.viewportDimension, + )); + } else { + final itemOffset = + box.localToGlobal(Offset.zero, ancestor: viewport).dx; + positions.add(ItemPosition( + index: key.index, + itemLeadingEdge: (widget.reverse + ? scrollController.position.viewportDimension - + (itemOffset + box.size.width) + : itemOffset) + .round() / + scrollController.position.viewportDimension, + itemTrailingEdge: (widget.reverse + ? scrollController.position.viewportDimension - + itemOffset + : (itemOffset + box.size.width)) + .round() / + scrollController.position.viewportDimension, + )); + } + } + widget.itemPositionsNotifier?.itemPositions.value = positions; + updateScheduled = false; + }); + } + } +} diff --git a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/post_mount_callback.dart b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/post_mount_callback.dart new file mode 100644 index 00000000..ddb3e3d4 --- /dev/null +++ b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/post_mount_callback.dart @@ -0,0 +1,35 @@ +// 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/widgets.dart'; + +/// Widget whose [Element] calls a callback when the element is mounted. +class PostMountCallback extends StatelessWidget { + /// Creates a [PostMountCallback] widget. + const PostMountCallback({required this.child, this.callback, Key? key}) + : super(key: key); + + /// The widget below this widget in the tree. + final Widget child; + + /// Callback to call when the element for this widget is mounted. + final void Function()? callback; + + @override + StatelessElement createElement() => _PostMountCallbackElement(this); + + @override + Widget build(BuildContext context) => child; +} + +class _PostMountCallbackElement extends StatelessElement { + _PostMountCallbackElement(PostMountCallback widget) : super(widget); + + @override + void mount(Element? parent, dynamic newSlot) { + super.mount(parent, newSlot); + final postMountCallback = widget as PostMountCallback; + postMountCallback.callback?.call(); + } +} 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 new file mode 100644 index 00000000..a0b499c0 --- /dev/null +++ b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scroll_view.dart @@ -0,0 +1,79 @@ +// 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/gestures.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +import 'package:stream_chat_flutter/scrollable_positioned_list/src/viewport.dart'; + +/// {@template custom_scroll_view} +/// A version of [CustomScrollView] that 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} + const UnboundedCustomScrollView({ + Key? key, + Axis scrollDirection = Axis.vertical, + bool reverse = false, + ScrollController? controller, + bool? primary, + ScrollPhysics? physics, + bool shrinkWrap = false, + Key? center, + double anchor = 0.0, + double? cacheExtent, + List slivers = const [], + int? semanticChildCount, + DragStartBehavior dragStartBehavior = DragStartBehavior.start, + }) : _anchor = anchor, + super( + key: key, + scrollDirection: scrollDirection, + reverse: reverse, + controller: controller, + primary: primary, + physics: physics, + shrinkWrap: shrinkWrap, + center: center, + cacheExtent: cacheExtent, + semanticChildCount: semanticChildCount, + dragStartBehavior: dragStartBehavior, + slivers: slivers, + ); + + // [CustomScrollView] enforces constraints on [CustomScrollView.anchor], so + // we need our own version. + final double _anchor; + + @override + double get anchor => _anchor; + + /// Build the viewport. + @override + @protected + Widget buildViewport( + BuildContext context, + ViewportOffset offset, + AxisDirection axisDirection, + List slivers, + ) { + if (shrinkWrap) { + return ShrinkWrappingViewport( + axisDirection: axisDirection, + offset: offset, + slivers: slivers, + ); + } + return UnboundedViewport( + axisDirection: axisDirection, + offset: offset, + slivers: slivers, + cacheExtent: cacheExtent, + center: center, + anchor: anchor, + ); + } +} 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 new file mode 100644 index 00000000..765a90a0 --- /dev/null +++ b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scrollable_positioned_list.dart @@ -0,0 +1,593 @@ +// 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 'dart:async'; +import 'dart:math'; + +import 'package:collection/collection.dart' show IterableExtension; +import 'package:flutter/foundation.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter/widgets.dart'; + +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/positioned_list.dart'; +import 'package:stream_chat_flutter/scrollable_positioned_list/src/post_mount_callback.dart'; + +/// Number of screens to scroll when scrolling a long distance. +const int _screenScrollCount = 2; + +/// A scrollable list of widgets similar to [ListView], except scroll control +/// and position reporting is based on index rather than pixel offset. +/// +/// [ScrollablePositionedList] lays out children in the same way as [ListView]. +/// +/// The list can be displayed with the item at [initialScrollIndex] positioned +/// at a particular [initialAlignment]. +/// +/// The [itemScrollController] can be used to scroll or jump to particular items +/// in the list. The [itemPositionsNotifier] can be used to get a list of items +/// currently laid out by the list. +/// +/// All other parameters are the same as specified in [ListView]. +class ScrollablePositionedList extends StatefulWidget { + /// Create a [ScrollablePositionedList] whose items are provided by + /// [itemBuilder]. + const ScrollablePositionedList.builder({ + required this.itemCount, + required this.itemBuilder, + Key? key, + this.itemScrollController, + ItemPositionsListener? itemPositionsListener, + this.initialScrollIndex = 0, + this.initialAlignment = 0, + this.scrollDirection = Axis.vertical, + this.reverse = false, + this.physics, + this.semanticChildCount, + this.padding, + this.addSemanticIndexes = true, + this.addAutomaticKeepAlives = true, + this.addRepaintBoundaries = true, + this.minCacheExtent, + this.findChildIndexCallback, + }) : itemPositionsNotifier = itemPositionsListener as ItemPositionsNotifier?, + separatorBuilder = null, + super(key: key); + + /// Create a [ScrollablePositionedList] whose items are provided by + /// [itemBuilder] and separators provided by [separatorBuilder]. + const ScrollablePositionedList.separated({ + required this.itemCount, + required this.itemBuilder, + required this.separatorBuilder, + Key? key, + this.itemScrollController, + ItemPositionsListener? itemPositionsListener, + this.initialScrollIndex = 0, + this.initialAlignment = 0, + this.scrollDirection = Axis.vertical, + this.reverse = false, + this.physics, + this.semanticChildCount, + this.padding, + this.addSemanticIndexes = true, + this.addAutomaticKeepAlives = true, + this.addRepaintBoundaries = true, + this.minCacheExtent, + this.findChildIndexCallback, + }) : assert(separatorBuilder != null, 'seperatorBuilder cannot be null'), + itemPositionsNotifier = itemPositionsListener as ItemPositionsNotifier?, + super(key: key); + + /// 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; + + /// Number of items the [itemBuilder] can produce. + final int itemCount; + + /// Called to build children for the list with + /// 0 <= index < itemCount. + final IndexedWidgetBuilder itemBuilder; + + /// Called to build separators for between each item in the list. + /// Called with 0 <= index < itemCount - 1. + final IndexedWidgetBuilder? separatorBuilder; + + /// Controller for jumping or scrolling to an item. + final ItemScrollController? itemScrollController; + + /// Notifier that reports the items laid out in the list after each frame. + final ItemPositionsNotifier? itemPositionsNotifier; + + /// Index of an item to initially align within the viewport. + final int initialScrollIndex; + + /// Determines where the leading edge of the item at [initialScrollIndex] + /// should be placed. + /// + /// See [ItemScrollController.jumpTo] for an explanation of alignment. + final double initialAlignment; + + /// The axis along which the scroll view scrolls. + /// + /// Defaults to [Axis.vertical]. + final Axis scrollDirection; + + /// Whether the view scrolls in the reading direction. + /// + /// Defaults to false. + /// + /// See [ScrollView.reverse]. + final bool reverse; + + /// How the scroll view should respond to user input. + /// + /// For example, determines how the scroll view continues to animate after the + /// user stops dragging the scroll view. + /// + /// See [ScrollView.physics]. + final ScrollPhysics? physics; + + /// The number of children that will contribute semantic information. + /// + /// See [ScrollView.semanticChildCount] for more information. + final int? semanticChildCount; + + /// The amount of space by which to inset the children. + final EdgeInsets? padding; + + /// Whether to wrap each child in an [IndexedSemantics]. + /// + /// See [SliverChildBuilderDelegate.addSemanticIndexes]. + final bool addSemanticIndexes; + + /// Whether to wrap each child in an [AutomaticKeepAlive]. + /// + /// See [SliverChildBuilderDelegate.addAutomaticKeepAlives]. + final bool addAutomaticKeepAlives; + + /// Whether to wrap each child in a [RepaintBoundary]. + /// + /// See [SliverChildBuilderDelegate.addRepaintBoundaries]. + final bool addRepaintBoundaries; + + /// The minimum cache extent used by the underlying scroll lists. + /// See [ScrollView.cacheExtent]. + /// + /// Note that the [ScrollablePositionedList] uses two lists to simulate long + /// scrolls, so using the [ScrollController.scrollTo] method may result + /// in builds of widgets that would otherwise already be built in the + /// cache extent. + final double? minCacheExtent; + + @override + State createState() => _ScrollablePositionedListState(); +} + +/// Controller to jump or scroll to a particular position in a +/// [ScrollablePositionedList]. +class ItemScrollController { + /// Whether any ScrollablePositionedList objects are attached this object. + /// + /// If `false`, then [jumpTo] and [scrollTo] must not be called. + bool get isAttached => _scrollableListState != null; + + _ScrollablePositionedListState? _scrollableListState; + + /// Immediately, without animation, reconfigure the list so that the item at + /// [index]'s leading edge is at the given [alignment]. + /// + /// The [alignment] specifies the desired position for the leading edge of the + /// item. The [alignment] is expected to be a value in the range \[0.0, 1.0\] + /// and represents a proportion along the main axis of the viewport. + /// + /// For a vertically scrolling view that is not reversed: + /// * 0 aligns the top edge of the item with the top edge of the view. + /// * 1 aligns the top edge of the item with the bottom of the view. + /// * 0.5 aligns the top edge of the item with the center of the view. + /// + /// For a horizontally scrolling view that is not reversed: + /// * 0 aligns the left edge of the item with the left edge of the view + /// * 1 aligns the left edge of the item with the right edge of the view. + /// * 0.5 aligns the left edge of the item with the center of the view. + void jumpTo({required int index, double alignment = 0}) { + _scrollableListState!._jumpTo(index: index, alignment: alignment); + } + + /// Animate the list over [duration] using the given [curve] such that the + /// item at [index] ends up with its leading edge at the given [alignment]. + /// See [jumpTo] for an explanation of alignment. + /// + /// The [duration] must be greater than 0; otherwise, use [jumpTo]. + /// + /// When item position is not available, because it's too far, the scroll + /// is composed into three phases: + /// + /// 1. The currently displayed list view starts scrolling. + /// 2. Another list view, which scrolls with the same speed, fades over the + /// first one and shows items that are close to the scroll target. + /// 3. The second list view scrolls and stops on the target. + /// + /// The [opacityAnimationWeights] can be used to apply custom weights to these + /// three stages of this animation. The default weights, `[40, 20, 40]`, are + /// good with default [Curves.linear]. Different weights might be better for + /// other cases. For example, if you use [Curves.easeOut], consider setting + /// [opacityAnimationWeights] to `[20, 20, 60]`. + /// + /// See [TweenSequenceItem.weight] for more info. + Future scrollTo({ + required int index, + double alignment = 0, + required Duration duration, + 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'); + return _scrollableListState!._scrollTo( + index: index, + alignment: alignment, + duration: duration, + curve: curve, + opacityAnimationWeights: opacityAnimationWeights, + ); + } + + void _attach(_ScrollablePositionedListState scrollableListState) { + assert( + _scrollableListState == null, '_scrollableListState needs to be null'); + _scrollableListState = scrollableListState; + } + + void _detach() { + _scrollableListState = null; + } +} + +class _ScrollablePositionedListState extends State + with TickerProviderStateMixin { + /// Details for the primary (active) [ListView]. + _ListDisplayDetails primary = _ListDisplayDetails(const ValueKey('Ping')); + + /// Details for the secondary (transitional) [ListView] that is temporarily + /// shown when scrolling a long distance. + _ListDisplayDetails secondary = _ListDisplayDetails(const ValueKey('Pong')); + + final opacity = ProxyAnimation(const AlwaysStoppedAnimation(0)); + + void Function() startAnimationCallback = () {}; + + bool _isTransitioning = false; + + @override + void initState() { + super.initState(); + final ItemPosition? initialPosition = + PageStorage.of(context)!.readState(context); + primary + ..target = initialPosition?.index ?? widget.initialScrollIndex + ..alignment = initialPosition?.itemLeadingEdge ?? widget.initialAlignment; + if (widget.itemCount > 0 && primary.target > widget.itemCount - 1) { + primary.target = widget.itemCount - 1; + } + widget.itemScrollController?._attach(this); + primary.itemPositionsNotifier.itemPositions.addListener(_updatePositions); + secondary.itemPositionsNotifier.itemPositions.addListener(_updatePositions); + } + + @override + void deactivate() { + widget.itemScrollController?._detach(); + super.deactivate(); + } + + @override + void dispose() { + primary.itemPositionsNotifier.itemPositions + .removeListener(_updatePositions); + secondary.itemPositionsNotifier.itemPositions + .removeListener(_updatePositions); + super.dispose(); + } + + @override + void didUpdateWidget(ScrollablePositionedList oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.itemScrollController?._scrollableListState == this) { + oldWidget.itemScrollController?._detach(); + } + if (widget.itemScrollController?._scrollableListState != this) { + widget.itemScrollController?._detach(); + widget.itemScrollController?._attach(this); + } + + if (widget.itemCount == 0) { + primary.target = 0; + secondary.target = 0; + } else { + if (primary.target > widget.itemCount - 1) { + primary.target = widget.itemCount - 1; + } + if (secondary.target > widget.itemCount - 1) { + secondary.target = widget.itemCount - 1; + } + } + } + + @override + Widget build(BuildContext context) => 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, + addSemanticIndexes: widget.addSemanticIndexes, + semanticChildCount: widget.semanticChildCount, + padding: widget.padding, + addAutomaticKeepAlives: widget.addAutomaticKeepAlives, + addRepaintBoundaries: widget.addRepaintBoundaries, + findChildIndexCallback: widget.findChildIndexCallback, + ), + ), + ), + ), + if (_isTransitioning) + PostMountCallback( + key: secondary.key, + callback: startAnimationCallback, + child: FadeTransition( + opacity: opacity, + child: NotificationListener( + onNotification: (_) => false, + child: PositionedList( + 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.minCacheExtent ?? 0, + ); + + void _jumpTo({required int index, required double alignment}) { + _stopScroll(canceled: true); + if (index > widget.itemCount - 1) { + index = widget.itemCount - 1; + } + setState(() { + primary.scrollController.jumpTo(0); + primary + ..target = index + ..alignment = alignment; + }); + } + + Future _scrollTo({ + required int index, + required double alignment, + required Duration duration, + Curve curve = Curves.linear, + required List opacityAnimationWeights, + }) async { + if (index > widget.itemCount - 1) { + index = widget.itemCount - 1; + } + if (_isTransitioning) { + _stopScroll(canceled: true); + SchedulerBinding.instance!.addPostFrameCallback((_) { + _startScroll( + index: index, + alignment: alignment, + duration: duration, + curve: curve, + opacityAnimationWeights: opacityAnimationWeights, + ); + }); + } else { + await _startScroll( + index: index, + alignment: alignment, + duration: duration, + curve: curve, + opacityAnimationWeights: opacityAnimationWeights, + ); + } + } + + Future _startScroll({ + required int index, + required double alignment, + required Duration duration, + Curve curve = Curves.linear, + required List opacityAnimationWeights, + }) async { + final direction = index > primary.target ? 1 : -1; + final itemPosition = + primary.itemPositionsNotifier.itemPositions.value.firstWhereOrNull( + (ItemPosition itemPosition) => itemPosition.index == index, + ); + if (itemPosition != null) { + // Scroll directly. + final localScrollAmount = itemPosition.itemLeadingEdge * + primary.scrollController.position.viewportDimension; + await primary.scrollController.animateTo( + primary.scrollController.offset + + localScrollAmount - + alignment * primary.scrollController.position.viewportDimension, + duration: duration, + curve: curve, + ); + } else { + final scrollAmount = _screenScrollCount * + primary.scrollController.position.viewportDimension; + final startCompleter = Completer(); + final endCompleter = Completer(); + startAnimationCallback = () { + SchedulerBinding.instance!.addPostFrameCallback((_) { + startAnimationCallback = () {}; + + opacity.parent = _opacityAnimation(opacityAnimationWeights).animate( + AnimationController(vsync: this, duration: duration)..forward(), + ); + secondary.scrollController.jumpTo(-direction * + (_screenScrollCount * + primary.scrollController.position.viewportDimension - + alignment * + secondary.scrollController.position.viewportDimension)); + + startCompleter.complete(primary.scrollController.animateTo( + primary.scrollController.offset + direction * scrollAmount, + duration: duration, + curve: curve, + )); + endCompleter.complete(secondary.scrollController + .animateTo(0, duration: duration, curve: curve)); + }); + }; + setState(() { + // TODO: _startScroll can be re-entrant, which invalidates this assert. + // assert(!_isTransitioning); + secondary + ..target = index + ..alignment = alignment; + _isTransitioning = true; + }); + await Future.wait([startCompleter.future, endCompleter.future]); + _stopScroll(); + } + } + + void _stopScroll({bool canceled = false}) { + if (!_isTransitioning) { + return; + } + + if (canceled) { + if (primary.scrollController.hasClients) { + primary.scrollController.jumpTo(primary.scrollController.offset); + } + if (secondary.scrollController.hasClients) { + secondary.scrollController.jumpTo(secondary.scrollController.offset); + } + } + + 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) { + const startOpacity = 0.0; + const endOpacity = 1.0; + return TweenSequence(>[ + TweenSequenceItem( + tween: ConstantTween(startOpacity), + weight: opacityAnimationWeights[0], + ), + TweenSequenceItem( + tween: Tween(begin: startOpacity, end: endOpacity), + weight: opacityAnimationWeights[1], + ), + TweenSequenceItem( + tween: ConstantTween(endOpacity), + weight: opacityAnimationWeights[2], + ), + ]); + } + + void _updatePositions() { + final itemPositions = primary.itemPositionsNotifier.itemPositions.value + .where((ItemPosition position) => + position.itemLeadingEdge < 1 && position.itemTrailingEdge > 0); + if (itemPositions.isNotEmpty) { + PageStorage.of(context)!.writeState( + context, + itemPositions.reduce((value, element) => + value.itemLeadingEdge < element.itemLeadingEdge ? value : element), + ); + } + widget.itemPositionsNotifier?.itemPositions.value = itemPositions; + } +} + +class _ListDisplayDetails { + _ListDisplayDetails(this.key); + + final itemPositionsNotifier = ItemPositionsNotifier(); + final scrollController = ScrollController(keepScrollOffset: false); + + /// The index of the item to scroll to. + int target = 0; + + /// The desired alignment for [target]. + /// + /// See [ItemScrollController.jumpTo] for an explanation of alignment. + double alignment = 0; + + final Key key; +} 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 new file mode 100644 index 00000000..01dce022 --- /dev/null +++ b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/viewport.dart @@ -0,0 +1,326 @@ +// 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. + +// ignore_for_file: lines_longer_than_80_chars + +import 'dart:math' as math; + +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +/// {@template unbounded_viewport} +/// A render object that is bigger on the inside. +/// +/// 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 +class UnboundedViewport extends Viewport { + /// {@macro unbounded_viewport} + UnboundedViewport({ + Key? key, + AxisDirection axisDirection = AxisDirection.down, + AxisDirection? crossAxisDirection, + double anchor = 0.0, + required ViewportOffset offset, + Key? center, + double? cacheExtent, + List slivers = const [], + }) : _anchor = anchor, + super( + key: key, + axisDirection: axisDirection, + crossAxisDirection: crossAxisDirection, + offset: offset, + center: center, + cacheExtent: cacheExtent, + slivers: slivers, + ); + + // [Viewport] enforces constraints on [Viewport.anchor], so we need our own + // version. + final double _anchor; + + @override + double get anchor => _anchor; + + @override + RenderViewport createRenderObject(BuildContext context) => + UnboundedRenderViewport( + axisDirection: axisDirection, + crossAxisDirection: crossAxisDirection ?? + Viewport.getDefaultCrossAxisDirection(context, axisDirection), + anchor: anchor, + offset: offset, + cacheExtent: cacheExtent, + ); +} + +/// A render object that is bigger on the inside. +/// +/// Version of [RenderViewport] with some modifications to how extents are +/// computed to allow scroll extents outside 0 to 1. See [RenderViewport] +/// for more information. +/// +// Differences from [RenderViewport] are marked with a //***** Differences +// comment. +class UnboundedRenderViewport extends RenderViewport { + /// Creates a viewport for [RenderSliver] objects. + UnboundedRenderViewport({ + AxisDirection axisDirection = AxisDirection.down, + required AxisDirection crossAxisDirection, + required ViewportOffset offset, + double anchor = 0.0, + List? children, + RenderSliver? center, + double? cacheExtent, + }) : _anchor = anchor, + super( + axisDirection: axisDirection, + crossAxisDirection: crossAxisDirection, + offset: offset, + center: center, + cacheExtent: cacheExtent, + children: children, + ); + + static const int _maxLayoutCycles = 10; + + double _anchor; + + // Out-of-band data computed during layout. + late double _minScrollExtent; + late double _maxScrollExtent; + bool _hasVisualOverflow = false; + + /// 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; + + @override + double get anchor => _anchor; + + @override + set anchor(double value) { + if (value == _anchor) return; + _anchor = value; + markNeedsLayout(); + } + + @override + void performResize() { + super.performResize(); + // TODO: Figure out why this override is needed as a result of + // https://github.com/flutter/flutter/pull/61973 and see if it can be + // removed somehow. + switch (axis) { + case Axis.vertical: + offset.applyViewportDimension(size.height); + break; + case Axis.horizontal: + offset.applyViewportDimension(size.width); + break; + } + } + + @override + Rect describeSemanticsClip(RenderSliver? child) { + if (_calculatedCacheExtent == null) { + return semanticBounds; + } + + switch (axis) { + case Axis.vertical: + return Rect.fromLTRB( + semanticBounds.left, + semanticBounds.top - _calculatedCacheExtent!, + semanticBounds.right, + semanticBounds.bottom + _calculatedCacheExtent!, + ); + default: + return Rect.fromLTRB( + semanticBounds.left - _calculatedCacheExtent!, + semanticBounds.top, + semanticBounds.right + _calculatedCacheExtent!, + semanticBounds.bottom, + ); + } + } + + @override + void performLayout() { + if (center == null) { + assert(firstChild == null, 'firstChild cannot be null'); + _minScrollExtent = 0.0; + _maxScrollExtent = 0.0; + _hasVisualOverflow = false; + offset.applyContentDimensions(0, 0); + return; + } + assert(center!.parent == this, 'center.parent cannot be equal to this'); + + late double mainAxisExtent; + late double crossAxisExtent; + switch (axis) { + case Axis.vertical: + mainAxisExtent = size.height; + crossAxisExtent = size.width; + break; + case Axis.horizontal: + mainAxisExtent = size.width; + crossAxisExtent = size.height; + break; + } + + final centerOffsetAdjustment = center!.centerOffsetAdjustment; + + double correction; + var count = 0; + do { + correction = _attemptLayout( + mainAxisExtent, + crossAxisExtent, + offset.pixels + centerOffsetAdjustment, + ); + if (correction != 0.0) { + offset.correctBy(correction); + } else { + // *** 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); + if (offset.applyContentDimensions(minScrollOffset, maxScrollOffset)) { + break; + } + // *** End of difference from [RenderViewport]. + } + count += 1; + } while (count < _maxLayoutCycles); + assert(() { + if (count >= _maxLayoutCycles) { + assert(count != 1, 'count not equal to 1'); + throw FlutterError( + 'A RenderViewport exceeded its maximum number of layout cycles.\n' + 'RenderViewport render objects, during layout, can retry if either their ' + 'slivers or their ViewportOffset decide that the offset should be corrected ' + 'to take into account information collected during that layout.\n' + 'In the case of this RenderViewport object, however, this happened $count ' + 'times and still there was no consensus on the scroll offset. This usually ' + 'indicates a bug. Specifically, it means that one of the following three ' + 'problems is being experienced by the RenderViewport object:\n' + ' * One of the RenderSliver children or the ViewportOffset have a bug such' + ' that they always think that they need to correct the offset regardless.\n' + ' * Some combination of the RenderSliver children and the ViewportOffset' + ' have a bad interaction such that one applies a correction then another' + ' applies a reverse correction, leading to an infinite loop of corrections.\n' + ' * There is a pathological case that would eventually resolve, but it is' + ' so complicated that it cannot be resolved in any reasonable number of' + ' layout passes.', + ); + } + return true; + }(), 'count needs to be bigger than _maxLayoutCycles'); + } + + double _attemptLayout( + double mainAxisExtent, + 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'); + _minScrollExtent = 0.0; + _maxScrollExtent = 0.0; + _hasVisualOverflow = false; + + // 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; + } +} diff --git a/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart index 46786f88..fc0fb586 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart @@ -142,7 +142,9 @@ class GiphyAttachment extends AttachmentWidget { }); }, child: Text( - context.translations.cancelLabel.toLowerCase(), + context.translations.cancelLabel + .toLowerCase() + .capitalize(), style: StreamChatTheme.of(context) .textTheme .bodyBold diff --git a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart index 95229418..41b8d039 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart @@ -72,7 +72,11 @@ class ImageAttachment extends AttachmentWidget { } var imageUri = Uri.parse(imageUrl); - if (imageUri.host == 'stream-io-cdn.com') { + if (imageUri.host.endsWith('stream-io-cdn.com') && + imageUri.queryParameters['h'] == '*' && + imageUri.queryParameters['w'] == '*' && + imageUri.queryParameters['crop'] == '*' && + imageUri.queryParameters['resize'] == '*') { imageUri = imageUri.replace(queryParameters: { ...imageUri.queryParameters, 'h': '400', @@ -80,7 +84,7 @@ class ImageAttachment extends AttachmentWidget { 'crop': 'center', 'resize': 'crop', }); - } else if (imageUri.host == 'stream-cloud-uploads.imgix.net') { + } else if (imageUri.host.endsWith('stream-cloud-uploads.imgix.net')) { imageUri = imageUri.replace(queryParameters: { ...imageUri.queryParameters, 'height': '400', @@ -93,7 +97,7 @@ class ImageAttachment extends AttachmentWidget { return _buildImageAttachment( context, CachedNetworkImage( - cacheKey: imageUrl, + cacheKey: imageUri.replace(queryParameters: {}).toString(), height: size?.height, width: size?.width, placeholder: (context, __) { diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index bc903a1c..5424a1c2 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -6,8 +6,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; -import 'package:rxdart/rxdart.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; +import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/info_tile.dart'; import 'package:stream_chat_flutter/src/message_widget.dart'; @@ -145,7 +144,8 @@ class MessageListView extends StatefulWidget { this.threadBuilder, this.onThreadTap, this.dateDividerBuilder, - this.scrollPhysics = const ClampingScrollPhysics(), + this.scrollPhysics = + const ClampingScrollPhysics(), // we need to use ClampingScrollPhysics to avoid the list view to animate and break while loading this.initialScrollIndex, this.initialAlignment, this.scrollController, @@ -225,7 +225,7 @@ class MessageListView extends StatefulWidget { final ItemPositionsListener? itemPositionListener; /// The ScrollPhysics used by the ListView - final ScrollPhysics scrollPhysics; + final ScrollPhysics? scrollPhysics; /// Called when message item gets swiped final OnMessageSwiped? onMessageSwiped; @@ -296,7 +296,7 @@ class MessageListView extends StatefulWidget { class _MessageListViewState extends State { ItemScrollController? _scrollController; void Function(Message)? _onThreadTap; - bool _showScrollToBottom = false; + final ValueNotifier _showScrollToBottom = ValueNotifier(false); late final ItemPositionsListener _itemPositionListener; int? _messageListLength; StreamChannelState? streamChannel; @@ -306,12 +306,17 @@ class _MessageListViewState extends State { final initialScrollIndex = widget.initialScrollIndex; if (initialScrollIndex != null) return initialScrollIndex; if (streamChannel!.initialMessageId != null) { - final messages = streamChannel!.channel.state!.messages; + final messages = streamChannel!.channel.state!.messages + .where(widget.messageFilter ?? + defaultMessageFilter( + streamChannel!.channel.client.state.currentUser!.id, + )) + .toList(growable: false); final totalMessages = messages.length; final messageIndex = messages.indexWhere((e) => e.id == streamChannel!.initialMessageId); final index = totalMessages - messageIndex; - if (index != 0) return index - 1; + if (index != 0) return index + 1; return index; } return 0; @@ -320,7 +325,7 @@ class _MessageListViewState extends State { double get _initialAlignment { final initialAlignment = widget.initialAlignment; if (initialAlignment != null) return initialAlignment; - return 0; + return 0.1; } bool _isInitialMessage(String id) => streamChannel!.initialMessageId == id; @@ -329,7 +334,6 @@ class _MessageListViewState extends State { bool get _isThreadConversation => widget.parentMessage != null; - bool _topPaginationActive = false; bool _bottomPaginationActive = false; int initialIndex = 0; @@ -337,6 +341,8 @@ class _MessageListViewState extends State { List messages = []; + Map messagesIndex = {}; + bool initialMessageHighlightComplete = false; bool _inBetweenList = false; @@ -382,6 +388,9 @@ class _MessageListViewState extends State { Widget _buildListView(List data) { messages = data; + for (var index = 0; index < messages.length; index++) { + messagesIndex[messages[index].id] = index; + } final newMessagesListLength = messages.length; if (_messageListLength != null) { @@ -390,14 +399,13 @@ class _MessageListViewState extends State { final first = _itemPositionListener.itemPositions.value.first; final diff = newMessagesListLength - _messageListLength!; if (diff > 0) { - initialIndex = first.index + diff; - initialAlignment = first.itemLeadingEdge; + if (messages[0].user?.id != + streamChannel!.channel.client.state.currentUser?.id) { + initialIndex = first.index + diff; + initialAlignment = first.itemLeadingEdge; + } } } - } else if (!_topPaginationActive && _upToDate) { - // Reset the index in-case we send any new message - initialIndex = 0; - initialAlignment = 0; } } @@ -441,7 +449,6 @@ class _MessageListViewState extends State { onStartOfPage: () async { _inBetweenList = false; if (!_upToDate) { - _topPaginationActive = false; _bottomPaginationActive = true; return _paginateData( streamChannel, @@ -451,7 +458,6 @@ class _MessageListViewState extends State { }, onEndOfPage: () async { _inBetweenList = false; - _topPaginationActive = true; _bottomPaginationActive = false; return _paginateData( streamChannel, @@ -462,17 +468,26 @@ class _MessageListViewState extends State { _inBetweenList = true; }, child: ScrollablePositionedList.separated( - key: _upToDate - ? null - : ValueKey(initialIndex + initialAlignment), + key: (initialIndex != 0 && initialAlignment != 0) + ? ValueKey('$initialIndex-$initialAlignment') + : null, itemPositionsListener: _itemPositionListener, initialScrollIndex: initialIndex, initialAlignment: initialAlignment, physics: widget.scrollPhysics, itemScrollController: _scrollController, reverse: widget.reverse, - addAutomaticKeepAlives: false, itemCount: itemCount, + findChildIndexCallback: (Key key) { + final indexedKey = key as IndexedKey; + final valueKey = indexedKey.key as ValueKey?; + if (valueKey != null) { + final index = messagesIndex[valueKey.value]; + if (index != null) { + return ((index + 2) * 2) - 1; + } + } + }, // Item Count -> 8 (1 parent, 2 header+footer, 2 top+bottom, 3 messages) // eg: |Type| rev(|Index(item)|) rev(|Index(separator)|) |Index(item)| |Index(separator)| @@ -624,14 +639,30 @@ class _MessageListViewState extends State { } else { messageWidget = buildMessage(message, messages, i - 2); } - return messageWidget; + return KeyedSubtree( + key: ValueKey(message.id), + child: messageWidget, + ); }, ), ), ); }, ), - if (widget.showScrollToBottom) _buildScrollToBottom(), + BetterStreamBuilder( + stream: streamChannel!.channel.state!.isUpToDateStream, + initialData: streamChannel!.channel.state!.isUpToDate, + builder: (context, snapshot) => ValueListenableBuilder( + valueListenable: _showScrollToBottom, + child: _buildScrollToBottom(), + builder: (context, value, child) { + if (!snapshot || value) { + return child!; + } + return const Offstage(); + }, + ), + ), if (widget.showFloatingDateDivider) _buildFloatingDateDivider(itemCount), ], @@ -751,24 +782,15 @@ class _MessageListViewState extends State { .index; } - Widget _buildScrollToBottom() => StreamBuilder>( - stream: Rx.combineLatest2( - streamChannel!.channel.state!.isUpToDateStream.distinct(), - streamChannel!.channel.state!.unreadCountStream.distinct(), - (bool isUpToDate, int unreadCount) => Tuple2(isUpToDate, unreadCount), - ), + Widget _buildScrollToBottom() => StreamBuilder( + stream: streamChannel!.channel.state!.unreadCountStream, builder: (_, snapshot) { if (snapshot.hasError) { return const Offstage(); } else if (!snapshot.hasData) { return const Offstage(); } - final isUpToDate = snapshot.data!.item1; - final showScrollToBottom = !isUpToDate || _showScrollToBottom; - if (!showScrollToBottom) { - return const Offstage(); - } - final unreadCount = snapshot.data!.item2; + final unreadCount = snapshot.data!; final showUnreadCount = unreadCount > 0 && streamChannel!.channel.state!.members.any((e) => e.userId == @@ -783,16 +805,21 @@ class _MessageListViewState extends State { children: [ FloatingActionButton( backgroundColor: _streamTheme.colorTheme.barsBg, - onPressed: () { + onPressed: () async { if (unreadCount > 0) { streamChannel!.channel.markRead(); } if (!_upToDate) { _bottomPaginationActive = false; - _topPaginationActive = false; - streamChannel!.reloadChannel(); + initialAlignment = 0; + initialIndex = 0; + await streamChannel!.reloadChannel(); + + WidgetsBinding.instance?.addPostFrameCallback((_) { + _scrollController!.jumpTo(index: 0); + }); } else { - setState(() => _showScrollToBottom = false); + _showScrollToBottom.value = false; _scrollController!.scrollTo( index: 0, duration: const Duration(seconds: 1), @@ -854,9 +881,8 @@ class _MessageListViewState extends State { int index, ) { final messageWidget = buildMessage(message, messages, index); - return VisibilityDetector( - key: ValueKey('BOTTOM-MESSAGE-${message.id}'), + key: ValueKey('visibility: ${message.id}'), onVisibilityChanged: (visibility) { final isVisible = visibility.visibleBounds != Rect.zero; if (isVisible) { @@ -868,8 +894,8 @@ class _MessageListViewState extends State { } } if (mounted) { - if (_showScrollToBottom == isVisible) { - setState(() => _showScrollToBottom = !isVisible); + if (_showScrollToBottom.value == isVisible) { + _showScrollToBottom.value = !isVisible; } } }, @@ -948,16 +974,11 @@ class _MessageListViewState extends State { return defaultMessageWidget; } - Widget buildMessage( - Message message, - List messages, - int index, - ) { + Widget buildMessage(Message message, List messages, int index) { if ((message.type == 'system' || message.type == 'error') && message.text?.isNotEmpty == true) { return widget.systemMessageBuilder?.call(context, message) ?? SystemMessage( - key: ValueKey('MESSAGE-${message.id}'), message: message, onMessageTap: (message) { if (widget.onSystemMessageTap != null) { @@ -1037,7 +1058,6 @@ class _MessageListViewState extends State { members.firstWhereOrNull((e) => e.user!.id == currentUser!.id); Widget messageWidget = MessageWidget( - key: ValueKey('MESSAGE-${message.id}'), message: message, reverse: isMyMessage, showReactions: !message.isDeleted, @@ -1049,23 +1069,20 @@ class _MessageListViewState extends State { showSendingIndicator: showSendingIndicator, showUserAvatar: showUserAvatar, onQuotedMessageTap: (quotedMessageId) async { - // ignore: prefer_function_declarations_over_variables - final scrollToIndex = () { + if (messages.map((e) => e.id).contains(quotedMessageId)) { final index = messages.indexWhere((m) => m.id == quotedMessageId); _scrollController?.scrollTo( - index: index, - duration: const Duration(milliseconds: 350), + index: index + 2, // +2 to account for loader and footer + duration: const Duration(seconds: 1), + curve: Curves.easeInOut, + alignment: 0.1, ); - }; - if (messages.map((e) => e.id).contains(quotedMessageId)) { - scrollToIndex(); } else { - await streamChannel!.loadChannelAtMessage(quotedMessageId).then((_) { - WidgetsBinding.instance!.addPostFrameCallback((_) { - if (messages.map((e) => e.id).contains(quotedMessageId)) { - scrollToIndex(); - } - }); + await streamChannel! + .loadChannelAtMessage(quotedMessageId) + .then((_) async { + initialIndex = 21; // 19 + 2 | 19 is the index of the message + initialAlignment = 0.1; }); } }, @@ -1198,7 +1215,7 @@ class _MessageListViewState extends State { child: child, ), child: Padding( - padding: const EdgeInsets.only(top: 4), + padding: const EdgeInsets.symmetric(vertical: 4), child: child, ), ); @@ -1229,27 +1246,25 @@ class _MessageListViewState extends State { initialIndex = _initialIndex; initialAlignment = _initialAlignment; - WidgetsBinding.instance!.addPostFrameCallback((timeStamp) { - if (_scrollController?.isAttached == true) { - _scrollController?.jumpTo( - index: initialIndex, - alignment: initialAlignment, - ); - } - }); + if (_scrollController?.isAttached == true) { + _scrollController?.jumpTo( + index: initialIndex, + alignment: initialAlignment, + ); + } _messageNewListener = streamChannel!.channel.on(EventType.messageNew).listen((event) { if (_upToDate) { _bottomPaginationActive = false; - _topPaginationActive = false; } if (event.message?.parentId == widget.parentMessage?.id && event.message!.user!.id == streamChannel!.channel.client.state.currentUser!.id) { WidgetsBinding.instance!.addPostFrameCallback((_) { - _scrollController?.jumpTo( + _scrollController?.scrollTo( index: 0, + duration: const Duration(seconds: 1), ); }); } diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index 5ba78a26..f6ae52c6 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -608,6 +608,8 @@ class _MessageWidgetState extends State final bottomRowPadding = widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 0.5; + final showReactions = _shouldShowReactions; + return Material( type: widget.message.pinned && widget.showPinHighlight ? MaterialType.card @@ -671,17 +673,23 @@ class _MessageWidgetState extends State SizedBox(width: avatarWidth + 4), Flexible( child: PortalEntry( - portal: Container( - transform: Matrix4.translationValues( - widget.reverse ? 12 : -12, - 0, - 0, - ), - constraints: const BoxConstraints( - maxWidth: 22 * 6.0, - ), - child: _buildReactionIndicator(context), - ), + visible: showReactions, + portal: showReactions + ? Container( + transform: + Matrix4.translationValues( + widget.reverse ? 12 : -12, + 0, + 0, + ), + constraints: const BoxConstraints( + maxWidth: 22 * 6.0, + ), + child: _buildReactionIndicator( + context, + ), + ) + : null, portalAnchor: Alignment(widget.reverse ? 1 : -1, -1), childAnchor: @@ -1036,9 +1044,7 @@ class _MessageWidgetState extends State return AnimatedSwitcher( duration: const Duration(milliseconds: 300), - child: (widget.showReactions && - (widget.message.reactionCounts?.isNotEmpty == true) && - !widget.message.isDeleted) + child: _shouldShowReactions ? GestureDetector( onTap: () => _showMessageReactionsModalBottomSheet(context), child: ReactionBubble( @@ -1058,6 +1064,11 @@ class _MessageWidgetState extends State ); } + bool get _shouldShowReactions => + widget.showReactions && + (widget.message.reactionCounts?.isNotEmpty == true) && + !widget.message.isDeleted; + void _showMessageActionModalBottomSheet(BuildContext context) { final channel = StreamChannel.of(context).channel; diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index a36baba9..d89b4ccd 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: 3.1.1 +version: 3.2.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -23,7 +23,7 @@ dependencies: flutter_markdown: ^0.6.1 flutter_portal: ^0.4.0 flutter_slidable: ^0.6.0 - flutter_svg: ^0.22.0 + flutter_svg: ^0.23.0+1 http_parser: ^4.0.0 image_gallery_saver: ^1.7.0 image_picker: ^0.8.2 @@ -32,12 +32,11 @@ dependencies: meta: ^1.3.0 path_provider: ^2.0.1 photo_manager: ^1.2.6+1 - photo_view: ^0.12.0 + photo_view: ^0.13.0 rxdart: ^0.27.0 - scrollable_positioned_list: ^0.2.0-nullsafety.0 - share_plus: ^2.0.3 + share_plus: ^3.0.4 shimmer: ^2.0.0 - stream_chat_flutter_core: ^3.1.1 + stream_chat_flutter_core: ^3.2.0 substring_highlight: ^1.0.26 synchronized: ^3.0.0 url_launcher: ^6.0.3 @@ -58,6 +57,6 @@ dev_dependencies: dart_code_metrics: ^4.4.0 flutter_test: sdk: flutter - golden_toolkit: ^0.10.0 - mocktail: ^0.1.2 + golden_toolkit: ^0.11.0 + mocktail: ^0.2.0 path: ^1.8.0 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 new file mode 100644 index 00000000..d32f3969 --- /dev/null +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/horizontal_scrollable_positioned_list_test.dart @@ -0,0 +1,298 @@ +// 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 'dart:async'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart'; + +const screenHeight = 400.0; +const screenWidth = 400.0; +const itemWidth = screenWidth / 10.0; +const itemCount = 500; +const scrollDuration = Duration(seconds: 1); + +void main() { + Future setUpWidgetTest( + WidgetTester tester, { + ItemScrollController? itemScrollController, + ItemPositionsListener? itemPositionsListener, + bool reverse = false, + EdgeInsets? padding, + int initialScrollIndex = 0, + }) async { + tester.binding.window.devicePixelRatioTestValue = 1.0; + tester.binding.window.physicalSizeTestValue = + const Size(screenWidth, screenHeight); + + await tester.pumpWidget( + MaterialApp( + home: ScrollablePositionedList.builder( + itemCount: itemCount, + itemScrollController: itemScrollController, + itemBuilder: (context, index) => SizedBox( + width: itemWidth, + child: Text('Item $index'), + ), + itemPositionsListener: itemPositionsListener, + scrollDirection: Axis.horizontal, + reverse: reverse, + padding: padding, + initialScrollIndex: initialScrollIndex, + ), + ), + ); + } + + testWidgets('List positioned with 0 at left', (WidgetTester tester) async { + final itemPositionsListener = ItemPositionsListener.create(); + await setUpWidgetTest(tester, itemPositionsListener: itemPositionsListener); + + expect(tester.getTopLeft(find.text('Item 0')).dx, 0); + expect(tester.getBottomRight(find.text('Item 9')).dx, screenWidth); + 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 == 0) + .itemTrailingEdge, + 1 / 10); + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 9) + .itemTrailingEdge, + 1); + }); + + testWidgets('List positioned with 0 at right', (WidgetTester tester) async { + final itemPositionsListener = ItemPositionsListener.create(); + await setUpWidgetTest(tester, + itemPositionsListener: itemPositionsListener, reverse: true); + + expect(tester.getBottomRight(find.text('Item 0')).dx, screenWidth); + expect(tester.getTopLeft(find.text('Item 9')).dx, 0); + 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 == 0) + .itemTrailingEdge, + 1 / 10); + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 9) + .itemTrailingEdge, + 1); + }); + + testWidgets('Scroll to 2 (already on screen)', (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + final itemPositionsListener = ItemPositionsListener.create(); + await setUpWidgetTest(tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener); + + 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')).dx, 0); + + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 2) + .itemLeadingEdge, + 0); + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 2) + .itemTrailingEdge, + 1 / 10); + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 11) + .itemTrailingEdge, + 1); + }); + + testWidgets('Scroll to 100 (not already on screen)', + (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 == 100) + .itemTrailingEdge, + 1 / 10); + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 109) + .itemTrailingEdge, + 1); + }); + + testWidgets('Jump to 100', (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')).dx, 0); + expect(tester.getBottomRight(find.text('Item 109')).dy, screenWidth); + + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 100) + .itemLeadingEdge, + 0); + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 100) + .itemTrailingEdge, + 1 / 10); + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 109) + .itemLeadingEdge, + 9 / 10); + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 109) + .itemTrailingEdge, + 1); + }); + + testWidgets('padding test - centered sliver at left', + (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(itemWidth + 10, 10)); + expect(tester.getBottomRight(find.text('Item 1')), + const Offset(10 + itemWidth * 2, screenHeight - 10)); + + unawaited( + itemScrollController.scrollTo(index: 490, duration: scrollDuration)); + await tester.pumpAndSettle(); + + await tester.drag( + find.byType(ScrollablePositionedList), const Offset(-100, 0)); + await tester.pumpAndSettle(); + + expect(tester.getBottomRight(find.text('Item 499')), + const Offset(screenWidth - 10, screenHeight - 10)); + }); + + testWidgets('padding test - centered sliver not at left', + (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + initialScrollIndex: 2, + padding: const EdgeInsets.all(10), + ); + + await tester.drag( + find.byType(ScrollablePositionedList), const Offset(200, 0)); + await tester.pumpAndSettle(); + + expect(tester.getTopLeft(find.text('Item 0')), const Offset(10, 10)); + expect(tester.getTopLeft(find.text('Item 2')), + const Offset(10 + itemWidth * 2, 10)); + expect(tester.getTopLeft(find.text('Item 3')), + const Offset(10 + itemWidth * 3, 10)); + }); + + testWidgets('padding test - reversed - centered sliver at right', + (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + padding: const EdgeInsets.all(10), + reverse: true, + ); + + expect(tester.getTopRight(find.text('Item 0')), + const Offset(screenWidth - 10, 10)); + expect(tester.getTopRight(find.text('Item 1')), + const Offset(screenWidth - (itemWidth + 10), 10)); + expect(tester.getBottomLeft(find.text('Item 1')), + const Offset(screenWidth - (10 + itemWidth * 2), screenHeight - 10)); + + unawaited( + itemScrollController.scrollTo(index: 490, duration: scrollDuration)); + await tester.pumpAndSettle(); + + await tester.drag( + find.byType(ScrollablePositionedList), const Offset(100, 0)); + await tester.pumpAndSettle(); + + expect(tester.getTopLeft(find.text('Item 499')), const Offset(10, 10)); + }); + + testWidgets('padding test - reversed - centered sliver not at right', + (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + initialScrollIndex: 2, + padding: const EdgeInsets.all(10), + reverse: true, + ); + + await tester.drag( + find.byType(ScrollablePositionedList), const Offset(-200, 0)); + await tester.pumpAndSettle(); + + expect(tester.getTopRight(find.text('Item 0')), + const Offset(screenWidth - 10, 10)); + expect(tester.getTopRight(find.text('Item 2')), + const Offset(screenWidth - (10 + itemWidth * 2), 10)); + expect(tester.getTopRight(find.text('Item 3')), + const Offset(screenWidth - (10 + itemWidth * 3), 10)); + }); +} 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 new file mode 100644 index 00000000..9888249c --- /dev/null +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/positioned_list_test.dart @@ -0,0 +1,363 @@ +// 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; +const cacheExtent = itemHeight * 2; + +void main() { + final itemPositionsNotifier = ItemPositionsListener.create(); + + Future setUpWidgetTest( + WidgetTester tester, { + int topItem = 0, + ScrollController? scrollController, + double anchor = 0, + int itemCount = defaultItemCount, + }) async { + tester.binding.window.devicePixelRatioTestValue = 1.0; + tester.binding.window.physicalSizeTestValue = + const Size(screenWidth, screenHeight); + + await tester.pumpWidget( + MaterialApp( + home: PositionedList( + itemCount: itemCount, + positionedIndex: topItem, + alignment: anchor, + controller: scrollController, + itemBuilder: (context, index) => SizedBox( + height: itemHeight, + child: Text('Item $index'), + ), + itemPositionsNotifier: itemPositionsNotifier as ItemPositionsNotifier, + cacheExtent: cacheExtent, + ), + ), + ); + } + + testWidgets('short list', (WidgetTester tester) async { + await setUpWidgetTest(tester, itemCount: 5); + await tester.pump(); + + expect(find.text('Item 0'), findsOneWidget); + expect(find.text('Item 4'), findsOneWidget); + expect(find.text('Item 5'), findsNothing); + + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 0) + .itemLeadingEdge, + 0); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 4) + .itemTrailingEdge, + 1 / 2); + }); + + testWidgets('List positioned with 0 at top', (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', (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', (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', + (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', + (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', + (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', + (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', + (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', + (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', + (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', + (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); + }); +} diff --git a/packages/stream_chat_flutter/test/scrollable_positioned_list/reversed_positioned_list_test.dart b/packages/stream_chat_flutter/test/scrollable_positioned_list/reversed_positioned_list_test.dart new file mode 100644 index 00000000..272e2459 --- /dev/null +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/reversed_positioned_list_test.dart @@ -0,0 +1,270 @@ +// 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, + ScrollController? scrollController, + double anchor = 0, + int itemCount = defaultItemCount, + }) async { + tester.binding.window.devicePixelRatioTestValue = 1.0; + tester.binding.window.physicalSizeTestValue = + const Size(screenWidth, screenHeight); + + await tester.pumpWidget( + MaterialApp( + home: PositionedList( + itemCount: itemCount, + positionedIndex: topItem, + alignment: anchor, + controller: scrollController, + itemBuilder: (context, index) => SizedBox( + height: itemHeight, + child: Text('Item $index'), + ), + itemPositionsNotifier: itemPositionsNotifier as ItemPositionsNotifier, + reverse: true, + ), + ), + ); + } + + testWidgets('short list', (WidgetTester tester) async { + await setUpWidgetTest(tester, itemCount: 5); + await tester.pump(); + + expect(tester.getBottomRight(find.text('Item 0')).dy, screenHeight); + expect(find.text('Item 4'), findsOneWidget); + expect(find.text('Item 5'), findsNothing); + + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 0) + .itemLeadingEdge, + 0); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 4) + .itemTrailingEdge, + 1 / 2); + }); + + testWidgets('List positioned with 0 at bottom', (WidgetTester tester) async { + await setUpWidgetTest(tester); + await tester.pump(); + + expect(tester.getBottomRight(find.text('Item 0')).dy, screenHeight); + expect(tester.getTopLeft(find.text('Item 9')).dy, 0); + 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); + }); + + testWidgets('List positioned with 5 at bottom', (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 15 at bottom', (WidgetTester tester) async { + await setUpWidgetTest(tester, topItem: 15); + await tester.pump(); + + expect(find.text('Item 14'), findsNothing); + expect(find.text('Item 15'), findsOneWidget); + expect(find.text('Item 24'), findsOneWidget); + expect(find.text('Item 25'), findsNothing); + }); + + testWidgets('List positioned with 15 at top', (WidgetTester tester) async { + await setUpWidgetTest(tester, topItem: 15, anchor: 1); + await tester.pump(); + + expect(find.text('Item 15'), findsNothing); + expect(find.text('Item 14'), findsOneWidget); + expect(find.text('Item 5'), findsOneWidget); + expect(find.text('Item 4'), findsNothing); + + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 15) + .itemLeadingEdge, + 1); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 14) + .itemTrailingEdge, + 1); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 14) + .itemLeadingEdge, + 9 / 10); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 5) + .itemLeadingEdge, + 0); + }); + + testWidgets('List positioned with 5 at bottom then scroll up 2', + (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 6'), findsNothing); + expect(find.text('Item 7'), findsOneWidget); + + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 7) + .itemLeadingEdge, + 0); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 7) + .itemTrailingEdge, + 1 / 10); + }); + + testWidgets('List positioned with 0 at bottom scroll to item 5', + (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 bottom then scroll up 2 programatically', + (WidgetTester tester) async { + final scrollController = ScrollController(); + await setUpWidgetTest(tester, + topItem: 5, scrollController: scrollController); + + scrollController.jumpTo(itemHeight * 2); + await tester.pump(); + + expect(find.text('Item 6'), findsNothing); + expect(find.text('Item 7'), findsOneWidget); + expect(find.text('Item 16'), findsOneWidget); + expect(find.text('Item 17'), findsNothing); + + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 6) + .itemLeadingEdge, + -1 / 10); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 7) + .itemLeadingEdge, + 0); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 16) + .itemTrailingEdge, + 1); + }); + + testWidgets('List positioned with 5 at bottom and initial scroll offset', + (WidgetTester tester) async { + final scrollController = + ScrollController(initialScrollOffset: itemHeight * 2); + await setUpWidgetTest(tester, + topItem: 5, scrollController: scrollController); + + expect(find.text('Item 6'), findsNothing); + expect(find.text('Item 7'), findsOneWidget); + expect(find.text('Item 16'), findsOneWidget); + expect(find.text('Item 17'), findsNothing); + + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 6) + .itemLeadingEdge, + -1 / 10); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 7) + .itemLeadingEdge, + 0); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 16) + .itemTrailingEdge, + 1); + }); +} 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 new file mode 100644 index 00000000..b0c487ea --- /dev/null +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/reversed_scrollable_positioned_list_test.dart @@ -0,0 +1,239 @@ +// 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 'dart:async'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.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( + home: ScrollablePositionedList.builder( + itemCount: itemCount, + initialScrollIndex: initialIndex, + itemScrollController: itemScrollController, + itemBuilder: (context, index) => SizedBox( + height: itemHeight, + child: Text('Item $index'), + ), + itemPositionsListener: itemPositionsListener, + reverse: true, + padding: padding, + ), + ), + ); + } + + testWidgets('List positioned with 0 at bottom', (WidgetTester tester) async { + final itemPositionsListener = ItemPositionsListener.create(); + await setUpWidgetTest(tester, itemPositionsListener: itemPositionsListener); + + expect(tester.getBottomRight(find.text('Item 0')).dy, screenHeight); + expect(tester.getTopLeft(find.text('Item 9')).dy, 0); + 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)', + (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.getBottomRight(find.text('Item 1')).dy, screenHeight); + + unawaited( + itemScrollController.scrollTo(index: 2, duration: scrollDuration)); + await tester.pump(); + await tester.pump(scrollDuration); + + expect(find.text('Item 1'), findsNothing); + expect(tester.getBottomRight(find.text('Item 2')).dy, screenHeight); + + 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', + (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)', + (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', (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.getBottomRight(find.text('Item 100')).dy, screenHeight); + expect(tester.getTopLeft(find.text('Item 109')).dy, 0); + + 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', + (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + padding: const EdgeInsets.all(10), + ); + + expect(tester.getBottomLeft(find.text('Item 0')), + const Offset(10, screenHeight - 10)); + expect(tester.getBottomLeft(find.text('Item 1')), + const Offset(10, screenHeight - (itemHeight + 10))); + expect(tester.getTopRight(find.text('Item 1')), + const Offset(screenWidth - 10, screenHeight - (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, 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.getBottomLeft(find.text('Item 0')), + const Offset(10, screenHeight - 10)); + expect(tester.getBottomLeft(find.text('Item 2')), + const Offset(10, screenHeight - (10 + itemHeight * 2))); + expect(tester.getBottomLeft(find.text('Item 3')), + const Offset(10, screenHeight - (10 + itemHeight * 3))); + }); +} 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 new file mode 100644 index 00000000..5c4f22f1 --- /dev/null +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/scrollable_positioned_list_test.dart @@ -0,0 +1,2274 @@ +// 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 'dart:async'; +import 'dart:math'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.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/scroll_view.dart'; + +const screenHeight = 400.0; +const screenWidth = 400.0; +const itemHeight = screenHeight / 10.0; +const defaultItemCount = 500; +const scrollDuration = Duration(seconds: 1); +const scrollDurationTolerance = Duration(milliseconds: 1); +const tolerance = 1e-3; + +void main() { + Future setUpWidgetTest( + WidgetTester tester, { + Key? key, + ItemScrollController? itemScrollController, + ItemPositionsListener? itemPositionsListener, + int initialIndex = 0, + double initialAlignment = 0.0, + int itemCount = defaultItemCount, + ScrollPhysics? physics, + bool addSemanticIndexes = true, + int? semanticChildCount, + EdgeInsets? padding, + bool addRepaintBoundaries = true, + bool addAutomaticKeepAlives = true, + double? minCacheExtent, + bool variableHeight = false, + }) async { + tester.binding.window.devicePixelRatioTestValue = 1.0; + tester.binding.window.physicalSizeTestValue = + const Size(screenWidth, screenHeight); + + await tester.pumpWidget( + MaterialApp( + home: ScrollablePositionedList.builder( + key: key, + 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'''); + return SizedBox( + height: + variableHeight ? (itemHeight + (index % 13) * 5) : itemHeight, + child: Text('Item $index'), + ); + }, + itemPositionsListener: itemPositionsListener, + initialScrollIndex: initialIndex, + initialAlignment: initialAlignment, + physics: physics, + addSemanticIndexes: addSemanticIndexes, + semanticChildCount: semanticChildCount, + padding: padding, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + minCacheExtent: minCacheExtent, + ), + ), + ); + } + + testWidgets('List positioned with 0 at top', (WidgetTester tester) async { + final itemPositionsListener = ItemPositionsListener.create(); + await setUpWidgetTest(tester, itemPositionsListener: itemPositionsListener); + + 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); + expect( + itemPositionsListener.itemPositions.value + .where((position) => position.index == 10), + isEmpty); + }); + + testWidgets('List positioned with 0 at top - use default values', + (WidgetTester tester) async { + final itemPositionsListener = ItemPositionsListener.create(); + tester.binding.window.devicePixelRatioTestValue = 1.0; + tester.binding.window.physicalSizeTestValue = + const Size(screenWidth, screenHeight); + + await tester.pumpWidget( + MaterialApp( + home: ScrollablePositionedList.builder( + itemCount: defaultItemCount, + itemBuilder: (context, index) => SizedBox( + height: itemHeight, + child: Text('Item $index'), + ), + itemPositionsListener: itemPositionsListener, + ), + ), + ); + + 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('List positioned with 5 at top', (WidgetTester tester) async { + final itemPositionsListener = ItemPositionsListener.create(); + await setUpWidgetTest(tester, + itemPositionsListener: itemPositionsListener, initialIndex: 5); + + expect(find.text('Item 4'), findsNothing); + expect(find.text('Item 5'), findsOneWidget); + + expect( + itemPositionsListener.itemPositions.value + .where((position) => position.index == 4), + isEmpty); + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 5) + .itemLeadingEdge, + 0); + }); + + testWidgets('List positioned with 9 at middle', (WidgetTester tester) async { + final itemPositionsListener = ItemPositionsListener.create(); + await setUpWidgetTest(tester, + itemPositionsListener: itemPositionsListener, + initialIndex: 9, + initialAlignment: 0.5); + + expect(tester.getTopLeft(find.text('Item 9')).dy, screenHeight / 2); + + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 9) + .itemLeadingEdge, + 0.5); + }); + + testWidgets('List positioned with 9 half way off top', + (WidgetTester tester) async { + final itemPositionsListener = ItemPositionsListener.create(); + await setUpWidgetTest(tester, + itemPositionsListener: itemPositionsListener, + initialIndex: 9, + initialAlignment: -(itemHeight / screenHeight) / 2); + + expect(tester.getTopLeft(find.text('Item 9')).dy, -itemHeight / 2); + + 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('Scroll to 9 half way off top', (WidgetTester tester) async { + final itemPositionsListener = ItemPositionsListener.create(); + final itemScrollController = ItemScrollController(); + expect(itemScrollController.isAttached, false); + await setUpWidgetTest(tester, + itemPositionsListener: itemPositionsListener, + itemScrollController: itemScrollController); + expect(itemScrollController.isAttached, true); + + unawaited(itemScrollController.scrollTo( + index: 9, + duration: scrollDuration, + alignment: -(itemHeight / screenHeight) / 2)); + await tester.pump(); + await tester.pump(); + await tester.pump(scrollDuration + scrollDurationTolerance); + + expect(tester.getTopLeft(find.text('Item 9')).dy, -itemHeight / 2); + + 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('Jump to 9 half way off top', (WidgetTester tester) async { + final itemPositionsListener = ItemPositionsListener.create(); + final itemScrollController = ItemScrollController(); + await setUpWidgetTest(tester, + itemPositionsListener: itemPositionsListener, + itemScrollController: itemScrollController); + + itemScrollController.jumpTo( + index: 9, alignment: -(itemHeight / screenHeight) / 2); + await tester.pump(); + + expect(tester.getTopLeft(find.text('Item 9')).dy, -itemHeight / 2); + + 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 positioned with 9 at middle scroll to 15 at bottom', + (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + final itemPositionsListener = ItemPositionsListener.create(); + await setUpWidgetTest(tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + initialIndex: 9, + initialAlignment: 0.5); + + unawaited(itemScrollController.scrollTo( + index: 16, duration: scrollDuration, alignment: 1)); + await tester.pump(); + await tester.pump(); + await tester.pump(scrollDuration + scrollDurationTolerance); + + expect(tester.getBottomRight(find.text('Item 15')).dy, screenHeight); + + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 15) + .itemTrailingEdge, + 1.0); + }); + + testWidgets('Scroll to 1 (already on screen)', (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 ~/ 2); + expect(find.text('Item 0'), findsOneWidget); + expect(find.text('Item 1'), findsOneWidget); + expect(find.text('Item 10'), findsOneWidget); + + await tester.pump(scrollDuration ~/ 2); + expect(find.text('Item 0'), findsNothing); + expect(find.text('Item 1'), findsOneWidget); + expect(find.text('Item 10'), findsOneWidget); + + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 1) + .itemLeadingEdge, + 0); + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 10) + .itemTrailingEdge, + 1); + expect( + itemPositionsListener.itemPositions.value + .where((position) => position.index == 11), + isEmpty); + }); + + testWidgets('Scroll to 1 then 2 (both already on screen)', + (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(find.text('Item 1'), findsOneWidget); + + unawaited( + itemScrollController.scrollTo(index: 2, duration: scrollDuration)); + await tester.pump(); + await tester.pump(scrollDuration); + + expect(find.text('Item 1'), findsNothing); + expect(find.text('Item 2'), findsOneWidget); + + expect( + itemPositionsListener.itemPositions.value + .where((position) => position.index == 1), + isEmpty); + 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', + (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.pump(); + await tester.pump(); + await tester.pump(scrollDuration + scrollDurationTolerance); + + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 5) + .itemLeadingEdge, + 0); + + unawaited( + itemScrollController.scrollTo(index: 0, duration: scrollDuration)); + await tester.pump(); + await tester.pump(); + await tester.pump(scrollDuration + scrollDurationTolerance); + + 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 20 without fading', (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + final itemPositionsListener = ItemPositionsListener.create(); + await setUpWidgetTest(tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener); + + var fadeTransition = tester.widget(find + .descendant( + of: find.byType(ScrollablePositionedList), + matching: find.byType(FadeTransition)) + .last); + 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(find + .descendant( + of: find.byType(ScrollablePositionedList), + matching: find.byType(FadeTransition)) + .last); + expect(fadeTransition.opacity, initialOpacity); + + await tester.pumpAndSettle(); + + expect(find.text('Item 14'), findsNothing); + expect(find.text('Item 20'), findsOneWidget); + }); + + testWidgets('Scroll to 100 (not already on screen)', + (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.pump(); + await tester.pump(); + await tester.pump(scrollDuration + scrollDurationTolerance); + + 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); + + await tester.pumpAndSettle(); + }); + + testWidgets('Scroll to 100 (not already on screen) front scroll view', + (WidgetTester tester) async { + 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(); + await tester.pump(); + expect(fadeTransitionFinder.evaluate().length, 2); + expect( + tester.widget(fadeTransitionFinder.last).opacity.value, + closeTo(0, 0.01), + ); + + await tester.pump(scrollDuration ~/ 2); + + expect(tester.getTopLeft(find.text('Item 10')).dy, 0); + expect(tester.getBottomLeft(find.text('Item 19')).dy, screenHeight); + expect(fadeTransitionFinder.evaluate().length, 2); + expect( + tester.widget(fadeTransitionFinder.last).opacity.value, + closeTo(0.5, 0.01), + ); + + await tester.pump(scrollDuration ~/ 2 - scrollDurationTolerance); + expect(fadeTransitionFinder.evaluate().length, 2); + expect( + tester.widget(fadeTransitionFinder.last).opacity.value, + closeTo(1, 0.01), + ); + + await tester.pumpAndSettle(); + + expect(fadeTransitionFinder.evaluate().length, 1); + expect( + tester.widget(fadeTransitionFinder.last).opacity.value, + closeTo(1, 0.01), + ); + }); + + testWidgets('Scroll to 100 (not already on screen) back scroll view', + (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); + + expect(tester.getBottomLeft(find.text('Item 99')).dy, screenHeight); + + await tester.pumpAndSettle(); + expect(find.text('Item 25', skipOffstage: false), findsNothing); + }); + + testWidgets('Scroll to 100 (not already on screen) then back to 0', + (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.pump(); + await tester.pump(); + await tester.pump(scrollDuration + scrollDurationTolerance); + expect(find.text('Item 0'), findsNothing); + + unawaited( + itemScrollController.scrollTo(index: 0, duration: scrollDuration)); + 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)); + 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)); + + expect(find.text('Item 0'), findsOneWidget); + expect(tester.getTopLeft(find.text('Item 0')).dy, 0); + + 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 then back to 0 back scroll view', + (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + await setUpWidgetTest(tester, itemScrollController: itemScrollController); + + unawaited( + itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + await tester.pumpAndSettle(); + + unawaited( + itemScrollController.scrollTo(index: 0, duration: scrollDuration)); + await tester.pump(); + await tester.pump(); + await tester.pump(scrollDuration ~/ 2); + + expect(tester.getTopLeft(find.text('Item 90')).dy, 0); + expect(tester.getBottomLeft(find.text('Item 99')).dy, screenHeight); + + await tester.pumpAndSettle(); + }); + + testWidgets('Scroll to 100 then back to 0 front scroll view', + (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 + scrollDurationTolerance); + + unawaited( + itemScrollController.scrollTo(index: 0, duration: scrollDuration)); + await tester.pump(); + await tester.pump(); + await tester.pump(scrollDuration ~/ 2); + + 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)); + + await tester.pumpAndSettle(); + }); + + testWidgets('Scroll 100-0-100', (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 + scrollDurationTolerance); + + unawaited( + itemScrollController.scrollTo(index: 0, duration: scrollDuration)); + await tester.pump(); + await tester.pump(); + await tester.pump(scrollDuration + scrollDurationTolerance); + + unawaited( + itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + await tester.pump(); + await tester.pump(); + await tester.pump(scrollDuration ~/ 2); + + expect(tester.getTopLeft(find.text('Item 10')).dy, 0); + expect(tester.getBottomLeft(find.text('Item 19')).dy, screenHeight); + + await tester.pumpAndSettle(); + }); + + testWidgets('Jump to 100', (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + final itemPositionsListener = ItemPositionsListener.create(); + await setUpWidgetTest(tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener); + + itemScrollController.jumpTo(index: 100); + await tester.pump(); + await tester.pump(); + await tester.pump(scrollDuration + scrollDurationTolerance); + + expect(tester.getTopLeft(find.text('Item 100')).dy, 0); + expect(tester.getBottomLeft(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('Jump to 100 and position at bottom', + (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + final itemPositionsListener = ItemPositionsListener.create(); + await setUpWidgetTest(tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener); + + itemScrollController.jumpTo(index: 100, alignment: 1); + await tester.pump(); + await tester.pump(); + await tester.pump(scrollDuration + scrollDurationTolerance); + + expect(tester.getBottomLeft(find.text('Item 99')).dy, screenHeight); + + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 99) + .itemTrailingEdge, + 1.0); + expect( + itemPositionsListener.itemPositions.value + .where((position) => position.index == 100), + isEmpty); + }); + + testWidgets('Jump to 100 and position at middle', + (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + final itemPositionsListener = ItemPositionsListener.create(); + await setUpWidgetTest(tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener); + + itemScrollController.jumpTo(index: 100, alignment: 0.5); + await tester.pump(); + await tester.pump(); + await tester.pump(scrollDuration + scrollDurationTolerance); + + expect(tester.getTopLeft(find.text('Item 100')).dy, screenHeight / 2); + + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 100) + .itemLeadingEdge, + 0.5); + }); + + testWidgets('Manually scroll a significant distance, jump to 100', + (WidgetTester tester) async { + // Test for https://github.com/google/flutter.widgets/issues/144. + final itemScrollController = ItemScrollController(); + final itemPositionsListener = ItemPositionsListener.create(); + await setUpWidgetTest(tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + variableHeight: true); + + final listFinder = find.byType(ScrollablePositionedList); + for (var i = 0; i < 5; i += 1) { + await tester.drag(listFinder, const Offset(0, -screenHeight)); + await tester.pumpAndSettle(); + } + + itemScrollController.jumpTo(index: 100); + await tester.pumpAndSettle(); + + expect(tester.getTopLeft(find.text('Item 100')).dy, 0); + }, skip: true); + + testWidgets('Scroll to 100 and position at bottom', + (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + final itemPositionsListener = ItemPositionsListener.create(); + await setUpWidgetTest(tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener); + + unawaited(itemScrollController.scrollTo( + index: 100, alignment: 1, duration: scrollDuration)); + await tester.pump(); + await tester.pump(); + await tester.pump(scrollDuration + scrollDurationTolerance); + + expect(tester.getBottomLeft(find.text('Item 99')).dy, screenHeight); + + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 99) + .itemTrailingEdge, + 1.0); + expect( + itemPositionsListener.itemPositions.value + .where((position) => position.index == 100), + isEmpty); + }); + + testWidgets('Scroll to 100 and position at middle', + (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + final itemPositionsListener = ItemPositionsListener.create(); + await setUpWidgetTest(tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener); + + unawaited(itemScrollController.scrollTo( + index: 100, alignment: 0.5, duration: scrollDuration)); + await tester.pump(); + await tester.pump(); + await tester.pump(scrollDuration + scrollDurationTolerance); + + expect(tester.getTopLeft(find.text('Item 100')).dy, screenHeight / 2); + + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 100) + .itemLeadingEdge, + 0.5); + }); + + testWidgets('Scroll to 9 and position at middle', + (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + final itemPositionsListener = ItemPositionsListener.create(); + await setUpWidgetTest(tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener); + + unawaited(itemScrollController.scrollTo( + index: 9, alignment: 0.5, duration: scrollDuration)); + await tester.pump(); + await tester.pump(); + await tester.pump(scrollDuration + scrollDurationTolerance); + + expect(tester.getTopLeft(find.text('Item 9')).dy, screenHeight / 2); + + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 9) + .itemLeadingEdge, + 0.5); + }); + + testWidgets('Scroll up a little then jump to 100', + (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + final itemPositionsListener = ItemPositionsListener.create(); + await setUpWidgetTest(tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener); + + await tester.drag( + find.byType(ScrollablePositionedList), const Offset(0, -10)); + await tester.pumpAndSettle(); + + itemScrollController.jumpTo(index: 100); + await tester.pump(); + await tester.pump(); + await tester.pump(scrollDuration + scrollDurationTolerance); + + expect(tester.getTopLeft(find.text('Item 100')).dy, 0); + expect(tester.getBottomLeft(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('Scroll to 100 Jump to 0 Scroll to 100', + (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 + scrollDurationTolerance); + + itemScrollController.jumpTo(index: 0); + await tester.pump(); + await tester.pump(); + await tester.pump(scrollDuration + scrollDurationTolerance); + + unawaited( + itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + await tester.pump(); + await tester.pump(); + await tester.pump(scrollDuration ~/ 2); + + expect(tester.getTopLeft(find.text('Item 10')).dy, 0); + expect(tester.getBottomLeft(find.text('Item 19')).dy, screenHeight); + + await tester.pumpAndSettle(); + + expect(tester.getTopLeft(find.text('Item 100')).dy, 0); + expect(tester.getBottomLeft(find.text('Item 109')).dy, screenHeight); + }); + + testWidgets('Scroll to 100 stop before half way', + (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 - scrollDuration ~/ 20); + + await tester.tap(find.byType(ScrollablePositionedList)); + 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); + expect(fadeTransition.opacity.value, 1.0); + + await tester.pumpAndSettle(); + }); + + testWidgets('Scroll to 100 stop half way', (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); + + await tester.tap(find.byType(ScrollablePositionedList)); + 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); + expect(fadeTransition.opacity.value, 1.0); + + await tester.pumpAndSettle(); + }); + + testWidgets('Scroll to 0 stop before half way', (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + await setUpWidgetTest(tester, itemScrollController: itemScrollController); + + unawaited( + itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + await tester.pumpAndSettle(); + + unawaited( + itemScrollController.scrollTo(index: 0, duration: scrollDuration)); + await tester.pump(); + await tester.pump(); + await tester.pump(scrollDuration ~/ 2 - scrollDuration ~/ 20); + + await tester.tap(find.byType(ScrollablePositionedList)); + await tester.pump(); + + expect(tester.getTopLeft(find.text('Item 91')).dy, 0); + expect(find.byType(FadeTransition), findsNWidgets(2)); + + await tester.pumpAndSettle(); + }); + + testWidgets('Scroll to 100 stop after half way', (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 + scrollDuration ~/ 20); + + expect(find.text('Item 9', skipOffstage: false), findsOneWidget); + expect(tester.getBottomLeft(find.text('Item 100')).dy, + closeTo(screenHeight, tolerance)); + + await tester.tap(find.byType(ScrollablePositionedList)); + await tester.pump(); + + expect(tester.getBottomLeft(find.text('Item 100')).dy, + closeTo(screenHeight, tolerance)); + expect(find.text('Item 9', skipOffstage: false), findsNothing); + expect(find.byType(FadeTransition), findsNWidgets(2)); + + await tester.pumpAndSettle(); + }); + + testWidgets('Scroll to 0 stop after half way', (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + await setUpWidgetTest(tester, itemScrollController: itemScrollController); + + unawaited( + itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + await tester.pumpAndSettle(); + + unawaited( + itemScrollController.scrollTo(index: 0, duration: scrollDuration)); + await tester.pump(); + await tester.pump(); + await tester.pump(scrollDuration ~/ 2 + scrollDuration ~/ 20); + + await tester.tap(find.byType(ScrollablePositionedList)); + await tester.pump(); + + expect(tester.getTopLeft(find.text('Item 9')).dy, closeTo(0, tolerance)); + final fadeTransition = tester.widget(find + .descendant( + of: find.byType(ScrollablePositionedList), + matching: find.byType(FadeTransition)) + .last); + expect(fadeTransition.opacity.value, 1.0); + + await tester.pumpAndSettle(); + }); + + testWidgets('Scroll to 0 stop half way', (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + await setUpWidgetTest(tester, itemScrollController: itemScrollController); + + unawaited( + itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + await tester.pumpAndSettle(); + + unawaited( + itemScrollController.scrollTo(index: 0, duration: scrollDuration)); + await tester.pump(); + await tester.pump(); + await tester.pump(scrollDuration ~/ 2); + + await tester.tap(find.byType(ScrollablePositionedList)); + await tester.pump(); + + expect(tester.getTopLeft(find.text('Item 90')).dy, 0); + expect(find.byType(FadeTransition), findsNWidgets(2)); + + await tester.pumpAndSettle(); + }); + + testWidgets('Scroll to 100 jump to 250 half way', + (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); + + itemScrollController.jumpTo(index: 250); + await tester.pump(); + + expect(tester.getTopLeft(find.text('Item 250')).dy, 0); + + expect(find.text('Item 100'), findsNothing); + + await tester.pumpAndSettle(); + }); + + testWidgets('Scroll to 250, scroll to 100, jump to 0 half way', + (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + await setUpWidgetTest(tester, itemScrollController: itemScrollController); + + unawaited( + itemScrollController.scrollTo(index: 250, duration: scrollDuration)); + await tester.pumpAndSettle(); + + unawaited( + itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + await tester.pump(); + await tester.pump(); + await tester.pump(scrollDuration ~/ 2); + + itemScrollController.jumpTo(index: 0); + await tester.pump(); + + expect(tester.getTopLeft(find.text('Item 0')).dy, 0); + expect(find.text('Item 100'), findsNothing); + + await tester.pumpAndSettle(); + }); + + testWidgets('Scroll to 100 scroll to 250 half way', + (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); + + unawaited( + itemScrollController.scrollTo(index: 250, duration: scrollDuration)); + + await tester.pumpAndSettle(); + expect(tester.getTopLeft(find.text('Item 250')).dy, 0); + expect(find.text('Item 100'), findsNothing); + }); + + testWidgets('Scroll to 250, scroll to 100, scroll to 0 half way', + (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + await setUpWidgetTest(tester, itemScrollController: itemScrollController); + + unawaited( + itemScrollController.scrollTo(index: 250, duration: scrollDuration)); + await tester.pumpAndSettle(); + + unawaited( + itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + await tester.pump(); + await tester.pump(); + await tester.pump(scrollDuration ~/ 2); + + unawaited( + itemScrollController.scrollTo(index: 0, duration: scrollDuration)); + await tester.pumpAndSettle(); + + expect(tester.getTopLeft(find.text('Item 0')).dy, 0); + expect(find.text('Item 100'), findsNothing); + }); + + testWidgets( + 'Scroll to 100, scroll to 200, then scroll to 300 without waiting', + (WidgetTester tester) async { + // Possibly https://github.com/google/flutter.widgets/issues/171. + final itemScrollController = ItemScrollController(); + await setUpWidgetTest(tester, itemScrollController: itemScrollController); + + unawaited( + itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited( + itemScrollController.scrollTo(index: 200, duration: scrollDuration)); + unawaited( + itemScrollController.scrollTo(index: 300, duration: scrollDuration)); + await tester.pumpAndSettle(); + + expect(find.text('Item 100'), findsNothing); + expect(find.text('Item 200'), findsNothing); + + final itemFinder = find.text('Item 300'); + expect(itemFinder, findsOneWidget); + expect(tester.getTopLeft(itemFinder).dy, 0); + }, 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); + + itemScrollController.jumpTo(index: 400, alignment: 1); + await tester.pumpAndSettle(); + + final listFinder = find.byType(ScrollablePositionedList); + + 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: 400, alignment: 1, duration: scrollDuration)); + await tester.pumpAndSettle(); + + final itemFinder = find.text('Item 399'); + expect(itemFinder, findsOneWidget); + expect(tester.getBottomLeft(itemFinder).dy, screenHeight); + }); + + testWidgets('physics', (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + await setUpWidgetTest(tester, + itemScrollController: itemScrollController, + physics: const BouncingScrollPhysics()); + + await tester.drag( + find.byType(ScrollablePositionedList), const Offset(0, 50)); + await tester.pump(const Duration(milliseconds: 200)); + + expect(tester.getTopLeft(find.text('Item 0')).dy, greaterThan(0)); + + await tester.pumpAndSettle(); + expect(tester.getTopLeft(find.text('Item 0')).dy, 0); + + unawaited( + itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + await tester.pumpAndSettle(); + itemScrollController.jumpTo(index: 0); + await tester.pumpAndSettle(); + + await tester.drag( + find.byType(ScrollablePositionedList), const Offset(0, 50)); + await tester.pump(const Duration(milliseconds: 200)); + + expect(tester.getTopLeft(find.text('Item 0')).dy, greaterThan(0)); + + await tester.pumpAndSettle(); + expect(tester.getTopLeft(find.text('Item 0')).dy, 0); + }); + + testWidgets('correct index sematics', (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + await setUpWidgetTest(tester, + itemScrollController: itemScrollController, initialIndex: 5); + + await tester.drag( + find.byType(ScrollablePositionedList), const Offset(0, itemHeight * 2)); + await tester.pumpAndSettle(); + + final indexSemantics3 = tester.widget(find.ancestor( + of: find.text('Item 3'), matching: find.byType(IndexedSemantics))); + expect(indexSemantics3.index, 3); + final indexSemantics4 = tester.widget(find.ancestor( + of: find.text('Item 4'), matching: find.byType(IndexedSemantics))); + expect(indexSemantics4.index, 4); + final indexSemantics5 = tester.widget(find.ancestor( + of: find.text('Item 5'), matching: find.byType(IndexedSemantics))); + expect(indexSemantics5.index, 5); + final indexSemantics6 = tester.widget(find.ancestor( + of: find.text('Item 6'), matching: find.byType(IndexedSemantics))); + expect(indexSemantics6.index, 6); + + unawaited( + itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + await tester.pumpAndSettle(); + itemScrollController.jumpTo(index: 0); + await tester.pumpAndSettle(); + + await tester.drag( + find.byType(ScrollablePositionedList), const Offset(0, itemHeight * 2)); + await tester.pumpAndSettle(); + + final indexSemantics3b = tester.widget(find.ancestor( + of: find.text('Item 3'), matching: find.byType(IndexedSemantics))); + expect(indexSemantics3b.index, 3); + final indexSemantics4b = tester.widget(find.ancestor( + of: find.text('Item 4'), matching: find.byType(IndexedSemantics))); + expect(indexSemantics4b.index, 4); + final indexSemantics5b = tester.widget(find.ancestor( + of: find.text('Item 5'), matching: find.byType(IndexedSemantics))); + expect(indexSemantics5b.index, 5); + final indexSemantics6b = tester.widget(find.ancestor( + of: find.text('Item 6'), matching: find.byType(IndexedSemantics))); + expect(indexSemantics6b.index, 6); + }); + + testWidgets('addIndexSemantics = false', (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + initialIndex: 5, + addSemanticIndexes: false, + ); + + expect(find.byType(IndexedSemantics), findsNothing); + + unawaited( + itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + await tester.pumpAndSettle(); + itemScrollController.jumpTo(index: 0); + await tester.pumpAndSettle(); + + expect(find.byType(IndexedSemantics), findsNothing); + }); + + testWidgets('semanticChildCount specified', (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + + await setUpWidgetTest( + tester, + semanticChildCount: 30, + itemScrollController: itemScrollController, + ); + + final customScrollView = + tester.widget(find.byType(UnboundedCustomScrollView)); + expect(customScrollView.semanticChildCount, 30); + + unawaited( + itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + await tester.pumpAndSettle(); + + final customScrollView2 = + tester.widget(find.byType(UnboundedCustomScrollView)); + expect(customScrollView2.semanticChildCount, 30); + }); + + testWidgets('semanticChildCount not specified', (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + ); + + final customScrollView = + tester.widget(find.byType(UnboundedCustomScrollView)); + expect(customScrollView.semanticChildCount, defaultItemCount); + + unawaited( + itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + await tester.pumpAndSettle(); + + final customScrollView2 = + tester.widget(find.byType(UnboundedCustomScrollView)); + expect(customScrollView2.semanticChildCount, defaultItemCount); + }); + + testWidgets('Semantic tree contains items in cache', + (WidgetTester tester) async { + final itemPositionsListener = ItemPositionsListener.create(); + await setUpWidgetTest(tester, itemPositionsListener: itemPositionsListener); + + final root = WidgetsBinding + .instance!.pipelineOwner.semanticsOwner!.rootSemanticsNode!; + + final semanticNodes = [root]; + + collectSemanticNodes(root, semanticNodes); + + expect(semanticNodes.where((element) => element.label == 'Item 10'), + isNotEmpty); + }); + + testWidgets('padding test - centered at top', (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.getTopRight(find.text('Item 1')), + const Offset(screenWidth - 10, itemHeight + 10)); + + 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.getBottomRight(find.text('Item 499')), + const Offset(screenWidth - 10, screenHeight - 10)); + }); + + testWidgets('padding test - centered not at top', + (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)); + }); + + testWidgets('padding - first element centered - scroll up', + (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + padding: const EdgeInsets.all(10), + ); + + await tester.drag( + find.byType(ScrollablePositionedList), const Offset(0, 100)); + await tester.pumpAndSettle(); + + expect(tester.getTopLeft(find.text('Item 0')), const Offset(10, 10)); + }); + + testWidgets('padding - last element centered - scroll down', + (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + padding: const EdgeInsets.all(10), + ); + + unawaited(itemScrollController.scrollTo( + index: defaultItemCount - 1, duration: scrollDuration)); + await tester.pumpAndSettle(); + + await tester.drag( + find.byType(ScrollablePositionedList), const Offset(0, -100)); + await tester.pumpAndSettle(); + + expect( + tester.getBottomLeft(find.text('Item ${defaultItemCount - 1}')), + const Offset(10, screenHeight - 10), + ); + }); + + testWidgets('no repaint boundaries', (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + initialIndex: 2, + padding: const EdgeInsets.all(10), + addRepaintBoundaries: false, + ); + + expect( + tester + .widgetList(find.descendant( + of: find.byType(ScrollablePositionedList), + matching: find.byType(RepaintBoundary))) + .length, + lessThan(5)); + }); + + testWidgets('no automatic keep alives', (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + initialIndex: 2, + padding: const EdgeInsets.all(10), + addAutomaticKeepAlives: false, + ); + + expect( + find.descendant( + of: find.byType(ScrollablePositionedList), + matching: find.byType(AutomaticKeepAlive)), + findsNothing); + }); + + testWidgets('Jump to end of list', (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + await setUpWidgetTest(tester, itemScrollController: itemScrollController); + + itemScrollController.jumpTo(index: defaultItemCount - 1); + await tester.pumpAndSettle(); + + expect(tester.getBottomLeft(find.text('Item ${defaultItemCount - 1}')).dy, + screenHeight); + }); + + testWidgets('Scroll to end of list', (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + await setUpWidgetTest(tester, itemScrollController: itemScrollController); + + unawaited(itemScrollController.scrollTo( + index: defaultItemCount - 1, duration: scrollDuration)); + await tester.pumpAndSettle(); + + expect(tester.getBottomLeft(find.text('Item ${defaultItemCount - 1}')).dy, + screenHeight); + }); + + testWidgets('Scroll to end of list, jump to beginning, jump to end', + (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + await setUpWidgetTest(tester, itemScrollController: itemScrollController); + + unawaited(itemScrollController.scrollTo( + index: defaultItemCount - 1, duration: scrollDuration)); + await tester.pumpAndSettle(); + itemScrollController.jumpTo(index: 0); + await tester.pumpAndSettle(); + itemScrollController.jumpTo(index: defaultItemCount - 1); + await tester.pumpAndSettle(); + + expect(tester.getBottomLeft(find.text('Item ${defaultItemCount - 1}')).dy, + screenHeight); + }); + + testWidgets('Jump to end of list, scroll to beginning, scroll to end', + (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + await setUpWidgetTest(tester, itemScrollController: itemScrollController); + + itemScrollController.jumpTo(index: defaultItemCount - 1); + await tester.pumpAndSettle(); + + unawaited( + itemScrollController.scrollTo(index: 0, duration: scrollDuration)); + await tester.pumpAndSettle(); + unawaited(itemScrollController.scrollTo( + index: defaultItemCount - 1, duration: scrollDuration)); + await tester.pumpAndSettle(); + + expect(tester.getBottomLeft(find.text('Item ${defaultItemCount - 1}')).dy, + screenHeight); + }); + + testWidgets( + 'Jump to end of list, jump to beginning with alignment not at top', + (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + await setUpWidgetTest(tester, itemScrollController: itemScrollController); + + itemScrollController.jumpTo(index: defaultItemCount - 1); + await tester.pumpAndSettle(); + + itemScrollController.jumpTo(index: 0, alignment: 0.3); + await tester.pumpAndSettle(); + + expect(tester.getTopLeft(find.text('Item 0')).dy, 0); + }); + + testWidgets("Short list, can't scroll past end", (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + await setUpWidgetTest(tester, + itemScrollController: itemScrollController, itemCount: 3); + + await tester.drag( + find.byType(ScrollablePositionedList), const Offset(0, -10)); + await tester.pumpAndSettle(); + + expect(tester.getTopLeft(find.text('Item 0')).dy, 0); + }); + + testWidgets('List can be keyed', (WidgetTester tester) async { + const key = ValueKey('key'); + + await setUpWidgetTest(tester, key: key); + + expect(find.byKey(key), findsOneWidget); + }); + + testWidgets( + 'Maintain programmatic 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, + ), + ), + const Center( + child: Text('Test'), + ) + ], + ), + ), + ); + + itemScrollController.jumpTo( + index: 9, alignment: -(itemHeight / screenHeight) / 2); + await tester.pump(); + + 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.getTopLeft(find.text('Item 9')).dy, -itemHeight / 2); + + 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('Maintain user scroll position (1 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, + ), + ), + const Center( + child: Text('Test'), + ) + ], + ), + ), + ); + + await tester.drag( + find.byType(ScrollablePositionedList), const Offset(0, -itemHeight)); + await tester.pumpAndSettle(); + + final item0Bottom = tester.getBottomRight(find.text('Item 0')).dy; + expect(item0Bottom, 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 0')).dy, item0Bottom); + + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 0) + .itemLeadingEdge, + -(itemHeight / screenHeight) / 2); + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 0) + .itemTrailingEdge, + (itemHeight / screenHeight) / 2); + }); + + 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'), + ), + itemPositionsListener: itemPositionsListener, + ), + ), + const Center( + child: Text('Test'), + ) + ], + ), + ), + ); + + itemScrollController.jumpTo(index: 9); + await tester.pump(); + + expect(tester.getBottomRight(find.text('Item 9')).dy, itemHeight); + + await tester.drag( + find.byType(ScrollablePositionedList), const Offset(0, -itemHeight)); + 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); + + 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(); + await setUpWidgetTest(tester, + itemScrollController: itemScrollController, itemCount: 0); + + expect(find.text('Item 0'), findsNothing); + }); + + testWidgets('Jump to 100 then set itemCount to 0', + (WidgetTester tester) async { + tester.binding.window.devicePixelRatioTestValue = 1.0; + tester.binding.window.physicalSizeTestValue = + const Size(screenWidth, screenHeight); + + final itemScrollController = ItemScrollController(); + final itemPositionsListener = ItemPositionsListener.create(); + final itemCount = ValueNotifier(defaultItemCount); + + await tester.pumpWidget( + 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'), + ); + }, + ), + ), + ), + ); + + await tester.pumpAndSettle(); + itemScrollController.jumpTo(index: 100); + await tester.pumpAndSettle(); + expect(find.text('Item 100'), findsOneWidget); + + itemCount.value = 0; + await tester.pumpAndSettle(); + + expect(find.text('Item 0'), findsNothing); + expect(find.text('Item 100'), findsNothing); + expect(itemPositionsListener.itemPositions.value.isEmpty, isTrue); + }); + + testWidgets('List positioned with 100 at top then set itemCount to 100', + (WidgetTester tester) async { + tester.binding.window.devicePixelRatioTestValue = 1.0; + tester.binding.window.physicalSizeTestValue = + const Size(screenWidth, screenHeight); + + final itemCount = ValueNotifier(defaultItemCount); + + await tester.pumpWidget( + 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'), + ); + }, + ), + ), + ), + ); + + await tester.pumpAndSettle(); + expect(tester.getTopLeft(find.text('Item 100')).dy, 0); + + itemCount.value = 100; + await tester.pumpAndSettle(); + + expect(find.text('Item 100'), findsNothing); + expect(tester.getBottomLeft(find.text('Item 99')).dy, screenHeight); + }); + + testWidgets('List positioned with 499 at bottom then set itemCount to 100', + (WidgetTester tester) async { + tester.binding.window.devicePixelRatioTestValue = 1.0; + tester.binding.window.physicalSizeTestValue = + const Size(screenWidth, screenHeight); + + final itemCount = ValueNotifier(defaultItemCount); + + await tester.pumpWidget( + 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'), + ); + }, + ), + ), + ), + ); + + await tester.pumpAndSettle(); + expect(tester.getBottomRight(find.text('Item 499')).dy, screenHeight); + + itemCount.value = 100; + await tester.pumpAndSettle(); + + expect(find.text('Item 100'), findsNothing); + expect(tester.getBottomLeft(find.text('Item 99')).dy, screenHeight); + }); + + testWidgets('Large minCacheExtent', (WidgetTester tester) async { + await setUpWidgetTest(tester, minCacheExtent: itemHeight * 200); + + expect(find.text('Item 100', skipOffstage: false), findsOneWidget); + }); + + testWidgets('Scroll to 20 without fading small minCacheExtent', + (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + final itemPositionsListener = ItemPositionsListener.create(); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + minCacheExtent: 10, + ); + + var fadeTransition = tester.widget(find + .descendant( + of: find.byType(ScrollablePositionedList), + matching: find.byType(FadeTransition)) + .last); + 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(find + .descendant( + of: find.byType(ScrollablePositionedList), + matching: find.byType(FadeTransition)) + .last); + expect(fadeTransition.opacity, initialOpacity); + + await tester.pumpAndSettle(); + + expect(find.text('Item 14'), findsNothing); + expect(find.text('Item 20'), findsOneWidget); + }); + + testWidgets('Scroll to 100 without fading for large minCacheExtent', + (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + minCacheExtent: itemHeight * 200, + ); + + var fadeTransition = tester.widget(find + .descendant( + of: find.byType(ScrollablePositionedList), + matching: find.byType(FadeTransition)) + .last); + final initialOpacity = fadeTransition.opacity; + + unawaited( + itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + await tester.pump(); + await tester.pump(); + await tester.pump(scrollDuration ~/ 2); + + fadeTransition = tester.widget(find + .descendant( + of: find.byType(ScrollablePositionedList), + matching: find.byType(FadeTransition)) + .last); + expect(fadeTransition.opacity, initialOpacity); + + await tester.pumpAndSettle(); + + expect(find.text('Item 100'), findsOneWidget); + }); + + testWidgets('Position list when not enough above top item to fill viewport', + (WidgetTester tester) async { + const alignment = 0.8; + + await setUpWidgetTest( + tester, + itemCount: 2, + initialAlignment: alignment, + ); + + await tester.pumpAndSettle(); + + expect(tester.getTopLeft(find.text('Item 0')).dy, screenHeight * alignment); + }); + + testWidgets('Rebuild with scroll controller', (WidgetTester tester) async { + tester.binding.window.devicePixelRatioTestValue = 1.0; + tester.binding.window.physicalSizeTestValue = + const Size(screenWidth, screenHeight); + final key = ValueNotifier(const ValueKey('key')); + final itemScrollController = ItemScrollController(); + + await tester.pumpWidget( + 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'), + ), + ), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + key.value = const ValueKey('newKey'); + await tester.pumpAndSettle(); + + unawaited( + itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + await tester.pumpAndSettle(); + + expect(find.text('Item 100'), findsOneWidget); + }); + + testWidgets('Double rebuild with scroll controller', + (WidgetTester tester) async { + tester.binding.window.devicePixelRatioTestValue = 1.0; + tester.binding.window.physicalSizeTestValue = + const Size(screenWidth, screenHeight); + final outerKey = ValueNotifier(const ValueKey('outerKey')); + final innerKey = GlobalKey(); + final listKey = ValueNotifier(const ValueKey(null)); + final itemScrollController = ItemScrollController(); + + await tester.pumpWidget( + MaterialApp( + home: ValueListenableBuilder( + valueListenable: outerKey, + builder: (context, outerKey, child) => ValueListenableBuilder( + valueListenable: listKey, + builder: (context, listKey, child) => Container( + key: outerKey, + child: Builder( + builder: (context) => Container( + key: innerKey, + child: ScrollablePositionedList.builder( + key: listKey, + itemCount: 200, + itemScrollController: itemScrollController, + itemBuilder: (context, index) => SizedBox( + height: itemHeight, + child: Text('Item $index'), + ), + ), + ), + ), + ), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + outerKey.value = const ValueKey('newOuterKey'); + listKey.value = const ValueKey('newListKey'); + await tester.pumpAndSettle(); + + unawaited( + itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + await tester.pumpAndSettle(); + + expect(find.text('Item 100'), findsOneWidget); + }); + + testWidgets('Key change with scroll controller', (WidgetTester tester) async { + tester.binding.window.devicePixelRatioTestValue = 1.0; + tester.binding.window.physicalSizeTestValue = + const Size(screenWidth, screenHeight); + final key = ValueNotifier(const ValueKey('key')); + final itemScrollController = ItemScrollController(); + + await tester.pumpWidget( + 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'), + ), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + key.value = const ValueKey('newKey'); + await tester.pumpAndSettle(); + }); + + testWidgets('Scroll after rebuild', (WidgetTester tester) async { + tester.binding.window.devicePixelRatioTestValue = 1.0; + tester.binding.window.physicalSizeTestValue = + const Size(screenWidth, screenHeight); + final key = ValueNotifier(const ValueKey('key')); + final itemScrollController = ItemScrollController(); + + await tester.pumpWidget( + 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'), + ), + ), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + key.value = const ValueKey('newKey'); + await tester.pumpAndSettle(); + + unawaited( + itemScrollController.scrollTo(index: 70, duration: scrollDuration)); + await tester.pumpAndSettle(); + + expect(find.text('Item 70'), findsOneWidget); + }); + + testWidgets('Scroll after rebuild when resusing state', + (WidgetTester tester) async { + tester.binding.window.devicePixelRatioTestValue = 1.0; + tester.binding.window.physicalSizeTestValue = + const Size(screenWidth, screenHeight); + final containerKey = ValueNotifier(const ValueKey('key')); + final scrollKey = GlobalKey(); + final itemScrollController = ItemScrollController(); + + await tester.pumpWidget( + 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'), + ), + ), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + containerKey.value = const ValueKey('newKey'); + await tester.pumpAndSettle(); + + unawaited( + itemScrollController.scrollTo(index: 70, duration: scrollDuration)); + await tester.pumpAndSettle(); + + expect(find.text('Item 70'), findsOneWidget); + }); + + testWidgets('Scroll after changing scroll controller', + (WidgetTester tester) async { + tester.binding.window.devicePixelRatioTestValue = 1.0; + tester.binding.window.physicalSizeTestValue = + const Size(screenWidth, screenHeight); + + final itemScrollController0 = ItemScrollController(); + final itemScrollController1 = ItemScrollController(); + final itemScrollControllerListenable = ValueNotifier(itemScrollController0); + + await tester.pumpWidget( + 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'), + ), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + expect(itemScrollController0.isAttached, true); + expect(itemScrollController1.isAttached, false); + + itemScrollControllerListenable.value = itemScrollController1; + await tester.pumpAndSettle(); + + expect(itemScrollController0.isAttached, false); + expect(itemScrollController1.isAttached, true); + + unawaited( + itemScrollController1.scrollTo(index: 70, duration: scrollDuration)); + await tester.pumpAndSettle(); + + expect(find.text('Item 70'), findsOneWidget); + }); + + testWidgets('Scroll after swapping scroll controllers', + (WidgetTester tester) async { + tester.binding.window.devicePixelRatioTestValue = 1.0; + tester.binding.window.physicalSizeTestValue = + const Size(screenWidth, screenHeight); + + final itemScrollController0 = ItemScrollController(); + final itemScrollController1 = ItemScrollController(); + final topItemScrollControllerListenable = + ValueNotifier(itemScrollController0); + final bottomItemScrollControllerListenable = + ValueNotifier(itemScrollController1); + + await tester.pumpWidget(MaterialApp( + home: Column( + children: [ + 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'), + ), + ), + ), + ), + 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'), + ), + ), + ), + ), + ], + ), + )); + + await tester.pumpAndSettle(); + expect(itemScrollController0.isAttached, true); + expect(itemScrollController1.isAttached, true); + + topItemScrollControllerListenable.value = itemScrollController1; + bottomItemScrollControllerListenable.value = itemScrollController0; + await tester.pumpAndSettle(); + + expect(itemScrollController0.isAttached, true); + expect(itemScrollController1.isAttached, true); + + unawaited( + itemScrollController1.scrollTo(index: 70, duration: scrollDuration)); + unawaited( + itemScrollController0.scrollTo(index: 50, duration: scrollDuration)); + await tester.pumpAndSettle(); + + expect(find.text('Item 70'), findsOneWidget); + expect(find.text('Item 50'), findsOneWidget); + }); +} + +bool collectSemanticNodes(SemanticsNode root, List nodes) { + nodes.add(root); + if (!root.hasChildren) return true; + root.visitChildren((child) => collectSemanticNodes(child, nodes)); + return true; +} diff --git a/packages/stream_chat_flutter/test/scrollable_positioned_list/separated_positioned_list_test.dart b/packages/stream_chat_flutter/test/scrollable_positioned_list/separated_positioned_list_test.dart new file mode 100644 index 00000000..47f75a5e --- /dev/null +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/separated_positioned_list_test.dart @@ -0,0 +1,261 @@ +// 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 separatorHeight = screenHeight / 20.0; +const defaultItemCount = 500; +const cacheExtent = itemHeight * 2; + +void main() { + final itemPositionsNotifier = ItemPositionsListener.create(); + + Future setUpWidgetTest( + WidgetTester tester, { + int topItem = 0, + ScrollController? scrollController, + double anchor = 0, + int itemCount = defaultItemCount, + }) async { + tester.binding.window.devicePixelRatioTestValue = 1.0; + tester.binding.window.physicalSizeTestValue = + const Size(screenWidth, screenHeight); + + await tester.pumpWidget( + MaterialApp( + home: PositionedList( + itemCount: itemCount, + positionedIndex: topItem, + alignment: anchor, + controller: scrollController, + itemBuilder: (context, index) => SizedBox( + height: itemHeight, + child: Text('Item $index'), + ), + separatorBuilder: (context, index) => SizedBox( + height: separatorHeight, + child: Text('Separator $index'), + ), + itemPositionsNotifier: itemPositionsNotifier as ItemPositionsNotifier, + cacheExtent: cacheExtent, + ), + ), + ); + } + + testWidgets('Empty list', (WidgetTester tester) async { + await setUpWidgetTest(tester, itemCount: 0); + + expect(find.text('Item 0'), findsNothing); + expect(find.text('Separator 0'), findsNothing); + }); + + testWidgets('Short list', (WidgetTester tester) async { + await setUpWidgetTest(tester, itemCount: 3); + + expect(find.text('Item 0'), findsOneWidget); + expect(find.text('Separator 0'), findsOneWidget); + expect(find.text('Item 1'), findsOneWidget); + expect(find.text('Separator 1'), findsOneWidget); + expect(find.text('Item 2'), findsOneWidget); + expect(find.text('Separator 2'), findsNothing); + expect(find.text('Item 3'), findsNothing); + + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 0) + .itemLeadingEdge, + 0); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 2) + .itemTrailingEdge, + _screenProportion(numberOfItems: 3, numberOfSeparators: 2)); + }); + + testWidgets('Short list centered at 1 scrolled up', + (WidgetTester tester) async { + await setUpWidgetTest(tester, itemCount: 3, topItem: 1); + + await tester.drag( + find.byType(PositionedList), const Offset(0, itemHeight * 2)); + await tester.pumpAndSettle(); + + expect(find.text('Item 0'), findsOneWidget); + expect(find.text('Separator 0'), findsOneWidget); + expect(find.text('Item 1'), findsOneWidget); + expect(find.text('Separator 1'), findsOneWidget); + expect(find.text('Item 2'), findsOneWidget); + expect(find.text('Separator 2'), findsNothing); + expect(find.text('Item 3'), findsNothing); + + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 0) + .itemLeadingEdge, + 0); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 2) + .itemTrailingEdge, + _screenProportion(numberOfItems: 3, numberOfSeparators: 2)); + }); + + testWidgets('List positioned with 0 at top', (WidgetTester tester) async { + await setUpWidgetTest(tester); + await tester.pump(); + + expect(find.text('Item 0'), findsOneWidget); + expect(find.text('Separator 5'), findsOneWidget); + expect(find.text('Item 6'), findsOneWidget); + expect(find.text('Separator 6'), findsNothing); + expect(find.text('Item 7'), findsNothing); + + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 0) + .itemLeadingEdge, + 0); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 5) + .itemTrailingEdge, + 1 - _screenProportion(numberOfItems: 1, numberOfSeparators: 1)); + + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 6) + .itemTrailingEdge, + 1); + }); + + testWidgets('List positioned with 5 at top', (WidgetTester tester) async { + await setUpWidgetTest(tester, topItem: 5); + await tester.pump(); + + expect(find.text('Item 4'), findsNothing); + expect(find.text('Separator 4'), findsNothing); + expect(find.text('Item 5'), findsOneWidget); + expect(find.text('Separator 5'), findsOneWidget); + + expect(find.text('Separator 10'), findsOneWidget); + expect(find.text('Item 11'), findsOneWidget); + expect(find.text('Separator 11'), findsNothing); + + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 5) + .itemLeadingEdge, + 0); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 6) + .itemLeadingEdge, + _screenProportion(numberOfItems: 1, numberOfSeparators: 1)); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 11) + .itemTrailingEdge, + 1); + }); + + testWidgets('List positioned with 20 at bottom', (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('Separator 19'), findsOneWidget); + expect(find.text('Item 14'), findsOneWidget); + expect(find.text('Separator 13'), findsOneWidget); + expect(find.text('Item 13'), findsOneWidget); + expect(find.text('Separator 12'), findsNothing); + + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 19) + .itemTrailingEdge, + 1 - _screenProportion(numberOfItems: 0, numberOfSeparators: 1)); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 20) + .itemLeadingEdge, + 1); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 13) + .itemLeadingEdge, + _screenProportion(numberOfItems: -0.5, numberOfSeparators: 0)); + }); + + testWidgets('List positioned with item 20 at halfway', + (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 item 20 half off top of screen', + (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, + _screenProportion(numberOfItems: -0.5, numberOfSeparators: 0)); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 20) + .itemTrailingEdge, + _screenProportion(numberOfItems: 0.5, numberOfSeparators: 0)); + }); + + testWidgets('List positioned with 5 at top then scroll up 2 items', + (WidgetTester tester) async { + await setUpWidgetTest(tester, topItem: 5); + + await tester.drag(find.byType(PositionedList), + const Offset(0, 2 * (itemHeight + separatorHeight))); + await tester.pump(); + + expect(find.text('Separator 2'), findsNothing); + expect(find.text('Item 3'), findsOneWidget); + + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 2) + .itemLeadingEdge, + _screenProportion(numberOfItems: -1, numberOfSeparators: -1)); + expect( + itemPositionsNotifier.itemPositions.value + .firstWhere((position) => position.index == 3) + .itemLeadingEdge, + 0); + }); +} + +double _screenProportion( + {required double numberOfItems, required double numberOfSeparators}) => + (numberOfItems * itemHeight + numberOfSeparators * separatorHeight) / + screenHeight; 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 new file mode 100644 index 00000000..0cc091c9 --- /dev/null +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/separated_scrollable_positioned_list_test.dart @@ -0,0 +1,602 @@ +// 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 'dart:async'; +import 'dart:ui'; + +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/scroll_view.dart'; + +const screenHeight = 400.0; +const screenWidth = 400.0; +const itemHeight = screenHeight / 10.0; +const separatorHeight = screenHeight / 20.0; +const defaultItemCount = 500; +const scrollDuration = Duration(seconds: 1); +const scrollDurationTolerance = Duration(milliseconds: 1); +const tolerance = 1e-3; + +void main() { + Future setUpWidgetTest( + WidgetTester tester, { + Key? key, + ItemScrollController? itemScrollController, + ItemPositionsListener? itemPositionsListener, + int initialIndex = 0, + double initialAlignment = 0.0, + int? itemCount, + ScrollPhysics? physics, + bool addSemanticIndexes = true, + int? semanticChildCount, + EdgeInsets? padding, + bool addRepaintBoundaries = true, + bool addAutomaticKeepAlives = true, + }) async { + tester.binding.window.devicePixelRatioTestValue = 1.0; + tester.binding.window.physicalSizeTestValue = + const Size(screenWidth, screenHeight); + + await tester.pumpWidget( + MaterialApp( + home: ScrollablePositionedList.separated( + itemCount: itemCount ?? defaultItemCount, + itemScrollController: itemScrollController, + itemBuilder: (context, index) => SizedBox( + height: itemHeight, + child: Text('Item $index'), + ), + separatorBuilder: (context, index) => SizedBox( + height: separatorHeight, + child: Text('Separator $index'), + ), + key: key, + itemPositionsListener: itemPositionsListener, + initialScrollIndex: initialIndex, + initialAlignment: initialAlignment, + physics: physics, + addSemanticIndexes: addSemanticIndexes, + semanticChildCount: semanticChildCount, + padding: padding, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + ), + ), + ); + } + + testWidgets('List positioned with 0 at top', (WidgetTester tester) async { + final itemPositionsListener = ItemPositionsListener.create(); + await setUpWidgetTest(tester, itemPositionsListener: itemPositionsListener); + + expect(find.text('Item 0'), findsOneWidget); + expect(find.text('Separator 5'), findsOneWidget); + expect(find.text('Item 6'), findsOneWidget); + expect(find.text('Separator 6'), findsNothing); + expect(find.text('Item 7'), findsNothing); + + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 0) + .itemLeadingEdge, + 0); + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 5) + .itemTrailingEdge, + 1 - _screenProportion(numberOfItems: 1, numberOfSeparators: 1)); + + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 6) + .itemTrailingEdge, + 1); + expect( + itemPositionsListener.itemPositions.value + .where((position) => position.index == 7), + isEmpty); + }); + + testWidgets('List positioned with 0 at top - use default values', + (WidgetTester tester) async { + final itemPositionsListener = ItemPositionsListener.create(); + tester.binding.window.devicePixelRatioTestValue = 1.0; + tester.binding.window.physicalSizeTestValue = + const Size(screenWidth, screenHeight); + + await tester.pumpWidget( + MaterialApp( + home: ScrollablePositionedList.separated( + itemCount: defaultItemCount, + itemBuilder: (context, index) => SizedBox( + height: itemHeight, + child: Text('Item $index'), + ), + separatorBuilder: (context, index) => SizedBox( + height: separatorHeight, + child: Text('Separator $index'), + ), + itemPositionsListener: itemPositionsListener, + ), + ), + ); + + expect(find.text('Item 0'), findsOneWidget); + expect(find.text('Separator 5'), findsOneWidget); + expect(find.text('Item 6'), findsOneWidget); + expect(find.text('Separator 6'), findsNothing); + expect(find.text('Item 7'), findsNothing); + + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 0) + .itemLeadingEdge, + 0); + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 5) + .itemTrailingEdge, + 1 - _screenProportion(numberOfItems: 1, numberOfSeparators: 1)); + + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 6) + .itemTrailingEdge, + 1); + expect( + itemPositionsListener.itemPositions.value + .where((position) => position.index == 7), + isEmpty); + }); + + testWidgets('List positioned with 5 at top', (WidgetTester tester) async { + final itemPositionsListener = ItemPositionsListener.create(); + await setUpWidgetTest(tester, + itemPositionsListener: itemPositionsListener, initialIndex: 5); + + expect(find.text('Item 4'), findsNothing); + expect(find.text('Separator 4'), findsNothing); + expect(find.text('Item 5'), findsOneWidget); + expect(find.text('Separator 5'), findsOneWidget); + expect(find.text('Separator 10'), findsOneWidget); + expect(find.text('Item 11'), findsOneWidget); + expect(find.text('Separator 11'), findsNothing); + + expect( + itemPositionsListener.itemPositions.value + .where((position) => position.index == 4), + isEmpty); + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 5) + .itemLeadingEdge, + 0); + }); + + testWidgets('List positioned with 9 at middle', (WidgetTester tester) async { + final itemPositionsListener = ItemPositionsListener.create(); + await setUpWidgetTest(tester, + itemPositionsListener: itemPositionsListener, + initialIndex: 9, + initialAlignment: 0.5); + + expect(tester.getTopLeft(find.text('Item 9')).dy, screenHeight / 2); + expect(tester.getTopLeft(find.text('Item 8')).dy, + screenHeight / 2 - itemHeight - separatorHeight); + expect(tester.getTopLeft(find.text('Item 10')).dy, + screenHeight / 2 + itemHeight + separatorHeight); + + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 9) + .itemLeadingEdge, + 0.5); + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 8) + .itemLeadingEdge, + 0.5 - _screenProportion(numberOfItems: 1, numberOfSeparators: 1)); + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 10) + .itemLeadingEdge, + 0.5 + _screenProportion(numberOfItems: 1, numberOfSeparators: 1)); + }); + + testWidgets('Scroll to 9 half way off top', (WidgetTester tester) async { + final itemPositionsListener = ItemPositionsListener.create(); + final itemScrollController = ItemScrollController(); + await setUpWidgetTest(tester, + itemPositionsListener: itemPositionsListener, + itemScrollController: itemScrollController); + + unawaited(itemScrollController.scrollTo( + index: 9, + duration: scrollDuration, + alignment: -(itemHeight / screenHeight) / 2)); + await tester.pump(); + await tester.pump(); + await tester.pump(scrollDuration + scrollDurationTolerance); + + expect(tester.getTopLeft(find.text('Item 9')).dy, -itemHeight / 2); + + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 9) + .itemLeadingEdge, + _screenProportion(numberOfItems: -0.5, numberOfSeparators: 0)); + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 9) + .itemTrailingEdge, + _screenProportion(numberOfItems: 0.5, numberOfSeparators: 0)); + }); + + testWidgets('Jump to 9 half way off top', (WidgetTester tester) async { + final itemPositionsListener = ItemPositionsListener.create(); + final itemScrollController = ItemScrollController(); + await setUpWidgetTest(tester, + itemPositionsListener: itemPositionsListener, + itemScrollController: itemScrollController); + + itemScrollController.jumpTo( + index: 9, alignment: -(itemHeight / screenHeight) / 2); + await tester.pump(); + + expect(tester.getTopLeft(find.text('Item 9')).dy, -itemHeight / 2); + + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 9) + .itemLeadingEdge, + _screenProportion(numberOfItems: -0.5, numberOfSeparators: 0)); + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 9) + .itemTrailingEdge, + _screenProportion(numberOfItems: 0.5, numberOfSeparators: 0)); + }); + + testWidgets('List positioned with 9 at middle scroll to 16 at bottom', + (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + final itemPositionsListener = ItemPositionsListener.create(); + await setUpWidgetTest(tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + initialIndex: 9, + initialAlignment: 0.5); + + unawaited(itemScrollController.scrollTo( + index: 16, duration: scrollDuration, alignment: 1)); + await tester.pump(); + await tester.pump(); + await tester.pump(scrollDuration + scrollDurationTolerance); + + expect(tester.getBottomRight(find.text('Item 15')).dy, + screenHeight - separatorHeight); + + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 15) + .itemTrailingEdge, + 1 - _screenProportion(numberOfItems: 0, numberOfSeparators: 1)); + }); + + testWidgets('physics', (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + await setUpWidgetTest(tester, + itemScrollController: itemScrollController, + physics: const BouncingScrollPhysics()); + + await tester.drag( + find.byType(ScrollablePositionedList), const Offset(0, 50)); + await tester.pump(const Duration(milliseconds: 200)); + + expect(tester.getTopLeft(find.text('Item 0')).dy, greaterThan(0)); + + await tester.pumpAndSettle(); + expect(tester.getTopLeft(find.text('Item 0')).dy, 0); + + unawaited( + itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + await tester.pumpAndSettle(); + itemScrollController.jumpTo(index: 0); + await tester.pumpAndSettle(); + + await tester.drag( + find.byType(ScrollablePositionedList), const Offset(0, 50)); + await tester.pump(const Duration(milliseconds: 200)); + + expect(tester.getTopLeft(find.text('Item 0')).dy, greaterThan(0)); + + await tester.pumpAndSettle(); + expect(tester.getTopLeft(find.text('Item 0')).dy, 0); + }); + + testWidgets('correct index semantics', (WidgetTester tester) async { + await setUpWidgetTest(tester, initialIndex: 5); + + await tester.drag( + find.byType(ScrollablePositionedList), const Offset(0, itemHeight * 4)); + await tester.pumpAndSettle(); + + final indexSemantics3 = tester.widget(find.ancestor( + of: find.text('Item 3'), matching: find.byType(IndexedSemantics))); + expect(indexSemantics3.index, 3); + final indexSemantics4 = tester.widget(find.ancestor( + of: find.text('Item 4'), matching: find.byType(IndexedSemantics))); + expect(indexSemantics4.index, 4); + }); + + testWidgets('addIndexSemantics = false', (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + initialIndex: 5, + addSemanticIndexes: false, + ); + + expect(find.byType(IndexedSemantics), findsNothing); + + unawaited( + itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + await tester.pumpAndSettle(); + + expect(find.byType(IndexedSemantics), findsNothing); + }); + + testWidgets('semanticChildCount specified', (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + + await setUpWidgetTest( + tester, + semanticChildCount: 30, + itemScrollController: itemScrollController, + ); + + final customScrollView = + tester.widget(find.byType(UnboundedCustomScrollView)); + expect(customScrollView.semanticChildCount, 30); + + unawaited( + itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + await tester.pumpAndSettle(); + + final customScrollView2 = + tester.widget(find.byType(UnboundedCustomScrollView)); + expect(customScrollView2.semanticChildCount, 30); + }); + + testWidgets('semanticChildCount not specified', (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + ); + + final customScrollView = + tester.widget(find.byType(UnboundedCustomScrollView)); + expect(customScrollView.semanticChildCount, defaultItemCount); + + unawaited( + itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + await tester.pumpAndSettle(); + + final customScrollView2 = + tester.widget(find.byType(UnboundedCustomScrollView)); + expect(customScrollView2.semanticChildCount, defaultItemCount); + }); + + testWidgets('padding test - centered at top', (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 + separatorHeight)); + expect(tester.getTopRight(find.text('Item 1')), + const Offset(screenWidth - 10, itemHeight + 10 + separatorHeight)); + + unawaited( + itemScrollController.scrollTo(index: 494, duration: scrollDuration)); + await tester.pumpAndSettle(); + + await tester.drag( + find.byType(ScrollablePositionedList), const Offset(0, -500)); + await tester.pumpAndSettle(); + + expect(tester.getBottomRight(find.text('Item 499')), + const Offset(screenWidth - 10, screenHeight - 10)); + }); + + testWidgets('padding test - centered sliver not at top', + (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 + 2 * (separatorHeight + itemHeight))); + expect( + tester.getTopRight(find.text('Item 3')), + const Offset( + screenWidth - 10, 10 + 3 * (itemHeight + separatorHeight))); + }); + + testWidgets('no repaint bounderies', (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + initialIndex: 2, + padding: const EdgeInsets.all(10), + addRepaintBoundaries: false, + ); + + expect( + tester + .widgetList(find.descendant( + of: find.byType(ScrollablePositionedList), + matching: find.byType(RepaintBoundary))) + .length, + lessThan(5)); + }); + + testWidgets('no automatic keep alives', (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + initialIndex: 2, + padding: const EdgeInsets.all(10), + addAutomaticKeepAlives: false, + ); + + expect( + find.descendant( + of: find.byType(ScrollablePositionedList), + matching: find.byType(AutomaticKeepAlive)), + findsNothing); + }); + + testWidgets('List can be keyed', (WidgetTester tester) async { + const key = ValueKey('key'); + + await setUpWidgetTest(tester, key: key); + + expect(find.byKey(key), findsOneWidget); + }); + + testWidgets('Empty list then update to single item list', + (WidgetTester tester) async { + tester.binding.window.devicePixelRatioTestValue = 1.0; + tester.binding.window.physicalSizeTestValue = + const Size(screenWidth, screenHeight); + + final itemScrollController = ItemScrollController(); + final itemPositionsListener = ItemPositionsListener.create(); + final itemCount = ValueNotifier(0); + + await tester.pumpWidget( + 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'), + ), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + itemCount.value = 1; + await tester.pumpAndSettle(); + + expect(find.text('Item 0'), findsOneWidget); + expect(find.text('Separator 0'), findsNothing); + }); + + testWidgets('ItemPositions: Empty list then update to 10 items list', + (WidgetTester tester) async { + tester.binding.window.devicePixelRatioTestValue = 1.0; + tester.binding.window.physicalSizeTestValue = + const Size(screenWidth, screenHeight); + + final itemScrollController = ItemScrollController(); + final itemPositionsListener = ItemPositionsListener.create(); + final itemCount = ValueNotifier(0); + + await tester.pumpWidget( + 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'), + ), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + expect(find.text('Item 0'), findsNothing); + expect(find.text('Separator 0'), findsNothing); + expect(itemPositionsListener.itemPositions.value, []); + + itemCount.value = 10; + await tester.pumpAndSettle(); + + expect(find.text('Item 0'), findsOneWidget); + expect(find.text('Separator 5'), findsOneWidget); + expect(find.text('Item 6'), findsOneWidget); + expect(find.text('Separator 6'), findsNothing); + expect(find.text('Item 7'), findsNothing); + + expect(itemPositionsListener.itemPositions.value, isNotEmpty); + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 0) + .itemLeadingEdge, + 0); + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 5) + .itemTrailingEdge, + 1 - _screenProportion(numberOfItems: 1, numberOfSeparators: 1)); + + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 6) + .itemTrailingEdge, + 1); + expect( + itemPositionsListener.itemPositions.value + .where((position) => position.index == 7), + isEmpty); + }); +} + +double _screenProportion( + {required double numberOfItems, required double numberOfSeparators}) => + (numberOfItems * itemHeight + numberOfSeparators * separatorHeight) / + screenHeight; 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 new file mode 100644 index 00000000..55574003 --- /dev/null +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/seperated_horizontal_scrollable_positioned_list_test.dart @@ -0,0 +1,220 @@ +// 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 'dart:async'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart'; + +const screenHeight = 400.0; +const screenWidth = 400.0; +const itemWidth = screenWidth / 10.0; +const separatorWidth = screenWidth / 20.0; +const itemCount = 500; +const scrollDuration = Duration(seconds: 1); +const tolerance = 10e-5; + +void main() { + Future setUpWidgetTest( + WidgetTester tester, { + ItemScrollController? itemScrollController, + ItemPositionsListener? itemPositionsListener, + bool reverse = false, + EdgeInsets? padding, + int initialScrollIndex = 0, + }) async { + tester.binding.window.devicePixelRatioTestValue = 1.0; + tester.binding.window.physicalSizeTestValue = + const Size(screenWidth, screenHeight); + + await tester.pumpWidget( + MaterialApp( + home: ScrollablePositionedList.separated( + itemCount: itemCount, + itemScrollController: itemScrollController, + itemBuilder: (context, index) => SizedBox( + width: itemWidth, + child: Text('Item $index'), + ), + separatorBuilder: (context, index) => SizedBox( + width: separatorWidth, + child: Text('Separator $index'), + ), + itemPositionsListener: itemPositionsListener, + scrollDirection: Axis.horizontal, + reverse: reverse, + padding: padding, + initialScrollIndex: initialScrollIndex, + ), + ), + ); + } + + testWidgets('List positioned with 0 at left', (WidgetTester tester) async { + final itemPositionsListener = ItemPositionsListener.create(); + await setUpWidgetTest(tester, itemPositionsListener: itemPositionsListener); + + expect(tester.getTopLeft(find.text('Item 0')).dx, 0); + expect(tester.getBottomLeft(find.text('Item 1')).dx, + itemWidth + separatorWidth); + + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 0) + .itemLeadingEdge, + 0); + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 1) + .itemLeadingEdge, + _screenProportion(numberOfItems: 1, numberOfSeparators: 1)); + }); + + testWidgets('Scroll to 2 (already on screen)', (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + final itemPositionsListener = ItemPositionsListener.create(); + await setUpWidgetTest(tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener); + + 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')).dx, 0); + expect( + tester.getTopLeft(find.text('Item 3')).dx, itemWidth + separatorWidth); + + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 2) + .itemLeadingEdge, + 0); + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 3) + .itemLeadingEdge, + _screenProportion(numberOfItems: 1, numberOfSeparators: 1)); + }); + + testWidgets('Scroll to 100 (not already on screen)', + (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 == 101) + .itemLeadingEdge, + _screenProportion(numberOfItems: 1, numberOfSeparators: 1)); + }); + + testWidgets('Jump to 100', (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')).dx, 0); + expect(tester.getTopLeft(find.text('Item 101')).dx, + itemWidth + separatorWidth); + + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 100) + .itemLeadingEdge, + 0); + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 101) + .itemLeadingEdge, + _screenProportion(numberOfItems: 1, numberOfSeparators: 1)); + }); + + testWidgets('padding test - centered sliver at left', + (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(itemWidth + 10 + separatorWidth, 10)); + expect(tester.getBottomRight(find.text('Item 1')), + const Offset(10 + itemWidth * 2 + separatorWidth, screenHeight - 10)); + + unawaited( + itemScrollController.scrollTo(index: 494, duration: scrollDuration)); + await tester.pumpAndSettle(); + + await tester.drag( + find.byType(ScrollablePositionedList), const Offset(-500, 0)); + await tester.pumpAndSettle(); + + expect(tester.getBottomRight(find.text('Item 499')), + const Offset(screenWidth - 10, screenHeight - 10)); + }); + + testWidgets('padding test - centered sliver not at left', + (WidgetTester tester) async { + final itemScrollController = ItemScrollController(); + final itemPositionsListener = ItemPositionsListener.create(); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + initialScrollIndex: 2, + padding: const EdgeInsets.all(10), + ); + + await tester.drag( + find.byType(ScrollablePositionedList), const Offset(300, 0)); + await tester.pumpAndSettle(); + + expect(tester.getTopLeft(find.text('Item 0')), const Offset(10, 10)); + expect(tester.getTopLeft(find.text('Item 2')), + const Offset(10 + 2 * (itemWidth + separatorWidth), 10)); + expect(tester.getTopLeft(find.text('Item 3')), + const Offset(10 + 3 * (itemWidth + separatorWidth), 10)); + + expect( + itemPositionsListener.itemPositions.value + .firstWhere((position) => position.index == 2) + .itemLeadingEdge, + closeTo( + 10 / screenWidth + 2 * ((itemWidth + separatorWidth) / screenWidth), + tolerance)); + }); +} + +double _screenProportion( + {required double numberOfItems, required double numberOfSeparators}) => + (numberOfItems * itemWidth + numberOfSeparators * separatorWidth) / + screenHeight; diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index 96bae0c7..77ac23a0 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.2.0 + +- Updated `stream_chat` dependency to [`3.2.0`](https://pub.dev/packages/stream_chat/changelog). + ## 3.1.1 - Updated `stream_chat` dependency to [`3.1.1`](https://pub.dev/packages/stream_chat/changelog). diff --git a/packages/stream_chat_flutter_core/lib/src/message_list_core.dart b/packages/stream_chat_flutter_core/lib/src/message_list_core.dart index b9b423ce..74e40d7f 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_list_core.dart @@ -9,6 +9,14 @@ import 'package:stream_chat_flutter_core/src/better_stream_builder.dart'; import 'package:stream_chat_flutter_core/src/stream_channel.dart'; import 'package:stream_chat_flutter_core/src/typedef.dart'; +/// Default filter for the message list +bool Function(Message) defaultMessageFilter(String currentUserId) => + (Message m) { + final isMyMessage = m.user?.id == currentUserId; + if (m.shadowed && !isMyMessage) return false; + return true; + }; + /// [MessageListCore] is a simplified class that allows fetching a list of /// messages while exposing UI builders. /// @@ -132,25 +140,20 @@ class MessageListCoreState extends State { ? _streamChannel!.channel.state?.threads[widget.parentMessage!.id] : _streamChannel!.channel.state?.messages; - bool defaultFilter(Message m) { - final isMyMessage = m.user?.id == _currentUser?.id; - if (m.shadowed && !isMyMessage) return false; - return true; - } - return BetterStreamBuilder>( initialData: initialData, comparator: const ListEquality().equals, - stream: messagesStream!.map( - (messages) => - messages?.where(widget.messageFilter ?? defaultFilter).toList( - growable: false, - ), - ), + stream: messagesStream, errorBuilder: widget.errorBuilder, noDataBuilder: widget.loadingBuilder, builder: (context, data) { - final messageList = data.reversed.toList(growable: false); + final messageList = data + .where( + widget.messageFilter ?? defaultMessageFilter(_currentUser!.id), + ) + .toList(growable: false) + .reversed + .toList(growable: false); if (messageList.isEmpty && !_isThreadConversation) { if (_upToDate) { return widget.emptyBuilder(context); diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml index 1bc8a1f9..85b82caa 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: 3.1.1 +version: 3.2.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -11,17 +11,17 @@ environment: dependencies: collection: ^1.15.0 - connectivity_plus: ^1.0.1 + connectivity_plus: ^2.0.2 flutter: sdk: flutter meta: ^1.3.0 rxdart: ^0.27.0 - stream_chat: ^3.1.1 + stream_chat: ^3.2.0 dev_dependencies: dart_code_metrics: ^4.4.0 fake_async: ^1.2.0 flutter_test: sdk: flutter - mocktail: ^0.1.3 + mocktail: ^0.2.0 diff --git a/packages/stream_chat_flutter_core/test/channels_bloc_test.dart b/packages/stream_chat_flutter_core/test/channels_bloc_test.dart index a237a23c..e6365868 100644 --- a/packages/stream_chat_flutter_core/test/channels_bloc_test.dart +++ b/packages/stream_chat_flutter_core/test/channels_bloc_test.dart @@ -10,7 +10,7 @@ import 'mocks.dart'; void main() { setUpAll(() { - registerFallbackValue(const PaginationParams()); + registerFallbackValue(const PaginationParams()); }); List _generateChannels( diff --git a/packages/stream_chat_persistence/build.yaml b/packages/stream_chat_persistence/build.yaml index 373486d0..9b900235 100644 --- a/packages/stream_chat_persistence/build.yaml +++ b/packages/stream_chat_persistence/build.yaml @@ -1,7 +1,7 @@ targets: $default: builders: - moor_generator: + drift_dev: options: generate_connect_constructor: true data_class_to_companions: false diff --git a/packages/stream_chat_persistence/moor_schemas/moor_schema_v1.json b/packages/stream_chat_persistence/drift_schemas/drift_schema_v1.json similarity index 100% rename from packages/stream_chat_persistence/moor_schemas/moor_schema_v1.json rename to packages/stream_chat_persistence/drift_schemas/drift_schema_v1.json diff --git a/packages/stream_chat_persistence/moor_schemas/moor_schema_v2.json b/packages/stream_chat_persistence/drift_schemas/drift_schema_v2.json similarity index 100% rename from packages/stream_chat_persistence/moor_schemas/moor_schema_v2.json rename to packages/stream_chat_persistence/drift_schemas/drift_schema_v2.json diff --git a/packages/stream_chat_persistence/lib/src/converter/list_converter.dart b/packages/stream_chat_persistence/lib/src/converter/list_converter.dart index d813afc3..f1b8cec2 100644 --- a/packages/stream_chat_persistence/lib/src/converter/list_converter.dart +++ b/packages/stream_chat_persistence/lib/src/converter/list_converter.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:moor/moor.dart'; +import 'package:drift/drift.dart'; /// Maps a [List] of type [T] into a [String] understood /// by the sqlite backend. diff --git a/packages/stream_chat_persistence/lib/src/converter/map_converter.dart b/packages/stream_chat_persistence/lib/src/converter/map_converter.dart index 64284a9d..7afd9225 100644 --- a/packages/stream_chat_persistence/lib/src/converter/map_converter.dart +++ b/packages/stream_chat_persistence/lib/src/converter/map_converter.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:moor/moor.dart'; +import 'package:drift/drift.dart'; /// Maps a [Map] of type [String], [T] into a [String] understood /// by the sqlite backend. diff --git a/packages/stream_chat_persistence/lib/src/converter/message_sending_status_converter.dart b/packages/stream_chat_persistence/lib/src/converter/message_sending_status_converter.dart index 9d6ea11f..a6bb6d73 100644 --- a/packages/stream_chat_persistence/lib/src/converter/message_sending_status_converter.dart +++ b/packages/stream_chat_persistence/lib/src/converter/message_sending_status_converter.dart @@ -1,4 +1,4 @@ -import 'package:moor/moor.dart'; +import 'package:drift/drift.dart'; import 'package:stream_chat/stream_chat.dart'; /// Maps a [MessageSendingStatus] into a [int] understood diff --git a/packages/stream_chat_persistence/lib/src/dao/channel_dao.dart b/packages/stream_chat_persistence/lib/src/dao/channel_dao.dart index 36013f51..50f3b2a6 100644 --- a/packages/stream_chat_persistence/lib/src/dao/channel_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/channel_dao.dart @@ -1,6 +1,6 @@ -import 'package:moor/moor.dart'; +import 'package:drift/drift.dart'; import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import 'package:stream_chat_persistence/src/entity/channels.dart'; import 'package:stream_chat_persistence/src/entity/users.dart'; import 'package:stream_chat_persistence/src/mapper/mapper.dart'; @@ -8,11 +8,11 @@ import 'package:stream_chat_persistence/src/mapper/mapper.dart'; part 'channel_dao.g.dart'; /// The Data Access Object for operations in [Channels] table. -@UseDao(tables: [Channels, Users]) -class ChannelDao extends DatabaseAccessor +@DriftAccessor(tables: [Channels, Users]) +class ChannelDao extends DatabaseAccessor with _$ChannelDaoMixin { /// Creates a new channel dao instance - ChannelDao(MoorChatDatabase db) : super(db); + ChannelDao(DriftChatDatabase db) : super(db); /// Get channel by cid Future getChannelByCid(String cid) async => diff --git a/packages/stream_chat_persistence/lib/src/dao/channel_dao.g.dart b/packages/stream_chat_persistence/lib/src/dao/channel_dao.g.dart index a3ac1713..5c4cce4a 100644 --- a/packages/stream_chat_persistence/lib/src/dao/channel_dao.g.dart +++ b/packages/stream_chat_persistence/lib/src/dao/channel_dao.g.dart @@ -6,7 +6,7 @@ part of 'channel_dao.dart'; // DaoGenerator // ************************************************************************** -mixin _$ChannelDaoMixin on DatabaseAccessor { +mixin _$ChannelDaoMixin on DatabaseAccessor { $ChannelsTable get channels => attachedDatabase.channels; $UsersTable get users => attachedDatabase.users; } diff --git a/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart b/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart index d1ed0925..75a5db13 100644 --- a/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart @@ -1,8 +1,8 @@ import 'dart:convert'; -import 'package:moor/moor.dart'; +import 'package:drift/drift.dart'; import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import 'package:stream_chat_persistence/src/entity/channel_queries.dart'; import 'package:stream_chat_persistence/src/entity/channels.dart'; import 'package:stream_chat_persistence/src/entity/users.dart'; @@ -11,11 +11,11 @@ import 'package:stream_chat_persistence/src/mapper/mapper.dart'; part 'channel_query_dao.g.dart'; /// The Data Access Object for operations in [ChannelQueries] table. -@UseDao(tables: [ChannelQueries, Channels, Users]) -class ChannelQueryDao extends DatabaseAccessor +@DriftAccessor(tables: [ChannelQueries, Channels, Users]) +class ChannelQueryDao extends DatabaseAccessor with _$ChannelQueryDaoMixin { /// Creates a new channel query dao instance - ChannelQueryDao(MoorChatDatabase db) : super(db); + ChannelQueryDao(DriftChatDatabase db) : super(db); String _computeHash(Filter? filter) { if (filter == null) { diff --git a/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.g.dart b/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.g.dart index 5879d8bb..3f8f94cb 100644 --- a/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.g.dart +++ b/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.g.dart @@ -6,7 +6,7 @@ part of 'channel_query_dao.dart'; // DaoGenerator // ************************************************************************** -mixin _$ChannelQueryDaoMixin on DatabaseAccessor { +mixin _$ChannelQueryDaoMixin on DatabaseAccessor { $ChannelQueriesTable get channelQueries => attachedDatabase.channelQueries; $ChannelsTable get channels => attachedDatabase.channels; $UsersTable get users => attachedDatabase.users; diff --git a/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.dart b/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.dart index 88a03b29..b9b4a52d 100644 --- a/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.dart @@ -1,6 +1,6 @@ -import 'package:moor/moor.dart'; +import 'package:drift/drift.dart'; import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import 'package:stream_chat_persistence/src/entity/connection_events.dart'; import 'package:stream_chat_persistence/src/mapper/mapper.dart'; @@ -8,11 +8,11 @@ import 'package:stream_chat_persistence/src/mapper/mapper.dart'; part 'connection_event_dao.g.dart'; /// The Data Access Object for operations in [ConnectionEvents] table. -@UseDao(tables: [ConnectionEvents]) -class ConnectionEventDao extends DatabaseAccessor +@DriftAccessor(tables: [ConnectionEvents]) +class ConnectionEventDao extends DatabaseAccessor with _$ConnectionEventDaoMixin { /// Creates a new connection event dao instance - ConnectionEventDao(MoorChatDatabase db) : super(db); + ConnectionEventDao(DriftChatDatabase db) : super(db); /// Get the latest stored connection event Future get connectionEvent => select(connectionEvents) diff --git a/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.g.dart b/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.g.dart index 28eb0913..ed29a03b 100644 --- a/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.g.dart +++ b/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.g.dart @@ -6,7 +6,7 @@ part of 'connection_event_dao.dart'; // DaoGenerator // ************************************************************************** -mixin _$ConnectionEventDaoMixin on DatabaseAccessor { +mixin _$ConnectionEventDaoMixin on DatabaseAccessor { $ConnectionEventsTable get connectionEvents => attachedDatabase.connectionEvents; } diff --git a/packages/stream_chat_persistence/lib/src/dao/member_dao.dart b/packages/stream_chat_persistence/lib/src/dao/member_dao.dart index e471b1b5..3a604ca1 100644 --- a/packages/stream_chat_persistence/lib/src/dao/member_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/member_dao.dart @@ -1,6 +1,6 @@ -import 'package:moor/moor.dart'; +import 'package:drift/drift.dart'; import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import 'package:stream_chat_persistence/src/entity/members.dart'; import 'package:stream_chat_persistence/src/entity/users.dart'; @@ -10,11 +10,11 @@ import 'package:stream_chat_persistence/src/mapper/mapper.dart'; part 'member_dao.g.dart'; /// The Data Access Object for operations in [Members] table. -@UseDao(tables: [Members, Users]) -class MemberDao extends DatabaseAccessor +@DriftAccessor(tables: [Members, Users]) +class MemberDao extends DatabaseAccessor with _$MemberDaoMixin { /// Creates a new member dao instance - MemberDao(MoorChatDatabase db) : super(db); + MemberDao(DriftChatDatabase db) : super(db); /// Get all members where [Members.channelCid] matches [cid] Future> getMembersByCid(String cid) async => diff --git a/packages/stream_chat_persistence/lib/src/dao/member_dao.g.dart b/packages/stream_chat_persistence/lib/src/dao/member_dao.g.dart index dbda1351..e85b1858 100644 --- a/packages/stream_chat_persistence/lib/src/dao/member_dao.g.dart +++ b/packages/stream_chat_persistence/lib/src/dao/member_dao.g.dart @@ -6,7 +6,7 @@ part of 'member_dao.dart'; // DaoGenerator // ************************************************************************** -mixin _$MemberDaoMixin on DatabaseAccessor { +mixin _$MemberDaoMixin on DatabaseAccessor { $MembersTable get members => attachedDatabase.members; $UsersTable get users => attachedDatabase.users; } diff --git a/packages/stream_chat_persistence/lib/src/dao/message_dao.dart b/packages/stream_chat_persistence/lib/src/dao/message_dao.dart index ddaafdc5..31ca74ee 100644 --- a/packages/stream_chat_persistence/lib/src/dao/message_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/message_dao.dart @@ -1,6 +1,6 @@ -import 'package:moor/moor.dart'; +import 'package:drift/drift.dart'; import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import 'package:stream_chat_persistence/src/entity/messages.dart'; import 'package:stream_chat_persistence/src/entity/users.dart'; @@ -9,13 +9,13 @@ import 'package:stream_chat_persistence/src/mapper/mapper.dart'; part 'message_dao.g.dart'; /// The Data Access Object for operations in [Messages] table. -@UseDao(tables: [Messages, Users]) -class MessageDao extends DatabaseAccessor +@DriftAccessor(tables: [Messages, Users]) +class MessageDao extends DatabaseAccessor with _$MessageDaoMixin { /// Creates a new message dao instance MessageDao(this._db) : super(_db); - final MoorChatDatabase _db; + final DriftChatDatabase _db; $UsersTable get _users => alias(users, 'users'); diff --git a/packages/stream_chat_persistence/lib/src/dao/message_dao.g.dart b/packages/stream_chat_persistence/lib/src/dao/message_dao.g.dart index e27a5d42..42069b23 100644 --- a/packages/stream_chat_persistence/lib/src/dao/message_dao.g.dart +++ b/packages/stream_chat_persistence/lib/src/dao/message_dao.g.dart @@ -6,7 +6,7 @@ part of 'message_dao.dart'; // DaoGenerator // ************************************************************************** -mixin _$MessageDaoMixin on DatabaseAccessor { +mixin _$MessageDaoMixin on DatabaseAccessor { $MessagesTable get messages => attachedDatabase.messages; $UsersTable get users => attachedDatabase.users; } diff --git a/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart b/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart index bac52aa6..8712dea5 100644 --- a/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart @@ -1,6 +1,6 @@ -import 'package:moor/moor.dart'; +import 'package:drift/drift.dart'; import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import 'package:stream_chat_persistence/src/entity/pinned_messages.dart'; import 'package:stream_chat_persistence/src/entity/users.dart'; @@ -9,13 +9,13 @@ import 'package:stream_chat_persistence/src/mapper/mapper.dart'; part 'pinned_message_dao.g.dart'; /// The Data Access Object for operations in [Messages] table. -@UseDao(tables: [PinnedMessages, Users]) -class PinnedMessageDao extends DatabaseAccessor +@DriftAccessor(tables: [PinnedMessages, Users]) +class PinnedMessageDao extends DatabaseAccessor with _$PinnedMessageDaoMixin { /// Creates a new message dao instance PinnedMessageDao(this._db) : super(_db); - final MoorChatDatabase _db; + final DriftChatDatabase _db; $UsersTable get _users => alias(users, 'users'); diff --git a/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.g.dart b/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.g.dart index e5e07850..f695bf08 100644 --- a/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.g.dart +++ b/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.g.dart @@ -6,7 +6,7 @@ part of 'pinned_message_dao.dart'; // DaoGenerator // ************************************************************************** -mixin _$PinnedMessageDaoMixin on DatabaseAccessor { +mixin _$PinnedMessageDaoMixin on DatabaseAccessor { $PinnedMessagesTable get pinnedMessages => attachedDatabase.pinnedMessages; $UsersTable get users => attachedDatabase.users; } diff --git a/packages/stream_chat_persistence/lib/src/dao/pinned_message_reaction_dao.dart b/packages/stream_chat_persistence/lib/src/dao/pinned_message_reaction_dao.dart index 932bb838..4a37984a 100644 --- a/packages/stream_chat_persistence/lib/src/dao/pinned_message_reaction_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/pinned_message_reaction_dao.dart @@ -1,6 +1,6 @@ -import 'package:moor/moor.dart'; +import 'package:drift/drift.dart'; import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import 'package:stream_chat_persistence/src/entity/pinned_message_reactions.dart'; import 'package:stream_chat_persistence/src/entity/users.dart'; import 'package:stream_chat_persistence/src/mapper/mapper.dart'; @@ -8,11 +8,11 @@ import 'package:stream_chat_persistence/src/mapper/mapper.dart'; part 'pinned_message_reaction_dao.g.dart'; /// The Data Access Object for operations in [PinnedMessageReactions] table. -@UseDao(tables: [PinnedMessageReactions, Users]) -class PinnedMessageReactionDao extends DatabaseAccessor +@DriftAccessor(tables: [PinnedMessageReactions, Users]) +class PinnedMessageReactionDao extends DatabaseAccessor with _$PinnedMessageReactionDaoMixin { /// Creates a new reaction dao instance - PinnedMessageReactionDao(MoorChatDatabase db) : super(db); + PinnedMessageReactionDao(DriftChatDatabase db) : super(db); /// Returns all the reactions of a particular message by matching /// [Reactions.messageId] with [messageId] diff --git a/packages/stream_chat_persistence/lib/src/dao/pinned_message_reaction_dao.g.dart b/packages/stream_chat_persistence/lib/src/dao/pinned_message_reaction_dao.g.dart index 707a7c12..48e212bf 100644 --- a/packages/stream_chat_persistence/lib/src/dao/pinned_message_reaction_dao.g.dart +++ b/packages/stream_chat_persistence/lib/src/dao/pinned_message_reaction_dao.g.dart @@ -6,7 +6,7 @@ part of 'pinned_message_reaction_dao.dart'; // DaoGenerator // ************************************************************************** -mixin _$PinnedMessageReactionDaoMixin on DatabaseAccessor { +mixin _$PinnedMessageReactionDaoMixin on DatabaseAccessor { $PinnedMessageReactionsTable get pinnedMessageReactions => attachedDatabase.pinnedMessageReactions; $UsersTable get users => attachedDatabase.users; diff --git a/packages/stream_chat_persistence/lib/src/dao/reaction_dao.dart b/packages/stream_chat_persistence/lib/src/dao/reaction_dao.dart index c178ae03..7b0b5b8c 100644 --- a/packages/stream_chat_persistence/lib/src/dao/reaction_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/reaction_dao.dart @@ -1,6 +1,6 @@ -import 'package:moor/moor.dart'; +import 'package:drift/drift.dart'; import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import 'package:stream_chat_persistence/src/entity/reactions.dart'; import 'package:stream_chat_persistence/src/entity/users.dart'; import 'package:stream_chat_persistence/src/mapper/mapper.dart'; @@ -8,11 +8,11 @@ import 'package:stream_chat_persistence/src/mapper/mapper.dart'; part 'reaction_dao.g.dart'; /// The Data Access Object for operations in [Reactions] table. -@UseDao(tables: [Reactions, Users]) -class ReactionDao extends DatabaseAccessor +@DriftAccessor(tables: [Reactions, Users]) +class ReactionDao extends DatabaseAccessor with _$ReactionDaoMixin { /// Creates a new reaction dao instance - ReactionDao(MoorChatDatabase db) : super(db); + ReactionDao(DriftChatDatabase db) : super(db); /// Returns all the reactions of a particular message by matching /// [Reactions.messageId] with [messageId] diff --git a/packages/stream_chat_persistence/lib/src/dao/reaction_dao.g.dart b/packages/stream_chat_persistence/lib/src/dao/reaction_dao.g.dart index b95e2126..2eefbceb 100644 --- a/packages/stream_chat_persistence/lib/src/dao/reaction_dao.g.dart +++ b/packages/stream_chat_persistence/lib/src/dao/reaction_dao.g.dart @@ -6,7 +6,7 @@ part of 'reaction_dao.dart'; // DaoGenerator // ************************************************************************** -mixin _$ReactionDaoMixin on DatabaseAccessor { +mixin _$ReactionDaoMixin on DatabaseAccessor { $ReactionsTable get reactions => attachedDatabase.reactions; $UsersTable get users => attachedDatabase.users; } diff --git a/packages/stream_chat_persistence/lib/src/dao/read_dao.dart b/packages/stream_chat_persistence/lib/src/dao/read_dao.dart index 87bfa19b..0a578d69 100644 --- a/packages/stream_chat_persistence/lib/src/dao/read_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/read_dao.dart @@ -1,6 +1,6 @@ -import 'package:moor/moor.dart'; +import 'package:drift/drift.dart'; import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import 'package:stream_chat_persistence/src/entity/reads.dart'; import 'package:stream_chat_persistence/src/entity/users.dart'; import 'package:stream_chat_persistence/src/mapper/mapper.dart'; @@ -8,10 +8,10 @@ import 'package:stream_chat_persistence/src/mapper/mapper.dart'; part 'read_dao.g.dart'; /// The Data Access Object for operations in [Reads] table. -@UseDao(tables: [Reads, Users]) -class ReadDao extends DatabaseAccessor with _$ReadDaoMixin { +@DriftAccessor(tables: [Reads, Users]) +class ReadDao extends DatabaseAccessor with _$ReadDaoMixin { /// Creates a new read dao instance - ReadDao(MoorChatDatabase db) : super(db); + ReadDao(DriftChatDatabase db) : super(db); /// Get all reads where [Reads.channelCid] matches [cid] Future> getReadsByCid(String cid) async => (select(reads).join([ diff --git a/packages/stream_chat_persistence/lib/src/dao/read_dao.g.dart b/packages/stream_chat_persistence/lib/src/dao/read_dao.g.dart index b9ac9228..bfe5873c 100644 --- a/packages/stream_chat_persistence/lib/src/dao/read_dao.g.dart +++ b/packages/stream_chat_persistence/lib/src/dao/read_dao.g.dart @@ -6,7 +6,7 @@ part of 'read_dao.dart'; // DaoGenerator // ************************************************************************** -mixin _$ReadDaoMixin on DatabaseAccessor { +mixin _$ReadDaoMixin on DatabaseAccessor { $ReadsTable get reads => attachedDatabase.reads; $UsersTable get users => attachedDatabase.users; } diff --git a/packages/stream_chat_persistence/lib/src/dao/user_dao.dart b/packages/stream_chat_persistence/lib/src/dao/user_dao.dart index b1e29e7b..a53a4131 100644 --- a/packages/stream_chat_persistence/lib/src/dao/user_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/user_dao.dart @@ -1,16 +1,16 @@ -import 'package:moor/moor.dart'; +import 'package:drift/drift.dart'; import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import 'package:stream_chat_persistence/src/entity/users.dart'; import 'package:stream_chat_persistence/src/mapper/user_mapper.dart'; part 'user_dao.g.dart'; /// The Data Access Object for operations in [Users] table. -@UseDao(tables: [Users]) -class UserDao extends DatabaseAccessor with _$UserDaoMixin { +@DriftAccessor(tables: [Users]) +class UserDao extends DatabaseAccessor with _$UserDaoMixin { /// Creates a new user dao instance - UserDao(MoorChatDatabase db) : super(db); + UserDao(DriftChatDatabase db) : super(db); /// Updates the users data with the new [userList] data Future updateUsers(List userList) => batch( diff --git a/packages/stream_chat_persistence/lib/src/dao/user_dao.g.dart b/packages/stream_chat_persistence/lib/src/dao/user_dao.g.dart index edd9a33b..94af5378 100644 --- a/packages/stream_chat_persistence/lib/src/dao/user_dao.g.dart +++ b/packages/stream_chat_persistence/lib/src/dao/user_dao.g.dart @@ -6,6 +6,6 @@ part of 'user_dao.dart'; // DaoGenerator // ************************************************************************** -mixin _$UserDaoMixin on DatabaseAccessor { +mixin _$UserDaoMixin on DatabaseAccessor { $UsersTable get users => attachedDatabase.users; } diff --git a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart b/packages/stream_chat_persistence/lib/src/db/drift_chat_database.dart similarity index 91% rename from packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart rename to packages/stream_chat_persistence/lib/src/db/drift_chat_database.dart index 35befbc1..8561eed4 100644 --- a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart +++ b/packages/stream_chat_persistence/lib/src/db/drift_chat_database.dart @@ -1,4 +1,4 @@ -import 'package:moor/moor.dart'; +import 'package:drift/drift.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/converter/converter.dart'; @@ -7,10 +7,10 @@ import 'package:stream_chat_persistence/src/entity/entity.dart'; export 'shared/shared_db.dart'; -part 'moor_chat_database.g.dart'; +part 'drift_chat_database.g.dart'; /// A chat database implemented using moor -@UseMoor( +@DriftDatabase( tables: [ Channels, Messages, @@ -36,15 +36,15 @@ part 'moor_chat_database.g.dart'; ConnectionEventDao, ], ) -class MoorChatDatabase extends _$MoorChatDatabase { +class DriftChatDatabase extends _$DriftChatDatabase { /// Creates a new moor chat database instance - MoorChatDatabase( + DriftChatDatabase( this._userId, QueryExecutor executor, ) : super(executor); /// Instantiate a new database instance - MoorChatDatabase.connect( + DriftChatDatabase.connect( this._userId, DatabaseConnection connection, ) : super.connect(connection); diff --git a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.g.dart b/packages/stream_chat_persistence/lib/src/db/drift_chat_database.g.dart similarity index 94% rename from packages/stream_chat_persistence/lib/src/db/moor_chat_database.g.dart rename to packages/stream_chat_persistence/lib/src/db/drift_chat_database.g.dart index 7360f9ab..bd735b8b 100644 --- a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.g.dart +++ b/packages/stream_chat_persistence/lib/src/db/drift_chat_database.g.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'moor_chat_database.dart'; +part of 'drift_chat_database.dart'; // ************************************************************************** // MoorGenerator @@ -56,9 +56,7 @@ class ChannelEntity extends DataClass implements Insertable { required this.memberCount, this.createdById, this.extraData}); - factory ChannelEntity.fromData( - Map data, GeneratedDatabase db, - {String? prefix}) { + factory ChannelEntity.fromData(Map data, {String? prefix}) { final effectivePrefix = prefix ?? ''; return ChannelEntity( id: const StringType() @@ -119,7 +117,7 @@ class ChannelEntity extends DataClass implements Insertable { factory ChannelEntity.fromJson(Map json, {ValueSerializer? serializer}) { - serializer ??= moorRuntimeOptions.defaultSerializer; + serializer ??= driftRuntimeOptions.defaultSerializer; return ChannelEntity( id: serializer.fromJson(json['id']), type: serializer.fromJson(json['type']), @@ -137,7 +135,7 @@ class ChannelEntity extends DataClass implements Insertable { } @override Map toJson({ValueSerializer? serializer}) { - serializer ??= moorRuntimeOptions.defaultSerializer; + serializer ??= driftRuntimeOptions.defaultSerializer; return { 'id': serializer.toJson(id), 'type': serializer.toJson(type), @@ -202,28 +200,8 @@ class ChannelEntity extends DataClass implements Insertable { } @override - int get hashCode => $mrjf($mrjc( - id.hashCode, - $mrjc( - type.hashCode, - $mrjc( - cid.hashCode, - $mrjc( - config.hashCode, - $mrjc( - frozen.hashCode, - $mrjc( - lastMessageAt.hashCode, - $mrjc( - createdAt.hashCode, - $mrjc( - updatedAt.hashCode, - $mrjc( - deletedAt.hashCode, - $mrjc( - memberCount.hashCode, - $mrjc(createdById.hashCode, - extraData.hashCode)))))))))))); + int get hashCode => Object.hash(id, type, cid, config, frozen, lastMessageAt, + createdAt, updatedAt, deletedAt, memberCount, createdById, extraData); @override bool operator ==(Object other) => identical(this, other) || @@ -561,7 +539,7 @@ class $ChannelsTable extends Channels Set get $primaryKey => {cid}; @override ChannelEntity map(Map data, {String? tablePrefix}) { - return ChannelEntity.fromData(data, _db, + return ChannelEntity.fromData(data, prefix: tablePrefix != null ? '$tablePrefix.' : null); } @@ -678,9 +656,7 @@ class MessageEntity extends DataClass implements Insertable { required this.channelCid, this.i18n, this.extraData}); - factory MessageEntity.fromData( - Map data, GeneratedDatabase db, - {String? prefix}) { + factory MessageEntity.fromData(Map data, {String? prefix}) { final effectivePrefix = prefix ?? ''; return MessageEntity( id: const StringType() @@ -814,7 +790,7 @@ class MessageEntity extends DataClass implements Insertable { factory MessageEntity.fromJson(Map json, {ValueSerializer? serializer}) { - serializer ??= moorRuntimeOptions.defaultSerializer; + serializer ??= driftRuntimeOptions.defaultSerializer; return MessageEntity( id: serializer.fromJson(json['id']), messageText: serializer.fromJson(json['messageText']), @@ -847,7 +823,7 @@ class MessageEntity extends DataClass implements Insertable { } @override Map toJson({ValueSerializer? serializer}) { - serializer ??= moorRuntimeOptions.defaultSerializer; + serializer ??= driftRuntimeOptions.defaultSerializer; return { 'id': serializer.toJson(id), 'messageText': serializer.toJson(messageText), @@ -969,49 +945,33 @@ class MessageEntity extends DataClass implements Insertable { } @override - int get hashCode => $mrjf($mrjc( - id.hashCode, - $mrjc( - messageText.hashCode, - $mrjc( - attachments.hashCode, - $mrjc( - status.hashCode, - $mrjc( - type.hashCode, - $mrjc( - mentionedUsers.hashCode, - $mrjc( - reactionCounts.hashCode, - $mrjc( - reactionScores.hashCode, - $mrjc( - parentId.hashCode, - $mrjc( - quotedMessageId.hashCode, - $mrjc( - replyCount.hashCode, - $mrjc( - showInChannel.hashCode, - $mrjc( - shadowed.hashCode, - $mrjc( - command.hashCode, - $mrjc( - createdAt - .hashCode, - $mrjc( - updatedAt - .hashCode, - $mrjc( - deletedAt - .hashCode, - $mrjc( - userId - .hashCode, - $mrjc( - pinned.hashCode, - $mrjc(pinnedAt.hashCode, $mrjc(pinExpires.hashCode, $mrjc(pinnedByUserId.hashCode, $mrjc(channelCid.hashCode, $mrjc(i18n.hashCode, extraData.hashCode))))))))))))))))))))))))); + int get hashCode => Object.hashAll([ + id, + messageText, + attachments, + status, + type, + mentionedUsers, + reactionCounts, + reactionScores, + parentId, + quotedMessageId, + replyCount, + showInChannel, + shadowed, + command, + createdAt, + updatedAt, + deletedAt, + userId, + pinned, + pinnedAt, + pinExpires, + pinnedByUserId, + channelCid, + i18n, + extraData + ]); @override bool operator ==(Object other) => identical(this, other) || @@ -1644,7 +1604,7 @@ class $MessagesTable extends Messages Set get $primaryKey => {id}; @override MessageEntity map(Map data, {String? tablePrefix}) { - return MessageEntity.fromData(data, _db, + return MessageEntity.fromData(data, prefix: tablePrefix != null ? '$tablePrefix.' : null); } @@ -1772,8 +1732,7 @@ class PinnedMessageEntity extends DataClass required this.channelCid, this.i18n, this.extraData}); - factory PinnedMessageEntity.fromData( - Map data, GeneratedDatabase db, + factory PinnedMessageEntity.fromData(Map data, {String? prefix}) { final effectivePrefix = prefix ?? ''; return PinnedMessageEntity( @@ -1911,7 +1870,7 @@ class PinnedMessageEntity extends DataClass factory PinnedMessageEntity.fromJson(Map json, {ValueSerializer? serializer}) { - serializer ??= moorRuntimeOptions.defaultSerializer; + serializer ??= driftRuntimeOptions.defaultSerializer; return PinnedMessageEntity( id: serializer.fromJson(json['id']), messageText: serializer.fromJson(json['messageText']), @@ -1944,7 +1903,7 @@ class PinnedMessageEntity extends DataClass } @override Map toJson({ValueSerializer? serializer}) { - serializer ??= moorRuntimeOptions.defaultSerializer; + serializer ??= driftRuntimeOptions.defaultSerializer; return { 'id': serializer.toJson(id), 'messageText': serializer.toJson(messageText), @@ -2066,49 +2025,33 @@ class PinnedMessageEntity extends DataClass } @override - int get hashCode => $mrjf($mrjc( - id.hashCode, - $mrjc( - messageText.hashCode, - $mrjc( - attachments.hashCode, - $mrjc( - status.hashCode, - $mrjc( - type.hashCode, - $mrjc( - mentionedUsers.hashCode, - $mrjc( - reactionCounts.hashCode, - $mrjc( - reactionScores.hashCode, - $mrjc( - parentId.hashCode, - $mrjc( - quotedMessageId.hashCode, - $mrjc( - replyCount.hashCode, - $mrjc( - showInChannel.hashCode, - $mrjc( - shadowed.hashCode, - $mrjc( - command.hashCode, - $mrjc( - createdAt - .hashCode, - $mrjc( - updatedAt - .hashCode, - $mrjc( - deletedAt - .hashCode, - $mrjc( - userId - .hashCode, - $mrjc( - pinned.hashCode, - $mrjc(pinnedAt.hashCode, $mrjc(pinExpires.hashCode, $mrjc(pinnedByUserId.hashCode, $mrjc(channelCid.hashCode, $mrjc(i18n.hashCode, extraData.hashCode))))))))))))))))))))))))); + int get hashCode => Object.hashAll([ + id, + messageText, + attachments, + status, + type, + mentionedUsers, + reactionCounts, + reactionScores, + parentId, + quotedMessageId, + replyCount, + showInChannel, + shadowed, + command, + createdAt, + updatedAt, + deletedAt, + userId, + pinned, + pinnedAt, + pinExpires, + pinnedByUserId, + channelCid, + i18n, + extraData + ]); @override bool operator ==(Object other) => identical(this, other) || @@ -2744,7 +2687,7 @@ class $PinnedMessagesTable extends PinnedMessages Set get $primaryKey => {id}; @override PinnedMessageEntity map(Map data, {String? tablePrefix}) { - return PinnedMessageEntity.fromData(data, _db, + return PinnedMessageEntity.fromData(data, prefix: tablePrefix != null ? '$tablePrefix.' : null); } @@ -2795,8 +2738,7 @@ class PinnedMessageReactionEntity extends DataClass required this.createdAt, required this.score, this.extraData}); - factory PinnedMessageReactionEntity.fromData( - Map data, GeneratedDatabase db, + factory PinnedMessageReactionEntity.fromData(Map data, {String? prefix}) { final effectivePrefix = prefix ?? ''; return PinnedMessageReactionEntity( @@ -2832,7 +2774,7 @@ class PinnedMessageReactionEntity extends DataClass factory PinnedMessageReactionEntity.fromJson(Map json, {ValueSerializer? serializer}) { - serializer ??= moorRuntimeOptions.defaultSerializer; + serializer ??= driftRuntimeOptions.defaultSerializer; return PinnedMessageReactionEntity( userId: serializer.fromJson(json['userId']), messageId: serializer.fromJson(json['messageId']), @@ -2844,7 +2786,7 @@ class PinnedMessageReactionEntity extends DataClass } @override Map toJson({ValueSerializer? serializer}) { - serializer ??= moorRuntimeOptions.defaultSerializer; + serializer ??= driftRuntimeOptions.defaultSerializer; return { 'userId': serializer.toJson(userId), 'messageId': serializer.toJson(messageId), @@ -2884,14 +2826,8 @@ class PinnedMessageReactionEntity extends DataClass } @override - int get hashCode => $mrjf($mrjc( - userId.hashCode, - $mrjc( - messageId.hashCode, - $mrjc( - type.hashCode, - $mrjc(createdAt.hashCode, - $mrjc(score.hashCode, extraData.hashCode)))))); + int get hashCode => + Object.hash(userId, messageId, type, createdAt, score, extraData); @override bool operator ==(Object other) => identical(this, other) || @@ -3090,7 +3026,7 @@ class $PinnedMessageReactionsTable extends PinnedMessageReactions @override PinnedMessageReactionEntity map(Map data, {String? tablePrefix}) { - return PinnedMessageReactionEntity.fromData(data, _db, + return PinnedMessageReactionEntity.fromData(data, prefix: tablePrefix != null ? '$tablePrefix.' : null); } @@ -3128,9 +3064,7 @@ class ReactionEntity extends DataClass implements Insertable { required this.createdAt, required this.score, this.extraData}); - factory ReactionEntity.fromData( - Map data, GeneratedDatabase db, - {String? prefix}) { + factory ReactionEntity.fromData(Map data, {String? prefix}) { final effectivePrefix = prefix ?? ''; return ReactionEntity( userId: const StringType() @@ -3164,7 +3098,7 @@ class ReactionEntity extends DataClass implements Insertable { factory ReactionEntity.fromJson(Map json, {ValueSerializer? serializer}) { - serializer ??= moorRuntimeOptions.defaultSerializer; + serializer ??= driftRuntimeOptions.defaultSerializer; return ReactionEntity( userId: serializer.fromJson(json['userId']), messageId: serializer.fromJson(json['messageId']), @@ -3176,7 +3110,7 @@ class ReactionEntity extends DataClass implements Insertable { } @override Map toJson({ValueSerializer? serializer}) { - serializer ??= moorRuntimeOptions.defaultSerializer; + serializer ??= driftRuntimeOptions.defaultSerializer; return { 'userId': serializer.toJson(userId), 'messageId': serializer.toJson(messageId), @@ -3216,14 +3150,8 @@ class ReactionEntity extends DataClass implements Insertable { } @override - int get hashCode => $mrjf($mrjc( - userId.hashCode, - $mrjc( - messageId.hashCode, - $mrjc( - type.hashCode, - $mrjc(createdAt.hashCode, - $mrjc(score.hashCode, extraData.hashCode)))))); + int get hashCode => + Object.hash(userId, messageId, type, createdAt, score, extraData); @override bool operator ==(Object other) => identical(this, other) || @@ -3418,7 +3346,7 @@ class $ReactionsTable extends Reactions Set get $primaryKey => {messageId, type, userId}; @override ReactionEntity map(Map data, {String? tablePrefix}) { - return ReactionEntity.fromData(data, _db, + return ReactionEntity.fromData(data, prefix: tablePrefix != null ? '$tablePrefix.' : null); } @@ -3468,8 +3396,7 @@ class UserEntity extends DataClass implements Insertable { required this.online, required this.banned, required this.extraData}); - factory UserEntity.fromData(Map data, GeneratedDatabase db, - {String? prefix}) { + factory UserEntity.fromData(Map data, {String? prefix}) { final effectivePrefix = prefix ?? ''; return UserEntity( id: const StringType() @@ -3518,7 +3445,7 @@ class UserEntity extends DataClass implements Insertable { factory UserEntity.fromJson(Map json, {ValueSerializer? serializer}) { - serializer ??= moorRuntimeOptions.defaultSerializer; + serializer ??= driftRuntimeOptions.defaultSerializer; return UserEntity( id: serializer.fromJson(json['id']), role: serializer.fromJson(json['role']), @@ -3533,7 +3460,7 @@ class UserEntity extends DataClass implements Insertable { } @override Map toJson({ValueSerializer? serializer}) { - serializer ??= moorRuntimeOptions.defaultSerializer; + serializer ??= driftRuntimeOptions.defaultSerializer; return { 'id': serializer.toJson(id), 'role': serializer.toJson(role), @@ -3585,22 +3512,8 @@ class UserEntity extends DataClass implements Insertable { } @override - int get hashCode => $mrjf($mrjc( - id.hashCode, - $mrjc( - role.hashCode, - $mrjc( - language.hashCode, - $mrjc( - createdAt.hashCode, - $mrjc( - updatedAt.hashCode, - $mrjc( - lastActive.hashCode, - $mrjc( - online.hashCode, - $mrjc( - banned.hashCode, extraData.hashCode))))))))); + int get hashCode => Object.hash(id, role, language, createdAt, updatedAt, + lastActive, online, banned, extraData); @override bool operator ==(Object other) => identical(this, other) || @@ -3863,7 +3776,7 @@ class $UsersTable extends Users with TableInfo<$UsersTable, UserEntity> { Set get $primaryKey => {id}; @override UserEntity map(Map data, {String? tablePrefix}) { - return UserEntity.fromData(data, _db, + return UserEntity.fromData(data, prefix: tablePrefix != null ? '$tablePrefix.' : null); } @@ -3921,8 +3834,7 @@ class MemberEntity extends DataClass implements Insertable { required this.isModerator, required this.createdAt, required this.updatedAt}); - factory MemberEntity.fromData(Map data, GeneratedDatabase db, - {String? prefix}) { + factory MemberEntity.fromData(Map data, {String? prefix}) { final effectivePrefix = prefix ?? ''; return MemberEntity( userId: const StringType() @@ -3974,7 +3886,7 @@ class MemberEntity extends DataClass implements Insertable { factory MemberEntity.fromJson(Map json, {ValueSerializer? serializer}) { - serializer ??= moorRuntimeOptions.defaultSerializer; + serializer ??= driftRuntimeOptions.defaultSerializer; return MemberEntity( userId: serializer.fromJson(json['userId']), channelCid: serializer.fromJson(json['channelCid']), @@ -3993,7 +3905,7 @@ class MemberEntity extends DataClass implements Insertable { } @override Map toJson({ValueSerializer? serializer}) { - serializer ??= moorRuntimeOptions.defaultSerializer; + serializer ??= driftRuntimeOptions.defaultSerializer; return { 'userId': serializer.toJson(userId), 'channelCid': serializer.toJson(channelCid), @@ -4057,26 +3969,18 @@ class MemberEntity extends DataClass implements Insertable { } @override - int get hashCode => $mrjf($mrjc( - userId.hashCode, - $mrjc( - channelCid.hashCode, - $mrjc( - role.hashCode, - $mrjc( - inviteAcceptedAt.hashCode, - $mrjc( - inviteRejectedAt.hashCode, - $mrjc( - invited.hashCode, - $mrjc( - banned.hashCode, - $mrjc( - shadowBanned.hashCode, - $mrjc( - isModerator.hashCode, - $mrjc(createdAt.hashCode, - updatedAt.hashCode))))))))))); + int get hashCode => Object.hash( + userId, + channelCid, + role, + inviteAcceptedAt, + inviteRejectedAt, + invited, + banned, + shadowBanned, + isModerator, + createdAt, + updatedAt); @override bool operator ==(Object other) => identical(this, other) || @@ -4405,7 +4309,7 @@ class $MembersTable extends Members Set get $primaryKey => {userId, channelCid}; @override MemberEntity map(Map data, {String? tablePrefix}) { - return MemberEntity.fromData(data, _db, + return MemberEntity.fromData(data, prefix: tablePrefix != null ? '$tablePrefix.' : null); } @@ -4432,8 +4336,7 @@ class ReadEntity extends DataClass implements Insertable { required this.userId, required this.channelCid, required this.unreadMessages}); - factory ReadEntity.fromData(Map data, GeneratedDatabase db, - {String? prefix}) { + factory ReadEntity.fromData(Map data, {String? prefix}) { final effectivePrefix = prefix ?? ''; return ReadEntity( lastRead: const DateTimeType() @@ -4458,7 +4361,7 @@ class ReadEntity extends DataClass implements Insertable { factory ReadEntity.fromJson(Map json, {ValueSerializer? serializer}) { - serializer ??= moorRuntimeOptions.defaultSerializer; + serializer ??= driftRuntimeOptions.defaultSerializer; return ReadEntity( lastRead: serializer.fromJson(json['lastRead']), userId: serializer.fromJson(json['userId']), @@ -4468,7 +4371,7 @@ class ReadEntity extends DataClass implements Insertable { } @override Map toJson({ValueSerializer? serializer}) { - serializer ??= moorRuntimeOptions.defaultSerializer; + serializer ??= driftRuntimeOptions.defaultSerializer; return { 'lastRead': serializer.toJson(lastRead), 'userId': serializer.toJson(userId), @@ -4500,10 +4403,7 @@ class ReadEntity extends DataClass implements Insertable { } @override - int get hashCode => $mrjf($mrjc( - lastRead.hashCode, - $mrjc(userId.hashCode, - $mrjc(channelCid.hashCode, unreadMessages.hashCode)))); + int get hashCode => Object.hash(lastRead, userId, channelCid, unreadMessages); @override bool operator ==(Object other) => identical(this, other) || @@ -4660,7 +4560,7 @@ class $ReadsTable extends Reads with TableInfo<$ReadsTable, ReadEntity> { Set get $primaryKey => {userId, channelCid}; @override ReadEntity map(Map data, {String? tablePrefix}) { - return ReadEntity.fromData(data, _db, + return ReadEntity.fromData(data, prefix: tablePrefix != null ? '$tablePrefix.' : null); } @@ -4678,8 +4578,7 @@ class ChannelQueryEntity extends DataClass /// The channel cid of this query final String channelCid; ChannelQueryEntity({required this.queryHash, required this.channelCid}); - factory ChannelQueryEntity.fromData( - Map data, GeneratedDatabase db, + factory ChannelQueryEntity.fromData(Map data, {String? prefix}) { final effectivePrefix = prefix ?? ''; return ChannelQueryEntity( @@ -4699,7 +4598,7 @@ class ChannelQueryEntity extends DataClass factory ChannelQueryEntity.fromJson(Map json, {ValueSerializer? serializer}) { - serializer ??= moorRuntimeOptions.defaultSerializer; + serializer ??= driftRuntimeOptions.defaultSerializer; return ChannelQueryEntity( queryHash: serializer.fromJson(json['queryHash']), channelCid: serializer.fromJson(json['channelCid']), @@ -4707,7 +4606,7 @@ class ChannelQueryEntity extends DataClass } @override Map toJson({ValueSerializer? serializer}) { - serializer ??= moorRuntimeOptions.defaultSerializer; + serializer ??= driftRuntimeOptions.defaultSerializer; return { 'queryHash': serializer.toJson(queryHash), 'channelCid': serializer.toJson(channelCid), @@ -4729,7 +4628,7 @@ class ChannelQueryEntity extends DataClass } @override - int get hashCode => $mrjf($mrjc(queryHash.hashCode, channelCid.hashCode)); + int get hashCode => Object.hash(queryHash, channelCid); @override bool operator ==(Object other) => identical(this, other) || @@ -4835,7 +4734,7 @@ class $ChannelQueriesTable extends ChannelQueries Set get $primaryKey => {queryHash, channelCid}; @override ChannelQueryEntity map(Map data, {String? tablePrefix}) { - return ChannelQueryEntity.fromData(data, _db, + return ChannelQueryEntity.fromData(data, prefix: tablePrefix != null ? '$tablePrefix.' : null); } @@ -4875,8 +4774,7 @@ class ConnectionEventEntity extends DataClass this.unreadChannels, this.lastEventAt, this.lastSyncAt}); - factory ConnectionEventEntity.fromData( - Map data, GeneratedDatabase db, + factory ConnectionEventEntity.fromData(Map data, {String? prefix}) { final effectivePrefix = prefix ?? ''; return ConnectionEventEntity( @@ -4922,7 +4820,7 @@ class ConnectionEventEntity extends DataClass factory ConnectionEventEntity.fromJson(Map json, {ValueSerializer? serializer}) { - serializer ??= moorRuntimeOptions.defaultSerializer; + serializer ??= driftRuntimeOptions.defaultSerializer; return ConnectionEventEntity( id: serializer.fromJson(json['id']), type: serializer.fromJson(json['type']), @@ -4935,7 +4833,7 @@ class ConnectionEventEntity extends DataClass } @override Map toJson({ValueSerializer? serializer}) { - serializer ??= moorRuntimeOptions.defaultSerializer; + serializer ??= driftRuntimeOptions.defaultSerializer; return { 'id': serializer.toJson(id), 'type': serializer.toJson(type), @@ -4982,16 +4880,8 @@ class ConnectionEventEntity extends DataClass } @override - int get hashCode => $mrjf($mrjc( - id.hashCode, - $mrjc( - type.hashCode, - $mrjc( - ownUser.hashCode, - $mrjc( - totalUnreadCount.hashCode, - $mrjc(unreadChannels.hashCode, - $mrjc(lastEventAt.hashCode, lastSyncAt.hashCode))))))); + int get hashCode => Object.hash(id, type, ownUser, totalUnreadCount, + unreadChannels, lastEventAt, lastSyncAt); @override bool operator ==(Object other) => identical(this, other) || @@ -5212,7 +5102,7 @@ class $ConnectionEventsTable extends ConnectionEvents Set get $primaryKey => {id}; @override ConnectionEventEntity map(Map data, {String? tablePrefix}) { - return ConnectionEventEntity.fromData(data, _db, + return ConnectionEventEntity.fromData(data, prefix: tablePrefix != null ? '$tablePrefix.' : null); } @@ -5225,9 +5115,10 @@ class $ConnectionEventsTable extends ConnectionEvents MapConverter(); } -abstract class _$MoorChatDatabase extends GeneratedDatabase { - _$MoorChatDatabase(QueryExecutor e) : super(SqlTypeSystem.defaultInstance, e); - _$MoorChatDatabase.connect(DatabaseConnection c) : super.connect(c); +abstract class _$DriftChatDatabase extends GeneratedDatabase { + _$DriftChatDatabase(QueryExecutor e) + : super(SqlTypeSystem.defaultInstance, e); + _$DriftChatDatabase.connect(DatabaseConnection c) : super.connect(c); late final $ChannelsTable channels = $ChannelsTable(this); late final $MessagesTable messages = $MessagesTable(this); late final $PinnedMessagesTable pinnedMessages = $PinnedMessagesTable(this); @@ -5240,20 +5131,20 @@ abstract class _$MoorChatDatabase extends GeneratedDatabase { late final $ChannelQueriesTable channelQueries = $ChannelQueriesTable(this); late final $ConnectionEventsTable connectionEvents = $ConnectionEventsTable(this); - late final UserDao userDao = UserDao(this as MoorChatDatabase); - late final ChannelDao channelDao = ChannelDao(this as MoorChatDatabase); - late final MessageDao messageDao = MessageDao(this as MoorChatDatabase); + late final UserDao userDao = UserDao(this as DriftChatDatabase); + late final ChannelDao channelDao = ChannelDao(this as DriftChatDatabase); + late final MessageDao messageDao = MessageDao(this as DriftChatDatabase); late final PinnedMessageDao pinnedMessageDao = - PinnedMessageDao(this as MoorChatDatabase); + PinnedMessageDao(this as DriftChatDatabase); late final PinnedMessageReactionDao pinnedMessageReactionDao = - PinnedMessageReactionDao(this as MoorChatDatabase); - late final MemberDao memberDao = MemberDao(this as MoorChatDatabase); - late final ReactionDao reactionDao = ReactionDao(this as MoorChatDatabase); - late final ReadDao readDao = ReadDao(this as MoorChatDatabase); + PinnedMessageReactionDao(this as DriftChatDatabase); + late final MemberDao memberDao = MemberDao(this as DriftChatDatabase); + late final ReactionDao reactionDao = ReactionDao(this as DriftChatDatabase); + late final ReadDao readDao = ReadDao(this as DriftChatDatabase); late final ChannelQueryDao channelQueryDao = - ChannelQueryDao(this as MoorChatDatabase); + ChannelQueryDao(this as DriftChatDatabase); late final ConnectionEventDao connectionEventDao = - ConnectionEventDao(this as MoorChatDatabase); + ConnectionEventDao(this as DriftChatDatabase); @override Iterable get allTables => allSchemaEntities.whereType(); @override diff --git a/packages/stream_chat_persistence/lib/src/db/shared/native_db.dart b/packages/stream_chat_persistence/lib/src/db/shared/native_db.dart index d70426e1..a30dd59d 100644 --- a/packages/stream_chat_persistence/lib/src/db/shared/native_db.dart +++ b/packages/stream_chat_persistence/lib/src/db/shared/native_db.dart @@ -2,27 +2,27 @@ import 'dart:io'; import 'dart:isolate'; -import 'package:moor/ffi.dart'; -import 'package:moor/isolate.dart'; -import 'package:moor/moor.dart'; +import 'package:drift/drift.dart'; +import 'package:drift/isolate.dart'; +import 'package:drift/native.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import 'package:stream_chat_persistence/src/stream_chat_persistence_client.dart'; import 'package:stream_chat_persistence/stream_chat_persistence.dart'; -/// A Helper class to construct new instances of [MoorChatDatabase] specifically -/// for native platform applications +/// A Helper class to construct new instances of [DriftChatDatabase] +/// specifically for native platform applications. class SharedDB { - /// Returns a new instance of [MoorChatDatabase]. - static MoorChatDatabase constructDatabase( + /// Returns a new instance of [DriftChatDatabase]. + static DriftChatDatabase constructDatabase( String userId, { bool logStatements = false, ConnectionMode connectionMode = ConnectionMode.regular, }) { final dbName = 'db_$userId'; if (connectionMode == ConnectionMode.background) { - return MoorChatDatabase.connect( + return DriftChatDatabase.connect( userId, DatabaseConnection.delayed(Future(() async { final isolate = await _createMoorIsolate( @@ -33,7 +33,7 @@ class SharedDB { })), ); } - return MoorChatDatabase( + return DriftChatDatabase( userId, LazyDatabase( () async => _constructDatabase( @@ -44,7 +44,7 @@ class SharedDB { ); } - static Future _constructDatabase( + static Future _constructDatabase( String dbName, { bool logStatements = false, }) async { @@ -52,27 +52,27 @@ class SharedDB { final dir = await getApplicationDocumentsDirectory(); final path = join(dir.path, '$dbName.sqlite'); final file = File(path); - return VmDatabase(file, logStatements: logStatements); + return NativeDatabase(file, logStatements: logStatements); } if (Platform.isMacOS || Platform.isLinux) { final file = File('$dbName.sqlite'); - return VmDatabase(file, logStatements: logStatements); + return NativeDatabase(file, logStatements: logStatements); } - return VmDatabase.memory(logStatements: logStatements); + return NativeDatabase.memory(logStatements: logStatements); } static void _startBackground(_IsolateStartRequest request) { - final executor = LazyDatabase(() async => VmDatabase( + final executor = LazyDatabase(() async => NativeDatabase( File(request.targetPath), logStatements: request.logStatements, )); - final moorIsolate = MoorIsolate.inCurrent( + final moorIsolate = DriftIsolate.inCurrent( () => DatabaseConnection.fromExecutor(executor), ); request.sendMoorIsolate.send(moorIsolate); } - static Future _createMoorIsolate( + static Future _createMoorIsolate( String dbName, { bool logStatements = false, }) async { @@ -89,7 +89,7 @@ class SharedDB { ), ); - return await receivePort.first as MoorIsolate; + return await receivePort.first as DriftIsolate; } } diff --git a/packages/stream_chat_persistence/lib/src/db/shared/unsupported_db.dart b/packages/stream_chat_persistence/lib/src/db/shared/unsupported_db.dart index 1054a1df..c840aa5f 100644 --- a/packages/stream_chat_persistence/lib/src/db/shared/unsupported_db.dart +++ b/packages/stream_chat_persistence/lib/src/db/shared/unsupported_db.dart @@ -1,11 +1,11 @@ // coverage:ignore-file -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import 'package:stream_chat_persistence/stream_chat_persistence.dart'; -/// A Helper class to construct new instances of [MoorChatDatabase] +/// A Helper class to construct new instances of [DriftChatDatabase] class SharedDB { - /// Returns a new instance of [MoorChatDatabase]. - static MoorChatDatabase constructDatabase( + /// Returns a new instance of [DriftChatDatabase]. + static DriftChatDatabase constructDatabase( String userId, { bool logStatements = false, ConnectionMode connectionMode = ConnectionMode.regular, diff --git a/packages/stream_chat_persistence/lib/src/db/shared/web_db.dart b/packages/stream_chat_persistence/lib/src/db/shared/web_db.dart index 5b1d8311..394b38d0 100644 --- a/packages/stream_chat_persistence/lib/src/db/shared/web_db.dart +++ b/packages/stream_chat_persistence/lib/src/db/shared/web_db.dart @@ -1,19 +1,19 @@ // coverage:ignore-file -import 'package:moor/moor_web.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:drift/web.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import 'package:stream_chat_persistence/src/stream_chat_persistence_client.dart'; -/// A Helper class to construct new instances of [MoorChatDatabase] specifically -/// for Web applications +/// A Helper class to construct new instances of [DriftChatDatabase] +/// specifically for Web applications. class SharedDB { - /// Returns a new instance of [MoorChatDatabase]. - static MoorChatDatabase constructDatabase( + /// Returns a new instance of [DriftChatDatabase]. + static DriftChatDatabase constructDatabase( String userId, { bool logStatements = false, ConnectionMode connectionMode = ConnectionMode.regular, // Ignored on web }) { final dbName = 'db_$userId'; final queryExecutor = WebDatabase(dbName, logStatements: logStatements); - return MoorChatDatabase(userId, queryExecutor); + return DriftChatDatabase(userId, queryExecutor); } } diff --git a/packages/stream_chat_persistence/lib/src/entity/channel_queries.dart b/packages/stream_chat_persistence/lib/src/entity/channel_queries.dart index 61985d26..a9d3dc6c 100644 --- a/packages/stream_chat_persistence/lib/src/entity/channel_queries.dart +++ b/packages/stream_chat_persistence/lib/src/entity/channel_queries.dart @@ -1,5 +1,5 @@ // coverage:ignore-file -import 'package:moor/moor.dart'; +import 'package:drift/drift.dart'; /// Represents a [ChannelQueries] table in [MoorChatDatabase]. @DataClassName('ChannelQueryEntity') diff --git a/packages/stream_chat_persistence/lib/src/entity/channels.dart b/packages/stream_chat_persistence/lib/src/entity/channels.dart index 2c65719b..a685db68 100644 --- a/packages/stream_chat_persistence/lib/src/entity/channels.dart +++ b/packages/stream_chat_persistence/lib/src/entity/channels.dart @@ -1,5 +1,5 @@ // coverage:ignore-file -import 'package:moor/moor.dart'; +import 'package:drift/drift.dart'; import 'package:stream_chat_persistence/src/converter/map_converter.dart'; /// Represents a [Channels] table in [MoorChatDatabase]. diff --git a/packages/stream_chat_persistence/lib/src/entity/connection_events.dart b/packages/stream_chat_persistence/lib/src/entity/connection_events.dart index 9e91a9cb..7853e08e 100644 --- a/packages/stream_chat_persistence/lib/src/entity/connection_events.dart +++ b/packages/stream_chat_persistence/lib/src/entity/connection_events.dart @@ -1,5 +1,5 @@ // coverage:ignore-file -import 'package:moor/moor.dart'; +import 'package:drift/drift.dart'; import 'package:stream_chat_persistence/src/converter/map_converter.dart'; /// Represents a [ConnectionEvents] table in [MoorChatDatabase]. diff --git a/packages/stream_chat_persistence/lib/src/entity/members.dart b/packages/stream_chat_persistence/lib/src/entity/members.dart index 2773ddaa..c1709981 100644 --- a/packages/stream_chat_persistence/lib/src/entity/members.dart +++ b/packages/stream_chat_persistence/lib/src/entity/members.dart @@ -1,5 +1,5 @@ // coverage:ignore-file -import 'package:moor/moor.dart'; +import 'package:drift/drift.dart'; /// Represents a [Members] table in [MoorChatDatabase]. @DataClassName('MemberEntity') diff --git a/packages/stream_chat_persistence/lib/src/entity/messages.dart b/packages/stream_chat_persistence/lib/src/entity/messages.dart index 108b903f..f40bd292 100644 --- a/packages/stream_chat_persistence/lib/src/entity/messages.dart +++ b/packages/stream_chat_persistence/lib/src/entity/messages.dart @@ -1,5 +1,5 @@ // coverage:ignore-file -import 'package:moor/moor.dart'; +import 'package:drift/drift.dart'; import 'package:stream_chat_persistence/src/converter/list_converter.dart'; import 'package:stream_chat_persistence/src/converter/map_converter.dart'; import 'package:stream_chat_persistence/src/converter/message_sending_status_converter.dart'; diff --git a/packages/stream_chat_persistence/lib/src/entity/pinned_message_reactions.dart b/packages/stream_chat_persistence/lib/src/entity/pinned_message_reactions.dart index fbc9f5ca..8228ee5f 100644 --- a/packages/stream_chat_persistence/lib/src/entity/pinned_message_reactions.dart +++ b/packages/stream_chat_persistence/lib/src/entity/pinned_message_reactions.dart @@ -1,5 +1,5 @@ // coverage:ignore-file -import 'package:moor/moor.dart'; +import 'package:drift/drift.dart'; import 'package:stream_chat_persistence/src/entity/reactions.dart'; diff --git a/packages/stream_chat_persistence/lib/src/entity/pinned_messages.dart b/packages/stream_chat_persistence/lib/src/entity/pinned_messages.dart index 043f69c5..61672fc8 100644 --- a/packages/stream_chat_persistence/lib/src/entity/pinned_messages.dart +++ b/packages/stream_chat_persistence/lib/src/entity/pinned_messages.dart @@ -1,5 +1,5 @@ // coverage:ignore-file -import 'package:moor/moor.dart'; +import 'package:drift/drift.dart'; import 'package:stream_chat_persistence/src/entity/messages.dart'; diff --git a/packages/stream_chat_persistence/lib/src/entity/reactions.dart b/packages/stream_chat_persistence/lib/src/entity/reactions.dart index 62380408..712b7b18 100644 --- a/packages/stream_chat_persistence/lib/src/entity/reactions.dart +++ b/packages/stream_chat_persistence/lib/src/entity/reactions.dart @@ -1,5 +1,5 @@ // coverage:ignore-file -import 'package:moor/moor.dart'; +import 'package:drift/drift.dart'; import 'package:stream_chat_persistence/src/converter/map_converter.dart'; /// Represents a [Reactions] table in [MoorChatDatabase]. diff --git a/packages/stream_chat_persistence/lib/src/entity/reads.dart b/packages/stream_chat_persistence/lib/src/entity/reads.dart index 1f59b3c8..18d44934 100644 --- a/packages/stream_chat_persistence/lib/src/entity/reads.dart +++ b/packages/stream_chat_persistence/lib/src/entity/reads.dart @@ -1,5 +1,5 @@ // coverage:ignore-file -import 'package:moor/moor.dart'; +import 'package:drift/drift.dart'; /// Represents a [Reads] table in [MoorChatDatabase]. @DataClassName('ReadEntity') diff --git a/packages/stream_chat_persistence/lib/src/entity/users.dart b/packages/stream_chat_persistence/lib/src/entity/users.dart index 286bfb40..a5db56cc 100644 --- a/packages/stream_chat_persistence/lib/src/entity/users.dart +++ b/packages/stream_chat_persistence/lib/src/entity/users.dart @@ -1,5 +1,5 @@ // coverage:ignore-file -import 'package:moor/moor.dart'; +import 'package:drift/drift.dart'; import 'package:stream_chat_persistence/src/converter/map_converter.dart'; /// Represents a [Users] table in [MoorChatDatabase]. diff --git a/packages/stream_chat_persistence/lib/src/mapper/channel_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/channel_mapper.dart index 71f5b708..ef4111f6 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/channel_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/channel_mapper.dart @@ -1,5 +1,5 @@ import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; /// Useful mapping functions for [ChannelEntity] extension ChannelEntityX on ChannelEntity { diff --git a/packages/stream_chat_persistence/lib/src/mapper/event_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/event_mapper.dart index f0350797..0c3adb7b 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/event_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/event_mapper.dart @@ -1,5 +1,5 @@ import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; /// Useful mapping functions for [ConnectionEventEntity] extension ConnectionEventX on ConnectionEventEntity { diff --git a/packages/stream_chat_persistence/lib/src/mapper/member_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/member_mapper.dart index 414cab16..925abe16 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/member_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/member_mapper.dart @@ -1,5 +1,5 @@ import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; /// Useful mapping functions for [MemberEntity] extension MemberEntityX on MemberEntity { diff --git a/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart index f49631a9..72d3d30f 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; /// Useful mapping functions for [MessageEntity] extension MessageEntityX on MessageEntity { diff --git a/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart index d47aa109..91a25a33 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; /// Useful mapping functions for [PinnedMessageEntity] extension PinnedMessageEntityX on PinnedMessageEntity { diff --git a/packages/stream_chat_persistence/lib/src/mapper/pinned_message_reaction_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/pinned_message_reaction_mapper.dart index e4602bb0..ecab7cb4 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/pinned_message_reaction_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/pinned_message_reaction_mapper.dart @@ -1,5 +1,5 @@ import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; /// Useful mapping functions for [PinnedMessageReactionEntity] extension PinnedMessageReactionEntityX on PinnedMessageReactionEntity { diff --git a/packages/stream_chat_persistence/lib/src/mapper/reaction_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/reaction_mapper.dart index 62a0c427..a524fafe 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/reaction_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/reaction_mapper.dart @@ -1,5 +1,5 @@ import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; /// Useful mapping functions for [ReactionEntity] extension ReactionEntityX on ReactionEntity { diff --git a/packages/stream_chat_persistence/lib/src/mapper/read_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/read_mapper.dart index 889776e2..68f68898 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/read_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/read_mapper.dart @@ -1,5 +1,5 @@ import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; /// Useful mapping functions for [ReadEntity] extension ReadEntityX on ReadEntity { diff --git a/packages/stream_chat_persistence/lib/src/mapper/user_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/user_mapper.dart index 8b38a74d..f6409c39 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/user_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/user_mapper.dart @@ -1,5 +1,5 @@ import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; /// Useful mapping functions for [UserEntity] extension UserEntityX on UserEntity { 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 9d81fecd..00c4ab06 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 @@ -4,7 +4,7 @@ import 'package:meta/meta.dart'; import 'package:mutex/mutex.dart'; import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; /// Various connection modes on which [StreamChatPersistenceClient] can work enum ConnectionMode { @@ -15,8 +15,8 @@ enum ConnectionMode { background, } -/// Signature for a function which provides instance of [MoorChatDatabase] -typedef DatabaseProvider = MoorChatDatabase Function(String, ConnectionMode); +/// Signature for a function which provides instance of [DriftChatDatabase] +typedef DatabaseProvider = DriftChatDatabase Function(String, ConnectionMode); final _levelEmojiMapper = { Level.INFO: 'ℹ️', @@ -24,7 +24,7 @@ final _levelEmojiMapper = { Level.SEVERE: '🚨', }; -/// A [MoorChatDatabase] based implementation of the [ChatPersistenceClient] +/// A [DriftChatDatabase] based implementation of the [ChatPersistenceClient] class StreamChatPersistenceClient extends ChatPersistenceClient { /// Creates a new instance of the stream chat persistence client StreamChatPersistenceClient({ @@ -37,9 +37,9 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { _logger.onRecord.listen(logHandlerFunction ?? _defaultLogHandler); } - /// [MoorChatDatabase] instance used by this client. + /// [DriftChatDatabase] instance used by this client. @visibleForTesting - MoorChatDatabase? db; + DriftChatDatabase? db; final Logger _logger; final ConnectionMode _connectionMode; @@ -70,7 +70,7 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { return true; } - MoorChatDatabase _defaultDatabaseProvider( + DriftChatDatabase _defaultDatabaseProvider( String userId, ConnectionMode mode, ) => diff --git a/packages/stream_chat_persistence/pubspec.yaml b/packages/stream_chat_persistence/pubspec.yaml index 3e24da27..3bf970d3 100644 --- a/packages/stream_chat_persistence/pubspec.yaml +++ b/packages/stream_chat_persistence/pubspec.yaml @@ -10,11 +10,11 @@ environment: flutter: ">=1.17.0" dependencies: + drift: ^1.0.0 flutter: sdk: flutter logging: ^1.0.1 meta: ^1.3.0 - moor: ^4.4.0 mutex: ^3.0.0 path: ^1.8.0 path_provider: ^2.0.1 @@ -24,7 +24,8 @@ dependencies: dev_dependencies: build_runner: ^2.0.1 dart_code_metrics: ^4.4.0 + drift_dev: ^1.0.0 flutter_test: sdk: flutter - mocktail: ^0.1.1 - moor_generator: ^4.2.1 \ No newline at end of file + mocktail: ^0.2.0 + \ No newline at end of file diff --git a/packages/stream_chat_persistence/test/mock_chat_database.dart b/packages/stream_chat_persistence/test/mock_chat_database.dart index 19b115e5..196c8573 100644 --- a/packages/stream_chat_persistence/test/mock_chat_database.dart +++ b/packages/stream_chat_persistence/test/mock_chat_database.dart @@ -1,8 +1,8 @@ import 'package:mocktail/mocktail.dart'; import 'package:stream_chat_persistence/src/dao/dao.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; -class MockChatDatabase extends Mock implements MoorChatDatabase { +class MockChatDatabase extends Mock implements DriftChatDatabase { UserDao? _userDao; @override diff --git a/packages/stream_chat_persistence/test/src/dao/channel_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/channel_dao_test.dart index ff75c13c..df52caa6 100644 --- a/packages/stream_chat_persistence/test/src/dao/channel_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/channel_dao_test.dart @@ -1,13 +1,13 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/dao/channel_dao.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import '../../stream_chat_persistence_client_test.dart'; void main() { late ChannelDao channelDao; - late MoorChatDatabase database; + late DriftChatDatabase database; setUp(() { database = testDatabaseProvider('testUserId'); diff --git a/packages/stream_chat_persistence/test/src/dao/channel_query_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/channel_query_dao_test.dart index bc07919a..8319d297 100644 --- a/packages/stream_chat_persistence/test/src/dao/channel_query_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/channel_query_dao_test.dart @@ -1,13 +1,13 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/dao/channel_query_dao.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import '../../stream_chat_persistence_client_test.dart'; import '../utils/date_matcher.dart'; void main() { - late MoorChatDatabase database; + late DriftChatDatabase database; late ChannelQueryDao channelQueryDao; setUp(() { diff --git a/packages/stream_chat_persistence/test/src/dao/connection_event_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/connection_event_dao_test.dart index 6b576169..ed517366 100644 --- a/packages/stream_chat_persistence/test/src/dao/connection_event_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/connection_event_dao_test.dart @@ -1,14 +1,14 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/dao/connection_event_dao.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import '../../stream_chat_persistence_client_test.dart'; import '../utils/date_matcher.dart'; void main() { late ConnectionEventDao eventDao; - late MoorChatDatabase database; + late DriftChatDatabase database; setUp(() { database = testDatabaseProvider('testUserId'); diff --git a/packages/stream_chat_persistence/test/src/dao/member_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/member_dao_test.dart index 33531ca9..4a3a31da 100644 --- a/packages/stream_chat_persistence/test/src/dao/member_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/member_dao_test.dart @@ -3,14 +3,14 @@ import 'dart:math' as math; import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/dao/dao.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import '../../stream_chat_persistence_client_test.dart'; import '../utils/date_matcher.dart'; void main() { late MemberDao memberDao; - late MoorChatDatabase database; + late DriftChatDatabase database; setUp(() { database = testDatabaseProvider('testUserId'); diff --git a/packages/stream_chat_persistence/test/src/dao/message_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/message_dao_test.dart index 31546ecf..b31884fc 100644 --- a/packages/stream_chat_persistence/test/src/dao/message_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/message_dao_test.dart @@ -3,13 +3,13 @@ import 'dart:math' as math; import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/dao/dao.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import '../../stream_chat_persistence_client_test.dart'; void main() { late MessageDao messageDao; - late MoorChatDatabase database; + late DriftChatDatabase database; setUp(() { database = testDatabaseProvider('testUserId'); diff --git a/packages/stream_chat_persistence/test/src/dao/pinned_message_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/pinned_message_dao_test.dart index 45f2ed0a..4dcf5e0e 100644 --- a/packages/stream_chat_persistence/test/src/dao/pinned_message_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/pinned_message_dao_test.dart @@ -3,13 +3,13 @@ import 'dart:math' as math; import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/dao/dao.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import '../../stream_chat_persistence_client_test.dart'; void main() { late PinnedMessageDao pinnedMessageDao; - late MoorChatDatabase database; + late DriftChatDatabase database; setUp(() { database = testDatabaseProvider('testUserId'); diff --git a/packages/stream_chat_persistence/test/src/dao/pinned_message_reaction_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/pinned_message_reaction_dao_test.dart index 5c451ec1..44abc4a6 100644 --- a/packages/stream_chat_persistence/test/src/dao/pinned_message_reaction_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/pinned_message_reaction_dao_test.dart @@ -3,13 +3,13 @@ import 'dart:math' as math; import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/dao/pinned_message_reaction_dao.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import '../../stream_chat_persistence_client_test.dart'; void main() { late PinnedMessageReactionDao pinnedMessageReactionDao; - late MoorChatDatabase database; + late DriftChatDatabase database; setUp(() { database = testDatabaseProvider('testUserId'); diff --git a/packages/stream_chat_persistence/test/src/dao/reaction_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/reaction_dao_test.dart index 8109e75e..932a555e 100644 --- a/packages/stream_chat_persistence/test/src/dao/reaction_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/reaction_dao_test.dart @@ -3,13 +3,13 @@ import 'dart:math' as math; import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/dao/reaction_dao.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import '../../stream_chat_persistence_client_test.dart'; void main() { late ReactionDao reactionDao; - late MoorChatDatabase database; + late DriftChatDatabase database; setUp(() { database = testDatabaseProvider('testUserId'); diff --git a/packages/stream_chat_persistence/test/src/dao/read_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/read_dao_test.dart index 1cebd1eb..07d955fc 100644 --- a/packages/stream_chat_persistence/test/src/dao/read_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/read_dao_test.dart @@ -1,14 +1,14 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/dao/dao.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import '../../stream_chat_persistence_client_test.dart'; import '../utils/date_matcher.dart'; void main() { late ReadDao readDao; - late MoorChatDatabase database; + late DriftChatDatabase database; setUp(() { database = testDatabaseProvider('testUserId'); diff --git a/packages/stream_chat_persistence/test/src/dao/user_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/user_dao_test.dart index ab88881a..fb095e36 100644 --- a/packages/stream_chat_persistence/test/src/dao/user_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/user_dao_test.dart @@ -3,13 +3,13 @@ import 'dart:math' as math; import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/dao/dao.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import '../../stream_chat_persistence_client_test.dart'; void main() { late UserDao userDao; - late MoorChatDatabase database; + late DriftChatDatabase database; setUp(() { database = testDatabaseProvider('testUserId'); diff --git a/packages/stream_chat_persistence/test/src/db/moor_chat_database_test.dart b/packages/stream_chat_persistence/test/src/db/drift_chat_database_test.dart similarity index 62% rename from packages/stream_chat_persistence/test/src/db/moor_chat_database_test.dart rename to packages/stream_chat_persistence/test/src/db/drift_chat_database_test.dart index eb1b6933..441f186d 100644 --- a/packages/stream_chat_persistence/test/src/db/moor_chat_database_test.dart +++ b/packages/stream_chat_persistence/test/src/db/drift_chat_database_test.dart @@ -1,19 +1,19 @@ +import 'package:drift/drift.dart' hide isNotNull; +import 'package:drift/isolate.dart'; +import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:moor/ffi.dart'; -import 'package:moor/isolate.dart'; -import 'package:moor/moor.dart' hide isNotNull; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; DatabaseConnection _backgroundConnection() => - DatabaseConnection.fromExecutor(VmDatabase.memory()); + DatabaseConnection.fromExecutor(NativeDatabase.memory()); void main() { test( 'default constructor should create a new instance of MoorChatDatabase', () async { const userId = 'testUserId'; - final executor = VmDatabase.memory(); - final database = MoorChatDatabase(userId, executor); + final executor = NativeDatabase.memory(); + final database = DriftChatDatabase(userId, executor); expect(database, isNotNull); expect(database.userId, userId); @@ -27,10 +27,10 @@ void main() { 'connect constructor should create a new instance of MoorChatDatabase', () async { const userId = 'testUserId'; - final isolate = await MoorIsolate.spawn(_backgroundConnection); + final isolate = await DriftIsolate.spawn(_backgroundConnection); final connection = DatabaseConnection.delayed(isolate.connect()); - final database = MoorChatDatabase.connect(userId, connection); + final database = DriftChatDatabase.connect(userId, connection); expect(database, isNotNull); expect(database.userId, userId); diff --git a/packages/stream_chat_persistence/test/src/mapper/channel_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/channel_mapper_test.dart index 2c87097b..e9718af9 100644 --- a/packages/stream_chat_persistence/test/src/mapper/channel_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/channel_mapper_test.dart @@ -2,7 +2,7 @@ import 'dart:math' as math; import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import 'package:stream_chat_persistence/src/mapper/channel_mapper.dart'; import '../utils/date_matcher.dart'; diff --git a/packages/stream_chat_persistence/test/src/mapper/event_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/event_mapper_test.dart index a7c72d0d..6acc4e05 100644 --- a/packages/stream_chat_persistence/test/src/mapper/event_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/event_mapper_test.dart @@ -1,6 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import 'package:stream_chat_persistence/src/mapper/event_mapper.dart'; import '../utils/date_matcher.dart'; diff --git a/packages/stream_chat_persistence/test/src/mapper/member_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/member_mapper_test.dart index 19f299b3..a3878409 100644 --- a/packages/stream_chat_persistence/test/src/mapper/member_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/member_mapper_test.dart @@ -2,7 +2,7 @@ import 'dart:math' as math; import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import 'package:stream_chat_persistence/src/mapper/member_mapper.dart'; import '../utils/date_matcher.dart'; diff --git a/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart index d26d905b..d2ea8b93 100644 --- a/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart @@ -3,7 +3,7 @@ import 'dart:math' as math; import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import 'package:stream_chat_persistence/src/mapper/message_mapper.dart'; import '../utils/date_matcher.dart'; diff --git a/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart index 9f258b76..9c2378fd 100644 --- a/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart @@ -3,7 +3,7 @@ import 'dart:math' as math; import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import 'package:stream_chat_persistence/src/mapper/pinned_message_mapper.dart'; import '../utils/date_matcher.dart'; diff --git a/packages/stream_chat_persistence/test/src/mapper/pinned_message_reaction_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/pinned_message_reaction_mapper_test.dart index f031d941..2753f4f4 100644 --- a/packages/stream_chat_persistence/test/src/mapper/pinned_message_reaction_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/pinned_message_reaction_mapper_test.dart @@ -1,6 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import 'package:stream_chat_persistence/src/mapper/pinned_message_reaction_mapper.dart'; import '../utils/date_matcher.dart'; diff --git a/packages/stream_chat_persistence/test/src/mapper/reaction_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/reaction_mapper_test.dart index 831d6068..844eb3b8 100644 --- a/packages/stream_chat_persistence/test/src/mapper/reaction_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/reaction_mapper_test.dart @@ -1,6 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import 'package:stream_chat_persistence/src/mapper/reaction_mapper.dart'; import '../utils/date_matcher.dart'; diff --git a/packages/stream_chat_persistence/test/src/mapper/read_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/read_mapper_test.dart index f152e476..98f43ede 100644 --- a/packages/stream_chat_persistence/test/src/mapper/read_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/read_mapper_test.dart @@ -1,6 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import 'package:stream_chat_persistence/src/mapper/read_mapper.dart'; import '../utils/date_matcher.dart'; diff --git a/packages/stream_chat_persistence/test/src/mapper/user_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/user_mapper_test.dart index 7b6ef2e8..8f9340a9 100644 --- a/packages/stream_chat_persistence/test/src/mapper/user_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/user_mapper_test.dart @@ -2,7 +2,7 @@ import 'dart:math' as math; import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import 'package:stream_chat_persistence/src/mapper/user_mapper.dart'; import '../utils/date_matcher.dart'; 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 5c2b6bff..4900aab2 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 @@ -1,15 +1,15 @@ +import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:moor/ffi.dart'; import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import 'package:stream_chat_persistence/src/stream_chat_persistence_client.dart'; import 'mock_chat_database.dart'; import 'src/utils/date_matcher.dart'; -MoorChatDatabase testDatabaseProvider(String userId, [ConnectionMode? mode]) => - MoorChatDatabase(userId, VmDatabase.memory()); +DriftChatDatabase testDatabaseProvider(String userId, [ConnectionMode? mode]) => + DriftChatDatabase(userId, NativeDatabase.memory()); void main() { group('connect', () { @@ -19,7 +19,7 @@ void main() { expect(client.db, isNull); await client.connect(userId, databaseProvider: testDatabaseProvider); expect(client.db, isNotNull); - expect(client.db, isA()); + expect(client.db, isA()); expect(client.db!.userId, userId); addTearDown(() async { @@ -33,7 +33,7 @@ void main() { await client.connect(userId, databaseProvider: testDatabaseProvider); expect(client.db, isNotNull); expect(client.db, isNotNull); - expect(client.db, isA()); + expect(client.db, isA()); expect(client.db!.userId, userId); expect( () => client.connect(userId, databaseProvider: testDatabaseProvider), @@ -58,7 +58,7 @@ void main() { group('client functions', () { const userId = 'testUserId'; final mockDatabase = MockChatDatabase(); - MoorChatDatabase _mockDatabaseProvider(_, __) => mockDatabase; + DriftChatDatabase _mockDatabaseProvider(_, __) => mockDatabase; late StreamChatPersistenceClient client; setUp(() async {