Merge branch 'develop' of https://github.com/GetStream/stream-chat-flutter into message-input-controller
This commit is contained in:
@@ -4,6 +4,7 @@ analyzer:
|
||||
exclude:
|
||||
- packages/*/lib/**/*.g.dart
|
||||
- packages/*/lib/src/emoji/**
|
||||
- packages/*/lib/scrollable_positioned_list/**
|
||||
- packages/*/lib/**/*.freezed.dart
|
||||
|
||||
linter:
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 128 KiB |
@@ -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:
|
||||
|
||||

|
||||
|
||||
## 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<JsonWebKeyPair> 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<List<int>> 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<String> encryptMessage(String message, List<int> 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<String> decryptMessage(String encryptedMessage, List<int> 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<String>(
|
||||
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).
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1934,8 +1934,6 @@ class ChannelClientState {
|
||||
read: newReads,
|
||||
pinnedMessages: updatedState.pinnedMessages,
|
||||
);
|
||||
|
||||
_computeUnread();
|
||||
}
|
||||
|
||||
int _sortByCreatedAt(Message a, Message b) =>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
@@ -118,9 +118,9 @@ void main() {
|
||||
|
||||
setUpAll(() {
|
||||
// Fallback values
|
||||
registerFallbackValue<Message>(FakeMessage());
|
||||
registerFallbackValue<List<Message>>(<Message>[]);
|
||||
registerFallbackValue<AttachmentFile>(FakeAttachmentFile());
|
||||
registerFallbackValue(FakeMessage());
|
||||
registerFallbackValue(<Message>[]);
|
||||
registerFallbackValue(FakeAttachmentFile());
|
||||
|
||||
// detached loggers
|
||||
when(() => client.detachedLogger(any())).thenAnswer((invocation) {
|
||||
@@ -176,9 +176,9 @@ void main() {
|
||||
|
||||
setUpAll(() {
|
||||
// Fallback values
|
||||
registerFallbackValue<Message>(FakeMessage());
|
||||
registerFallbackValue<AttachmentFile>(FakeAttachmentFile());
|
||||
registerFallbackValue<Event>(FakeEvent());
|
||||
registerFallbackValue(FakeMessage());
|
||||
registerFallbackValue(FakeAttachmentFile());
|
||||
registerFallbackValue(FakeEvent());
|
||||
|
||||
// detached loggers
|
||||
when(() => client.detachedLogger(any())).thenAnswer((invocation) {
|
||||
|
||||
@@ -31,7 +31,7 @@ void main() {
|
||||
|
||||
setUpAll(() {
|
||||
// fallback values
|
||||
registerFallbackValue<User>(FakeUser());
|
||||
registerFallbackValue(FakeUser());
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
@@ -230,7 +230,7 @@ void main() {
|
||||
|
||||
setUpAll(() {
|
||||
// fallback values
|
||||
registerFallbackValue<User>(FakeUser());
|
||||
registerFallbackValue(FakeUser());
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
@@ -311,7 +311,7 @@ void main() {
|
||||
|
||||
setUpAll(() {
|
||||
// fallback values
|
||||
registerFallbackValue<User>(FakeUser());
|
||||
registerFallbackValue(FakeUser());
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
@@ -399,7 +399,7 @@ void main() {
|
||||
|
||||
setUpAll(() {
|
||||
// fallback values
|
||||
registerFallbackValue<User>(FakeUser());
|
||||
registerFallbackValue(FakeUser());
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
@@ -523,9 +523,9 @@ void main() {
|
||||
|
||||
setUpAll(() {
|
||||
// fallback values
|
||||
registerFallbackValue<Event>(FakeEvent());
|
||||
registerFallbackValue<PaginationParams>(const PaginationParams());
|
||||
registerFallbackValue<ChannelState>(FakeChannelState());
|
||||
registerFallbackValue(FakeEvent());
|
||||
registerFallbackValue(const PaginationParams());
|
||||
registerFallbackValue(FakeChannelState());
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
@@ -827,9 +827,9 @@ void main() {
|
||||
|
||||
setUpAll(() {
|
||||
// fallback values
|
||||
registerFallbackValue<Event>(FakeEvent());
|
||||
registerFallbackValue<Message>(FakeMessage());
|
||||
registerFallbackValue<PaginationParams>(const PaginationParams());
|
||||
registerFallbackValue(FakeEvent());
|
||||
registerFallbackValue(FakeMessage());
|
||||
registerFallbackValue(const PaginationParams());
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
|
||||
@@ -15,7 +15,7 @@ void main() {
|
||||
|
||||
setUp(() {
|
||||
fileUploader = StreamAttachmentFileUploader(client);
|
||||
registerFallbackValue<MultipartFile>(FakeMultiPartFile());
|
||||
registerFallbackValue(FakeMultiPartFile());
|
||||
});
|
||||
|
||||
Response successResponse(String path, {Object? data}) => Response(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
+7
@@ -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';
|
||||
@@ -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<Set<Element>?>? elementNotifier;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> 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<RegistryWidget> {
|
||||
final Set<Element> 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();
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
+62
@@ -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<Iterable<ItemPosition>> 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)''';
|
||||
}
|
||||
+13
@@ -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<Iterable<ItemPosition>> itemPositions = ValueNotifier([]);
|
||||
}
|
||||
@@ -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<StatefulWidget> createState() => _PositionedListState();
|
||||
}
|
||||
|
||||
class _PositionedListState extends State<PositionedList> {
|
||||
final Key _centerKey = UniqueKey();
|
||||
|
||||
final registeredElements = ValueNotifier<Set<Element>?>(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: <Widget>[
|
||||
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 = <ItemPosition>[];
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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<Widget> slivers = const <Widget>[],
|
||||
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<Widget> 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
+593
@@ -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<StatefulWidget> 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<void> scrollTo({
|
||||
required int index,
|
||||
double alignment = 0,
|
||||
required Duration duration,
|
||||
Curve curve = Curves.linear,
|
||||
List<double> 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<ScrollablePositionedList>
|
||||
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<double>(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: <Widget>[
|
||||
PostMountCallback(
|
||||
key: primary.key,
|
||||
callback: startAnimationCallback,
|
||||
child: FadeTransition(
|
||||
opacity: ReverseAnimation(opacity),
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
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<ScrollNotification>(
|
||||
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<void> _scrollTo({
|
||||
required int index,
|
||||
required double alignment,
|
||||
required Duration duration,
|
||||
Curve curve = Curves.linear,
|
||||
required List<double> 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<void> _startScroll({
|
||||
required int index,
|
||||
required double alignment,
|
||||
required Duration duration,
|
||||
Curve curve = Curves.linear,
|
||||
required List<double> 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<void>();
|
||||
final endCompleter = Completer<void>();
|
||||
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<void>([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<double>(0);
|
||||
});
|
||||
}
|
||||
|
||||
Animatable<double> _opacityAnimation(List<double> opacityAnimationWeights) {
|
||||
const startOpacity = 0.0;
|
||||
const endOpacity = 1.0;
|
||||
return TweenSequence<double>(<TweenSequenceItem<double>>[
|
||||
TweenSequenceItem<double>(
|
||||
tween: ConstantTween<double>(startOpacity),
|
||||
weight: opacityAnimationWeights[0],
|
||||
),
|
||||
TweenSequenceItem<double>(
|
||||
tween: Tween<double>(begin: startOpacity, end: endOpacity),
|
||||
weight: opacityAnimationWeights[1],
|
||||
),
|
||||
TweenSequenceItem<double>(
|
||||
tween: ConstantTween<double>(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;
|
||||
}
|
||||
@@ -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<Widget> slivers = const <Widget>[],
|
||||
}) : _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<RenderSliver>? 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<double>(math.min(0, top), bottom);
|
||||
final minScrollOffset = math.min<double>(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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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, __) {
|
||||
|
||||
@@ -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<MessageListView> {
|
||||
ItemScrollController? _scrollController;
|
||||
void Function(Message)? _onThreadTap;
|
||||
bool _showScrollToBottom = false;
|
||||
final ValueNotifier<bool> _showScrollToBottom = ValueNotifier(false);
|
||||
late final ItemPositionsListener _itemPositionListener;
|
||||
int? _messageListLength;
|
||||
StreamChannelState? streamChannel;
|
||||
@@ -306,12 +306,17 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
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<MessageListView> {
|
||||
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<MessageListView> {
|
||||
|
||||
bool get _isThreadConversation => widget.parentMessage != null;
|
||||
|
||||
bool _topPaginationActive = false;
|
||||
bool _bottomPaginationActive = false;
|
||||
|
||||
int initialIndex = 0;
|
||||
@@ -337,6 +341,8 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
|
||||
List<Message> messages = <Message>[];
|
||||
|
||||
Map<String, int> messagesIndex = {};
|
||||
|
||||
bool initialMessageHighlightComplete = false;
|
||||
|
||||
bool _inBetweenList = false;
|
||||
@@ -382,6 +388,9 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
|
||||
Widget _buildListView(List<Message> 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<MessageListView> {
|
||||
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<MessageListView> {
|
||||
onStartOfPage: () async {
|
||||
_inBetweenList = false;
|
||||
if (!_upToDate) {
|
||||
_topPaginationActive = false;
|
||||
_bottomPaginationActive = true;
|
||||
return _paginateData(
|
||||
streamChannel,
|
||||
@@ -451,7 +458,6 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
},
|
||||
onEndOfPage: () async {
|
||||
_inBetweenList = false;
|
||||
_topPaginationActive = true;
|
||||
_bottomPaginationActive = false;
|
||||
return _paginateData(
|
||||
streamChannel,
|
||||
@@ -462,17 +468,26 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
_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<String>?;
|
||||
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<MessageListView> {
|
||||
} else {
|
||||
messageWidget = buildMessage(message, messages, i - 2);
|
||||
}
|
||||
return messageWidget;
|
||||
return KeyedSubtree(
|
||||
key: ValueKey(message.id),
|
||||
child: messageWidget,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (widget.showScrollToBottom) _buildScrollToBottom(),
|
||||
BetterStreamBuilder<bool>(
|
||||
stream: streamChannel!.channel.state!.isUpToDateStream,
|
||||
initialData: streamChannel!.channel.state!.isUpToDate,
|
||||
builder: (context, snapshot) => ValueListenableBuilder<bool>(
|
||||
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<MessageListView> {
|
||||
.index;
|
||||
}
|
||||
|
||||
Widget _buildScrollToBottom() => StreamBuilder<Tuple2<bool, int>>(
|
||||
stream: Rx.combineLatest2(
|
||||
streamChannel!.channel.state!.isUpToDateStream.distinct(),
|
||||
streamChannel!.channel.state!.unreadCountStream.distinct(),
|
||||
(bool isUpToDate, int unreadCount) => Tuple2(isUpToDate, unreadCount),
|
||||
),
|
||||
Widget _buildScrollToBottom() => StreamBuilder<int>(
|
||||
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<MessageListView> {
|
||||
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<MessageListView> {
|
||||
int index,
|
||||
) {
|
||||
final messageWidget = buildMessage(message, messages, index);
|
||||
|
||||
return VisibilityDetector(
|
||||
key: ValueKey<String>('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<MessageListView> {
|
||||
}
|
||||
}
|
||||
if (mounted) {
|
||||
if (_showScrollToBottom == isVisible) {
|
||||
setState(() => _showScrollToBottom = !isVisible);
|
||||
if (_showScrollToBottom.value == isVisible) {
|
||||
_showScrollToBottom.value = !isVisible;
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -948,16 +974,11 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
return defaultMessageWidget;
|
||||
}
|
||||
|
||||
Widget buildMessage(
|
||||
Message message,
|
||||
List<Message> messages,
|
||||
int index,
|
||||
) {
|
||||
Widget buildMessage(Message message, List<Message> messages, int index) {
|
||||
if ((message.type == 'system' || message.type == 'error') &&
|
||||
message.text?.isNotEmpty == true) {
|
||||
return widget.systemMessageBuilder?.call(context, message) ??
|
||||
SystemMessage(
|
||||
key: ValueKey<String>('MESSAGE-${message.id}'),
|
||||
message: message,
|
||||
onMessageTap: (message) {
|
||||
if (widget.onSystemMessageTap != null) {
|
||||
@@ -1037,7 +1058,6 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
members.firstWhereOrNull((e) => e.user!.id == currentUser!.id);
|
||||
|
||||
Widget messageWidget = MessageWidget(
|
||||
key: ValueKey<String>('MESSAGE-${message.id}'),
|
||||
message: message,
|
||||
reverse: isMyMessage,
|
||||
showReactions: !message.isDeleted,
|
||||
@@ -1049,23 +1069,20 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
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<MessageListView> {
|
||||
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<MessageListView> {
|
||||
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),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -608,6 +608,8 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
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<MessageWidget>
|
||||
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<MessageWidget>
|
||||
|
||||
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<MessageWidget>
|
||||
);
|
||||
}
|
||||
|
||||
bool get _shouldShowReactions =>
|
||||
widget.showReactions &&
|
||||
(widget.message.reactionCounts?.isNotEmpty == true) &&
|
||||
!widget.message.isDeleted;
|
||||
|
||||
void _showMessageActionModalBottomSheet(BuildContext context) {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+298
@@ -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<void> 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));
|
||||
});
|
||||
}
|
||||
+363
@@ -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<void> 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);
|
||||
});
|
||||
}
|
||||
+270
@@ -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<void> 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);
|
||||
});
|
||||
}
|
||||
+239
@@ -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<void> 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)));
|
||||
});
|
||||
}
|
||||
+2274
File diff suppressed because it is too large
Load Diff
+261
@@ -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<void> 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;
|
||||
+602
@@ -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<void> 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<IndexedSemantics>(find.ancestor(
|
||||
of: find.text('Item 3'), matching: find.byType(IndexedSemantics)));
|
||||
expect(indexSemantics3.index, 3);
|
||||
final indexSemantics4 = tester.widget<IndexedSemantics>(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<CustomScrollView>(find.byType(UnboundedCustomScrollView));
|
||||
expect(customScrollView.semanticChildCount, 30);
|
||||
|
||||
unawaited(
|
||||
itemScrollController.scrollTo(index: 100, duration: scrollDuration));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final customScrollView2 =
|
||||
tester.widget<CustomScrollView>(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<CustomScrollView>(find.byType(UnboundedCustomScrollView));
|
||||
expect(customScrollView.semanticChildCount, defaultItemCount);
|
||||
|
||||
unawaited(
|
||||
itemScrollController.scrollTo(index: 100, duration: scrollDuration));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final customScrollView2 =
|
||||
tester.widget<CustomScrollView>(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<int>(0);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ValueListenableBuilder<int>(
|
||||
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<int>(0);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ValueListenableBuilder<int>(
|
||||
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;
|
||||
+220
@@ -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<void> 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;
|
||||
@@ -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).
|
||||
|
||||
@@ -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<MessageListCore> {
|
||||
? _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<List<Message>>(
|
||||
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);
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import 'mocks.dart';
|
||||
|
||||
void main() {
|
||||
setUpAll(() {
|
||||
registerFallbackValue<PaginationParams>(const PaginationParams());
|
||||
registerFallbackValue(const PaginationParams());
|
||||
});
|
||||
|
||||
List<Channel> _generateChannels(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
targets:
|
||||
$default:
|
||||
builders:
|
||||
moor_generator:
|
||||
drift_dev:
|
||||
options:
|
||||
generate_connect_constructor: true
|
||||
data_class_to_companions: false
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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<MoorChatDatabase>
|
||||
@DriftAccessor(tables: [Channels, Users])
|
||||
class ChannelDao extends DatabaseAccessor<DriftChatDatabase>
|
||||
with _$ChannelDaoMixin {
|
||||
/// Creates a new channel dao instance
|
||||
ChannelDao(MoorChatDatabase db) : super(db);
|
||||
ChannelDao(DriftChatDatabase db) : super(db);
|
||||
|
||||
/// Get channel by cid
|
||||
Future<ChannelModel?> getChannelByCid(String cid) async =>
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'channel_dao.dart';
|
||||
// DaoGenerator
|
||||
// **************************************************************************
|
||||
|
||||
mixin _$ChannelDaoMixin on DatabaseAccessor<MoorChatDatabase> {
|
||||
mixin _$ChannelDaoMixin on DatabaseAccessor<DriftChatDatabase> {
|
||||
$ChannelsTable get channels => attachedDatabase.channels;
|
||||
$UsersTable get users => attachedDatabase.users;
|
||||
}
|
||||
|
||||
@@ -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<MoorChatDatabase>
|
||||
@DriftAccessor(tables: [ChannelQueries, Channels, Users])
|
||||
class ChannelQueryDao extends DatabaseAccessor<DriftChatDatabase>
|
||||
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) {
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'channel_query_dao.dart';
|
||||
// DaoGenerator
|
||||
// **************************************************************************
|
||||
|
||||
mixin _$ChannelQueryDaoMixin on DatabaseAccessor<MoorChatDatabase> {
|
||||
mixin _$ChannelQueryDaoMixin on DatabaseAccessor<DriftChatDatabase> {
|
||||
$ChannelQueriesTable get channelQueries => attachedDatabase.channelQueries;
|
||||
$ChannelsTable get channels => attachedDatabase.channels;
|
||||
$UsersTable get users => attachedDatabase.users;
|
||||
|
||||
@@ -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<MoorChatDatabase>
|
||||
@DriftAccessor(tables: [ConnectionEvents])
|
||||
class ConnectionEventDao extends DatabaseAccessor<DriftChatDatabase>
|
||||
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<Event?> get connectionEvent => select(connectionEvents)
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'connection_event_dao.dart';
|
||||
// DaoGenerator
|
||||
// **************************************************************************
|
||||
|
||||
mixin _$ConnectionEventDaoMixin on DatabaseAccessor<MoorChatDatabase> {
|
||||
mixin _$ConnectionEventDaoMixin on DatabaseAccessor<DriftChatDatabase> {
|
||||
$ConnectionEventsTable get connectionEvents =>
|
||||
attachedDatabase.connectionEvents;
|
||||
}
|
||||
|
||||
@@ -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<MoorChatDatabase>
|
||||
@DriftAccessor(tables: [Members, Users])
|
||||
class MemberDao extends DatabaseAccessor<DriftChatDatabase>
|
||||
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<List<Member>> getMembersByCid(String cid) async =>
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'member_dao.dart';
|
||||
// DaoGenerator
|
||||
// **************************************************************************
|
||||
|
||||
mixin _$MemberDaoMixin on DatabaseAccessor<MoorChatDatabase> {
|
||||
mixin _$MemberDaoMixin on DatabaseAccessor<DriftChatDatabase> {
|
||||
$MembersTable get members => attachedDatabase.members;
|
||||
$UsersTable get users => attachedDatabase.users;
|
||||
}
|
||||
|
||||
@@ -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<MoorChatDatabase>
|
||||
@DriftAccessor(tables: [Messages, Users])
|
||||
class MessageDao extends DatabaseAccessor<DriftChatDatabase>
|
||||
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');
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'message_dao.dart';
|
||||
// DaoGenerator
|
||||
// **************************************************************************
|
||||
|
||||
mixin _$MessageDaoMixin on DatabaseAccessor<MoorChatDatabase> {
|
||||
mixin _$MessageDaoMixin on DatabaseAccessor<DriftChatDatabase> {
|
||||
$MessagesTable get messages => attachedDatabase.messages;
|
||||
$UsersTable get users => attachedDatabase.users;
|
||||
}
|
||||
|
||||
@@ -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<MoorChatDatabase>
|
||||
@DriftAccessor(tables: [PinnedMessages, Users])
|
||||
class PinnedMessageDao extends DatabaseAccessor<DriftChatDatabase>
|
||||
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');
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'pinned_message_dao.dart';
|
||||
// DaoGenerator
|
||||
// **************************************************************************
|
||||
|
||||
mixin _$PinnedMessageDaoMixin on DatabaseAccessor<MoorChatDatabase> {
|
||||
mixin _$PinnedMessageDaoMixin on DatabaseAccessor<DriftChatDatabase> {
|
||||
$PinnedMessagesTable get pinnedMessages => attachedDatabase.pinnedMessages;
|
||||
$UsersTable get users => attachedDatabase.users;
|
||||
}
|
||||
|
||||
@@ -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<MoorChatDatabase>
|
||||
@DriftAccessor(tables: [PinnedMessageReactions, Users])
|
||||
class PinnedMessageReactionDao extends DatabaseAccessor<DriftChatDatabase>
|
||||
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]
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'pinned_message_reaction_dao.dart';
|
||||
// DaoGenerator
|
||||
// **************************************************************************
|
||||
|
||||
mixin _$PinnedMessageReactionDaoMixin on DatabaseAccessor<MoorChatDatabase> {
|
||||
mixin _$PinnedMessageReactionDaoMixin on DatabaseAccessor<DriftChatDatabase> {
|
||||
$PinnedMessageReactionsTable get pinnedMessageReactions =>
|
||||
attachedDatabase.pinnedMessageReactions;
|
||||
$UsersTable get users => attachedDatabase.users;
|
||||
|
||||
@@ -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<MoorChatDatabase>
|
||||
@DriftAccessor(tables: [Reactions, Users])
|
||||
class ReactionDao extends DatabaseAccessor<DriftChatDatabase>
|
||||
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]
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'reaction_dao.dart';
|
||||
// DaoGenerator
|
||||
// **************************************************************************
|
||||
|
||||
mixin _$ReactionDaoMixin on DatabaseAccessor<MoorChatDatabase> {
|
||||
mixin _$ReactionDaoMixin on DatabaseAccessor<DriftChatDatabase> {
|
||||
$ReactionsTable get reactions => attachedDatabase.reactions;
|
||||
$UsersTable get users => attachedDatabase.users;
|
||||
}
|
||||
|
||||
@@ -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<MoorChatDatabase> with _$ReadDaoMixin {
|
||||
@DriftAccessor(tables: [Reads, Users])
|
||||
class ReadDao extends DatabaseAccessor<DriftChatDatabase> 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<List<Read>> getReadsByCid(String cid) async => (select(reads).join([
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'read_dao.dart';
|
||||
// DaoGenerator
|
||||
// **************************************************************************
|
||||
|
||||
mixin _$ReadDaoMixin on DatabaseAccessor<MoorChatDatabase> {
|
||||
mixin _$ReadDaoMixin on DatabaseAccessor<DriftChatDatabase> {
|
||||
$ReadsTable get reads => attachedDatabase.reads;
|
||||
$UsersTable get users => attachedDatabase.users;
|
||||
}
|
||||
|
||||
@@ -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<MoorChatDatabase> with _$UserDaoMixin {
|
||||
@DriftAccessor(tables: [Users])
|
||||
class UserDao extends DatabaseAccessor<DriftChatDatabase> 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<void> updateUsers(List<User> userList) => batch(
|
||||
|
||||
@@ -6,6 +6,6 @@ part of 'user_dao.dart';
|
||||
// DaoGenerator
|
||||
// **************************************************************************
|
||||
|
||||
mixin _$UserDaoMixin on DatabaseAccessor<MoorChatDatabase> {
|
||||
mixin _$UserDaoMixin on DatabaseAccessor<DriftChatDatabase> {
|
||||
$UsersTable get users => attachedDatabase.users;
|
||||
}
|
||||
|
||||
+6
-6
@@ -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);
|
||||
+133
-242
@@ -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<ChannelEntity> {
|
||||
required this.memberCount,
|
||||
this.createdById,
|
||||
this.extraData});
|
||||
factory ChannelEntity.fromData(
|
||||
Map<String, dynamic> data, GeneratedDatabase db,
|
||||
{String? prefix}) {
|
||||
factory ChannelEntity.fromData(Map<String, dynamic> data, {String? prefix}) {
|
||||
final effectivePrefix = prefix ?? '';
|
||||
return ChannelEntity(
|
||||
id: const StringType()
|
||||
@@ -119,7 +117,7 @@ class ChannelEntity extends DataClass implements Insertable<ChannelEntity> {
|
||||
|
||||
factory ChannelEntity.fromJson(Map<String, dynamic> json,
|
||||
{ValueSerializer? serializer}) {
|
||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return ChannelEntity(
|
||||
id: serializer.fromJson<String>(json['id']),
|
||||
type: serializer.fromJson<String>(json['type']),
|
||||
@@ -137,7 +135,7 @@ class ChannelEntity extends DataClass implements Insertable<ChannelEntity> {
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<String>(id),
|
||||
'type': serializer.toJson<String>(type),
|
||||
@@ -202,28 +200,8 @@ class ChannelEntity extends DataClass implements Insertable<ChannelEntity> {
|
||||
}
|
||||
|
||||
@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<GeneratedColumn> get $primaryKey => {cid};
|
||||
@override
|
||||
ChannelEntity map(Map<String, dynamic> 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<MessageEntity> {
|
||||
required this.channelCid,
|
||||
this.i18n,
|
||||
this.extraData});
|
||||
factory MessageEntity.fromData(
|
||||
Map<String, dynamic> data, GeneratedDatabase db,
|
||||
{String? prefix}) {
|
||||
factory MessageEntity.fromData(Map<String, dynamic> data, {String? prefix}) {
|
||||
final effectivePrefix = prefix ?? '';
|
||||
return MessageEntity(
|
||||
id: const StringType()
|
||||
@@ -814,7 +790,7 @@ class MessageEntity extends DataClass implements Insertable<MessageEntity> {
|
||||
|
||||
factory MessageEntity.fromJson(Map<String, dynamic> json,
|
||||
{ValueSerializer? serializer}) {
|
||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return MessageEntity(
|
||||
id: serializer.fromJson<String>(json['id']),
|
||||
messageText: serializer.fromJson<String?>(json['messageText']),
|
||||
@@ -847,7 +823,7 @@ class MessageEntity extends DataClass implements Insertable<MessageEntity> {
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<String>(id),
|
||||
'messageText': serializer.toJson<String?>(messageText),
|
||||
@@ -969,49 +945,33 @@ class MessageEntity extends DataClass implements Insertable<MessageEntity> {
|
||||
}
|
||||
|
||||
@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<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
MessageEntity map(Map<String, dynamic> 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<String, dynamic> data, GeneratedDatabase db,
|
||||
factory PinnedMessageEntity.fromData(Map<String, dynamic> data,
|
||||
{String? prefix}) {
|
||||
final effectivePrefix = prefix ?? '';
|
||||
return PinnedMessageEntity(
|
||||
@@ -1911,7 +1870,7 @@ class PinnedMessageEntity extends DataClass
|
||||
|
||||
factory PinnedMessageEntity.fromJson(Map<String, dynamic> json,
|
||||
{ValueSerializer? serializer}) {
|
||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return PinnedMessageEntity(
|
||||
id: serializer.fromJson<String>(json['id']),
|
||||
messageText: serializer.fromJson<String?>(json['messageText']),
|
||||
@@ -1944,7 +1903,7 @@ class PinnedMessageEntity extends DataClass
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<String>(id),
|
||||
'messageText': serializer.toJson<String?>(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<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
PinnedMessageEntity map(Map<String, dynamic> 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<String, dynamic> data, GeneratedDatabase db,
|
||||
factory PinnedMessageReactionEntity.fromData(Map<String, dynamic> data,
|
||||
{String? prefix}) {
|
||||
final effectivePrefix = prefix ?? '';
|
||||
return PinnedMessageReactionEntity(
|
||||
@@ -2832,7 +2774,7 @@ class PinnedMessageReactionEntity extends DataClass
|
||||
|
||||
factory PinnedMessageReactionEntity.fromJson(Map<String, dynamic> json,
|
||||
{ValueSerializer? serializer}) {
|
||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return PinnedMessageReactionEntity(
|
||||
userId: serializer.fromJson<String>(json['userId']),
|
||||
messageId: serializer.fromJson<String>(json['messageId']),
|
||||
@@ -2844,7 +2786,7 @@ class PinnedMessageReactionEntity extends DataClass
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'userId': serializer.toJson<String>(userId),
|
||||
'messageId': serializer.toJson<String>(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<String, dynamic> 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<ReactionEntity> {
|
||||
required this.createdAt,
|
||||
required this.score,
|
||||
this.extraData});
|
||||
factory ReactionEntity.fromData(
|
||||
Map<String, dynamic> data, GeneratedDatabase db,
|
||||
{String? prefix}) {
|
||||
factory ReactionEntity.fromData(Map<String, dynamic> data, {String? prefix}) {
|
||||
final effectivePrefix = prefix ?? '';
|
||||
return ReactionEntity(
|
||||
userId: const StringType()
|
||||
@@ -3164,7 +3098,7 @@ class ReactionEntity extends DataClass implements Insertable<ReactionEntity> {
|
||||
|
||||
factory ReactionEntity.fromJson(Map<String, dynamic> json,
|
||||
{ValueSerializer? serializer}) {
|
||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return ReactionEntity(
|
||||
userId: serializer.fromJson<String>(json['userId']),
|
||||
messageId: serializer.fromJson<String>(json['messageId']),
|
||||
@@ -3176,7 +3110,7 @@ class ReactionEntity extends DataClass implements Insertable<ReactionEntity> {
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'userId': serializer.toJson<String>(userId),
|
||||
'messageId': serializer.toJson<String>(messageId),
|
||||
@@ -3216,14 +3150,8 @@ class ReactionEntity extends DataClass implements Insertable<ReactionEntity> {
|
||||
}
|
||||
|
||||
@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<GeneratedColumn> get $primaryKey => {messageId, type, userId};
|
||||
@override
|
||||
ReactionEntity map(Map<String, dynamic> 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<UserEntity> {
|
||||
required this.online,
|
||||
required this.banned,
|
||||
required this.extraData});
|
||||
factory UserEntity.fromData(Map<String, dynamic> data, GeneratedDatabase db,
|
||||
{String? prefix}) {
|
||||
factory UserEntity.fromData(Map<String, dynamic> data, {String? prefix}) {
|
||||
final effectivePrefix = prefix ?? '';
|
||||
return UserEntity(
|
||||
id: const StringType()
|
||||
@@ -3518,7 +3445,7 @@ class UserEntity extends DataClass implements Insertable<UserEntity> {
|
||||
|
||||
factory UserEntity.fromJson(Map<String, dynamic> json,
|
||||
{ValueSerializer? serializer}) {
|
||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return UserEntity(
|
||||
id: serializer.fromJson<String>(json['id']),
|
||||
role: serializer.fromJson<String?>(json['role']),
|
||||
@@ -3533,7 +3460,7 @@ class UserEntity extends DataClass implements Insertable<UserEntity> {
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<String>(id),
|
||||
'role': serializer.toJson<String?>(role),
|
||||
@@ -3585,22 +3512,8 @@ class UserEntity extends DataClass implements Insertable<UserEntity> {
|
||||
}
|
||||
|
||||
@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<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
UserEntity map(Map<String, dynamic> 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<MemberEntity> {
|
||||
required this.isModerator,
|
||||
required this.createdAt,
|
||||
required this.updatedAt});
|
||||
factory MemberEntity.fromData(Map<String, dynamic> data, GeneratedDatabase db,
|
||||
{String? prefix}) {
|
||||
factory MemberEntity.fromData(Map<String, dynamic> data, {String? prefix}) {
|
||||
final effectivePrefix = prefix ?? '';
|
||||
return MemberEntity(
|
||||
userId: const StringType()
|
||||
@@ -3974,7 +3886,7 @@ class MemberEntity extends DataClass implements Insertable<MemberEntity> {
|
||||
|
||||
factory MemberEntity.fromJson(Map<String, dynamic> json,
|
||||
{ValueSerializer? serializer}) {
|
||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return MemberEntity(
|
||||
userId: serializer.fromJson<String>(json['userId']),
|
||||
channelCid: serializer.fromJson<String>(json['channelCid']),
|
||||
@@ -3993,7 +3905,7 @@ class MemberEntity extends DataClass implements Insertable<MemberEntity> {
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'userId': serializer.toJson<String>(userId),
|
||||
'channelCid': serializer.toJson<String>(channelCid),
|
||||
@@ -4057,26 +3969,18 @@ class MemberEntity extends DataClass implements Insertable<MemberEntity> {
|
||||
}
|
||||
|
||||
@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<GeneratedColumn> get $primaryKey => {userId, channelCid};
|
||||
@override
|
||||
MemberEntity map(Map<String, dynamic> 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<ReadEntity> {
|
||||
required this.userId,
|
||||
required this.channelCid,
|
||||
required this.unreadMessages});
|
||||
factory ReadEntity.fromData(Map<String, dynamic> data, GeneratedDatabase db,
|
||||
{String? prefix}) {
|
||||
factory ReadEntity.fromData(Map<String, dynamic> data, {String? prefix}) {
|
||||
final effectivePrefix = prefix ?? '';
|
||||
return ReadEntity(
|
||||
lastRead: const DateTimeType()
|
||||
@@ -4458,7 +4361,7 @@ class ReadEntity extends DataClass implements Insertable<ReadEntity> {
|
||||
|
||||
factory ReadEntity.fromJson(Map<String, dynamic> json,
|
||||
{ValueSerializer? serializer}) {
|
||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return ReadEntity(
|
||||
lastRead: serializer.fromJson<DateTime>(json['lastRead']),
|
||||
userId: serializer.fromJson<String>(json['userId']),
|
||||
@@ -4468,7 +4371,7 @@ class ReadEntity extends DataClass implements Insertable<ReadEntity> {
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'lastRead': serializer.toJson<DateTime>(lastRead),
|
||||
'userId': serializer.toJson<String>(userId),
|
||||
@@ -4500,10 +4403,7 @@ class ReadEntity extends DataClass implements Insertable<ReadEntity> {
|
||||
}
|
||||
|
||||
@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<GeneratedColumn> get $primaryKey => {userId, channelCid};
|
||||
@override
|
||||
ReadEntity map(Map<String, dynamic> 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<String, dynamic> data, GeneratedDatabase db,
|
||||
factory ChannelQueryEntity.fromData(Map<String, dynamic> data,
|
||||
{String? prefix}) {
|
||||
final effectivePrefix = prefix ?? '';
|
||||
return ChannelQueryEntity(
|
||||
@@ -4699,7 +4598,7 @@ class ChannelQueryEntity extends DataClass
|
||||
|
||||
factory ChannelQueryEntity.fromJson(Map<String, dynamic> json,
|
||||
{ValueSerializer? serializer}) {
|
||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return ChannelQueryEntity(
|
||||
queryHash: serializer.fromJson<String>(json['queryHash']),
|
||||
channelCid: serializer.fromJson<String>(json['channelCid']),
|
||||
@@ -4707,7 +4606,7 @@ class ChannelQueryEntity extends DataClass
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'queryHash': serializer.toJson<String>(queryHash),
|
||||
'channelCid': serializer.toJson<String>(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<GeneratedColumn> get $primaryKey => {queryHash, channelCid};
|
||||
@override
|
||||
ChannelQueryEntity map(Map<String, dynamic> 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<String, dynamic> data, GeneratedDatabase db,
|
||||
factory ConnectionEventEntity.fromData(Map<String, dynamic> data,
|
||||
{String? prefix}) {
|
||||
final effectivePrefix = prefix ?? '';
|
||||
return ConnectionEventEntity(
|
||||
@@ -4922,7 +4820,7 @@ class ConnectionEventEntity extends DataClass
|
||||
|
||||
factory ConnectionEventEntity.fromJson(Map<String, dynamic> json,
|
||||
{ValueSerializer? serializer}) {
|
||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return ConnectionEventEntity(
|
||||
id: serializer.fromJson<int>(json['id']),
|
||||
type: serializer.fromJson<String>(json['type']),
|
||||
@@ -4935,7 +4833,7 @@ class ConnectionEventEntity extends DataClass
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<int>(id),
|
||||
'type': serializer.toJson<String>(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<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
ConnectionEventEntity map(Map<String, dynamic> 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<TableInfo> get allTables => allSchemaEntities.whereType<TableInfo>();
|
||||
@override
|
||||
@@ -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<VmDatabase> _constructDatabase(
|
||||
static Future<NativeDatabase> _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<MoorIsolate> _createMoorIsolate(
|
||||
static Future<DriftIsolate> _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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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].
|
||||
|
||||
@@ -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].
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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].
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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].
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
) =>
|
||||
|
||||
@@ -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
|
||||
mocktail: ^0.2.0
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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(() {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user