Merge branch 'message-input-controller' of https://github.com/GetStream/stream-chat-flutter into message-input-controller
# Conflicts: # packages/stream_chat_flutter_core/lib/src/message_input_controller.dart
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,23 @@
|
||||
## Upcoming
|
||||
|
||||
✅ Added
|
||||
|
||||
🛑️ Breaking Changes from `3.2.0`
|
||||
|
||||
- `MessageInput` now works with a `MessageInputController` instead of a `TextEditingController`
|
||||
|
||||
## 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, __) {
|
||||
|
||||
@@ -9,13 +9,11 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
import 'package:stream_chat_flutter/src/commands_overlay.dart';
|
||||
import 'package:stream_chat_flutter/src/emoji/emoji.dart';
|
||||
import 'package:stream_chat_flutter/src/emoji_overlay.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/src/media_list_view.dart';
|
||||
import 'package:stream_chat_flutter/src/message_list_view.dart';
|
||||
import 'package:stream_chat_flutter/src/multi_overlay.dart';
|
||||
import 'package:stream_chat_flutter/src/quoted_message_widget.dart';
|
||||
@@ -79,6 +77,20 @@ typedef ActionButtonBuilder = Widget Function(
|
||||
IconButton defaultActionButton,
|
||||
);
|
||||
|
||||
/// Widget builder for widgets that require may required data from the
|
||||
/// [MessageInputController]
|
||||
typedef MessageRelatedBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
MessageInputController messageInputController,
|
||||
);
|
||||
|
||||
/// Widget builder for a custom attachment picker.
|
||||
typedef AttachmentsPickerBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
MessageInputController messageInputController,
|
||||
StreamAttachmentPicker defaultPicker,
|
||||
);
|
||||
|
||||
/// Location for actions on the [MessageInput]
|
||||
enum ActionsLocation {
|
||||
/// Align to left
|
||||
@@ -204,6 +216,8 @@ class MessageInput extends StatefulWidget {
|
||||
this.commandButtonBuilder,
|
||||
this.customOverlays = const [],
|
||||
this.mentionAllAppUsers = false,
|
||||
this.attachmentsPickerBuilder,
|
||||
this.sendButtonBuilder,
|
||||
}) : assert(
|
||||
initialMessage == null || editMessage == null,
|
||||
"Can't provide both `initialMessage` and `editMessage`",
|
||||
@@ -322,6 +336,12 @@ class MessageInput extends StatefulWidget {
|
||||
/// Defaults to false.
|
||||
final bool mentionAllAppUsers;
|
||||
|
||||
/// Builds bottom sheet when attachment picker is opened.
|
||||
final AttachmentsPickerBuilder? attachmentsPickerBuilder;
|
||||
|
||||
/// Builder for creating send button
|
||||
final MessageRelatedBuilder? sendButtonBuilder;
|
||||
|
||||
@override
|
||||
MessageInputState createState() => MessageInputState();
|
||||
|
||||
@@ -349,7 +369,6 @@ class MessageInputState extends State<MessageInput> {
|
||||
Command? _chosenCommand;
|
||||
bool _actionsShrunk = false;
|
||||
bool _openFilePickerSection = false;
|
||||
int _filePickerIndex = 0;
|
||||
|
||||
/// The editing controller passed to the input TextField
|
||||
late final MessageInputController messageInputController =
|
||||
@@ -524,7 +543,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
widget.actionsLocation == ActionsLocation.right)
|
||||
_buildExpandActionsButton(context),
|
||||
if (widget.sendButtonLocation == SendButtonLocation.outside)
|
||||
_animateSendButton(context),
|
||||
_buildSendButton(context),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -588,25 +607,18 @@ class MessageInputState extends State<MessageInput> {
|
||||
],
|
||||
);
|
||||
|
||||
Widget _animateSendButton(BuildContext context) {
|
||||
late Widget sendButton;
|
||||
if (_timeOut > 0) {
|
||||
sendButton = _CountdownButton(count: _timeOut);
|
||||
} else if (!_messageIsPresent &&
|
||||
messageInputController.attachments.isEmpty) {
|
||||
sendButton = widget.idleSendButton ?? _buildIdleSendButton(context);
|
||||
} else {
|
||||
sendButton = widget.activeSendButton != null
|
||||
? InkWell(
|
||||
onTap: sendMessage,
|
||||
child: widget.activeSendButton,
|
||||
)
|
||||
: _buildSendButton(context);
|
||||
Widget _buildSendButton(BuildContext context) {
|
||||
if (widget.sendButtonBuilder != null) {
|
||||
return widget.sendButtonBuilder!(context, messageInputController);
|
||||
}
|
||||
|
||||
return AnimatedSwitcher(
|
||||
duration: _streamChatTheme.messageInputTheme.sendAnimationDuration!,
|
||||
child: sendButton,
|
||||
return StreamMessageSendButton(
|
||||
onSendMessage: sendMessage,
|
||||
timeOut: _timeOut,
|
||||
isIdle: !_messageIsPresent && messageInputController.attachments.isEmpty,
|
||||
isEditEnabled: widget.editMessage != null,
|
||||
idleSendButton: widget.idleSendButton,
|
||||
activeSendButton: widget.activeSendButton,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -817,7 +829,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
widget.actionsLocation == ActionsLocation.rightInside)
|
||||
_buildExpandActionsButton(context),
|
||||
if (widget.sendButtonLocation == SendButtonLocation.inside)
|
||||
_animateSendButton(context),
|
||||
_buildSendButton(context),
|
||||
],
|
||||
),
|
||||
).merge(passedDecoration);
|
||||
@@ -946,236 +958,33 @@ class MessageInputState extends State<MessageInput> {
|
||||
}
|
||||
|
||||
Widget _buildFilePickerSection() {
|
||||
final _attachmentContainsFile =
|
||||
messageInputController.attachments.any((it) => it.type == 'file');
|
||||
|
||||
final attachmentLimitCrossed =
|
||||
messageInputController.attachments.length >= widget.attachmentLimit;
|
||||
|
||||
Color _getIconColor(int index) {
|
||||
final streamChatThemeData = _streamChatTheme;
|
||||
switch (index) {
|
||||
case 0:
|
||||
return messageInputController.attachments.isEmpty
|
||||
? streamChatThemeData.colorTheme.accentPrimary
|
||||
: (!_attachmentContainsFile
|
||||
? streamChatThemeData.colorTheme.accentPrimary
|
||||
: streamChatThemeData.colorTheme.textHighEmphasis
|
||||
.withOpacity(0.2));
|
||||
case 1:
|
||||
return _attachmentContainsFile
|
||||
? streamChatThemeData.colorTheme.accentPrimary
|
||||
: (messageInputController.attachments.isEmpty
|
||||
? streamChatThemeData.colorTheme.textHighEmphasis
|
||||
.withOpacity(0.5)
|
||||
: streamChatThemeData.colorTheme.textHighEmphasis
|
||||
.withOpacity(0.2));
|
||||
case 2:
|
||||
return attachmentLimitCrossed
|
||||
? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2)
|
||||
: _attachmentContainsFile &&
|
||||
messageInputController.attachments.isNotEmpty
|
||||
? streamChatThemeData.colorTheme.textHighEmphasis
|
||||
.withOpacity(0.2)
|
||||
: streamChatThemeData.colorTheme.textHighEmphasis
|
||||
.withOpacity(0.5);
|
||||
case 3:
|
||||
return attachmentLimitCrossed
|
||||
? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2)
|
||||
: _attachmentContainsFile &&
|
||||
messageInputController.attachments.isNotEmpty
|
||||
? streamChatThemeData.colorTheme.textHighEmphasis
|
||||
.withOpacity(0.2)
|
||||
: streamChatThemeData.colorTheme.textHighEmphasis
|
||||
.withOpacity(0.5);
|
||||
default:
|
||||
return Colors.black;
|
||||
}
|
||||
}
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: _openFilePickerSection
|
||||
? const Duration(milliseconds: 300)
|
||||
: const Duration(),
|
||||
curve: Curves.easeOut,
|
||||
height: _openFilePickerSection ? _kMinMediaPickerSize : 0,
|
||||
child: SingleChildScrollView(
|
||||
child: SizedBox(
|
||||
height: _kMinMediaPickerSize,
|
||||
child: Material(
|
||||
color: _streamChatTheme.colorTheme.inputBg,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: StreamSvgIcon.pictures(
|
||||
color: _getIconColor(0),
|
||||
),
|
||||
onPressed: _attachmentContainsFile &&
|
||||
messageInputController.attachments.isNotEmpty
|
||||
? null
|
||||
: () {
|
||||
setState(() {
|
||||
_filePickerIndex = 0;
|
||||
});
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
iconSize: 32,
|
||||
icon: StreamSvgIcon.files(
|
||||
color: _getIconColor(1),
|
||||
),
|
||||
onPressed: !_attachmentContainsFile &&
|
||||
messageInputController.attachments.isNotEmpty
|
||||
? null
|
||||
: () {
|
||||
pickFile(DefaultAttachmentTypes.file);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: StreamSvgIcon.camera(
|
||||
color: _getIconColor(2),
|
||||
),
|
||||
onPressed: attachmentLimitCrossed ||
|
||||
(_attachmentContainsFile &&
|
||||
messageInputController.attachments.isNotEmpty)
|
||||
? null
|
||||
: () {
|
||||
pickFile(
|
||||
DefaultAttachmentTypes.image,
|
||||
camera: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
padding: const EdgeInsets.all(0),
|
||||
icon: StreamSvgIcon.record(
|
||||
color: _getIconColor(3),
|
||||
),
|
||||
onPressed: attachmentLimitCrossed ||
|
||||
(_attachmentContainsFile &&
|
||||
messageInputController.attachments.isNotEmpty)
|
||||
? null
|
||||
: () {
|
||||
pickFile(
|
||||
DefaultAttachmentTypes.video,
|
||||
camera: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: _streamChatTheme.colorTheme.barsBg,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: _streamChatTheme.colorTheme.inputBg,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_openFilePickerSection)
|
||||
Expanded(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: _streamChatTheme.colorTheme.barsBg,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: _PickerWidget(
|
||||
filePickerIndex: _filePickerIndex,
|
||||
streamChatTheme: _streamChatTheme,
|
||||
containsFile: _attachmentContainsFile,
|
||||
selectedMedias: messageInputController.attachments
|
||||
.map((e) => e.id)
|
||||
.toList(),
|
||||
onAddMoreFilesClick: pickFile,
|
||||
onMediaSelected: (media) {
|
||||
if (messageInputController.attachments
|
||||
.any((e) => e.id == media.id)) {
|
||||
setState(() => messageInputController.attachments
|
||||
.removeWhere((e) => e.id == media.id));
|
||||
} else {
|
||||
_addAssetAttachment(media);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _addAssetAttachment(AssetEntity medium) async {
|
||||
final mediaFile = await medium.originFile.timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () => medium.originFile,
|
||||
final picker = StreamAttachmentPicker(
|
||||
messageInputController: messageInputController,
|
||||
onFilePicked: pickFile,
|
||||
isOpen: _openFilePickerSection,
|
||||
pickerSize: _openFilePickerSection ? _kMinMediaPickerSize : 0,
|
||||
attachmentLimit: widget.attachmentLimit,
|
||||
onAttachmentLimitExceeded: widget.onAttachmentLimitExceed,
|
||||
maxAttachmentSize: widget.maxAttachmentSize,
|
||||
compressedVideoQuality: widget.compressedVideoQuality,
|
||||
compressedVideoFrameRate: widget.compressedVideoFrameRate,
|
||||
onChangeInputState: (val) {
|
||||
setState(() {
|
||||
_inputEnabled = val;
|
||||
});
|
||||
},
|
||||
onError: _showErrorAlert,
|
||||
);
|
||||
|
||||
if (mediaFile == null) return;
|
||||
|
||||
var file = AttachmentFile(
|
||||
path: mediaFile.path,
|
||||
size: await mediaFile.length(),
|
||||
bytes: mediaFile.readAsBytesSync(),
|
||||
);
|
||||
|
||||
if (file.size! > widget.maxAttachmentSize) {
|
||||
if (medium.type == AssetType.video && file.path != null) {
|
||||
final mediaInfo = await (VideoService.compressVideo(
|
||||
file.path!,
|
||||
frameRate: widget.compressedVideoFrameRate,
|
||||
quality: widget.compressedVideoQuality,
|
||||
) as FutureOr<MediaInfo>);
|
||||
|
||||
if (mediaInfo.filesize! > widget.maxAttachmentSize) {
|
||||
_showErrorAlert(
|
||||
context.translations.fileTooLargeAfterCompressionError(
|
||||
widget.maxAttachmentSize / (1024 * 1024),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
file = AttachmentFile(
|
||||
name: file.name,
|
||||
size: mediaInfo.filesize,
|
||||
bytes: await mediaInfo.file?.readAsBytes(),
|
||||
path: mediaInfo.path,
|
||||
);
|
||||
} else {
|
||||
_showErrorAlert(context.translations.fileTooLargeError(
|
||||
widget.maxAttachmentSize / (1024 * 1024),
|
||||
));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
final attachment = Attachment(
|
||||
id: medium.id,
|
||||
file: file,
|
||||
type: medium.type == AssetType.image ? 'image' : 'video',
|
||||
if (_openFilePickerSection && widget.attachmentsPickerBuilder != null) {
|
||||
return widget.attachmentsPickerBuilder!(
|
||||
context,
|
||||
messageInputController,
|
||||
picker,
|
||||
);
|
||||
_addAttachments([attachment]);
|
||||
});
|
||||
}
|
||||
|
||||
return picker;
|
||||
}
|
||||
|
||||
Widget _buildMentionsOverlayEntry() {
|
||||
@@ -1736,53 +1545,12 @@ class MessageInputState extends State<MessageInput> {
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildIdleSendButton(BuildContext context) => Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: StreamSvgIcon(
|
||||
assetName: _getIdleSendIcon(),
|
||||
color: _messageInputTheme.sendButtonIdleColor,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildSendButton(BuildContext context) => Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: IconButton(
|
||||
onPressed: sendMessage,
|
||||
padding: const EdgeInsets.all(0),
|
||||
splashRadius: 24,
|
||||
constraints: const BoxConstraints.tightFor(
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
icon: StreamSvgIcon(
|
||||
assetName: _getSendIcon(),
|
||||
color: _messageInputTheme.sendButtonColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
String _getIdleSendIcon() {
|
||||
if (_commandEnabled) {
|
||||
return 'Icon_search.svg';
|
||||
} else {
|
||||
return 'Icon_circle_right.svg';
|
||||
}
|
||||
}
|
||||
|
||||
String _getSendIcon() {
|
||||
if (widget.editMessage != null) {
|
||||
return 'Icon_circle_up.svg';
|
||||
} else if (_commandEnabled) {
|
||||
return 'Icon_search.svg';
|
||||
} else {
|
||||
return 'Icon_circle_up.svg';
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends the current message
|
||||
Future<void> sendMessage() async {
|
||||
var text = messageInputController.text.trim();
|
||||
if (text.isEmpty && messageInputController.attachments.isEmpty) {
|
||||
final attachments = messageInputController.attachments;
|
||||
|
||||
if (text.isEmpty && attachments.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1805,7 +1573,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
if (widget.editMessage != null) {
|
||||
message = widget.editMessage!.copyWith(
|
||||
text: text,
|
||||
attachments: messageInputController.attachments,
|
||||
attachments: attachments,
|
||||
mentionedUsers: messageInputController.mentionedUsers
|
||||
.where((u) => text.contains('@${u.name}'))
|
||||
.toList(),
|
||||
@@ -1814,7 +1582,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
message = (widget.initialMessage ?? Message()).copyWith(
|
||||
parentId: widget.parentMessage?.id,
|
||||
text: text,
|
||||
attachments: messageInputController.attachments,
|
||||
attachments: attachments,
|
||||
mentionedUsers: messageInputController.mentionedUsers
|
||||
.where((u) => text.contains('@${u.name}'))
|
||||
.toList(),
|
||||
@@ -1960,7 +1728,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
void didChangeDependencies() {
|
||||
_streamChatTheme = StreamChatTheme.of(context);
|
||||
_messageInputTheme = MessageInputTheme.of(context);
|
||||
if (widget.editMessage == null) _startSlowMode();
|
||||
if (widget.editMessage == null && _timeOut <= 0) _startSlowMode();
|
||||
|
||||
if ((widget.editMessage != null || widget.initialMessage != null) &&
|
||||
!_initialized) {
|
||||
@@ -1970,140 +1738,3 @@ class MessageInputState extends State<MessageInput> {
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
}
|
||||
|
||||
class _PickerWidget extends StatefulWidget {
|
||||
const _PickerWidget({
|
||||
Key? key,
|
||||
required this.filePickerIndex,
|
||||
required this.containsFile,
|
||||
required this.selectedMedias,
|
||||
required this.onAddMoreFilesClick,
|
||||
required this.onMediaSelected,
|
||||
required this.streamChatTheme,
|
||||
}) : super(key: key);
|
||||
|
||||
final int filePickerIndex;
|
||||
final bool containsFile;
|
||||
final List<String> selectedMedias;
|
||||
final void Function(DefaultAttachmentTypes) onAddMoreFilesClick;
|
||||
final void Function(AssetEntity) onMediaSelected;
|
||||
final StreamChatThemeData streamChatTheme;
|
||||
|
||||
@override
|
||||
_PickerWidgetState createState() => _PickerWidgetState();
|
||||
}
|
||||
|
||||
class _PickerWidgetState extends State<_PickerWidget> {
|
||||
Future<bool>? requestPermission;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
requestPermission = PhotoManager.requestPermission();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.filePickerIndex != 0) {
|
||||
return const Offstage();
|
||||
}
|
||||
return FutureBuilder<bool>(
|
||||
future: requestPermission,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const Offstage();
|
||||
}
|
||||
|
||||
if (snapshot.data!) {
|
||||
if (widget.containsFile) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
widget.onAddMoreFilesClick(DefaultAttachmentTypes.file);
|
||||
},
|
||||
child: Container(
|
||||
constraints: const BoxConstraints.expand(),
|
||||
color: widget.streamChatTheme.colorTheme.inputBg,
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
context.translations.addMoreFilesLabel,
|
||||
style: TextStyle(
|
||||
color: widget.streamChatTheme.colorTheme.accentPrimary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return MediaListView(
|
||||
selectedIds: widget.selectedMedias,
|
||||
onSelect: widget.onMediaSelected,
|
||||
);
|
||||
}
|
||||
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
PhotoManager.openSetting();
|
||||
},
|
||||
child: Container(
|
||||
color: widget.streamChatTheme.colorTheme.inputBg,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
'svgs/icon_picture_empty_state.svg',
|
||||
package: 'stream_chat_flutter',
|
||||
height: 140,
|
||||
color: widget.streamChatTheme.colorTheme.disabled,
|
||||
),
|
||||
Text(
|
||||
context.translations.enablePhotoAndVideoAccessMessage,
|
||||
style: widget.streamChatTheme.textTheme.body.copyWith(
|
||||
color: widget.streamChatTheme.colorTheme.textLowEmphasis,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Center(
|
||||
child: Text(
|
||||
context.translations.allowGalleryAccessMessage,
|
||||
style: widget.streamChatTheme.textTheme.bodyBold.copyWith(
|
||||
color: widget.streamChatTheme.colorTheme.accentPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CountdownButton extends StatelessWidget {
|
||||
const _CountdownButton({
|
||||
Key? key,
|
||||
required this.count,
|
||||
}) : super(key: key);
|
||||
|
||||
final int count;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: StreamChatTheme.of(context).colorTheme.disabled,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: Center(
|
||||
child: Text('$count'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// Button for showing visual component of slow mode.
|
||||
class CountdownButton extends StatelessWidget {
|
||||
/// Constructor for creating [CountdownButton].
|
||||
const CountdownButton({
|
||||
Key? key,
|
||||
required this.count,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Count of time remaining to show to the user.
|
||||
final int count;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: StreamChatTheme.of(context).colorTheme.disabled,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: Center(
|
||||
child: Text('$count'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,551 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/src/media_list_view.dart';
|
||||
import 'package:stream_chat_flutter/src/video_service.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:video_compress/video_compress.dart';
|
||||
|
||||
/// Callback for when a file has to be picked.
|
||||
typedef FilePickerCallback = void Function(
|
||||
DefaultAttachmentTypes fileType, {
|
||||
bool camera,
|
||||
});
|
||||
|
||||
/// Callback for building an icon for a custom attachment type.
|
||||
typedef CustomAttachmentIconBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
bool active,
|
||||
);
|
||||
|
||||
///
|
||||
class StreamAttachmentPicker extends StatefulWidget {
|
||||
final bool isOpen;
|
||||
final double pickerSize;
|
||||
final MessageInputController messageInputController;
|
||||
final int attachmentLimit;
|
||||
final AttachmentLimitExceedListener? onAttachmentLimitExceeded;
|
||||
final ValueChanged<bool>? onChangeInputState;
|
||||
final ValueChanged<String>? onError;
|
||||
final FilePickerCallback onFilePicked;
|
||||
|
||||
/// Video quality to use when compressing the videos
|
||||
final VideoQuality compressedVideoQuality;
|
||||
|
||||
/// Frame rate to use when compressing the videos
|
||||
final int compressedVideoFrameRate;
|
||||
|
||||
/// Max attachment size in bytes
|
||||
/// Defaults to 20 MB
|
||||
/// do not set it if you're using our default CDN
|
||||
final int maxAttachmentSize;
|
||||
|
||||
final List<DefaultAttachmentTypes> allowedAttachmentTypes;
|
||||
|
||||
final List<CustomAttachmentType> customAttachmentTypes;
|
||||
|
||||
const StreamAttachmentPicker({
|
||||
Key? key,
|
||||
required this.messageInputController,
|
||||
required this.onFilePicked,
|
||||
this.isOpen = false,
|
||||
this.pickerSize = 360.0,
|
||||
this.attachmentLimit = 10,
|
||||
this.onAttachmentLimitExceeded,
|
||||
this.maxAttachmentSize = 20971520,
|
||||
this.compressedVideoQuality = VideoQuality.DefaultQuality,
|
||||
this.compressedVideoFrameRate = 30,
|
||||
this.onChangeInputState,
|
||||
this.onError,
|
||||
this.allowedAttachmentTypes = const [
|
||||
DefaultAttachmentTypes.image,
|
||||
DefaultAttachmentTypes.file,
|
||||
DefaultAttachmentTypes.video,
|
||||
],
|
||||
this.customAttachmentTypes = const [],
|
||||
}) : super(key: key);
|
||||
|
||||
StreamAttachmentPicker copyWith({
|
||||
Key? key,
|
||||
MessageInputController? messageInputController,
|
||||
FilePickerCallback? onFilePicked,
|
||||
bool? isOpen,
|
||||
double? pickerSize,
|
||||
int? attachmentLimit,
|
||||
AttachmentLimitExceedListener? onAttachmentLimitExceeded,
|
||||
int? maxAttachmentSize,
|
||||
VideoQuality? compressedVideoQuality,
|
||||
int? compressedVideoFrameRate,
|
||||
ValueChanged<bool>? onChangeInputState,
|
||||
ValueChanged<String>? onError,
|
||||
List<DefaultAttachmentTypes>? allowedAttachmentTypes,
|
||||
List<CustomAttachmentType>? customAttachmentTypes = const [],
|
||||
}) =>
|
||||
StreamAttachmentPicker(
|
||||
key: key ?? this.key,
|
||||
messageInputController:
|
||||
messageInputController ?? this.messageInputController,
|
||||
onFilePicked: onFilePicked ?? this.onFilePicked,
|
||||
isOpen: isOpen ?? this.isOpen,
|
||||
pickerSize: pickerSize ?? this.pickerSize,
|
||||
attachmentLimit: attachmentLimit ?? this.attachmentLimit,
|
||||
onAttachmentLimitExceeded:
|
||||
onAttachmentLimitExceeded ?? this.onAttachmentLimitExceeded,
|
||||
maxAttachmentSize: maxAttachmentSize ?? this.maxAttachmentSize,
|
||||
compressedVideoQuality:
|
||||
compressedVideoQuality ?? this.compressedVideoQuality,
|
||||
compressedVideoFrameRate:
|
||||
compressedVideoFrameRate ?? this.compressedVideoFrameRate,
|
||||
onChangeInputState: onChangeInputState ?? this.onChangeInputState,
|
||||
onError: onError ?? this.onError,
|
||||
allowedAttachmentTypes:
|
||||
allowedAttachmentTypes ?? this.allowedAttachmentTypes,
|
||||
customAttachmentTypes:
|
||||
customAttachmentTypes ?? this.customAttachmentTypes,
|
||||
);
|
||||
|
||||
@override
|
||||
State<StreamAttachmentPicker> createState() => _StreamAttachmentPickerState();
|
||||
}
|
||||
|
||||
class _StreamAttachmentPickerState extends State<StreamAttachmentPicker> {
|
||||
int _filePickerIndex = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var _streamChatTheme = StreamChatTheme.of(context);
|
||||
var messageInputController = widget.messageInputController;
|
||||
|
||||
final _attachmentContainsImage =
|
||||
messageInputController.attachments.any((it) => it.type == 'image');
|
||||
|
||||
final _attachmentContainsFile =
|
||||
messageInputController.attachments.any((it) => it.type == 'file');
|
||||
|
||||
final _attachmentContainsVideo =
|
||||
messageInputController.attachments.any((it) => it.type == 'video');
|
||||
|
||||
final attachmentLimitCrossed =
|
||||
messageInputController.attachments.length >= widget.attachmentLimit;
|
||||
|
||||
Color _getIconColor(int index) {
|
||||
final streamChatThemeData = _streamChatTheme;
|
||||
switch (index) {
|
||||
case 0:
|
||||
return _filePickerIndex == 0 || _attachmentContainsImage
|
||||
? streamChatThemeData.colorTheme.accentPrimary
|
||||
: (_attachmentContainsImage
|
||||
? streamChatThemeData.colorTheme.accentPrimary
|
||||
: streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(
|
||||
messageInputController.attachments.isEmpty ? 0.5 : 0.2,
|
||||
));
|
||||
case 1:
|
||||
return _attachmentContainsFile
|
||||
? streamChatThemeData.colorTheme.accentPrimary
|
||||
: (messageInputController.attachments.isEmpty
|
||||
? streamChatThemeData.colorTheme.textHighEmphasis
|
||||
.withOpacity(0.5)
|
||||
: streamChatThemeData.colorTheme.textHighEmphasis
|
||||
.withOpacity(0.2));
|
||||
case 2:
|
||||
return widget.messageInputController.attachments.isNotEmpty &&
|
||||
(!_attachmentContainsImage || attachmentLimitCrossed)
|
||||
? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2)
|
||||
: _attachmentContainsFile &&
|
||||
messageInputController.attachments.isNotEmpty
|
||||
? streamChatThemeData.colorTheme.textHighEmphasis
|
||||
.withOpacity(0.2)
|
||||
: streamChatThemeData.colorTheme.textHighEmphasis
|
||||
.withOpacity(0.5);
|
||||
case 3:
|
||||
return widget.messageInputController.attachments.isNotEmpty &&
|
||||
(!_attachmentContainsVideo || attachmentLimitCrossed)
|
||||
? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2)
|
||||
: _attachmentContainsFile &&
|
||||
messageInputController.attachments.isNotEmpty
|
||||
? streamChatThemeData.colorTheme.textHighEmphasis
|
||||
.withOpacity(0.2)
|
||||
: streamChatThemeData.colorTheme.textHighEmphasis
|
||||
.withOpacity(0.5);
|
||||
default:
|
||||
return Colors.black;
|
||||
}
|
||||
}
|
||||
|
||||
return AnimatedContainer(
|
||||
duration:
|
||||
widget.isOpen ? const Duration(milliseconds: 300) : const Duration(),
|
||||
curve: Curves.easeOut,
|
||||
height: widget.isOpen ? widget.pickerSize : 0,
|
||||
child: SingleChildScrollView(
|
||||
child: SizedBox(
|
||||
height: widget.pickerSize,
|
||||
child: Material(
|
||||
color: _streamChatTheme.colorTheme.inputBg,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (widget.allowedAttachmentTypes
|
||||
.contains(DefaultAttachmentTypes.image))
|
||||
IconButton(
|
||||
icon: StreamSvgIcon.pictures(
|
||||
color: _getIconColor(0),
|
||||
),
|
||||
onPressed:
|
||||
messageInputController.attachments.isNotEmpty &&
|
||||
!_attachmentContainsImage
|
||||
? null
|
||||
: () {
|
||||
setState(() {
|
||||
_filePickerIndex = 0;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (widget.allowedAttachmentTypes
|
||||
.contains(DefaultAttachmentTypes.file))
|
||||
IconButton(
|
||||
iconSize: 32,
|
||||
icon: StreamSvgIcon.files(
|
||||
color: _getIconColor(1),
|
||||
),
|
||||
onPressed: messageInputController
|
||||
.attachments.isNotEmpty &&
|
||||
!_attachmentContainsFile
|
||||
? null
|
||||
: () {
|
||||
widget
|
||||
.onFilePicked(DefaultAttachmentTypes.file);
|
||||
},
|
||||
),
|
||||
if (widget.allowedAttachmentTypes
|
||||
.contains(DefaultAttachmentTypes.image))
|
||||
IconButton(
|
||||
icon: StreamSvgIcon.camera(
|
||||
color: _getIconColor(2),
|
||||
),
|
||||
onPressed: attachmentLimitCrossed ||
|
||||
(messageInputController
|
||||
.attachments.isNotEmpty &&
|
||||
!_attachmentContainsVideo)
|
||||
? null
|
||||
: () {
|
||||
widget.onFilePicked(
|
||||
DefaultAttachmentTypes.image,
|
||||
camera: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
if (widget.allowedAttachmentTypes
|
||||
.contains(DefaultAttachmentTypes.video))
|
||||
IconButton(
|
||||
padding: const EdgeInsets.all(0),
|
||||
icon: StreamSvgIcon.record(
|
||||
color: _getIconColor(3),
|
||||
),
|
||||
onPressed: attachmentLimitCrossed ||
|
||||
(messageInputController
|
||||
.attachments.isNotEmpty &&
|
||||
!_attachmentContainsVideo)
|
||||
? null
|
||||
: () {
|
||||
widget.onFilePicked(
|
||||
DefaultAttachmentTypes.video,
|
||||
camera: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
for (int i = 0;
|
||||
i < widget.customAttachmentTypes.length;
|
||||
i++)
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
if (messageInputController.attachments.isNotEmpty) {
|
||||
if (!messageInputController.attachments.any((e) =>
|
||||
e.type ==
|
||||
widget.customAttachmentTypes[i].type)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_filePickerIndex = i + 1;
|
||||
});
|
||||
},
|
||||
icon: widget.customAttachmentTypes[i]
|
||||
.iconBuilder(context, _filePickerIndex == i + 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: _streamChatTheme.colorTheme.barsBg,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: _streamChatTheme.colorTheme.inputBg,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.isOpen &&
|
||||
(widget.allowedAttachmentTypes
|
||||
.contains(DefaultAttachmentTypes.image) ||
|
||||
(widget.allowedAttachmentTypes
|
||||
.contains(DefaultAttachmentTypes.file))))
|
||||
Expanded(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: _streamChatTheme.colorTheme.barsBg,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: _PickerWidget(
|
||||
filePickerIndex: _filePickerIndex,
|
||||
streamChatTheme: _streamChatTheme,
|
||||
containsFile: _attachmentContainsFile,
|
||||
selectedMedias: messageInputController.attachments
|
||||
.map((e) => e.id)
|
||||
.toList(),
|
||||
onAddMoreFilesClick: widget.onFilePicked,
|
||||
onMediaSelected: (media) {
|
||||
if (messageInputController.attachments
|
||||
.any((e) => e.id == media.id)) {
|
||||
setState(() => messageInputController.attachments
|
||||
.removeWhere((e) => e.id == media.id));
|
||||
} else {
|
||||
_addAssetAttachment(media);
|
||||
}
|
||||
},
|
||||
allowedAttachmentTypes: widget.allowedAttachmentTypes,
|
||||
customAttachmentTypes: widget.customAttachmentTypes,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _addAssetAttachment(AssetEntity medium) async {
|
||||
final mediaFile = await medium.originFile.timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () => medium.originFile,
|
||||
);
|
||||
|
||||
if (mediaFile == null) return;
|
||||
|
||||
var file = AttachmentFile(
|
||||
path: mediaFile.path,
|
||||
size: await mediaFile.length(),
|
||||
bytes: mediaFile.readAsBytesSync(),
|
||||
);
|
||||
|
||||
if (file.size! > widget.maxAttachmentSize) {
|
||||
if (medium.type == AssetType.video && file.path != null) {
|
||||
final mediaInfo = await (VideoService.compressVideo(
|
||||
file.path!,
|
||||
frameRate: widget.compressedVideoFrameRate,
|
||||
quality: widget.compressedVideoQuality,
|
||||
) as FutureOr<MediaInfo>);
|
||||
|
||||
if (mediaInfo.filesize! > widget.maxAttachmentSize) {
|
||||
widget.onError?.call(
|
||||
context.translations.fileTooLargeAfterCompressionError(
|
||||
widget.maxAttachmentSize / (1024 * 1024),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
file = AttachmentFile(
|
||||
name: file.name,
|
||||
size: mediaInfo.filesize,
|
||||
bytes: await mediaInfo.file?.readAsBytes(),
|
||||
path: mediaInfo.path,
|
||||
);
|
||||
} else {
|
||||
widget.onError?.call(context.translations.fileTooLargeError(
|
||||
widget.maxAttachmentSize / (1024 * 1024),
|
||||
));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
final attachment = Attachment(
|
||||
id: medium.id,
|
||||
file: file,
|
||||
type: medium.type == AssetType.image ? 'image' : 'video',
|
||||
);
|
||||
_addAttachments([attachment]);
|
||||
});
|
||||
}
|
||||
|
||||
/// Adds an attachment to the [messageInputController.attachments] map
|
||||
void _addAttachments(Iterable<Attachment> attachments) {
|
||||
final limit = widget.attachmentLimit;
|
||||
final length =
|
||||
widget.messageInputController.attachments.length + attachments.length;
|
||||
if (length > limit) {
|
||||
final onAttachmentLimitExceed = widget.onAttachmentLimitExceeded;
|
||||
if (onAttachmentLimitExceed != null) {
|
||||
return onAttachmentLimitExceed(
|
||||
widget.attachmentLimit,
|
||||
context.translations.attachmentLimitExceedError(limit),
|
||||
);
|
||||
}
|
||||
return widget.onError?.call(
|
||||
context.translations.attachmentLimitExceedError(limit),
|
||||
);
|
||||
}
|
||||
for (final attachment in attachments) {
|
||||
widget.messageInputController.addAttachment(attachment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _PickerWidget extends StatefulWidget {
|
||||
const _PickerWidget({
|
||||
Key? key,
|
||||
required this.filePickerIndex,
|
||||
required this.containsFile,
|
||||
required this.selectedMedias,
|
||||
required this.onAddMoreFilesClick,
|
||||
required this.onMediaSelected,
|
||||
required this.streamChatTheme,
|
||||
required this.allowedAttachmentTypes,
|
||||
required this.customAttachmentTypes,
|
||||
}) : super(key: key);
|
||||
|
||||
final int filePickerIndex;
|
||||
final bool containsFile;
|
||||
final List<String> selectedMedias;
|
||||
final void Function(DefaultAttachmentTypes) onAddMoreFilesClick;
|
||||
final void Function(AssetEntity) onMediaSelected;
|
||||
final StreamChatThemeData streamChatTheme;
|
||||
final List<DefaultAttachmentTypes> allowedAttachmentTypes;
|
||||
final List<CustomAttachmentType> customAttachmentTypes;
|
||||
|
||||
@override
|
||||
_PickerWidgetState createState() => _PickerWidgetState();
|
||||
}
|
||||
|
||||
class _PickerWidgetState extends State<_PickerWidget> {
|
||||
Future<bool>? requestPermission;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
requestPermission = PhotoManager.requestPermission();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.filePickerIndex != 0) {
|
||||
return widget.customAttachmentTypes[widget.filePickerIndex - 1]
|
||||
.pickerBuilder(context);
|
||||
}
|
||||
return FutureBuilder<bool>(
|
||||
future: requestPermission,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const Offstage();
|
||||
}
|
||||
|
||||
if (snapshot.data!) {
|
||||
if (widget.containsFile ||
|
||||
!widget.allowedAttachmentTypes
|
||||
.contains(DefaultAttachmentTypes.image)) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
widget.onAddMoreFilesClick(DefaultAttachmentTypes.file);
|
||||
},
|
||||
child: Container(
|
||||
constraints: const BoxConstraints.expand(),
|
||||
color: widget.streamChatTheme.colorTheme.inputBg,
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
context.translations.addMoreFilesLabel,
|
||||
style: TextStyle(
|
||||
color: widget.streamChatTheme.colorTheme.accentPrimary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return MediaListView(
|
||||
selectedIds: widget.selectedMedias,
|
||||
onSelect: widget.onMediaSelected,
|
||||
);
|
||||
}
|
||||
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
PhotoManager.openSetting();
|
||||
},
|
||||
child: Container(
|
||||
color: widget.streamChatTheme.colorTheme.inputBg,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
'svgs/icon_picture_empty_state.svg',
|
||||
package: 'stream_chat_flutter',
|
||||
height: 140,
|
||||
color: widget.streamChatTheme.colorTheme.disabled,
|
||||
),
|
||||
Text(
|
||||
context.translations.enablePhotoAndVideoAccessMessage,
|
||||
style: widget.streamChatTheme.textTheme.body.copyWith(
|
||||
color: widget.streamChatTheme.colorTheme.textLowEmphasis,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Center(
|
||||
child: Text(
|
||||
context.translations.allowGalleryAccessMessage,
|
||||
style: widget.streamChatTheme.textTheme.bodyBold.copyWith(
|
||||
color: widget.streamChatTheme.colorTheme.accentPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CustomAttachmentType {
|
||||
String type;
|
||||
CustomAttachmentIconBuilder iconBuilder;
|
||||
WidgetBuilder pickerBuilder;
|
||||
|
||||
CustomAttachmentType({
|
||||
required this.type,
|
||||
required this.iconBuilder,
|
||||
required this.pickerBuilder,
|
||||
});
|
||||
}
|
||||
@@ -1 +1,98 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
class StreamMessageSendButton extends StatelessWidget {
|
||||
final int timeOut;
|
||||
final bool isIdle;
|
||||
final bool isCommandEnabled;
|
||||
final bool isEditEnabled;
|
||||
final Widget? idleSendButton;
|
||||
final Widget? activeSendButton;
|
||||
final VoidCallback onSendMessage;
|
||||
|
||||
const StreamMessageSendButton({
|
||||
Key? key,
|
||||
this.timeOut = 0,
|
||||
this.isIdle = true,
|
||||
this.isCommandEnabled = false,
|
||||
this.isEditEnabled = false,
|
||||
this.idleSendButton,
|
||||
this.activeSendButton,
|
||||
required this.onSendMessage,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var _streamChatTheme = StreamChatTheme.of(context);
|
||||
|
||||
late Widget sendButton;
|
||||
if (timeOut > 0) {
|
||||
sendButton = CountdownButton(count: timeOut);
|
||||
} else if (isIdle) {
|
||||
sendButton = idleSendButton ?? _buildIdleSendButton(context);
|
||||
} else {
|
||||
sendButton = activeSendButton != null
|
||||
? InkWell(
|
||||
onTap: onSendMessage,
|
||||
child: activeSendButton,
|
||||
)
|
||||
: _buildSendButton(context);
|
||||
}
|
||||
|
||||
return AnimatedSwitcher(
|
||||
duration: _streamChatTheme.messageInputTheme.sendAnimationDuration!,
|
||||
child: sendButton,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIdleSendButton(BuildContext context) {
|
||||
var _messageInputTheme = MessageInputTheme.of(context);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: StreamSvgIcon(
|
||||
assetName: _getIdleSendIcon(),
|
||||
color: _messageInputTheme.sendButtonIdleColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSendButton(BuildContext context) {
|
||||
var _messageInputTheme = MessageInputTheme.of(context);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: IconButton(
|
||||
onPressed: onSendMessage,
|
||||
padding: const EdgeInsets.all(0),
|
||||
splashRadius: 24,
|
||||
constraints: const BoxConstraints.tightFor(
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
icon: StreamSvgIcon(
|
||||
assetName: _getSendIcon(),
|
||||
color: _messageInputTheme.sendButtonColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getIdleSendIcon() {
|
||||
if (isCommandEnabled) {
|
||||
return 'Icon_search.svg';
|
||||
} else {
|
||||
return 'Icon_circle_right.svg';
|
||||
}
|
||||
}
|
||||
|
||||
String _getSendIcon() {
|
||||
if (isEditEnabled) {
|
||||
return 'Icon_circle_up.svg';
|
||||
} else if (isCommandEnabled) {
|
||||
return 'Icon_search.svg';
|
||||
} else {
|
||||
return 'Icon_circle_up.svg';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,9 @@ export 'src/message_search_item.dart';
|
||||
export 'src/message_search_list_view.dart';
|
||||
export 'src/message_text.dart';
|
||||
export 'src/message_widget.dart';
|
||||
export 'src/mip/countdown_button.dart';
|
||||
export 'src/mip/stream_attachment_picker.dart';
|
||||
export 'src/mip/stream_message_send_button.dart';
|
||||
export 'src/mip/stream_message_text_field.dart';
|
||||
export 'src/option_list_tile.dart';
|
||||
export 'src/reaction_icon.dart';
|
||||
|
||||
@@ -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,11 @@
|
||||
✅ Added
|
||||
|
||||
- Added `MessageInputController` to hold `Message` related data.
|
||||
|
||||
## 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).
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
|
||||
/// A Controller for storing and handling the [Message]
|
||||
/// Controller for storing and mutating a [Message] value.
|
||||
class MessageInputController extends ValueNotifier<Message> {
|
||||
/// Creates a controller for an editable text field.
|
||||
///
|
||||
@@ -17,7 +17,8 @@ class MessageInputController extends ValueNotifier<Message> {
|
||||
factory MessageInputController.fromText(String? text) =>
|
||||
MessageInputController._(Message(text: text));
|
||||
|
||||
/// Creates a controller for an editable textfield from initial [attachments].
|
||||
/// Creates a controller for an editable text field from an initial
|
||||
/// [attachments].
|
||||
factory MessageInputController.fromAttachments(
|
||||
List<Attachment> attachments,
|
||||
) =>
|
||||
@@ -136,8 +137,9 @@ class MessageInputController extends ValueNotifier<Message> {
|
||||
/// After calling this function, [text], [attachments] and [mentionedUsers]
|
||||
/// all will be empty.
|
||||
///
|
||||
/// Calling this will notify the listeners of this [MessageInputController]
|
||||
/// that they need to update (it calls [notifyListeners]). For this reason,
|
||||
/// Calling this will notify all the listeners of this
|
||||
/// [MessageInputController] that they need to update
|
||||
/// (it calls [notifyListeners]). For this reason,
|
||||
/// this method should only be called between frames, e.g. in response to user
|
||||
/// actions, not during the build, layout, or paint phases.
|
||||
void clear() {
|
||||
|
||||
@@ -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(() {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user