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:
|
exclude:
|
||||||
- packages/*/lib/**/*.g.dart
|
- packages/*/lib/**/*.g.dart
|
||||||
- packages/*/lib/src/emoji/**
|
- packages/*/lib/src/emoji/**
|
||||||
|
- packages/*/lib/scrollable_positioned_list/**
|
||||||
- packages/*/lib/**/*.freezed.dart
|
- packages/*/lib/**/*.freezed.dart
|
||||||
|
|
||||||
linter:
|
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
|
🐞 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
|
## 3.1.1
|
||||||
|
|
||||||
|
|||||||
@@ -1934,8 +1934,6 @@ class ChannelClientState {
|
|||||||
read: newReads,
|
read: newReads,
|
||||||
pinnedMessages: updatedState.pinnedMessages,
|
pinnedMessages: updatedState.pinnedMessages,
|
||||||
);
|
);
|
||||||
|
|
||||||
_computeUnread();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int _sortByCreatedAt(Message a, Message b) =>
|
int _sortByCreatedAt(Message a, Message b) =>
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart';
|
|||||||
/// Current package version
|
/// Current package version
|
||||||
/// Used in [StreamChatClient] to build the `x-stream-client` header
|
/// Used in [StreamChatClient] to build the `x-stream-client` header
|
||||||
// ignore: constant_identifier_names
|
// ignore: constant_identifier_names
|
||||||
const PACKAGE_VERSION = '3.1.1';
|
const PACKAGE_VERSION = '3.2.0';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: stream_chat
|
name: stream_chat
|
||||||
homepage: https://getstream.io/
|
homepage: https://getstream.io/
|
||||||
description: The official Dart client for Stream Chat, a service for building chat applications.
|
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
|
repository: https://github.com/GetStream/stream-chat-flutter
|
||||||
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ dependencies:
|
|||||||
collection: ^1.15.0
|
collection: ^1.15.0
|
||||||
dio: ^4.0.0
|
dio: ^4.0.0
|
||||||
equatable: ^2.0.0
|
equatable: ^2.0.0
|
||||||
freezed_annotation: ^0.14.0
|
freezed_annotation: ^0.15.0
|
||||||
http_parser: ^4.0.0
|
http_parser: ^4.0.0
|
||||||
jose: ^0.3.2
|
jose: ^0.3.2
|
||||||
json_annotation: ^4.0.1
|
json_annotation: ^4.0.1
|
||||||
@@ -28,7 +28,7 @@ dependencies:
|
|||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
build_runner: ^2.0.1
|
build_runner: ^2.0.1
|
||||||
dart_code_metrics: ^4.4.0
|
dart_code_metrics: ^4.4.0
|
||||||
freezed: ^0.14.1+3
|
freezed: ^0.15.0+1
|
||||||
json_serializable: ^5.0.2
|
json_serializable: ^6.0.1
|
||||||
mocktail: ^0.1.1
|
mocktail: ^0.2.0
|
||||||
test: ^1.17.12
|
test: ^1.17.12
|
||||||
@@ -118,9 +118,9 @@ void main() {
|
|||||||
|
|
||||||
setUpAll(() {
|
setUpAll(() {
|
||||||
// Fallback values
|
// Fallback values
|
||||||
registerFallbackValue<Message>(FakeMessage());
|
registerFallbackValue(FakeMessage());
|
||||||
registerFallbackValue<List<Message>>(<Message>[]);
|
registerFallbackValue(<Message>[]);
|
||||||
registerFallbackValue<AttachmentFile>(FakeAttachmentFile());
|
registerFallbackValue(FakeAttachmentFile());
|
||||||
|
|
||||||
// detached loggers
|
// detached loggers
|
||||||
when(() => client.detachedLogger(any())).thenAnswer((invocation) {
|
when(() => client.detachedLogger(any())).thenAnswer((invocation) {
|
||||||
@@ -176,9 +176,9 @@ void main() {
|
|||||||
|
|
||||||
setUpAll(() {
|
setUpAll(() {
|
||||||
// Fallback values
|
// Fallback values
|
||||||
registerFallbackValue<Message>(FakeMessage());
|
registerFallbackValue(FakeMessage());
|
||||||
registerFallbackValue<AttachmentFile>(FakeAttachmentFile());
|
registerFallbackValue(FakeAttachmentFile());
|
||||||
registerFallbackValue<Event>(FakeEvent());
|
registerFallbackValue(FakeEvent());
|
||||||
|
|
||||||
// detached loggers
|
// detached loggers
|
||||||
when(() => client.detachedLogger(any())).thenAnswer((invocation) {
|
when(() => client.detachedLogger(any())).thenAnswer((invocation) {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ void main() {
|
|||||||
|
|
||||||
setUpAll(() {
|
setUpAll(() {
|
||||||
// fallback values
|
// fallback values
|
||||||
registerFallbackValue<User>(FakeUser());
|
registerFallbackValue(FakeUser());
|
||||||
});
|
});
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
@@ -230,7 +230,7 @@ void main() {
|
|||||||
|
|
||||||
setUpAll(() {
|
setUpAll(() {
|
||||||
// fallback values
|
// fallback values
|
||||||
registerFallbackValue<User>(FakeUser());
|
registerFallbackValue(FakeUser());
|
||||||
});
|
});
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
@@ -311,7 +311,7 @@ void main() {
|
|||||||
|
|
||||||
setUpAll(() {
|
setUpAll(() {
|
||||||
// fallback values
|
// fallback values
|
||||||
registerFallbackValue<User>(FakeUser());
|
registerFallbackValue(FakeUser());
|
||||||
});
|
});
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
@@ -399,7 +399,7 @@ void main() {
|
|||||||
|
|
||||||
setUpAll(() {
|
setUpAll(() {
|
||||||
// fallback values
|
// fallback values
|
||||||
registerFallbackValue<User>(FakeUser());
|
registerFallbackValue(FakeUser());
|
||||||
});
|
});
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
@@ -523,9 +523,9 @@ void main() {
|
|||||||
|
|
||||||
setUpAll(() {
|
setUpAll(() {
|
||||||
// fallback values
|
// fallback values
|
||||||
registerFallbackValue<Event>(FakeEvent());
|
registerFallbackValue(FakeEvent());
|
||||||
registerFallbackValue<PaginationParams>(const PaginationParams());
|
registerFallbackValue(const PaginationParams());
|
||||||
registerFallbackValue<ChannelState>(FakeChannelState());
|
registerFallbackValue(FakeChannelState());
|
||||||
});
|
});
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
@@ -827,9 +827,9 @@ void main() {
|
|||||||
|
|
||||||
setUpAll(() {
|
setUpAll(() {
|
||||||
// fallback values
|
// fallback values
|
||||||
registerFallbackValue<Event>(FakeEvent());
|
registerFallbackValue(FakeEvent());
|
||||||
registerFallbackValue<Message>(FakeMessage());
|
registerFallbackValue(FakeMessage());
|
||||||
registerFallbackValue<PaginationParams>(const PaginationParams());
|
registerFallbackValue(const PaginationParams());
|
||||||
});
|
});
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ void main() {
|
|||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
fileUploader = StreamAttachmentFileUploader(client);
|
fileUploader = StreamAttachmentFileUploader(client);
|
||||||
registerFallbackValue<MultipartFile>(FakeMultiPartFile());
|
registerFallbackValue(FakeMultiPartFile());
|
||||||
});
|
});
|
||||||
|
|
||||||
Response successResponse(String path, {Object? data}) => Response(
|
Response successResponse(String path, {Object? data}) => Response(
|
||||||
|
|||||||
@@ -1,10 +1,23 @@
|
|||||||
## Upcoming
|
## 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 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
|
||||||
|
|
||||||
|
- Fixed message highlight animation alignment in `MessageListView`
|
||||||
- [[#491]](https://github.com/GetStream/stream-chat-flutter/issues/491): Fix `MediaListView` showing media in wrong order.
|
- [[#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
|
✅ 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(
|
child: Text(
|
||||||
context.translations.cancelLabel.toLowerCase(),
|
context.translations.cancelLabel
|
||||||
|
.toLowerCase()
|
||||||
|
.capitalize(),
|
||||||
style: StreamChatTheme.of(context)
|
style: StreamChatTheme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.bodyBold
|
.bodyBold
|
||||||
|
|||||||
@@ -72,7 +72,11 @@ class ImageAttachment extends AttachmentWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var imageUri = Uri.parse(imageUrl);
|
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 = imageUri.replace(queryParameters: {
|
||||||
...imageUri.queryParameters,
|
...imageUri.queryParameters,
|
||||||
'h': '400',
|
'h': '400',
|
||||||
@@ -80,7 +84,7 @@ class ImageAttachment extends AttachmentWidget {
|
|||||||
'crop': 'center',
|
'crop': 'center',
|
||||||
'resize': 'crop',
|
'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 = imageUri.replace(queryParameters: {
|
||||||
...imageUri.queryParameters,
|
...imageUri.queryParameters,
|
||||||
'height': '400',
|
'height': '400',
|
||||||
@@ -93,7 +97,7 @@ class ImageAttachment extends AttachmentWidget {
|
|||||||
return _buildImageAttachment(
|
return _buildImageAttachment(
|
||||||
context,
|
context,
|
||||||
CachedNetworkImage(
|
CachedNetworkImage(
|
||||||
cacheKey: imageUrl,
|
cacheKey: imageUri.replace(queryParameters: {}).toString(),
|
||||||
height: size?.height,
|
height: size?.height,
|
||||||
width: size?.width,
|
width: size?.width,
|
||||||
placeholder: (context, __) {
|
placeholder: (context, __) {
|
||||||
|
|||||||
@@ -9,13 +9,11 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
|
||||||
import 'package:shimmer/shimmer.dart';
|
import 'package:shimmer/shimmer.dart';
|
||||||
import 'package:stream_chat_flutter/src/commands_overlay.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/emoji.dart';
|
||||||
import 'package:stream_chat_flutter/src/emoji_overlay.dart';
|
import 'package:stream_chat_flutter/src/emoji_overlay.dart';
|
||||||
import 'package:stream_chat_flutter/src/extension.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/message_list_view.dart';
|
||||||
import 'package:stream_chat_flutter/src/multi_overlay.dart';
|
import 'package:stream_chat_flutter/src/multi_overlay.dart';
|
||||||
import 'package:stream_chat_flutter/src/quoted_message_widget.dart';
|
import 'package:stream_chat_flutter/src/quoted_message_widget.dart';
|
||||||
@@ -79,6 +77,20 @@ typedef ActionButtonBuilder = Widget Function(
|
|||||||
IconButton defaultActionButton,
|
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]
|
/// Location for actions on the [MessageInput]
|
||||||
enum ActionsLocation {
|
enum ActionsLocation {
|
||||||
/// Align to left
|
/// Align to left
|
||||||
@@ -204,6 +216,8 @@ class MessageInput extends StatefulWidget {
|
|||||||
this.commandButtonBuilder,
|
this.commandButtonBuilder,
|
||||||
this.customOverlays = const [],
|
this.customOverlays = const [],
|
||||||
this.mentionAllAppUsers = false,
|
this.mentionAllAppUsers = false,
|
||||||
|
this.attachmentsPickerBuilder,
|
||||||
|
this.sendButtonBuilder,
|
||||||
}) : assert(
|
}) : assert(
|
||||||
initialMessage == null || editMessage == null,
|
initialMessage == null || editMessage == null,
|
||||||
"Can't provide both `initialMessage` and `editMessage`",
|
"Can't provide both `initialMessage` and `editMessage`",
|
||||||
@@ -322,6 +336,12 @@ class MessageInput extends StatefulWidget {
|
|||||||
/// Defaults to false.
|
/// Defaults to false.
|
||||||
final bool mentionAllAppUsers;
|
final bool mentionAllAppUsers;
|
||||||
|
|
||||||
|
/// Builds bottom sheet when attachment picker is opened.
|
||||||
|
final AttachmentsPickerBuilder? attachmentsPickerBuilder;
|
||||||
|
|
||||||
|
/// Builder for creating send button
|
||||||
|
final MessageRelatedBuilder? sendButtonBuilder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MessageInputState createState() => MessageInputState();
|
MessageInputState createState() => MessageInputState();
|
||||||
|
|
||||||
@@ -349,7 +369,6 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
Command? _chosenCommand;
|
Command? _chosenCommand;
|
||||||
bool _actionsShrunk = false;
|
bool _actionsShrunk = false;
|
||||||
bool _openFilePickerSection = false;
|
bool _openFilePickerSection = false;
|
||||||
int _filePickerIndex = 0;
|
|
||||||
|
|
||||||
/// The editing controller passed to the input TextField
|
/// The editing controller passed to the input TextField
|
||||||
late final MessageInputController messageInputController =
|
late final MessageInputController messageInputController =
|
||||||
@@ -524,7 +543,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
widget.actionsLocation == ActionsLocation.right)
|
widget.actionsLocation == ActionsLocation.right)
|
||||||
_buildExpandActionsButton(context),
|
_buildExpandActionsButton(context),
|
||||||
if (widget.sendButtonLocation == SendButtonLocation.outside)
|
if (widget.sendButtonLocation == SendButtonLocation.outside)
|
||||||
_animateSendButton(context),
|
_buildSendButton(context),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -588,25 +607,18 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _animateSendButton(BuildContext context) {
|
Widget _buildSendButton(BuildContext context) {
|
||||||
late Widget sendButton;
|
if (widget.sendButtonBuilder != null) {
|
||||||
if (_timeOut > 0) {
|
return widget.sendButtonBuilder!(context, messageInputController);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return AnimatedSwitcher(
|
return StreamMessageSendButton(
|
||||||
duration: _streamChatTheme.messageInputTheme.sendAnimationDuration!,
|
onSendMessage: sendMessage,
|
||||||
child: sendButton,
|
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)
|
widget.actionsLocation == ActionsLocation.rightInside)
|
||||||
_buildExpandActionsButton(context),
|
_buildExpandActionsButton(context),
|
||||||
if (widget.sendButtonLocation == SendButtonLocation.inside)
|
if (widget.sendButtonLocation == SendButtonLocation.inside)
|
||||||
_animateSendButton(context),
|
_buildSendButton(context),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
).merge(passedDecoration);
|
).merge(passedDecoration);
|
||||||
@@ -946,236 +958,33 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildFilePickerSection() {
|
Widget _buildFilePickerSection() {
|
||||||
final _attachmentContainsFile =
|
final picker = StreamAttachmentPicker(
|
||||||
messageInputController.attachments.any((it) => it.type == 'file');
|
messageInputController: messageInputController,
|
||||||
|
onFilePicked: pickFile,
|
||||||
final attachmentLimitCrossed =
|
isOpen: _openFilePickerSection,
|
||||||
messageInputController.attachments.length >= widget.attachmentLimit;
|
pickerSize: _openFilePickerSection ? _kMinMediaPickerSize : 0,
|
||||||
|
attachmentLimit: widget.attachmentLimit,
|
||||||
Color _getIconColor(int index) {
|
onAttachmentLimitExceeded: widget.onAttachmentLimitExceed,
|
||||||
final streamChatThemeData = _streamChatTheme;
|
maxAttachmentSize: widget.maxAttachmentSize,
|
||||||
switch (index) {
|
compressedVideoQuality: widget.compressedVideoQuality,
|
||||||
case 0:
|
compressedVideoFrameRate: widget.compressedVideoFrameRate,
|
||||||
return messageInputController.attachments.isEmpty
|
onChangeInputState: (val) {
|
||||||
? streamChatThemeData.colorTheme.accentPrimary
|
setState(() {
|
||||||
: (!_attachmentContainsFile
|
_inputEnabled = val;
|
||||||
? streamChatThemeData.colorTheme.accentPrimary
|
});
|
||||||
: streamChatThemeData.colorTheme.textHighEmphasis
|
},
|
||||||
.withOpacity(0.2));
|
onError: _showErrorAlert,
|
||||||
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,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (mediaFile == null) return;
|
if (_openFilePickerSection && widget.attachmentsPickerBuilder != null) {
|
||||||
|
return widget.attachmentsPickerBuilder!(
|
||||||
var file = AttachmentFile(
|
context,
|
||||||
path: mediaFile.path,
|
messageInputController,
|
||||||
size: await mediaFile.length(),
|
picker,
|
||||||
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',
|
|
||||||
);
|
);
|
||||||
_addAttachments([attachment]);
|
}
|
||||||
});
|
|
||||||
|
return picker;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMentionsOverlayEntry() {
|
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
|
/// Sends the current message
|
||||||
Future<void> sendMessage() async {
|
Future<void> sendMessage() async {
|
||||||
var text = messageInputController.text.trim();
|
var text = messageInputController.text.trim();
|
||||||
if (text.isEmpty && messageInputController.attachments.isEmpty) {
|
final attachments = messageInputController.attachments;
|
||||||
|
|
||||||
|
if (text.isEmpty && attachments.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1805,7 +1573,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
if (widget.editMessage != null) {
|
if (widget.editMessage != null) {
|
||||||
message = widget.editMessage!.copyWith(
|
message = widget.editMessage!.copyWith(
|
||||||
text: text,
|
text: text,
|
||||||
attachments: messageInputController.attachments,
|
attachments: attachments,
|
||||||
mentionedUsers: messageInputController.mentionedUsers
|
mentionedUsers: messageInputController.mentionedUsers
|
||||||
.where((u) => text.contains('@${u.name}'))
|
.where((u) => text.contains('@${u.name}'))
|
||||||
.toList(),
|
.toList(),
|
||||||
@@ -1814,7 +1582,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
message = (widget.initialMessage ?? Message()).copyWith(
|
message = (widget.initialMessage ?? Message()).copyWith(
|
||||||
parentId: widget.parentMessage?.id,
|
parentId: widget.parentMessage?.id,
|
||||||
text: text,
|
text: text,
|
||||||
attachments: messageInputController.attachments,
|
attachments: attachments,
|
||||||
mentionedUsers: messageInputController.mentionedUsers
|
mentionedUsers: messageInputController.mentionedUsers
|
||||||
.where((u) => text.contains('@${u.name}'))
|
.where((u) => text.contains('@${u.name}'))
|
||||||
.toList(),
|
.toList(),
|
||||||
@@ -1960,7 +1728,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
void didChangeDependencies() {
|
void didChangeDependencies() {
|
||||||
_streamChatTheme = StreamChatTheme.of(context);
|
_streamChatTheme = StreamChatTheme.of(context);
|
||||||
_messageInputTheme = MessageInputTheme.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) &&
|
if ((widget.editMessage != null || widget.initialMessage != null) &&
|
||||||
!_initialized) {
|
!_initialized) {
|
||||||
@@ -1970,140 +1738,3 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
super.didChangeDependencies();
|
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/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:jiffy/jiffy.dart';
|
import 'package:jiffy/jiffy.dart';
|
||||||
import 'package:rxdart/rxdart.dart';
|
import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
|
||||||
import 'package:stream_chat_flutter/src/extension.dart';
|
import 'package:stream_chat_flutter/src/extension.dart';
|
||||||
import 'package:stream_chat_flutter/src/info_tile.dart';
|
import 'package:stream_chat_flutter/src/info_tile.dart';
|
||||||
import 'package:stream_chat_flutter/src/message_widget.dart';
|
import 'package:stream_chat_flutter/src/message_widget.dart';
|
||||||
@@ -145,7 +144,8 @@ class MessageListView extends StatefulWidget {
|
|||||||
this.threadBuilder,
|
this.threadBuilder,
|
||||||
this.onThreadTap,
|
this.onThreadTap,
|
||||||
this.dateDividerBuilder,
|
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.initialScrollIndex,
|
||||||
this.initialAlignment,
|
this.initialAlignment,
|
||||||
this.scrollController,
|
this.scrollController,
|
||||||
@@ -225,7 +225,7 @@ class MessageListView extends StatefulWidget {
|
|||||||
final ItemPositionsListener? itemPositionListener;
|
final ItemPositionsListener? itemPositionListener;
|
||||||
|
|
||||||
/// The ScrollPhysics used by the ListView
|
/// The ScrollPhysics used by the ListView
|
||||||
final ScrollPhysics scrollPhysics;
|
final ScrollPhysics? scrollPhysics;
|
||||||
|
|
||||||
/// Called when message item gets swiped
|
/// Called when message item gets swiped
|
||||||
final OnMessageSwiped? onMessageSwiped;
|
final OnMessageSwiped? onMessageSwiped;
|
||||||
@@ -296,7 +296,7 @@ class MessageListView extends StatefulWidget {
|
|||||||
class _MessageListViewState extends State<MessageListView> {
|
class _MessageListViewState extends State<MessageListView> {
|
||||||
ItemScrollController? _scrollController;
|
ItemScrollController? _scrollController;
|
||||||
void Function(Message)? _onThreadTap;
|
void Function(Message)? _onThreadTap;
|
||||||
bool _showScrollToBottom = false;
|
final ValueNotifier<bool> _showScrollToBottom = ValueNotifier(false);
|
||||||
late final ItemPositionsListener _itemPositionListener;
|
late final ItemPositionsListener _itemPositionListener;
|
||||||
int? _messageListLength;
|
int? _messageListLength;
|
||||||
StreamChannelState? streamChannel;
|
StreamChannelState? streamChannel;
|
||||||
@@ -306,12 +306,17 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
final initialScrollIndex = widget.initialScrollIndex;
|
final initialScrollIndex = widget.initialScrollIndex;
|
||||||
if (initialScrollIndex != null) return initialScrollIndex;
|
if (initialScrollIndex != null) return initialScrollIndex;
|
||||||
if (streamChannel!.initialMessageId != null) {
|
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 totalMessages = messages.length;
|
||||||
final messageIndex =
|
final messageIndex =
|
||||||
messages.indexWhere((e) => e.id == streamChannel!.initialMessageId);
|
messages.indexWhere((e) => e.id == streamChannel!.initialMessageId);
|
||||||
final index = totalMessages - messageIndex;
|
final index = totalMessages - messageIndex;
|
||||||
if (index != 0) return index - 1;
|
if (index != 0) return index + 1;
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
@@ -320,7 +325,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
double get _initialAlignment {
|
double get _initialAlignment {
|
||||||
final initialAlignment = widget.initialAlignment;
|
final initialAlignment = widget.initialAlignment;
|
||||||
if (initialAlignment != null) return initialAlignment;
|
if (initialAlignment != null) return initialAlignment;
|
||||||
return 0;
|
return 0.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _isInitialMessage(String id) => streamChannel!.initialMessageId == id;
|
bool _isInitialMessage(String id) => streamChannel!.initialMessageId == id;
|
||||||
@@ -329,7 +334,6 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
|
|
||||||
bool get _isThreadConversation => widget.parentMessage != null;
|
bool get _isThreadConversation => widget.parentMessage != null;
|
||||||
|
|
||||||
bool _topPaginationActive = false;
|
|
||||||
bool _bottomPaginationActive = false;
|
bool _bottomPaginationActive = false;
|
||||||
|
|
||||||
int initialIndex = 0;
|
int initialIndex = 0;
|
||||||
@@ -337,6 +341,8 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
|
|
||||||
List<Message> messages = <Message>[];
|
List<Message> messages = <Message>[];
|
||||||
|
|
||||||
|
Map<String, int> messagesIndex = {};
|
||||||
|
|
||||||
bool initialMessageHighlightComplete = false;
|
bool initialMessageHighlightComplete = false;
|
||||||
|
|
||||||
bool _inBetweenList = false;
|
bool _inBetweenList = false;
|
||||||
@@ -382,6 +388,9 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
|
|
||||||
Widget _buildListView(List<Message> data) {
|
Widget _buildListView(List<Message> data) {
|
||||||
messages = data;
|
messages = data;
|
||||||
|
for (var index = 0; index < messages.length; index++) {
|
||||||
|
messagesIndex[messages[index].id] = index;
|
||||||
|
}
|
||||||
final newMessagesListLength = messages.length;
|
final newMessagesListLength = messages.length;
|
||||||
|
|
||||||
if (_messageListLength != null) {
|
if (_messageListLength != null) {
|
||||||
@@ -390,14 +399,13 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
final first = _itemPositionListener.itemPositions.value.first;
|
final first = _itemPositionListener.itemPositions.value.first;
|
||||||
final diff = newMessagesListLength - _messageListLength!;
|
final diff = newMessagesListLength - _messageListLength!;
|
||||||
if (diff > 0) {
|
if (diff > 0) {
|
||||||
initialIndex = first.index + diff;
|
if (messages[0].user?.id !=
|
||||||
initialAlignment = first.itemLeadingEdge;
|
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 {
|
onStartOfPage: () async {
|
||||||
_inBetweenList = false;
|
_inBetweenList = false;
|
||||||
if (!_upToDate) {
|
if (!_upToDate) {
|
||||||
_topPaginationActive = false;
|
|
||||||
_bottomPaginationActive = true;
|
_bottomPaginationActive = true;
|
||||||
return _paginateData(
|
return _paginateData(
|
||||||
streamChannel,
|
streamChannel,
|
||||||
@@ -451,7 +458,6 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
},
|
},
|
||||||
onEndOfPage: () async {
|
onEndOfPage: () async {
|
||||||
_inBetweenList = false;
|
_inBetweenList = false;
|
||||||
_topPaginationActive = true;
|
|
||||||
_bottomPaginationActive = false;
|
_bottomPaginationActive = false;
|
||||||
return _paginateData(
|
return _paginateData(
|
||||||
streamChannel,
|
streamChannel,
|
||||||
@@ -462,17 +468,26 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
_inBetweenList = true;
|
_inBetweenList = true;
|
||||||
},
|
},
|
||||||
child: ScrollablePositionedList.separated(
|
child: ScrollablePositionedList.separated(
|
||||||
key: _upToDate
|
key: (initialIndex != 0 && initialAlignment != 0)
|
||||||
? null
|
? ValueKey('$initialIndex-$initialAlignment')
|
||||||
: ValueKey(initialIndex + initialAlignment),
|
: null,
|
||||||
itemPositionsListener: _itemPositionListener,
|
itemPositionsListener: _itemPositionListener,
|
||||||
initialScrollIndex: initialIndex,
|
initialScrollIndex: initialIndex,
|
||||||
initialAlignment: initialAlignment,
|
initialAlignment: initialAlignment,
|
||||||
physics: widget.scrollPhysics,
|
physics: widget.scrollPhysics,
|
||||||
itemScrollController: _scrollController,
|
itemScrollController: _scrollController,
|
||||||
reverse: widget.reverse,
|
reverse: widget.reverse,
|
||||||
addAutomaticKeepAlives: false,
|
|
||||||
itemCount: itemCount,
|
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)
|
// 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)|
|
// eg: |Type| rev(|Index(item)|) rev(|Index(separator)|) |Index(item)| |Index(separator)|
|
||||||
@@ -624,14 +639,30 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
} else {
|
} else {
|
||||||
messageWidget = buildMessage(message, messages, i - 2);
|
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)
|
if (widget.showFloatingDateDivider)
|
||||||
_buildFloatingDateDivider(itemCount),
|
_buildFloatingDateDivider(itemCount),
|
||||||
],
|
],
|
||||||
@@ -751,24 +782,15 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
.index;
|
.index;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildScrollToBottom() => StreamBuilder<Tuple2<bool, int>>(
|
Widget _buildScrollToBottom() => StreamBuilder<int>(
|
||||||
stream: Rx.combineLatest2(
|
stream: streamChannel!.channel.state!.unreadCountStream,
|
||||||
streamChannel!.channel.state!.isUpToDateStream.distinct(),
|
|
||||||
streamChannel!.channel.state!.unreadCountStream.distinct(),
|
|
||||||
(bool isUpToDate, int unreadCount) => Tuple2(isUpToDate, unreadCount),
|
|
||||||
),
|
|
||||||
builder: (_, snapshot) {
|
builder: (_, snapshot) {
|
||||||
if (snapshot.hasError) {
|
if (snapshot.hasError) {
|
||||||
return const Offstage();
|
return const Offstage();
|
||||||
} else if (!snapshot.hasData) {
|
} else if (!snapshot.hasData) {
|
||||||
return const Offstage();
|
return const Offstage();
|
||||||
}
|
}
|
||||||
final isUpToDate = snapshot.data!.item1;
|
final unreadCount = snapshot.data!;
|
||||||
final showScrollToBottom = !isUpToDate || _showScrollToBottom;
|
|
||||||
if (!showScrollToBottom) {
|
|
||||||
return const Offstage();
|
|
||||||
}
|
|
||||||
final unreadCount = snapshot.data!.item2;
|
|
||||||
final showUnreadCount = unreadCount > 0 &&
|
final showUnreadCount = unreadCount > 0 &&
|
||||||
streamChannel!.channel.state!.members.any((e) =>
|
streamChannel!.channel.state!.members.any((e) =>
|
||||||
e.userId ==
|
e.userId ==
|
||||||
@@ -783,16 +805,21 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
children: [
|
children: [
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
backgroundColor: _streamTheme.colorTheme.barsBg,
|
backgroundColor: _streamTheme.colorTheme.barsBg,
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
if (unreadCount > 0) {
|
if (unreadCount > 0) {
|
||||||
streamChannel!.channel.markRead();
|
streamChannel!.channel.markRead();
|
||||||
}
|
}
|
||||||
if (!_upToDate) {
|
if (!_upToDate) {
|
||||||
_bottomPaginationActive = false;
|
_bottomPaginationActive = false;
|
||||||
_topPaginationActive = false;
|
initialAlignment = 0;
|
||||||
streamChannel!.reloadChannel();
|
initialIndex = 0;
|
||||||
|
await streamChannel!.reloadChannel();
|
||||||
|
|
||||||
|
WidgetsBinding.instance?.addPostFrameCallback((_) {
|
||||||
|
_scrollController!.jumpTo(index: 0);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
setState(() => _showScrollToBottom = false);
|
_showScrollToBottom.value = false;
|
||||||
_scrollController!.scrollTo(
|
_scrollController!.scrollTo(
|
||||||
index: 0,
|
index: 0,
|
||||||
duration: const Duration(seconds: 1),
|
duration: const Duration(seconds: 1),
|
||||||
@@ -854,9 +881,8 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
int index,
|
int index,
|
||||||
) {
|
) {
|
||||||
final messageWidget = buildMessage(message, messages, index);
|
final messageWidget = buildMessage(message, messages, index);
|
||||||
|
|
||||||
return VisibilityDetector(
|
return VisibilityDetector(
|
||||||
key: ValueKey<String>('BOTTOM-MESSAGE-${message.id}'),
|
key: ValueKey('visibility: ${message.id}'),
|
||||||
onVisibilityChanged: (visibility) {
|
onVisibilityChanged: (visibility) {
|
||||||
final isVisible = visibility.visibleBounds != Rect.zero;
|
final isVisible = visibility.visibleBounds != Rect.zero;
|
||||||
if (isVisible) {
|
if (isVisible) {
|
||||||
@@ -868,8 +894,8 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
if (_showScrollToBottom == isVisible) {
|
if (_showScrollToBottom.value == isVisible) {
|
||||||
setState(() => _showScrollToBottom = !isVisible);
|
_showScrollToBottom.value = !isVisible;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -948,16 +974,11 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
return defaultMessageWidget;
|
return defaultMessageWidget;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildMessage(
|
Widget buildMessage(Message message, List<Message> messages, int index) {
|
||||||
Message message,
|
|
||||||
List<Message> messages,
|
|
||||||
int index,
|
|
||||||
) {
|
|
||||||
if ((message.type == 'system' || message.type == 'error') &&
|
if ((message.type == 'system' || message.type == 'error') &&
|
||||||
message.text?.isNotEmpty == true) {
|
message.text?.isNotEmpty == true) {
|
||||||
return widget.systemMessageBuilder?.call(context, message) ??
|
return widget.systemMessageBuilder?.call(context, message) ??
|
||||||
SystemMessage(
|
SystemMessage(
|
||||||
key: ValueKey<String>('MESSAGE-${message.id}'),
|
|
||||||
message: message,
|
message: message,
|
||||||
onMessageTap: (message) {
|
onMessageTap: (message) {
|
||||||
if (widget.onSystemMessageTap != null) {
|
if (widget.onSystemMessageTap != null) {
|
||||||
@@ -1037,7 +1058,6 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
members.firstWhereOrNull((e) => e.user!.id == currentUser!.id);
|
members.firstWhereOrNull((e) => e.user!.id == currentUser!.id);
|
||||||
|
|
||||||
Widget messageWidget = MessageWidget(
|
Widget messageWidget = MessageWidget(
|
||||||
key: ValueKey<String>('MESSAGE-${message.id}'),
|
|
||||||
message: message,
|
message: message,
|
||||||
reverse: isMyMessage,
|
reverse: isMyMessage,
|
||||||
showReactions: !message.isDeleted,
|
showReactions: !message.isDeleted,
|
||||||
@@ -1049,23 +1069,20 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
showSendingIndicator: showSendingIndicator,
|
showSendingIndicator: showSendingIndicator,
|
||||||
showUserAvatar: showUserAvatar,
|
showUserAvatar: showUserAvatar,
|
||||||
onQuotedMessageTap: (quotedMessageId) async {
|
onQuotedMessageTap: (quotedMessageId) async {
|
||||||
// ignore: prefer_function_declarations_over_variables
|
if (messages.map((e) => e.id).contains(quotedMessageId)) {
|
||||||
final scrollToIndex = () {
|
|
||||||
final index = messages.indexWhere((m) => m.id == quotedMessageId);
|
final index = messages.indexWhere((m) => m.id == quotedMessageId);
|
||||||
_scrollController?.scrollTo(
|
_scrollController?.scrollTo(
|
||||||
index: index,
|
index: index + 2, // +2 to account for loader and footer
|
||||||
duration: const Duration(milliseconds: 350),
|
duration: const Duration(seconds: 1),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
alignment: 0.1,
|
||||||
);
|
);
|
||||||
};
|
|
||||||
if (messages.map((e) => e.id).contains(quotedMessageId)) {
|
|
||||||
scrollToIndex();
|
|
||||||
} else {
|
} else {
|
||||||
await streamChannel!.loadChannelAtMessage(quotedMessageId).then((_) {
|
await streamChannel!
|
||||||
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
.loadChannelAtMessage(quotedMessageId)
|
||||||
if (messages.map((e) => e.id).contains(quotedMessageId)) {
|
.then((_) async {
|
||||||
scrollToIndex();
|
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: child,
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(top: 4),
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -1229,27 +1246,25 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
initialIndex = _initialIndex;
|
initialIndex = _initialIndex;
|
||||||
initialAlignment = _initialAlignment;
|
initialAlignment = _initialAlignment;
|
||||||
|
|
||||||
WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
|
if (_scrollController?.isAttached == true) {
|
||||||
if (_scrollController?.isAttached == true) {
|
_scrollController?.jumpTo(
|
||||||
_scrollController?.jumpTo(
|
index: initialIndex,
|
||||||
index: initialIndex,
|
alignment: initialAlignment,
|
||||||
alignment: initialAlignment,
|
);
|
||||||
);
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
_messageNewListener =
|
_messageNewListener =
|
||||||
streamChannel!.channel.on(EventType.messageNew).listen((event) {
|
streamChannel!.channel.on(EventType.messageNew).listen((event) {
|
||||||
if (_upToDate) {
|
if (_upToDate) {
|
||||||
_bottomPaginationActive = false;
|
_bottomPaginationActive = false;
|
||||||
_topPaginationActive = false;
|
|
||||||
}
|
}
|
||||||
if (event.message?.parentId == widget.parentMessage?.id &&
|
if (event.message?.parentId == widget.parentMessage?.id &&
|
||||||
event.message!.user!.id ==
|
event.message!.user!.id ==
|
||||||
streamChannel!.channel.client.state.currentUser!.id) {
|
streamChannel!.channel.client.state.currentUser!.id) {
|
||||||
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||||
_scrollController?.jumpTo(
|
_scrollController?.scrollTo(
|
||||||
index: 0,
|
index: 0,
|
||||||
|
duration: const Duration(seconds: 1),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -608,6 +608,8 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
final bottomRowPadding =
|
final bottomRowPadding =
|
||||||
widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 0.5;
|
widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 0.5;
|
||||||
|
|
||||||
|
final showReactions = _shouldShowReactions;
|
||||||
|
|
||||||
return Material(
|
return Material(
|
||||||
type: widget.message.pinned && widget.showPinHighlight
|
type: widget.message.pinned && widget.showPinHighlight
|
||||||
? MaterialType.card
|
? MaterialType.card
|
||||||
@@ -671,17 +673,23 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
SizedBox(width: avatarWidth + 4),
|
SizedBox(width: avatarWidth + 4),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: PortalEntry(
|
child: PortalEntry(
|
||||||
portal: Container(
|
visible: showReactions,
|
||||||
transform: Matrix4.translationValues(
|
portal: showReactions
|
||||||
widget.reverse ? 12 : -12,
|
? Container(
|
||||||
0,
|
transform:
|
||||||
0,
|
Matrix4.translationValues(
|
||||||
),
|
widget.reverse ? 12 : -12,
|
||||||
constraints: const BoxConstraints(
|
0,
|
||||||
maxWidth: 22 * 6.0,
|
0,
|
||||||
),
|
),
|
||||||
child: _buildReactionIndicator(context),
|
constraints: const BoxConstraints(
|
||||||
),
|
maxWidth: 22 * 6.0,
|
||||||
|
),
|
||||||
|
child: _buildReactionIndicator(
|
||||||
|
context,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
portalAnchor:
|
portalAnchor:
|
||||||
Alignment(widget.reverse ? 1 : -1, -1),
|
Alignment(widget.reverse ? 1 : -1, -1),
|
||||||
childAnchor:
|
childAnchor:
|
||||||
@@ -1036,9 +1044,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
|
|
||||||
return AnimatedSwitcher(
|
return AnimatedSwitcher(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
child: (widget.showReactions &&
|
child: _shouldShowReactions
|
||||||
(widget.message.reactionCounts?.isNotEmpty == true) &&
|
|
||||||
!widget.message.isDeleted)
|
|
||||||
? GestureDetector(
|
? GestureDetector(
|
||||||
onTap: () => _showMessageReactionsModalBottomSheet(context),
|
onTap: () => _showMessageReactionsModalBottomSheet(context),
|
||||||
child: ReactionBubble(
|
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) {
|
void _showMessageActionModalBottomSheet(BuildContext context) {
|
||||||
final channel = StreamChannel.of(context).channel;
|
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_search_list_view.dart';
|
||||||
export 'src/message_text.dart';
|
export 'src/message_text.dart';
|
||||||
export 'src/message_widget.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/mip/stream_message_text_field.dart';
|
||||||
export 'src/option_list_tile.dart';
|
export 'src/option_list_tile.dart';
|
||||||
export 'src/reaction_icon.dart';
|
export 'src/reaction_icon.dart';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: stream_chat_flutter
|
name: stream_chat_flutter
|
||||||
homepage: https://github.com/GetStream/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.
|
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
|
repository: https://github.com/GetStream/stream-chat-flutter
|
||||||
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ dependencies:
|
|||||||
flutter_markdown: ^0.6.1
|
flutter_markdown: ^0.6.1
|
||||||
flutter_portal: ^0.4.0
|
flutter_portal: ^0.4.0
|
||||||
flutter_slidable: ^0.6.0
|
flutter_slidable: ^0.6.0
|
||||||
flutter_svg: ^0.22.0
|
flutter_svg: ^0.23.0+1
|
||||||
http_parser: ^4.0.0
|
http_parser: ^4.0.0
|
||||||
image_gallery_saver: ^1.7.0
|
image_gallery_saver: ^1.7.0
|
||||||
image_picker: ^0.8.2
|
image_picker: ^0.8.2
|
||||||
@@ -32,12 +32,11 @@ dependencies:
|
|||||||
meta: ^1.3.0
|
meta: ^1.3.0
|
||||||
path_provider: ^2.0.1
|
path_provider: ^2.0.1
|
||||||
photo_manager: ^1.2.6+1
|
photo_manager: ^1.2.6+1
|
||||||
photo_view: ^0.12.0
|
photo_view: ^0.13.0
|
||||||
rxdart: ^0.27.0
|
rxdart: ^0.27.0
|
||||||
scrollable_positioned_list: ^0.2.0-nullsafety.0
|
share_plus: ^3.0.4
|
||||||
share_plus: ^2.0.3
|
|
||||||
shimmer: ^2.0.0
|
shimmer: ^2.0.0
|
||||||
stream_chat_flutter_core: ^3.1.1
|
stream_chat_flutter_core: ^3.2.0
|
||||||
substring_highlight: ^1.0.26
|
substring_highlight: ^1.0.26
|
||||||
synchronized: ^3.0.0
|
synchronized: ^3.0.0
|
||||||
url_launcher: ^6.0.3
|
url_launcher: ^6.0.3
|
||||||
@@ -58,6 +57,6 @@ dev_dependencies:
|
|||||||
dart_code_metrics: ^4.4.0
|
dart_code_metrics: ^4.4.0
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
golden_toolkit: ^0.10.0
|
golden_toolkit: ^0.11.0
|
||||||
mocktail: ^0.1.2
|
mocktail: ^0.2.0
|
||||||
path: ^1.8.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
|
## 3.1.1
|
||||||
|
|
||||||
- Updated `stream_chat` dependency to [`3.1.1`](https://pub.dev/packages/stream_chat/changelog).
|
- 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:flutter/widgets.dart';
|
||||||
import 'package:stream_chat/stream_chat.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> {
|
class MessageInputController extends ValueNotifier<Message> {
|
||||||
/// Creates a controller for an editable text field.
|
/// Creates a controller for an editable text field.
|
||||||
///
|
///
|
||||||
@@ -17,7 +17,8 @@ class MessageInputController extends ValueNotifier<Message> {
|
|||||||
factory MessageInputController.fromText(String? text) =>
|
factory MessageInputController.fromText(String? text) =>
|
||||||
MessageInputController._(Message(text: 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(
|
factory MessageInputController.fromAttachments(
|
||||||
List<Attachment> attachments,
|
List<Attachment> attachments,
|
||||||
) =>
|
) =>
|
||||||
@@ -136,8 +137,9 @@ class MessageInputController extends ValueNotifier<Message> {
|
|||||||
/// After calling this function, [text], [attachments] and [mentionedUsers]
|
/// After calling this function, [text], [attachments] and [mentionedUsers]
|
||||||
/// all will be empty.
|
/// all will be empty.
|
||||||
///
|
///
|
||||||
/// Calling this will notify the listeners of this [MessageInputController]
|
/// Calling this will notify all the listeners of this
|
||||||
/// that they need to update (it calls [notifyListeners]). For this reason,
|
/// [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
|
/// this method should only be called between frames, e.g. in response to user
|
||||||
/// actions, not during the build, layout, or paint phases.
|
/// actions, not during the build, layout, or paint phases.
|
||||||
void clear() {
|
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/stream_channel.dart';
|
||||||
import 'package:stream_chat_flutter_core/src/typedef.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
|
/// [MessageListCore] is a simplified class that allows fetching a list of
|
||||||
/// messages while exposing UI builders.
|
/// messages while exposing UI builders.
|
||||||
///
|
///
|
||||||
@@ -132,25 +140,20 @@ class MessageListCoreState extends State<MessageListCore> {
|
|||||||
? _streamChannel!.channel.state?.threads[widget.parentMessage!.id]
|
? _streamChannel!.channel.state?.threads[widget.parentMessage!.id]
|
||||||
: _streamChannel!.channel.state?.messages;
|
: _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>>(
|
return BetterStreamBuilder<List<Message>>(
|
||||||
initialData: initialData,
|
initialData: initialData,
|
||||||
comparator: const ListEquality().equals,
|
comparator: const ListEquality().equals,
|
||||||
stream: messagesStream!.map(
|
stream: messagesStream,
|
||||||
(messages) =>
|
|
||||||
messages?.where(widget.messageFilter ?? defaultFilter).toList(
|
|
||||||
growable: false,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
errorBuilder: widget.errorBuilder,
|
errorBuilder: widget.errorBuilder,
|
||||||
noDataBuilder: widget.loadingBuilder,
|
noDataBuilder: widget.loadingBuilder,
|
||||||
builder: (context, data) {
|
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 (messageList.isEmpty && !_isThreadConversation) {
|
||||||
if (_upToDate) {
|
if (_upToDate) {
|
||||||
return widget.emptyBuilder(context);
|
return widget.emptyBuilder(context);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: stream_chat_flutter_core
|
name: stream_chat_flutter_core
|
||||||
homepage: https://github.com/GetStream/stream-chat-flutter
|
homepage: https://github.com/GetStream/stream-chat-flutter
|
||||||
description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and 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
|
repository: https://github.com/GetStream/stream-chat-flutter
|
||||||
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
||||||
|
|
||||||
@@ -11,17 +11,17 @@ environment:
|
|||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
collection: ^1.15.0
|
collection: ^1.15.0
|
||||||
connectivity_plus: ^1.0.1
|
connectivity_plus: ^2.0.2
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
meta: ^1.3.0
|
meta: ^1.3.0
|
||||||
rxdart: ^0.27.0
|
rxdart: ^0.27.0
|
||||||
stream_chat: ^3.1.1
|
stream_chat: ^3.2.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
dart_code_metrics: ^4.4.0
|
dart_code_metrics: ^4.4.0
|
||||||
fake_async: ^1.2.0
|
fake_async: ^1.2.0
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
mocktail: ^0.1.3
|
mocktail: ^0.2.0
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import 'mocks.dart';
|
|||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
setUpAll(() {
|
setUpAll(() {
|
||||||
registerFallbackValue<PaginationParams>(const PaginationParams());
|
registerFallbackValue(const PaginationParams());
|
||||||
});
|
});
|
||||||
|
|
||||||
List<Channel> _generateChannels(
|
List<Channel> _generateChannels(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
targets:
|
targets:
|
||||||
$default:
|
$default:
|
||||||
builders:
|
builders:
|
||||||
moor_generator:
|
drift_dev:
|
||||||
options:
|
options:
|
||||||
generate_connect_constructor: true
|
generate_connect_constructor: true
|
||||||
data_class_to_companions: false
|
data_class_to_companions: false
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:moor/moor.dart';
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
/// Maps a [List] of type [T] into a [String] understood
|
/// Maps a [List] of type [T] into a [String] understood
|
||||||
/// by the sqlite backend.
|
/// by the sqlite backend.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:moor/moor.dart';
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
/// Maps a [Map] of type [String], [T] into a [String] understood
|
/// Maps a [Map] of type [String], [T] into a [String] understood
|
||||||
/// by the sqlite backend.
|
/// 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';
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
|
|
||||||
/// Maps a [MessageSendingStatus] into a [int] understood
|
/// 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/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/channels.dart';
|
||||||
import 'package:stream_chat_persistence/src/entity/users.dart';
|
import 'package:stream_chat_persistence/src/entity/users.dart';
|
||||||
import 'package:stream_chat_persistence/src/mapper/mapper.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';
|
part 'channel_dao.g.dart';
|
||||||
|
|
||||||
/// The Data Access Object for operations in [Channels] table.
|
/// The Data Access Object for operations in [Channels] table.
|
||||||
@UseDao(tables: [Channels, Users])
|
@DriftAccessor(tables: [Channels, Users])
|
||||||
class ChannelDao extends DatabaseAccessor<MoorChatDatabase>
|
class ChannelDao extends DatabaseAccessor<DriftChatDatabase>
|
||||||
with _$ChannelDaoMixin {
|
with _$ChannelDaoMixin {
|
||||||
/// Creates a new channel dao instance
|
/// Creates a new channel dao instance
|
||||||
ChannelDao(MoorChatDatabase db) : super(db);
|
ChannelDao(DriftChatDatabase db) : super(db);
|
||||||
|
|
||||||
/// Get channel by cid
|
/// Get channel by cid
|
||||||
Future<ChannelModel?> getChannelByCid(String cid) async =>
|
Future<ChannelModel?> getChannelByCid(String cid) async =>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'channel_dao.dart';
|
|||||||
// DaoGenerator
|
// DaoGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
mixin _$ChannelDaoMixin on DatabaseAccessor<MoorChatDatabase> {
|
mixin _$ChannelDaoMixin on DatabaseAccessor<DriftChatDatabase> {
|
||||||
$ChannelsTable get channels => attachedDatabase.channels;
|
$ChannelsTable get channels => attachedDatabase.channels;
|
||||||
$UsersTable get users => attachedDatabase.users;
|
$UsersTable get users => attachedDatabase.users;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:moor/moor.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:stream_chat/stream_chat.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/channel_queries.dart';
|
||||||
import 'package:stream_chat_persistence/src/entity/channels.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/entity/users.dart';
|
||||||
@@ -11,11 +11,11 @@ import 'package:stream_chat_persistence/src/mapper/mapper.dart';
|
|||||||
part 'channel_query_dao.g.dart';
|
part 'channel_query_dao.g.dart';
|
||||||
|
|
||||||
/// The Data Access Object for operations in [ChannelQueries] table.
|
/// The Data Access Object for operations in [ChannelQueries] table.
|
||||||
@UseDao(tables: [ChannelQueries, Channels, Users])
|
@DriftAccessor(tables: [ChannelQueries, Channels, Users])
|
||||||
class ChannelQueryDao extends DatabaseAccessor<MoorChatDatabase>
|
class ChannelQueryDao extends DatabaseAccessor<DriftChatDatabase>
|
||||||
with _$ChannelQueryDaoMixin {
|
with _$ChannelQueryDaoMixin {
|
||||||
/// Creates a new channel query dao instance
|
/// Creates a new channel query dao instance
|
||||||
ChannelQueryDao(MoorChatDatabase db) : super(db);
|
ChannelQueryDao(DriftChatDatabase db) : super(db);
|
||||||
|
|
||||||
String _computeHash(Filter? filter) {
|
String _computeHash(Filter? filter) {
|
||||||
if (filter == null) {
|
if (filter == null) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'channel_query_dao.dart';
|
|||||||
// DaoGenerator
|
// DaoGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
mixin _$ChannelQueryDaoMixin on DatabaseAccessor<MoorChatDatabase> {
|
mixin _$ChannelQueryDaoMixin on DatabaseAccessor<DriftChatDatabase> {
|
||||||
$ChannelQueriesTable get channelQueries => attachedDatabase.channelQueries;
|
$ChannelQueriesTable get channelQueries => attachedDatabase.channelQueries;
|
||||||
$ChannelsTable get channels => attachedDatabase.channels;
|
$ChannelsTable get channels => attachedDatabase.channels;
|
||||||
$UsersTable get users => attachedDatabase.users;
|
$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/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/entity/connection_events.dart';
|
||||||
|
|
||||||
import 'package:stream_chat_persistence/src/mapper/mapper.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';
|
part 'connection_event_dao.g.dart';
|
||||||
|
|
||||||
/// The Data Access Object for operations in [ConnectionEvents] table.
|
/// The Data Access Object for operations in [ConnectionEvents] table.
|
||||||
@UseDao(tables: [ConnectionEvents])
|
@DriftAccessor(tables: [ConnectionEvents])
|
||||||
class ConnectionEventDao extends DatabaseAccessor<MoorChatDatabase>
|
class ConnectionEventDao extends DatabaseAccessor<DriftChatDatabase>
|
||||||
with _$ConnectionEventDaoMixin {
|
with _$ConnectionEventDaoMixin {
|
||||||
/// Creates a new connection event dao instance
|
/// Creates a new connection event dao instance
|
||||||
ConnectionEventDao(MoorChatDatabase db) : super(db);
|
ConnectionEventDao(DriftChatDatabase db) : super(db);
|
||||||
|
|
||||||
/// Get the latest stored connection event
|
/// Get the latest stored connection event
|
||||||
Future<Event?> get connectionEvent => select(connectionEvents)
|
Future<Event?> get connectionEvent => select(connectionEvents)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'connection_event_dao.dart';
|
|||||||
// DaoGenerator
|
// DaoGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
mixin _$ConnectionEventDaoMixin on DatabaseAccessor<MoorChatDatabase> {
|
mixin _$ConnectionEventDaoMixin on DatabaseAccessor<DriftChatDatabase> {
|
||||||
$ConnectionEventsTable get connectionEvents =>
|
$ConnectionEventsTable get connectionEvents =>
|
||||||
attachedDatabase.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/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/members.dart';
|
||||||
import 'package:stream_chat_persistence/src/entity/users.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';
|
part 'member_dao.g.dart';
|
||||||
|
|
||||||
/// The Data Access Object for operations in [Members] table.
|
/// The Data Access Object for operations in [Members] table.
|
||||||
@UseDao(tables: [Members, Users])
|
@DriftAccessor(tables: [Members, Users])
|
||||||
class MemberDao extends DatabaseAccessor<MoorChatDatabase>
|
class MemberDao extends DatabaseAccessor<DriftChatDatabase>
|
||||||
with _$MemberDaoMixin {
|
with _$MemberDaoMixin {
|
||||||
/// Creates a new member dao instance
|
/// Creates a new member dao instance
|
||||||
MemberDao(MoorChatDatabase db) : super(db);
|
MemberDao(DriftChatDatabase db) : super(db);
|
||||||
|
|
||||||
/// Get all members where [Members.channelCid] matches [cid]
|
/// Get all members where [Members.channelCid] matches [cid]
|
||||||
Future<List<Member>> getMembersByCid(String cid) async =>
|
Future<List<Member>> getMembersByCid(String cid) async =>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'member_dao.dart';
|
|||||||
// DaoGenerator
|
// DaoGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
mixin _$MemberDaoMixin on DatabaseAccessor<MoorChatDatabase> {
|
mixin _$MemberDaoMixin on DatabaseAccessor<DriftChatDatabase> {
|
||||||
$MembersTable get members => attachedDatabase.members;
|
$MembersTable get members => attachedDatabase.members;
|
||||||
$UsersTable get users => attachedDatabase.users;
|
$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/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/messages.dart';
|
||||||
import 'package:stream_chat_persistence/src/entity/users.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';
|
part 'message_dao.g.dart';
|
||||||
|
|
||||||
/// The Data Access Object for operations in [Messages] table.
|
/// The Data Access Object for operations in [Messages] table.
|
||||||
@UseDao(tables: [Messages, Users])
|
@DriftAccessor(tables: [Messages, Users])
|
||||||
class MessageDao extends DatabaseAccessor<MoorChatDatabase>
|
class MessageDao extends DatabaseAccessor<DriftChatDatabase>
|
||||||
with _$MessageDaoMixin {
|
with _$MessageDaoMixin {
|
||||||
/// Creates a new message dao instance
|
/// Creates a new message dao instance
|
||||||
MessageDao(this._db) : super(_db);
|
MessageDao(this._db) : super(_db);
|
||||||
|
|
||||||
final MoorChatDatabase _db;
|
final DriftChatDatabase _db;
|
||||||
|
|
||||||
$UsersTable get _users => alias(users, 'users');
|
$UsersTable get _users => alias(users, 'users');
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'message_dao.dart';
|
|||||||
// DaoGenerator
|
// DaoGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
mixin _$MessageDaoMixin on DatabaseAccessor<MoorChatDatabase> {
|
mixin _$MessageDaoMixin on DatabaseAccessor<DriftChatDatabase> {
|
||||||
$MessagesTable get messages => attachedDatabase.messages;
|
$MessagesTable get messages => attachedDatabase.messages;
|
||||||
$UsersTable get users => attachedDatabase.users;
|
$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/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/pinned_messages.dart';
|
||||||
import 'package:stream_chat_persistence/src/entity/users.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';
|
part 'pinned_message_dao.g.dart';
|
||||||
|
|
||||||
/// The Data Access Object for operations in [Messages] table.
|
/// The Data Access Object for operations in [Messages] table.
|
||||||
@UseDao(tables: [PinnedMessages, Users])
|
@DriftAccessor(tables: [PinnedMessages, Users])
|
||||||
class PinnedMessageDao extends DatabaseAccessor<MoorChatDatabase>
|
class PinnedMessageDao extends DatabaseAccessor<DriftChatDatabase>
|
||||||
with _$PinnedMessageDaoMixin {
|
with _$PinnedMessageDaoMixin {
|
||||||
/// Creates a new message dao instance
|
/// Creates a new message dao instance
|
||||||
PinnedMessageDao(this._db) : super(_db);
|
PinnedMessageDao(this._db) : super(_db);
|
||||||
|
|
||||||
final MoorChatDatabase _db;
|
final DriftChatDatabase _db;
|
||||||
|
|
||||||
$UsersTable get _users => alias(users, 'users');
|
$UsersTable get _users => alias(users, 'users');
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'pinned_message_dao.dart';
|
|||||||
// DaoGenerator
|
// DaoGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
mixin _$PinnedMessageDaoMixin on DatabaseAccessor<MoorChatDatabase> {
|
mixin _$PinnedMessageDaoMixin on DatabaseAccessor<DriftChatDatabase> {
|
||||||
$PinnedMessagesTable get pinnedMessages => attachedDatabase.pinnedMessages;
|
$PinnedMessagesTable get pinnedMessages => attachedDatabase.pinnedMessages;
|
||||||
$UsersTable get users => attachedDatabase.users;
|
$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/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/pinned_message_reactions.dart';
|
||||||
import 'package:stream_chat_persistence/src/entity/users.dart';
|
import 'package:stream_chat_persistence/src/entity/users.dart';
|
||||||
import 'package:stream_chat_persistence/src/mapper/mapper.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';
|
part 'pinned_message_reaction_dao.g.dart';
|
||||||
|
|
||||||
/// The Data Access Object for operations in [PinnedMessageReactions] table.
|
/// The Data Access Object for operations in [PinnedMessageReactions] table.
|
||||||
@UseDao(tables: [PinnedMessageReactions, Users])
|
@DriftAccessor(tables: [PinnedMessageReactions, Users])
|
||||||
class PinnedMessageReactionDao extends DatabaseAccessor<MoorChatDatabase>
|
class PinnedMessageReactionDao extends DatabaseAccessor<DriftChatDatabase>
|
||||||
with _$PinnedMessageReactionDaoMixin {
|
with _$PinnedMessageReactionDaoMixin {
|
||||||
/// Creates a new reaction dao instance
|
/// 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
|
/// Returns all the reactions of a particular message by matching
|
||||||
/// [Reactions.messageId] with [messageId]
|
/// [Reactions.messageId] with [messageId]
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'pinned_message_reaction_dao.dart';
|
|||||||
// DaoGenerator
|
// DaoGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
mixin _$PinnedMessageReactionDaoMixin on DatabaseAccessor<MoorChatDatabase> {
|
mixin _$PinnedMessageReactionDaoMixin on DatabaseAccessor<DriftChatDatabase> {
|
||||||
$PinnedMessageReactionsTable get pinnedMessageReactions =>
|
$PinnedMessageReactionsTable get pinnedMessageReactions =>
|
||||||
attachedDatabase.pinnedMessageReactions;
|
attachedDatabase.pinnedMessageReactions;
|
||||||
$UsersTable get users => attachedDatabase.users;
|
$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/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/reactions.dart';
|
||||||
import 'package:stream_chat_persistence/src/entity/users.dart';
|
import 'package:stream_chat_persistence/src/entity/users.dart';
|
||||||
import 'package:stream_chat_persistence/src/mapper/mapper.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';
|
part 'reaction_dao.g.dart';
|
||||||
|
|
||||||
/// The Data Access Object for operations in [Reactions] table.
|
/// The Data Access Object for operations in [Reactions] table.
|
||||||
@UseDao(tables: [Reactions, Users])
|
@DriftAccessor(tables: [Reactions, Users])
|
||||||
class ReactionDao extends DatabaseAccessor<MoorChatDatabase>
|
class ReactionDao extends DatabaseAccessor<DriftChatDatabase>
|
||||||
with _$ReactionDaoMixin {
|
with _$ReactionDaoMixin {
|
||||||
/// Creates a new reaction dao instance
|
/// 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
|
/// Returns all the reactions of a particular message by matching
|
||||||
/// [Reactions.messageId] with [messageId]
|
/// [Reactions.messageId] with [messageId]
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'reaction_dao.dart';
|
|||||||
// DaoGenerator
|
// DaoGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
mixin _$ReactionDaoMixin on DatabaseAccessor<MoorChatDatabase> {
|
mixin _$ReactionDaoMixin on DatabaseAccessor<DriftChatDatabase> {
|
||||||
$ReactionsTable get reactions => attachedDatabase.reactions;
|
$ReactionsTable get reactions => attachedDatabase.reactions;
|
||||||
$UsersTable get users => attachedDatabase.users;
|
$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/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/reads.dart';
|
||||||
import 'package:stream_chat_persistence/src/entity/users.dart';
|
import 'package:stream_chat_persistence/src/entity/users.dart';
|
||||||
import 'package:stream_chat_persistence/src/mapper/mapper.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';
|
part 'read_dao.g.dart';
|
||||||
|
|
||||||
/// The Data Access Object for operations in [Reads] table.
|
/// The Data Access Object for operations in [Reads] table.
|
||||||
@UseDao(tables: [Reads, Users])
|
@DriftAccessor(tables: [Reads, Users])
|
||||||
class ReadDao extends DatabaseAccessor<MoorChatDatabase> with _$ReadDaoMixin {
|
class ReadDao extends DatabaseAccessor<DriftChatDatabase> with _$ReadDaoMixin {
|
||||||
/// Creates a new read dao instance
|
/// Creates a new read dao instance
|
||||||
ReadDao(MoorChatDatabase db) : super(db);
|
ReadDao(DriftChatDatabase db) : super(db);
|
||||||
|
|
||||||
/// Get all reads where [Reads.channelCid] matches [cid]
|
/// Get all reads where [Reads.channelCid] matches [cid]
|
||||||
Future<List<Read>> getReadsByCid(String cid) async => (select(reads).join([
|
Future<List<Read>> getReadsByCid(String cid) async => (select(reads).join([
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'read_dao.dart';
|
|||||||
// DaoGenerator
|
// DaoGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
mixin _$ReadDaoMixin on DatabaseAccessor<MoorChatDatabase> {
|
mixin _$ReadDaoMixin on DatabaseAccessor<DriftChatDatabase> {
|
||||||
$ReadsTable get reads => attachedDatabase.reads;
|
$ReadsTable get reads => attachedDatabase.reads;
|
||||||
$UsersTable get users => attachedDatabase.users;
|
$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/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/entity/users.dart';
|
||||||
import 'package:stream_chat_persistence/src/mapper/user_mapper.dart';
|
import 'package:stream_chat_persistence/src/mapper/user_mapper.dart';
|
||||||
|
|
||||||
part 'user_dao.g.dart';
|
part 'user_dao.g.dart';
|
||||||
|
|
||||||
/// The Data Access Object for operations in [Users] table.
|
/// The Data Access Object for operations in [Users] table.
|
||||||
@UseDao(tables: [Users])
|
@DriftAccessor(tables: [Users])
|
||||||
class UserDao extends DatabaseAccessor<MoorChatDatabase> with _$UserDaoMixin {
|
class UserDao extends DatabaseAccessor<DriftChatDatabase> with _$UserDaoMixin {
|
||||||
/// Creates a new user dao instance
|
/// 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
|
/// Updates the users data with the new [userList] data
|
||||||
Future<void> updateUsers(List<User> userList) => batch(
|
Future<void> updateUsers(List<User> userList) => batch(
|
||||||
|
|||||||
@@ -6,6 +6,6 @@ part of 'user_dao.dart';
|
|||||||
// DaoGenerator
|
// DaoGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
mixin _$UserDaoMixin on DatabaseAccessor<MoorChatDatabase> {
|
mixin _$UserDaoMixin on DatabaseAccessor<DriftChatDatabase> {
|
||||||
$UsersTable get users => attachedDatabase.users;
|
$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/stream_chat.dart';
|
||||||
|
|
||||||
import 'package:stream_chat_persistence/src/converter/converter.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';
|
export 'shared/shared_db.dart';
|
||||||
|
|
||||||
part 'moor_chat_database.g.dart';
|
part 'drift_chat_database.g.dart';
|
||||||
|
|
||||||
/// A chat database implemented using moor
|
/// A chat database implemented using moor
|
||||||
@UseMoor(
|
@DriftDatabase(
|
||||||
tables: [
|
tables: [
|
||||||
Channels,
|
Channels,
|
||||||
Messages,
|
Messages,
|
||||||
@@ -36,15 +36,15 @@ part 'moor_chat_database.g.dart';
|
|||||||
ConnectionEventDao,
|
ConnectionEventDao,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
class MoorChatDatabase extends _$MoorChatDatabase {
|
class DriftChatDatabase extends _$DriftChatDatabase {
|
||||||
/// Creates a new moor chat database instance
|
/// Creates a new moor chat database instance
|
||||||
MoorChatDatabase(
|
DriftChatDatabase(
|
||||||
this._userId,
|
this._userId,
|
||||||
QueryExecutor executor,
|
QueryExecutor executor,
|
||||||
) : super(executor);
|
) : super(executor);
|
||||||
|
|
||||||
/// Instantiate a new database instance
|
/// Instantiate a new database instance
|
||||||
MoorChatDatabase.connect(
|
DriftChatDatabase.connect(
|
||||||
this._userId,
|
this._userId,
|
||||||
DatabaseConnection connection,
|
DatabaseConnection connection,
|
||||||
) : super.connect(connection);
|
) : super.connect(connection);
|
||||||
+133
-242
@@ -1,6 +1,6 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
part of 'moor_chat_database.dart';
|
part of 'drift_chat_database.dart';
|
||||||
|
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
// MoorGenerator
|
// MoorGenerator
|
||||||
@@ -56,9 +56,7 @@ class ChannelEntity extends DataClass implements Insertable<ChannelEntity> {
|
|||||||
required this.memberCount,
|
required this.memberCount,
|
||||||
this.createdById,
|
this.createdById,
|
||||||
this.extraData});
|
this.extraData});
|
||||||
factory ChannelEntity.fromData(
|
factory ChannelEntity.fromData(Map<String, dynamic> data, {String? prefix}) {
|
||||||
Map<String, dynamic> data, GeneratedDatabase db,
|
|
||||||
{String? prefix}) {
|
|
||||||
final effectivePrefix = prefix ?? '';
|
final effectivePrefix = prefix ?? '';
|
||||||
return ChannelEntity(
|
return ChannelEntity(
|
||||||
id: const StringType()
|
id: const StringType()
|
||||||
@@ -119,7 +117,7 @@ class ChannelEntity extends DataClass implements Insertable<ChannelEntity> {
|
|||||||
|
|
||||||
factory ChannelEntity.fromJson(Map<String, dynamic> json,
|
factory ChannelEntity.fromJson(Map<String, dynamic> json,
|
||||||
{ValueSerializer? serializer}) {
|
{ValueSerializer? serializer}) {
|
||||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
return ChannelEntity(
|
return ChannelEntity(
|
||||||
id: serializer.fromJson<String>(json['id']),
|
id: serializer.fromJson<String>(json['id']),
|
||||||
type: serializer.fromJson<String>(json['type']),
|
type: serializer.fromJson<String>(json['type']),
|
||||||
@@ -137,7 +135,7 @@ class ChannelEntity extends DataClass implements Insertable<ChannelEntity> {
|
|||||||
}
|
}
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
return <String, dynamic>{
|
return <String, dynamic>{
|
||||||
'id': serializer.toJson<String>(id),
|
'id': serializer.toJson<String>(id),
|
||||||
'type': serializer.toJson<String>(type),
|
'type': serializer.toJson<String>(type),
|
||||||
@@ -202,28 +200,8 @@ class ChannelEntity extends DataClass implements Insertable<ChannelEntity> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => $mrjf($mrjc(
|
int get hashCode => Object.hash(id, type, cid, config, frozen, lastMessageAt,
|
||||||
id.hashCode,
|
createdAt, updatedAt, deletedAt, memberCount, createdById, extraData);
|
||||||
$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))))))))))));
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
@@ -561,7 +539,7 @@ class $ChannelsTable extends Channels
|
|||||||
Set<GeneratedColumn> get $primaryKey => {cid};
|
Set<GeneratedColumn> get $primaryKey => {cid};
|
||||||
@override
|
@override
|
||||||
ChannelEntity map(Map<String, dynamic> data, {String? tablePrefix}) {
|
ChannelEntity map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||||
return ChannelEntity.fromData(data, _db,
|
return ChannelEntity.fromData(data,
|
||||||
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -678,9 +656,7 @@ class MessageEntity extends DataClass implements Insertable<MessageEntity> {
|
|||||||
required this.channelCid,
|
required this.channelCid,
|
||||||
this.i18n,
|
this.i18n,
|
||||||
this.extraData});
|
this.extraData});
|
||||||
factory MessageEntity.fromData(
|
factory MessageEntity.fromData(Map<String, dynamic> data, {String? prefix}) {
|
||||||
Map<String, dynamic> data, GeneratedDatabase db,
|
|
||||||
{String? prefix}) {
|
|
||||||
final effectivePrefix = prefix ?? '';
|
final effectivePrefix = prefix ?? '';
|
||||||
return MessageEntity(
|
return MessageEntity(
|
||||||
id: const StringType()
|
id: const StringType()
|
||||||
@@ -814,7 +790,7 @@ class MessageEntity extends DataClass implements Insertable<MessageEntity> {
|
|||||||
|
|
||||||
factory MessageEntity.fromJson(Map<String, dynamic> json,
|
factory MessageEntity.fromJson(Map<String, dynamic> json,
|
||||||
{ValueSerializer? serializer}) {
|
{ValueSerializer? serializer}) {
|
||||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
return MessageEntity(
|
return MessageEntity(
|
||||||
id: serializer.fromJson<String>(json['id']),
|
id: serializer.fromJson<String>(json['id']),
|
||||||
messageText: serializer.fromJson<String?>(json['messageText']),
|
messageText: serializer.fromJson<String?>(json['messageText']),
|
||||||
@@ -847,7 +823,7 @@ class MessageEntity extends DataClass implements Insertable<MessageEntity> {
|
|||||||
}
|
}
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
return <String, dynamic>{
|
return <String, dynamic>{
|
||||||
'id': serializer.toJson<String>(id),
|
'id': serializer.toJson<String>(id),
|
||||||
'messageText': serializer.toJson<String?>(messageText),
|
'messageText': serializer.toJson<String?>(messageText),
|
||||||
@@ -969,49 +945,33 @@ class MessageEntity extends DataClass implements Insertable<MessageEntity> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => $mrjf($mrjc(
|
int get hashCode => Object.hashAll([
|
||||||
id.hashCode,
|
id,
|
||||||
$mrjc(
|
messageText,
|
||||||
messageText.hashCode,
|
attachments,
|
||||||
$mrjc(
|
status,
|
||||||
attachments.hashCode,
|
type,
|
||||||
$mrjc(
|
mentionedUsers,
|
||||||
status.hashCode,
|
reactionCounts,
|
||||||
$mrjc(
|
reactionScores,
|
||||||
type.hashCode,
|
parentId,
|
||||||
$mrjc(
|
quotedMessageId,
|
||||||
mentionedUsers.hashCode,
|
replyCount,
|
||||||
$mrjc(
|
showInChannel,
|
||||||
reactionCounts.hashCode,
|
shadowed,
|
||||||
$mrjc(
|
command,
|
||||||
reactionScores.hashCode,
|
createdAt,
|
||||||
$mrjc(
|
updatedAt,
|
||||||
parentId.hashCode,
|
deletedAt,
|
||||||
$mrjc(
|
userId,
|
||||||
quotedMessageId.hashCode,
|
pinned,
|
||||||
$mrjc(
|
pinnedAt,
|
||||||
replyCount.hashCode,
|
pinExpires,
|
||||||
$mrjc(
|
pinnedByUserId,
|
||||||
showInChannel.hashCode,
|
channelCid,
|
||||||
$mrjc(
|
i18n,
|
||||||
shadowed.hashCode,
|
extraData
|
||||||
$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)))))))))))))))))))))))));
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
@@ -1644,7 +1604,7 @@ class $MessagesTable extends Messages
|
|||||||
Set<GeneratedColumn> get $primaryKey => {id};
|
Set<GeneratedColumn> get $primaryKey => {id};
|
||||||
@override
|
@override
|
||||||
MessageEntity map(Map<String, dynamic> data, {String? tablePrefix}) {
|
MessageEntity map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||||
return MessageEntity.fromData(data, _db,
|
return MessageEntity.fromData(data,
|
||||||
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1772,8 +1732,7 @@ class PinnedMessageEntity extends DataClass
|
|||||||
required this.channelCid,
|
required this.channelCid,
|
||||||
this.i18n,
|
this.i18n,
|
||||||
this.extraData});
|
this.extraData});
|
||||||
factory PinnedMessageEntity.fromData(
|
factory PinnedMessageEntity.fromData(Map<String, dynamic> data,
|
||||||
Map<String, dynamic> data, GeneratedDatabase db,
|
|
||||||
{String? prefix}) {
|
{String? prefix}) {
|
||||||
final effectivePrefix = prefix ?? '';
|
final effectivePrefix = prefix ?? '';
|
||||||
return PinnedMessageEntity(
|
return PinnedMessageEntity(
|
||||||
@@ -1911,7 +1870,7 @@ class PinnedMessageEntity extends DataClass
|
|||||||
|
|
||||||
factory PinnedMessageEntity.fromJson(Map<String, dynamic> json,
|
factory PinnedMessageEntity.fromJson(Map<String, dynamic> json,
|
||||||
{ValueSerializer? serializer}) {
|
{ValueSerializer? serializer}) {
|
||||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
return PinnedMessageEntity(
|
return PinnedMessageEntity(
|
||||||
id: serializer.fromJson<String>(json['id']),
|
id: serializer.fromJson<String>(json['id']),
|
||||||
messageText: serializer.fromJson<String?>(json['messageText']),
|
messageText: serializer.fromJson<String?>(json['messageText']),
|
||||||
@@ -1944,7 +1903,7 @@ class PinnedMessageEntity extends DataClass
|
|||||||
}
|
}
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
return <String, dynamic>{
|
return <String, dynamic>{
|
||||||
'id': serializer.toJson<String>(id),
|
'id': serializer.toJson<String>(id),
|
||||||
'messageText': serializer.toJson<String?>(messageText),
|
'messageText': serializer.toJson<String?>(messageText),
|
||||||
@@ -2066,49 +2025,33 @@ class PinnedMessageEntity extends DataClass
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => $mrjf($mrjc(
|
int get hashCode => Object.hashAll([
|
||||||
id.hashCode,
|
id,
|
||||||
$mrjc(
|
messageText,
|
||||||
messageText.hashCode,
|
attachments,
|
||||||
$mrjc(
|
status,
|
||||||
attachments.hashCode,
|
type,
|
||||||
$mrjc(
|
mentionedUsers,
|
||||||
status.hashCode,
|
reactionCounts,
|
||||||
$mrjc(
|
reactionScores,
|
||||||
type.hashCode,
|
parentId,
|
||||||
$mrjc(
|
quotedMessageId,
|
||||||
mentionedUsers.hashCode,
|
replyCount,
|
||||||
$mrjc(
|
showInChannel,
|
||||||
reactionCounts.hashCode,
|
shadowed,
|
||||||
$mrjc(
|
command,
|
||||||
reactionScores.hashCode,
|
createdAt,
|
||||||
$mrjc(
|
updatedAt,
|
||||||
parentId.hashCode,
|
deletedAt,
|
||||||
$mrjc(
|
userId,
|
||||||
quotedMessageId.hashCode,
|
pinned,
|
||||||
$mrjc(
|
pinnedAt,
|
||||||
replyCount.hashCode,
|
pinExpires,
|
||||||
$mrjc(
|
pinnedByUserId,
|
||||||
showInChannel.hashCode,
|
channelCid,
|
||||||
$mrjc(
|
i18n,
|
||||||
shadowed.hashCode,
|
extraData
|
||||||
$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)))))))))))))))))))))))));
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
@@ -2744,7 +2687,7 @@ class $PinnedMessagesTable extends PinnedMessages
|
|||||||
Set<GeneratedColumn> get $primaryKey => {id};
|
Set<GeneratedColumn> get $primaryKey => {id};
|
||||||
@override
|
@override
|
||||||
PinnedMessageEntity map(Map<String, dynamic> data, {String? tablePrefix}) {
|
PinnedMessageEntity map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||||
return PinnedMessageEntity.fromData(data, _db,
|
return PinnedMessageEntity.fromData(data,
|
||||||
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2795,8 +2738,7 @@ class PinnedMessageReactionEntity extends DataClass
|
|||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
required this.score,
|
required this.score,
|
||||||
this.extraData});
|
this.extraData});
|
||||||
factory PinnedMessageReactionEntity.fromData(
|
factory PinnedMessageReactionEntity.fromData(Map<String, dynamic> data,
|
||||||
Map<String, dynamic> data, GeneratedDatabase db,
|
|
||||||
{String? prefix}) {
|
{String? prefix}) {
|
||||||
final effectivePrefix = prefix ?? '';
|
final effectivePrefix = prefix ?? '';
|
||||||
return PinnedMessageReactionEntity(
|
return PinnedMessageReactionEntity(
|
||||||
@@ -2832,7 +2774,7 @@ class PinnedMessageReactionEntity extends DataClass
|
|||||||
|
|
||||||
factory PinnedMessageReactionEntity.fromJson(Map<String, dynamic> json,
|
factory PinnedMessageReactionEntity.fromJson(Map<String, dynamic> json,
|
||||||
{ValueSerializer? serializer}) {
|
{ValueSerializer? serializer}) {
|
||||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
return PinnedMessageReactionEntity(
|
return PinnedMessageReactionEntity(
|
||||||
userId: serializer.fromJson<String>(json['userId']),
|
userId: serializer.fromJson<String>(json['userId']),
|
||||||
messageId: serializer.fromJson<String>(json['messageId']),
|
messageId: serializer.fromJson<String>(json['messageId']),
|
||||||
@@ -2844,7 +2786,7 @@ class PinnedMessageReactionEntity extends DataClass
|
|||||||
}
|
}
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
return <String, dynamic>{
|
return <String, dynamic>{
|
||||||
'userId': serializer.toJson<String>(userId),
|
'userId': serializer.toJson<String>(userId),
|
||||||
'messageId': serializer.toJson<String>(messageId),
|
'messageId': serializer.toJson<String>(messageId),
|
||||||
@@ -2884,14 +2826,8 @@ class PinnedMessageReactionEntity extends DataClass
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => $mrjf($mrjc(
|
int get hashCode =>
|
||||||
userId.hashCode,
|
Object.hash(userId, messageId, type, createdAt, score, extraData);
|
||||||
$mrjc(
|
|
||||||
messageId.hashCode,
|
|
||||||
$mrjc(
|
|
||||||
type.hashCode,
|
|
||||||
$mrjc(createdAt.hashCode,
|
|
||||||
$mrjc(score.hashCode, extraData.hashCode))))));
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
@@ -3090,7 +3026,7 @@ class $PinnedMessageReactionsTable extends PinnedMessageReactions
|
|||||||
@override
|
@override
|
||||||
PinnedMessageReactionEntity map(Map<String, dynamic> data,
|
PinnedMessageReactionEntity map(Map<String, dynamic> data,
|
||||||
{String? tablePrefix}) {
|
{String? tablePrefix}) {
|
||||||
return PinnedMessageReactionEntity.fromData(data, _db,
|
return PinnedMessageReactionEntity.fromData(data,
|
||||||
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3128,9 +3064,7 @@ class ReactionEntity extends DataClass implements Insertable<ReactionEntity> {
|
|||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
required this.score,
|
required this.score,
|
||||||
this.extraData});
|
this.extraData});
|
||||||
factory ReactionEntity.fromData(
|
factory ReactionEntity.fromData(Map<String, dynamic> data, {String? prefix}) {
|
||||||
Map<String, dynamic> data, GeneratedDatabase db,
|
|
||||||
{String? prefix}) {
|
|
||||||
final effectivePrefix = prefix ?? '';
|
final effectivePrefix = prefix ?? '';
|
||||||
return ReactionEntity(
|
return ReactionEntity(
|
||||||
userId: const StringType()
|
userId: const StringType()
|
||||||
@@ -3164,7 +3098,7 @@ class ReactionEntity extends DataClass implements Insertable<ReactionEntity> {
|
|||||||
|
|
||||||
factory ReactionEntity.fromJson(Map<String, dynamic> json,
|
factory ReactionEntity.fromJson(Map<String, dynamic> json,
|
||||||
{ValueSerializer? serializer}) {
|
{ValueSerializer? serializer}) {
|
||||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
return ReactionEntity(
|
return ReactionEntity(
|
||||||
userId: serializer.fromJson<String>(json['userId']),
|
userId: serializer.fromJson<String>(json['userId']),
|
||||||
messageId: serializer.fromJson<String>(json['messageId']),
|
messageId: serializer.fromJson<String>(json['messageId']),
|
||||||
@@ -3176,7 +3110,7 @@ class ReactionEntity extends DataClass implements Insertable<ReactionEntity> {
|
|||||||
}
|
}
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
return <String, dynamic>{
|
return <String, dynamic>{
|
||||||
'userId': serializer.toJson<String>(userId),
|
'userId': serializer.toJson<String>(userId),
|
||||||
'messageId': serializer.toJson<String>(messageId),
|
'messageId': serializer.toJson<String>(messageId),
|
||||||
@@ -3216,14 +3150,8 @@ class ReactionEntity extends DataClass implements Insertable<ReactionEntity> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => $mrjf($mrjc(
|
int get hashCode =>
|
||||||
userId.hashCode,
|
Object.hash(userId, messageId, type, createdAt, score, extraData);
|
||||||
$mrjc(
|
|
||||||
messageId.hashCode,
|
|
||||||
$mrjc(
|
|
||||||
type.hashCode,
|
|
||||||
$mrjc(createdAt.hashCode,
|
|
||||||
$mrjc(score.hashCode, extraData.hashCode))))));
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
@@ -3418,7 +3346,7 @@ class $ReactionsTable extends Reactions
|
|||||||
Set<GeneratedColumn> get $primaryKey => {messageId, type, userId};
|
Set<GeneratedColumn> get $primaryKey => {messageId, type, userId};
|
||||||
@override
|
@override
|
||||||
ReactionEntity map(Map<String, dynamic> data, {String? tablePrefix}) {
|
ReactionEntity map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||||
return ReactionEntity.fromData(data, _db,
|
return ReactionEntity.fromData(data,
|
||||||
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3468,8 +3396,7 @@ class UserEntity extends DataClass implements Insertable<UserEntity> {
|
|||||||
required this.online,
|
required this.online,
|
||||||
required this.banned,
|
required this.banned,
|
||||||
required this.extraData});
|
required this.extraData});
|
||||||
factory UserEntity.fromData(Map<String, dynamic> data, GeneratedDatabase db,
|
factory UserEntity.fromData(Map<String, dynamic> data, {String? prefix}) {
|
||||||
{String? prefix}) {
|
|
||||||
final effectivePrefix = prefix ?? '';
|
final effectivePrefix = prefix ?? '';
|
||||||
return UserEntity(
|
return UserEntity(
|
||||||
id: const StringType()
|
id: const StringType()
|
||||||
@@ -3518,7 +3445,7 @@ class UserEntity extends DataClass implements Insertable<UserEntity> {
|
|||||||
|
|
||||||
factory UserEntity.fromJson(Map<String, dynamic> json,
|
factory UserEntity.fromJson(Map<String, dynamic> json,
|
||||||
{ValueSerializer? serializer}) {
|
{ValueSerializer? serializer}) {
|
||||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
return UserEntity(
|
return UserEntity(
|
||||||
id: serializer.fromJson<String>(json['id']),
|
id: serializer.fromJson<String>(json['id']),
|
||||||
role: serializer.fromJson<String?>(json['role']),
|
role: serializer.fromJson<String?>(json['role']),
|
||||||
@@ -3533,7 +3460,7 @@ class UserEntity extends DataClass implements Insertable<UserEntity> {
|
|||||||
}
|
}
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
return <String, dynamic>{
|
return <String, dynamic>{
|
||||||
'id': serializer.toJson<String>(id),
|
'id': serializer.toJson<String>(id),
|
||||||
'role': serializer.toJson<String?>(role),
|
'role': serializer.toJson<String?>(role),
|
||||||
@@ -3585,22 +3512,8 @@ class UserEntity extends DataClass implements Insertable<UserEntity> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => $mrjf($mrjc(
|
int get hashCode => Object.hash(id, role, language, createdAt, updatedAt,
|
||||||
id.hashCode,
|
lastActive, online, banned, extraData);
|
||||||
$mrjc(
|
|
||||||
role.hashCode,
|
|
||||||
$mrjc(
|
|
||||||
language.hashCode,
|
|
||||||
$mrjc(
|
|
||||||
createdAt.hashCode,
|
|
||||||
$mrjc(
|
|
||||||
updatedAt.hashCode,
|
|
||||||
$mrjc(
|
|
||||||
lastActive.hashCode,
|
|
||||||
$mrjc(
|
|
||||||
online.hashCode,
|
|
||||||
$mrjc(
|
|
||||||
banned.hashCode, extraData.hashCode)))))))));
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
@@ -3863,7 +3776,7 @@ class $UsersTable extends Users with TableInfo<$UsersTable, UserEntity> {
|
|||||||
Set<GeneratedColumn> get $primaryKey => {id};
|
Set<GeneratedColumn> get $primaryKey => {id};
|
||||||
@override
|
@override
|
||||||
UserEntity map(Map<String, dynamic> data, {String? tablePrefix}) {
|
UserEntity map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||||
return UserEntity.fromData(data, _db,
|
return UserEntity.fromData(data,
|
||||||
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3921,8 +3834,7 @@ class MemberEntity extends DataClass implements Insertable<MemberEntity> {
|
|||||||
required this.isModerator,
|
required this.isModerator,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
required this.updatedAt});
|
required this.updatedAt});
|
||||||
factory MemberEntity.fromData(Map<String, dynamic> data, GeneratedDatabase db,
|
factory MemberEntity.fromData(Map<String, dynamic> data, {String? prefix}) {
|
||||||
{String? prefix}) {
|
|
||||||
final effectivePrefix = prefix ?? '';
|
final effectivePrefix = prefix ?? '';
|
||||||
return MemberEntity(
|
return MemberEntity(
|
||||||
userId: const StringType()
|
userId: const StringType()
|
||||||
@@ -3974,7 +3886,7 @@ class MemberEntity extends DataClass implements Insertable<MemberEntity> {
|
|||||||
|
|
||||||
factory MemberEntity.fromJson(Map<String, dynamic> json,
|
factory MemberEntity.fromJson(Map<String, dynamic> json,
|
||||||
{ValueSerializer? serializer}) {
|
{ValueSerializer? serializer}) {
|
||||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
return MemberEntity(
|
return MemberEntity(
|
||||||
userId: serializer.fromJson<String>(json['userId']),
|
userId: serializer.fromJson<String>(json['userId']),
|
||||||
channelCid: serializer.fromJson<String>(json['channelCid']),
|
channelCid: serializer.fromJson<String>(json['channelCid']),
|
||||||
@@ -3993,7 +3905,7 @@ class MemberEntity extends DataClass implements Insertable<MemberEntity> {
|
|||||||
}
|
}
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
return <String, dynamic>{
|
return <String, dynamic>{
|
||||||
'userId': serializer.toJson<String>(userId),
|
'userId': serializer.toJson<String>(userId),
|
||||||
'channelCid': serializer.toJson<String>(channelCid),
|
'channelCid': serializer.toJson<String>(channelCid),
|
||||||
@@ -4057,26 +3969,18 @@ class MemberEntity extends DataClass implements Insertable<MemberEntity> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => $mrjf($mrjc(
|
int get hashCode => Object.hash(
|
||||||
userId.hashCode,
|
userId,
|
||||||
$mrjc(
|
channelCid,
|
||||||
channelCid.hashCode,
|
role,
|
||||||
$mrjc(
|
inviteAcceptedAt,
|
||||||
role.hashCode,
|
inviteRejectedAt,
|
||||||
$mrjc(
|
invited,
|
||||||
inviteAcceptedAt.hashCode,
|
banned,
|
||||||
$mrjc(
|
shadowBanned,
|
||||||
inviteRejectedAt.hashCode,
|
isModerator,
|
||||||
$mrjc(
|
createdAt,
|
||||||
invited.hashCode,
|
updatedAt);
|
||||||
$mrjc(
|
|
||||||
banned.hashCode,
|
|
||||||
$mrjc(
|
|
||||||
shadowBanned.hashCode,
|
|
||||||
$mrjc(
|
|
||||||
isModerator.hashCode,
|
|
||||||
$mrjc(createdAt.hashCode,
|
|
||||||
updatedAt.hashCode)))))))))));
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
@@ -4405,7 +4309,7 @@ class $MembersTable extends Members
|
|||||||
Set<GeneratedColumn> get $primaryKey => {userId, channelCid};
|
Set<GeneratedColumn> get $primaryKey => {userId, channelCid};
|
||||||
@override
|
@override
|
||||||
MemberEntity map(Map<String, dynamic> data, {String? tablePrefix}) {
|
MemberEntity map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||||
return MemberEntity.fromData(data, _db,
|
return MemberEntity.fromData(data,
|
||||||
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4432,8 +4336,7 @@ class ReadEntity extends DataClass implements Insertable<ReadEntity> {
|
|||||||
required this.userId,
|
required this.userId,
|
||||||
required this.channelCid,
|
required this.channelCid,
|
||||||
required this.unreadMessages});
|
required this.unreadMessages});
|
||||||
factory ReadEntity.fromData(Map<String, dynamic> data, GeneratedDatabase db,
|
factory ReadEntity.fromData(Map<String, dynamic> data, {String? prefix}) {
|
||||||
{String? prefix}) {
|
|
||||||
final effectivePrefix = prefix ?? '';
|
final effectivePrefix = prefix ?? '';
|
||||||
return ReadEntity(
|
return ReadEntity(
|
||||||
lastRead: const DateTimeType()
|
lastRead: const DateTimeType()
|
||||||
@@ -4458,7 +4361,7 @@ class ReadEntity extends DataClass implements Insertable<ReadEntity> {
|
|||||||
|
|
||||||
factory ReadEntity.fromJson(Map<String, dynamic> json,
|
factory ReadEntity.fromJson(Map<String, dynamic> json,
|
||||||
{ValueSerializer? serializer}) {
|
{ValueSerializer? serializer}) {
|
||||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
return ReadEntity(
|
return ReadEntity(
|
||||||
lastRead: serializer.fromJson<DateTime>(json['lastRead']),
|
lastRead: serializer.fromJson<DateTime>(json['lastRead']),
|
||||||
userId: serializer.fromJson<String>(json['userId']),
|
userId: serializer.fromJson<String>(json['userId']),
|
||||||
@@ -4468,7 +4371,7 @@ class ReadEntity extends DataClass implements Insertable<ReadEntity> {
|
|||||||
}
|
}
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
return <String, dynamic>{
|
return <String, dynamic>{
|
||||||
'lastRead': serializer.toJson<DateTime>(lastRead),
|
'lastRead': serializer.toJson<DateTime>(lastRead),
|
||||||
'userId': serializer.toJson<String>(userId),
|
'userId': serializer.toJson<String>(userId),
|
||||||
@@ -4500,10 +4403,7 @@ class ReadEntity extends DataClass implements Insertable<ReadEntity> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => $mrjf($mrjc(
|
int get hashCode => Object.hash(lastRead, userId, channelCid, unreadMessages);
|
||||||
lastRead.hashCode,
|
|
||||||
$mrjc(userId.hashCode,
|
|
||||||
$mrjc(channelCid.hashCode, unreadMessages.hashCode))));
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
@@ -4660,7 +4560,7 @@ class $ReadsTable extends Reads with TableInfo<$ReadsTable, ReadEntity> {
|
|||||||
Set<GeneratedColumn> get $primaryKey => {userId, channelCid};
|
Set<GeneratedColumn> get $primaryKey => {userId, channelCid};
|
||||||
@override
|
@override
|
||||||
ReadEntity map(Map<String, dynamic> data, {String? tablePrefix}) {
|
ReadEntity map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||||
return ReadEntity.fromData(data, _db,
|
return ReadEntity.fromData(data,
|
||||||
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4678,8 +4578,7 @@ class ChannelQueryEntity extends DataClass
|
|||||||
/// The channel cid of this query
|
/// The channel cid of this query
|
||||||
final String channelCid;
|
final String channelCid;
|
||||||
ChannelQueryEntity({required this.queryHash, required this.channelCid});
|
ChannelQueryEntity({required this.queryHash, required this.channelCid});
|
||||||
factory ChannelQueryEntity.fromData(
|
factory ChannelQueryEntity.fromData(Map<String, dynamic> data,
|
||||||
Map<String, dynamic> data, GeneratedDatabase db,
|
|
||||||
{String? prefix}) {
|
{String? prefix}) {
|
||||||
final effectivePrefix = prefix ?? '';
|
final effectivePrefix = prefix ?? '';
|
||||||
return ChannelQueryEntity(
|
return ChannelQueryEntity(
|
||||||
@@ -4699,7 +4598,7 @@ class ChannelQueryEntity extends DataClass
|
|||||||
|
|
||||||
factory ChannelQueryEntity.fromJson(Map<String, dynamic> json,
|
factory ChannelQueryEntity.fromJson(Map<String, dynamic> json,
|
||||||
{ValueSerializer? serializer}) {
|
{ValueSerializer? serializer}) {
|
||||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
return ChannelQueryEntity(
|
return ChannelQueryEntity(
|
||||||
queryHash: serializer.fromJson<String>(json['queryHash']),
|
queryHash: serializer.fromJson<String>(json['queryHash']),
|
||||||
channelCid: serializer.fromJson<String>(json['channelCid']),
|
channelCid: serializer.fromJson<String>(json['channelCid']),
|
||||||
@@ -4707,7 +4606,7 @@ class ChannelQueryEntity extends DataClass
|
|||||||
}
|
}
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
return <String, dynamic>{
|
return <String, dynamic>{
|
||||||
'queryHash': serializer.toJson<String>(queryHash),
|
'queryHash': serializer.toJson<String>(queryHash),
|
||||||
'channelCid': serializer.toJson<String>(channelCid),
|
'channelCid': serializer.toJson<String>(channelCid),
|
||||||
@@ -4729,7 +4628,7 @@ class ChannelQueryEntity extends DataClass
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => $mrjf($mrjc(queryHash.hashCode, channelCid.hashCode));
|
int get hashCode => Object.hash(queryHash, channelCid);
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
@@ -4835,7 +4734,7 @@ class $ChannelQueriesTable extends ChannelQueries
|
|||||||
Set<GeneratedColumn> get $primaryKey => {queryHash, channelCid};
|
Set<GeneratedColumn> get $primaryKey => {queryHash, channelCid};
|
||||||
@override
|
@override
|
||||||
ChannelQueryEntity map(Map<String, dynamic> data, {String? tablePrefix}) {
|
ChannelQueryEntity map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||||
return ChannelQueryEntity.fromData(data, _db,
|
return ChannelQueryEntity.fromData(data,
|
||||||
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4875,8 +4774,7 @@ class ConnectionEventEntity extends DataClass
|
|||||||
this.unreadChannels,
|
this.unreadChannels,
|
||||||
this.lastEventAt,
|
this.lastEventAt,
|
||||||
this.lastSyncAt});
|
this.lastSyncAt});
|
||||||
factory ConnectionEventEntity.fromData(
|
factory ConnectionEventEntity.fromData(Map<String, dynamic> data,
|
||||||
Map<String, dynamic> data, GeneratedDatabase db,
|
|
||||||
{String? prefix}) {
|
{String? prefix}) {
|
||||||
final effectivePrefix = prefix ?? '';
|
final effectivePrefix = prefix ?? '';
|
||||||
return ConnectionEventEntity(
|
return ConnectionEventEntity(
|
||||||
@@ -4922,7 +4820,7 @@ class ConnectionEventEntity extends DataClass
|
|||||||
|
|
||||||
factory ConnectionEventEntity.fromJson(Map<String, dynamic> json,
|
factory ConnectionEventEntity.fromJson(Map<String, dynamic> json,
|
||||||
{ValueSerializer? serializer}) {
|
{ValueSerializer? serializer}) {
|
||||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
return ConnectionEventEntity(
|
return ConnectionEventEntity(
|
||||||
id: serializer.fromJson<int>(json['id']),
|
id: serializer.fromJson<int>(json['id']),
|
||||||
type: serializer.fromJson<String>(json['type']),
|
type: serializer.fromJson<String>(json['type']),
|
||||||
@@ -4935,7 +4833,7 @@ class ConnectionEventEntity extends DataClass
|
|||||||
}
|
}
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
return <String, dynamic>{
|
return <String, dynamic>{
|
||||||
'id': serializer.toJson<int>(id),
|
'id': serializer.toJson<int>(id),
|
||||||
'type': serializer.toJson<String>(type),
|
'type': serializer.toJson<String>(type),
|
||||||
@@ -4982,16 +4880,8 @@ class ConnectionEventEntity extends DataClass
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => $mrjf($mrjc(
|
int get hashCode => Object.hash(id, type, ownUser, totalUnreadCount,
|
||||||
id.hashCode,
|
unreadChannels, lastEventAt, lastSyncAt);
|
||||||
$mrjc(
|
|
||||||
type.hashCode,
|
|
||||||
$mrjc(
|
|
||||||
ownUser.hashCode,
|
|
||||||
$mrjc(
|
|
||||||
totalUnreadCount.hashCode,
|
|
||||||
$mrjc(unreadChannels.hashCode,
|
|
||||||
$mrjc(lastEventAt.hashCode, lastSyncAt.hashCode)))))));
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
@@ -5212,7 +5102,7 @@ class $ConnectionEventsTable extends ConnectionEvents
|
|||||||
Set<GeneratedColumn> get $primaryKey => {id};
|
Set<GeneratedColumn> get $primaryKey => {id};
|
||||||
@override
|
@override
|
||||||
ConnectionEventEntity map(Map<String, dynamic> data, {String? tablePrefix}) {
|
ConnectionEventEntity map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||||
return ConnectionEventEntity.fromData(data, _db,
|
return ConnectionEventEntity.fromData(data,
|
||||||
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5225,9 +5115,10 @@ class $ConnectionEventsTable extends ConnectionEvents
|
|||||||
MapConverter();
|
MapConverter();
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class _$MoorChatDatabase extends GeneratedDatabase {
|
abstract class _$DriftChatDatabase extends GeneratedDatabase {
|
||||||
_$MoorChatDatabase(QueryExecutor e) : super(SqlTypeSystem.defaultInstance, e);
|
_$DriftChatDatabase(QueryExecutor e)
|
||||||
_$MoorChatDatabase.connect(DatabaseConnection c) : super.connect(c);
|
: super(SqlTypeSystem.defaultInstance, e);
|
||||||
|
_$DriftChatDatabase.connect(DatabaseConnection c) : super.connect(c);
|
||||||
late final $ChannelsTable channels = $ChannelsTable(this);
|
late final $ChannelsTable channels = $ChannelsTable(this);
|
||||||
late final $MessagesTable messages = $MessagesTable(this);
|
late final $MessagesTable messages = $MessagesTable(this);
|
||||||
late final $PinnedMessagesTable pinnedMessages = $PinnedMessagesTable(this);
|
late final $PinnedMessagesTable pinnedMessages = $PinnedMessagesTable(this);
|
||||||
@@ -5240,20 +5131,20 @@ abstract class _$MoorChatDatabase extends GeneratedDatabase {
|
|||||||
late final $ChannelQueriesTable channelQueries = $ChannelQueriesTable(this);
|
late final $ChannelQueriesTable channelQueries = $ChannelQueriesTable(this);
|
||||||
late final $ConnectionEventsTable connectionEvents =
|
late final $ConnectionEventsTable connectionEvents =
|
||||||
$ConnectionEventsTable(this);
|
$ConnectionEventsTable(this);
|
||||||
late final UserDao userDao = UserDao(this as MoorChatDatabase);
|
late final UserDao userDao = UserDao(this as DriftChatDatabase);
|
||||||
late final ChannelDao channelDao = ChannelDao(this as MoorChatDatabase);
|
late final ChannelDao channelDao = ChannelDao(this as DriftChatDatabase);
|
||||||
late final MessageDao messageDao = MessageDao(this as MoorChatDatabase);
|
late final MessageDao messageDao = MessageDao(this as DriftChatDatabase);
|
||||||
late final PinnedMessageDao pinnedMessageDao =
|
late final PinnedMessageDao pinnedMessageDao =
|
||||||
PinnedMessageDao(this as MoorChatDatabase);
|
PinnedMessageDao(this as DriftChatDatabase);
|
||||||
late final PinnedMessageReactionDao pinnedMessageReactionDao =
|
late final PinnedMessageReactionDao pinnedMessageReactionDao =
|
||||||
PinnedMessageReactionDao(this as MoorChatDatabase);
|
PinnedMessageReactionDao(this as DriftChatDatabase);
|
||||||
late final MemberDao memberDao = MemberDao(this as MoorChatDatabase);
|
late final MemberDao memberDao = MemberDao(this as DriftChatDatabase);
|
||||||
late final ReactionDao reactionDao = ReactionDao(this as MoorChatDatabase);
|
late final ReactionDao reactionDao = ReactionDao(this as DriftChatDatabase);
|
||||||
late final ReadDao readDao = ReadDao(this as MoorChatDatabase);
|
late final ReadDao readDao = ReadDao(this as DriftChatDatabase);
|
||||||
late final ChannelQueryDao channelQueryDao =
|
late final ChannelQueryDao channelQueryDao =
|
||||||
ChannelQueryDao(this as MoorChatDatabase);
|
ChannelQueryDao(this as DriftChatDatabase);
|
||||||
late final ConnectionEventDao connectionEventDao =
|
late final ConnectionEventDao connectionEventDao =
|
||||||
ConnectionEventDao(this as MoorChatDatabase);
|
ConnectionEventDao(this as DriftChatDatabase);
|
||||||
@override
|
@override
|
||||||
Iterable<TableInfo> get allTables => allSchemaEntities.whereType<TableInfo>();
|
Iterable<TableInfo> get allTables => allSchemaEntities.whereType<TableInfo>();
|
||||||
@override
|
@override
|
||||||
@@ -2,27 +2,27 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:isolate';
|
import 'dart:isolate';
|
||||||
|
|
||||||
import 'package:moor/ffi.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:moor/isolate.dart';
|
import 'package:drift/isolate.dart';
|
||||||
import 'package:moor/moor.dart';
|
import 'package:drift/native.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:path_provider/path_provider.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/src/stream_chat_persistence_client.dart';
|
||||||
import 'package:stream_chat_persistence/stream_chat_persistence.dart';
|
import 'package:stream_chat_persistence/stream_chat_persistence.dart';
|
||||||
|
|
||||||
/// A Helper class to construct new instances of [MoorChatDatabase] specifically
|
/// A Helper class to construct new instances of [DriftChatDatabase]
|
||||||
/// for native platform applications
|
/// specifically for native platform applications.
|
||||||
class SharedDB {
|
class SharedDB {
|
||||||
/// Returns a new instance of [MoorChatDatabase].
|
/// Returns a new instance of [DriftChatDatabase].
|
||||||
static MoorChatDatabase constructDatabase(
|
static DriftChatDatabase constructDatabase(
|
||||||
String userId, {
|
String userId, {
|
||||||
bool logStatements = false,
|
bool logStatements = false,
|
||||||
ConnectionMode connectionMode = ConnectionMode.regular,
|
ConnectionMode connectionMode = ConnectionMode.regular,
|
||||||
}) {
|
}) {
|
||||||
final dbName = 'db_$userId';
|
final dbName = 'db_$userId';
|
||||||
if (connectionMode == ConnectionMode.background) {
|
if (connectionMode == ConnectionMode.background) {
|
||||||
return MoorChatDatabase.connect(
|
return DriftChatDatabase.connect(
|
||||||
userId,
|
userId,
|
||||||
DatabaseConnection.delayed(Future(() async {
|
DatabaseConnection.delayed(Future(() async {
|
||||||
final isolate = await _createMoorIsolate(
|
final isolate = await _createMoorIsolate(
|
||||||
@@ -33,7 +33,7 @@ class SharedDB {
|
|||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return MoorChatDatabase(
|
return DriftChatDatabase(
|
||||||
userId,
|
userId,
|
||||||
LazyDatabase(
|
LazyDatabase(
|
||||||
() async => _constructDatabase(
|
() async => _constructDatabase(
|
||||||
@@ -44,7 +44,7 @@ class SharedDB {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<VmDatabase> _constructDatabase(
|
static Future<NativeDatabase> _constructDatabase(
|
||||||
String dbName, {
|
String dbName, {
|
||||||
bool logStatements = false,
|
bool logStatements = false,
|
||||||
}) async {
|
}) async {
|
||||||
@@ -52,27 +52,27 @@ class SharedDB {
|
|||||||
final dir = await getApplicationDocumentsDirectory();
|
final dir = await getApplicationDocumentsDirectory();
|
||||||
final path = join(dir.path, '$dbName.sqlite');
|
final path = join(dir.path, '$dbName.sqlite');
|
||||||
final file = File(path);
|
final file = File(path);
|
||||||
return VmDatabase(file, logStatements: logStatements);
|
return NativeDatabase(file, logStatements: logStatements);
|
||||||
}
|
}
|
||||||
if (Platform.isMacOS || Platform.isLinux) {
|
if (Platform.isMacOS || Platform.isLinux) {
|
||||||
final file = File('$dbName.sqlite');
|
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) {
|
static void _startBackground(_IsolateStartRequest request) {
|
||||||
final executor = LazyDatabase(() async => VmDatabase(
|
final executor = LazyDatabase(() async => NativeDatabase(
|
||||||
File(request.targetPath),
|
File(request.targetPath),
|
||||||
logStatements: request.logStatements,
|
logStatements: request.logStatements,
|
||||||
));
|
));
|
||||||
final moorIsolate = MoorIsolate.inCurrent(
|
final moorIsolate = DriftIsolate.inCurrent(
|
||||||
() => DatabaseConnection.fromExecutor(executor),
|
() => DatabaseConnection.fromExecutor(executor),
|
||||||
);
|
);
|
||||||
request.sendMoorIsolate.send(moorIsolate);
|
request.sendMoorIsolate.send(moorIsolate);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<MoorIsolate> _createMoorIsolate(
|
static Future<DriftIsolate> _createMoorIsolate(
|
||||||
String dbName, {
|
String dbName, {
|
||||||
bool logStatements = false,
|
bool logStatements = false,
|
||||||
}) async {
|
}) 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
|
// 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';
|
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 {
|
class SharedDB {
|
||||||
/// Returns a new instance of [MoorChatDatabase].
|
/// Returns a new instance of [DriftChatDatabase].
|
||||||
static MoorChatDatabase constructDatabase(
|
static DriftChatDatabase constructDatabase(
|
||||||
String userId, {
|
String userId, {
|
||||||
bool logStatements = false,
|
bool logStatements = false,
|
||||||
ConnectionMode connectionMode = ConnectionMode.regular,
|
ConnectionMode connectionMode = ConnectionMode.regular,
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
// coverage:ignore-file
|
// coverage:ignore-file
|
||||||
import 'package:moor/moor_web.dart';
|
import 'package:drift/web.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/src/stream_chat_persistence_client.dart';
|
||||||
|
|
||||||
/// A Helper class to construct new instances of [MoorChatDatabase] specifically
|
/// A Helper class to construct new instances of [DriftChatDatabase]
|
||||||
/// for Web applications
|
/// specifically for Web applications.
|
||||||
class SharedDB {
|
class SharedDB {
|
||||||
/// Returns a new instance of [MoorChatDatabase].
|
/// Returns a new instance of [DriftChatDatabase].
|
||||||
static MoorChatDatabase constructDatabase(
|
static DriftChatDatabase constructDatabase(
|
||||||
String userId, {
|
String userId, {
|
||||||
bool logStatements = false,
|
bool logStatements = false,
|
||||||
ConnectionMode connectionMode = ConnectionMode.regular, // Ignored on web
|
ConnectionMode connectionMode = ConnectionMode.regular, // Ignored on web
|
||||||
}) {
|
}) {
|
||||||
final dbName = 'db_$userId';
|
final dbName = 'db_$userId';
|
||||||
final queryExecutor = WebDatabase(dbName, logStatements: logStatements);
|
final queryExecutor = WebDatabase(dbName, logStatements: logStatements);
|
||||||
return MoorChatDatabase(userId, queryExecutor);
|
return DriftChatDatabase(userId, queryExecutor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// coverage:ignore-file
|
// coverage:ignore-file
|
||||||
import 'package:moor/moor.dart';
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
/// Represents a [ChannelQueries] table in [MoorChatDatabase].
|
/// Represents a [ChannelQueries] table in [MoorChatDatabase].
|
||||||
@DataClassName('ChannelQueryEntity')
|
@DataClassName('ChannelQueryEntity')
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// coverage:ignore-file
|
// coverage:ignore-file
|
||||||
import 'package:moor/moor.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:stream_chat_persistence/src/converter/map_converter.dart';
|
import 'package:stream_chat_persistence/src/converter/map_converter.dart';
|
||||||
|
|
||||||
/// Represents a [Channels] table in [MoorChatDatabase].
|
/// Represents a [Channels] table in [MoorChatDatabase].
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// coverage:ignore-file
|
// coverage:ignore-file
|
||||||
import 'package:moor/moor.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:stream_chat_persistence/src/converter/map_converter.dart';
|
import 'package:stream_chat_persistence/src/converter/map_converter.dart';
|
||||||
|
|
||||||
/// Represents a [ConnectionEvents] table in [MoorChatDatabase].
|
/// Represents a [ConnectionEvents] table in [MoorChatDatabase].
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// coverage:ignore-file
|
// coverage:ignore-file
|
||||||
import 'package:moor/moor.dart';
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
/// Represents a [Members] table in [MoorChatDatabase].
|
/// Represents a [Members] table in [MoorChatDatabase].
|
||||||
@DataClassName('MemberEntity')
|
@DataClassName('MemberEntity')
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// coverage:ignore-file
|
// 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/list_converter.dart';
|
||||||
import 'package:stream_chat_persistence/src/converter/map_converter.dart';
|
import 'package:stream_chat_persistence/src/converter/map_converter.dart';
|
||||||
import 'package:stream_chat_persistence/src/converter/message_sending_status_converter.dart';
|
import 'package:stream_chat_persistence/src/converter/message_sending_status_converter.dart';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// coverage:ignore-file
|
// coverage:ignore-file
|
||||||
import 'package:moor/moor.dart';
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
import 'package:stream_chat_persistence/src/entity/reactions.dart';
|
import 'package:stream_chat_persistence/src/entity/reactions.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// coverage:ignore-file
|
// coverage:ignore-file
|
||||||
import 'package:moor/moor.dart';
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
import 'package:stream_chat_persistence/src/entity/messages.dart';
|
import 'package:stream_chat_persistence/src/entity/messages.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// coverage:ignore-file
|
// coverage:ignore-file
|
||||||
import 'package:moor/moor.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:stream_chat_persistence/src/converter/map_converter.dart';
|
import 'package:stream_chat_persistence/src/converter/map_converter.dart';
|
||||||
|
|
||||||
/// Represents a [Reactions] table in [MoorChatDatabase].
|
/// Represents a [Reactions] table in [MoorChatDatabase].
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// coverage:ignore-file
|
// coverage:ignore-file
|
||||||
import 'package:moor/moor.dart';
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
/// Represents a [Reads] table in [MoorChatDatabase].
|
/// Represents a [Reads] table in [MoorChatDatabase].
|
||||||
@DataClassName('ReadEntity')
|
@DataClassName('ReadEntity')
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// coverage:ignore-file
|
// coverage:ignore-file
|
||||||
import 'package:moor/moor.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:stream_chat_persistence/src/converter/map_converter.dart';
|
import 'package:stream_chat_persistence/src/converter/map_converter.dart';
|
||||||
|
|
||||||
/// Represents a [Users] table in [MoorChatDatabase].
|
/// Represents a [Users] table in [MoorChatDatabase].
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:stream_chat/stream_chat.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';
|
||||||
|
|
||||||
/// Useful mapping functions for [ChannelEntity]
|
/// Useful mapping functions for [ChannelEntity]
|
||||||
extension ChannelEntityX on ChannelEntity {
|
extension ChannelEntityX on ChannelEntity {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:stream_chat/stream_chat.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';
|
||||||
|
|
||||||
/// Useful mapping functions for [ConnectionEventEntity]
|
/// Useful mapping functions for [ConnectionEventEntity]
|
||||||
extension ConnectionEventX on ConnectionEventEntity {
|
extension ConnectionEventX on ConnectionEventEntity {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:stream_chat/stream_chat.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';
|
||||||
|
|
||||||
/// Useful mapping functions for [MemberEntity]
|
/// Useful mapping functions for [MemberEntity]
|
||||||
extension MemberEntityX on MemberEntity {
|
extension MemberEntityX on MemberEntity {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:stream_chat/stream_chat.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';
|
||||||
|
|
||||||
/// Useful mapping functions for [MessageEntity]
|
/// Useful mapping functions for [MessageEntity]
|
||||||
extension MessageEntityX on MessageEntity {
|
extension MessageEntityX on MessageEntity {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:stream_chat/stream_chat.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';
|
||||||
|
|
||||||
/// Useful mapping functions for [PinnedMessageEntity]
|
/// Useful mapping functions for [PinnedMessageEntity]
|
||||||
extension PinnedMessageEntityX on PinnedMessageEntity {
|
extension PinnedMessageEntityX on PinnedMessageEntity {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:stream_chat/stream_chat.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';
|
||||||
|
|
||||||
/// Useful mapping functions for [PinnedMessageReactionEntity]
|
/// Useful mapping functions for [PinnedMessageReactionEntity]
|
||||||
extension PinnedMessageReactionEntityX on PinnedMessageReactionEntity {
|
extension PinnedMessageReactionEntityX on PinnedMessageReactionEntity {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:stream_chat/stream_chat.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';
|
||||||
|
|
||||||
/// Useful mapping functions for [ReactionEntity]
|
/// Useful mapping functions for [ReactionEntity]
|
||||||
extension ReactionEntityX on ReactionEntity {
|
extension ReactionEntityX on ReactionEntity {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:stream_chat/stream_chat.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';
|
||||||
|
|
||||||
/// Useful mapping functions for [ReadEntity]
|
/// Useful mapping functions for [ReadEntity]
|
||||||
extension ReadEntityX on ReadEntity {
|
extension ReadEntityX on ReadEntity {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:stream_chat/stream_chat.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';
|
||||||
|
|
||||||
/// Useful mapping functions for [UserEntity]
|
/// Useful mapping functions for [UserEntity]
|
||||||
extension UserEntityX on UserEntity {
|
extension UserEntityX on UserEntity {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'package:meta/meta.dart';
|
|||||||
import 'package:mutex/mutex.dart';
|
import 'package:mutex/mutex.dart';
|
||||||
import 'package:stream_chat/stream_chat.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
|
/// Various connection modes on which [StreamChatPersistenceClient] can work
|
||||||
enum ConnectionMode {
|
enum ConnectionMode {
|
||||||
@@ -15,8 +15,8 @@ enum ConnectionMode {
|
|||||||
background,
|
background,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Signature for a function which provides instance of [MoorChatDatabase]
|
/// Signature for a function which provides instance of [DriftChatDatabase]
|
||||||
typedef DatabaseProvider = MoorChatDatabase Function(String, ConnectionMode);
|
typedef DatabaseProvider = DriftChatDatabase Function(String, ConnectionMode);
|
||||||
|
|
||||||
final _levelEmojiMapper = {
|
final _levelEmojiMapper = {
|
||||||
Level.INFO: 'ℹ️',
|
Level.INFO: 'ℹ️',
|
||||||
@@ -24,7 +24,7 @@ final _levelEmojiMapper = {
|
|||||||
Level.SEVERE: '🚨',
|
Level.SEVERE: '🚨',
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A [MoorChatDatabase] based implementation of the [ChatPersistenceClient]
|
/// A [DriftChatDatabase] based implementation of the [ChatPersistenceClient]
|
||||||
class StreamChatPersistenceClient extends ChatPersistenceClient {
|
class StreamChatPersistenceClient extends ChatPersistenceClient {
|
||||||
/// Creates a new instance of the stream chat persistence client
|
/// Creates a new instance of the stream chat persistence client
|
||||||
StreamChatPersistenceClient({
|
StreamChatPersistenceClient({
|
||||||
@@ -37,9 +37,9 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
|
|||||||
_logger.onRecord.listen(logHandlerFunction ?? _defaultLogHandler);
|
_logger.onRecord.listen(logHandlerFunction ?? _defaultLogHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [MoorChatDatabase] instance used by this client.
|
/// [DriftChatDatabase] instance used by this client.
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
MoorChatDatabase? db;
|
DriftChatDatabase? db;
|
||||||
|
|
||||||
final Logger _logger;
|
final Logger _logger;
|
||||||
final ConnectionMode _connectionMode;
|
final ConnectionMode _connectionMode;
|
||||||
@@ -70,7 +70,7 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
MoorChatDatabase _defaultDatabaseProvider(
|
DriftChatDatabase _defaultDatabaseProvider(
|
||||||
String userId,
|
String userId,
|
||||||
ConnectionMode mode,
|
ConnectionMode mode,
|
||||||
) =>
|
) =>
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ environment:
|
|||||||
flutter: ">=1.17.0"
|
flutter: ">=1.17.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
|
drift: ^1.0.0
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
logging: ^1.0.1
|
logging: ^1.0.1
|
||||||
meta: ^1.3.0
|
meta: ^1.3.0
|
||||||
moor: ^4.4.0
|
|
||||||
mutex: ^3.0.0
|
mutex: ^3.0.0
|
||||||
path: ^1.8.0
|
path: ^1.8.0
|
||||||
path_provider: ^2.0.1
|
path_provider: ^2.0.1
|
||||||
@@ -24,7 +24,8 @@ dependencies:
|
|||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
build_runner: ^2.0.1
|
build_runner: ^2.0.1
|
||||||
dart_code_metrics: ^4.4.0
|
dart_code_metrics: ^4.4.0
|
||||||
|
drift_dev: ^1.0.0
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
mocktail: ^0.1.1
|
mocktail: ^0.2.0
|
||||||
moor_generator: ^4.2.1
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
import 'package:stream_chat_persistence/src/dao/dao.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;
|
UserDao? _userDao;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:stream_chat/stream_chat.dart';
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
import 'package:stream_chat_persistence/src/dao/channel_dao.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';
|
import '../../stream_chat_persistence_client_test.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
late ChannelDao channelDao;
|
late ChannelDao channelDao;
|
||||||
late MoorChatDatabase database;
|
late DriftChatDatabase database;
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
database = testDatabaseProvider('testUserId');
|
database = testDatabaseProvider('testUserId');
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:stream_chat/stream_chat.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/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 '../../stream_chat_persistence_client_test.dart';
|
||||||
import '../utils/date_matcher.dart';
|
import '../utils/date_matcher.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
late MoorChatDatabase database;
|
late DriftChatDatabase database;
|
||||||
late ChannelQueryDao channelQueryDao;
|
late ChannelQueryDao channelQueryDao;
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user