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:
Deven Joshi
2021-11-10 19:09:02 +05:30
119 changed files with 7687 additions and 999 deletions
+1
View File
@@ -4,6 +4,7 @@ analyzer:
exclude:
- packages/*/lib/**/*.g.dart
- packages/*/lib/src/emoji/**
- packages/*/lib/scrollable_positioned_list/**
- packages/*/lib/**/*.freezed.dart
linter:
Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

@@ -0,0 +1,258 @@
---
id: end_to_end_chat_encryption
sidebar_position: 12
title: End To End Chat Encryption
---
## Introduction
When you communicate over a chat application with another person or group,
you may exchange sensitive information, like personally identifiable information, financial details, or passwords.
A chat application should use end-to-end encryption to ensure that users' data stays secure.
:::note
Before you start, keep in mind that this guide is a basic example intended for educational purposes only.
If you want to implement end-to-end encryption in your production app, please consult a security professional first.
Theres a lot more to consider from a security perspective that isnt 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 senders messages.
The receiver can only decrypt the senders message with the matching private key.
Check out the diagram below for an example:
![](../assets/end_to_end_encryption.png)
## Setup
### Dependencies
Add the [webcrypto](https://pub.dev/packages/webcrypto) package in your `pubspec.yaml` file.
```yaml
dependencies:
webcrypto: ^0.5.2 # latest version
```
### Generate Key Pair
Write a function that generates a key pair using the **ECDH** algorithm and the **P-256** elliptic curve (**P-256** is well-supported and
offers the right balance of security and performance).
The pair will consist of two keys:
- **PublicKey**: The key that is linked to a user to encrypt messages.
- **PrivateKey**: The key that is stored locally to decrypt messages.
```dart
Future<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 encryptions strength,
// each encryption process must use a random and distinct IV.
// Its 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 Streams server.
Here, you have used it to encrypt the message before sending it to Streams backend.
### Showing Decrypted Messages
Now, its 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).
+3 -2
View File
@@ -1,8 +1,9 @@
## Upcoming
## 3.2.0
🐞 Fixed
- markAllRead() now updates local channel states.
- `markAllRead()` now updates local channel states.
- [[#744]](https://github.com/GetStream/stream-chat-flutter/issues/744) Fixed unread count not updating correctly
## 3.1.1
@@ -1934,8 +1934,6 @@ class ChannelClientState {
read: newReads,
pinnedMessages: updatedState.pinnedMessages,
);
_computeUnread();
}
int _sortByCreatedAt(Message a, Message b) =>
+1 -1
View File
@@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart';
/// Current package version
/// Used in [StreamChatClient] to build the `x-stream-client` header
// ignore: constant_identifier_names
const PACKAGE_VERSION = '3.1.1';
const PACKAGE_VERSION = '3.2.0';
+5 -5
View File
@@ -1,7 +1,7 @@
name: stream_chat
homepage: https://getstream.io/
description: The official Dart client for Stream Chat, a service for building chat applications.
version: 3.1.1
version: 3.2.0
repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -13,7 +13,7 @@ dependencies:
collection: ^1.15.0
dio: ^4.0.0
equatable: ^2.0.0
freezed_annotation: ^0.14.0
freezed_annotation: ^0.15.0
http_parser: ^4.0.0
jose: ^0.3.2
json_annotation: ^4.0.1
@@ -28,7 +28,7 @@ dependencies:
dev_dependencies:
build_runner: ^2.0.1
dart_code_metrics: ^4.4.0
freezed: ^0.14.1+3
json_serializable: ^5.0.2
mocktail: ^0.1.1
freezed: ^0.15.0+1
json_serializable: ^6.0.1
mocktail: ^0.2.0
test: ^1.17.12
@@ -118,9 +118,9 @@ void main() {
setUpAll(() {
// Fallback values
registerFallbackValue<Message>(FakeMessage());
registerFallbackValue<List<Message>>(<Message>[]);
registerFallbackValue<AttachmentFile>(FakeAttachmentFile());
registerFallbackValue(FakeMessage());
registerFallbackValue(<Message>[]);
registerFallbackValue(FakeAttachmentFile());
// detached loggers
when(() => client.detachedLogger(any())).thenAnswer((invocation) {
@@ -176,9 +176,9 @@ void main() {
setUpAll(() {
// Fallback values
registerFallbackValue<Message>(FakeMessage());
registerFallbackValue<AttachmentFile>(FakeAttachmentFile());
registerFallbackValue<Event>(FakeEvent());
registerFallbackValue(FakeMessage());
registerFallbackValue(FakeAttachmentFile());
registerFallbackValue(FakeEvent());
// detached loggers
when(() => client.detachedLogger(any())).thenAnswer((invocation) {
@@ -31,7 +31,7 @@ void main() {
setUpAll(() {
// fallback values
registerFallbackValue<User>(FakeUser());
registerFallbackValue(FakeUser());
});
setUp(() {
@@ -230,7 +230,7 @@ void main() {
setUpAll(() {
// fallback values
registerFallbackValue<User>(FakeUser());
registerFallbackValue(FakeUser());
});
setUp(() {
@@ -311,7 +311,7 @@ void main() {
setUpAll(() {
// fallback values
registerFallbackValue<User>(FakeUser());
registerFallbackValue(FakeUser());
});
setUp(() {
@@ -399,7 +399,7 @@ void main() {
setUpAll(() {
// fallback values
registerFallbackValue<User>(FakeUser());
registerFallbackValue(FakeUser());
});
setUp(() {
@@ -523,9 +523,9 @@ void main() {
setUpAll(() {
// fallback values
registerFallbackValue<Event>(FakeEvent());
registerFallbackValue<PaginationParams>(const PaginationParams());
registerFallbackValue<ChannelState>(FakeChannelState());
registerFallbackValue(FakeEvent());
registerFallbackValue(const PaginationParams());
registerFallbackValue(FakeChannelState());
});
setUp(() async {
@@ -827,9 +827,9 @@ void main() {
setUpAll(() {
// fallback values
registerFallbackValue<Event>(FakeEvent());
registerFallbackValue<Message>(FakeMessage());
registerFallbackValue<PaginationParams>(const PaginationParams());
registerFallbackValue(FakeEvent());
registerFallbackValue(FakeMessage());
registerFallbackValue(const PaginationParams());
});
setUp(() async {
@@ -15,7 +15,7 @@ void main() {
setUp(() {
fileUploader = StreamAttachmentFileUploader(client);
registerFallbackValue<MultipartFile>(FakeMultiPartFile());
registerFallbackValue(FakeMultiPartFile());
});
Response successResponse(String path, {Object? data}) => Response(
+13
View File
@@ -1,10 +1,23 @@
## Upcoming
✅ Added
🛑️ Breaking Changes from `3.2.0`
- `MessageInput` now works with a `MessageInputController` instead of a `TextEditingController`
## 3.2.0
- Updated Dart SDK constraints to `>=2.14.0 <3.0.0`
- Updated `stream_chat_flutter_core` dependency to [`3.2.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog).
🐞 Fixed
- Fixed message highlight animation alignment in `MessageListView`
- [[#491]](https://github.com/GetStream/stream-chat-flutter/issues/491): Fix `MediaListView` showing media in wrong order.
- Fixed `MessageListView` initialIndex not working in some cases.
- Improved `MessageListView` rendering in case of reordering.
- Fix image thumbnail generation when using Stream CDN
✅ Added
@@ -0,0 +1,26 @@
Copyright 2018 the Dart project authors, Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -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';
}
@@ -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)''';
}
@@ -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;
});
}
}
}
@@ -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,
);
}
}
@@ -0,0 +1,593 @@
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:math';
import 'package:collection/collection.dart' show IterableExtension;
import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/src/item_positions_listener.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/src/item_positions_notifier.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/src/positioned_list.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/src/post_mount_callback.dart';
/// Number of screens to scroll when scrolling a long distance.
const int _screenScrollCount = 2;
/// A scrollable list of widgets similar to [ListView], except scroll control
/// and position reporting is based on index rather than pixel offset.
///
/// [ScrollablePositionedList] lays out children in the same way as [ListView].
///
/// The list can be displayed with the item at [initialScrollIndex] positioned
/// at a particular [initialAlignment].
///
/// The [itemScrollController] can be used to scroll or jump to particular items
/// in the list. The [itemPositionsNotifier] can be used to get a list of items
/// currently laid out by the list.
///
/// All other parameters are the same as specified in [ListView].
class ScrollablePositionedList extends StatefulWidget {
/// Create a [ScrollablePositionedList] whose items are provided by
/// [itemBuilder].
const ScrollablePositionedList.builder({
required this.itemCount,
required this.itemBuilder,
Key? key,
this.itemScrollController,
ItemPositionsListener? itemPositionsListener,
this.initialScrollIndex = 0,
this.initialAlignment = 0,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.physics,
this.semanticChildCount,
this.padding,
this.addSemanticIndexes = true,
this.addAutomaticKeepAlives = true,
this.addRepaintBoundaries = true,
this.minCacheExtent,
this.findChildIndexCallback,
}) : itemPositionsNotifier = itemPositionsListener as ItemPositionsNotifier?,
separatorBuilder = null,
super(key: key);
/// Create a [ScrollablePositionedList] whose items are provided by
/// [itemBuilder] and separators provided by [separatorBuilder].
const ScrollablePositionedList.separated({
required this.itemCount,
required this.itemBuilder,
required this.separatorBuilder,
Key? key,
this.itemScrollController,
ItemPositionsListener? itemPositionsListener,
this.initialScrollIndex = 0,
this.initialAlignment = 0,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.physics,
this.semanticChildCount,
this.padding,
this.addSemanticIndexes = true,
this.addAutomaticKeepAlives = true,
this.addRepaintBoundaries = true,
this.minCacheExtent,
this.findChildIndexCallback,
}) : assert(separatorBuilder != null, 'seperatorBuilder cannot be null'),
itemPositionsNotifier = itemPositionsListener as ItemPositionsNotifier?,
super(key: key);
/// Called to find the new index of a child based on its key in case of
/// reordering.
///
/// If not provided, a child widget may not map to its existing [RenderObject]
/// when the order in which children are returned from [builder] changes.
/// This may result in state-loss.
///
/// This callback should take an input [Key], and it should return the
/// index of the child element with that associated key, or null if not found.
final ChildIndexGetter? findChildIndexCallback;
/// Number of items the [itemBuilder] can produce.
final int itemCount;
/// Called to build children for the list with
/// 0 <= index < itemCount.
final IndexedWidgetBuilder itemBuilder;
/// Called to build separators for between each item in the list.
/// Called with 0 <= index < itemCount - 1.
final IndexedWidgetBuilder? separatorBuilder;
/// Controller for jumping or scrolling to an item.
final ItemScrollController? itemScrollController;
/// Notifier that reports the items laid out in the list after each frame.
final ItemPositionsNotifier? itemPositionsNotifier;
/// Index of an item to initially align within the viewport.
final int initialScrollIndex;
/// Determines where the leading edge of the item at [initialScrollIndex]
/// should be placed.
///
/// See [ItemScrollController.jumpTo] for an explanation of alignment.
final double initialAlignment;
/// The axis along which the scroll view scrolls.
///
/// Defaults to [Axis.vertical].
final Axis scrollDirection;
/// Whether the view scrolls in the reading direction.
///
/// Defaults to false.
///
/// See [ScrollView.reverse].
final bool reverse;
/// How the scroll view should respond to user input.
///
/// For example, determines how the scroll view continues to animate after the
/// user stops dragging the scroll view.
///
/// See [ScrollView.physics].
final ScrollPhysics? physics;
/// The number of children that will contribute semantic information.
///
/// See [ScrollView.semanticChildCount] for more information.
final int? semanticChildCount;
/// The amount of space by which to inset the children.
final EdgeInsets? padding;
/// Whether to wrap each child in an [IndexedSemantics].
///
/// See [SliverChildBuilderDelegate.addSemanticIndexes].
final bool addSemanticIndexes;
/// Whether to wrap each child in an [AutomaticKeepAlive].
///
/// See [SliverChildBuilderDelegate.addAutomaticKeepAlives].
final bool addAutomaticKeepAlives;
/// Whether to wrap each child in a [RepaintBoundary].
///
/// See [SliverChildBuilderDelegate.addRepaintBoundaries].
final bool addRepaintBoundaries;
/// The minimum cache extent used by the underlying scroll lists.
/// See [ScrollView.cacheExtent].
///
/// Note that the [ScrollablePositionedList] uses two lists to simulate long
/// scrolls, so using the [ScrollController.scrollTo] method may result
/// in builds of widgets that would otherwise already be built in the
/// cache extent.
final double? minCacheExtent;
@override
State<StatefulWidget> createState() => _ScrollablePositionedListState();
}
/// Controller to jump or scroll to a particular position in a
/// [ScrollablePositionedList].
class ItemScrollController {
/// Whether any ScrollablePositionedList objects are attached this object.
///
/// If `false`, then [jumpTo] and [scrollTo] must not be called.
bool get isAttached => _scrollableListState != null;
_ScrollablePositionedListState? _scrollableListState;
/// Immediately, without animation, reconfigure the list so that the item at
/// [index]'s leading edge is at the given [alignment].
///
/// The [alignment] specifies the desired position for the leading edge of the
/// item. The [alignment] is expected to be a value in the range \[0.0, 1.0\]
/// and represents a proportion along the main axis of the viewport.
///
/// For a vertically scrolling view that is not reversed:
/// * 0 aligns the top edge of the item with the top edge of the view.
/// * 1 aligns the top edge of the item with the bottom of the view.
/// * 0.5 aligns the top edge of the item with the center of the view.
///
/// For a horizontally scrolling view that is not reversed:
/// * 0 aligns the left edge of the item with the left edge of the view
/// * 1 aligns the left edge of the item with the right edge of the view.
/// * 0.5 aligns the left edge of the item with the center of the view.
void jumpTo({required int index, double alignment = 0}) {
_scrollableListState!._jumpTo(index: index, alignment: alignment);
}
/// Animate the list over [duration] using the given [curve] such that the
/// item at [index] ends up with its leading edge at the given [alignment].
/// See [jumpTo] for an explanation of alignment.
///
/// The [duration] must be greater than 0; otherwise, use [jumpTo].
///
/// When item position is not available, because it's too far, the scroll
/// is composed into three phases:
///
/// 1. The currently displayed list view starts scrolling.
/// 2. Another list view, which scrolls with the same speed, fades over the
/// first one and shows items that are close to the scroll target.
/// 3. The second list view scrolls and stops on the target.
///
/// The [opacityAnimationWeights] can be used to apply custom weights to these
/// three stages of this animation. The default weights, `[40, 20, 40]`, are
/// good with default [Curves.linear]. Different weights might be better for
/// other cases. For example, if you use [Curves.easeOut], consider setting
/// [opacityAnimationWeights] to `[20, 20, 60]`.
///
/// See [TweenSequenceItem.weight] for more info.
Future<void> scrollTo({
required int index,
double alignment = 0,
required Duration duration,
Curve curve = Curves.linear,
List<double> opacityAnimationWeights = const [40, 20, 40],
}) {
assert(_scrollableListState != null, '_scrollableListState cannot be null');
assert(opacityAnimationWeights.length == 3,
'opacityAnimationWeights.length is not equal to 3');
assert(duration > Duration.zero,
'duration needs to be bigger than Duration.zero');
return _scrollableListState!._scrollTo(
index: index,
alignment: alignment,
duration: duration,
curve: curve,
opacityAnimationWeights: opacityAnimationWeights,
);
}
void _attach(_ScrollablePositionedListState scrollableListState) {
assert(
_scrollableListState == null, '_scrollableListState needs to be null');
_scrollableListState = scrollableListState;
}
void _detach() {
_scrollableListState = null;
}
}
class _ScrollablePositionedListState extends State<ScrollablePositionedList>
with TickerProviderStateMixin {
/// Details for the primary (active) [ListView].
_ListDisplayDetails primary = _ListDisplayDetails(const ValueKey('Ping'));
/// Details for the secondary (transitional) [ListView] that is temporarily
/// shown when scrolling a long distance.
_ListDisplayDetails secondary = _ListDisplayDetails(const ValueKey('Pong'));
final opacity = ProxyAnimation(const AlwaysStoppedAnimation<double>(0));
void Function() startAnimationCallback = () {};
bool _isTransitioning = false;
@override
void initState() {
super.initState();
final ItemPosition? initialPosition =
PageStorage.of(context)!.readState(context);
primary
..target = initialPosition?.index ?? widget.initialScrollIndex
..alignment = initialPosition?.itemLeadingEdge ?? widget.initialAlignment;
if (widget.itemCount > 0 && primary.target > widget.itemCount - 1) {
primary.target = widget.itemCount - 1;
}
widget.itemScrollController?._attach(this);
primary.itemPositionsNotifier.itemPositions.addListener(_updatePositions);
secondary.itemPositionsNotifier.itemPositions.addListener(_updatePositions);
}
@override
void deactivate() {
widget.itemScrollController?._detach();
super.deactivate();
}
@override
void dispose() {
primary.itemPositionsNotifier.itemPositions
.removeListener(_updatePositions);
secondary.itemPositionsNotifier.itemPositions
.removeListener(_updatePositions);
super.dispose();
}
@override
void didUpdateWidget(ScrollablePositionedList oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.itemScrollController?._scrollableListState == this) {
oldWidget.itemScrollController?._detach();
}
if (widget.itemScrollController?._scrollableListState != this) {
widget.itemScrollController?._detach();
widget.itemScrollController?._attach(this);
}
if (widget.itemCount == 0) {
primary.target = 0;
secondary.target = 0;
} else {
if (primary.target > widget.itemCount - 1) {
primary.target = widget.itemCount - 1;
}
if (secondary.target > widget.itemCount - 1) {
secondary.target = widget.itemCount - 1;
}
}
}
@override
Widget build(BuildContext context) => LayoutBuilder(
builder: (context, constraints) {
final cacheExtent = _cacheExtent(constraints);
return GestureDetector(
onPanDown: (_) => _stopScroll(canceled: true),
excludeFromSemantics: true,
child: Stack(
children: <Widget>[
PostMountCallback(
key: primary.key,
callback: startAnimationCallback,
child: FadeTransition(
opacity: ReverseAnimation(opacity),
child: NotificationListener<ScrollNotification>(
onNotification: (_) => _isTransitioning,
child: PositionedList(
itemBuilder: widget.itemBuilder,
separatorBuilder: widget.separatorBuilder,
itemCount: widget.itemCount,
positionedIndex: primary.target,
controller: primary.scrollController,
itemPositionsNotifier: primary.itemPositionsNotifier,
scrollDirection: widget.scrollDirection,
reverse: widget.reverse,
cacheExtent: cacheExtent,
alignment: primary.alignment,
physics: widget.physics,
addSemanticIndexes: widget.addSemanticIndexes,
semanticChildCount: widget.semanticChildCount,
padding: widget.padding,
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
addRepaintBoundaries: widget.addRepaintBoundaries,
findChildIndexCallback: widget.findChildIndexCallback,
),
),
),
),
if (_isTransitioning)
PostMountCallback(
key: secondary.key,
callback: startAnimationCallback,
child: FadeTransition(
opacity: opacity,
child: NotificationListener<ScrollNotification>(
onNotification: (_) => false,
child: PositionedList(
itemBuilder: widget.itemBuilder,
separatorBuilder: widget.separatorBuilder,
itemCount: widget.itemCount,
itemPositionsNotifier:
secondary.itemPositionsNotifier,
positionedIndex: secondary.target,
controller: secondary.scrollController,
scrollDirection: widget.scrollDirection,
reverse: widget.reverse,
cacheExtent: cacheExtent,
alignment: secondary.alignment,
physics: widget.physics,
addSemanticIndexes: widget.addSemanticIndexes,
semanticChildCount: widget.semanticChildCount,
padding: widget.padding,
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
addRepaintBoundaries: widget.addRepaintBoundaries,
),
),
),
),
],
),
);
},
);
double _cacheExtent(BoxConstraints constraints) => max(
constraints.maxHeight * _screenScrollCount,
widget.minCacheExtent ?? 0,
);
void _jumpTo({required int index, required double alignment}) {
_stopScroll(canceled: true);
if (index > widget.itemCount - 1) {
index = widget.itemCount - 1;
}
setState(() {
primary.scrollController.jumpTo(0);
primary
..target = index
..alignment = alignment;
});
}
Future<void> _scrollTo({
required int index,
required double alignment,
required Duration duration,
Curve curve = Curves.linear,
required List<double> opacityAnimationWeights,
}) async {
if (index > widget.itemCount - 1) {
index = widget.itemCount - 1;
}
if (_isTransitioning) {
_stopScroll(canceled: true);
SchedulerBinding.instance!.addPostFrameCallback((_) {
_startScroll(
index: index,
alignment: alignment,
duration: duration,
curve: curve,
opacityAnimationWeights: opacityAnimationWeights,
);
});
} else {
await _startScroll(
index: index,
alignment: alignment,
duration: duration,
curve: curve,
opacityAnimationWeights: opacityAnimationWeights,
);
}
}
Future<void> _startScroll({
required int index,
required double alignment,
required Duration duration,
Curve curve = Curves.linear,
required List<double> opacityAnimationWeights,
}) async {
final direction = index > primary.target ? 1 : -1;
final itemPosition =
primary.itemPositionsNotifier.itemPositions.value.firstWhereOrNull(
(ItemPosition itemPosition) => itemPosition.index == index,
);
if (itemPosition != null) {
// Scroll directly.
final localScrollAmount = itemPosition.itemLeadingEdge *
primary.scrollController.position.viewportDimension;
await primary.scrollController.animateTo(
primary.scrollController.offset +
localScrollAmount -
alignment * primary.scrollController.position.viewportDimension,
duration: duration,
curve: curve,
);
} else {
final scrollAmount = _screenScrollCount *
primary.scrollController.position.viewportDimension;
final startCompleter = Completer<void>();
final endCompleter = Completer<void>();
startAnimationCallback = () {
SchedulerBinding.instance!.addPostFrameCallback((_) {
startAnimationCallback = () {};
opacity.parent = _opacityAnimation(opacityAnimationWeights).animate(
AnimationController(vsync: this, duration: duration)..forward(),
);
secondary.scrollController.jumpTo(-direction *
(_screenScrollCount *
primary.scrollController.position.viewportDimension -
alignment *
secondary.scrollController.position.viewportDimension));
startCompleter.complete(primary.scrollController.animateTo(
primary.scrollController.offset + direction * scrollAmount,
duration: duration,
curve: curve,
));
endCompleter.complete(secondary.scrollController
.animateTo(0, duration: duration, curve: curve));
});
};
setState(() {
// TODO: _startScroll can be re-entrant, which invalidates this assert.
// assert(!_isTransitioning);
secondary
..target = index
..alignment = alignment;
_isTransitioning = true;
});
await Future.wait<void>([startCompleter.future, endCompleter.future]);
_stopScroll();
}
}
void _stopScroll({bool canceled = false}) {
if (!_isTransitioning) {
return;
}
if (canceled) {
if (primary.scrollController.hasClients) {
primary.scrollController.jumpTo(primary.scrollController.offset);
}
if (secondary.scrollController.hasClients) {
secondary.scrollController.jumpTo(secondary.scrollController.offset);
}
}
setState(() {
if (opacity.value >= 0.5) {
// Secondary [ListView] is more visible than the primary; make it the
// new primary.
final temp = primary;
primary = secondary;
secondary = temp;
}
_isTransitioning = false;
opacity.parent = const AlwaysStoppedAnimation<double>(0);
});
}
Animatable<double> _opacityAnimation(List<double> opacityAnimationWeights) {
const startOpacity = 0.0;
const endOpacity = 1.0;
return TweenSequence<double>(<TweenSequenceItem<double>>[
TweenSequenceItem<double>(
tween: ConstantTween<double>(startOpacity),
weight: opacityAnimationWeights[0],
),
TweenSequenceItem<double>(
tween: Tween<double>(begin: startOpacity, end: endOpacity),
weight: opacityAnimationWeights[1],
),
TweenSequenceItem<double>(
tween: ConstantTween<double>(endOpacity),
weight: opacityAnimationWeights[2],
),
]);
}
void _updatePositions() {
final itemPositions = primary.itemPositionsNotifier.itemPositions.value
.where((ItemPosition position) =>
position.itemLeadingEdge < 1 && position.itemTrailingEdge > 0);
if (itemPositions.isNotEmpty) {
PageStorage.of(context)!.writeState(
context,
itemPositions.reduce((value, element) =>
value.itemLeadingEdge < element.itemLeadingEdge ? value : element),
);
}
widget.itemPositionsNotifier?.itemPositions.value = itemPositions;
}
}
class _ListDisplayDetails {
_ListDisplayDetails(this.key);
final itemPositionsNotifier = ItemPositionsNotifier();
final scrollController = ScrollController(keepScrollOffset: false);
/// The index of the item to scroll to.
int target = 0;
/// The desired alignment for [target].
///
/// See [ItemScrollController.jumpTo] for an explanation of alignment.
double alignment = 0;
final Key key;
}
@@ -0,0 +1,326 @@
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// ignore_for_file: lines_longer_than_80_chars
import 'dart:math' as math;
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
/// {@template unbounded_viewport}
/// A render object that is bigger on the inside.
///
/// Version of [Viewport] with some modifications to how extents are
/// computed to allow scroll extents outside 0 to 1. See [Viewport]
/// for more information.
/// description
class UnboundedViewport extends Viewport {
/// {@macro unbounded_viewport}
UnboundedViewport({
Key? key,
AxisDirection axisDirection = AxisDirection.down,
AxisDirection? crossAxisDirection,
double anchor = 0.0,
required ViewportOffset offset,
Key? center,
double? cacheExtent,
List<Widget> slivers = const <Widget>[],
}) : _anchor = anchor,
super(
key: key,
axisDirection: axisDirection,
crossAxisDirection: crossAxisDirection,
offset: offset,
center: center,
cacheExtent: cacheExtent,
slivers: slivers,
);
// [Viewport] enforces constraints on [Viewport.anchor], so we need our own
// version.
final double _anchor;
@override
double get anchor => _anchor;
@override
RenderViewport createRenderObject(BuildContext context) =>
UnboundedRenderViewport(
axisDirection: axisDirection,
crossAxisDirection: crossAxisDirection ??
Viewport.getDefaultCrossAxisDirection(context, axisDirection),
anchor: anchor,
offset: offset,
cacheExtent: cacheExtent,
);
}
/// A render object that is bigger on the inside.
///
/// Version of [RenderViewport] with some modifications to how extents are
/// computed to allow scroll extents outside 0 to 1. See [RenderViewport]
/// for more information.
///
// Differences from [RenderViewport] are marked with a //***** Differences
// comment.
class UnboundedRenderViewport extends RenderViewport {
/// Creates a viewport for [RenderSliver] objects.
UnboundedRenderViewport({
AxisDirection axisDirection = AxisDirection.down,
required AxisDirection crossAxisDirection,
required ViewportOffset offset,
double anchor = 0.0,
List<RenderSliver>? children,
RenderSliver? center,
double? cacheExtent,
}) : _anchor = anchor,
super(
axisDirection: axisDirection,
crossAxisDirection: crossAxisDirection,
offset: offset,
center: center,
cacheExtent: cacheExtent,
children: children,
);
static const int _maxLayoutCycles = 10;
double _anchor;
// Out-of-band data computed during layout.
late double _minScrollExtent;
late double _maxScrollExtent;
bool _hasVisualOverflow = false;
/// This value is set during layout based on the [CacheExtentStyle].
///
/// When the style is [CacheExtentStyle.viewport], it is the main axis extent
/// of the viewport multiplied by the requested cache extent, which is still
/// expressed in pixels.
double? _calculatedCacheExtent;
@override
double get anchor => _anchor;
@override
set anchor(double value) {
if (value == _anchor) return;
_anchor = value;
markNeedsLayout();
}
@override
void performResize() {
super.performResize();
// TODO: Figure out why this override is needed as a result of
// https://github.com/flutter/flutter/pull/61973 and see if it can be
// removed somehow.
switch (axis) {
case Axis.vertical:
offset.applyViewportDimension(size.height);
break;
case Axis.horizontal:
offset.applyViewportDimension(size.width);
break;
}
}
@override
Rect describeSemanticsClip(RenderSliver? child) {
if (_calculatedCacheExtent == null) {
return semanticBounds;
}
switch (axis) {
case Axis.vertical:
return Rect.fromLTRB(
semanticBounds.left,
semanticBounds.top - _calculatedCacheExtent!,
semanticBounds.right,
semanticBounds.bottom + _calculatedCacheExtent!,
);
default:
return Rect.fromLTRB(
semanticBounds.left - _calculatedCacheExtent!,
semanticBounds.top,
semanticBounds.right + _calculatedCacheExtent!,
semanticBounds.bottom,
);
}
}
@override
void performLayout() {
if (center == null) {
assert(firstChild == null, 'firstChild cannot be null');
_minScrollExtent = 0.0;
_maxScrollExtent = 0.0;
_hasVisualOverflow = false;
offset.applyContentDimensions(0, 0);
return;
}
assert(center!.parent == this, 'center.parent cannot be equal to this');
late double mainAxisExtent;
late double crossAxisExtent;
switch (axis) {
case Axis.vertical:
mainAxisExtent = size.height;
crossAxisExtent = size.width;
break;
case Axis.horizontal:
mainAxisExtent = size.width;
crossAxisExtent = size.height;
break;
}
final centerOffsetAdjustment = center!.centerOffsetAdjustment;
double correction;
var count = 0;
do {
correction = _attemptLayout(
mainAxisExtent,
crossAxisExtent,
offset.pixels + centerOffsetAdjustment,
);
if (correction != 0.0) {
offset.correctBy(correction);
} else {
// *** Difference from [RenderViewport].
final top = _minScrollExtent + mainAxisExtent * anchor;
final bottom = _maxScrollExtent - mainAxisExtent * (1.0 - anchor);
final maxScrollOffset = math.max<double>(math.min(0, top), bottom);
final minScrollOffset = math.min<double>(top, maxScrollOffset);
if (offset.applyContentDimensions(minScrollOffset, maxScrollOffset)) {
break;
}
// *** End of difference from [RenderViewport].
}
count += 1;
} while (count < _maxLayoutCycles);
assert(() {
if (count >= _maxLayoutCycles) {
assert(count != 1, 'count not equal to 1');
throw FlutterError(
'A RenderViewport exceeded its maximum number of layout cycles.\n'
'RenderViewport render objects, during layout, can retry if either their '
'slivers or their ViewportOffset decide that the offset should be corrected '
'to take into account information collected during that layout.\n'
'In the case of this RenderViewport object, however, this happened $count '
'times and still there was no consensus on the scroll offset. This usually '
'indicates a bug. Specifically, it means that one of the following three '
'problems is being experienced by the RenderViewport object:\n'
' * One of the RenderSliver children or the ViewportOffset have a bug such'
' that they always think that they need to correct the offset regardless.\n'
' * Some combination of the RenderSliver children and the ViewportOffset'
' have a bad interaction such that one applies a correction then another'
' applies a reverse correction, leading to an infinite loop of corrections.\n'
' * There is a pathological case that would eventually resolve, but it is'
' so complicated that it cannot be resolved in any reasonable number of'
' layout passes.',
);
}
return true;
}(), 'count needs to be bigger than _maxLayoutCycles');
}
double _attemptLayout(
double mainAxisExtent,
double crossAxisExtent,
double correctedOffset,
) {
assert(!mainAxisExtent.isNaN, 'assert mainAxisExtent.isNaN');
assert(mainAxisExtent >= 0.0, 'assert mainAxisExtent >= 0.0');
assert(crossAxisExtent.isFinite, 'assert crossAxisExtent.isFinite');
assert(crossAxisExtent >= 0.0, 'assert crossAxisExtent >= 0.0');
assert(correctedOffset.isFinite, 'assert correctedOffset.isFinite');
_minScrollExtent = 0.0;
_maxScrollExtent = 0.0;
_hasVisualOverflow = false;
// centerOffset is the offset from the leading edge of the RenderViewport
// to the zero scroll offset (the line between the forward slivers and the
// reverse slivers).
final centerOffset = mainAxisExtent * anchor - correctedOffset;
final reverseDirectionRemainingPaintExtent =
centerOffset.clamp(0.0, mainAxisExtent);
final forwardDirectionRemainingPaintExtent =
(mainAxisExtent - centerOffset).clamp(0.0, mainAxisExtent);
switch (cacheExtentStyle) {
case CacheExtentStyle.pixel:
_calculatedCacheExtent = cacheExtent;
break;
case CacheExtentStyle.viewport:
_calculatedCacheExtent = mainAxisExtent * cacheExtent!;
break;
}
final fullCacheExtent = mainAxisExtent + 2 * _calculatedCacheExtent!;
final centerCacheOffset = centerOffset + _calculatedCacheExtent!;
final reverseDirectionRemainingCacheExtent =
centerCacheOffset.clamp(0.0, fullCacheExtent);
final forwardDirectionRemainingCacheExtent =
(fullCacheExtent - centerCacheOffset).clamp(0.0, fullCacheExtent);
final leadingNegativeChild = childBefore(center!);
if (leadingNegativeChild != null) {
// negative scroll offsets
final result = layoutChildSequence(
child: leadingNegativeChild,
scrollOffset: math.max(mainAxisExtent, centerOffset) - mainAxisExtent,
overlap: 0,
layoutOffset: forwardDirectionRemainingPaintExtent,
remainingPaintExtent: reverseDirectionRemainingPaintExtent,
mainAxisExtent: mainAxisExtent,
crossAxisExtent: crossAxisExtent,
growthDirection: GrowthDirection.reverse,
advance: childBefore,
remainingCacheExtent: reverseDirectionRemainingCacheExtent,
cacheOrigin: (mainAxisExtent - centerOffset)
.clamp(-_calculatedCacheExtent!, 0.0),
);
if (result != 0.0) return -result;
}
// positive scroll offsets
return layoutChildSequence(
child: center,
scrollOffset: math.max(0, -centerOffset),
overlap: leadingNegativeChild == null ? math.min(0, -centerOffset) : 0.0,
layoutOffset: centerOffset >= mainAxisExtent
? centerOffset
: reverseDirectionRemainingPaintExtent,
remainingPaintExtent: forwardDirectionRemainingPaintExtent,
mainAxisExtent: mainAxisExtent,
crossAxisExtent: crossAxisExtent,
growthDirection: GrowthDirection.forward,
advance: childAfter,
remainingCacheExtent: forwardDirectionRemainingCacheExtent,
cacheOrigin: centerOffset.clamp(-_calculatedCacheExtent!, 0.0),
);
}
@override
bool get hasVisualOverflow => _hasVisualOverflow;
@override
void updateOutOfBandData(
GrowthDirection growthDirection,
SliverGeometry childLayoutGeometry,
) {
switch (growthDirection) {
case GrowthDirection.forward:
_maxScrollExtent += childLayoutGeometry.scrollExtent;
break;
case GrowthDirection.reverse:
_minScrollExtent -= childLayoutGeometry.scrollExtent;
break;
}
if (childLayoutGeometry.hasVisualOverflow) _hasVisualOverflow = true;
}
}
@@ -142,7 +142,9 @@ class GiphyAttachment extends AttachmentWidget {
});
},
child: Text(
context.translations.cancelLabel.toLowerCase(),
context.translations.cancelLabel
.toLowerCase()
.capitalize(),
style: StreamChatTheme.of(context)
.textTheme
.bodyBold
@@ -72,7 +72,11 @@ class ImageAttachment extends AttachmentWidget {
}
var imageUri = Uri.parse(imageUrl);
if (imageUri.host == 'stream-io-cdn.com') {
if (imageUri.host.endsWith('stream-io-cdn.com') &&
imageUri.queryParameters['h'] == '*' &&
imageUri.queryParameters['w'] == '*' &&
imageUri.queryParameters['crop'] == '*' &&
imageUri.queryParameters['resize'] == '*') {
imageUri = imageUri.replace(queryParameters: {
...imageUri.queryParameters,
'h': '400',
@@ -80,7 +84,7 @@ class ImageAttachment extends AttachmentWidget {
'crop': 'center',
'resize': 'crop',
});
} else if (imageUri.host == 'stream-cloud-uploads.imgix.net') {
} else if (imageUri.host.endsWith('stream-cloud-uploads.imgix.net')) {
imageUri = imageUri.replace(queryParameters: {
...imageUri.queryParameters,
'height': '400',
@@ -93,7 +97,7 @@ class ImageAttachment extends AttachmentWidget {
return _buildImageAttachment(
context,
CachedNetworkImage(
cacheKey: imageUrl,
cacheKey: imageUri.replace(queryParameters: {}).toString(),
height: size?.height,
width: size?.width,
placeholder: (context, __) {
@@ -9,13 +9,11 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:image_picker/image_picker.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:shimmer/shimmer.dart';
import 'package:stream_chat_flutter/src/commands_overlay.dart';
import 'package:stream_chat_flutter/src/emoji/emoji.dart';
import 'package:stream_chat_flutter/src/emoji_overlay.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/media_list_view.dart';
import 'package:stream_chat_flutter/src/message_list_view.dart';
import 'package:stream_chat_flutter/src/multi_overlay.dart';
import 'package:stream_chat_flutter/src/quoted_message_widget.dart';
@@ -79,6 +77,20 @@ typedef ActionButtonBuilder = Widget Function(
IconButton defaultActionButton,
);
/// Widget builder for widgets that require may required data from the
/// [MessageInputController]
typedef MessageRelatedBuilder = Widget Function(
BuildContext context,
MessageInputController messageInputController,
);
/// Widget builder for a custom attachment picker.
typedef AttachmentsPickerBuilder = Widget Function(
BuildContext context,
MessageInputController messageInputController,
StreamAttachmentPicker defaultPicker,
);
/// Location for actions on the [MessageInput]
enum ActionsLocation {
/// Align to left
@@ -204,6 +216,8 @@ class MessageInput extends StatefulWidget {
this.commandButtonBuilder,
this.customOverlays = const [],
this.mentionAllAppUsers = false,
this.attachmentsPickerBuilder,
this.sendButtonBuilder,
}) : assert(
initialMessage == null || editMessage == null,
"Can't provide both `initialMessage` and `editMessage`",
@@ -322,6 +336,12 @@ class MessageInput extends StatefulWidget {
/// Defaults to false.
final bool mentionAllAppUsers;
/// Builds bottom sheet when attachment picker is opened.
final AttachmentsPickerBuilder? attachmentsPickerBuilder;
/// Builder for creating send button
final MessageRelatedBuilder? sendButtonBuilder;
@override
MessageInputState createState() => MessageInputState();
@@ -349,7 +369,6 @@ class MessageInputState extends State<MessageInput> {
Command? _chosenCommand;
bool _actionsShrunk = false;
bool _openFilePickerSection = false;
int _filePickerIndex = 0;
/// The editing controller passed to the input TextField
late final MessageInputController messageInputController =
@@ -524,7 +543,7 @@ class MessageInputState extends State<MessageInput> {
widget.actionsLocation == ActionsLocation.right)
_buildExpandActionsButton(context),
if (widget.sendButtonLocation == SendButtonLocation.outside)
_animateSendButton(context),
_buildSendButton(context),
],
);
@@ -588,25 +607,18 @@ class MessageInputState extends State<MessageInput> {
],
);
Widget _animateSendButton(BuildContext context) {
late Widget sendButton;
if (_timeOut > 0) {
sendButton = _CountdownButton(count: _timeOut);
} else if (!_messageIsPresent &&
messageInputController.attachments.isEmpty) {
sendButton = widget.idleSendButton ?? _buildIdleSendButton(context);
} else {
sendButton = widget.activeSendButton != null
? InkWell(
onTap: sendMessage,
child: widget.activeSendButton,
)
: _buildSendButton(context);
Widget _buildSendButton(BuildContext context) {
if (widget.sendButtonBuilder != null) {
return widget.sendButtonBuilder!(context, messageInputController);
}
return AnimatedSwitcher(
duration: _streamChatTheme.messageInputTheme.sendAnimationDuration!,
child: sendButton,
return StreamMessageSendButton(
onSendMessage: sendMessage,
timeOut: _timeOut,
isIdle: !_messageIsPresent && messageInputController.attachments.isEmpty,
isEditEnabled: widget.editMessage != null,
idleSendButton: widget.idleSendButton,
activeSendButton: widget.activeSendButton,
);
}
@@ -817,7 +829,7 @@ class MessageInputState extends State<MessageInput> {
widget.actionsLocation == ActionsLocation.rightInside)
_buildExpandActionsButton(context),
if (widget.sendButtonLocation == SendButtonLocation.inside)
_animateSendButton(context),
_buildSendButton(context),
],
),
).merge(passedDecoration);
@@ -946,236 +958,33 @@ class MessageInputState extends State<MessageInput> {
}
Widget _buildFilePickerSection() {
final _attachmentContainsFile =
messageInputController.attachments.any((it) => it.type == 'file');
final attachmentLimitCrossed =
messageInputController.attachments.length >= widget.attachmentLimit;
Color _getIconColor(int index) {
final streamChatThemeData = _streamChatTheme;
switch (index) {
case 0:
return messageInputController.attachments.isEmpty
? streamChatThemeData.colorTheme.accentPrimary
: (!_attachmentContainsFile
? streamChatThemeData.colorTheme.accentPrimary
: streamChatThemeData.colorTheme.textHighEmphasis
.withOpacity(0.2));
case 1:
return _attachmentContainsFile
? streamChatThemeData.colorTheme.accentPrimary
: (messageInputController.attachments.isEmpty
? streamChatThemeData.colorTheme.textHighEmphasis
.withOpacity(0.5)
: streamChatThemeData.colorTheme.textHighEmphasis
.withOpacity(0.2));
case 2:
return attachmentLimitCrossed
? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2)
: _attachmentContainsFile &&
messageInputController.attachments.isNotEmpty
? streamChatThemeData.colorTheme.textHighEmphasis
.withOpacity(0.2)
: streamChatThemeData.colorTheme.textHighEmphasis
.withOpacity(0.5);
case 3:
return attachmentLimitCrossed
? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2)
: _attachmentContainsFile &&
messageInputController.attachments.isNotEmpty
? streamChatThemeData.colorTheme.textHighEmphasis
.withOpacity(0.2)
: streamChatThemeData.colorTheme.textHighEmphasis
.withOpacity(0.5);
default:
return Colors.black;
}
}
return AnimatedContainer(
duration: _openFilePickerSection
? const Duration(milliseconds: 300)
: const Duration(),
curve: Curves.easeOut,
height: _openFilePickerSection ? _kMinMediaPickerSize : 0,
child: SingleChildScrollView(
child: SizedBox(
height: _kMinMediaPickerSize,
child: Material(
color: _streamChatTheme.colorTheme.inputBg,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
IconButton(
icon: StreamSvgIcon.pictures(
color: _getIconColor(0),
),
onPressed: _attachmentContainsFile &&
messageInputController.attachments.isNotEmpty
? null
: () {
setState(() {
_filePickerIndex = 0;
});
},
),
IconButton(
iconSize: 32,
icon: StreamSvgIcon.files(
color: _getIconColor(1),
),
onPressed: !_attachmentContainsFile &&
messageInputController.attachments.isNotEmpty
? null
: () {
pickFile(DefaultAttachmentTypes.file);
},
),
IconButton(
icon: StreamSvgIcon.camera(
color: _getIconColor(2),
),
onPressed: attachmentLimitCrossed ||
(_attachmentContainsFile &&
messageInputController.attachments.isNotEmpty)
? null
: () {
pickFile(
DefaultAttachmentTypes.image,
camera: true,
);
},
),
IconButton(
padding: const EdgeInsets.all(0),
icon: StreamSvgIcon.record(
color: _getIconColor(3),
),
onPressed: attachmentLimitCrossed ||
(_attachmentContainsFile &&
messageInputController.attachments.isNotEmpty)
? null
: () {
pickFile(
DefaultAttachmentTypes.video,
camera: true,
);
},
),
],
),
DecoratedBox(
decoration: BoxDecoration(
color: _streamChatTheme.colorTheme.barsBg,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: Center(
child: Padding(
padding: const EdgeInsets.all(8),
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: _streamChatTheme.colorTheme.inputBg,
borderRadius: BorderRadius.circular(4),
),
),
),
),
),
if (_openFilePickerSection)
Expanded(
child: DecoratedBox(
decoration: BoxDecoration(
color: _streamChatTheme.colorTheme.barsBg,
borderRadius: BorderRadius.circular(8),
),
child: _PickerWidget(
filePickerIndex: _filePickerIndex,
streamChatTheme: _streamChatTheme,
containsFile: _attachmentContainsFile,
selectedMedias: messageInputController.attachments
.map((e) => e.id)
.toList(),
onAddMoreFilesClick: pickFile,
onMediaSelected: (media) {
if (messageInputController.attachments
.any((e) => e.id == media.id)) {
setState(() => messageInputController.attachments
.removeWhere((e) => e.id == media.id));
} else {
_addAssetAttachment(media);
}
},
),
),
),
],
),
),
),
),
);
}
void _addAssetAttachment(AssetEntity medium) async {
final mediaFile = await medium.originFile.timeout(
const Duration(seconds: 5),
onTimeout: () => medium.originFile,
final picker = StreamAttachmentPicker(
messageInputController: messageInputController,
onFilePicked: pickFile,
isOpen: _openFilePickerSection,
pickerSize: _openFilePickerSection ? _kMinMediaPickerSize : 0,
attachmentLimit: widget.attachmentLimit,
onAttachmentLimitExceeded: widget.onAttachmentLimitExceed,
maxAttachmentSize: widget.maxAttachmentSize,
compressedVideoQuality: widget.compressedVideoQuality,
compressedVideoFrameRate: widget.compressedVideoFrameRate,
onChangeInputState: (val) {
setState(() {
_inputEnabled = val;
});
},
onError: _showErrorAlert,
);
if (mediaFile == null) return;
var file = AttachmentFile(
path: mediaFile.path,
size: await mediaFile.length(),
bytes: mediaFile.readAsBytesSync(),
);
if (file.size! > widget.maxAttachmentSize) {
if (medium.type == AssetType.video && file.path != null) {
final mediaInfo = await (VideoService.compressVideo(
file.path!,
frameRate: widget.compressedVideoFrameRate,
quality: widget.compressedVideoQuality,
) as FutureOr<MediaInfo>);
if (mediaInfo.filesize! > widget.maxAttachmentSize) {
_showErrorAlert(
context.translations.fileTooLargeAfterCompressionError(
widget.maxAttachmentSize / (1024 * 1024),
),
);
return;
}
file = AttachmentFile(
name: file.name,
size: mediaInfo.filesize,
bytes: await mediaInfo.file?.readAsBytes(),
path: mediaInfo.path,
);
} else {
_showErrorAlert(context.translations.fileTooLargeError(
widget.maxAttachmentSize / (1024 * 1024),
));
return;
}
}
setState(() {
final attachment = Attachment(
id: medium.id,
file: file,
type: medium.type == AssetType.image ? 'image' : 'video',
if (_openFilePickerSection && widget.attachmentsPickerBuilder != null) {
return widget.attachmentsPickerBuilder!(
context,
messageInputController,
picker,
);
_addAttachments([attachment]);
});
}
return picker;
}
Widget _buildMentionsOverlayEntry() {
@@ -1736,53 +1545,12 @@ class MessageInputState extends State<MessageInput> {
});
}
Widget _buildIdleSendButton(BuildContext context) => Padding(
padding: const EdgeInsets.all(8),
child: StreamSvgIcon(
assetName: _getIdleSendIcon(),
color: _messageInputTheme.sendButtonIdleColor,
),
);
Widget _buildSendButton(BuildContext context) => Padding(
padding: const EdgeInsets.all(8),
child: IconButton(
onPressed: sendMessage,
padding: const EdgeInsets.all(0),
splashRadius: 24,
constraints: const BoxConstraints.tightFor(
height: 24,
width: 24,
),
icon: StreamSvgIcon(
assetName: _getSendIcon(),
color: _messageInputTheme.sendButtonColor,
),
),
);
String _getIdleSendIcon() {
if (_commandEnabled) {
return 'Icon_search.svg';
} else {
return 'Icon_circle_right.svg';
}
}
String _getSendIcon() {
if (widget.editMessage != null) {
return 'Icon_circle_up.svg';
} else if (_commandEnabled) {
return 'Icon_search.svg';
} else {
return 'Icon_circle_up.svg';
}
}
/// Sends the current message
Future<void> sendMessage() async {
var text = messageInputController.text.trim();
if (text.isEmpty && messageInputController.attachments.isEmpty) {
final attachments = messageInputController.attachments;
if (text.isEmpty && attachments.isEmpty) {
return;
}
@@ -1805,7 +1573,7 @@ class MessageInputState extends State<MessageInput> {
if (widget.editMessage != null) {
message = widget.editMessage!.copyWith(
text: text,
attachments: messageInputController.attachments,
attachments: attachments,
mentionedUsers: messageInputController.mentionedUsers
.where((u) => text.contains('@${u.name}'))
.toList(),
@@ -1814,7 +1582,7 @@ class MessageInputState extends State<MessageInput> {
message = (widget.initialMessage ?? Message()).copyWith(
parentId: widget.parentMessage?.id,
text: text,
attachments: messageInputController.attachments,
attachments: attachments,
mentionedUsers: messageInputController.mentionedUsers
.where((u) => text.contains('@${u.name}'))
.toList(),
@@ -1960,7 +1728,7 @@ class MessageInputState extends State<MessageInput> {
void didChangeDependencies() {
_streamChatTheme = StreamChatTheme.of(context);
_messageInputTheme = MessageInputTheme.of(context);
if (widget.editMessage == null) _startSlowMode();
if (widget.editMessage == null && _timeOut <= 0) _startSlowMode();
if ((widget.editMessage != null || widget.initialMessage != null) &&
!_initialized) {
@@ -1970,140 +1738,3 @@ class MessageInputState extends State<MessageInput> {
super.didChangeDependencies();
}
}
class _PickerWidget extends StatefulWidget {
const _PickerWidget({
Key? key,
required this.filePickerIndex,
required this.containsFile,
required this.selectedMedias,
required this.onAddMoreFilesClick,
required this.onMediaSelected,
required this.streamChatTheme,
}) : super(key: key);
final int filePickerIndex;
final bool containsFile;
final List<String> selectedMedias;
final void Function(DefaultAttachmentTypes) onAddMoreFilesClick;
final void Function(AssetEntity) onMediaSelected;
final StreamChatThemeData streamChatTheme;
@override
_PickerWidgetState createState() => _PickerWidgetState();
}
class _PickerWidgetState extends State<_PickerWidget> {
Future<bool>? requestPermission;
@override
void initState() {
super.initState();
requestPermission = PhotoManager.requestPermission();
}
@override
Widget build(BuildContext context) {
if (widget.filePickerIndex != 0) {
return const Offstage();
}
return FutureBuilder<bool>(
future: requestPermission,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Offstage();
}
if (snapshot.data!) {
if (widget.containsFile) {
return GestureDetector(
onTap: () {
widget.onAddMoreFilesClick(DefaultAttachmentTypes.file);
},
child: Container(
constraints: const BoxConstraints.expand(),
color: widget.streamChatTheme.colorTheme.inputBg,
alignment: Alignment.center,
child: Text(
context.translations.addMoreFilesLabel,
style: TextStyle(
color: widget.streamChatTheme.colorTheme.accentPrimary,
fontWeight: FontWeight.bold,
),
),
),
);
}
return MediaListView(
selectedIds: widget.selectedMedias,
onSelect: widget.onMediaSelected,
);
}
return InkWell(
onTap: () async {
PhotoManager.openSetting();
},
child: Container(
color: widget.streamChatTheme.colorTheme.inputBg,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SvgPicture.asset(
'svgs/icon_picture_empty_state.svg',
package: 'stream_chat_flutter',
height: 140,
color: widget.streamChatTheme.colorTheme.disabled,
),
Text(
context.translations.enablePhotoAndVideoAccessMessage,
style: widget.streamChatTheme.textTheme.body.copyWith(
color: widget.streamChatTheme.colorTheme.textLowEmphasis,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 6),
Center(
child: Text(
context.translations.allowGalleryAccessMessage,
style: widget.streamChatTheme.textTheme.bodyBold.copyWith(
color: widget.streamChatTheme.colorTheme.accentPrimary,
),
),
),
],
),
),
);
},
);
}
}
class _CountdownButton extends StatelessWidget {
const _CountdownButton({
Key? key,
required this.count,
}) : super(key: key);
final int count;
@override
Widget build(BuildContext context) => Padding(
padding: const EdgeInsets.all(8),
child: DecoratedBox(
decoration: BoxDecoration(
color: StreamChatTheme.of(context).colorTheme.disabled,
shape: BoxShape.circle,
),
child: SizedBox(
height: 24,
width: 24,
child: Center(
child: Text('$count'),
),
),
),
);
}
@@ -6,8 +6,7 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:rxdart/rxdart.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/info_tile.dart';
import 'package:stream_chat_flutter/src/message_widget.dart';
@@ -145,7 +144,8 @@ class MessageListView extends StatefulWidget {
this.threadBuilder,
this.onThreadTap,
this.dateDividerBuilder,
this.scrollPhysics = const ClampingScrollPhysics(),
this.scrollPhysics =
const ClampingScrollPhysics(), // we need to use ClampingScrollPhysics to avoid the list view to animate and break while loading
this.initialScrollIndex,
this.initialAlignment,
this.scrollController,
@@ -225,7 +225,7 @@ class MessageListView extends StatefulWidget {
final ItemPositionsListener? itemPositionListener;
/// The ScrollPhysics used by the ListView
final ScrollPhysics scrollPhysics;
final ScrollPhysics? scrollPhysics;
/// Called when message item gets swiped
final OnMessageSwiped? onMessageSwiped;
@@ -296,7 +296,7 @@ class MessageListView extends StatefulWidget {
class _MessageListViewState extends State<MessageListView> {
ItemScrollController? _scrollController;
void Function(Message)? _onThreadTap;
bool _showScrollToBottom = false;
final ValueNotifier<bool> _showScrollToBottom = ValueNotifier(false);
late final ItemPositionsListener _itemPositionListener;
int? _messageListLength;
StreamChannelState? streamChannel;
@@ -306,12 +306,17 @@ class _MessageListViewState extends State<MessageListView> {
final initialScrollIndex = widget.initialScrollIndex;
if (initialScrollIndex != null) return initialScrollIndex;
if (streamChannel!.initialMessageId != null) {
final messages = streamChannel!.channel.state!.messages;
final messages = streamChannel!.channel.state!.messages
.where(widget.messageFilter ??
defaultMessageFilter(
streamChannel!.channel.client.state.currentUser!.id,
))
.toList(growable: false);
final totalMessages = messages.length;
final messageIndex =
messages.indexWhere((e) => e.id == streamChannel!.initialMessageId);
final index = totalMessages - messageIndex;
if (index != 0) return index - 1;
if (index != 0) return index + 1;
return index;
}
return 0;
@@ -320,7 +325,7 @@ class _MessageListViewState extends State<MessageListView> {
double get _initialAlignment {
final initialAlignment = widget.initialAlignment;
if (initialAlignment != null) return initialAlignment;
return 0;
return 0.1;
}
bool _isInitialMessage(String id) => streamChannel!.initialMessageId == id;
@@ -329,7 +334,6 @@ class _MessageListViewState extends State<MessageListView> {
bool get _isThreadConversation => widget.parentMessage != null;
bool _topPaginationActive = false;
bool _bottomPaginationActive = false;
int initialIndex = 0;
@@ -337,6 +341,8 @@ class _MessageListViewState extends State<MessageListView> {
List<Message> messages = <Message>[];
Map<String, int> messagesIndex = {};
bool initialMessageHighlightComplete = false;
bool _inBetweenList = false;
@@ -382,6 +388,9 @@ class _MessageListViewState extends State<MessageListView> {
Widget _buildListView(List<Message> data) {
messages = data;
for (var index = 0; index < messages.length; index++) {
messagesIndex[messages[index].id] = index;
}
final newMessagesListLength = messages.length;
if (_messageListLength != null) {
@@ -390,14 +399,13 @@ class _MessageListViewState extends State<MessageListView> {
final first = _itemPositionListener.itemPositions.value.first;
final diff = newMessagesListLength - _messageListLength!;
if (diff > 0) {
initialIndex = first.index + diff;
initialAlignment = first.itemLeadingEdge;
if (messages[0].user?.id !=
streamChannel!.channel.client.state.currentUser?.id) {
initialIndex = first.index + diff;
initialAlignment = first.itemLeadingEdge;
}
}
}
} else if (!_topPaginationActive && _upToDate) {
// Reset the index in-case we send any new message
initialIndex = 0;
initialAlignment = 0;
}
}
@@ -441,7 +449,6 @@ class _MessageListViewState extends State<MessageListView> {
onStartOfPage: () async {
_inBetweenList = false;
if (!_upToDate) {
_topPaginationActive = false;
_bottomPaginationActive = true;
return _paginateData(
streamChannel,
@@ -451,7 +458,6 @@ class _MessageListViewState extends State<MessageListView> {
},
onEndOfPage: () async {
_inBetweenList = false;
_topPaginationActive = true;
_bottomPaginationActive = false;
return _paginateData(
streamChannel,
@@ -462,17 +468,26 @@ class _MessageListViewState extends State<MessageListView> {
_inBetweenList = true;
},
child: ScrollablePositionedList.separated(
key: _upToDate
? null
: ValueKey(initialIndex + initialAlignment),
key: (initialIndex != 0 && initialAlignment != 0)
? ValueKey('$initialIndex-$initialAlignment')
: null,
itemPositionsListener: _itemPositionListener,
initialScrollIndex: initialIndex,
initialAlignment: initialAlignment,
physics: widget.scrollPhysics,
itemScrollController: _scrollController,
reverse: widget.reverse,
addAutomaticKeepAlives: false,
itemCount: itemCount,
findChildIndexCallback: (Key key) {
final indexedKey = key as IndexedKey;
final valueKey = indexedKey.key as ValueKey<String>?;
if (valueKey != null) {
final index = messagesIndex[valueKey.value];
if (index != null) {
return ((index + 2) * 2) - 1;
}
}
},
// Item Count -> 8 (1 parent, 2 header+footer, 2 top+bottom, 3 messages)
// eg: |Type| rev(|Index(item)|) rev(|Index(separator)|) |Index(item)| |Index(separator)|
@@ -624,14 +639,30 @@ class _MessageListViewState extends State<MessageListView> {
} else {
messageWidget = buildMessage(message, messages, i - 2);
}
return messageWidget;
return KeyedSubtree(
key: ValueKey(message.id),
child: messageWidget,
);
},
),
),
);
},
),
if (widget.showScrollToBottom) _buildScrollToBottom(),
BetterStreamBuilder<bool>(
stream: streamChannel!.channel.state!.isUpToDateStream,
initialData: streamChannel!.channel.state!.isUpToDate,
builder: (context, snapshot) => ValueListenableBuilder<bool>(
valueListenable: _showScrollToBottom,
child: _buildScrollToBottom(),
builder: (context, value, child) {
if (!snapshot || value) {
return child!;
}
return const Offstage();
},
),
),
if (widget.showFloatingDateDivider)
_buildFloatingDateDivider(itemCount),
],
@@ -751,24 +782,15 @@ class _MessageListViewState extends State<MessageListView> {
.index;
}
Widget _buildScrollToBottom() => StreamBuilder<Tuple2<bool, int>>(
stream: Rx.combineLatest2(
streamChannel!.channel.state!.isUpToDateStream.distinct(),
streamChannel!.channel.state!.unreadCountStream.distinct(),
(bool isUpToDate, int unreadCount) => Tuple2(isUpToDate, unreadCount),
),
Widget _buildScrollToBottom() => StreamBuilder<int>(
stream: streamChannel!.channel.state!.unreadCountStream,
builder: (_, snapshot) {
if (snapshot.hasError) {
return const Offstage();
} else if (!snapshot.hasData) {
return const Offstage();
}
final isUpToDate = snapshot.data!.item1;
final showScrollToBottom = !isUpToDate || _showScrollToBottom;
if (!showScrollToBottom) {
return const Offstage();
}
final unreadCount = snapshot.data!.item2;
final unreadCount = snapshot.data!;
final showUnreadCount = unreadCount > 0 &&
streamChannel!.channel.state!.members.any((e) =>
e.userId ==
@@ -783,16 +805,21 @@ class _MessageListViewState extends State<MessageListView> {
children: [
FloatingActionButton(
backgroundColor: _streamTheme.colorTheme.barsBg,
onPressed: () {
onPressed: () async {
if (unreadCount > 0) {
streamChannel!.channel.markRead();
}
if (!_upToDate) {
_bottomPaginationActive = false;
_topPaginationActive = false;
streamChannel!.reloadChannel();
initialAlignment = 0;
initialIndex = 0;
await streamChannel!.reloadChannel();
WidgetsBinding.instance?.addPostFrameCallback((_) {
_scrollController!.jumpTo(index: 0);
});
} else {
setState(() => _showScrollToBottom = false);
_showScrollToBottom.value = false;
_scrollController!.scrollTo(
index: 0,
duration: const Duration(seconds: 1),
@@ -854,9 +881,8 @@ class _MessageListViewState extends State<MessageListView> {
int index,
) {
final messageWidget = buildMessage(message, messages, index);
return VisibilityDetector(
key: ValueKey<String>('BOTTOM-MESSAGE-${message.id}'),
key: ValueKey('visibility: ${message.id}'),
onVisibilityChanged: (visibility) {
final isVisible = visibility.visibleBounds != Rect.zero;
if (isVisible) {
@@ -868,8 +894,8 @@ class _MessageListViewState extends State<MessageListView> {
}
}
if (mounted) {
if (_showScrollToBottom == isVisible) {
setState(() => _showScrollToBottom = !isVisible);
if (_showScrollToBottom.value == isVisible) {
_showScrollToBottom.value = !isVisible;
}
}
},
@@ -948,16 +974,11 @@ class _MessageListViewState extends State<MessageListView> {
return defaultMessageWidget;
}
Widget buildMessage(
Message message,
List<Message> messages,
int index,
) {
Widget buildMessage(Message message, List<Message> messages, int index) {
if ((message.type == 'system' || message.type == 'error') &&
message.text?.isNotEmpty == true) {
return widget.systemMessageBuilder?.call(context, message) ??
SystemMessage(
key: ValueKey<String>('MESSAGE-${message.id}'),
message: message,
onMessageTap: (message) {
if (widget.onSystemMessageTap != null) {
@@ -1037,7 +1058,6 @@ class _MessageListViewState extends State<MessageListView> {
members.firstWhereOrNull((e) => e.user!.id == currentUser!.id);
Widget messageWidget = MessageWidget(
key: ValueKey<String>('MESSAGE-${message.id}'),
message: message,
reverse: isMyMessage,
showReactions: !message.isDeleted,
@@ -1049,23 +1069,20 @@ class _MessageListViewState extends State<MessageListView> {
showSendingIndicator: showSendingIndicator,
showUserAvatar: showUserAvatar,
onQuotedMessageTap: (quotedMessageId) async {
// ignore: prefer_function_declarations_over_variables
final scrollToIndex = () {
if (messages.map((e) => e.id).contains(quotedMessageId)) {
final index = messages.indexWhere((m) => m.id == quotedMessageId);
_scrollController?.scrollTo(
index: index,
duration: const Duration(milliseconds: 350),
index: index + 2, // +2 to account for loader and footer
duration: const Duration(seconds: 1),
curve: Curves.easeInOut,
alignment: 0.1,
);
};
if (messages.map((e) => e.id).contains(quotedMessageId)) {
scrollToIndex();
} else {
await streamChannel!.loadChannelAtMessage(quotedMessageId).then((_) {
WidgetsBinding.instance!.addPostFrameCallback((_) {
if (messages.map((e) => e.id).contains(quotedMessageId)) {
scrollToIndex();
}
});
await streamChannel!
.loadChannelAtMessage(quotedMessageId)
.then((_) async {
initialIndex = 21; // 19 + 2 | 19 is the index of the message
initialAlignment = 0.1;
});
}
},
@@ -1198,7 +1215,7 @@ class _MessageListViewState extends State<MessageListView> {
child: child,
),
child: Padding(
padding: const EdgeInsets.only(top: 4),
padding: const EdgeInsets.symmetric(vertical: 4),
child: child,
),
);
@@ -1229,27 +1246,25 @@ class _MessageListViewState extends State<MessageListView> {
initialIndex = _initialIndex;
initialAlignment = _initialAlignment;
WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
if (_scrollController?.isAttached == true) {
_scrollController?.jumpTo(
index: initialIndex,
alignment: initialAlignment,
);
}
});
if (_scrollController?.isAttached == true) {
_scrollController?.jumpTo(
index: initialIndex,
alignment: initialAlignment,
);
}
_messageNewListener =
streamChannel!.channel.on(EventType.messageNew).listen((event) {
if (_upToDate) {
_bottomPaginationActive = false;
_topPaginationActive = false;
}
if (event.message?.parentId == widget.parentMessage?.id &&
event.message!.user!.id ==
streamChannel!.channel.client.state.currentUser!.id) {
WidgetsBinding.instance!.addPostFrameCallback((_) {
_scrollController?.jumpTo(
_scrollController?.scrollTo(
index: 0,
duration: const Duration(seconds: 1),
);
});
}
@@ -608,6 +608,8 @@ class _MessageWidgetState extends State<MessageWidget>
final bottomRowPadding =
widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 0.5;
final showReactions = _shouldShowReactions;
return Material(
type: widget.message.pinned && widget.showPinHighlight
? MaterialType.card
@@ -671,17 +673,23 @@ class _MessageWidgetState extends State<MessageWidget>
SizedBox(width: avatarWidth + 4),
Flexible(
child: PortalEntry(
portal: Container(
transform: Matrix4.translationValues(
widget.reverse ? 12 : -12,
0,
0,
),
constraints: const BoxConstraints(
maxWidth: 22 * 6.0,
),
child: _buildReactionIndicator(context),
),
visible: showReactions,
portal: showReactions
? Container(
transform:
Matrix4.translationValues(
widget.reverse ? 12 : -12,
0,
0,
),
constraints: const BoxConstraints(
maxWidth: 22 * 6.0,
),
child: _buildReactionIndicator(
context,
),
)
: null,
portalAnchor:
Alignment(widget.reverse ? 1 : -1, -1),
childAnchor:
@@ -1036,9 +1044,7 @@ class _MessageWidgetState extends State<MessageWidget>
return AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: (widget.showReactions &&
(widget.message.reactionCounts?.isNotEmpty == true) &&
!widget.message.isDeleted)
child: _shouldShowReactions
? GestureDetector(
onTap: () => _showMessageReactionsModalBottomSheet(context),
child: ReactionBubble(
@@ -1058,6 +1064,11 @@ class _MessageWidgetState extends State<MessageWidget>
);
}
bool get _shouldShowReactions =>
widget.showReactions &&
(widget.message.reactionCounts?.isNotEmpty == true) &&
!widget.message.isDeleted;
void _showMessageActionModalBottomSheet(BuildContext context) {
final channel = StreamChannel.of(context).channel;
@@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Button for showing visual component of slow mode.
class CountdownButton extends StatelessWidget {
/// Constructor for creating [CountdownButton].
const CountdownButton({
Key? key,
required this.count,
}) : super(key: key);
/// Count of time remaining to show to the user.
final int count;
@override
Widget build(BuildContext context) => Padding(
padding: const EdgeInsets.all(8),
child: DecoratedBox(
decoration: BoxDecoration(
color: StreamChatTheme.of(context).colorTheme.disabled,
shape: BoxShape.circle,
),
child: SizedBox(
height: 24,
width: 24,
child: Center(
child: Text('$count'),
),
),
),
);
}
@@ -0,0 +1,551 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/media_list_view.dart';
import 'package:stream_chat_flutter/src/video_service.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:video_compress/video_compress.dart';
/// Callback for when a file has to be picked.
typedef FilePickerCallback = void Function(
DefaultAttachmentTypes fileType, {
bool camera,
});
/// Callback for building an icon for a custom attachment type.
typedef CustomAttachmentIconBuilder = Widget Function(
BuildContext context,
bool active,
);
///
class StreamAttachmentPicker extends StatefulWidget {
final bool isOpen;
final double pickerSize;
final MessageInputController messageInputController;
final int attachmentLimit;
final AttachmentLimitExceedListener? onAttachmentLimitExceeded;
final ValueChanged<bool>? onChangeInputState;
final ValueChanged<String>? onError;
final FilePickerCallback onFilePicked;
/// Video quality to use when compressing the videos
final VideoQuality compressedVideoQuality;
/// Frame rate to use when compressing the videos
final int compressedVideoFrameRate;
/// Max attachment size in bytes
/// Defaults to 20 MB
/// do not set it if you're using our default CDN
final int maxAttachmentSize;
final List<DefaultAttachmentTypes> allowedAttachmentTypes;
final List<CustomAttachmentType> customAttachmentTypes;
const StreamAttachmentPicker({
Key? key,
required this.messageInputController,
required this.onFilePicked,
this.isOpen = false,
this.pickerSize = 360.0,
this.attachmentLimit = 10,
this.onAttachmentLimitExceeded,
this.maxAttachmentSize = 20971520,
this.compressedVideoQuality = VideoQuality.DefaultQuality,
this.compressedVideoFrameRate = 30,
this.onChangeInputState,
this.onError,
this.allowedAttachmentTypes = const [
DefaultAttachmentTypes.image,
DefaultAttachmentTypes.file,
DefaultAttachmentTypes.video,
],
this.customAttachmentTypes = const [],
}) : super(key: key);
StreamAttachmentPicker copyWith({
Key? key,
MessageInputController? messageInputController,
FilePickerCallback? onFilePicked,
bool? isOpen,
double? pickerSize,
int? attachmentLimit,
AttachmentLimitExceedListener? onAttachmentLimitExceeded,
int? maxAttachmentSize,
VideoQuality? compressedVideoQuality,
int? compressedVideoFrameRate,
ValueChanged<bool>? onChangeInputState,
ValueChanged<String>? onError,
List<DefaultAttachmentTypes>? allowedAttachmentTypes,
List<CustomAttachmentType>? customAttachmentTypes = const [],
}) =>
StreamAttachmentPicker(
key: key ?? this.key,
messageInputController:
messageInputController ?? this.messageInputController,
onFilePicked: onFilePicked ?? this.onFilePicked,
isOpen: isOpen ?? this.isOpen,
pickerSize: pickerSize ?? this.pickerSize,
attachmentLimit: attachmentLimit ?? this.attachmentLimit,
onAttachmentLimitExceeded:
onAttachmentLimitExceeded ?? this.onAttachmentLimitExceeded,
maxAttachmentSize: maxAttachmentSize ?? this.maxAttachmentSize,
compressedVideoQuality:
compressedVideoQuality ?? this.compressedVideoQuality,
compressedVideoFrameRate:
compressedVideoFrameRate ?? this.compressedVideoFrameRate,
onChangeInputState: onChangeInputState ?? this.onChangeInputState,
onError: onError ?? this.onError,
allowedAttachmentTypes:
allowedAttachmentTypes ?? this.allowedAttachmentTypes,
customAttachmentTypes:
customAttachmentTypes ?? this.customAttachmentTypes,
);
@override
State<StreamAttachmentPicker> createState() => _StreamAttachmentPickerState();
}
class _StreamAttachmentPickerState extends State<StreamAttachmentPicker> {
int _filePickerIndex = 0;
@override
Widget build(BuildContext context) {
var _streamChatTheme = StreamChatTheme.of(context);
var messageInputController = widget.messageInputController;
final _attachmentContainsImage =
messageInputController.attachments.any((it) => it.type == 'image');
final _attachmentContainsFile =
messageInputController.attachments.any((it) => it.type == 'file');
final _attachmentContainsVideo =
messageInputController.attachments.any((it) => it.type == 'video');
final attachmentLimitCrossed =
messageInputController.attachments.length >= widget.attachmentLimit;
Color _getIconColor(int index) {
final streamChatThemeData = _streamChatTheme;
switch (index) {
case 0:
return _filePickerIndex == 0 || _attachmentContainsImage
? streamChatThemeData.colorTheme.accentPrimary
: (_attachmentContainsImage
? streamChatThemeData.colorTheme.accentPrimary
: streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(
messageInputController.attachments.isEmpty ? 0.5 : 0.2,
));
case 1:
return _attachmentContainsFile
? streamChatThemeData.colorTheme.accentPrimary
: (messageInputController.attachments.isEmpty
? streamChatThemeData.colorTheme.textHighEmphasis
.withOpacity(0.5)
: streamChatThemeData.colorTheme.textHighEmphasis
.withOpacity(0.2));
case 2:
return widget.messageInputController.attachments.isNotEmpty &&
(!_attachmentContainsImage || attachmentLimitCrossed)
? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2)
: _attachmentContainsFile &&
messageInputController.attachments.isNotEmpty
? streamChatThemeData.colorTheme.textHighEmphasis
.withOpacity(0.2)
: streamChatThemeData.colorTheme.textHighEmphasis
.withOpacity(0.5);
case 3:
return widget.messageInputController.attachments.isNotEmpty &&
(!_attachmentContainsVideo || attachmentLimitCrossed)
? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2)
: _attachmentContainsFile &&
messageInputController.attachments.isNotEmpty
? streamChatThemeData.colorTheme.textHighEmphasis
.withOpacity(0.2)
: streamChatThemeData.colorTheme.textHighEmphasis
.withOpacity(0.5);
default:
return Colors.black;
}
}
return AnimatedContainer(
duration:
widget.isOpen ? const Duration(milliseconds: 300) : const Duration(),
curve: Curves.easeOut,
height: widget.isOpen ? widget.pickerSize : 0,
child: SingleChildScrollView(
child: SizedBox(
height: widget.pickerSize,
child: Material(
color: _streamChatTheme.colorTheme.inputBg,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
if (widget.allowedAttachmentTypes
.contains(DefaultAttachmentTypes.image))
IconButton(
icon: StreamSvgIcon.pictures(
color: _getIconColor(0),
),
onPressed:
messageInputController.attachments.isNotEmpty &&
!_attachmentContainsImage
? null
: () {
setState(() {
_filePickerIndex = 0;
});
},
),
if (widget.allowedAttachmentTypes
.contains(DefaultAttachmentTypes.file))
IconButton(
iconSize: 32,
icon: StreamSvgIcon.files(
color: _getIconColor(1),
),
onPressed: messageInputController
.attachments.isNotEmpty &&
!_attachmentContainsFile
? null
: () {
widget
.onFilePicked(DefaultAttachmentTypes.file);
},
),
if (widget.allowedAttachmentTypes
.contains(DefaultAttachmentTypes.image))
IconButton(
icon: StreamSvgIcon.camera(
color: _getIconColor(2),
),
onPressed: attachmentLimitCrossed ||
(messageInputController
.attachments.isNotEmpty &&
!_attachmentContainsVideo)
? null
: () {
widget.onFilePicked(
DefaultAttachmentTypes.image,
camera: true,
);
},
),
if (widget.allowedAttachmentTypes
.contains(DefaultAttachmentTypes.video))
IconButton(
padding: const EdgeInsets.all(0),
icon: StreamSvgIcon.record(
color: _getIconColor(3),
),
onPressed: attachmentLimitCrossed ||
(messageInputController
.attachments.isNotEmpty &&
!_attachmentContainsVideo)
? null
: () {
widget.onFilePicked(
DefaultAttachmentTypes.video,
camera: true,
);
},
),
for (int i = 0;
i < widget.customAttachmentTypes.length;
i++)
IconButton(
onPressed: () {
if (messageInputController.attachments.isNotEmpty) {
if (!messageInputController.attachments.any((e) =>
e.type ==
widget.customAttachmentTypes[i].type)) {
return;
}
}
setState(() {
_filePickerIndex = i + 1;
});
},
icon: widget.customAttachmentTypes[i]
.iconBuilder(context, _filePickerIndex == i + 1),
),
],
),
DecoratedBox(
decoration: BoxDecoration(
color: _streamChatTheme.colorTheme.barsBg,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: Center(
child: Padding(
padding: const EdgeInsets.all(8),
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: _streamChatTheme.colorTheme.inputBg,
borderRadius: BorderRadius.circular(4),
),
),
),
),
),
if (widget.isOpen &&
(widget.allowedAttachmentTypes
.contains(DefaultAttachmentTypes.image) ||
(widget.allowedAttachmentTypes
.contains(DefaultAttachmentTypes.file))))
Expanded(
child: DecoratedBox(
decoration: BoxDecoration(
color: _streamChatTheme.colorTheme.barsBg,
borderRadius: BorderRadius.circular(8),
),
child: _PickerWidget(
filePickerIndex: _filePickerIndex,
streamChatTheme: _streamChatTheme,
containsFile: _attachmentContainsFile,
selectedMedias: messageInputController.attachments
.map((e) => e.id)
.toList(),
onAddMoreFilesClick: widget.onFilePicked,
onMediaSelected: (media) {
if (messageInputController.attachments
.any((e) => e.id == media.id)) {
setState(() => messageInputController.attachments
.removeWhere((e) => e.id == media.id));
} else {
_addAssetAttachment(media);
}
},
allowedAttachmentTypes: widget.allowedAttachmentTypes,
customAttachmentTypes: widget.customAttachmentTypes,
),
),
),
],
),
),
),
),
);
}
void _addAssetAttachment(AssetEntity medium) async {
final mediaFile = await medium.originFile.timeout(
const Duration(seconds: 5),
onTimeout: () => medium.originFile,
);
if (mediaFile == null) return;
var file = AttachmentFile(
path: mediaFile.path,
size: await mediaFile.length(),
bytes: mediaFile.readAsBytesSync(),
);
if (file.size! > widget.maxAttachmentSize) {
if (medium.type == AssetType.video && file.path != null) {
final mediaInfo = await (VideoService.compressVideo(
file.path!,
frameRate: widget.compressedVideoFrameRate,
quality: widget.compressedVideoQuality,
) as FutureOr<MediaInfo>);
if (mediaInfo.filesize! > widget.maxAttachmentSize) {
widget.onError?.call(
context.translations.fileTooLargeAfterCompressionError(
widget.maxAttachmentSize / (1024 * 1024),
),
);
return;
}
file = AttachmentFile(
name: file.name,
size: mediaInfo.filesize,
bytes: await mediaInfo.file?.readAsBytes(),
path: mediaInfo.path,
);
} else {
widget.onError?.call(context.translations.fileTooLargeError(
widget.maxAttachmentSize / (1024 * 1024),
));
return;
}
}
setState(() {
final attachment = Attachment(
id: medium.id,
file: file,
type: medium.type == AssetType.image ? 'image' : 'video',
);
_addAttachments([attachment]);
});
}
/// Adds an attachment to the [messageInputController.attachments] map
void _addAttachments(Iterable<Attachment> attachments) {
final limit = widget.attachmentLimit;
final length =
widget.messageInputController.attachments.length + attachments.length;
if (length > limit) {
final onAttachmentLimitExceed = widget.onAttachmentLimitExceeded;
if (onAttachmentLimitExceed != null) {
return onAttachmentLimitExceed(
widget.attachmentLimit,
context.translations.attachmentLimitExceedError(limit),
);
}
return widget.onError?.call(
context.translations.attachmentLimitExceedError(limit),
);
}
for (final attachment in attachments) {
widget.messageInputController.addAttachment(attachment);
}
}
}
class _PickerWidget extends StatefulWidget {
const _PickerWidget({
Key? key,
required this.filePickerIndex,
required this.containsFile,
required this.selectedMedias,
required this.onAddMoreFilesClick,
required this.onMediaSelected,
required this.streamChatTheme,
required this.allowedAttachmentTypes,
required this.customAttachmentTypes,
}) : super(key: key);
final int filePickerIndex;
final bool containsFile;
final List<String> selectedMedias;
final void Function(DefaultAttachmentTypes) onAddMoreFilesClick;
final void Function(AssetEntity) onMediaSelected;
final StreamChatThemeData streamChatTheme;
final List<DefaultAttachmentTypes> allowedAttachmentTypes;
final List<CustomAttachmentType> customAttachmentTypes;
@override
_PickerWidgetState createState() => _PickerWidgetState();
}
class _PickerWidgetState extends State<_PickerWidget> {
Future<bool>? requestPermission;
@override
void initState() {
super.initState();
requestPermission = PhotoManager.requestPermission();
}
@override
Widget build(BuildContext context) {
if (widget.filePickerIndex != 0) {
return widget.customAttachmentTypes[widget.filePickerIndex - 1]
.pickerBuilder(context);
}
return FutureBuilder<bool>(
future: requestPermission,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Offstage();
}
if (snapshot.data!) {
if (widget.containsFile ||
!widget.allowedAttachmentTypes
.contains(DefaultAttachmentTypes.image)) {
return GestureDetector(
onTap: () {
widget.onAddMoreFilesClick(DefaultAttachmentTypes.file);
},
child: Container(
constraints: const BoxConstraints.expand(),
color: widget.streamChatTheme.colorTheme.inputBg,
alignment: Alignment.center,
child: Text(
context.translations.addMoreFilesLabel,
style: TextStyle(
color: widget.streamChatTheme.colorTheme.accentPrimary,
fontWeight: FontWeight.bold,
),
),
),
);
}
return MediaListView(
selectedIds: widget.selectedMedias,
onSelect: widget.onMediaSelected,
);
}
return InkWell(
onTap: () async {
PhotoManager.openSetting();
},
child: Container(
color: widget.streamChatTheme.colorTheme.inputBg,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SvgPicture.asset(
'svgs/icon_picture_empty_state.svg',
package: 'stream_chat_flutter',
height: 140,
color: widget.streamChatTheme.colorTheme.disabled,
),
Text(
context.translations.enablePhotoAndVideoAccessMessage,
style: widget.streamChatTheme.textTheme.body.copyWith(
color: widget.streamChatTheme.colorTheme.textLowEmphasis,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 6),
Center(
child: Text(
context.translations.allowGalleryAccessMessage,
style: widget.streamChatTheme.textTheme.bodyBold.copyWith(
color: widget.streamChatTheme.colorTheme.accentPrimary,
),
),
),
],
),
),
);
},
);
}
}
class CustomAttachmentType {
String type;
CustomAttachmentIconBuilder iconBuilder;
WidgetBuilder pickerBuilder;
CustomAttachmentType({
required this.type,
required this.iconBuilder,
required this.pickerBuilder,
});
}
@@ -1 +1,98 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class StreamMessageSendButton extends StatelessWidget {
final int timeOut;
final bool isIdle;
final bool isCommandEnabled;
final bool isEditEnabled;
final Widget? idleSendButton;
final Widget? activeSendButton;
final VoidCallback onSendMessage;
const StreamMessageSendButton({
Key? key,
this.timeOut = 0,
this.isIdle = true,
this.isCommandEnabled = false,
this.isEditEnabled = false,
this.idleSendButton,
this.activeSendButton,
required this.onSendMessage,
}) : super(key: key);
@override
Widget build(BuildContext context) {
var _streamChatTheme = StreamChatTheme.of(context);
late Widget sendButton;
if (timeOut > 0) {
sendButton = CountdownButton(count: timeOut);
} else if (isIdle) {
sendButton = idleSendButton ?? _buildIdleSendButton(context);
} else {
sendButton = activeSendButton != null
? InkWell(
onTap: onSendMessage,
child: activeSendButton,
)
: _buildSendButton(context);
}
return AnimatedSwitcher(
duration: _streamChatTheme.messageInputTheme.sendAnimationDuration!,
child: sendButton,
);
}
Widget _buildIdleSendButton(BuildContext context) {
var _messageInputTheme = MessageInputTheme.of(context);
return Padding(
padding: const EdgeInsets.all(8),
child: StreamSvgIcon(
assetName: _getIdleSendIcon(),
color: _messageInputTheme.sendButtonIdleColor,
),
);
}
Widget _buildSendButton(BuildContext context) {
var _messageInputTheme = MessageInputTheme.of(context);
return Padding(
padding: const EdgeInsets.all(8),
child: IconButton(
onPressed: onSendMessage,
padding: const EdgeInsets.all(0),
splashRadius: 24,
constraints: const BoxConstraints.tightFor(
height: 24,
width: 24,
),
icon: StreamSvgIcon(
assetName: _getSendIcon(),
color: _messageInputTheme.sendButtonColor,
),
),
);
}
String _getIdleSendIcon() {
if (isCommandEnabled) {
return 'Icon_search.svg';
} else {
return 'Icon_circle_right.svg';
}
}
String _getSendIcon() {
if (isEditEnabled) {
return 'Icon_circle_up.svg';
} else if (isCommandEnabled) {
return 'Icon_search.svg';
} else {
return 'Icon_circle_up.svg';
}
}
}
@@ -28,6 +28,9 @@ export 'src/message_search_item.dart';
export 'src/message_search_list_view.dart';
export 'src/message_text.dart';
export 'src/message_widget.dart';
export 'src/mip/countdown_button.dart';
export 'src/mip/stream_attachment_picker.dart';
export 'src/mip/stream_message_send_button.dart';
export 'src/mip/stream_message_text_field.dart';
export 'src/option_list_tile.dart';
export 'src/reaction_icon.dart';
+7 -8
View File
@@ -1,7 +1,7 @@
name: stream_chat_flutter
homepage: https://github.com/GetStream/stream-chat-flutter
description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter.
version: 3.1.1
version: 3.2.0
repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -23,7 +23,7 @@ dependencies:
flutter_markdown: ^0.6.1
flutter_portal: ^0.4.0
flutter_slidable: ^0.6.0
flutter_svg: ^0.22.0
flutter_svg: ^0.23.0+1
http_parser: ^4.0.0
image_gallery_saver: ^1.7.0
image_picker: ^0.8.2
@@ -32,12 +32,11 @@ dependencies:
meta: ^1.3.0
path_provider: ^2.0.1
photo_manager: ^1.2.6+1
photo_view: ^0.12.0
photo_view: ^0.13.0
rxdart: ^0.27.0
scrollable_positioned_list: ^0.2.0-nullsafety.0
share_plus: ^2.0.3
share_plus: ^3.0.4
shimmer: ^2.0.0
stream_chat_flutter_core: ^3.1.1
stream_chat_flutter_core: ^3.2.0
substring_highlight: ^1.0.26
synchronized: ^3.0.0
url_launcher: ^6.0.3
@@ -58,6 +57,6 @@ dev_dependencies:
dart_code_metrics: ^4.4.0
flutter_test:
sdk: flutter
golden_toolkit: ^0.10.0
mocktail: ^0.1.2
golden_toolkit: ^0.11.0
mocktail: ^0.2.0
path: ^1.8.0
@@ -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));
});
}
@@ -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);
});
}
@@ -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);
});
}
@@ -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)));
});
}
@@ -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;
@@ -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;
@@ -0,0 +1,220 @@
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart';
const screenHeight = 400.0;
const screenWidth = 400.0;
const itemWidth = screenWidth / 10.0;
const separatorWidth = screenWidth / 20.0;
const itemCount = 500;
const scrollDuration = Duration(seconds: 1);
const tolerance = 10e-5;
void main() {
Future<void> setUpWidgetTest(
WidgetTester tester, {
ItemScrollController? itemScrollController,
ItemPositionsListener? itemPositionsListener,
bool reverse = false,
EdgeInsets? padding,
int initialScrollIndex = 0,
}) async {
tester.binding.window.devicePixelRatioTestValue = 1.0;
tester.binding.window.physicalSizeTestValue =
const Size(screenWidth, screenHeight);
await tester.pumpWidget(
MaterialApp(
home: ScrollablePositionedList.separated(
itemCount: itemCount,
itemScrollController: itemScrollController,
itemBuilder: (context, index) => SizedBox(
width: itemWidth,
child: Text('Item $index'),
),
separatorBuilder: (context, index) => SizedBox(
width: separatorWidth,
child: Text('Separator $index'),
),
itemPositionsListener: itemPositionsListener,
scrollDirection: Axis.horizontal,
reverse: reverse,
padding: padding,
initialScrollIndex: initialScrollIndex,
),
),
);
}
testWidgets('List positioned with 0 at left', (WidgetTester tester) async {
final itemPositionsListener = ItemPositionsListener.create();
await setUpWidgetTest(tester, itemPositionsListener: itemPositionsListener);
expect(tester.getTopLeft(find.text('Item 0')).dx, 0);
expect(tester.getBottomLeft(find.text('Item 1')).dx,
itemWidth + separatorWidth);
expect(
itemPositionsListener.itemPositions.value
.firstWhere((position) => position.index == 0)
.itemLeadingEdge,
0);
expect(
itemPositionsListener.itemPositions.value
.firstWhere((position) => position.index == 1)
.itemLeadingEdge,
_screenProportion(numberOfItems: 1, numberOfSeparators: 1));
});
testWidgets('Scroll to 2 (already on screen)', (WidgetTester tester) async {
final itemScrollController = ItemScrollController();
final itemPositionsListener = ItemPositionsListener.create();
await setUpWidgetTest(tester,
itemScrollController: itemScrollController,
itemPositionsListener: itemPositionsListener);
unawaited(
itemScrollController.scrollTo(index: 2, duration: scrollDuration));
await tester.pump();
await tester.pump(scrollDuration);
expect(find.text('Item 1'), findsNothing);
expect(tester.getTopLeft(find.text('Item 2')).dx, 0);
expect(
tester.getTopLeft(find.text('Item 3')).dx, itemWidth + separatorWidth);
expect(
itemPositionsListener.itemPositions.value
.firstWhere((position) => position.index == 2)
.itemLeadingEdge,
0);
expect(
itemPositionsListener.itemPositions.value
.firstWhere((position) => position.index == 3)
.itemLeadingEdge,
_screenProportion(numberOfItems: 1, numberOfSeparators: 1));
});
testWidgets('Scroll to 100 (not already on screen)',
(WidgetTester tester) async {
final itemScrollController = ItemScrollController();
final itemPositionsListener = ItemPositionsListener.create();
await setUpWidgetTest(tester,
itemScrollController: itemScrollController,
itemPositionsListener: itemPositionsListener);
unawaited(
itemScrollController.scrollTo(index: 100, duration: scrollDuration));
await tester.pumpAndSettle();
expect(find.text('Item 99'), findsNothing);
expect(find.text('Item 100'), findsOneWidget);
expect(
itemPositionsListener.itemPositions.value
.firstWhere((position) => position.index == 100)
.itemLeadingEdge,
0);
expect(
itemPositionsListener.itemPositions.value
.firstWhere((position) => position.index == 101)
.itemLeadingEdge,
_screenProportion(numberOfItems: 1, numberOfSeparators: 1));
});
testWidgets('Jump to 100', (WidgetTester tester) async {
final itemScrollController = ItemScrollController();
final itemPositionsListener = ItemPositionsListener.create();
await setUpWidgetTest(tester,
itemScrollController: itemScrollController,
itemPositionsListener: itemPositionsListener);
itemScrollController.jumpTo(index: 100);
await tester.pumpAndSettle();
expect(tester.getTopLeft(find.text('Item 100')).dx, 0);
expect(tester.getTopLeft(find.text('Item 101')).dx,
itemWidth + separatorWidth);
expect(
itemPositionsListener.itemPositions.value
.firstWhere((position) => position.index == 100)
.itemLeadingEdge,
0);
expect(
itemPositionsListener.itemPositions.value
.firstWhere((position) => position.index == 101)
.itemLeadingEdge,
_screenProportion(numberOfItems: 1, numberOfSeparators: 1));
});
testWidgets('padding test - centered sliver at left',
(WidgetTester tester) async {
final itemScrollController = ItemScrollController();
await setUpWidgetTest(
tester,
itemScrollController: itemScrollController,
padding: const EdgeInsets.all(10),
);
expect(tester.getTopLeft(find.text('Item 0')), const Offset(10, 10));
expect(tester.getTopLeft(find.text('Item 1')),
const Offset(itemWidth + 10 + separatorWidth, 10));
expect(tester.getBottomRight(find.text('Item 1')),
const Offset(10 + itemWidth * 2 + separatorWidth, screenHeight - 10));
unawaited(
itemScrollController.scrollTo(index: 494, duration: scrollDuration));
await tester.pumpAndSettle();
await tester.drag(
find.byType(ScrollablePositionedList), const Offset(-500, 0));
await tester.pumpAndSettle();
expect(tester.getBottomRight(find.text('Item 499')),
const Offset(screenWidth - 10, screenHeight - 10));
});
testWidgets('padding test - centered sliver not at left',
(WidgetTester tester) async {
final itemScrollController = ItemScrollController();
final itemPositionsListener = ItemPositionsListener.create();
await setUpWidgetTest(
tester,
itemScrollController: itemScrollController,
itemPositionsListener: itemPositionsListener,
initialScrollIndex: 2,
padding: const EdgeInsets.all(10),
);
await tester.drag(
find.byType(ScrollablePositionedList), const Offset(300, 0));
await tester.pumpAndSettle();
expect(tester.getTopLeft(find.text('Item 0')), const Offset(10, 10));
expect(tester.getTopLeft(find.text('Item 2')),
const Offset(10 + 2 * (itemWidth + separatorWidth), 10));
expect(tester.getTopLeft(find.text('Item 3')),
const Offset(10 + 3 * (itemWidth + separatorWidth), 10));
expect(
itemPositionsListener.itemPositions.value
.firstWhere((position) => position.index == 2)
.itemLeadingEdge,
closeTo(
10 / screenWidth + 2 * ((itemWidth + separatorWidth) / screenWidth),
tolerance));
});
}
double _screenProportion(
{required double numberOfItems, required double numberOfSeparators}) =>
(numberOfItems * itemWidth + numberOfSeparators * separatorWidth) /
screenHeight;
@@ -1,3 +1,11 @@
✅ Added
- Added `MessageInputController` to hold `Message` related data.
## 3.2.0
- Updated `stream_chat` dependency to [`3.2.0`](https://pub.dev/packages/stream_chat/changelog).
## 3.1.1
- Updated `stream_chat` dependency to [`3.1.1`](https://pub.dev/packages/stream_chat/changelog).
@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:stream_chat/stream_chat.dart';
/// A Controller for storing and handling the [Message]
/// Controller for storing and mutating a [Message] value.
class MessageInputController extends ValueNotifier<Message> {
/// Creates a controller for an editable text field.
///
@@ -17,7 +17,8 @@ class MessageInputController extends ValueNotifier<Message> {
factory MessageInputController.fromText(String? text) =>
MessageInputController._(Message(text: text));
/// Creates a controller for an editable textfield from initial [attachments].
/// Creates a controller for an editable text field from an initial
/// [attachments].
factory MessageInputController.fromAttachments(
List<Attachment> attachments,
) =>
@@ -136,8 +137,9 @@ class MessageInputController extends ValueNotifier<Message> {
/// After calling this function, [text], [attachments] and [mentionedUsers]
/// all will be empty.
///
/// Calling this will notify the listeners of this [MessageInputController]
/// that they need to update (it calls [notifyListeners]). For this reason,
/// Calling this will notify all the listeners of this
/// [MessageInputController] that they need to update
/// (it calls [notifyListeners]). For this reason,
/// this method should only be called between frames, e.g. in response to user
/// actions, not during the build, layout, or paint phases.
void clear() {
@@ -9,6 +9,14 @@ import 'package:stream_chat_flutter_core/src/better_stream_builder.dart';
import 'package:stream_chat_flutter_core/src/stream_channel.dart';
import 'package:stream_chat_flutter_core/src/typedef.dart';
/// Default filter for the message list
bool Function(Message) defaultMessageFilter(String currentUserId) =>
(Message m) {
final isMyMessage = m.user?.id == currentUserId;
if (m.shadowed && !isMyMessage) return false;
return true;
};
/// [MessageListCore] is a simplified class that allows fetching a list of
/// messages while exposing UI builders.
///
@@ -132,25 +140,20 @@ class MessageListCoreState extends State<MessageListCore> {
? _streamChannel!.channel.state?.threads[widget.parentMessage!.id]
: _streamChannel!.channel.state?.messages;
bool defaultFilter(Message m) {
final isMyMessage = m.user?.id == _currentUser?.id;
if (m.shadowed && !isMyMessage) return false;
return true;
}
return BetterStreamBuilder<List<Message>>(
initialData: initialData,
comparator: const ListEquality().equals,
stream: messagesStream!.map(
(messages) =>
messages?.where(widget.messageFilter ?? defaultFilter).toList(
growable: false,
),
),
stream: messagesStream,
errorBuilder: widget.errorBuilder,
noDataBuilder: widget.loadingBuilder,
builder: (context, data) {
final messageList = data.reversed.toList(growable: false);
final messageList = data
.where(
widget.messageFilter ?? defaultMessageFilter(_currentUser!.id),
)
.toList(growable: false)
.reversed
.toList(growable: false);
if (messageList.isEmpty && !_isThreadConversation) {
if (_upToDate) {
return widget.emptyBuilder(context);
@@ -1,7 +1,7 @@
name: stream_chat_flutter_core
homepage: https://github.com/GetStream/stream-chat-flutter
description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter.
version: 3.1.1
version: 3.2.0
repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -11,17 +11,17 @@ environment:
dependencies:
collection: ^1.15.0
connectivity_plus: ^1.0.1
connectivity_plus: ^2.0.2
flutter:
sdk: flutter
meta: ^1.3.0
rxdart: ^0.27.0
stream_chat: ^3.1.1
stream_chat: ^3.2.0
dev_dependencies:
dart_code_metrics: ^4.4.0
fake_async: ^1.2.0
flutter_test:
sdk: flutter
mocktail: ^0.1.3
mocktail: ^0.2.0
@@ -10,7 +10,7 @@ import 'mocks.dart';
void main() {
setUpAll(() {
registerFallbackValue<PaginationParams>(const PaginationParams());
registerFallbackValue(const PaginationParams());
});
List<Channel> _generateChannels(
+1 -1
View File
@@ -1,7 +1,7 @@
targets:
$default:
builders:
moor_generator:
drift_dev:
options:
generate_connect_constructor: true
data_class_to_companions: false
@@ -1,6 +1,6 @@
import 'dart:convert';
import 'package:moor/moor.dart';
import 'package:drift/drift.dart';
/// Maps a [List] of type [T] into a [String] understood
/// by the sqlite backend.
@@ -1,6 +1,6 @@
import 'dart:convert';
import 'package:moor/moor.dart';
import 'package:drift/drift.dart';
/// Maps a [Map] of type [String], [T] into a [String] understood
/// by the sqlite backend.
@@ -1,4 +1,4 @@
import 'package:moor/moor.dart';
import 'package:drift/drift.dart';
import 'package:stream_chat/stream_chat.dart';
/// Maps a [MessageSendingStatus] into a [int] understood
@@ -1,6 +1,6 @@
import 'package:moor/moor.dart';
import 'package:drift/drift.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/db/drift_chat_database.dart';
import 'package:stream_chat_persistence/src/entity/channels.dart';
import 'package:stream_chat_persistence/src/entity/users.dart';
import 'package:stream_chat_persistence/src/mapper/mapper.dart';
@@ -8,11 +8,11 @@ import 'package:stream_chat_persistence/src/mapper/mapper.dart';
part 'channel_dao.g.dart';
/// The Data Access Object for operations in [Channels] table.
@UseDao(tables: [Channels, Users])
class ChannelDao extends DatabaseAccessor<MoorChatDatabase>
@DriftAccessor(tables: [Channels, Users])
class ChannelDao extends DatabaseAccessor<DriftChatDatabase>
with _$ChannelDaoMixin {
/// Creates a new channel dao instance
ChannelDao(MoorChatDatabase db) : super(db);
ChannelDao(DriftChatDatabase db) : super(db);
/// Get channel by cid
Future<ChannelModel?> getChannelByCid(String cid) async =>
@@ -6,7 +6,7 @@ part of 'channel_dao.dart';
// DaoGenerator
// **************************************************************************
mixin _$ChannelDaoMixin on DatabaseAccessor<MoorChatDatabase> {
mixin _$ChannelDaoMixin on DatabaseAccessor<DriftChatDatabase> {
$ChannelsTable get channels => attachedDatabase.channels;
$UsersTable get users => attachedDatabase.users;
}
@@ -1,8 +1,8 @@
import 'dart:convert';
import 'package:moor/moor.dart';
import 'package:drift/drift.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/db/drift_chat_database.dart';
import 'package:stream_chat_persistence/src/entity/channel_queries.dart';
import 'package:stream_chat_persistence/src/entity/channels.dart';
import 'package:stream_chat_persistence/src/entity/users.dart';
@@ -11,11 +11,11 @@ import 'package:stream_chat_persistence/src/mapper/mapper.dart';
part 'channel_query_dao.g.dart';
/// The Data Access Object for operations in [ChannelQueries] table.
@UseDao(tables: [ChannelQueries, Channels, Users])
class ChannelQueryDao extends DatabaseAccessor<MoorChatDatabase>
@DriftAccessor(tables: [ChannelQueries, Channels, Users])
class ChannelQueryDao extends DatabaseAccessor<DriftChatDatabase>
with _$ChannelQueryDaoMixin {
/// Creates a new channel query dao instance
ChannelQueryDao(MoorChatDatabase db) : super(db);
ChannelQueryDao(DriftChatDatabase db) : super(db);
String _computeHash(Filter? filter) {
if (filter == null) {
@@ -6,7 +6,7 @@ part of 'channel_query_dao.dart';
// DaoGenerator
// **************************************************************************
mixin _$ChannelQueryDaoMixin on DatabaseAccessor<MoorChatDatabase> {
mixin _$ChannelQueryDaoMixin on DatabaseAccessor<DriftChatDatabase> {
$ChannelQueriesTable get channelQueries => attachedDatabase.channelQueries;
$ChannelsTable get channels => attachedDatabase.channels;
$UsersTable get users => attachedDatabase.users;
@@ -1,6 +1,6 @@
import 'package:moor/moor.dart';
import 'package:drift/drift.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/db/drift_chat_database.dart';
import 'package:stream_chat_persistence/src/entity/connection_events.dart';
import 'package:stream_chat_persistence/src/mapper/mapper.dart';
@@ -8,11 +8,11 @@ import 'package:stream_chat_persistence/src/mapper/mapper.dart';
part 'connection_event_dao.g.dart';
/// The Data Access Object for operations in [ConnectionEvents] table.
@UseDao(tables: [ConnectionEvents])
class ConnectionEventDao extends DatabaseAccessor<MoorChatDatabase>
@DriftAccessor(tables: [ConnectionEvents])
class ConnectionEventDao extends DatabaseAccessor<DriftChatDatabase>
with _$ConnectionEventDaoMixin {
/// Creates a new connection event dao instance
ConnectionEventDao(MoorChatDatabase db) : super(db);
ConnectionEventDao(DriftChatDatabase db) : super(db);
/// Get the latest stored connection event
Future<Event?> get connectionEvent => select(connectionEvents)
@@ -6,7 +6,7 @@ part of 'connection_event_dao.dart';
// DaoGenerator
// **************************************************************************
mixin _$ConnectionEventDaoMixin on DatabaseAccessor<MoorChatDatabase> {
mixin _$ConnectionEventDaoMixin on DatabaseAccessor<DriftChatDatabase> {
$ConnectionEventsTable get connectionEvents =>
attachedDatabase.connectionEvents;
}
@@ -1,6 +1,6 @@
import 'package:moor/moor.dart';
import 'package:drift/drift.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/db/drift_chat_database.dart';
import 'package:stream_chat_persistence/src/entity/members.dart';
import 'package:stream_chat_persistence/src/entity/users.dart';
@@ -10,11 +10,11 @@ import 'package:stream_chat_persistence/src/mapper/mapper.dart';
part 'member_dao.g.dart';
/// The Data Access Object for operations in [Members] table.
@UseDao(tables: [Members, Users])
class MemberDao extends DatabaseAccessor<MoorChatDatabase>
@DriftAccessor(tables: [Members, Users])
class MemberDao extends DatabaseAccessor<DriftChatDatabase>
with _$MemberDaoMixin {
/// Creates a new member dao instance
MemberDao(MoorChatDatabase db) : super(db);
MemberDao(DriftChatDatabase db) : super(db);
/// Get all members where [Members.channelCid] matches [cid]
Future<List<Member>> getMembersByCid(String cid) async =>
@@ -6,7 +6,7 @@ part of 'member_dao.dart';
// DaoGenerator
// **************************************************************************
mixin _$MemberDaoMixin on DatabaseAccessor<MoorChatDatabase> {
mixin _$MemberDaoMixin on DatabaseAccessor<DriftChatDatabase> {
$MembersTable get members => attachedDatabase.members;
$UsersTable get users => attachedDatabase.users;
}
@@ -1,6 +1,6 @@
import 'package:moor/moor.dart';
import 'package:drift/drift.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/db/drift_chat_database.dart';
import 'package:stream_chat_persistence/src/entity/messages.dart';
import 'package:stream_chat_persistence/src/entity/users.dart';
@@ -9,13 +9,13 @@ import 'package:stream_chat_persistence/src/mapper/mapper.dart';
part 'message_dao.g.dart';
/// The Data Access Object for operations in [Messages] table.
@UseDao(tables: [Messages, Users])
class MessageDao extends DatabaseAccessor<MoorChatDatabase>
@DriftAccessor(tables: [Messages, Users])
class MessageDao extends DatabaseAccessor<DriftChatDatabase>
with _$MessageDaoMixin {
/// Creates a new message dao instance
MessageDao(this._db) : super(_db);
final MoorChatDatabase _db;
final DriftChatDatabase _db;
$UsersTable get _users => alias(users, 'users');
@@ -6,7 +6,7 @@ part of 'message_dao.dart';
// DaoGenerator
// **************************************************************************
mixin _$MessageDaoMixin on DatabaseAccessor<MoorChatDatabase> {
mixin _$MessageDaoMixin on DatabaseAccessor<DriftChatDatabase> {
$MessagesTable get messages => attachedDatabase.messages;
$UsersTable get users => attachedDatabase.users;
}
@@ -1,6 +1,6 @@
import 'package:moor/moor.dart';
import 'package:drift/drift.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/db/drift_chat_database.dart';
import 'package:stream_chat_persistence/src/entity/pinned_messages.dart';
import 'package:stream_chat_persistence/src/entity/users.dart';
@@ -9,13 +9,13 @@ import 'package:stream_chat_persistence/src/mapper/mapper.dart';
part 'pinned_message_dao.g.dart';
/// The Data Access Object for operations in [Messages] table.
@UseDao(tables: [PinnedMessages, Users])
class PinnedMessageDao extends DatabaseAccessor<MoorChatDatabase>
@DriftAccessor(tables: [PinnedMessages, Users])
class PinnedMessageDao extends DatabaseAccessor<DriftChatDatabase>
with _$PinnedMessageDaoMixin {
/// Creates a new message dao instance
PinnedMessageDao(this._db) : super(_db);
final MoorChatDatabase _db;
final DriftChatDatabase _db;
$UsersTable get _users => alias(users, 'users');
@@ -6,7 +6,7 @@ part of 'pinned_message_dao.dart';
// DaoGenerator
// **************************************************************************
mixin _$PinnedMessageDaoMixin on DatabaseAccessor<MoorChatDatabase> {
mixin _$PinnedMessageDaoMixin on DatabaseAccessor<DriftChatDatabase> {
$PinnedMessagesTable get pinnedMessages => attachedDatabase.pinnedMessages;
$UsersTable get users => attachedDatabase.users;
}
@@ -1,6 +1,6 @@
import 'package:moor/moor.dart';
import 'package:drift/drift.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/db/drift_chat_database.dart';
import 'package:stream_chat_persistence/src/entity/pinned_message_reactions.dart';
import 'package:stream_chat_persistence/src/entity/users.dart';
import 'package:stream_chat_persistence/src/mapper/mapper.dart';
@@ -8,11 +8,11 @@ import 'package:stream_chat_persistence/src/mapper/mapper.dart';
part 'pinned_message_reaction_dao.g.dart';
/// The Data Access Object for operations in [PinnedMessageReactions] table.
@UseDao(tables: [PinnedMessageReactions, Users])
class PinnedMessageReactionDao extends DatabaseAccessor<MoorChatDatabase>
@DriftAccessor(tables: [PinnedMessageReactions, Users])
class PinnedMessageReactionDao extends DatabaseAccessor<DriftChatDatabase>
with _$PinnedMessageReactionDaoMixin {
/// Creates a new reaction dao instance
PinnedMessageReactionDao(MoorChatDatabase db) : super(db);
PinnedMessageReactionDao(DriftChatDatabase db) : super(db);
/// Returns all the reactions of a particular message by matching
/// [Reactions.messageId] with [messageId]
@@ -6,7 +6,7 @@ part of 'pinned_message_reaction_dao.dart';
// DaoGenerator
// **************************************************************************
mixin _$PinnedMessageReactionDaoMixin on DatabaseAccessor<MoorChatDatabase> {
mixin _$PinnedMessageReactionDaoMixin on DatabaseAccessor<DriftChatDatabase> {
$PinnedMessageReactionsTable get pinnedMessageReactions =>
attachedDatabase.pinnedMessageReactions;
$UsersTable get users => attachedDatabase.users;
@@ -1,6 +1,6 @@
import 'package:moor/moor.dart';
import 'package:drift/drift.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/db/drift_chat_database.dart';
import 'package:stream_chat_persistence/src/entity/reactions.dart';
import 'package:stream_chat_persistence/src/entity/users.dart';
import 'package:stream_chat_persistence/src/mapper/mapper.dart';
@@ -8,11 +8,11 @@ import 'package:stream_chat_persistence/src/mapper/mapper.dart';
part 'reaction_dao.g.dart';
/// The Data Access Object for operations in [Reactions] table.
@UseDao(tables: [Reactions, Users])
class ReactionDao extends DatabaseAccessor<MoorChatDatabase>
@DriftAccessor(tables: [Reactions, Users])
class ReactionDao extends DatabaseAccessor<DriftChatDatabase>
with _$ReactionDaoMixin {
/// Creates a new reaction dao instance
ReactionDao(MoorChatDatabase db) : super(db);
ReactionDao(DriftChatDatabase db) : super(db);
/// Returns all the reactions of a particular message by matching
/// [Reactions.messageId] with [messageId]
@@ -6,7 +6,7 @@ part of 'reaction_dao.dart';
// DaoGenerator
// **************************************************************************
mixin _$ReactionDaoMixin on DatabaseAccessor<MoorChatDatabase> {
mixin _$ReactionDaoMixin on DatabaseAccessor<DriftChatDatabase> {
$ReactionsTable get reactions => attachedDatabase.reactions;
$UsersTable get users => attachedDatabase.users;
}
@@ -1,6 +1,6 @@
import 'package:moor/moor.dart';
import 'package:drift/drift.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/db/drift_chat_database.dart';
import 'package:stream_chat_persistence/src/entity/reads.dart';
import 'package:stream_chat_persistence/src/entity/users.dart';
import 'package:stream_chat_persistence/src/mapper/mapper.dart';
@@ -8,10 +8,10 @@ import 'package:stream_chat_persistence/src/mapper/mapper.dart';
part 'read_dao.g.dart';
/// The Data Access Object for operations in [Reads] table.
@UseDao(tables: [Reads, Users])
class ReadDao extends DatabaseAccessor<MoorChatDatabase> with _$ReadDaoMixin {
@DriftAccessor(tables: [Reads, Users])
class ReadDao extends DatabaseAccessor<DriftChatDatabase> with _$ReadDaoMixin {
/// Creates a new read dao instance
ReadDao(MoorChatDatabase db) : super(db);
ReadDao(DriftChatDatabase db) : super(db);
/// Get all reads where [Reads.channelCid] matches [cid]
Future<List<Read>> getReadsByCid(String cid) async => (select(reads).join([
@@ -6,7 +6,7 @@ part of 'read_dao.dart';
// DaoGenerator
// **************************************************************************
mixin _$ReadDaoMixin on DatabaseAccessor<MoorChatDatabase> {
mixin _$ReadDaoMixin on DatabaseAccessor<DriftChatDatabase> {
$ReadsTable get reads => attachedDatabase.reads;
$UsersTable get users => attachedDatabase.users;
}
@@ -1,16 +1,16 @@
import 'package:moor/moor.dart';
import 'package:drift/drift.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/db/drift_chat_database.dart';
import 'package:stream_chat_persistence/src/entity/users.dart';
import 'package:stream_chat_persistence/src/mapper/user_mapper.dart';
part 'user_dao.g.dart';
/// The Data Access Object for operations in [Users] table.
@UseDao(tables: [Users])
class UserDao extends DatabaseAccessor<MoorChatDatabase> with _$UserDaoMixin {
@DriftAccessor(tables: [Users])
class UserDao extends DatabaseAccessor<DriftChatDatabase> with _$UserDaoMixin {
/// Creates a new user dao instance
UserDao(MoorChatDatabase db) : super(db);
UserDao(DriftChatDatabase db) : super(db);
/// Updates the users data with the new [userList] data
Future<void> updateUsers(List<User> userList) => batch(
@@ -6,6 +6,6 @@ part of 'user_dao.dart';
// DaoGenerator
// **************************************************************************
mixin _$UserDaoMixin on DatabaseAccessor<MoorChatDatabase> {
mixin _$UserDaoMixin on DatabaseAccessor<DriftChatDatabase> {
$UsersTable get users => attachedDatabase.users;
}
@@ -1,4 +1,4 @@
import 'package:moor/moor.dart';
import 'package:drift/drift.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_persistence/src/converter/converter.dart';
@@ -7,10 +7,10 @@ import 'package:stream_chat_persistence/src/entity/entity.dart';
export 'shared/shared_db.dart';
part 'moor_chat_database.g.dart';
part 'drift_chat_database.g.dart';
/// A chat database implemented using moor
@UseMoor(
@DriftDatabase(
tables: [
Channels,
Messages,
@@ -36,15 +36,15 @@ part 'moor_chat_database.g.dart';
ConnectionEventDao,
],
)
class MoorChatDatabase extends _$MoorChatDatabase {
class DriftChatDatabase extends _$DriftChatDatabase {
/// Creates a new moor chat database instance
MoorChatDatabase(
DriftChatDatabase(
this._userId,
QueryExecutor executor,
) : super(executor);
/// Instantiate a new database instance
MoorChatDatabase.connect(
DriftChatDatabase.connect(
this._userId,
DatabaseConnection connection,
) : super.connect(connection);
@@ -1,6 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'moor_chat_database.dart';
part of 'drift_chat_database.dart';
// **************************************************************************
// MoorGenerator
@@ -56,9 +56,7 @@ class ChannelEntity extends DataClass implements Insertable<ChannelEntity> {
required this.memberCount,
this.createdById,
this.extraData});
factory ChannelEntity.fromData(
Map<String, dynamic> data, GeneratedDatabase db,
{String? prefix}) {
factory ChannelEntity.fromData(Map<String, dynamic> data, {String? prefix}) {
final effectivePrefix = prefix ?? '';
return ChannelEntity(
id: const StringType()
@@ -119,7 +117,7 @@ class ChannelEntity extends DataClass implements Insertable<ChannelEntity> {
factory ChannelEntity.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
serializer ??= driftRuntimeOptions.defaultSerializer;
return ChannelEntity(
id: serializer.fromJson<String>(json['id']),
type: serializer.fromJson<String>(json['type']),
@@ -137,7 +135,7 @@ class ChannelEntity extends DataClass implements Insertable<ChannelEntity> {
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<String>(id),
'type': serializer.toJson<String>(type),
@@ -202,28 +200,8 @@ class ChannelEntity extends DataClass implements Insertable<ChannelEntity> {
}
@override
int get hashCode => $mrjf($mrjc(
id.hashCode,
$mrjc(
type.hashCode,
$mrjc(
cid.hashCode,
$mrjc(
config.hashCode,
$mrjc(
frozen.hashCode,
$mrjc(
lastMessageAt.hashCode,
$mrjc(
createdAt.hashCode,
$mrjc(
updatedAt.hashCode,
$mrjc(
deletedAt.hashCode,
$mrjc(
memberCount.hashCode,
$mrjc(createdById.hashCode,
extraData.hashCode))))))))))));
int get hashCode => Object.hash(id, type, cid, config, frozen, lastMessageAt,
createdAt, updatedAt, deletedAt, memberCount, createdById, extraData);
@override
bool operator ==(Object other) =>
identical(this, other) ||
@@ -561,7 +539,7 @@ class $ChannelsTable extends Channels
Set<GeneratedColumn> get $primaryKey => {cid};
@override
ChannelEntity map(Map<String, dynamic> data, {String? tablePrefix}) {
return ChannelEntity.fromData(data, _db,
return ChannelEntity.fromData(data,
prefix: tablePrefix != null ? '$tablePrefix.' : null);
}
@@ -678,9 +656,7 @@ class MessageEntity extends DataClass implements Insertable<MessageEntity> {
required this.channelCid,
this.i18n,
this.extraData});
factory MessageEntity.fromData(
Map<String, dynamic> data, GeneratedDatabase db,
{String? prefix}) {
factory MessageEntity.fromData(Map<String, dynamic> data, {String? prefix}) {
final effectivePrefix = prefix ?? '';
return MessageEntity(
id: const StringType()
@@ -814,7 +790,7 @@ class MessageEntity extends DataClass implements Insertable<MessageEntity> {
factory MessageEntity.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
serializer ??= driftRuntimeOptions.defaultSerializer;
return MessageEntity(
id: serializer.fromJson<String>(json['id']),
messageText: serializer.fromJson<String?>(json['messageText']),
@@ -847,7 +823,7 @@ class MessageEntity extends DataClass implements Insertable<MessageEntity> {
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<String>(id),
'messageText': serializer.toJson<String?>(messageText),
@@ -969,49 +945,33 @@ class MessageEntity extends DataClass implements Insertable<MessageEntity> {
}
@override
int get hashCode => $mrjf($mrjc(
id.hashCode,
$mrjc(
messageText.hashCode,
$mrjc(
attachments.hashCode,
$mrjc(
status.hashCode,
$mrjc(
type.hashCode,
$mrjc(
mentionedUsers.hashCode,
$mrjc(
reactionCounts.hashCode,
$mrjc(
reactionScores.hashCode,
$mrjc(
parentId.hashCode,
$mrjc(
quotedMessageId.hashCode,
$mrjc(
replyCount.hashCode,
$mrjc(
showInChannel.hashCode,
$mrjc(
shadowed.hashCode,
$mrjc(
command.hashCode,
$mrjc(
createdAt
.hashCode,
$mrjc(
updatedAt
.hashCode,
$mrjc(
deletedAt
.hashCode,
$mrjc(
userId
.hashCode,
$mrjc(
pinned.hashCode,
$mrjc(pinnedAt.hashCode, $mrjc(pinExpires.hashCode, $mrjc(pinnedByUserId.hashCode, $mrjc(channelCid.hashCode, $mrjc(i18n.hashCode, extraData.hashCode)))))))))))))))))))))))));
int get hashCode => Object.hashAll([
id,
messageText,
attachments,
status,
type,
mentionedUsers,
reactionCounts,
reactionScores,
parentId,
quotedMessageId,
replyCount,
showInChannel,
shadowed,
command,
createdAt,
updatedAt,
deletedAt,
userId,
pinned,
pinnedAt,
pinExpires,
pinnedByUserId,
channelCid,
i18n,
extraData
]);
@override
bool operator ==(Object other) =>
identical(this, other) ||
@@ -1644,7 +1604,7 @@ class $MessagesTable extends Messages
Set<GeneratedColumn> get $primaryKey => {id};
@override
MessageEntity map(Map<String, dynamic> data, {String? tablePrefix}) {
return MessageEntity.fromData(data, _db,
return MessageEntity.fromData(data,
prefix: tablePrefix != null ? '$tablePrefix.' : null);
}
@@ -1772,8 +1732,7 @@ class PinnedMessageEntity extends DataClass
required this.channelCid,
this.i18n,
this.extraData});
factory PinnedMessageEntity.fromData(
Map<String, dynamic> data, GeneratedDatabase db,
factory PinnedMessageEntity.fromData(Map<String, dynamic> data,
{String? prefix}) {
final effectivePrefix = prefix ?? '';
return PinnedMessageEntity(
@@ -1911,7 +1870,7 @@ class PinnedMessageEntity extends DataClass
factory PinnedMessageEntity.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
serializer ??= driftRuntimeOptions.defaultSerializer;
return PinnedMessageEntity(
id: serializer.fromJson<String>(json['id']),
messageText: serializer.fromJson<String?>(json['messageText']),
@@ -1944,7 +1903,7 @@ class PinnedMessageEntity extends DataClass
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<String>(id),
'messageText': serializer.toJson<String?>(messageText),
@@ -2066,49 +2025,33 @@ class PinnedMessageEntity extends DataClass
}
@override
int get hashCode => $mrjf($mrjc(
id.hashCode,
$mrjc(
messageText.hashCode,
$mrjc(
attachments.hashCode,
$mrjc(
status.hashCode,
$mrjc(
type.hashCode,
$mrjc(
mentionedUsers.hashCode,
$mrjc(
reactionCounts.hashCode,
$mrjc(
reactionScores.hashCode,
$mrjc(
parentId.hashCode,
$mrjc(
quotedMessageId.hashCode,
$mrjc(
replyCount.hashCode,
$mrjc(
showInChannel.hashCode,
$mrjc(
shadowed.hashCode,
$mrjc(
command.hashCode,
$mrjc(
createdAt
.hashCode,
$mrjc(
updatedAt
.hashCode,
$mrjc(
deletedAt
.hashCode,
$mrjc(
userId
.hashCode,
$mrjc(
pinned.hashCode,
$mrjc(pinnedAt.hashCode, $mrjc(pinExpires.hashCode, $mrjc(pinnedByUserId.hashCode, $mrjc(channelCid.hashCode, $mrjc(i18n.hashCode, extraData.hashCode)))))))))))))))))))))))));
int get hashCode => Object.hashAll([
id,
messageText,
attachments,
status,
type,
mentionedUsers,
reactionCounts,
reactionScores,
parentId,
quotedMessageId,
replyCount,
showInChannel,
shadowed,
command,
createdAt,
updatedAt,
deletedAt,
userId,
pinned,
pinnedAt,
pinExpires,
pinnedByUserId,
channelCid,
i18n,
extraData
]);
@override
bool operator ==(Object other) =>
identical(this, other) ||
@@ -2744,7 +2687,7 @@ class $PinnedMessagesTable extends PinnedMessages
Set<GeneratedColumn> get $primaryKey => {id};
@override
PinnedMessageEntity map(Map<String, dynamic> data, {String? tablePrefix}) {
return PinnedMessageEntity.fromData(data, _db,
return PinnedMessageEntity.fromData(data,
prefix: tablePrefix != null ? '$tablePrefix.' : null);
}
@@ -2795,8 +2738,7 @@ class PinnedMessageReactionEntity extends DataClass
required this.createdAt,
required this.score,
this.extraData});
factory PinnedMessageReactionEntity.fromData(
Map<String, dynamic> data, GeneratedDatabase db,
factory PinnedMessageReactionEntity.fromData(Map<String, dynamic> data,
{String? prefix}) {
final effectivePrefix = prefix ?? '';
return PinnedMessageReactionEntity(
@@ -2832,7 +2774,7 @@ class PinnedMessageReactionEntity extends DataClass
factory PinnedMessageReactionEntity.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
serializer ??= driftRuntimeOptions.defaultSerializer;
return PinnedMessageReactionEntity(
userId: serializer.fromJson<String>(json['userId']),
messageId: serializer.fromJson<String>(json['messageId']),
@@ -2844,7 +2786,7 @@ class PinnedMessageReactionEntity extends DataClass
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'userId': serializer.toJson<String>(userId),
'messageId': serializer.toJson<String>(messageId),
@@ -2884,14 +2826,8 @@ class PinnedMessageReactionEntity extends DataClass
}
@override
int get hashCode => $mrjf($mrjc(
userId.hashCode,
$mrjc(
messageId.hashCode,
$mrjc(
type.hashCode,
$mrjc(createdAt.hashCode,
$mrjc(score.hashCode, extraData.hashCode))))));
int get hashCode =>
Object.hash(userId, messageId, type, createdAt, score, extraData);
@override
bool operator ==(Object other) =>
identical(this, other) ||
@@ -3090,7 +3026,7 @@ class $PinnedMessageReactionsTable extends PinnedMessageReactions
@override
PinnedMessageReactionEntity map(Map<String, dynamic> data,
{String? tablePrefix}) {
return PinnedMessageReactionEntity.fromData(data, _db,
return PinnedMessageReactionEntity.fromData(data,
prefix: tablePrefix != null ? '$tablePrefix.' : null);
}
@@ -3128,9 +3064,7 @@ class ReactionEntity extends DataClass implements Insertable<ReactionEntity> {
required this.createdAt,
required this.score,
this.extraData});
factory ReactionEntity.fromData(
Map<String, dynamic> data, GeneratedDatabase db,
{String? prefix}) {
factory ReactionEntity.fromData(Map<String, dynamic> data, {String? prefix}) {
final effectivePrefix = prefix ?? '';
return ReactionEntity(
userId: const StringType()
@@ -3164,7 +3098,7 @@ class ReactionEntity extends DataClass implements Insertable<ReactionEntity> {
factory ReactionEntity.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
serializer ??= driftRuntimeOptions.defaultSerializer;
return ReactionEntity(
userId: serializer.fromJson<String>(json['userId']),
messageId: serializer.fromJson<String>(json['messageId']),
@@ -3176,7 +3110,7 @@ class ReactionEntity extends DataClass implements Insertable<ReactionEntity> {
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'userId': serializer.toJson<String>(userId),
'messageId': serializer.toJson<String>(messageId),
@@ -3216,14 +3150,8 @@ class ReactionEntity extends DataClass implements Insertable<ReactionEntity> {
}
@override
int get hashCode => $mrjf($mrjc(
userId.hashCode,
$mrjc(
messageId.hashCode,
$mrjc(
type.hashCode,
$mrjc(createdAt.hashCode,
$mrjc(score.hashCode, extraData.hashCode))))));
int get hashCode =>
Object.hash(userId, messageId, type, createdAt, score, extraData);
@override
bool operator ==(Object other) =>
identical(this, other) ||
@@ -3418,7 +3346,7 @@ class $ReactionsTable extends Reactions
Set<GeneratedColumn> get $primaryKey => {messageId, type, userId};
@override
ReactionEntity map(Map<String, dynamic> data, {String? tablePrefix}) {
return ReactionEntity.fromData(data, _db,
return ReactionEntity.fromData(data,
prefix: tablePrefix != null ? '$tablePrefix.' : null);
}
@@ -3468,8 +3396,7 @@ class UserEntity extends DataClass implements Insertable<UserEntity> {
required this.online,
required this.banned,
required this.extraData});
factory UserEntity.fromData(Map<String, dynamic> data, GeneratedDatabase db,
{String? prefix}) {
factory UserEntity.fromData(Map<String, dynamic> data, {String? prefix}) {
final effectivePrefix = prefix ?? '';
return UserEntity(
id: const StringType()
@@ -3518,7 +3445,7 @@ class UserEntity extends DataClass implements Insertable<UserEntity> {
factory UserEntity.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
serializer ??= driftRuntimeOptions.defaultSerializer;
return UserEntity(
id: serializer.fromJson<String>(json['id']),
role: serializer.fromJson<String?>(json['role']),
@@ -3533,7 +3460,7 @@ class UserEntity extends DataClass implements Insertable<UserEntity> {
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<String>(id),
'role': serializer.toJson<String?>(role),
@@ -3585,22 +3512,8 @@ class UserEntity extends DataClass implements Insertable<UserEntity> {
}
@override
int get hashCode => $mrjf($mrjc(
id.hashCode,
$mrjc(
role.hashCode,
$mrjc(
language.hashCode,
$mrjc(
createdAt.hashCode,
$mrjc(
updatedAt.hashCode,
$mrjc(
lastActive.hashCode,
$mrjc(
online.hashCode,
$mrjc(
banned.hashCode, extraData.hashCode)))))))));
int get hashCode => Object.hash(id, role, language, createdAt, updatedAt,
lastActive, online, banned, extraData);
@override
bool operator ==(Object other) =>
identical(this, other) ||
@@ -3863,7 +3776,7 @@ class $UsersTable extends Users with TableInfo<$UsersTable, UserEntity> {
Set<GeneratedColumn> get $primaryKey => {id};
@override
UserEntity map(Map<String, dynamic> data, {String? tablePrefix}) {
return UserEntity.fromData(data, _db,
return UserEntity.fromData(data,
prefix: tablePrefix != null ? '$tablePrefix.' : null);
}
@@ -3921,8 +3834,7 @@ class MemberEntity extends DataClass implements Insertable<MemberEntity> {
required this.isModerator,
required this.createdAt,
required this.updatedAt});
factory MemberEntity.fromData(Map<String, dynamic> data, GeneratedDatabase db,
{String? prefix}) {
factory MemberEntity.fromData(Map<String, dynamic> data, {String? prefix}) {
final effectivePrefix = prefix ?? '';
return MemberEntity(
userId: const StringType()
@@ -3974,7 +3886,7 @@ class MemberEntity extends DataClass implements Insertable<MemberEntity> {
factory MemberEntity.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
serializer ??= driftRuntimeOptions.defaultSerializer;
return MemberEntity(
userId: serializer.fromJson<String>(json['userId']),
channelCid: serializer.fromJson<String>(json['channelCid']),
@@ -3993,7 +3905,7 @@ class MemberEntity extends DataClass implements Insertable<MemberEntity> {
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'userId': serializer.toJson<String>(userId),
'channelCid': serializer.toJson<String>(channelCid),
@@ -4057,26 +3969,18 @@ class MemberEntity extends DataClass implements Insertable<MemberEntity> {
}
@override
int get hashCode => $mrjf($mrjc(
userId.hashCode,
$mrjc(
channelCid.hashCode,
$mrjc(
role.hashCode,
$mrjc(
inviteAcceptedAt.hashCode,
$mrjc(
inviteRejectedAt.hashCode,
$mrjc(
invited.hashCode,
$mrjc(
banned.hashCode,
$mrjc(
shadowBanned.hashCode,
$mrjc(
isModerator.hashCode,
$mrjc(createdAt.hashCode,
updatedAt.hashCode)))))))))));
int get hashCode => Object.hash(
userId,
channelCid,
role,
inviteAcceptedAt,
inviteRejectedAt,
invited,
banned,
shadowBanned,
isModerator,
createdAt,
updatedAt);
@override
bool operator ==(Object other) =>
identical(this, other) ||
@@ -4405,7 +4309,7 @@ class $MembersTable extends Members
Set<GeneratedColumn> get $primaryKey => {userId, channelCid};
@override
MemberEntity map(Map<String, dynamic> data, {String? tablePrefix}) {
return MemberEntity.fromData(data, _db,
return MemberEntity.fromData(data,
prefix: tablePrefix != null ? '$tablePrefix.' : null);
}
@@ -4432,8 +4336,7 @@ class ReadEntity extends DataClass implements Insertable<ReadEntity> {
required this.userId,
required this.channelCid,
required this.unreadMessages});
factory ReadEntity.fromData(Map<String, dynamic> data, GeneratedDatabase db,
{String? prefix}) {
factory ReadEntity.fromData(Map<String, dynamic> data, {String? prefix}) {
final effectivePrefix = prefix ?? '';
return ReadEntity(
lastRead: const DateTimeType()
@@ -4458,7 +4361,7 @@ class ReadEntity extends DataClass implements Insertable<ReadEntity> {
factory ReadEntity.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
serializer ??= driftRuntimeOptions.defaultSerializer;
return ReadEntity(
lastRead: serializer.fromJson<DateTime>(json['lastRead']),
userId: serializer.fromJson<String>(json['userId']),
@@ -4468,7 +4371,7 @@ class ReadEntity extends DataClass implements Insertable<ReadEntity> {
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'lastRead': serializer.toJson<DateTime>(lastRead),
'userId': serializer.toJson<String>(userId),
@@ -4500,10 +4403,7 @@ class ReadEntity extends DataClass implements Insertable<ReadEntity> {
}
@override
int get hashCode => $mrjf($mrjc(
lastRead.hashCode,
$mrjc(userId.hashCode,
$mrjc(channelCid.hashCode, unreadMessages.hashCode))));
int get hashCode => Object.hash(lastRead, userId, channelCid, unreadMessages);
@override
bool operator ==(Object other) =>
identical(this, other) ||
@@ -4660,7 +4560,7 @@ class $ReadsTable extends Reads with TableInfo<$ReadsTable, ReadEntity> {
Set<GeneratedColumn> get $primaryKey => {userId, channelCid};
@override
ReadEntity map(Map<String, dynamic> data, {String? tablePrefix}) {
return ReadEntity.fromData(data, _db,
return ReadEntity.fromData(data,
prefix: tablePrefix != null ? '$tablePrefix.' : null);
}
@@ -4678,8 +4578,7 @@ class ChannelQueryEntity extends DataClass
/// The channel cid of this query
final String channelCid;
ChannelQueryEntity({required this.queryHash, required this.channelCid});
factory ChannelQueryEntity.fromData(
Map<String, dynamic> data, GeneratedDatabase db,
factory ChannelQueryEntity.fromData(Map<String, dynamic> data,
{String? prefix}) {
final effectivePrefix = prefix ?? '';
return ChannelQueryEntity(
@@ -4699,7 +4598,7 @@ class ChannelQueryEntity extends DataClass
factory ChannelQueryEntity.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
serializer ??= driftRuntimeOptions.defaultSerializer;
return ChannelQueryEntity(
queryHash: serializer.fromJson<String>(json['queryHash']),
channelCid: serializer.fromJson<String>(json['channelCid']),
@@ -4707,7 +4606,7 @@ class ChannelQueryEntity extends DataClass
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'queryHash': serializer.toJson<String>(queryHash),
'channelCid': serializer.toJson<String>(channelCid),
@@ -4729,7 +4628,7 @@ class ChannelQueryEntity extends DataClass
}
@override
int get hashCode => $mrjf($mrjc(queryHash.hashCode, channelCid.hashCode));
int get hashCode => Object.hash(queryHash, channelCid);
@override
bool operator ==(Object other) =>
identical(this, other) ||
@@ -4835,7 +4734,7 @@ class $ChannelQueriesTable extends ChannelQueries
Set<GeneratedColumn> get $primaryKey => {queryHash, channelCid};
@override
ChannelQueryEntity map(Map<String, dynamic> data, {String? tablePrefix}) {
return ChannelQueryEntity.fromData(data, _db,
return ChannelQueryEntity.fromData(data,
prefix: tablePrefix != null ? '$tablePrefix.' : null);
}
@@ -4875,8 +4774,7 @@ class ConnectionEventEntity extends DataClass
this.unreadChannels,
this.lastEventAt,
this.lastSyncAt});
factory ConnectionEventEntity.fromData(
Map<String, dynamic> data, GeneratedDatabase db,
factory ConnectionEventEntity.fromData(Map<String, dynamic> data,
{String? prefix}) {
final effectivePrefix = prefix ?? '';
return ConnectionEventEntity(
@@ -4922,7 +4820,7 @@ class ConnectionEventEntity extends DataClass
factory ConnectionEventEntity.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
serializer ??= driftRuntimeOptions.defaultSerializer;
return ConnectionEventEntity(
id: serializer.fromJson<int>(json['id']),
type: serializer.fromJson<String>(json['type']),
@@ -4935,7 +4833,7 @@ class ConnectionEventEntity extends DataClass
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<int>(id),
'type': serializer.toJson<String>(type),
@@ -4982,16 +4880,8 @@ class ConnectionEventEntity extends DataClass
}
@override
int get hashCode => $mrjf($mrjc(
id.hashCode,
$mrjc(
type.hashCode,
$mrjc(
ownUser.hashCode,
$mrjc(
totalUnreadCount.hashCode,
$mrjc(unreadChannels.hashCode,
$mrjc(lastEventAt.hashCode, lastSyncAt.hashCode)))))));
int get hashCode => Object.hash(id, type, ownUser, totalUnreadCount,
unreadChannels, lastEventAt, lastSyncAt);
@override
bool operator ==(Object other) =>
identical(this, other) ||
@@ -5212,7 +5102,7 @@ class $ConnectionEventsTable extends ConnectionEvents
Set<GeneratedColumn> get $primaryKey => {id};
@override
ConnectionEventEntity map(Map<String, dynamic> data, {String? tablePrefix}) {
return ConnectionEventEntity.fromData(data, _db,
return ConnectionEventEntity.fromData(data,
prefix: tablePrefix != null ? '$tablePrefix.' : null);
}
@@ -5225,9 +5115,10 @@ class $ConnectionEventsTable extends ConnectionEvents
MapConverter();
}
abstract class _$MoorChatDatabase extends GeneratedDatabase {
_$MoorChatDatabase(QueryExecutor e) : super(SqlTypeSystem.defaultInstance, e);
_$MoorChatDatabase.connect(DatabaseConnection c) : super.connect(c);
abstract class _$DriftChatDatabase extends GeneratedDatabase {
_$DriftChatDatabase(QueryExecutor e)
: super(SqlTypeSystem.defaultInstance, e);
_$DriftChatDatabase.connect(DatabaseConnection c) : super.connect(c);
late final $ChannelsTable channels = $ChannelsTable(this);
late final $MessagesTable messages = $MessagesTable(this);
late final $PinnedMessagesTable pinnedMessages = $PinnedMessagesTable(this);
@@ -5240,20 +5131,20 @@ abstract class _$MoorChatDatabase extends GeneratedDatabase {
late final $ChannelQueriesTable channelQueries = $ChannelQueriesTable(this);
late final $ConnectionEventsTable connectionEvents =
$ConnectionEventsTable(this);
late final UserDao userDao = UserDao(this as MoorChatDatabase);
late final ChannelDao channelDao = ChannelDao(this as MoorChatDatabase);
late final MessageDao messageDao = MessageDao(this as MoorChatDatabase);
late final UserDao userDao = UserDao(this as DriftChatDatabase);
late final ChannelDao channelDao = ChannelDao(this as DriftChatDatabase);
late final MessageDao messageDao = MessageDao(this as DriftChatDatabase);
late final PinnedMessageDao pinnedMessageDao =
PinnedMessageDao(this as MoorChatDatabase);
PinnedMessageDao(this as DriftChatDatabase);
late final PinnedMessageReactionDao pinnedMessageReactionDao =
PinnedMessageReactionDao(this as MoorChatDatabase);
late final MemberDao memberDao = MemberDao(this as MoorChatDatabase);
late final ReactionDao reactionDao = ReactionDao(this as MoorChatDatabase);
late final ReadDao readDao = ReadDao(this as MoorChatDatabase);
PinnedMessageReactionDao(this as DriftChatDatabase);
late final MemberDao memberDao = MemberDao(this as DriftChatDatabase);
late final ReactionDao reactionDao = ReactionDao(this as DriftChatDatabase);
late final ReadDao readDao = ReadDao(this as DriftChatDatabase);
late final ChannelQueryDao channelQueryDao =
ChannelQueryDao(this as MoorChatDatabase);
ChannelQueryDao(this as DriftChatDatabase);
late final ConnectionEventDao connectionEventDao =
ConnectionEventDao(this as MoorChatDatabase);
ConnectionEventDao(this as DriftChatDatabase);
@override
Iterable<TableInfo> get allTables => allSchemaEntities.whereType<TableInfo>();
@override
@@ -2,27 +2,27 @@
import 'dart:io';
import 'dart:isolate';
import 'package:moor/ffi.dart';
import 'package:moor/isolate.dart';
import 'package:moor/moor.dart';
import 'package:drift/drift.dart';
import 'package:drift/isolate.dart';
import 'package:drift/native.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/db/drift_chat_database.dart';
import 'package:stream_chat_persistence/src/stream_chat_persistence_client.dart';
import 'package:stream_chat_persistence/stream_chat_persistence.dart';
/// A Helper class to construct new instances of [MoorChatDatabase] specifically
/// for native platform applications
/// A Helper class to construct new instances of [DriftChatDatabase]
/// specifically for native platform applications.
class SharedDB {
/// Returns a new instance of [MoorChatDatabase].
static MoorChatDatabase constructDatabase(
/// Returns a new instance of [DriftChatDatabase].
static DriftChatDatabase constructDatabase(
String userId, {
bool logStatements = false,
ConnectionMode connectionMode = ConnectionMode.regular,
}) {
final dbName = 'db_$userId';
if (connectionMode == ConnectionMode.background) {
return MoorChatDatabase.connect(
return DriftChatDatabase.connect(
userId,
DatabaseConnection.delayed(Future(() async {
final isolate = await _createMoorIsolate(
@@ -33,7 +33,7 @@ class SharedDB {
})),
);
}
return MoorChatDatabase(
return DriftChatDatabase(
userId,
LazyDatabase(
() async => _constructDatabase(
@@ -44,7 +44,7 @@ class SharedDB {
);
}
static Future<VmDatabase> _constructDatabase(
static Future<NativeDatabase> _constructDatabase(
String dbName, {
bool logStatements = false,
}) async {
@@ -52,27 +52,27 @@ class SharedDB {
final dir = await getApplicationDocumentsDirectory();
final path = join(dir.path, '$dbName.sqlite');
final file = File(path);
return VmDatabase(file, logStatements: logStatements);
return NativeDatabase(file, logStatements: logStatements);
}
if (Platform.isMacOS || Platform.isLinux) {
final file = File('$dbName.sqlite');
return VmDatabase(file, logStatements: logStatements);
return NativeDatabase(file, logStatements: logStatements);
}
return VmDatabase.memory(logStatements: logStatements);
return NativeDatabase.memory(logStatements: logStatements);
}
static void _startBackground(_IsolateStartRequest request) {
final executor = LazyDatabase(() async => VmDatabase(
final executor = LazyDatabase(() async => NativeDatabase(
File(request.targetPath),
logStatements: request.logStatements,
));
final moorIsolate = MoorIsolate.inCurrent(
final moorIsolate = DriftIsolate.inCurrent(
() => DatabaseConnection.fromExecutor(executor),
);
request.sendMoorIsolate.send(moorIsolate);
}
static Future<MoorIsolate> _createMoorIsolate(
static Future<DriftIsolate> _createMoorIsolate(
String dbName, {
bool logStatements = false,
}) async {
@@ -89,7 +89,7 @@ class SharedDB {
),
);
return await receivePort.first as MoorIsolate;
return await receivePort.first as DriftIsolate;
}
}
@@ -1,11 +1,11 @@
// coverage:ignore-file
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/db/drift_chat_database.dart';
import 'package:stream_chat_persistence/stream_chat_persistence.dart';
/// A Helper class to construct new instances of [MoorChatDatabase]
/// A Helper class to construct new instances of [DriftChatDatabase]
class SharedDB {
/// Returns a new instance of [MoorChatDatabase].
static MoorChatDatabase constructDatabase(
/// Returns a new instance of [DriftChatDatabase].
static DriftChatDatabase constructDatabase(
String userId, {
bool logStatements = false,
ConnectionMode connectionMode = ConnectionMode.regular,
@@ -1,19 +1,19 @@
// coverage:ignore-file
import 'package:moor/moor_web.dart';
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
import 'package:drift/web.dart';
import 'package:stream_chat_persistence/src/db/drift_chat_database.dart';
import 'package:stream_chat_persistence/src/stream_chat_persistence_client.dart';
/// A Helper class to construct new instances of [MoorChatDatabase] specifically
/// for Web applications
/// A Helper class to construct new instances of [DriftChatDatabase]
/// specifically for Web applications.
class SharedDB {
/// Returns a new instance of [MoorChatDatabase].
static MoorChatDatabase constructDatabase(
/// Returns a new instance of [DriftChatDatabase].
static DriftChatDatabase constructDatabase(
String userId, {
bool logStatements = false,
ConnectionMode connectionMode = ConnectionMode.regular, // Ignored on web
}) {
final dbName = 'db_$userId';
final queryExecutor = WebDatabase(dbName, logStatements: logStatements);
return MoorChatDatabase(userId, queryExecutor);
return DriftChatDatabase(userId, queryExecutor);
}
}
@@ -1,5 +1,5 @@
// coverage:ignore-file
import 'package:moor/moor.dart';
import 'package:drift/drift.dart';
/// Represents a [ChannelQueries] table in [MoorChatDatabase].
@DataClassName('ChannelQueryEntity')
@@ -1,5 +1,5 @@
// coverage:ignore-file
import 'package:moor/moor.dart';
import 'package:drift/drift.dart';
import 'package:stream_chat_persistence/src/converter/map_converter.dart';
/// Represents a [Channels] table in [MoorChatDatabase].
@@ -1,5 +1,5 @@
// coverage:ignore-file
import 'package:moor/moor.dart';
import 'package:drift/drift.dart';
import 'package:stream_chat_persistence/src/converter/map_converter.dart';
/// Represents a [ConnectionEvents] table in [MoorChatDatabase].
@@ -1,5 +1,5 @@
// coverage:ignore-file
import 'package:moor/moor.dart';
import 'package:drift/drift.dart';
/// Represents a [Members] table in [MoorChatDatabase].
@DataClassName('MemberEntity')
@@ -1,5 +1,5 @@
// coverage:ignore-file
import 'package:moor/moor.dart';
import 'package:drift/drift.dart';
import 'package:stream_chat_persistence/src/converter/list_converter.dart';
import 'package:stream_chat_persistence/src/converter/map_converter.dart';
import 'package:stream_chat_persistence/src/converter/message_sending_status_converter.dart';
@@ -1,5 +1,5 @@
// coverage:ignore-file
import 'package:moor/moor.dart';
import 'package:drift/drift.dart';
import 'package:stream_chat_persistence/src/entity/reactions.dart';
@@ -1,5 +1,5 @@
// coverage:ignore-file
import 'package:moor/moor.dart';
import 'package:drift/drift.dart';
import 'package:stream_chat_persistence/src/entity/messages.dart';
@@ -1,5 +1,5 @@
// coverage:ignore-file
import 'package:moor/moor.dart';
import 'package:drift/drift.dart';
import 'package:stream_chat_persistence/src/converter/map_converter.dart';
/// Represents a [Reactions] table in [MoorChatDatabase].
@@ -1,5 +1,5 @@
// coverage:ignore-file
import 'package:moor/moor.dart';
import 'package:drift/drift.dart';
/// Represents a [Reads] table in [MoorChatDatabase].
@DataClassName('ReadEntity')
@@ -1,5 +1,5 @@
// coverage:ignore-file
import 'package:moor/moor.dart';
import 'package:drift/drift.dart';
import 'package:stream_chat_persistence/src/converter/map_converter.dart';
/// Represents a [Users] table in [MoorChatDatabase].
@@ -1,5 +1,5 @@
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/db/drift_chat_database.dart';
/// Useful mapping functions for [ChannelEntity]
extension ChannelEntityX on ChannelEntity {
@@ -1,5 +1,5 @@
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/db/drift_chat_database.dart';
/// Useful mapping functions for [ConnectionEventEntity]
extension ConnectionEventX on ConnectionEventEntity {
@@ -1,5 +1,5 @@
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/db/drift_chat_database.dart';
/// Useful mapping functions for [MemberEntity]
extension MemberEntityX on MemberEntity {
@@ -1,7 +1,7 @@
import 'dart:convert';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/db/drift_chat_database.dart';
/// Useful mapping functions for [MessageEntity]
extension MessageEntityX on MessageEntity {
@@ -1,7 +1,7 @@
import 'dart:convert';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/db/drift_chat_database.dart';
/// Useful mapping functions for [PinnedMessageEntity]
extension PinnedMessageEntityX on PinnedMessageEntity {
@@ -1,5 +1,5 @@
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/db/drift_chat_database.dart';
/// Useful mapping functions for [PinnedMessageReactionEntity]
extension PinnedMessageReactionEntityX on PinnedMessageReactionEntity {
@@ -1,5 +1,5 @@
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/db/drift_chat_database.dart';
/// Useful mapping functions for [ReactionEntity]
extension ReactionEntityX on ReactionEntity {
@@ -1,5 +1,5 @@
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/db/drift_chat_database.dart';
/// Useful mapping functions for [ReadEntity]
extension ReadEntityX on ReadEntity {
@@ -1,5 +1,5 @@
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/db/drift_chat_database.dart';
/// Useful mapping functions for [UserEntity]
extension UserEntityX on UserEntity {
@@ -4,7 +4,7 @@ import 'package:meta/meta.dart';
import 'package:mutex/mutex.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/db/drift_chat_database.dart';
/// Various connection modes on which [StreamChatPersistenceClient] can work
enum ConnectionMode {
@@ -15,8 +15,8 @@ enum ConnectionMode {
background,
}
/// Signature for a function which provides instance of [MoorChatDatabase]
typedef DatabaseProvider = MoorChatDatabase Function(String, ConnectionMode);
/// Signature for a function which provides instance of [DriftChatDatabase]
typedef DatabaseProvider = DriftChatDatabase Function(String, ConnectionMode);
final _levelEmojiMapper = {
Level.INFO: '',
@@ -24,7 +24,7 @@ final _levelEmojiMapper = {
Level.SEVERE: '🚨',
};
/// A [MoorChatDatabase] based implementation of the [ChatPersistenceClient]
/// A [DriftChatDatabase] based implementation of the [ChatPersistenceClient]
class StreamChatPersistenceClient extends ChatPersistenceClient {
/// Creates a new instance of the stream chat persistence client
StreamChatPersistenceClient({
@@ -37,9 +37,9 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
_logger.onRecord.listen(logHandlerFunction ?? _defaultLogHandler);
}
/// [MoorChatDatabase] instance used by this client.
/// [DriftChatDatabase] instance used by this client.
@visibleForTesting
MoorChatDatabase? db;
DriftChatDatabase? db;
final Logger _logger;
final ConnectionMode _connectionMode;
@@ -70,7 +70,7 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
return true;
}
MoorChatDatabase _defaultDatabaseProvider(
DriftChatDatabase _defaultDatabaseProvider(
String userId,
ConnectionMode mode,
) =>
@@ -10,11 +10,11 @@ environment:
flutter: ">=1.17.0"
dependencies:
drift: ^1.0.0
flutter:
sdk: flutter
logging: ^1.0.1
meta: ^1.3.0
moor: ^4.4.0
mutex: ^3.0.0
path: ^1.8.0
path_provider: ^2.0.1
@@ -24,7 +24,8 @@ dependencies:
dev_dependencies:
build_runner: ^2.0.1
dart_code_metrics: ^4.4.0
drift_dev: ^1.0.0
flutter_test:
sdk: flutter
mocktail: ^0.1.1
moor_generator: ^4.2.1
mocktail: ^0.2.0
@@ -1,8 +1,8 @@
import 'package:mocktail/mocktail.dart';
import 'package:stream_chat_persistence/src/dao/dao.dart';
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/db/drift_chat_database.dart';
class MockChatDatabase extends Mock implements MoorChatDatabase {
class MockChatDatabase extends Mock implements DriftChatDatabase {
UserDao? _userDao;
@override
@@ -1,13 +1,13 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_persistence/src/dao/channel_dao.dart';
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/db/drift_chat_database.dart';
import '../../stream_chat_persistence_client_test.dart';
void main() {
late ChannelDao channelDao;
late MoorChatDatabase database;
late DriftChatDatabase database;
setUp(() {
database = testDatabaseProvider('testUserId');
@@ -1,13 +1,13 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_persistence/src/dao/channel_query_dao.dart';
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/db/drift_chat_database.dart';
import '../../stream_chat_persistence_client_test.dart';
import '../utils/date_matcher.dart';
void main() {
late MoorChatDatabase database;
late DriftChatDatabase database;
late ChannelQueryDao channelQueryDao;
setUp(() {

Some files were not shown because too many files have changed in this diff Show More