chore: 1.5.1 (#387)

* lint(persistence): Apply team linter

Signed-off-by: Sahil Kumar <[email protected]>

* style: flutter format

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): Add tests for channel dao

Signed-off-by: Sahil Kumar <[email protected]>

* refactor(persistence): remove dummy comments

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): Add test for channel query dao

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add tests for connection event dao

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add test for member dao

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add tests for message dao

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add test for pinned message dao

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add test for reaction dao

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add test for read dao

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): minor improvement in update function tests

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add tests for user dao

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add test for list converter

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add test for map converter

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add tests for message sending status converter

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add tests for channel mapper

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add test for event mapper

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add tests for member mapper

Signed-off-by: Sahil Kumar <[email protected]>

* remove useless dependencies

* test(persistence): add tests for message mapper

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add tests for pinned message mapper

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add tests for reaction mapper

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add tests for read mapper

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add tests for user mapper

Signed-off-by: Sahil Kumar <[email protected]>

* refactor(persistence): remove redundant UsersTable getter from ConnectionEventDao

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add tests for stream chat persistence client

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add tests moor chat database

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add moor schemas for future migration tests

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add clear queryCache test in channel query dao

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add getThreadMessagesByParentId along with pagination test in message, pinned message dao.

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add getMessagesByCid along with pagination test in message, pinned message dao.

Signed-off-by: Sahil Kumar <[email protected]>

* update ci

* apply locale to jiffy

* fix test

* test(persistence): add getMessagesByCid along with quotedMessage test in message, pinned message dao.

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add extra checks in getMessagesByCid along with pagination test in message, pinned message dao.

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add return channel with pagination applies test in channel query dao

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): improve message mapper, pinned message mapper test

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add client constructor test in stream chat persistence client

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add coverage:ignore-file for entities and db/shared directory

Signed-off-by: Sahil Kumar <[email protected]>

* skip pub publish --dry-run for develop branch

Signed-off-by: Sahil Kumar <[email protected]>

* add remove from coverage script

Signed-off-by: Sahil Kumar <[email protected]>

* fix

Signed-off-by: Sahil Kumar <[email protected]>

* fix

Signed-off-by: Sahil Kumar <[email protected]>

* fix

Signed-off-by: Sahil Kumar <[email protected]>

* fix

Signed-off-by: Sahil Kumar <[email protected]>

* fix locale

* fix message input _parseExistingMessage

* fix(UI-Kit): Show selected attachment file as file in message input widget

Signed-off-by: Sahil Kumar <[email protected]>

* fix: ws disconnection (#345)

* update ws removing custom implementation; fixed disconnection while connecting

* add test

* feat: Apply team lints to core package (#334)

* Add analyzer file to core package

* Apply team lint rules to core package

* clean up analyzer warnings in stream chat flutter

* remove unused line

* add ignore

* sync analysis options

* [Core -> MessageWidget] Fix variable dynamic types

Signed-off-by: Sahil Kumar <[email protected]>

Co-authored-by: Salvatore Giordano <[email protected]>
Co-authored-by: Sahil Kumar <[email protected]>

* test: add tests for stream chat flutter core (#354)

* test(core): add tests for channels bloc

Signed-off-by: Sahil Kumar <[email protected]>

* test(core): refactor some tests due to changes in channels bloc

Signed-off-by: Sahil Kumar <[email protected]>

* test(core): add tests for channel list core

Signed-off-by: Sahil Kumar <[email protected]>

* test(core): add tests for users bloc, minor improvements

Signed-off-by: Sahil Kumar <[email protected]>

* test(core): add tests for user list core, minor improvements

Signed-off-by: Sahil Kumar <[email protected]>

* test(core): add tests for message search bloc, refactor

Signed-off-by: Sahil Kumar <[email protected]>

* test(core): add tests for message search list core, refactor

Signed-off-by: Sahil Kumar <[email protected]>

* test(core): add tests for lazy load scroll view

Signed-off-by: Sahil Kumar <[email protected]>

* test(core): add tests for stream chat core

Signed-off-by: Sahil Kumar <[email protected]>

* test(core): add tests for message list core

Signed-off-by: Sahil Kumar <[email protected]>

* test(core): add tests for stream channel

Signed-off-by: Sahil Kumar <[email protected]>

* Fix merge conflicts

Signed-off-by: Sahil Kumar <[email protected]>

* flutter format

Signed-off-by: Sahil Kumar <[email protected]>

* fixes

Signed-off-by: Sahil Kumar <[email protected]>

* update workflow

Signed-off-by: Sahil Kumar <[email protected]>

* chore(core): fix analyzer issues

Signed-off-by: Sahil Kumar <[email protected]>

* chore(ui-kit): fix analyzer issues

Signed-off-by: Sahil Kumar <[email protected]>

* refactor(ui-kit): update giphy interaction (#361)

* refactor(ui-kit): update giphy interaction

Signed-off-by: Sahil Kumar <[email protected]>

* fix overflow

Co-authored-by: Salvatore Giordano <[email protected]>

* fix: Fixed sendingbutton.outside case (#355)

* fix: Fixed sendingbutton.outside case

* fix: Fixed all actionbutton cases

* fix review

Co-authored-by: Salvatore Giordano <[email protected]>

* fix(ui-kit): fix accessibility text size scaling in message widget

Signed-off-by: Sahil Kumar <[email protected]>

* fix(ui-kit): fix message widget long username overflow

Signed-off-by: Sahil Kumar <[email protected]>

* fix(ui-kit): fix message input dm checkbox accessibility scaling issue

Signed-off-by: Sahil Kumar <[email protected]>

* feat: exposed textbuilder callback (#367)

* exposed textbuilder callback

* exposed textbuilder callback

* fixes

Signed-off-by: Sahil Kumar <[email protected]>

* check if user.mutes is null

* check if user.mutes is null

* fixes #370: remove all controls on full screen media if message is of type ephemeral (#373)

* fix: fix channel list pagination spinner (#375)

Signed-off-by: Sahil Kumar <[email protected]>

* fix: attachments upload when size exceeds (#377)

* fix: Fixed file upload error

* fix: Fixed file upload error

* fix coverage script

Signed-off-by: Sahil Kumar <[email protected]>

* fix: message input (#382)

* feat: Added mentions suffix builder

* feat: Added builders for mentions and theme element for message input expanded button

* refactor: refactored PR to suit other style

* fix: fixes trailing commas and changes names

* doc: added documentation to new elements

* doc: added documentation to new elements

* bump version

* add meta dep to core

* sort deps

Co-authored-by: Sahil Kumar <[email protected]>
Co-authored-by: Neevash Ramdial (Nash) <[email protected]>
Co-authored-by: Deven Joshi <[email protected]>
This commit is contained in:
Salvatore Giordano
2021-04-07 12:24:29 +02:00
committed by GitHub
co-authored by Sahil Kumar Neevash Ramdial Deven Joshi
parent aeb6da8d38
commit 87ca818a9a
119 changed files with 11223 additions and 1240 deletions
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
# Fast fail the script on failures.
set -e
pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r '\.g\.dart$' -r '\.freezed\.dart$'
+13 -7
View File
@@ -8,6 +8,7 @@ on:
push:
branches:
- master
- develop
paths-ignore:
- 'docs/**'
@@ -32,6 +33,7 @@ jobs:
melos exec -c 3 --ignore="*example*" -- \
tuneup check
- name: 'Pub Check'
if: github.ref == 'refs/heads/master'
run: |
melos exec -c 1 --no-private --ignore="*example*" -- \
pub publish --dry-run
@@ -57,7 +59,7 @@ jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 5
timeout-minutes: 15
steps:
- uses: actions/checkout@v2
with:
@@ -68,31 +70,35 @@ jobs:
run: |
./.github/workflows/scripts/install-tools.sh
flutter pub global activate coverage
flutter pub global activate remove_from_coverage
- name: 'Bootstrap Workspace'
run: melos bootstrap
- name: 'Dart Test'
run: |
cd packages/stream_chat
flutter pub run test --coverage coverage/
format_coverage --lcov --in=coverage/ --out=lcov.info --packages=.packages --report-on=lib
format_coverage --lcov --in=coverage/ --out=coverage/lcov.info --packages=.packages --report-on=lib
- name: 'Flutter Test'
run: |
melos exec -c 3 --flutter --dir-exists=test --ignore="*example*" --ignore="*web*" -- \
flutter test --coverage
- name: CodeCov
run: bash <(curl -s https://codecov.io/bash) -t ${{ secrets.CODECOV_TOKEN }}
run: |
melos exec -c 3 --fail-fast --dir-exists=test --ignore="*example*" --ignore="*web*" -- \
"\$MELOS_ROOT_PATH/.github/workflows/scripts/coverage.sh"
bash <(curl -s https://codecov.io/bash) -t ${{ secrets.CODECOV_TOKEN }}
- uses: VeryGoodOpenSource/[email protected]
with:
path: packages/stream_chat/lcov.info
min_coverage: 50
path: packages/stream_chat/coverage/lcov.info
min_coverage: 40
- uses: VeryGoodOpenSource/[email protected]
with:
path: packages/stream_chat_persistence/coverage/lcov.info
min_coverage: 0.2
min_coverage: 95
- uses: VeryGoodOpenSource/[email protected]
with:
path: packages/stream_chat_flutter_core/coverage/lcov.info
min_coverage: 4.5
min_coverage: 90
- uses: VeryGoodOpenSource/[email protected]
with:
path: packages/stream_chat_flutter/coverage/lcov.info
+4
View File
@@ -1,3 +1,7 @@
## 1.5.1
- Minor fixes and improvements
## 1.5.0
- Minor fixes and improvements
+3 -5
View File
@@ -1,11 +1,11 @@
analyzer:
exclude:
exclude:
- lib/**/*.g.dart
- lib/**/*.freezed.dart
- example/*
- test/*
linter:
rules:
linter:
rules:
- always_use_package_imports
- avoid_empty_else
- avoid_relative_lib_imports
@@ -14,7 +14,6 @@ linter:
- cancel_subscriptions
- close_sinks
- control_flow_in_finally
- diagnostic_describe_all_properties
- empty_statements
- hash_and_equals
- invariant_booleans
@@ -33,7 +32,6 @@ linter:
- use_key_in_widget_constructors
- valid_regexps
- always_declare_return_types
- always_put_required_named_parameters_first
- always_require_non_null_named_parameters
- annotate_overrides
- avoid_bool_literals_in_conditional_expressions
@@ -146,7 +146,7 @@ class Channel {
/// Channel extra data
Map<String, dynamic> get extraData =>
state?._channelState?.channel?.extraData;
state?._channelState?.channel?.extraData ?? _extraData;
/// Channel extra data as a stream
Stream<Map<String, dynamic>> get extraDataStream =>
@@ -1210,9 +1210,10 @@ class ChannelClientState {
this._channel,
ChannelState channelState,
//ignore: unnecessary_parenthesis
) : _debouncedUpdatePersistenceChannelState = ((ChannelState state) {
_channel?._client?.chatPersistenceClient?.updateChannelState(state);
}).debounced(const Duration(seconds: 1)) {
) : _debouncedUpdatePersistenceChannelState = ((ChannelState state) =>
_channel?._client?.chatPersistenceClient
?.updateChannelState(state))
.debounced(const Duration(seconds: 1)) {
retryQueue = RetryQueue(
channel: _channel,
logger: Logger('RETRY QUEUE ${_channel.cid}'),
@@ -1597,7 +1598,7 @@ class ChannelClientState {
bool _countMessageAsUnread(Message message) {
final userId = _channel.client.state?.user?.id;
final userIsMuted = _channel.client.state.user.mutes.firstWhere(
final userIsMuted = _channel.client.state?.user?.mutes?.firstWhere(
(m) => m.user?.id == message.user.id,
orElse: () => null,
) !=
@@ -104,4 +104,25 @@ class PaginationParams {
lessThan: lessThan ?? this.lessThan,
lessThanOrEqual: lessThanOrEqual ?? this.lessThanOrEqual,
);
@override
int get hashCode =>
runtimeType.hashCode ^
limit.hashCode ^
offset.hashCode ^
greaterThan.hashCode ^
greaterThanOrEqual.hashCode ^
lessThan.hashCode ^
lessThanOrEqual.hashCode;
@override
bool operator ==(covariant PaginationParams other) =>
identical(this, other) ||
runtimeType == other.runtimeType &&
limit == other.limit &&
offset == other.offset &&
greaterThan == other.greaterThan &&
greaterThanOrEqual == other.greaterThanOrEqual &&
lessThan == other.lessThan &&
lessThanOrEqual == other.lessThanOrEqual;
}
@@ -6,9 +6,6 @@ import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/src/api/connection_status.dart';
import 'package:stream_chat/src/api/web_socket_channel_stub.dart'
if (dart.library.html) 'web_socket_channel_html.dart'
if (dart.library.io) 'web_socket_channel_io.dart';
import 'package:stream_chat/src/models/event.dart';
import 'package:stream_chat/src/models/user.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
@@ -36,7 +33,7 @@ class WebSocket {
this.connectPayload,
this.handler,
this.logger,
this.connectFunc = connectWebSocket,
this.connectFunc,
this.reconnectionMonitorInterval = 1,
this.healthCheckInterval = 20,
this.reconnectionMonitorTimeout = 40,
@@ -121,9 +118,9 @@ class WebSocket {
WebSocketChannel _channel;
Timer _healthCheck, _reconnectionMonitor;
DateTime _lastEventAt;
bool _manuallyDisconnected = false,
_connecting = false,
_reconnecting = false;
bool _manuallyDisconnected = false;
bool _connecting = false;
bool _reconnecting = false;
Event _decodeEvent(String source) => Event.fromJson(json.decode(source));
@@ -143,7 +140,8 @@ class WebSocket {
logger.info('connecting to $_path');
_channel = connectFunc(_path);
_channel =
connectFunc?.call(_path) ?? WebSocketChannel.connect(Uri.parse(_path));
_channel.stream.listen(
(data) {
final jsonData = json.decode(data);
@@ -177,6 +175,10 @@ class WebSocket {
}
void _onData(data) {
if (_manuallyDisconnected) {
return;
}
final event = _decodeEvent(data);
logger.info('received new event: $data');
@@ -302,6 +304,10 @@ class WebSocket {
/// Disconnects the WS and releases eventual resources
Future<void> disconnect() async {
_connecting = false;
if (!_connectionCompleter.isCompleted) {
_connectionCompleter.complete();
}
if (_manuallyDisconnected) {
return;
}
+2 -1
View File
@@ -738,7 +738,8 @@ class StreamChatClient {
await _chatPersistenceClient?.updateChannelQueries(
filter,
channels.map((c) => c.channel.cid).toList(),
paginationParams?.offset == null || paginationParams.offset == 0,
clearQueryCache:
paginationParams?.offset == null || paginationParams.offset == 0,
);
state.channels = updateData.key;
@@ -100,10 +100,9 @@ abstract class ChatPersistenceClient {
/// the list of matching rows will be deleted
Future<void> updateChannelQueries(
Map<String, dynamic> filter,
List<String> cids,
// ignore: avoid_positional_boolean_parameters
bool clearQueryCache,
);
List<String> cids, {
bool clearQueryCache = false,
});
/// Remove a message by [messageId]
Future<void> deleteMessageById(String messageId) =>
@@ -121,6 +121,46 @@ class Event {
_$EventToJson(this),
topLevelFields,
);
/// Creates a copy of [Event] with specified attributes overridden.
Event copyWith({
String type,
String cid,
String channelId,
String channelType,
String connectionId,
DateTime createdAt,
OwnUser me,
User user,
Message message,
EventChannel channel,
Member member,
Reaction reaction,
int totalUnreadCount,
int unreadChannels,
bool online,
String parentId,
Map<String, dynamic> extraData,
}) =>
Event(
type: type ?? this.type,
cid: cid ?? this.cid,
connectionId: connectionId ?? this.connectionId,
createdAt: createdAt ?? this.createdAt,
me: me ?? this.me,
user: user ?? this.user,
message: message ?? this.message,
totalUnreadCount: totalUnreadCount ?? this.totalUnreadCount,
unreadChannels: unreadChannels ?? this.unreadChannels,
reaction: reaction ?? this.reaction,
online: online ?? this.online,
channel: channel ?? this.channel,
member: member ?? this.member,
channelId: channelId ?? this.channelId,
channelType: channelType ?? this.channelType,
parentId: parentId ?? this.parentId,
extraData: extraData ?? this.extraData,
);
}
/// The channel embedded in the event object
@@ -27,4 +27,16 @@ class Read {
/// Serialize to json
Map<String, dynamic> toJson() => _$ReadToJson(this);
/// Creates a copy of [Read] with specified attributes overridden.
Read copyWith({
DateTime lastRead,
User user,
int unreadMessages,
}) =>
Read(
lastRead: lastRead ?? this.lastRead,
user: user ?? this.user,
unreadMessages: unreadMessages ?? this.unreadMessages,
);
}
@@ -100,4 +100,28 @@ class User {
/// Serialize to json
Map<String, dynamic> toJson() =>
Serialization.moveFromExtraDataToRoot(_$UserToJson(this), topLevelFields);
/// Creates a copy of [User] with specified attributes overridden.
User copyWith({
String id,
String role,
DateTime createdAt,
DateTime updatedAt,
DateTime lastActive,
bool online,
Map<String, dynamic> extraData,
bool banned,
List<String> teams,
}) =>
User(
id: id ?? this.id,
role: role ?? this.role,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
lastActive: lastActive ?? this.lastActive,
online: online ?? this.online,
extraData: extraData ?? this.extraData,
banned: banned ?? this.banned,
teams: teams ?? this.teams,
);
}
+1 -1
View File
@@ -3,4 +3,4 @@ import 'package:stream_chat/src/client.dart';
/// Current package version
/// Used in [StreamChatClient] to build the `x-stream-client` header
// ignore: constant_identifier_names
const PACKAGE_VERSION = '1.5.0';
const PACKAGE_VERSION = '1.5.1';
+2 -2
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: 1.5.0
version: 1.5.1
repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -20,7 +20,7 @@ dependencies:
mime: ^0.9.7
rxdart: ^0.25.0
uuid: ^2.2.2
web_socket_channel: ^1.1.0
web_socket_channel: ^1.2.0
dev_dependencies:
build_runner: ^1.10.0
@@ -1,9 +0,0 @@
import 'package:test/test.dart';
import 'package:stream_chat/src/api/web_socket_channel_stub.dart';
void main() {
test('src/api/web_socket_stub_test', () {
expect(
() => connectWebSocket('fakeurl'), throwsA(isA<UnimplementedError>()));
});
}
@@ -1,6 +1,5 @@
import 'dart:async';
import 'package:test/test.dart';
import 'package:logging/logging.dart';
import 'package:mockito/mockito.dart';
import 'package:stream_chat/src/api/connection_status.dart';
@@ -8,6 +7,7 @@ import 'package:stream_chat/src/api/websocket.dart';
import 'package:stream_chat/src/models/event.dart';
import 'package:stream_chat/src/models/user.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:test/test.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
class Functions {
@@ -47,10 +47,9 @@ void main() {
final mockWSChannel = MockWSChannel();
final StreamController<String> streamController =
StreamController<String>.broadcast();
final streamController = StreamController<String>.broadcast();
final computedUrl =
const computedUrl =
'wss://baseurl/connect?test=true&json=%7B%22payload%22%3A%22test%22%2C%22user_details%22%3A%7B%22id%22%3A%22testid%22%7D%7D';
when(connectFunc(computedUrl)).thenAnswer((_) => mockWSChannel);
@@ -60,7 +59,7 @@ void main() {
});
final timer = Timer.periodic(
Duration(milliseconds: 100),
const Duration(milliseconds: 100),
(_) => streamController.sink.add('{}'),
);
@@ -160,6 +159,42 @@ void main() {
return connect;
});
test('should close correctly the controller while connecting', () async {
final handleFunc = MockFunctions().handleFunc;
final ConnectWebSocket connectFunc = MockFunctions().connectFunc;
final ws = WebSocket(
baseUrl: 'baseurl',
user: User(id: 'testid'),
logger: Logger('ws'),
connectParams: {'test': 'true'},
connectPayload: {'payload': 'test'},
handler: handleFunc,
connectFunc: connectFunc,
);
final mockWSChannel = MockWSChannel();
final StreamController<String> streamController =
StreamController<String>.broadcast();
final computedUrl =
'wss://baseurl/connect?test=true&json=%7B%22payload%22%3A%22test%22%2C%22user_details%22%3A%7B%22id%22%3A%22testid%22%7D%7D';
when(connectFunc(computedUrl)).thenAnswer((_) => mockWSChannel);
when(mockWSChannel.sink).thenAnswer((_) => MockWSSink());
when(mockWSChannel.stream).thenAnswer((_) {
return streamController.stream;
});
ws.connect();
await ws.disconnect();
streamController.add('{}');
verify(connectFunc(computedUrl)).called(1);
verifyNever(handleFunc(any));
});
test('should run correctly health check', () async {
final handleFunc = MockFunctions().handleFunc;
@@ -1,3 +1,9 @@
## 1.5.2
- Fix accessibility text size overflows
- Updated Giphy attachment ui
- Minor fixes and improvements
## 1.5.1
- Fixed unread count not updating while the chat is open
@@ -59,138 +59,57 @@ class GiphyAttachment extends AttachmentWidget {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Stack(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: GestureDetector(
onTap: () => onAttachmentTap ?? _onImageTap(context),
child: ClipRRect(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
child: CachedNetworkImage(
height: size?.height,
width: size?.width,
placeholder: (_, __) {
return Container(
width: size?.width,
height: size?.height,
child: Center(
child: CircularProgressIndicator(),
),
);
},
imageUrl: imageUrl,
errorWidget: (context, url, error) {
return AttachmentError(size: size);
},
fit: BoxFit.cover,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
StreamSvgIcon.giphyIcon(),
SizedBox(width: 8),
Text(
'Giphy',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
Positioned(
bottom: 16,
left: 16,
child: Material(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 4.0,
),
child: Row(
children: [
StreamSvgIcon.lightning(
color:
StreamChatTheme.of(context).colorTheme.white,
size: 16,
),
Text(
'GIPHY',
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.white,
fontWeight: FontWeight.bold,
fontSize: 11,
),
),
],
SizedBox(width: 8),
if (attachment.title != null)
Flexible(
child: Text(
attachment.title,
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5),
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
),
),
],
],
),
),
if (attachment.title != null)
Container(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Card(
color: Colors.white,
elevation: 2,
shape: CircleBorder(),
child: IconButton(
padding: const EdgeInsets.all(0),
constraints: BoxConstraints.tight(Size(32, 32)),
icon: StreamSvgIcon.left(
size: 24.0,
),
splashRadius: 16,
onPressed: () {
streamChannel.channel.sendAction(message, {
'image_action': 'shuffle',
});
},
),
Padding(
padding: const EdgeInsets.all(2.0),
child: GestureDetector(
onTap: () => onAttachmentTap ?? _onImageTap(context),
child: CachedNetworkImage(
height: size?.height,
width: size?.width,
placeholder: (_, __) {
return Container(
width: size?.width,
height: size?.height,
child: Center(
child: CircularProgressIndicator(),
),
Expanded(
child: Center(
child: Text(
'"${attachment.title}"',
style: TextStyle(
fontStyle: FontStyle.italic,
),
),
),
),
Card(
color: Colors.white,
elevation: 2,
shape: CircleBorder(),
child: IconButton(
padding: const EdgeInsets.all(0),
constraints: BoxConstraints.tight(Size(32, 32)),
icon: StreamSvgIcon.right(
size: 24.0,
),
splashRadius: 16,
onPressed: () {
streamChannel.channel.sendAction(message, {
'image_action': 'shuffle',
});
},
),
),
],
),
);
},
imageUrl: imageUrl,
errorWidget: (context, url, error) {
return AttachmentError(size: size);
},
fit: BoxFit.cover,
),
),
SizedBox(
height: 4.0,
),
Container(
color: StreamChatTheme.of(context)
@@ -236,6 +155,38 @@ class GiphyAttachment extends AttachmentWidget {
.withOpacity(0.2),
height: 50.0,
),
Expanded(
child: Container(
height: 50,
child: TextButton(
onPressed: () {
streamChannel.channel.sendAction(message, {
'image_action': 'shuffle',
});
},
child: Text(
'Shuffle',
style: StreamChatTheme.of(context)
.textTheme
.bodyBold
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5),
),
),
),
),
),
Container(
width: 0.5,
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.2),
height: 50.0,
),
Expanded(
child: Container(
height: 50,
@@ -261,9 +212,7 @@ class GiphyAttachment extends AttachmentWidget {
],
),
),
SizedBox(
height: 4.0,
),
SizedBox(height: 4.0),
Align(
alignment: Alignment.centerRight,
child: Padding(
@@ -143,7 +143,7 @@ class _ChannelFileDisplayScreenState extends State<ChannelFileDisplayScreen> {
}
return LazyLoadScrollView(
onEndOfPage: () => messageSearchBloc.loadMore(
onEndOfPage: () => messageSearchBloc.search(
filter: {
'cid': {
r'$in': [StreamChannel.of(context).channel.cid]
@@ -628,13 +628,14 @@ class _ChannelListViewState extends State<ChannelListView> {
),
);
}
return Container(
height: 100,
padding: EdgeInsets.all(32),
child: Center(
child: snapshot.data ? CircularProgressIndicator() : Container(),
),
);
return snapshot.data
? Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: const CircularProgressIndicator(),
),
)
: Offstage();
});
}
@@ -166,7 +166,7 @@ class _ChannelMediaDisplayScreenState extends State<ChannelMediaDisplayScreen> {
}
return LazyLoadScrollView(
onEndOfPage: () => messageSearchBloc.loadMore(
onEndOfPage: () => messageSearchBloc.search(
filter: {
'cid': {
r'$in': [StreamChannel.of(context).channel.cid]
@@ -170,14 +170,14 @@ class ChannelPreview extends StatelessWidget {
if (lastMessageAt.millisecondsSinceEpoch >=
startOfDay.millisecondsSinceEpoch) {
stringDate = Jiffy(lastMessageAt.toLocal()).format('HH:mm');
stringDate = Jiffy(lastMessageAt.toLocal()).jm;
} else if (lastMessageAt.millisecondsSinceEpoch >=
startOfDay.subtract(Duration(days: 1)).millisecondsSinceEpoch) {
stringDate = 'Yesterday';
} else if (startOfDay.difference(lastMessageAt).inDays < 7) {
stringDate = Jiffy(lastMessageAt.toLocal()).EEEE;
} else {
stringDate = Jiffy(lastMessageAt.toLocal()).format('dd/MM/yyyy');
stringDate = Jiffy(lastMessageAt.toLocal()).yMd;
}
return Text(
@@ -28,14 +28,14 @@ class DateDivider extends StatelessWidget {
now.subtract(Duration(days: 7)),
Units.DAY,
)) {
dayInfo = createdAt.format('EEEE');
dayInfo = createdAt.EEEE;
} else if (Jiffy(createdAt).isAfter(
Jiffy(now).subtract(years: 1),
Units.DAY,
)) {
dayInfo = createdAt.format('MMMM d');
dayInfo = createdAt.MMMd;
} else {
dayInfo = createdAt.format('MMMM d');
dayInfo = createdAt.MMMd;
}
if (uppercase) dayInfo = dayInfo.toUpperCase();
@@ -97,3 +97,8 @@ extension InputDecorationX on InputDecoration {
);
}
}
extension BuildContextX on BuildContext {
double get textScaleFactor =>
MediaQuery.maybeOf(this)?.textScaleFactor ?? 1.0;
}
@@ -185,21 +185,22 @@ class _FullScreenMediaState extends State<FullScreenMedia>
widget.message, StreamChannel.of(context).channel);
},
),
ImageFooter(
currentPage: _currentPage,
totalPages: widget.mediaAttachments.length,
mediaAttachments: widget.mediaAttachments,
message: widget.message,
mediaSelectedCallBack: (val) {
setState(() {
_currentPage = val;
_pageController.animateToPage(val,
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut);
Navigator.pop(context);
});
},
),
if (widget.message.type != 'ephemeral')
ImageFooter(
currentPage: _currentPage,
totalPages: widget.mediaAttachments.length,
mediaAttachments: widget.mediaAttachments,
message: widget.message,
mediaSelectedCallBack: (val) {
setState(() {
_currentPage = val;
_pageController.animateToPage(val,
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut);
Navigator.pop(context);
});
},
),
],
),
),
@@ -220,7 +221,7 @@ class _FullScreenMediaState extends State<FullScreenMedia>
24) {
return 'yesterday';
} else {
return 'on ${Jiffy(dateTime).format("MMM do")}';
return 'on ${Jiffy(dateTime).MMMd}';
}
}
@@ -78,6 +78,7 @@ class _ImageFooterState extends State<ImageFooter> {
@override
Widget build(BuildContext context) {
final showShareButton = !kIsWeb;
return SizedBox.fromSize(
size: Size(
MediaQuery.of(context).size.width,
@@ -91,8 +92,10 @@ class _ImageFooterState extends State<ImageFooter> {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
kIsWeb
? SizedBox()
!showShareButton
? Container(
width: 48,
)
: IconButton(
icon: StreamSvgIcon.iconShare(
size: 24.0,
@@ -63,38 +63,43 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
backgroundColor:
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color,
actions: <Widget>[
IconButton(
icon: StreamSvgIcon.iconMenuPoint(
color: StreamChatTheme.of(context).colorTheme.black,
if (message.type != 'ephemeral')
IconButton(
icon: StreamSvgIcon.iconMenuPoint(
color: StreamChatTheme.of(context).colorTheme.black,
),
onPressed: () {
_showMessageActionModalBottomSheet(context);
},
),
onPressed: () {
_showMessageActionModalBottomSheet(context);
},
),
],
centerTitle: true,
title: InkWell(
onTap: onTitleTap,
child: Container(
height: preferredSize.height,
width: preferredSize.width,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
userName,
style: StreamChatTheme.of(context).textTheme.headlineBold,
title: message.type != 'ephemeral'
? InkWell(
onTap: onTitleTap,
child: Container(
height: preferredSize.height,
width: preferredSize.width,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
userName,
style: StreamChatTheme.of(context).textTheme.headlineBold,
),
Text(
sentAt,
style: StreamChatTheme.of(context)
.channelPreviewTheme
.subtitle,
),
],
),
),
Text(
sentAt,
style: StreamChatTheme.of(context).channelPreviewTheme.subtitle,
),
],
),
),
),
)
: SizedBox(),
);
}
@@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
import '../stream_chat_flutter.dart';
/// This widget is used for showing user tiles for mentions
/// Use [title], [subtitle], [leading], [trailing] for substituting widgets in respective positions
class MentionTile extends StatelessWidget {
/// Member to display in the tile
final Member member;
/// Widget to display as title
final Widget title;
/// Widget to display below [title]
final Widget subtitle;
/// Widget at the start of the tile
final Widget leading;
/// Widget at the end of tile
final Widget trailing;
MentionTile(
this.member, {
this.title,
this.subtitle,
this.leading,
this.trailing,
});
@override
Widget build(BuildContext context) {
return Container(
height: 56.0,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: 16.0,
),
leading ??
UserAvatar(
constraints: BoxConstraints.tight(
Size(
40,
40,
),
),
user: member.user,
),
SizedBox(
width: 8.0,
),
Expanded(
child: Align(
alignment: Alignment.centerLeft,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
title ??
Text(
'${member.user.name}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: StreamChatTheme.of(context).textTheme.bodyBold,
),
SizedBox(
height: 2.0,
),
subtitle ??
Text(
'@${member.userId}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: StreamChatTheme.of(context)
.textTheme
.footnoteBold
.copyWith(
color:
StreamChatTheme.of(context).colorTheme.grey,
),
),
],
),
),
),
trailing ??
Padding(
padding: const EdgeInsets.only(right: 18.0, left: 8.0),
child: StreamSvgIcon.mentions(
color: StreamChatTheme.of(context).colorTheme.accentBlue,
),
),
],
),
);
}
}
@@ -16,7 +16,6 @@ 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/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/user_avatar.dart';
import 'package:stream_chat_flutter/src/video_service.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:substring_highlight/substring_highlight.dart';
@@ -32,6 +31,13 @@ typedef AttachmentThumbnailBuilder = Widget Function(
Attachment,
);
/// Builder function for building a mention tile
/// Use [MentionTile] for the default implementation
typedef MentionTileBuilder = Widget Function(
BuildContext context,
Member member,
);
enum ActionsLocation {
left,
right,
@@ -125,6 +131,7 @@ class MessageInput extends StatefulWidget {
this.idleSendButton,
this.activeSendButton,
this.showCommandsButton = true,
this.mentionsTileBuilder,
}) : super(key: key);
/// Message to edit
@@ -191,6 +198,9 @@ class MessageInput extends StatefulWidget {
/// Send button widget in an active state
final Widget activeSendButton;
/// Customize the tile for the mentions overlay
final MentionTileBuilder mentionsTileBuilder;
@override
MessageInputState createState() => MessageInputState();
@@ -319,7 +329,11 @@ class MessageInputState extends State<MessageInput> {
),
if (widget.parentMessage != null && !widget.hideSendAsDm)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
padding: const EdgeInsets.only(
right: 12.0,
left: 12.0,
bottom: 12.0,
),
child: _buildDmCheckbox(),
),
_buildFilePickerSection(),
@@ -354,68 +368,68 @@ class MessageInputState extends State<MessageInput> {
}
Widget _buildDmCheckbox() {
return Container(
height: 36,
padding: const EdgeInsets.only(
left: 12,
bottom: 12,
top: 8,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
height: 16,
width: 16,
foregroundDecoration: BoxDecoration(
border: _sendAsDm
? null
: Border.all(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5),
width: 2,
),
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
height: 16,
width: 16,
foregroundDecoration: BoxDecoration(
border: _sendAsDm
? null
: Border.all(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5),
width: 2,
),
borderRadius: BorderRadius.circular(3),
),
child: Center(
child: Material(
borderRadius: BorderRadius.circular(3),
),
child: Center(
child: Material(
borderRadius: BorderRadius.circular(3),
color: _sendAsDm
? StreamChatTheme.of(context).colorTheme.accentBlue
: StreamChatTheme.of(context).colorTheme.white,
child: InkWell(
onTap: () {
setState(() {
_sendAsDm = !_sendAsDm;
});
},
child: AnimatedCrossFade(
duration: Duration(milliseconds: 300),
reverseDuration: Duration(milliseconds: 300),
crossFadeState: _sendAsDm
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
firstChild: StreamSvgIcon.check(
size: 16.0,
color: StreamChatTheme.of(context).colorTheme.white,
),
secondChild: SizedBox(
height: 16,
width: 16,
),
color: _sendAsDm
? StreamChatTheme.of(context).colorTheme.accentBlue
: StreamChatTheme.of(context).colorTheme.white,
child: InkWell(
onTap: () {
setState(() {
_sendAsDm = !_sendAsDm;
});
},
child: AnimatedCrossFade(
duration: Duration(milliseconds: 300),
reverseDuration: Duration(milliseconds: 300),
crossFadeState: _sendAsDm
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
firstChild: StreamSvgIcon.check(
size: 16.0,
color: StreamChatTheme.of(context).colorTheme.white,
),
secondChild: SizedBox(
height: 16,
width: 16,
),
),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Text('Also send as direct message'),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Text(
'Also send as direct message',
style: StreamChatTheme.of(context).textTheme.footnote.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5),
),
),
],
),
),
],
);
}
@@ -454,7 +468,9 @@ class MessageInputState extends State<MessageInput> {
? pi
: 0,
child: StreamSvgIcon.emptyCircleLeft(
color: StreamChatTheme.of(context).colorTheme.accentBlue,
color: StreamChatTheme.of(context)
.messageInputTheme
.expandButtonColor,
),
),
padding: const EdgeInsets.all(0),
@@ -491,10 +507,17 @@ class MessageInputState extends State<MessageInput> {
Expanded _buildTextInput(BuildContext context) {
final theme = StreamChatTheme.of(context);
final margin = (widget.sendButtonLocation == SendButtonLocation.inside
? const EdgeInsets.only(right: 8.0)
: EdgeInsets.zero) +
(widget.actionsLocation != ActionsLocation.left
? const EdgeInsets.only(left: 8.0)
: EdgeInsets.zero);
return Expanded(
child: Center(
child: Container(
clipBehavior: Clip.antiAlias,
margin: margin,
decoration: BoxDecoration(
borderRadius: theme.messageInputTheme.borderRadius,
gradient: _focusNode.hasFocus
@@ -656,6 +679,7 @@ class MessageInputState extends State<MessageInput> {
Timer _debounce;
String _previousValue;
void _onChanged(BuildContext context, String s) {
if (s == _previousValue) {
return;
@@ -1113,6 +1137,7 @@ class MessageInputState extends State<MessageInput> {
_showErrorAlert(
'The file is too large to upload. The file size limit is 20MB.',
);
return;
}
}
@@ -1301,80 +1326,9 @@ class MessageInputState extends State<MessageInput> {
_mentionsOverlay?.remove();
_mentionsOverlay = null;
},
child: Container(
height: 56.0,
child: Row(
crossAxisAlignment:
CrossAxisAlignment.center,
children: [
SizedBox(
width: 16.0,
),
UserAvatar(
constraints: BoxConstraints.tight(
Size(
40,
40,
),
),
user: m.user,
),
SizedBox(
width: 8.0,
),
Expanded(
child: Align(
alignment: Alignment.centerLeft,
child: Column(
mainAxisAlignment:
MainAxisAlignment.center,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
'${m.user.name}',
maxLines: 1,
overflow:
TextOverflow.ellipsis,
style: StreamChatTheme.of(
context)
.textTheme
.bodyBold,
),
SizedBox(
height: 2.0,
),
Text(
'@${m.userId}',
maxLines: 1,
overflow:
TextOverflow.ellipsis,
style: StreamChatTheme.of(
context)
.textTheme
.footnoteBold
.copyWith(
color: StreamChatTheme
.of(context)
.colorTheme
.grey),
),
],
),
),
),
Padding(
padding: const EdgeInsets.only(
right: 18.0, left: 8.0),
child: StreamSvgIcon.mentions(
color: StreamChatTheme.of(context)
.colorTheme
.accentBlue,
),
),
],
),
),
child: widget.mentionsTileBuilder != null
? widget.mentionsTileBuilder(context, m)
: MentionTile(m),
),
);
},
@@ -1962,12 +1916,6 @@ class MessageInputState extends State<MessageInput> {
final extraDataMap = <String, dynamic>{};
if (mimeType?.type == 'video' || mimeType?.type == 'image') {
attachmentType = mimeType.type;
} else {
attachmentType = 'file';
}
if (mimeType?.subtype != null) {
extraDataMap['mime_type'] = mimeType.subtype.toLowerCase();
}
@@ -1982,8 +1930,6 @@ class MessageInputState extends State<MessageInput> {
extraData: extraDataMap.isNotEmpty ? extraDataMap : null,
);
_attachments[attachment.id] = attachment;
if (file.size > _kMaxAttachmentSize) {
if (attachmentType == 'Video') {
final mediaInfo = await VideoService.compressVideo(file.path);
@@ -1992,7 +1938,6 @@ class MessageInputState extends State<MessageInput> {
_showErrorAlert(
'The file is too large to upload. The file size limit is 20MB. We tried compressing it, but it was not enough.',
);
_attachments.remove(attachment.id);
return;
}
file = AttachmentFile(
@@ -2005,9 +1950,12 @@ class MessageInputState extends State<MessageInput> {
_showErrorAlert(
'The file is too large to upload. The file size limit is 20MB.',
);
return;
}
}
_attachments[attachment.id] = attachment;
setState(() {
_attachments.update(attachment.id, (it) {
return it.copyWith(
@@ -2233,7 +2181,7 @@ class MessageInputState extends State<MessageInput> {
void _parseExistingMessage(Message message) {
textEditingController.text = message.text;
_messageIsPresent = true;
for (final attachment in message.attachments) {
for (final attachment in message?.attachments) {
_attachments[attachment.id] = attachment.copyWith(
uploadState: attachment.uploadState ?? UploadState.success(),
);
@@ -141,6 +141,7 @@ class MessageListView extends StatefulWidget {
this.onMessageTap,
this.onSystemMessageTap,
this.onAttachmentTap,
this.textBuilder,
}) : super(key: key);
/// Function used to build a custom message widget
@@ -230,9 +231,12 @@ class MessageListView extends StatefulWidget {
/// Called when system message is tapped
final OnMessageTap onSystemMessageTap;
// Customize onTap on attachment
/// Customize onTap on attachment
final void Function(Message message, Attachment attachment) onAttachmentTap;
/// Customize the MessageWidget textBuilder
final void Function(BuildContext context, Message message) textBuilder;
@override
_MessageListViewState createState() => _MessageListViewState();
}
@@ -839,6 +843,7 @@ class _MessageListViewState extends State<MessageListView> {
}
FocusScope.of(context).unfocus();
},
textBuilder: widget.textBuilder,
);
}
@@ -1015,6 +1020,7 @@ class _MessageListViewState extends State<MessageListView> {
FocusScope.of(context).unfocus();
},
onAttachmentTap: widget.onAttachmentTap,
textBuilder: widget.textBuilder,
);
if (!message.isDeleted &&
@@ -84,9 +84,9 @@ class MessageSearchItem extends StatelessWidget {
if (now.year != createdAt.year ||
now.month != createdAt.month ||
now.day != createdAt.day) {
stringDate = Jiffy(createdAt.toLocal()).format('dd/MM/yyyy');
stringDate = Jiffy(createdAt.toLocal()).yMd;
} else {
stringDate = Jiffy(createdAt.toLocal()).format('HH:mm');
stringDate = Jiffy(createdAt.toLocal()).jm;
}
return Text(
@@ -262,17 +262,13 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
);
if (widget.pullToRefresh) {
child = RefreshIndicator(
onRefresh: () async {
_messageSearchListController.loadData();
},
onRefresh: () => _messageSearchListController.loadData(),
child: child,
);
}
child = LazyLoadScrollView(
onEndOfPage: () async {
return _messageSearchListController.paginateData();
},
onEndOfPage: () => _messageSearchListController.paginateData(),
child: child,
);
@@ -585,7 +585,8 @@ class _MessageWidgetState extends State<MessageWidget>
),
],
),
if (showBottomRow) SizedBox(height: 20.0),
if (showBottomRow)
SizedBox(height: context.textScaleFactor * 18.0),
],
),
if (showBottomRow)
@@ -682,6 +683,8 @@ class _MessageWidgetState extends State<MessageWidget>
}
};
const usernameKey = Key('username');
children.addAll([
if (showInChannel || showThreadReplyIndicator) ...[
if (showThreadParticipants)
@@ -697,7 +700,10 @@ class _MessageWidgetState extends State<MessageWidget>
if (showUsername)
Text(
widget.message.user.name,
maxLines: 1,
key: usernameKey,
style: widget.messageTheme.messageAuthor,
overflow: TextOverflow.ellipsis,
),
if (showTimeStamp)
Text(
@@ -710,18 +716,17 @@ class _MessageWidgetState extends State<MessageWidget>
final showThreadTail = !(hasUrlAttachments || isGiphy || isOnlyEmoji) &&
(showThreadReplyIndicator || showInChannel);
return Flex(
direction: Axis.horizontal,
clipBehavior: Clip.none,
return Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (showThreadTail)
Container(
margin: EdgeInsets.only(
bottom: widget.messageTheme.replies.fontSize / 2,
bottom: context.textScaleFactor *
(widget.messageTheme.replies.fontSize / 2),
),
child: CustomPaint(
size: const Size(16, 32),
size: Size(16, 32) * context.textScaleFactor,
painter: _ThreadReplyPainter(
context: context,
color: widget.messageTheme.messageBorderColor,
@@ -729,16 +734,20 @@ class _MessageWidgetState extends State<MessageWidget>
),
),
...children.map(
(child) => Transform(
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
alignment: Alignment.center,
child: Container(
height: 16,
child: Center(
(child) {
Widget mappedChild = Transform(
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
alignment: Alignment.center,
child: Container(
height: context.textScaleFactor * 14,
child: child,
),
),
),
);
if (child.key == usernameKey) {
mappedChild = Flexible(child: mappedChild);
}
return mappedChild;
},
),
].insertBetween(const SizedBox(width: 8.0)),
);
@@ -3,9 +3,11 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_portal/flutter_portal.dart';
import 'package:jiffy/jiffy.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'dart:ui' as ui;
/// Widget used to provide information about the chat to the widget tree
///
@@ -121,4 +123,11 @@ class StreamChatState extends State<StreamChat> {
void initState() {
super.initState();
}
@override
void didChangeDependencies() {
final locale = ui.window.locale;
Jiffy.locale(locale.languageCode);
super.didChangeDependencies();
}
}
@@ -272,6 +272,7 @@ class StreamChatThemeData {
sendAnimationDuration: Duration(milliseconds: 300),
actionButtonColor: colorTheme.accentBlue,
actionButtonIdleColor: colorTheme.grey,
expandButtonColor: colorTheme.accentBlue,
sendButtonColor: colorTheme.accentBlue,
sendButtonIdleColor: colorTheme.greyGainsboro,
inputBackground: colorTheme.white,
@@ -935,6 +936,9 @@ class MessageInputTheme {
/// Background color of [MessageInput] action buttons
final Color actionButtonIdleColor;
/// Background color of [MessageInput] expand button
final Color expandButtonColor;
/// Background color of [MessageInput]
final Color inputBackground;
@@ -966,6 +970,7 @@ class MessageInputTheme {
this.activeBorderGradient,
this.idleBorderGradient,
this.borderRadius,
this.expandButtonColor,
});
/// Returns a new [MessageInputTheme] replacing some of its properties
@@ -976,6 +981,7 @@ class MessageInputTheme {
Color sendButtonColor,
Color actionButtonIdleColor,
Color sendButtonIdleColor,
Color expandButtonColor,
TextStyle inputTextStyle,
InputDecoration inputDecoration,
Gradient activeBorderGradient,
@@ -990,6 +996,7 @@ class MessageInputTheme {
sendButtonColor: sendButtonColor ?? this.sendButtonColor,
actionButtonIdleColor:
actionButtonIdleColor ?? this.actionButtonIdleColor,
expandButtonColor: expandButtonColor ?? this.expandButtonColor,
inputTextStyle: inputTextStyle ?? this.inputTextStyle,
sendButtonIdleColor: sendButtonIdleColor ?? this.sendButtonIdleColor,
inputDecoration: inputDecoration ?? this.inputDecoration,
@@ -1014,6 +1021,7 @@ class MessageInputTheme {
activeBorderGradient: other.activeBorderGradient,
idleBorderGradient: other.idleBorderGradient,
borderRadius: other.borderRadius,
expandButtonColor: other.expandButtonColor,
);
}
}
@@ -193,9 +193,7 @@ class _UserListViewState extends State<UserListView>
return child;
} else {
return RefreshIndicator(
onRefresh: () async {
_userListController.loadData();
},
onRefresh: () => _userListController.loadData(),
child: child,
);
}
@@ -243,9 +241,7 @@ class _UserListViewState extends State<UserListView>
child: Text(message),
),
TextButton(
onPressed: () {
_userListController.loadData();
},
onPressed: () => _userListController.loadData(),
child: Text('Retry'),
),
],
@@ -300,9 +296,7 @@ class _UserListViewState extends State<UserListView>
);
return LazyLoadScrollView(
onEndOfPage: () async {
return _userListController.paginateData();
},
onEndOfPage: () => _userListController.paginateData(),
child: child,
);
}
@@ -39,4 +39,5 @@ export 'src/channel_media_display_screen.dart';
export 'src/info_tile.dart';
export 'src/stream_chat.dart';
export 'src/connection_status_builder.dart';
export 'src/mention_tile.dart';
export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
+2 -5
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: 1.5.1
version: 1.5.2
repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -11,7 +11,7 @@ environment:
dependencies:
flutter:
sdk: flutter
stream_chat_flutter_core: ^1.5.0
stream_chat_flutter_core: ^1.5.1
photo_view: ^0.11.0
rxdart: ^0.25.0
scrollable_positioned_list: ^0.1.8
@@ -30,16 +30,13 @@ dependencies:
flutter_keyboard_visibility: ^4.0.2
video_compress: ^2.1.1
visibility_detector: ^0.1.5
http_parser: ^3.1.4
meta: ^1.2.4
lottie: ^0.7.0+1
substring_highlight: ^0.1.2
flutter_slidable: ^0.5.7
clipboard: ^0.1.2+8
image_gallery_saver: ^1.6.7
share_plus: ^1.2.0
photo_manager: ^1.0.0
transparent_image: ^1.0.0
ezanimation: ^0.4.1
synchronized: ^2.1.0
characters: ^1.0.0
@@ -69,7 +69,7 @@ void main() {
),
));
expect(find.text('22/06/2020'), findsOneWidget);
expect(find.text('6/22/2020'), findsOneWidget);
expect(find.text('test name'), findsOneWidget);
expect(find.text('1'), findsOneWidget);
expect(find.text('hello'), findsOneWidget);
@@ -1,3 +1,8 @@
## 1.5.1
* Improved test coverage to > 90%
* Minor fixes and improvements
## 1.5.0
* Minor fixes and improvements
@@ -0,0 +1,145 @@
analyzer:
exclude:
- lib/**/*.g.dart
- lib/**/*.freezed.dart
- example/*
- test/*
linter:
rules:
- always_use_package_imports
- avoid_empty_else
- avoid_relative_lib_imports
- avoid_slow_async_io
- avoid_types_as_parameter_names
- cancel_subscriptions
- close_sinks
- control_flow_in_finally
- empty_statements
- hash_and_equals
- invariant_booleans
- iterable_contains_unrelated_type
- list_remove_unrelated_type
- literal_only_boolean_expressions
- no_adjacent_strings_in_list
- no_duplicate_case_values
- no_logic_in_create_state
- prefer_void_to_null
- test_types_in_equals
- throw_in_finally
- unnecessary_statements
- unrelated_type_equality_checks
- omit_local_variable_types
- use_key_in_widget_constructors
- valid_regexps
- always_declare_return_types
- always_require_non_null_named_parameters
- annotate_overrides
- avoid_bool_literals_in_conditional_expressions
- avoid_catching_errors
- avoid_init_to_null
- avoid_null_checks_in_equality_operators
- avoid_positional_boolean_parameters
- avoid_private_typedef_functions
- avoid_redundant_argument_values
- avoid_return_types_on_setters
- avoid_returning_null
- avoid_returning_null_for_void
- avoid_shadowing_type_parameters
- avoid_single_cascade_in_expression_statements
- avoid_unnecessary_containers
- avoid_unused_constructor_parameters
- await_only_futures
- camel_case_extensions
- camel_case_types
- cascade_invocations
- constant_identifier_names
- curly_braces_in_flow_control_structures
- directives_ordering
- empty_catches
- empty_constructor_bodies
- exhaustive_cases
- file_names
- implementation_imports
- join_return_with_assignment
- leading_newlines_in_multiline_strings
- library_names
- library_prefixes
- lines_longer_than_80_chars
- missing_whitespace_between_adjacent_strings
- non_constant_identifier_names
- null_closures
- one_member_abstracts
- only_throw_errors
- package_api_docs
- package_prefixed_library_names
- parameter_assignments
- prefer_adjacent_string_concatenation
- prefer_asserts_in_initializer_lists
- prefer_asserts_with_message
- prefer_collection_literals
- prefer_conditional_assignment
- prefer_const_constructors
- prefer_const_constructors_in_immutables
- prefer_const_declarations
- prefer_const_literals_to_create_immutables
- prefer_constructors_over_static_methods
- prefer_contains
- prefer_equal_for_default_values
- prefer_expression_function_bodies
- prefer_final_fields
- prefer_final_in_for_each
- prefer_final_locals
- prefer_function_declarations_over_variables
- prefer_generic_function_type_aliases
- prefer_if_elements_to_conditional_expressions
- prefer_if_null_operators
- prefer_initializing_formals
- prefer_inlined_adds
- prefer_int_literals
- prefer_interpolation_to_compose_strings
- prefer_is_empty
- prefer_is_not_empty
- prefer_is_not_operator
- prefer_null_aware_operators
- prefer_single_quotes
- prefer_spread_collections
- prefer_typing_uninitialized_variables
- provide_deprecation_message
- public_member_api_docs
- recursive_getters
- sized_box_for_whitespace
- slash_for_doc_comments
- sort_child_properties_last
- sort_constructors_first
- sort_unnamed_constructors_first
- type_annotate_public_apis
- type_init_formals
- unnecessary_await_in_return
- unnecessary_brace_in_string_interps
- unnecessary_const
- unnecessary_getters_setters
- unnecessary_lambdas
- unnecessary_new
- unnecessary_null_aware_assignments
- unnecessary_null_in_if_null_operators
- unnecessary_nullable_for_final_variable_declarations
- unnecessary_parenthesis
- unnecessary_raw_strings
- unnecessary_string_escapes
- unnecessary_string_interpolations
- unnecessary_this
- use_is_even_rather_than_modulo
- use_late_for_private_fields_and_variables
- use_rethrow_when_possible
- use_setters_to_change_properties
- use_to_and_as_if_applicable
- package_names
- sort_pub_dependencies
# To be added when null-safe:
# - cast_nullable_to_non_nullable
#- unnecessary_null_checks
# - tighten_type_of_initializing_formals
# - null_check_on_nullable_type_parameter
@@ -5,11 +5,11 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter_core/src/channels_bloc.dart';
import 'package:stream_chat_flutter_core/src/stream_chat_core.dart';
import 'package:stream_chat_flutter_core/src/typedef.dart';
import 'stream_chat_core.dart';
/// [ChannelListCore] is a simplified class that allows fetching a list of channels while exposing UI builders.
/// [ChannelListCore] is a simplified class that allows fetching a list of
/// channels while exposing UI builders.
/// A [ChannelListController] is used to reload and paginate data.
///
///
@@ -52,10 +52,11 @@ import 'stream_chat_core.dart';
/// }
/// ```
///
/// Make sure to have a [StreamChatCore] ancestor in order to provide the information about the channels.
/// Make sure to have a [StreamChatCore] ancestor in order to provide the
/// information about the channels.
class ChannelListCore extends StatefulWidget {
/// Instantiate a new ChannelListView
ChannelListCore({
const ChannelListCore({
Key key,
@required this.errorBuilder,
@required this.emptyBuilder,
@@ -65,18 +66,31 @@ class ChannelListCore extends StatefulWidget {
this.options,
this.sort,
this.pagination = const PaginationParams(
offset: 0,
limit: 25,
),
this.channelListController,
}) : assert(errorBuilder != null),
assert(emptyBuilder != null),
assert(loadingBuilder != null),
assert(listBuilder != null),
}) : assert(
errorBuilder != null,
'Parameter errorBuilder should not be null',
),
assert(
emptyBuilder != null,
'Parameter emptyBuilder should not be null',
),
assert(
loadingBuilder != null,
'Parameter loadingBuilder should not be null',
),
assert(
listBuilder != null,
'Parameter listBuilder should not be null',
),
super(key: key);
/// A [ChannelListController] allows reloading and pagination.
/// Use [ChannelListController.loadData] and [ChannelListController.paginateData] respectively for reloading and pagination.
/// Use [ChannelListController.loadData] and
/// [ChannelListController.paginateData] respectively for reloading and
/// pagination.
final ChannelListController channelListController;
/// The builder that will be used in case of error
@@ -103,9 +117,10 @@ class ChannelListCore extends StatefulWidget {
final Map<String, dynamic> options;
/// The sorting used for the channels matching the filters.
/// Sorting is based on field and direction, multiple sorting options can be provided.
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count.
/// Direction can be ascending or descending.
/// Sorting is based on field and direction, multiple sorting options can be
/// provided.
/// You can sort based on last_updated, last_message_at, updated_at, created
/// _at or member_count. Direction can be ascending or descending.
final List<SortOption<ChannelModel>> sort;
/// Pagination parameters
@@ -115,10 +130,11 @@ class ChannelListCore extends StatefulWidget {
final PaginationParams pagination;
@override
_ChannelListCoreState createState() => _ChannelListCoreState();
ChannelListCoreState createState() => ChannelListCoreState();
}
class _ChannelListCoreState extends State<ChannelListCore> {
/// The current state of the [ChannelListCore].
class ChannelListCoreState extends State<ChannelListCore> {
@override
Widget build(BuildContext context) {
final channelsBloc = ChannelsBloc.of(context);
@@ -128,37 +144,25 @@ class _ChannelListCoreState extends State<ChannelListCore> {
StreamBuilder<List<Channel>> _buildListView(
ChannelsBlocState channelsBlocState,
) {
return StreamBuilder<List<Channel>>(
stream: channelsBlocState.channelsStream,
builder: (context, snapshot) {
if (snapshot.hasError) {
return _buildErrorWidget(snapshot, context, channelsBlocState);
}
if (!snapshot.hasData) {
return widget.loadingBuilder(context);
}
final channels = snapshot.data;
if (channels.isEmpty) {
return widget.emptyBuilder(context);
}
return widget.listBuilder(context, channels);
},
);
}
Widget _buildErrorWidget(
AsyncSnapshot<List<Channel>> snapshot,
BuildContext context,
ChannelsBlocState channelsBlocState,
) {
if (snapshot.error is Error) {
print((snapshot.error as Error).stackTrace);
}
return widget.errorBuilder(context, snapshot.error);
}
) =>
StreamBuilder<List<Channel>>(
stream: channelsBlocState.channelsStream,
builder: (context, snapshot) {
if (snapshot.hasError) {
return widget.errorBuilder(context, snapshot.error);
}
if (!snapshot.hasData) {
return widget.loadingBuilder(context);
}
final channels = snapshot.data;
if (channels.isEmpty) {
return widget.emptyBuilder(context);
}
return widget.listBuilder(context, channels);
},
);
/// Fetches initial channels and updates the widget
Future<void> loadData() {
final channelsBloc = ChannelsBloc.of(context);
return channelsBloc.queryChannels(
@@ -169,6 +173,7 @@ class _ChannelListCoreState extends State<ChannelListCore> {
);
}
/// Fetches more channels with updated pagination and updates the widget
Future<void> paginateData() {
final channelsBloc = ChannelsBloc.of(context);
return channelsBloc.queryChannels(
@@ -209,9 +214,9 @@ class _ChannelListCoreState extends State<ChannelListCore> {
if (widget.filter?.toString() != oldWidget.filter?.toString() ||
jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) ||
widget.options?.toString() != oldWidget.options?.toString() ||
widget.pagination?.toJson()?.toString() !=
oldWidget.pagination?.toJson()?.toString() ||
widget.options?.toString() != oldWidget.options?.toString()) {
oldWidget.pagination?.toJson()?.toString()) {
loadData();
}
}
@@ -223,14 +228,15 @@ class _ChannelListCoreState extends State<ChannelListCore> {
}
}
/// Controller used for loading more data and controlling pagination in [ChannelListCore].
/// Controller used for loading more data and controlling pagination in
/// [ChannelListCore].
class ChannelListController {
/// This function calls Stream's servers to load a list of channels. If there is existing data,
/// calling this function causes a reload.
/// This function calls Stream's servers to load a list of channels.
/// If there is existing data, calling this function causes a reload.
AsyncCallback loadData;
/// This function is used to load another page of data. Note, [loadData] should be
/// used to populate the initial page of data. Calling [paginateData] performs a query
/// to load subsequent pages.
/// This function is used to load another page of data. Note, [loadData]
/// should be used to populate the initial page of data. Calling
/// [paginateData] performs a query to load subsequent pages.
AsyncCallback paginateData;
}
@@ -1,5 +1,6 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/stream_chat.dart';
@@ -16,26 +17,29 @@ import 'package:stream_chat_flutter_core/src/stream_chat_core.dart';
///
/// API docs: https://getstream.io/chat/docs/flutter-dart/query_channels/
class ChannelsBloc extends StatefulWidget {
/// Creates a new [ChannelsBloc]. The parameter [child] must be supplied and not null.
/// Creates a new [ChannelsBloc]. The parameter [child] must be supplied and
/// not null.
const ChannelsBloc({
Key key,
@required this.child,
this.lockChannelsOrder = false,
this.channelsComparator,
this.shouldAddChannel,
}) : assert(child != null),
}) : assert(child != null, 'Parameter child should not be null.'),
super(key: key);
/// The widget child
final Widget child;
/// Set this to true to prevent channels to be brought to the top of the list when a new message arrives
/// Set this to true to prevent channels to be brought to the top of the list
/// when a new message arrives
final bool lockChannelsOrder;
/// Comparator used to sort the channels when a message.new event is received
final Comparator<Channel> channelsComparator;
/// Function used to evaluate if a channel should be added to the list when a message.new event is received
/// Function used to evaluate if a channel should be added to the list when a
/// message.new event is received
final bool Function(Event) shouldAddChannel;
@override
@@ -70,10 +74,9 @@ class ChannelsBlocState extends State<ChannelsBloc>
/// The current channel list as a stream
Stream<List<Channel>> get channelsStream => _channelsController.stream;
final BehaviorSubject<bool> _queryChannelsLoadingController =
BehaviorSubject.seeded(false);
final _queryChannelsLoadingController = BehaviorSubject.seeded(false);
final BehaviorSubject<List<Channel>> _channelsController = BehaviorSubject();
final _channelsController = BehaviorSubject<List<Channel>>();
/// The stream notifying the state of queryChannel call
Stream<bool> get queryChannelsLoading =>
@@ -81,45 +84,58 @@ class ChannelsBlocState extends State<ChannelsBloc>
final List<Channel> _hiddenChannels = [];
bool _paginationEnded = false;
/// Calls [client.queryChannels] updating [queryChannelsLoading] stream
Future<void> queryChannels({
Map<String, dynamic> filter,
List<SortOption<ChannelModel>> sortOptions,
PaginationParams paginationParams,
Map<String, dynamic> options,
bool onlyOffline = false,
}) async {
final client = StreamChatCore.of(context).client;
if (client.state?.user == null ||
_queryChannelsLoadingController.value == true) {
if (_paginationEnded || _queryChannelsLoadingController.value == true) {
return;
}
_queryChannelsLoadingController.sink.add(true);
if (_channelsController.hasValue) {
_queryChannelsLoadingController.add(true);
}
try {
final clear = paginationParams == null ||
paginationParams.offset == null ||
paginationParams.offset == 0;
final oldChannels = List<Channel>.from(channels ?? []);
var newChannels = <Channel>[];
await for (final channels in client.queryChannels(
filter: filter,
sort: sortOptions,
options: options,
paginationParams: paginationParams,
)) {
newChannels = channels;
if (clear) {
_channelsController.add(channels);
} else {
final l = oldChannels + channels;
_channelsController.add(l);
final temp = oldChannels + channels;
_channelsController.add(temp);
}
if (_channelsController.hasValue &&
_queryChannelsLoadingController.value) {
_queryChannelsLoadingController.sink.add(false);
}
_queryChannelsLoadingController.sink.add(false);
}
} catch (err, stackTrace) {
print(err);
print(stackTrace);
_queryChannelsLoadingController.addError(err, stackTrace);
if (newChannels.isEmpty || newChannels.length < paginationParams.limit) {
_paginationEnded = true;
}
} catch (e, stk) {
if (_channelsController.hasValue) {
_queryChannelsLoadingController.addError(e, stk);
} else {
_channelsController.addError(e, stk);
}
}
}
@@ -135,15 +151,14 @@ class ChannelsBlocState extends State<ChannelsBloc>
_subscriptions.add(client.on(EventType.messageNew).listen((e) {
final newChannels = List<Channel>.from(channels ?? []);
final index = newChannels.indexWhere((c) => c.cid == e.cid);
if (index > -1) {
if (index != -1) {
if (index > 0) {
final channel = newChannels.removeAt(index);
newChannels.insert(0, channel);
}
} else if (widget.shouldAddChannel != null &&
widget.shouldAddChannel(e)) {
} else if (widget.shouldAddChannel?.call(e) == true) {
final hiddenIndex = _hiddenChannels.indexWhere((c) => c.cid == e.cid);
if (hiddenIndex > -1) {
if (hiddenIndex != -1) {
newChannels.insert(0, _hiddenChannels[hiddenIndex]);
_hiddenChannels.removeAt(hiddenIndex);
} else {
@@ -170,13 +185,14 @@ class ChannelsBlocState extends State<ChannelsBloc>
_channelsController.add(newChannels);
}
}));
// ignore: cascade_invocations
_subscriptions.add(client
.on(
EventType.channelDeleted,
EventType.notificationRemovedFromChannel,
)
.listen((e) {
// ignore: cascade_invocations
final channel = e.channel;
_channelsController.add(List.from(
(channels ?? [])..removeWhere((c) => c.cid == channel.cid)));
@@ -1,13 +1,13 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
enum _LoadingStatus { LOADING, STABLE }
enum _LoadingStatus { loading, stable }
/// Wrapper around a [Scrollable] which triggers [onEndOfPage]/[onStartOfPage] the Scrollable
/// reaches to the start or end of the view extent.
class LazyLoadScrollView extends StatefulWidget {
/// Creates a new instance of [LazyLoadScrollView]. The parameter [child] must be
/// supplied and not null.
/// Creates a new instance of [LazyLoadScrollView]. The parameter [child]
/// must be supplied and not null.
const LazyLoadScrollView({
Key key,
@required this.child,
@@ -17,7 +17,7 @@ class LazyLoadScrollView extends StatefulWidget {
this.onPageScrollEnd,
this.onInBetweenOfPage,
this.scrollOffset = 100,
}) : assert(child != null),
}) : assert(child != null, 'Parameter child should not be null'),
super(key: key);
/// The [Widget] that this widget watches for changes on
@@ -46,18 +46,17 @@ class LazyLoadScrollView extends StatefulWidget {
}
class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
_LoadingStatus _loadMoreStatus = _LoadingStatus.STABLE;
double _scrollPosition = 0.0;
_LoadingStatus _loadMoreStatus = _LoadingStatus.stable;
double _scrollPosition = 0;
@override
Widget build(BuildContext context) {
return NotificationListener(
child: widget.child,
onNotification: _onNotification,
);
}
Widget build(BuildContext context) =>
NotificationListener<ScrollNotification>(
onNotification: _onNotification,
child: widget.child,
);
bool _onNotification(Notification notification) {
bool _onNotification(ScrollNotification notification) {
if (notification is ScrollStartNotification) {
if (widget.onPageScrollStart != null) {
widget.onPageScrollStart();
@@ -74,7 +73,7 @@ class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
final pixels = notification.metrics.pixels;
final maxScrollExtent = notification.metrics.maxScrollExtent;
final minScrollExtent = notification.metrics.minScrollExtent;
final scrollOffset = widget.scrollOffset;
final scrollOffset = widget.scrollOffset ?? 0;
if (pixels > (minScrollExtent + scrollOffset) &&
pixels < (maxScrollExtent - scrollOffset)) {
@@ -88,26 +87,19 @@ class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
final extentAfter = notification.metrics.extentAfter;
final scrollingDown = _scrollPosition < pixels;
if (scrollOffset == null || scrollOffset == 0) {
if (extentAfter == 0) {
if (scrollingDown) {
if (extentAfter <= scrollOffset) {
_onEndOfPage();
}
if (extentBefore == 0) {
_onStartOfPage();
return true;
}
} else {
if (scrollingDown) {
if (extentAfter <= scrollOffset) {
_onEndOfPage();
}
} else {
if (extentBefore <= scrollOffset) {
_onStartOfPage();
}
if (extentBefore <= scrollOffset) {
_onStartOfPage();
return true;
}
}
_scrollPosition = pixels;
return true;
}
if (notification is OverscrollNotification) {
if (notification.overscroll > 0) {
@@ -122,22 +114,22 @@ class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
}
void _onEndOfPage() {
if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.STABLE) {
_loadMoreStatus = _LoadingStatus.LOADING;
if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.stable) {
if (widget.onEndOfPage != null) {
_loadMoreStatus = _LoadingStatus.loading;
widget.onEndOfPage().whenComplete(() {
_loadMoreStatus = _LoadingStatus.STABLE;
_loadMoreStatus = _LoadingStatus.stable;
});
}
}
}
void _onStartOfPage() {
if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.STABLE) {
_loadMoreStatus = _LoadingStatus.LOADING;
if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.stable) {
if (widget.onStartOfPage != null) {
_loadMoreStatus = _LoadingStatus.loading;
widget.onStartOfPage().whenComplete(() {
_loadMoreStatus = _LoadingStatus.STABLE;
_loadMoreStatus = _LoadingStatus.stable;
});
}
}
@@ -1,12 +1,15 @@
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter_core/src/stream_channel.dart';
import 'package:stream_chat_flutter_core/src/typedef.dart';
import 'stream_channel.dart';
/// [MessageListCore] is a simplified class that allows fetching a list of messages while exposing UI builders.
/// [MessageListCore] is a simplified class that allows fetching a list of
/// messages while exposing UI builders.
///
/// A [MessageListController] is used to paginate data.
///
/// ```dart
@@ -50,12 +53,14 @@ import 'stream_channel.dart';
/// ```
///
///
/// Make sure to have a [StreamChannel] ancestor in order to provide the information about the channels.
/// Make sure to have a [StreamChannel] ancestor in order to provide the
/// information about the channels.
///
/// The widget uses a [ListView.custom] to render the list of channels.
///
class MessageListCore extends StatefulWidget {
/// Instantiate a new [MessageListView].
MessageListCore({
const MessageListCore({
Key key,
@required this.loadingBuilder,
@required this.emptyBuilder,
@@ -65,10 +70,16 @@ class MessageListCore extends StatefulWidget {
this.parentMessage,
this.messageListController,
this.messageFilter,
}) : assert(loadingBuilder != null),
assert(emptyBuilder != null),
assert(messageListBuilder != null),
assert(errorWidgetBuilder != null),
}) : assert(loadingBuilder != null, 'loadingBuilder should not be null'),
assert(emptyBuilder != null, 'emptyBuilder should not be null'),
assert(
messageListBuilder != null,
'messageListBuilder should not be null',
),
assert(
errorWidgetBuilder != null,
'errorWidgetBuilder should not be null',
),
super(key: key);
/// A [MessageListController] allows pagination.
@@ -84,12 +95,15 @@ class MessageListCore extends StatefulWidget {
/// Function used to build an empty widget
final WidgetBuilder emptyBuilder;
/// Callback triggered when an error occurs while performing the given request.
/// This parameter can be used to display an error message to users in the event
/// of a connection failure.
/// Callback triggered when an error occurs while performing the given
/// request.
///
/// This parameter can be used to display an error message to users in the
/// event of a connection failure.
final ErrorBuilder errorWidgetBuilder;
/// If true will show a scroll to bottom message when there are new messages and the scroll offset is not zero
/// If true will show a scroll to bottom message when there are new messages
/// and the scroll offset is not zero.
final bool showScrollToBottom;
/// If the current message belongs to a `thread`, this property represents the
@@ -100,32 +114,28 @@ class MessageListCore extends StatefulWidget {
final bool Function(Message) messageFilter;
@override
_MessageListCoreState createState() => _MessageListCoreState();
MessageListCoreState createState() => MessageListCoreState();
}
class _MessageListCoreState extends State<MessageListCore> {
StreamChannelState streamChannel;
/// The current state of the [MessageListCore].
class MessageListCoreState extends State<MessageListCore> {
StreamChannelState _streamChannel;
bool get _upToDate => streamChannel.channel.state.isUpToDate;
bool get _upToDate => _streamChannel.channel.state.isUpToDate;
bool get _isThreadConversation => widget.parentMessage != null;
OwnUser get _currentUser => streamChannel.channel.client.state.user;
OwnUser get _currentUser => _streamChannel.channel.client.state.user;
int initialIndex;
double initialAlignment;
List<Message> messages = <Message>[];
bool initialMessageHighlightComplete = false;
var _messages = <Message>[];
@override
Widget build(BuildContext context) {
final messagesStream = _isThreadConversation
? streamChannel.channel.state.threadsStream
? _streamChannel.channel.state.threadsStream
.where((threads) => threads.containsKey(widget.parentMessage.id))
.map((threads) => threads[widget.parentMessage.id])
: streamChannel.channel.state?.messagesStream;
: _streamChannel.channel.state?.messagesStream;
bool defaultFilter(Message m) {
final isMyMessage = m.user.id == _currentUser.id;
@@ -138,10 +148,10 @@ class _MessageListCoreState extends State<MessageListCore> {
stream: messagesStream?.map((messages) =>
messages?.where(widget.messageFilter ?? defaultFilter)?.toList()),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return widget.loadingBuilder(context);
} else if (snapshot.hasError) {
if (snapshot.hasError) {
return widget.errorWidgetBuilder(context, snapshot.error);
} else if (!snapshot.hasData) {
return widget.loadingBuilder(context);
} else {
final messageList = snapshot.data?.reversed?.toList() ?? [];
if (messageList.isEmpty && !_isThreadConversation) {
@@ -149,29 +159,32 @@ class _MessageListCoreState extends State<MessageListCore> {
return widget.emptyBuilder(context);
}
} else {
messages = messageList;
_messages = messageList;
}
return widget.messageListBuilder(context, messages);
return widget.messageListBuilder(context, _messages);
}
},
);
}
/// Fetches more messages with updated pagination and updates the widget.
///
/// Optionally pass the fetch direction, defaults to [QueryDirection.bottom]
Future<void> paginateData(
{QueryDirection direction = QueryDirection.bottom}) {
if (!_isThreadConversation) {
return streamChannel.queryMessages(direction: direction);
return _streamChannel.queryMessages(direction: direction);
} else {
return streamChannel.getReplies(widget.parentMessage.id);
return _streamChannel.getReplies(widget.parentMessage.id);
}
}
@override
void initState() {
streamChannel = StreamChannel.of(context);
_streamChannel = StreamChannel.of(context);
if (_isThreadConversation) {
streamChannel.getReplies(widget.parentMessage.id);
_streamChannel.getReplies(widget.parentMessage.id);
}
if (widget.messageListController != null) {
@@ -184,7 +197,7 @@ class _MessageListCoreState extends State<MessageListCore> {
@override
void dispose() {
if (!_upToDate) {
streamChannel.reloadChannel();
_streamChannel.reloadChannel();
}
super.dispose();
}
@@ -193,5 +206,5 @@ class _MessageListCoreState extends State<MessageListCore> {
/// Controller used for paginating data in [ChannelListView]
class MessageListController {
/// Call this function to load further data
Function({QueryDirection direction}) paginateData;
Future<void> Function({QueryDirection direction}) paginateData;
}
@@ -1,8 +1,7 @@
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/stream_chat.dart';
import 'stream_chat_core.dart';
import 'package:stream_chat_flutter_core/src/stream_chat_core.dart';
/// [MessageSearchBloc] is used to manage a list of messages with pagination.
/// This class can be used to load messages, perform queries, etc.
@@ -16,7 +15,7 @@ class MessageSearchBloc extends StatefulWidget {
const MessageSearchBloc({
Key key,
@required this.child,
}) : assert(child != null),
}) : assert(child != null, 'Parameter child should not be null.'),
super(key: key);
/// The widget child
@@ -49,17 +48,16 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
Stream<List<GetMessageResponse>> get messagesStream =>
_messageResponses.stream;
final BehaviorSubject<List<GetMessageResponse>> _messageResponses =
BehaviorSubject();
final _messageResponses = BehaviorSubject<List<GetMessageResponse>>();
final BehaviorSubject<bool> _queryMessagesLoadingController =
BehaviorSubject.seeded(false);
final _queryMessagesLoadingController = BehaviorSubject.seeded(false);
/// The stream notifying the state of queryUsers call
Stream<bool> get queryMessagesLoading =>
_queryMessagesLoadingController.stream;
/// Calls [StreamChatClient.search] updating [messageResponses] stream
/// Calls [StreamChatClient.search] updating
/// [messagesStream] and [queryMessagesLoading] stream
Future<void> search({
Map<String, dynamic> filter,
Map<String, dynamic> messageFilter,
@@ -67,33 +65,13 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
String query,
PaginationParams pagination,
}) async {
_messageResponses.add(null);
try {
final messages = await _search(
filter: filter,
messageFilter: messageFilter,
sort: sort,
query: query,
pagination: pagination,
);
_messageResponses.add(messages.results);
} catch (err, stk) {
_messageResponses.addError(err, stk);
}
}
final client = StreamChatCore.of(context).client;
/// Calls [StreamChatClient.search] updating [queryMessagesLoading] stream
Future<void> loadMore({
Map<String, dynamic> filter,
Map<String, dynamic> messageFilter,
List<SortOption> sort,
String query,
PaginationParams pagination,
}) async {
if (_queryMessagesLoadingController.value == true) {
return;
if (_queryMessagesLoadingController.value == true) return;
if (_messageResponses.hasValue) {
_queryMessagesLoadingController.add(true);
}
_queryMessagesLoadingController.add(true);
try {
final clear = pagination == null ||
pagination.offset == null ||
@@ -101,12 +79,12 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
final oldMessages = List<GetMessageResponse>.from(messageResponses ?? []);
final messages = await _search(
filter: filter,
messageFilter: messageFilter,
final messages = await client.search(
filter,
sort: sort,
query: query,
pagination: pagination,
paginationParams: pagination,
messageFilters: messageFilter,
);
if (clear) {
@@ -115,30 +93,18 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
final temp = oldMessages + messages.results;
_messageResponses.add(temp);
}
_queryMessagesLoadingController.add(false);
} catch (err, stackTrace) {
_queryMessagesLoadingController.addError(err, stackTrace);
if (_messageResponses.hasValue && _queryMessagesLoadingController.value) {
_queryMessagesLoadingController.add(false);
}
} catch (e, stk) {
if (_messageResponses.hasValue) {
_queryMessagesLoadingController.addError(e, stk);
} else {
_messageResponses.addError(e, stk);
}
}
}
Future<SearchMessagesResponse> _search({
Map<String, dynamic> filter,
Map<String, dynamic> messageFilter,
List<SortOption> sort,
String query,
PaginationParams pagination,
}) {
final client = StreamChatCore.of(context).client;
return client.search(
filter,
sort: sort,
query: query,
paginationParams: pagination,
messageFilters: messageFilter,
);
}
@override
Widget build(BuildContext context) {
super.build(context);
@@ -1,13 +1,15 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter_core/src/message_search_bloc.dart';
import 'package:stream_chat_flutter_core/src/typedef.dart';
import 'message_search_bloc.dart';
///
/// [MessageSearchListCore] is a simplified class that allows searching for messages across channels while exposing UI builders.
/// A [MessageSearchListController] is used to load and paginate data.
/// [MessageSearchListCore] is a simplified class that allows searching for
/// messages across channels while exposing UI builders.
/// A [MessageSearchListController] is used to load and paginate data.
///
/// ```dart
/// class MessageSearchPage extends StatelessWidget {
@@ -28,7 +30,8 @@ import 'message_search_bloc.dart';
/// }
/// ```
///
/// Make sure to have a [MessageSearchBloc] ancestor in order to provide the information about the messages.
/// Make sure to have a [MessageSearchBloc] ancestor in order to provide the
/// information about the messages.
/// The widget uses a [ListView.separated] to render the list of messages.
///
class MessageSearchListCore extends StatefulWidget {
@@ -50,14 +53,16 @@ class MessageSearchListCore extends StatefulWidget {
this.paginationParams,
this.messageFilters,
this.messageSearchListController,
}) : assert(emptyBuilder != null),
assert(errorBuilder != null),
assert(loadingBuilder != null),
assert(childBuilder != null),
}) : assert(emptyBuilder != null, 'emptyBuilder should not be null'),
assert(errorBuilder != null, 'errorBuilder should not be null'),
assert(loadingBuilder != null, 'loadingBuilder should not be null'),
assert(childBuilder != null, 'childBuilder should not be null'),
super(key: key);
/// A [MessageSearchListController] allows reloading and pagination.
/// Use [MessageSearchListController.loadData] and [MessageSearchListController.paginateData] respectively for reloading and pagination.
/// Use [MessageSearchListController.loadData] and
/// [MessageSearchListController.paginateData] respectively for reloading and
/// pagination.
final MessageSearchListController messageSearchListController;
/// Message String to search on
@@ -69,9 +74,10 @@ class MessageSearchListCore extends StatefulWidget {
final Map<String, dynamic> filters;
/// The sorting used for the channels matching the filters.
/// Sorting is based on field and direction, multiple sorting options can be provided.
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count.
/// Direction can be ascending or descending.
/// Sorting is based on field and direction, multiple sorting options can be
/// provided.
/// You can sort based on last_updated, last_message_at, updated_at, created_
/// at or member_count. Direction can be ascending or descending.
final List<SortOption> sortOptions;
/// Pagination parameters
@@ -98,22 +104,15 @@ class MessageSearchListCore extends StatefulWidget {
final WidgetBuilder loadingBuilder;
@override
_MessageSearchListCoreState createState() => _MessageSearchListCoreState();
MessageSearchListCoreState createState() => MessageSearchListCoreState();
}
class _MessageSearchListCoreState extends State<MessageSearchListCore> {
/// The current state of the [MessageSearchListCore].
class MessageSearchListCoreState extends State<MessageSearchListCore> {
@override
void initState() {
super.initState();
final messageSearchBloc = MessageSearchBloc.of(context);
messageSearchBloc.search(
filter: widget.filters,
sort: widget.sortOptions,
query: widget.messageQuery,
pagination: widget.paginationParams,
messageFilter: widget.messageFilters,
);
void didChangeDependencies() {
super.didChangeDependencies();
loadData();
if (widget.messageSearchListController != null) {
widget.messageSearchListController.loadData = loadData;
widget.messageSearchListController.paginateData = paginateData;
@@ -126,37 +125,28 @@ class _MessageSearchListCoreState extends State<MessageSearchListCore> {
return _buildListView(messageSearchBloc);
}
Widget _buildListView(MessageSearchBlocState messageSearchBloc) {
return StreamBuilder<List<GetMessageResponse>>(
stream: messageSearchBloc.messagesStream,
builder: (context, snapshot) {
if (snapshot.hasError) {
if (snapshot.error is Error) {
print((snapshot.error as Error).stackTrace);
Widget _buildListView(MessageSearchBlocState messageSearchBloc) =>
StreamBuilder<List<GetMessageResponse>>(
stream: messageSearchBloc.messagesStream,
builder: (context, snapshot) {
if (snapshot.hasError) {
return widget.errorBuilder(context, snapshot.error);
}
if (!snapshot.hasData) {
return widget.loadingBuilder(context);
}
final items = snapshot.data;
if (items.isEmpty) {
return widget.emptyBuilder(context);
}
return widget.childBuilder(snapshot.data);
},
);
return widget.errorBuilder(context, snapshot.error);
}
if (!snapshot.hasData) {
return widget.loadingBuilder(context);
}
final items = snapshot.data;
if (items.isEmpty) {
return widget.emptyBuilder(context);
}
return widget.childBuilder(snapshot.data);
},
);
}
void loadData() {
/// Fetches initial messages and updates the widget
Future<void> loadData() {
final messageSearchBloc = MessageSearchBloc.of(context);
messageSearchBloc.search(
return messageSearchBloc.search(
filter: widget.filters,
sort: widget.sortOptions,
query: widget.messageQuery,
@@ -165,10 +155,10 @@ class _MessageSearchListCoreState extends State<MessageSearchListCore> {
);
}
void paginateData() {
/// Fetches more messages with updated pagination and updates the widget
Future<void> paginateData() {
final messageSearchBloc = MessageSearchBloc.of(context);
messageSearchBloc.loadMore(
return messageSearchBloc.search(
filter: widget.filters,
sort: widget.sortOptions,
pagination: widget.paginationParams.copyWith(
@@ -184,19 +174,12 @@ class _MessageSearchListCoreState extends State<MessageSearchListCore> {
super.didUpdateWidget(oldWidget);
if (widget.filters?.toString() != oldWidget.filters?.toString() ||
jsonEncode(widget.sortOptions) != jsonEncode(oldWidget.sortOptions) ||
widget.paginationParams?.toJson()?.toString() !=
oldWidget.paginationParams?.toJson()?.toString() ||
widget.messageQuery?.toString() != oldWidget.messageQuery?.toString() ||
widget.messageFilters?.toString() !=
oldWidget.messageFilters?.toString()) {
final messageSearchBloc = MessageSearchBloc.of(context);
messageSearchBloc.search(
filter: widget.filters,
sort: widget.sortOptions,
query: widget.messageQuery,
pagination: widget.paginationParams,
messageFilter: widget.messageFilters,
);
oldWidget.messageFilters?.toString() ||
widget.paginationParams?.toJson()?.toString() !=
oldWidget.paginationParams?.toJson()?.toString()) {
loadData();
}
}
}
@@ -204,8 +187,8 @@ class _MessageSearchListCoreState extends State<MessageSearchListCore> {
/// Controller used for paginating data in [ChannelListView]
class MessageSearchListController {
/// Call this function to reload data
VoidCallback loadData;
AsyncCallback loadData;
/// Call this function to load further data
VoidCallback paginateData;
AsyncCallback paginateData;
}
@@ -1,5 +1,6 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/stream_chat.dart';
@@ -25,8 +26,8 @@ class StreamChannel extends StatefulWidget {
@required this.channel,
this.showLoading = true,
this.initialMessageId,
}) : assert(child != null),
assert(channel != null),
}) : assert(child != null, 'Child should not be null'),
assert(channel != null, 'Channel should not be null'),
super(key: key);
// ignore: public_member_api_docs
@@ -49,7 +50,8 @@ class StreamChannel extends StatefulWidget {
if (streamChannelState == null) {
throw Exception(
'You must have a StreamChannel widget at the top of your widget tree');
'You must have a StreamChannel widget at the top of your widget tree',
);
}
return streamChannelState;
@@ -204,17 +206,15 @@ class StreamChannelState extends State<StreamChannel> {
int before = 20,
int after = 20,
bool preferOffline = false,
}) {
return queryAtMessage(
messageId: messageId,
before: before,
after: after,
preferOffline: preferOffline,
);
}
}) =>
_queryAtMessage(
messageId: messageId,
before: before,
after: after,
preferOffline: preferOffline,
);
///
Future<void> queryAtMessage({
Future<void> _queryAtMessage({
String messageId,
int before = 20,
int after = 20,
@@ -254,15 +254,14 @@ class StreamChannelState extends State<StreamChannel> {
String messageId, {
int limit = 20,
bool preferOffline = false,
}) {
return channel.query(
messagesPagination: PaginationParams(
lessThan: messageId,
limit: limit,
),
preferOffline: preferOffline,
);
}
}) =>
channel.query(
messagesPagination: PaginationParams(
lessThan: messageId,
limit: limit,
),
preferOffline: preferOffline,
);
///
Future<ChannelState> queryAfterMessage(
@@ -297,7 +296,7 @@ class StreamChannelState extends State<StreamChannel> {
}
/// Reloads the channel with latest message
Future<void> reloadChannel() => queryAtMessage(before: 30);
Future<void> reloadChannel() => _queryAtMessage(before: 30);
List<Future<bool>> _futures;
@@ -305,8 +304,7 @@ class StreamChannelState extends State<StreamChannel> {
try {
await loadChannelAtMessage(initialMessageId);
return true;
} catch (e, stk) {
print('Error: $e\nStack: $stk');
} catch (_) {
rethrow;
}
}
@@ -349,9 +347,6 @@ class StreamChannelState extends State<StreamChannel> {
],
builder: (context, snapshot) {
if (snapshot.hasError) {
if (snapshot.error is Error) {
print((snapshot.error as Error).stackTrace);
}
var message = snapshot.error.toString();
if (snapshot.error is DioError) {
final dioError = snapshot.error as DioError;
@@ -361,14 +356,13 @@ class StreamChannelState extends State<StreamChannel> {
message = 'Check your connection and retry';
}
}
return Center(
child: Text(message),
);
return Center(child: Text(message));
}
final initialized = snapshot.data[0];
// ignore: avoid_bool_literals_in_conditional_expressions
final dataLoaded = initialMessageId == null ? true : snapshot.data[1];
if (widget.showLoading && (!initialized || !dataLoaded)) {
return Center(
return const Center(
child: CircularProgressIndicator(),
);
}
@@ -3,13 +3,12 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'typedef.dart';
import 'package:stream_chat_flutter_core/src/typedef.dart';
/// Widget used to provide information about the chat to the widget tree.
/// This Widget is used to react to life cycle changes and system updates.
/// When the app goes into the background, the websocket connection is kept alive
/// for two minutes before being terminated.
/// When the app goes into the background, the websocket connection is kept
/// alive for two minutes before being terminated.
///
/// Conversely, when app is resumed or restarted, a new connection is initiated.
///
@@ -36,16 +35,16 @@ import 'typedef.dart';
class StreamChatCore extends StatefulWidget {
/// Constructor used for creating a new instance of [StreamChatCore].
///
/// [StreamChatCore] is a stateful widget which reacts to system events and updates
/// Stream's connection status accordingly.
StreamChatCore({
/// [StreamChatCore] is a stateful widget which reacts to system events and
/// updates Stream's connection status accordingly.
const StreamChatCore({
Key key,
@required this.client,
@required this.child,
this.onBackgroundEventReceived,
this.backgroundKeepAlive = const Duration(minutes: 1),
}) : assert(client != null),
assert(child != null),
}) : assert(client != null, 'Stream Chat Client should not be null'),
assert(child != null, 'Child should not be null'),
super(key: key);
/// Instance of Stream Chat Client containing information about the current
@@ -55,7 +54,8 @@ class StreamChatCore extends StatefulWidget {
/// Widget descendant.
final Widget child;
/// The amount of time that will pass before disconnecting the client in the background
/// The amount of time that will pass before disconnecting the client in
/// the background
final Duration backgroundKeepAlive;
/// Handler called whenever the [client] receives a new [Event] while the app
@@ -90,15 +90,13 @@ class StreamChatCoreState extends State<StreamChatCore>
Timer _disconnectTimer;
@override
Widget build(BuildContext context) {
return widget.child;
}
Widget build(BuildContext context) => widget.child;
/// The current user
User get user => widget.client.state.user;
User get user => client.state?.user;
/// The current user as a stream
Stream<User> get userStream => widget.client.state.userStream;
Stream<User> get userStream => client.state?.userStream;
@override
void initState() {
@@ -110,21 +108,25 @@ class StreamChatCoreState extends State<StreamChatCore>
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (client.state?.user != null) {
if (user != null) {
if (state == AppLifecycleState.paused) {
if (widget.onBackgroundEventReceived != null) {
_eventSubscription =
client.on().listen(widget.onBackgroundEventReceived);
_disconnectTimer = Timer(
widget.backgroundKeepAlive,
client.disconnect,
);
} else {
if (widget.onBackgroundEventReceived == null) {
client.disconnect();
return;
}
_eventSubscription = client.on().listen(
widget.onBackgroundEventReceived,
);
void onTimerComplete() {
_eventSubscription.cancel();
client.disconnect();
}
_disconnectTimer = Timer(widget.backgroundKeepAlive, onTimerComplete);
} else if (state == AppLifecycleState.resumed) {
_eventSubscription?.cancel();
if (_disconnectTimer?.isActive == true) {
_eventSubscription.cancel();
_disconnectTimer.cancel();
} else {
if (client.wsConnectionStatus == ConnectionStatus.disconnected) {
@@ -2,8 +2,8 @@ import 'package:flutter/widgets.dart';
import 'package:stream_chat/stream_chat.dart';
/// A signature for a callback which exposes an error and returns a function.
/// This Callback can be used in cases where an API failure occurs and the widget
/// is unable to render data.
/// This Callback can be used in cases where an API failure occurs and the
/// widget is unable to render data.
typedef ErrorBuilder = Widget Function(BuildContext context, Object error);
/// A Signature for a handler function which will expose a [event].
@@ -1,11 +1,13 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter_core/src/users_bloc.dart';
///
/// [UserListCore] is a simplified class that allows fetching users while exposing UI builders.
/// [UserListCore] is a simplified class that allows fetching users while
/// exposing UI builders.
/// A [UserListController] is used to load and paginate data.
///
/// ```dart
@@ -50,34 +52,35 @@ import 'package:stream_chat_flutter_core/src/users_bloc.dart';
/// [UsersBloc] must be the ancestor of this widget. This is necessary since
/// [UserListCore] depends on functionality contained within [UsersBloc].
///
/// The parameters [listBuilder], [loadingBuilder], [emptyBuilder] and [errorBuilder] must all be supplied
/// and not null.
/// The parameters [listBuilder], [loadingBuilder], [emptyBuilder] and
/// [errorBuilder] must all be supplied and not null.
class UserListCore extends StatefulWidget {
/// Instantiate a new [UserListCore]
const UserListCore({
Key key,
@required this.errorBuilder,
@required this.emptyBuilder,
@required this.loadingBuilder,
@required this.listBuilder,
Key key,
this.filter,
this.options,
this.sort,
this.pagination,
this.groupAlphabetically = false,
this.userListController,
}) : assert(errorBuilder != null),
assert(emptyBuilder != null),
assert(loadingBuilder != null),
assert(listBuilder != null),
}) : assert(errorBuilder != null, ''),
assert(emptyBuilder != null, ''),
assert(loadingBuilder != null, ''),
assert(listBuilder != null, ''),
super(key: key);
/// A [UserListController] allows reloading and pagination.
/// Use [UserListController.loadData] and [UserListController.paginateData] respectively for reloading and pagination.
/// Use [UserListController.loadData] and [UserListController.paginateData]
/// respectively for reloading and pagination.
final UserListController userListController;
/// The builder that will be used in case of error
final Widget Function(Error error) errorBuilder;
final Widget Function(Object error) errorBuilder;
/// The builder that will be used to build the list
final Widget Function(BuildContext context, List<ListItem> users) listBuilder;
@@ -100,9 +103,9 @@ class UserListCore extends StatefulWidget {
final Map<String, dynamic> options;
/// The sorting used for the channels matching the filters.
/// Sorting is based on field and direction, multiple sorting options can be provided.
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count.
/// Direction can be ascending or descending.
/// Sorting is based on field and direction, multiple sorting options can be
/// provided. You can sort based on last_updated, last_message_at, updated_at,
/// created_at or member_count. Direction can be ascending or descending.
final List<SortOption> sort;
/// Pagination parameters
@@ -117,22 +120,16 @@ class UserListCore extends StatefulWidget {
final bool groupAlphabetically;
@override
_UserListCoreState createState() => _UserListCoreState();
UserListCoreState createState() => UserListCoreState();
}
class _UserListCoreState extends State<UserListCore>
/// The current state of the [UserListCore].
class UserListCoreState extends State<UserListCore>
with WidgetsBindingObserver {
@override
void initState() {
super.initState();
final usersBloc = UsersBloc.of(context);
usersBloc.queryUsers(
filter: widget.filter,
sort: widget.sort,
pagination: widget.pagination,
options: widget.options,
);
void didChangeDependencies() {
super.didChangeDependencies();
loadData();
if (widget.userListController != null) {
widget.userListController.loadData = loadData;
widget.userListController.paginateData = paginateData;
@@ -142,77 +139,64 @@ class _UserListCoreState extends State<UserListCore>
@override
Widget build(BuildContext context) {
final _usersBloc = UsersBloc.of(context);
return _buildListView(_usersBloc);
}
bool get isListAlreadySorted =>
bool get _isListAlreadySorted =>
widget.sort?.any((e) => e.field == 'name' && e.direction == 1) ?? false;
Stream<List<ListItem>> _buildUserStream(
UsersBlocState usersBlocState,
) {
return usersBlocState.usersStream.map(
(users) {
if (widget.groupAlphabetically) {
var temp = users;
if (!isListAlreadySorted) {
temp = users..sort((curr, next) => curr.name.compareTo(next.name));
) =>
usersBlocState.usersStream.map(
(users) {
if (widget.groupAlphabetically) {
var temp = users;
if (!_isListAlreadySorted) {
temp = users
..sort((curr, next) => curr.name.compareTo(next.name));
}
final groupedUsers = <String, List<User>>{};
for (final e in temp) {
final alphabet = e.name[0]?.toUpperCase();
groupedUsers[alphabet] = [...groupedUsers[alphabet] ?? [], e];
}
final items = <ListItem>[];
for (final key in groupedUsers.keys) {
items
..add(ListHeaderItem(key))
..addAll(groupedUsers[key].map((e) => ListUserItem(e)));
}
return items;
}
final groupedUsers = <String, List<User>>{};
for (final e in temp) {
final alphabet = e.name[0]?.toUpperCase();
groupedUsers[alphabet] = [...groupedUsers[alphabet] ?? [], e];
}
final items = <ListItem>[];
for (final key in groupedUsers.keys) {
items.add(ListHeaderItem(key));
items.addAll(groupedUsers[key].map((e) => ListUserItem(e)));
}
return items;
}
return users.map((e) => ListUserItem(e)).toList();
},
);
}
return users.map((e) => ListUserItem(e)).toList();
},
);
StreamBuilder<List<ListItem>> _buildListView(
UsersBlocState usersBlocState,
) {
return StreamBuilder(
stream: _buildUserStream(usersBlocState),
builder: (context, snapshot) {
if (snapshot.hasError) {
if (snapshot.error is Error) {
print((snapshot.error as Error).stackTrace);
) =>
StreamBuilder(
stream: _buildUserStream(usersBlocState),
builder: (context, snapshot) {
if (snapshot.hasError) {
return widget.errorBuilder(snapshot.error);
}
if (!snapshot.hasData) {
return widget.loadingBuilder(context);
}
final items = snapshot.data;
if (items.isEmpty) {
return widget.emptyBuilder(context);
}
return widget.listBuilder(context, items);
},
);
return widget.errorBuilder(snapshot.error);
}
if (!snapshot.hasData) {
return widget.loadingBuilder(context);
}
final items = snapshot.data;
if (items.isEmpty) {
return widget.emptyBuilder(context);
}
if (items.isEmpty) {
return widget.emptyBuilder(context);
}
return widget.listBuilder(context, items);
},
);
}
void loadData() {
// ignore: public_member_api_docs
Future<void> loadData() {
final _usersBloc = UsersBloc.of(context);
_usersBloc.queryUsers(
return _usersBloc.queryUsers(
filter: widget.filter,
sort: widget.sort,
pagination: widget.pagination,
@@ -220,10 +204,10 @@ class _UserListCoreState extends State<UserListCore>
);
}
void paginateData() {
// ignore: public_member_api_docs
Future<void> paginateData() {
final _usersBloc = UsersBloc.of(context);
_usersBloc.queryUsers(
return _usersBloc.queryUsers(
filter: widget.filter,
sort: widget.sort,
pagination: widget.pagination.copyWith(
@@ -238,29 +222,23 @@ class _UserListCoreState extends State<UserListCore>
super.didUpdateWidget(oldWidget);
if (widget.filter?.toString() != oldWidget.filter?.toString() ||
jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) ||
widget.options?.toString() != oldWidget.options?.toString() ||
widget.pagination?.toJson()?.toString() !=
oldWidget.pagination?.toJson()?.toString() ||
widget.options?.toString() != oldWidget.options?.toString()) {
final usersBloc = UsersBloc.of(context);
usersBloc.queryUsers(
filter: widget.filter,
sort: widget.sort,
pagination: widget.pagination,
options: widget.options,
);
oldWidget.pagination?.toJson()?.toString()) {
loadData();
}
}
}
/// Represents an item in a the user stream list.
/// Header items are prefixed with the key `HEADER` While users are prefixed with
/// `USER`.
/// Header items are prefixed with the key `HEADER` While users are prefixed
/// with `USER`.
abstract class ListItem {
// ignore: public_member_api_docs
/// Unique key per list item
String get key {
if (this is ListHeaderItem) {
final header = (this as ListHeaderItem).heading;
return 'HEADER-$header';
return 'HEADER-${header.toLowerCase()}';
}
if (this is ListUserItem) {
final user = (this as ListUserItem).user;
@@ -269,7 +247,8 @@ abstract class ListItem {
return null;
}
// ignore: public_member_api_docs
/// Helper function to build widget based on ListItem type
// ignore: missing_return
Widget when({
@required Widget Function(String heading) headerItem,
@required Widget Function(User user) userItem,
@@ -280,33 +259,32 @@ abstract class ListItem {
if (this is ListUserItem) {
return userItem((this as ListUserItem).user);
}
return SizedBox();
}
}
// ignore: public_member_api_docs
/// Header Item
class ListHeaderItem extends ListItem {
// ignore: public_member_api_docs
final String heading;
// ignore: public_member_api_docs
/// Constructs a new [ListHeaderItem]
ListHeaderItem(this.heading);
/// Heading used to build the item.
final String heading;
}
// ignore: public_member_api_docs
/// User Item
class ListUserItem extends ListItem {
// ignore: public_member_api_docs
final User user;
// ignore: public_member_api_docs
/// Constructs a new [ListUserItem]
ListUserItem(this.user);
/// [User] used to build the item.
final User user;
}
/// Controller used for paginating data in [ChannelListView]
class UserListController {
/// Call this function to reload data
VoidCallback loadData;
AsyncCallback loadData;
/// Call this function to load further data
VoidCallback paginateData;
AsyncCallback paginateData;
}
@@ -1,8 +1,8 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/stream_chat.dart';
import 'stream_chat_core.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// Widget dedicated to the management of a users list with pagination.
///
@@ -14,9 +14,12 @@ class UsersBloc extends StatefulWidget {
/// Instantiate a new [UsersBloc]. The parameter [child] must be supplied and
/// not null.
const UsersBloc({
Key key,
@required this.child,
}) : assert(child != null),
Key key,
}) : assert(
child != null,
'When constructing a UsersBloc, the parameter '
'child should not be null.'),
super(key: key);
/// The widget child
@@ -48,10 +51,9 @@ class UsersBlocState extends State<UsersBloc>
/// The current users list as a stream
Stream<List<User>> get usersStream => _usersController.stream;
final BehaviorSubject<List<User>> _usersController = BehaviorSubject();
final _usersController = BehaviorSubject<List<User>>();
final BehaviorSubject<bool> _queryUsersLoadingController =
BehaviorSubject.seeded(false);
final _queryUsersLoadingController = BehaviorSubject.seeded(false);
/// The stream notifying the state of queryUsers call
Stream<bool> get queryUsersLoading => _queryUsersLoadingController.stream;
@@ -67,11 +69,12 @@ class UsersBlocState extends State<UsersBloc>
}) async {
final client = StreamChatCore.of(context).client;
if (client.state?.user == null ||
_queryUsersLoadingController.value == true) {
return;
if (_queryUsersLoadingController.value == true) return;
if (_usersController.hasValue) {
_queryUsersLoadingController.add(true);
}
_queryUsersLoadingController.add(true);
try {
final clear = pagination == null ||
pagination.offset == null ||
@@ -92,10 +95,15 @@ class UsersBlocState extends State<UsersBloc>
final temp = oldUsers + usersResponse.users;
_usersController.add(temp);
}
_queryUsersLoadingController.add(false);
} catch (err, stackTrace) {
_queryUsersLoadingController.addError(err, stackTrace);
if (_usersController.hasValue && _queryUsersLoadingController.value) {
_queryUsersLoadingController.add(false);
}
} catch (e, stk) {
if (_usersController.hasValue) {
_queryUsersLoadingController.addError(e, stk);
} else {
_usersController.addError(e, stk);
}
}
}
@@ -1,14 +1,15 @@
library stream_chat_flutter_core;
export 'src/channel_list_core.dart';
export 'package:stream_chat/stream_chat.dart';
export 'src/channel_list_core.dart' hide ChannelListCoreState;
export 'src/channels_bloc.dart';
export 'src/lazy_load_scroll_view.dart';
export 'src/message_list_core.dart';
export 'src/message_list_core.dart' hide MessageListCoreState;
export 'src/message_search_bloc.dart';
export 'src/message_search_list_core.dart';
export 'src/message_search_list_core.dart' hide MessageSearchListCoreState;
export 'src/stream_channel.dart';
export 'src/stream_chat_core.dart';
export 'src/user_list_core.dart';
export 'src/users_bloc.dart';
export 'src/typedef.dart';
export 'package:stream_chat/stream_chat.dart';
export 'src/user_list_core.dart' hide UserListCoreState;
export 'src/users_bloc.dart';
@@ -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: 1.5.0
version: 1.5.1
repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -10,15 +10,13 @@ environment:
flutter: ">=1.17.0"
dependencies:
stream_chat: ^1.5.0
flutter:
sdk: flutter
meta: ^1.2.4
rxdart: ^0.25.0
stream_chat: ^1.5.1
dev_dependencies:
mockito: ^4.1.4
flutter_test:
sdk: flutter
fake_async: ^1.1.0
pedantic: ^1.9.2
mockito: ^4.1.3
@@ -0,0 +1,504 @@
import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
import 'package:mockito/mockito.dart';
import 'package:stream_chat_flutter_core/src/channel_list_core.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'mocks.dart';
void main() {
const pagination = PaginationParams(offset: 0, limit: 3);
List<Channel> _generateChannels(
StreamChatClient client, {
int count = 3,
int offset = 0,
}) {
return List.generate(
count,
(index) {
index = index + offset;
return Channel(
client,
'testType$index',
'testId$index',
{'extra_data_key': 'extra_data_value_$index'},
);
},
);
}
test(
'should throw assertion error in case listBuilder is null',
() {
final channelListCore = () => ChannelListCore(
listBuilder: null,
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
);
expect(channelListCore, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error in case loadingBuilder is null',
() {
final channelListCore = () => ChannelListCore(
listBuilder: (_, __) => Offstage(),
loadingBuilder: null,
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
);
expect(channelListCore, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error in case emptyBuilder is null',
() {
final channelListCore = () => ChannelListCore(
listBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: null,
errorBuilder: (BuildContext context, Object error) => Offstage(),
);
expect(channelListCore, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error in case errorBuilder is null',
() {
final channelListCore = () => ChannelListCore(
listBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: null,
);
expect(channelListCore, throwsA(isA<AssertionError>()));
},
);
testWidgets(
'should throw if ChannelListCore is used where ChannelsBloc is not present '
'in the widget tree',
(tester) async {
const channelListCoreKey = Key('channelListCore');
final channelListCore = ChannelListCore(
key: channelListCoreKey,
listBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
);
await tester.pumpWidget(channelListCore);
expect(find.byKey(channelListCoreKey), findsNothing);
expect(tester.takeException(), isInstanceOf<Exception>());
},
);
testWidgets(
'should render ChannelListCore if used with ChannelsBloc as an ancestor',
(tester) async {
const channelListCoreKey = Key('channelListCore');
final channelListCore = ChannelListCore(
key: channelListCoreKey,
listBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
);
final mockClient = MockClient();
when(mockClient.on(any, any, any, any)).thenAnswer((_) => Stream.empty());
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: ChannelsBloc(
child: channelListCore,
),
),
);
expect(find.byKey(channelListCoreKey), findsOneWidget);
},
);
testWidgets(
'should assign loadData and paginateData callback to '
'ChannelListController if passed',
(tester) async {
const channelListCoreKey = Key('channelListCore');
final controller = ChannelListController();
final channelListCore = ChannelListCore(
key: channelListCoreKey,
listBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
channelListController: controller,
);
expect(controller.loadData, isNull);
expect(controller.paginateData, isNull);
final mockClient = MockClient();
when(mockClient.on(any, any, any, any)).thenAnswer((_) => Stream.empty());
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: ChannelsBloc(
child: channelListCore,
),
),
);
expect(find.byKey(channelListCoreKey), findsOneWidget);
expect(controller.loadData, isNotNull);
expect(controller.paginateData, isNotNull);
},
);
testWidgets(
'should build error widget if channelsBlocState.channelsStream emits error',
(tester) async {
const channelListCoreKey = Key('channelListCore');
const errorWidgetKey = Key('errorWidget');
final channelListCore = ChannelListCore(
key: channelListCoreKey,
listBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) =>
Container(key: errorWidgetKey),
pagination: pagination,
);
final mockClient = MockClient();
when(mockClient.on(any, any, any, any)).thenAnswer((_) => Stream.empty());
const error = 'Error! Error! Error!';
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: pagination,
)).thenThrow(error);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: ChannelsBloc(
child: channelListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(errorWidgetKey), findsOneWidget);
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: pagination,
)).called(1);
},
);
testWidgets(
'should build empty widget if channelsBlocState.channelsStream emits empty data',
(tester) async {
const channelListCoreKey = Key('channelListCore');
const emptyWidgetKey = Key('emptyWidget');
final channelListCore = ChannelListCore(
key: channelListCoreKey,
listBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Container(key: emptyWidgetKey),
errorBuilder: (BuildContext context, Object error) => Offstage(),
pagination: pagination,
);
final mockClient = MockClient();
when(mockClient.on(any, any, any, any)).thenAnswer((_) => Stream.empty());
const channels = <Channel>[];
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: pagination,
)).thenAnswer((_) => Stream.value(channels));
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: ChannelsBloc(
child: channelListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(emptyWidgetKey), findsOneWidget);
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: pagination,
)).called(1);
},
);
testWidgets(
'should build list widget if channelsBlocState.channelsStream emits some data',
(tester) async {
const channelListCoreKey = Key('channelListCore');
const listWidgetKey = Key('listWidget');
final channelListCore = ChannelListCore(
key: channelListCoreKey,
listBuilder: (_, __) => Container(key: listWidgetKey),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
pagination: pagination,
);
final mockClient = MockClient();
when(mockClient.on(any, any, any, any)).thenAnswer((_) => Stream.empty());
final channels = _generateChannels(mockClient);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: pagination,
)).thenAnswer((_) => Stream.value(channels));
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: ChannelsBloc(
child: channelListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: pagination,
)).called(1);
},
);
testWidgets(
'should build list widget with paginated data '
'on calling channelListCoreState.paginateData',
(tester) async {
const channelListCoreKey = Key('channelListCore');
const listWidgetKey = Key('listWidget');
final channelListCore = ChannelListCore(
key: channelListCoreKey,
listBuilder: (_, channels) {
return Container(
key: listWidgetKey,
child: Text(
channels.map((e) => e.cid).join(','),
),
);
},
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
pagination: pagination,
);
final mockClient = MockClient();
when(mockClient.on(any, any, any, any)).thenAnswer((_) => Stream.empty());
final channels = _generateChannels(mockClient);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: pagination,
)).thenAnswer((_) => Stream.value(channels));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChatCore(
client: mockClient,
child: ChannelsBloc(
child: channelListCore,
),
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
expect(find.text(channels.map((e) => e.cid).join(',')), findsOneWidget);
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: pagination,
)).called(1);
final channelListCoreState = tester.state<ChannelListCoreState>(
find.byKey(channelListCoreKey),
);
final offset = channels.length;
final paginatedChannels = _generateChannels(mockClient, offset: offset);
final updatedPagination = pagination.copyWith(offset: offset);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: updatedPagination,
)).thenAnswer((_) => Stream.value(paginatedChannels));
await channelListCoreState.paginateData();
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
expect(
find.text([
...channels,
...paginatedChannels,
].map((e) => e.cid).join(',')),
findsOneWidget,
);
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: updatedPagination,
)).called(1);
},
);
testWidgets(
'should rebuild ChannelListCore with updated widget data '
'on calling setState()',
(tester) async {
StateSetter _stateSetter;
int limit = pagination.limit;
const channelListCoreKey = Key('channelListCore');
const listWidgetKey = Key('listWidget');
ChannelListCore channelListCoreBuilder(int limit) => ChannelListCore(
key: channelListCoreKey,
listBuilder: (_, channels) {
return Container(
key: listWidgetKey,
child: Text(
channels.map((e) => e.cid).join(','),
),
);
},
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
pagination: pagination.copyWith(limit: limit),
);
final mockClient = MockClient();
when(mockClient.on(any, any, any, any)).thenAnswer((_) => Stream.empty());
final channels = _generateChannels(mockClient);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: pagination,
)).thenAnswer((_) => Stream.value(channels));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChatCore(
client: mockClient,
child: ChannelsBloc(
child: StatefulBuilder(builder: (context, stateSetter) {
// Assigning stateSetter for rebuilding ChannelListCore
_stateSetter = stateSetter;
return channelListCoreBuilder(limit);
}),
),
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
expect(find.text(channels.map((e) => e.cid).join(',')), findsOneWidget);
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: pagination,
)).called(1);
// Rebuilding ChannelListCore with new pagination limit
_stateSetter(() => limit = 6);
final updatedChannels = _generateChannels(mockClient, count: limit);
final updatedPagination = pagination.copyWith(limit: limit);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: updatedPagination,
)).thenAnswer((_) => Stream.value(updatedChannels));
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
expect(
find.text(updatedChannels.map((e) => e.cid).join(',')),
findsOneWidget,
);
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: updatedPagination,
)).called(1);
},
);
}
@@ -0,0 +1,847 @@
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'matchers/channel_matcher.dart';
import 'mocks.dart';
void main() {
List<Channel> _generateChannels(
StreamChatClient client, {
int count = 3,
int offset = 0,
}) {
return List.generate(
count,
(index) {
index = index + offset;
return Channel(
client,
'testType$index',
'testId$index',
{'extra_data_key': 'extra_data_value_$index'},
);
},
);
}
test(
'should throw assertion error if child is null',
() async {
const channelsBlocKey = Key('channelsBloc');
final channelsBloc = () => ChannelsBloc(
key: channelsBlocKey,
child: null,
);
expect(channelsBloc, throwsA(isA<AssertionError>()));
},
);
testWidgets(
'should throw if ChannelsBloc is used where StreamChat is not present in the widget tree',
(tester) async {
const channelsBlocKey = Key('channelsBloc');
const childKey = Key('child');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(key: childKey),
);
await tester.pumpWidget(channelsBloc);
expect(find.byKey(channelsBlocKey), findsNothing);
expect(find.byKey(childKey), findsNothing);
expect(tester.takeException(), isInstanceOf<Exception>());
},
);
testWidgets(
'should render ChannelsBloc if used with StreamChatCore as an ancestor',
(tester) async {
const channelsBlocKey = Key('channelsBloc');
const childKey = Key('child');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
when(mockClient.on(any, any, any, any)).thenAnswer((_) => Stream.empty());
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
expect(find.byKey(channelsBlocKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
},
);
testWidgets(
'channelsBlocState.queryChannels() should emit data through channelsStream',
(tester) async {
const channelsBlocKey = Key('channelsBloc');
const childKey = Key('child');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Builder(
key: childKey,
builder: (context) {
return Offstage();
},
),
);
final mockClient = MockClient();
when(mockClient.on(any, any, any, any)).thenAnswer((_) => Stream.empty());
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
final offlineChannels = _generateChannels(mockClient);
final onlineChannels = _generateChannels(mockClient, offset: 3);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer(
(_) => Stream.fromIterable([offlineChannels, onlineChannels]),
);
channelsBlocState.queryChannels();
await expectLater(
channelsBlocState.channelsStream,
emitsInOrder([
isSameChannelListAs(offlineChannels),
isSameChannelListAs(onlineChannels),
]),
);
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
},
);
testWidgets(
'channelsBlocState.channelsStream should emit error '
'if client.queryChannels() throws',
(tester) async {
const channelsBlocKey = Key('channelsBloc');
const childKey = Key('child');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Builder(
key: childKey,
builder: (context) {
return Offstage();
},
),
);
final mockClient = MockClient();
when(mockClient.on(any, any, any, any)).thenAnswer((_) => Stream.empty());
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
final error = 'Error! Error! Error!';
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).thenThrow(error);
channelsBlocState.queryChannels();
await expectLater(
channelsBlocState.channelsStream,
emitsError(error),
);
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
},
);
testWidgets(
'calling channelsBlocState.queryChannels() again with an offset '
'should emit new data through channelsStream and also emit loading state '
'through queryChannelsLoading',
(tester) async {
const channelsBlocKey = Key('channelsBloc');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(),
);
final mockClient = MockClient();
when(mockClient.on(any, any, any, any)).thenAnswer((_) => Stream.empty());
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
final channels = _generateChannels(mockClient);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer((_) => Stream.value(channels));
channelsBlocState.queryChannels();
await expectLater(
channelsBlocState.channelsStream,
emits(isSameChannelListAs(channels)),
);
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
final offset = channels.length;
final paginationParams = PaginationParams(offset: offset);
final newChannels = _generateChannels(mockClient, offset: offset);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: paginationParams,
)).thenAnswer(
(_) => Stream.value(newChannels),
);
channelsBlocState.queryChannels(paginationParams: paginationParams);
await Future.wait([
expectLater(
channelsBlocState.queryChannelsLoading,
emitsInOrder([true, false]),
),
expectLater(
channelsBlocState.channelsStream,
emits(isSameChannelListAs(channels + newChannels)),
),
]);
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: paginationParams,
)).called(1);
},
);
testWidgets(
'calling channelsBlocState.queryChannels() again with an offset '
'should emit error through queryChannelsLoading if '
'client.queryChannels() throws',
(tester) async {
const channelsBlocKey = Key('channelsBloc');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(),
);
final mockClient = MockClient();
when(mockClient.on(any, any, any, any)).thenAnswer((_) => Stream.empty());
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
final channels = _generateChannels(mockClient);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer((_) => Stream.value(channels));
channelsBlocState.queryChannels();
await expectLater(
channelsBlocState.channelsStream,
emits(isSameChannelListAs(channels)),
);
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
final offset = channels.length;
final paginationParams = PaginationParams(offset: offset);
final error = 'Error! Error! Error!';
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: paginationParams,
)).thenThrow(error);
channelsBlocState.queryChannels(paginationParams: paginationParams);
await expectLater(
channelsBlocState.queryChannelsLoading,
emitsError(error),
);
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: paginationParams,
)).called(1);
},
);
group('event controller test', () {
StreamController<Event> eventController;
setUp(() {
eventController = StreamController<Event>.broadcast();
});
testWidgets(
'channel should get hide when EventType.channelHidden event is received',
(tester) async {
final mockClient = MockClient();
const channelsBlocKey = Key('channelsBloc');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(),
);
when(mockClient.on(any, any, any, any))
.thenAnswer((_) => Stream.empty());
when(mockClient.on(
EventType.channelHidden,
)).thenAnswer((_) => eventController.stream);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
final channels = _generateChannels(mockClient);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer(
(_) => Stream.value(channels),
);
await channelsBlocState.queryChannels();
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
final channelHiddenEvent = Event(
type: EventType.channelHidden,
cid: channels.first.cid,
);
eventController.add(channelHiddenEvent);
final newChannels = [...channels]
..removeWhere((it) => it.cid == channelHiddenEvent.cid);
await expectLater(
channelsBlocState.channelsStream,
emitsInOrder([
isSameChannelListAs(channels),
isSameChannelListAs(newChannels),
]),
);
verify(mockClient.on(EventType.channelHidden)).called(1);
},
);
testWidgets(
'channel should get removed when EventType.channelDeleted or '
'EventType.notificationRemovedFromChannel, event is received',
(tester) async {
final mockClient = MockClient();
const channelsBlocKey = Key('channelsBloc');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(),
);
when(mockClient.on(any, any, any, any))
.thenAnswer((_) => Stream.empty());
when(mockClient.on(
EventType.channelDeleted,
EventType.notificationRemovedFromChannel,
)).thenAnswer((_) => eventController.stream);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
final channels = _generateChannels(mockClient);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer(
(_) => Stream.value(channels),
);
await channelsBlocState.queryChannels();
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
final channelDeletedOrNotificationRemovedEvent = Event(
channel: EventChannel(cid: channels.first.cid),
);
eventController.add(channelDeletedOrNotificationRemovedEvent);
final channelCid = channelDeletedOrNotificationRemovedEvent.channel.cid;
final newChannels = [...channels]
..removeWhere((it) => it.cid == channelCid);
await expectLater(
channelsBlocState.channelsStream,
emitsInOrder([
isSameChannelListAs(channels),
isSameChannelListAs(newChannels),
]),
);
verify(mockClient.on(
EventType.channelDeleted,
EventType.notificationRemovedFromChannel,
)).called(1);
},
);
testWidgets(
'event channel should be moved to top of the list if present when'
'EventType.messageNew event is received',
(tester) async {
final mockClient = MockClient();
const channelsBlocKey = Key('channelsBloc');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(),
);
when(mockClient.on(any, any, any, any))
.thenAnswer((_) => Stream.empty());
when(mockClient.on(
EventType.messageNew,
)).thenAnswer((_) => eventController.stream);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
final channels = _generateChannels(mockClient);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer(
(_) => Stream.value(channels),
);
await channelsBlocState.queryChannels();
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
final messageNewEvent = Event(
type: EventType.messageNew,
cid: channels.last.cid,
);
eventController.add(messageNewEvent);
final channelCid = messageNewEvent.cid;
final index = channels.indexWhere((it) => it.cid == channelCid);
final updatedChannel = channels[index];
final newChannels = [...channels]
..removeAt(index)
..insert(0, updatedChannel);
await expectLater(
channelsBlocState.channelsStream,
emitsInOrder([
isSameChannelListAs(channels),
isSameChannelListAs(newChannels),
]),
);
verify(mockClient.on(EventType.messageNew)).called(1);
},
);
testWidgets(
'event channel should be moved to top of the list if present inside '
'hiddenChannels list and shouldAddChannel is true when '
'EventType.messageNew event is received',
(tester) async {
final hiddenChannelEventController = StreamController<Event>();
addTearDown(() {
hiddenChannelEventController.close();
});
final mockClient = MockClient();
final channels = _generateChannels(mockClient);
const channelsBlocKey = Key('channelsBloc');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(),
shouldAddChannel: (e) => channels.map((it) => it.cid).contains(e.cid),
);
when(mockClient.on(any, any, any, any))
.thenAnswer((_) => Stream.empty());
when(mockClient.on(
EventType.channelHidden,
)).thenAnswer((_) => hiddenChannelEventController.stream);
when(mockClient.on(
EventType.messageNew,
)).thenAnswer((_) => eventController.stream);
final messageNewEvent = Event(
type: EventType.messageNew,
cid: channels.last.cid,
);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer(
(_) => Stream.value(channels),
);
await channelsBlocState.queryChannels();
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
final channelHiddenEvent = Event(
type: EventType.channelHidden,
cid: channels.last.cid,
);
// Hiding the channel before passing messageNew event
hiddenChannelEventController.add(channelHiddenEvent);
final channelsAfterHiddenEvent = [...channels]..removeLast();
eventController.add(messageNewEvent);
final channelCid = messageNewEvent.cid;
final index = channels.indexWhere((it) => it.cid == channelCid);
final newChannels = [...channels]
..removeAt(index)
..insert(0, channels[index]);
await expectLater(
channelsBlocState.channelsStream,
emitsInOrder([
isSameChannelListAs(channels),
isSameChannelListAs(channelsAfterHiddenEvent),
isSameChannelListAs(newChannels),
]),
);
verify(mockClient.on(EventType.channelHidden)).called(1);
verify(mockClient.on(EventType.messageNew)).called(1);
},
);
testWidgets(
'event channel should be moved to top of the list if present inside '
'channel state and shouldAddChannel is true when '
'EventType.messageNew event is received',
(tester) async {
final mockClient = MockClient();
final channels = _generateChannels(mockClient);
final stateChannels = {
for (var c in _generateChannels(mockClient, offset: 5)) c.cid: c
};
const channelsBlocKey = Key('channelsBloc');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(),
shouldAddChannel: (_) => true,
);
when(mockClient.state.channels).thenReturn(stateChannels);
when(mockClient.on(any, any, any, any))
.thenAnswer((_) => Stream.empty());
when(mockClient.on(
EventType.messageNew,
)).thenAnswer((_) => eventController.stream);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer(
(_) => Stream.value(channels),
);
await channelsBlocState.queryChannels();
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
final messageNewEvent = Event(
type: EventType.messageNew,
cid: stateChannels.keys.first,
);
eventController.add(messageNewEvent);
final newChannels = [...channels]
..insert(0, stateChannels[stateChannels.keys.first]);
await expectLater(
channelsBlocState.channelsStream,
emitsInOrder([
isSameChannelListAs(channels),
isSameChannelListAs(newChannels),
]),
);
verify(mockClient.on(EventType.messageNew)).called(1);
},
);
testWidgets(
'channels should get sorted according to channelsComparator when '
'EventType.messageNew event is received',
(tester) async {
final mockClient = MockClient();
final channels = _generateChannels(mockClient);
int channelComparator(Channel a, Channel b) {
final aData = a.extraData['extra_data_key'] as String;
final bData = b.extraData['extra_data_key'] as String;
return bData.compareTo(aData);
}
const channelsBlocKey = Key('channelsBloc');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(),
shouldAddChannel: (_) => true,
channelsComparator: channelComparator,
);
when(mockClient.on(any, any, any, any))
.thenAnswer((_) => Stream.empty());
when(mockClient.on(
EventType.messageNew,
)).thenAnswer((_) => eventController.stream);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
when(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer(
(_) => Stream.value(channels),
);
await channelsBlocState.queryChannels();
verify(mockClient.queryChannels(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
final messageNewEvent = Event(
type: EventType.messageNew,
cid: channels.first.cid,
);
eventController.add(messageNewEvent);
final newChannels = [...channels]..sort(channelComparator);
await expectLater(
channelsBlocState.channelsStream,
emitsInOrder([
isSameChannelListAs(channels),
isSameChannelListAs(newChannels),
]),
);
verify(mockClient.on(EventType.messageNew)).called(1);
},
);
tearDown(() {
eventController.close();
});
});
}
@@ -0,0 +1,294 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat_flutter_core/src/lazy_load_scroll_view.dart';
void main() {
test(
'should throw assertion error if child is null',
() async {
const lazyLoadScrollViewKey = Key('lazyLoadScrollView');
final lazyLoadScrollView = () => LazyLoadScrollView(
key: lazyLoadScrollViewKey,
child: null,
);
expect(lazyLoadScrollView, throwsA(isA<AssertionError>()));
},
);
testWidgets(
'should render LazyLoadScrollView if child is provided',
(tester) async {
const lazyLoadScrollViewKey = Key('lazyLoadScrollView');
final lazyLoadScrollView = LazyLoadScrollView(
key: lazyLoadScrollViewKey,
child: Offstage(),
);
await tester.pumpWidget(lazyLoadScrollView);
expect(find.byKey(lazyLoadScrollViewKey), findsOneWidget);
},
);
testWidgets(
'should render LazyLoadScrollView if child is provided',
(tester) async {
const lazyLoadScrollViewKey = Key('lazyLoadScrollView');
const childKey = Key('childKey');
final lazyLoadScrollView = LazyLoadScrollView(
key: lazyLoadScrollViewKey,
child: Offstage(key: childKey),
);
await tester.pumpWidget(lazyLoadScrollView);
expect(find.byKey(lazyLoadScrollViewKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
},
);
testWidgets(
'should call onPageScrollStart if child scrollView scrolls',
(tester) async {
const lazyLoadScrollViewKey = Key('lazyLoadScrollView');
const childListViewKey = Key('childListView');
bool onPageScrollStartCalled = false;
final lazyLoadScrollView = LazyLoadScrollView(
key: lazyLoadScrollViewKey,
onPageScrollStart: () {
onPageScrollStartCalled = true;
},
child: ListView(
key: childListViewKey,
children: List.generate(
12,
(index) => Container(
height: 100,
child: Text('Item #$index'),
),
),
),
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: lazyLoadScrollView,
),
);
expect(find.byKey(lazyLoadScrollViewKey), findsOneWidget);
expect(find.byKey(childListViewKey), findsOneWidget);
expect(onPageScrollStartCalled, isFalse);
await tester.startGesture(const Offset(100.0, 100.0));
await tester.pump(const Duration(seconds: 1));
expect(onPageScrollStartCalled, isTrue);
},
);
testWidgets(
'should call onPageScrollStart, onPageScrollEnd '
'if child scrollView starts scrolling and ends',
(tester) async {
const lazyLoadScrollViewKey = Key('lazyLoadScrollView');
const childListViewKey = Key('childListView');
bool onPageScrollStartCalled = false;
bool onPageScrollEndCalled = false;
final lazyLoadScrollView = LazyLoadScrollView(
key: lazyLoadScrollViewKey,
onPageScrollStart: () {
onPageScrollStartCalled = true;
},
onPageScrollEnd: () {
onPageScrollEndCalled = true;
},
child: ListView(
key: childListViewKey,
children: List.generate(
12,
(index) => Container(
height: 100,
child: Text('Item #$index'),
),
),
),
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: lazyLoadScrollView,
),
);
expect(find.byKey(lazyLoadScrollViewKey), findsOneWidget);
expect(find.byKey(childListViewKey), findsOneWidget);
expect(onPageScrollStartCalled, isFalse);
expect(onPageScrollEndCalled, isFalse);
final gesture = await tester.createGesture();
await gesture.down(const Offset(100.0, 100.0));
await tester.pump(const Duration(seconds: 1));
expect(onPageScrollStartCalled, isTrue);
await gesture.up();
await tester.pump(const Duration(seconds: 1));
expect(onPageScrollEndCalled, isTrue);
},
);
testWidgets(
'should call onInBetweenOfPage if child scrollView '
'scroll position is in-between of view',
(tester) async {
const lazyLoadScrollViewKey = Key('lazyLoadScrollView');
const childListViewKey = Key('childListView');
bool onInBetweenOfPageCalled = false;
final lazyLoadScrollView = LazyLoadScrollView(
key: lazyLoadScrollViewKey,
onInBetweenOfPage: () async {
onInBetweenOfPageCalled = true;
},
child: ListView(
key: childListViewKey,
children: List.generate(
12,
(index) => Container(
height: 100,
child: Text('Item #$index'),
),
),
),
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: lazyLoadScrollView,
),
);
expect(find.byKey(lazyLoadScrollViewKey), findsOneWidget);
expect(find.byKey(childListViewKey), findsOneWidget);
expect(onInBetweenOfPageCalled, isFalse);
final gesture = await tester.createGesture();
await gesture.down(const Offset(100.0, 100.0));
await tester.pump(const Duration(seconds: 1));
await gesture.moveBy(const Offset(-200.0, -200.0));
await tester.pump(const Duration(seconds: 1));
expect(onInBetweenOfPageCalled, isTrue);
},
);
testWidgets(
'should call onStartOfPage if child scrollView '
'tends to reach the start',
(tester) async {
const lazyLoadScrollViewKey = Key('lazyLoadScrollView');
const childListViewKey = Key('childListView');
bool onStartOfPageCalled = false;
final lazyLoadScrollView = LazyLoadScrollView(
key: lazyLoadScrollViewKey,
onStartOfPage: () async {
onStartOfPageCalled = true;
},
child: ListView(
key: childListViewKey,
children: List.generate(
12,
(index) => Container(
height: 100,
child: Text('Item #$index'),
),
),
),
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: lazyLoadScrollView,
),
);
expect(find.byKey(lazyLoadScrollViewKey), findsOneWidget);
expect(find.byKey(childListViewKey), findsOneWidget);
expect(onStartOfPageCalled, isFalse);
final gesture = await tester.createGesture();
await gesture.down(const Offset(100.0, 100.0));
await tester.pump(const Duration(seconds: 1));
await gesture.moveBy(const Offset(-200.0, -200.0));
await tester.pump(const Duration(seconds: 1));
await gesture.moveBy(const Offset(201.0, 201.0));
await tester.pump(const Duration(seconds: 1));
expect(onStartOfPageCalled, isTrue);
},
);
testWidgets(
'should call onEndOfPage if child scrollView '
'tends to reach the end',
(tester) async {
const lazyLoadScrollViewKey = Key('lazyLoadScrollView');
const childListViewKey = Key('childListView');
bool onEndOfPageCalled = false;
final lazyLoadScrollView = LazyLoadScrollView(
key: lazyLoadScrollViewKey,
onEndOfPage: () async {
onEndOfPageCalled = true;
},
child: ListView(
key: childListViewKey,
children: List.generate(
12,
(index) => Container(
height: 100,
child: Text('Item #$index'),
),
),
),
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: lazyLoadScrollView,
),
);
expect(find.byKey(lazyLoadScrollViewKey), findsOneWidget);
expect(find.byKey(childListViewKey), findsOneWidget);
expect(onEndOfPageCalled, isFalse);
final gesture = await tester.createGesture();
await gesture.down(const Offset(100.0, 100.0));
await tester.pump(const Duration(seconds: 1));
await gesture.moveBy(const Offset(-601.0, -601.0));
await tester.pump(const Duration(seconds: 1));
expect(onEndOfPageCalled, isTrue);
},
);
}
@@ -0,0 +1,49 @@
import 'package:meta/meta.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
Matcher isSameChannelAs(Channel targetChannel) =>
_IsSameChannelAs(targetChannel: targetChannel);
class _IsSameChannelAs extends Matcher {
const _IsSameChannelAs({
@required this.targetChannel,
}) : assert(targetChannel != null, '');
final Channel targetChannel;
@override
bool matches(covariant Channel channel, Map matchState) =>
channel.cid == targetChannel.cid;
@override
Description describe(Description description) =>
description.add('is same channel as $targetChannel');
}
Matcher isSameChannelListAs(List<Channel> targetChannelList) =>
_IsSameChannelListAs(targetChannelList: targetChannelList);
class _IsSameChannelListAs extends Matcher {
const _IsSameChannelListAs({
@required this.targetChannelList,
}) : assert(targetChannelList != null, '');
final List<Channel> targetChannelList;
@override
bool matches(covariant List<Channel> channelList, Map matchState) {
bool matches = true;
for (var i = 0; i < channelList.length; i++) {
final channel = channelList[i];
final targetChannel = targetChannelList[i];
matches = isSameChannelAs(targetChannel).matches(channel, matchState);
if (!matches) break;
}
return matches;
}
@override
Description describe(Description description) =>
description.add('is same channelList as $targetChannelList');
}
@@ -0,0 +1,55 @@
import 'package:meta/meta.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
Matcher isSameMessageResponseAs(GetMessageResponse targetResponse) =>
_IsSameMessageResponseAs(targetResponse: targetResponse);
class _IsSameMessageResponseAs extends Matcher {
const _IsSameMessageResponseAs({
@required this.targetResponse,
}) : assert(targetResponse != null, '');
final GetMessageResponse targetResponse;
@override
bool matches(covariant GetMessageResponse response, Map matchState) =>
response.message.id == targetResponse.message.id &&
response.channel.cid == targetResponse.channel.cid;
@override
Description describe(Description description) =>
description.add('is same message response as $targetResponse');
}
Matcher isSameMessageResponseListAs(
List<GetMessageResponse> targetResponseList) =>
_IsSameMessageResponseListAs(targetResponseList: targetResponseList);
class _IsSameMessageResponseListAs extends Matcher {
const _IsSameMessageResponseListAs({
@required this.targetResponseList,
}) : assert(targetResponseList != null, '');
final List<GetMessageResponse> targetResponseList;
@override
bool matches(
covariant List<GetMessageResponse> responseList, Map matchState) {
bool matches = true;
for (var i = 0; i < responseList.length; i++) {
final response = responseList[i];
final targetResponse = targetResponseList[i];
matches = isSameMessageResponseAs(targetResponse).matches(
response,
matchState,
);
if (!matches) break;
}
return matches;
}
@override
Description describe(Description description) =>
description.add('is same userResponseList as $targetResponseList');
}
@@ -0,0 +1,49 @@
import 'package:meta/meta.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
Matcher isSameMessageAs(Message targetMessage) =>
_IsSameMessageAs(targetMessage: targetMessage);
class _IsSameMessageAs extends Matcher {
const _IsSameMessageAs({
@required this.targetMessage,
}) : assert(targetMessage != null, '');
final Message targetMessage;
@override
bool matches(covariant Message message, Map matchState) =>
message.id == targetMessage.id;
@override
Description describe(Description description) =>
description.add('is same message as $targetMessage');
}
Matcher isSameMessageListAs(List<Message> targetMessageList) =>
_IsSameMessageListAs(targetMessageList: targetMessageList);
class _IsSameMessageListAs extends Matcher {
const _IsSameMessageListAs({
@required this.targetMessageList,
}) : assert(targetMessageList != null, '');
final List<Message> targetMessageList;
@override
bool matches(covariant List<Message> messageList, Map matchState) {
bool matches = true;
for (var i = 0; i < messageList.length; i++) {
final message = messageList[i];
final targetMessage = targetMessageList[i];
matches = isSameMessageAs(targetMessage).matches(message, matchState);
if (!matches) break;
}
return matches;
}
@override
Description describe(Description description) =>
description.add('is same messageList as $targetMessageList');
}
@@ -0,0 +1,47 @@
import 'package:meta/meta.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
Matcher isSameUserAs(User targetUser) => _IsSameUserAs(targetUser: targetUser);
class _IsSameUserAs extends Matcher {
const _IsSameUserAs({
@required this.targetUser,
}) : assert(targetUser != null, '');
final User targetUser;
@override
bool matches(covariant User user, Map matchState) => user.id == targetUser.id;
@override
Description describe(Description description) =>
description.add('is same user as $targetUser');
}
Matcher isSameUserListAs(List<User> targetUserList) =>
_IsSameUserListAs(targetUserList: targetUserList);
class _IsSameUserListAs extends Matcher {
const _IsSameUserListAs({
@required this.targetUserList,
}) : assert(targetUserList != null, '');
final List<User> targetUserList;
@override
bool matches(covariant List<User> userList, Map matchState) {
bool matches = true;
for (var i = 0; i < userList.length; i++) {
final user = userList[i];
final targetUser = targetUserList[i];
matches = isSameUserAs(targetUser).matches(user, matchState);
if (!matches) break;
}
return matches;
}
@override
Description describe(Description description) =>
description.add('is same userList as $targetUserList');
}
@@ -0,0 +1,413 @@
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:stream_chat_flutter_core/src/message_list_core.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'mocks.dart';
void main() {
List<Message> _generateMessages({
int count = 3,
int offset = 0,
bool threads = false,
}) {
final users = List.generate(count, (index) {
index = count + offset;
return User(id: 'testUserId$index');
});
final messages = List.generate(
count,
(index) {
index = index + offset;
return Message(
id: 'testMessageId$index',
type: 'testType',
user: users[index],
createdAt: DateTime.now(),
shadowed: false,
replyCount: index,
updatedAt: DateTime.now(),
extraData: {'extra_test_field': 'extraTestData'},
text: 'Dummy text #$index',
pinned: true,
pinnedAt: DateTime.now(),
pinnedBy: User(id: 'testUserId$index'),
);
},
);
final threadMessages = List.generate(
count,
(index) {
index = index + offset;
return Message(
id: 'testThreadMessageId$index',
type: 'testType',
user: users[index],
parentId: messages[0].id,
createdAt: DateTime.now(),
shadowed: false,
replyCount: index,
updatedAt: DateTime.now(),
extraData: {'extra_test_field': 'extraTestData'},
text: 'Dummy text #$index',
pinned: true,
pinnedAt: DateTime.now(),
pinnedBy: User(id: 'testUserId$index'),
);
},
);
return threads ? threadMessages : messages;
}
test(
'should throw assertion error in case messageListBuilder is null',
() {
final messageListCore = () => MessageListCore(
messageListBuilder: null,
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorWidgetBuilder: (BuildContext context, Object error) =>
Offstage(),
);
expect(messageListCore, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error in case loadingBuilder is null',
() {
final messageListCore = () => MessageListCore(
messageListBuilder: (_, __) => Offstage(),
loadingBuilder: null,
emptyBuilder: (BuildContext context) => Offstage(),
errorWidgetBuilder: (BuildContext context, Object error) =>
Offstage(),
);
expect(messageListCore, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error in case emptyBuilder is null',
() {
final messageListCore = () => MessageListCore(
messageListBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: null,
errorWidgetBuilder: (BuildContext context, Object error) =>
Offstage(),
);
expect(messageListCore, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error in case errorWidgetBuilder is null',
() {
final messageListCore = () => MessageListCore(
messageListBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorWidgetBuilder: null,
);
expect(messageListCore, throwsA(isA<AssertionError>()));
},
);
testWidgets(
'should throw if MessageListCore is used where StreamChannel is not present '
'in the widget tree',
(tester) async {
const messageListCoreKey = Key('messageListCore');
final messageListCore = MessageListCore(
key: messageListCoreKey,
messageListBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorWidgetBuilder: (BuildContext context, Object error) => Offstage(),
);
await tester.pumpWidget(messageListCore);
expect(find.byKey(messageListCoreKey), findsNothing);
expect(tester.takeException(), isInstanceOf<Exception>());
},
);
testWidgets(
'should render MessageListCore if used with StreamChannel as an ancestor',
(tester) async {
const messageListCoreKey = Key('messageListCore');
final messageListCore = MessageListCore(
key: messageListCoreKey,
messageListBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorWidgetBuilder: (BuildContext context, Object error) => Offstage(),
);
final mockChannel = MockChannel();
when(mockChannel.state.isUpToDate).thenReturn(true);
await tester.pumpWidget(
StreamChannel(
channel: mockChannel,
child: messageListCore,
),
);
expect(find.byKey(messageListCoreKey), findsOneWidget);
},
);
testWidgets(
'should assign paginateData callback to MessageListController if passed',
(tester) async {
const messageListCoreKey = Key('messageListCore');
final controller = MessageListController();
final messageListCore = MessageListCore(
key: messageListCoreKey,
messageListBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorWidgetBuilder: (BuildContext context, Object error) => Offstage(),
messageListController: controller,
);
expect(controller.paginateData, isNull);
final mockChannel = MockChannel();
when(mockChannel.state.isUpToDate).thenReturn(true);
await tester.pumpWidget(
StreamChannel(
channel: mockChannel,
child: messageListCore,
),
);
expect(find.byKey(messageListCoreKey), findsOneWidget);
expect(controller.paginateData, isNotNull);
},
);
testWidgets(
'should build error widget if messagesStream emits error',
(tester) async {
const messageListCoreKey = Key('messageListCore');
const errorWidgetKey = Key('errorWidget');
final messageListCore = MessageListCore(
key: messageListCoreKey,
messageListBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorWidgetBuilder: (BuildContext context, Object error) => Offstage(
key: errorWidgetKey,
),
);
final mockChannel = MockChannel();
when(mockChannel.state.isUpToDate).thenReturn(true);
when(mockChannel.initialized).thenAnswer((_) async => true);
const error = 'Error! Error! Error!';
when(mockChannel.state.messagesStream)
.thenAnswer((_) => Stream.error(error));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChannel(
channel: mockChannel,
child: messageListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(errorWidgetKey), findsOneWidget);
},
);
testWidgets(
'should build empty widget if the channel is upToDate and '
'messagesStream emits empty data',
(tester) async {
const messageListCoreKey = Key('messageListCore');
const emptyWidgetKey = Key('emptyWidget');
final messageListCore = MessageListCore(
key: messageListCoreKey,
messageListBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(key: emptyWidgetKey),
errorWidgetBuilder: (BuildContext context, Object error) => Offstage(),
);
final mockChannel = MockChannel();
when(mockChannel.state.isUpToDate).thenReturn(true);
when(mockChannel.initialized).thenAnswer((_) async => true);
const messages = <Message>[];
when(mockChannel.state.messagesStream)
.thenAnswer((_) => Stream.value(messages));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChannel(
channel: mockChannel,
child: messageListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(emptyWidgetKey), findsOneWidget);
},
);
testWidgets(
'should build list widget if the channel is not upToDate and '
'messagesStream emits empty data',
(tester) async {
const messageListCoreKey = Key('messageListCore');
const listWidgetKey = Key('listWidget');
final messageListCore = MessageListCore(
key: messageListCoreKey,
messageListBuilder: (_, __) => Offstage(key: listWidgetKey),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorWidgetBuilder: (BuildContext context, Object error) => Offstage(),
);
final mockChannel = MockChannel();
when(mockChannel.state.isUpToDate).thenReturn(false);
when(mockChannel.initialized).thenAnswer((_) async => true);
const messages = <Message>[];
when(mockChannel.state.messagesStream)
.thenAnswer((_) => Stream.value(messages));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChannel(
channel: mockChannel,
child: messageListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
},
);
testWidgets(
'should build list widget and save it for future rebuilds '
'if messagesStream emits some data',
(tester) async {
const messageListCoreKey = Key('messageListCore');
const listWidgetKey = Key('listWidget');
final messageListCore = MessageListCore(
key: messageListCoreKey,
messageListBuilder: (_, messages) => Container(
key: listWidgetKey,
child: Text(
messages.reversed.map((it) => it.id).join(','),
),
),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorWidgetBuilder: (BuildContext context, Object error) => Offstage(),
);
final mockChannel = MockChannel();
when(mockChannel.state.isUpToDate).thenReturn(true);
when(mockChannel.initialized).thenAnswer((_) async => true);
final messages = _generateMessages();
when(mockChannel.state.messagesStream)
.thenAnswer((_) => Stream.value(messages));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChannel(
channel: mockChannel,
child: messageListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
expect(find.text(messages.map((it) => it.id).join(',')), findsOneWidget);
},
);
testWidgets(
'should build list widget with thread messages if parentMessage is passed',
(tester) async {
final messages = _generateMessages(threads: true);
final parentMessage = Message(id: messages.first.parentId);
const messageListCoreKey = Key('messageListCore');
const listWidgetKey = Key('listWidget');
final messageListCore = MessageListCore(
key: messageListCoreKey,
messageListBuilder: (_, messages) => Container(
key: listWidgetKey,
child: Text(
messages.reversed.map((it) => '${it.parentId}-${it.id}').join(','),
),
),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorWidgetBuilder: (BuildContext context, Object error) => Offstage(),
parentMessage: parentMessage,
);
final mockChannel = MockChannel();
when(mockChannel.state.isUpToDate).thenReturn(true);
when(mockChannel.initialized).thenAnswer((_) async => true);
final threads = {parentMessage.id: messages};
when(mockChannel.state.threads).thenReturn(threads);
when(mockChannel.state.threadsStream)
.thenAnswer((_) => Stream.value(threads));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChannel(
channel: mockChannel,
child: messageListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
expect(
find.text(messages.map((it) => '${it.parentId}-${it.id}').join(',')),
findsOneWidget,
);
},
);
}
@@ -0,0 +1,344 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter_core/src/message_search_bloc.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'matchers/get_message_response_matcher.dart';
import 'mocks.dart';
void main() {
List<GetMessageResponse> _generateMessages({
int count = 3,
int offset = 0,
}) {
return List.generate(
count,
(index) {
index = index + offset;
return GetMessageResponse()
..message = Message(
id: 'testId$index',
text: 'testTextData$index',
)
..channel = ChannelModel(
cid: 'testCid',
);
},
);
}
test(
'should throw assertion error if child is null',
() async {
const messageSearchBlocKey = Key('messageSearchBloc');
final messageSearchBloc = () => MessageSearchBloc(
key: messageSearchBlocKey,
child: null,
);
expect(messageSearchBloc, throwsA(isA<AssertionError>()));
},
);
testWidgets(
'messageSearchBlocState.search() should throw if used where '
'StreamChat is not present in the widget tree',
(tester) async {
const messageSearchBlocKey = Key('messageSearchBloc');
const childKey = Key('child');
final messageSearchBloc = MessageSearchBloc(
key: messageSearchBlocKey,
child: Offstage(key: childKey),
);
await tester.pumpWidget(messageSearchBloc);
expect(find.byKey(messageSearchBlocKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
final usersBlocState = tester.state<MessageSearchBlocState>(
find.byKey(messageSearchBlocKey),
);
try {
await usersBlocState.search();
} catch (e) {
expect(e, isInstanceOf<Exception>());
}
},
);
testWidgets(
'messageSearchBlocState.search() should emit data through usersStream',
(tester) async {
const messageSearchBlocKey = Key('messageSearchBloc');
const childKey = Key('child');
final messageSearchBloc = MessageSearchBloc(
key: messageSearchBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: messageSearchBloc,
),
);
final messageSearchBlocState = tester.state<MessageSearchBlocState>(
find.byKey(messageSearchBlocKey),
);
final messageResponseList = _generateMessages();
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer(
(_) async => SearchMessagesResponse()..results = messageResponseList,
);
messageSearchBlocState.search();
await expectLater(
messageSearchBlocState.messagesStream,
emits(isSameMessageResponseListAs(messageResponseList)),
);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
},
);
testWidgets(
'messageSearchBlocState.messagesStream should emit error '
'if client.search() throws',
(tester) async {
const messageSearchBlocKey = Key('messageSearchBloc');
const childKey = Key('child');
final messageSearchBloc = MessageSearchBloc(
key: messageSearchBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: messageSearchBloc,
),
);
final messageSearchBlocState = tester.state<MessageSearchBlocState>(
find.byKey(messageSearchBlocKey),
);
const error = 'Error! Error! Error!';
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).thenThrow(error);
messageSearchBlocState.search();
await expectLater(
messageSearchBlocState.messagesStream,
emitsError(error),
);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
},
);
testWidgets(
'calling messageSearchBlocState.search() again with an offset '
'should emit new data through messagesStream and also emit loading state '
'through queryMessagesLoading',
(tester) async {
const messageSearchBlocKey = Key('messageSearchBloc');
const childKey = Key('child');
final messageSearchBloc = MessageSearchBloc(
key: messageSearchBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: messageSearchBloc,
),
);
final messageSearchBlocState = tester.state<MessageSearchBlocState>(
find.byKey(messageSearchBlocKey),
);
final messageResponseList = _generateMessages();
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer(
(_) async => SearchMessagesResponse()..results = messageResponseList,
);
messageSearchBlocState.search();
await expectLater(
messageSearchBlocState.messagesStream,
emits(isSameMessageResponseListAs(messageResponseList)),
);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
final offset = messageResponseList.length;
final paginatedMessageResponseList = _generateMessages(offset: offset);
final pagination = PaginationParams(offset: offset);
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: pagination,
)).thenAnswer(
(_) async =>
SearchMessagesResponse()..results = paginatedMessageResponseList,
);
messageSearchBlocState.search(pagination: pagination);
await Future.wait([
expectLater(
messageSearchBlocState.queryMessagesLoading,
emitsInOrder([true, false]),
),
expectLater(
messageSearchBlocState.messagesStream,
emits(isSameMessageResponseListAs(
messageResponseList + paginatedMessageResponseList,
)),
),
]);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: pagination,
)).called(1);
},
);
testWidgets(
'calling messageSearchBlocState.search() again with an offset '
'should emit error through queryUsersLoading if '
'client.search() throws',
(tester) async {
const messageSearchBlocKey = Key('messageSearchBloc');
const childKey = Key('child');
final messageSearchBloc = MessageSearchBloc(
key: messageSearchBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: messageSearchBloc,
),
);
final messageSearchBlocState = tester.state<MessageSearchBlocState>(
find.byKey(messageSearchBlocKey),
);
final messageResponseList = _generateMessages();
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer(
(_) async => SearchMessagesResponse()..results = messageResponseList,
);
messageSearchBlocState.search();
await expectLater(
messageSearchBlocState.messagesStream,
emits(isSameMessageResponseListAs(messageResponseList)),
);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
final offset = messageResponseList.length;
final pagination = PaginationParams(offset: offset);
const error = 'Error! Error! Error!';
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: pagination,
)).thenThrow(error);
messageSearchBlocState.search(pagination: pagination);
await expectLater(
messageSearchBlocState.queryMessagesLoading,
emitsError(error),
);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: pagination,
)).called(1);
},
);
}
@@ -0,0 +1,535 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter_core/src/message_search_list_core.dart';
import 'mocks.dart';
void main() {
List<GetMessageResponse> _generateMessages({
int count = 3,
int offset = 0,
}) {
return List.generate(
count,
(index) {
index = index + offset;
return GetMessageResponse()
..message = Message(
id: 'testId$index',
text: 'testTextData$index',
)
..channel = ChannelModel(
cid: 'testCid',
);
},
);
}
test(
'should throw assertion error in case childBuilder is null',
() {
final messageSearchListCore = () => MessageSearchListCore(
childBuilder: null,
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
);
expect(messageSearchListCore, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error in case loadingBuilder is null',
() {
final messageSearchListCore = () => MessageSearchListCore(
childBuilder: (List<GetMessageResponse> messages) => Offstage(),
loadingBuilder: null,
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
);
expect(messageSearchListCore, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error in case emptyBuilder is null',
() {
final messageSearchListCore = () => MessageSearchListCore(
childBuilder: (List<GetMessageResponse> messages) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: null,
errorBuilder: (BuildContext context, Object error) => Offstage(),
);
expect(messageSearchListCore, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error in case errorBuilder is null',
() {
final messageSearchListCore = () => MessageSearchListCore(
childBuilder: (List<GetMessageResponse> messages) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: null,
);
expect(messageSearchListCore, throwsA(isA<AssertionError>()));
},
);
testWidgets(
'should throw if MessageSearchListCore is used where MessageSearchBloc '
'is not present in the widget tree',
(tester) async {
const messageSearchListCoreKey = Key('messageSearchListCore');
final messageSearchListCore = MessageSearchListCore(
key: messageSearchListCoreKey,
childBuilder: (List<GetMessageResponse> messages) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
);
await tester.pumpWidget(messageSearchListCore);
expect(find.byKey(messageSearchListCoreKey), findsNothing);
expect(tester.takeException(), isInstanceOf<Exception>());
},
);
testWidgets(
'should render MessageSearchListCore if used with '
'MessageSearchListCore as an ancestor',
(tester) async {
const messageSearchListCoreKey = Key('messageSearchListCore');
final messageSearchListCore = MessageSearchListCore(
key: messageSearchListCoreKey,
childBuilder: (List<GetMessageResponse> messages) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: MessageSearchBloc(
child: messageSearchListCore,
),
),
);
expect(find.byKey(messageSearchListCoreKey), findsOneWidget);
},
);
testWidgets(
'should assign loadData and paginateData callback to '
'UserListController if passed',
(tester) async {
const messageSearchListCoreKey = Key('messageSearchListCore');
final controller = MessageSearchListController();
final messageSearchListCore = MessageSearchListCore(
key: messageSearchListCoreKey,
childBuilder: (List<GetMessageResponse> messages) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
messageSearchListController: controller,
);
expect(controller.loadData, isNull);
expect(controller.paginateData, isNull);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: MessageSearchBloc(
child: messageSearchListCore,
),
),
);
expect(find.byKey(messageSearchListCoreKey), findsOneWidget);
expect(controller.loadData, isNotNull);
expect(controller.paginateData, isNotNull);
},
);
testWidgets(
'should build error widget if messageSearchBloc.messagesStream emits error',
(tester) async {
const messageSearchListCoreKey = Key('messageSearchListCore');
const errorWidgetKey = Key('errorWidget');
final messageSearchListCore = MessageSearchListCore(
key: messageSearchListCoreKey,
childBuilder: (List<GetMessageResponse> messages) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(
key: errorWidgetKey,
),
);
final mockClient = MockClient();
const error = 'Error! Error! Error!';
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).thenThrow(error);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: MessageSearchBloc(
child: messageSearchListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(errorWidgetKey), findsOneWidget);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
},
);
testWidgets(
'should build empty widget if messageSearchBloc.messagesStream '
'emits empty data',
(tester) async {
const messageSearchListCoreKey = Key('messageSearchListCore');
const emptyWidgetKey = Key('emptyWidget');
final messageSearchListCore = MessageSearchListCore(
key: messageSearchListCoreKey,
childBuilder: (List<GetMessageResponse> messages) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(key: emptyWidgetKey),
errorBuilder: (BuildContext context, Object error) => Offstage(),
);
final mockClient = MockClient();
final messageResponseList = <GetMessageResponse>[];
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer(
(_) async => SearchMessagesResponse()..results = messageResponseList,
);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: MessageSearchBloc(
child: messageSearchListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(emptyWidgetKey), findsOneWidget);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
},
);
testWidgets(
'should build child widget if usersBlocState.usersStream emits some data',
(tester) async {
const messageSearchListCoreKey = Key('messageSearchListCore');
const childWidgetKey = Key('childWidget');
final messageSearchListCore = MessageSearchListCore(
key: messageSearchListCoreKey,
childBuilder: (List<GetMessageResponse> messages) => Offstage(
key: childWidgetKey,
),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
);
final mockClient = MockClient();
final messageResponseList = _generateMessages();
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).thenAnswer(
(_) async => SearchMessagesResponse()..results = messageResponseList,
);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: MessageSearchBloc(
child: messageSearchListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(childWidgetKey), findsOneWidget);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: anyNamed('paginationParams'),
)).called(1);
},
);
testWidgets(
'should build child widget with paginated data '
'on calling channelListCoreState.paginateData',
(tester) async {
const messageSearchListCoreKey = Key('messageSearchListCore');
const childWidgetKey = Key('childWidget');
const pagination = PaginationParams();
final messageSearchListCore = MessageSearchListCore(
key: messageSearchListCoreKey,
childBuilder: (List<GetMessageResponse> messages) => Container(
key: childWidgetKey,
child: Text(
messages.map((e) => '${e.channel.cid}-${e.message.id}').join(','),
),
),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
paginationParams: pagination,
);
final mockClient = MockClient();
final messageResponseList = _generateMessages();
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: pagination,
)).thenAnswer(
(_) async => SearchMessagesResponse()..results = messageResponseList,
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChatCore(
client: mockClient,
child: MessageSearchBloc(
child: messageSearchListCore,
),
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(childWidgetKey), findsOneWidget);
expect(
find.text(
messageResponseList
.map((e) => '${e.channel.cid}-${e.message.id}')
.join(','),
),
findsOneWidget,
);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: pagination,
)).called(1);
final messageSearchListCoreState =
tester.state<MessageSearchListCoreState>(
find.byKey(messageSearchListCoreKey),
);
final offset = messageResponseList.length;
final paginatedMessageResponseList = _generateMessages(offset: offset);
final updatedPagination = pagination.copyWith(offset: offset);
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: updatedPagination,
)).thenAnswer(
(_) async =>
SearchMessagesResponse()..results = paginatedMessageResponseList,
);
await messageSearchListCoreState.paginateData();
await tester.pumpAndSettle();
expect(find.byKey(childWidgetKey), findsOneWidget);
expect(
find.text([
...messageResponseList,
...paginatedMessageResponseList,
].map((e) => '${e.channel.cid}-${e.message.id}').join(',')),
findsOneWidget,
);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: updatedPagination,
)).called(1);
},
);
testWidgets(
'should rebuild MessageSearchListCore with updated widget data '
'on calling setState()',
(tester) async {
const pagination = PaginationParams();
StateSetter _stateSetter;
int limit = pagination.limit;
const messageSearchListCoreKey = Key('messageSearchListCore');
const childWidgetKey = Key('childWidget');
MessageSearchListCore messageSearchListCoreBuilder(int limit) =>
MessageSearchListCore(
key: messageSearchListCoreKey,
childBuilder: (List<GetMessageResponse> messages) => Container(
key: childWidgetKey,
child: Text(
messages
.map((e) => '${e.channel.cid}-${e.message.id}')
.join(','),
),
),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(),
paginationParams: pagination.copyWith(limit: limit),
);
final mockClient = MockClient();
final messageResponseList = _generateMessages();
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: pagination,
)).thenAnswer(
(_) async => SearchMessagesResponse()..results = messageResponseList,
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChatCore(
client: mockClient,
child: MessageSearchBloc(
child: StatefulBuilder(builder: (context, stateSetter) {
// Assigning stateSetter for rebuilding UserListCore
_stateSetter = stateSetter;
return messageSearchListCoreBuilder(limit);
}),
),
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(childWidgetKey), findsOneWidget);
expect(
find.text(
messageResponseList
.map((e) => '${e.channel.cid}-${e.message.id}')
.join(','),
),
findsOneWidget,
);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: pagination,
)).called(1);
// Rebuilding MessageSearchListCore with new pagination limit
_stateSetter(() => limit = 6);
final updatedMessageResponseList = _generateMessages(count: limit);
final updatedPagination = pagination.copyWith(limit: limit);
when(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: updatedPagination,
)).thenAnswer(
(_) async =>
SearchMessagesResponse()..results = updatedMessageResponseList,
);
await tester.pumpAndSettle();
expect(find.byKey(childWidgetKey), findsOneWidget);
expect(
find.text(updatedMessageResponseList
.map((e) => '${e.channel.cid}-${e.message.id}')
.join(',')),
findsOneWidget,
);
verify(mockClient.search(
any,
query: anyNamed('query'),
sort: anyNamed('sort'),
messageFilters: anyNamed('messageFilters'),
paginationParams: updatedPagination,
)).called(1);
},
);
}
@@ -1,10 +1,39 @@
import 'package:mockito/mockito.dart';
import 'package:stream_chat/stream_chat.dart';
class MockClient extends Mock implements StreamChatClient {}
class MockLogger extends Mock implements Logger {}
class MockClientState extends Mock implements ClientState {}
class MockClient extends Mock implements StreamChatClient {
final Logger logger = MockLogger();
class MockChannel extends Mock implements Channel {}
ClientState _state;
@override
ClientState get state => _state ??= MockClientState();
}
class MockClientState extends Mock implements ClientState {
OwnUser _user;
@override
OwnUser get user => _user ??= OwnUser(
id: 'testUserId',
role: 'admin',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
}
class MockChannel extends Mock implements Channel {
ChannelClientState _state;
@override
ChannelClientState get state => _state ??= MockChannelState();
StreamChatClient _client;
@override
StreamChatClient get client => _client ??= MockClient();
}
class MockChannelState extends Mock implements ChannelClientState {}
@@ -0,0 +1,343 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'mocks.dart';
void main() {
List<Message> _generateMessages({
int count = 3,
int offset = 0,
bool threads = false,
}) {
final users = List.generate(count, (index) {
index = count + offset;
return User(id: 'testUserId$index');
});
final messages = List.generate(
count,
(index) {
index = index + offset;
return Message(
id: 'testMessageId$index',
type: 'testType',
user: users[index],
createdAt: DateTime.now(),
shadowed: false,
replyCount: index,
updatedAt: DateTime.now(),
extraData: {'extra_test_field': 'extraTestData'},
text: 'Dummy text #$index',
pinned: true,
pinnedAt: DateTime.now(),
pinnedBy: User(id: 'testUserId$index'),
);
},
);
final threadMessages = List.generate(
count,
(index) {
index = index + offset;
return Message(
id: 'testThreadMessageId$index',
type: 'testType',
user: users[index],
parentId: messages[0].id,
createdAt: DateTime.now(),
shadowed: false,
replyCount: index,
updatedAt: DateTime.now(),
extraData: {'extra_test_field': 'extraTestData'},
text: 'Dummy text #$index',
pinned: true,
pinnedAt: DateTime.now(),
pinnedBy: User(id: 'testUserId$index'),
);
},
);
return threads ? threadMessages : messages;
}
test(
'should throw assertion error if child is null',
() async {
final mockChannel = MockChannel();
const streamChannelKey = Key('streamChannel');
final streamChannel = () => StreamChannel(
key: streamChannelKey,
channel: mockChannel,
child: null,
);
expect(streamChannel, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error if channel is null',
() async {
const streamChannelKey = Key('streamChannel');
final streamChannel = () => StreamChannel(
key: streamChannelKey,
child: Offstage(),
channel: null,
);
expect(streamChannel, throwsA(isA<AssertionError>()));
},
);
testWidgets(
'should render StreamChannel if both channel and child is provided',
(tester) async {
final mockChannel = MockChannel();
const streamChannelKey = Key('streamChannel');
const childKey = Key('childKey');
final streamChannel = StreamChannel(
key: streamChannelKey,
channel: mockChannel,
child: Offstage(key: childKey),
);
await tester.pumpWidget(streamChannel);
expect(find.byKey(streamChannelKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
},
);
testWidgets(
'should build error widget if channel.initialized throws',
(tester) async {
final mockChannel = MockChannel();
const streamChannelKey = Key('streamChannel');
const childKey = Key('childKey');
final streamChannel = StreamChannel(
key: streamChannelKey,
channel: mockChannel,
child: Offstage(key: childKey),
);
final errorMessage = 'Error! Error! Error!';
final error = DioError(type: DioErrorType.RESPONSE, error: errorMessage);
when(mockChannel.initialized).thenAnswer((_) => Future.error(error));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: streamChannel,
),
);
await tester.pumpAndSettle();
expect(find.text(errorMessage), findsOneWidget);
verify(mockChannel.initialized).called(1);
},
);
testWidgets(
'should show circular progress indicator showLoading is true '
'and channel is still not initialized',
(tester) async {
final mockChannel = MockChannel();
const streamChannelKey = Key('streamChannel');
const childKey = Key('childKey');
final streamChannel = StreamChannel(
key: streamChannelKey,
channel: mockChannel,
child: Offstage(key: childKey),
showLoading: true,
);
when(mockChannel.initialized).thenAnswer((_) async => false);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: streamChannel,
),
);
await tester.pump(const Duration(seconds: 1));
expect(find.byType(CircularProgressIndicator), findsOneWidget);
verify(mockChannel.initialized).called(1);
},
);
testWidgets(
'should preload extra messages if initialMessageId is provided',
(tester) async {
final mockChannel = MockChannel();
const streamChannelKey = Key('streamChannel');
const childKey = Key('childKey');
final streamChannel = StreamChannel(
key: streamChannelKey,
channel: mockChannel,
child: Offstage(key: childKey),
initialMessageId: 'testInitialMessageId',
);
when(mockChannel.initialized).thenAnswer((_) async => true);
final messages = _generateMessages();
when(mockChannel.query(
options: anyNamed('options'),
messagesPagination: anyNamed('messagesPagination'),
membersPagination: anyNamed('membersPagination'),
watchersPagination: anyNamed('watchersPagination'),
preferOffline: anyNamed('preferOffline'),
)).thenAnswer((_) async => ChannelState(messages: messages));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: streamChannel,
),
);
await tester.pumpAndSettle();
verify(mockChannel.initialized).called(1);
verify(mockChannel.query(
options: anyNamed('options'),
messagesPagination: anyNamed('messagesPagination'),
membersPagination: anyNamed('membersPagination'),
watchersPagination: anyNamed('watchersPagination'),
preferOffline: anyNamed('preferOffline'),
)).called(
2, // Fetching After messages + Fetching Before messages,
);
},
);
testWidgets(
'should rebuild StreamChannel with updated widget data '
'on calling setState()',
(tester) async {
StateSetter _stateSetter;
var initialMessageId = 'testInitialMessageId';
final mockChannel = MockChannel();
const streamChannelKey = Key('streamChannel');
const childKey = Key('childKey');
StreamChannel streamChannelBuilder(String initialMessageId) =>
StreamChannel(
key: streamChannelKey,
channel: mockChannel,
child: Offstage(key: childKey),
initialMessageId: initialMessageId,
);
final beforePagination = PaginationParams(
lessThan: initialMessageId,
limit: 20,
);
final afterPagination = PaginationParams(
greaterThanOrEqual: initialMessageId,
limit: 20,
);
when(mockChannel.initialized).thenAnswer((_) async => true);
final messages = _generateMessages();
when(mockChannel.query(
options: anyNamed('options'),
messagesPagination: beforePagination,
membersPagination: anyNamed('membersPagination'),
watchersPagination: anyNamed('watchersPagination'),
preferOffline: anyNamed('preferOffline'),
)).thenAnswer((_) async => ChannelState(messages: messages));
when(mockChannel.query(
options: anyNamed('options'),
messagesPagination: afterPagination,
membersPagination: anyNamed('membersPagination'),
watchersPagination: anyNamed('watchersPagination'),
preferOffline: anyNamed('preferOffline'),
)).thenAnswer((_) async => ChannelState(messages: messages));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (context, stateSetter) {
// Assigning stateSetter for rebuilding StreamChannel
_stateSetter = stateSetter;
return streamChannelBuilder(initialMessageId);
},
),
),
);
await tester.pumpAndSettle();
verify(mockChannel.query(
options: anyNamed('options'),
messagesPagination: beforePagination,
membersPagination: anyNamed('membersPagination'),
watchersPagination: anyNamed('watchersPagination'),
preferOffline: anyNamed('preferOffline'),
)).called(1);
verify(mockChannel.query(
options: anyNamed('options'),
messagesPagination: afterPagination,
membersPagination: anyNamed('membersPagination'),
watchersPagination: anyNamed('watchersPagination'),
preferOffline: anyNamed('preferOffline'),
)).called(1);
_stateSetter(() => initialMessageId = 'testInitialMessageId2');
final updatedBeforePagination = beforePagination.copyWith(
lessThan: initialMessageId,
);
final updatedAfterPagination = afterPagination.copyWith(
greaterThanOrEqual: initialMessageId,
);
when(mockChannel.query(
options: anyNamed('options'),
messagesPagination: updatedBeforePagination,
membersPagination: anyNamed('membersPagination'),
watchersPagination: anyNamed('watchersPagination'),
preferOffline: anyNamed('preferOffline'),
)).thenAnswer((_) async => ChannelState(messages: messages));
when(mockChannel.query(
options: anyNamed('options'),
messagesPagination: updatedAfterPagination,
membersPagination: anyNamed('membersPagination'),
watchersPagination: anyNamed('watchersPagination'),
preferOffline: anyNamed('preferOffline'),
)).thenAnswer((_) async => ChannelState(messages: messages));
await tester.pumpAndSettle();
verify(mockChannel.query(
options: anyNamed('options'),
messagesPagination: updatedBeforePagination,
membersPagination: anyNamed('membersPagination'),
watchersPagination: anyNamed('watchersPagination'),
preferOffline: anyNamed('preferOffline'),
)).called(1);
verify(mockChannel.query(
options: anyNamed('options'),
messagesPagination: updatedAfterPagination,
membersPagination: anyNamed('membersPagination'),
watchersPagination: anyNamed('watchersPagination'),
preferOffline: anyNamed('preferOffline'),
)).called(1);
},
);
}
@@ -1,181 +1,289 @@
import 'dart:async';
import 'package:fake_async/fake_async.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:mockito/mockito.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'mocks.dart';
class MockShowLocalNotifications extends Mock {
class MockOnBackgroundEventReceived extends Mock {
void call(Event event);
}
void main() {
testWidgets(
'StreamChatCore.of(context) should throw an exception',
(WidgetTester tester) async {
await tester.pumpWidget(
Builder(
builder: (context) {
expect(() => StreamChatCore.of(context), throwsException);
return Container();
},
),
);
test(
'should throw assertion error in case client is null',
() {
final streamChatCore = () => StreamChatCore(
client: null,
child: Offstage(),
);
expect(streamChatCore, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error in case child is null',
() {
final mockClient = MockClient();
final streamChatCore = () => StreamChatCore(
client: mockClient,
child: null,
);
expect(streamChatCore, throwsA(isA<AssertionError>()));
},
);
testWidgets(
'StreamChatCore.of(context) should return the StreamChatCore ancestor',
(WidgetTester tester) async {
final client = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: client,
child: Builder(
builder: (context) {
final sc = StreamChatCore.of(context);
expect(sc, isNotNull);
return Container();
},
),
),
'should render StreamChatCore if both client and child is provided',
(tester) async {
final mockClient = MockClient();
const streamChatCoreKey = Key('streamChatCore');
const childKey = Key('child');
final streamChatCore = StreamChatCore(
key: streamChatCoreKey,
client: mockClient,
child: Offstage(key: childKey),
);
await tester.pumpWidget(streamChatCore);
expect(find.byKey(streamChatCoreKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
},
);
testWidgets(
'StreamChatCore.of(context).client should return the client',
(WidgetTester tester) async {
final client = MockClient();
final clientState = MockClientState();
when(client.state).thenReturn(clientState);
when(clientState.user).thenReturn(
OwnUser(
id: 'test',
),
);
final userStream = Stream<OwnUser>.value(
OwnUser(
id: 'test',
),
);
when(clientState.userStream).thenAnswer(
(_) => userStream,
'should render StreamChatCore if both client and child is provided',
(tester) async {
final mockClient = MockClient();
const streamChatCoreKey = Key('streamChatCore');
const childKey = Key('child');
final streamChatCore = StreamChatCore(
key: streamChatCoreKey,
client: mockClient,
child: Offstage(key: childKey),
);
await tester.pumpWidget(
StreamChatCore(
client: client,
child: Builder(
builder: (context) {
final sc = StreamChatCore.of(context);
expect(sc.client, client);
expect(sc.user, client.state.user);
expect(sc.userStream, userStream);
return Container();
},
),
),
);
await tester.pumpWidget(streamChatCore);
expect(find.byKey(streamChatCoreKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
},
);
testWidgets(
'StreamChatCore should disconnect on background',
(WidgetTester tester) async {
fakeAsync((_async) {
final client = MockClient();
final clientState = MockClientState();
final channel = MockChannel();
when(client.state).thenReturn(clientState);
when(clientState.user).thenReturn(
OwnUser(
id: 'test',
),
);
final showLocalNotificationMock = MockShowLocalNotifications().call;
final eventStreamController = StreamController<Event>();
when(client.on()).thenAnswer((_) => eventStreamController.stream);
'didChangeAppLifecycleState should call client.disconnect() and return '
'if onBackgroundEventReceived is null and the widget lifestyle changes to '
'AppLifecycleState.paused',
(tester) async {
final mockClient = MockClient();
const streamChatCoreKey = Key('streamChatCore');
const childKey = Key('child');
final streamChatCore = StreamChatCore(
key: streamChatCoreKey,
client: mockClient,
child: Offstage(key: childKey),
);
when(client.channel('test', id: 'testid')).thenReturn(channel);
await tester.pumpWidget(streamChatCore);
final scKey = GlobalKey<StreamChatCoreState>();
tester.pumpWidget(
StreamChatCore(
key: scKey,
client: client,
onBackgroundEventReceived: showLocalNotificationMock,
backgroundKeepAlive: const Duration(seconds: 4),
child: Builder(
builder: (context) {
return Container();
},
),
),
expect(find.byKey(streamChatCoreKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
when(mockClient.disconnect()).thenAnswer((_) async {
return;
});
final streamChatCoreState = tester.state<StreamChatCoreState>(
find.byKey(streamChatCoreKey),
);
streamChatCoreState.didChangeAppLifecycleState(AppLifecycleState.paused);
verify(mockClient.disconnect()).called(1);
},
);
testWidgets(
'didChangeAppLifecycleState should subscribe to client events for '
'backgroundKeepAlive duration if onBackgroundEventReceived is provided '
'and the widget lifestyle changes to AppLifecycleState.paused',
(tester) async {
await tester.runAsync(() async {
final mockClient = MockClient();
final mockOnBackgroundEventReceived = MockOnBackgroundEventReceived();
const backgroundKeepAlive = const Duration(seconds: 3);
const streamChatCoreKey = Key('streamChatCore');
const childKey = Key('child');
final streamChatCore = StreamChatCore(
key: streamChatCoreKey,
client: mockClient,
child: Offstage(key: childKey),
onBackgroundEventReceived: mockOnBackgroundEventReceived,
backgroundKeepAlive: backgroundKeepAlive,
);
final sc = scKey.currentState;
sc.didChangeAppLifecycleState(AppLifecycleState.paused);
await tester.pumpWidget(streamChatCore);
_async.elapse(Duration(seconds: 5));
expect(find.byKey(streamChatCoreKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
verify(client.disconnect()).called(1);
eventStreamController.close();
final event = Event();
when(mockClient.on()).thenAnswer((_) => Stream.value(event));
when(mockClient.disconnect()).thenAnswer((_) async {
return;
});
final streamChatCoreState = tester.state<StreamChatCoreState>(
find.byKey(streamChatCoreKey),
);
streamChatCoreState
.didChangeAppLifecycleState(AppLifecycleState.paused);
await untilCalled(mockOnBackgroundEventReceived.call(event));
verify(mockOnBackgroundEventReceived.call(event)).called(1);
await Future.delayed(backgroundKeepAlive);
verify(mockClient.disconnect()).called(1);
verifyNever(mockOnBackgroundEventReceived.call(event));
});
},
);
testWidgets(
'StreamChatCore should handle notifications when on background and connected',
(WidgetTester tester) async {
final client = MockClient();
final clientState = MockClientState();
final channel = MockChannel();
when(client.state).thenReturn(clientState);
when(clientState.user).thenReturn(
OwnUser(
id: 'test',
),
);
final showLocalNotificationMock = MockShowLocalNotifications().call;
final eventStreamController = StreamController<Event>();
when(client.on()).thenAnswer((_) => eventStreamController.stream);
'didChangeAppLifecycleState should cancel the backgroundKeepAlive timer '
'if it is currently running in case the widget lifestyle changes to '
'AppLifecycleState.resume',
(tester) async {
await tester.runAsync(() async {
final mockClient = MockClient();
final mockOnBackgroundEventReceived = MockOnBackgroundEventReceived();
const backgroundKeepAlive = const Duration(seconds: 3);
const streamChatCoreKey = Key('streamChatCore');
const childKey = Key('child');
final streamChatCore = StreamChatCore(
key: streamChatCoreKey,
client: mockClient,
child: Offstage(key: childKey),
onBackgroundEventReceived: mockOnBackgroundEventReceived,
backgroundKeepAlive: backgroundKeepAlive,
);
when(client.channel('test', id: 'testid')).thenReturn(channel);
await tester.pumpWidget(streamChatCore);
final scKey = GlobalKey<StreamChatCoreState>();
await tester.pumpWidget(
StreamChatCore(
key: scKey,
client: client,
onBackgroundEventReceived: showLocalNotificationMock,
backgroundKeepAlive: const Duration(seconds: 4),
child: Builder(
builder: (context) {
return Container();
},
),
),
);
expect(find.byKey(streamChatCoreKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
final sc = scKey.currentState;
sc.didChangeAppLifecycleState(AppLifecycleState.paused);
final event = Event(
type: EventType.messageNew,
message: Message(text: 'hey'),
channelType: 'test',
channelId: 'testid',
user: User(id: 'other user'),
);
eventStreamController.add(event);
final event = Event();
when(mockClient.on()).thenAnswer((_) => Stream.value(event));
await untilCalled(showLocalNotificationMock(event));
final streamChatCoreState = tester.state<StreamChatCoreState>(
find.byKey(streamChatCoreKey),
);
verify(showLocalNotificationMock(event)).called(1);
eventStreamController.close();
streamChatCoreState
.didChangeAppLifecycleState(AppLifecycleState.paused);
await untilCalled(mockOnBackgroundEventReceived.call(event));
verify(mockOnBackgroundEventReceived.call(event)).called(1);
streamChatCoreState
.didChangeAppLifecycleState(AppLifecycleState.resumed);
verifyNever(mockOnBackgroundEventReceived.call(event));
});
},
);
testWidgets(
'didChangeAppLifecycleState should call client.connect() '
'if the connectionStatus is ConnectionStatus.disconnected in case the '
'widget lifestyle changes to AppLifecycleState.resume',
(tester) async {
await tester.runAsync(() async {
final mockClient = MockClient();
const streamChatCoreKey = Key('streamChatCore');
const childKey = Key('child');
final streamChatCore = StreamChatCore(
key: streamChatCoreKey,
client: mockClient,
child: Offstage(key: childKey),
);
await tester.pumpWidget(streamChatCore);
expect(find.byKey(streamChatCoreKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
final event = Event();
when(mockClient.on()).thenAnswer((_) => Stream.value(event));
when(mockClient.connect()).thenAnswer((_) async => event);
when(mockClient.wsConnectionStatus)
.thenReturn(ConnectionStatus.disconnected);
final streamChatCoreState = tester.state<StreamChatCoreState>(
find.byKey(streamChatCoreKey),
);
streamChatCoreState
.didChangeAppLifecycleState(AppLifecycleState.paused);
await Future.delayed(const Duration(seconds: 1));
streamChatCoreState
.didChangeAppLifecycleState(AppLifecycleState.resumed);
verify(mockClient.connect()).called(1);
});
},
);
testWidgets(
'streamChatCoreState.userStream should emit all the user events '
'provided by client',
(tester) async {
await tester.runAsync(() async {
final userController = StreamController<OwnUser>();
final mockClient = MockClient();
const streamChatCoreKey = Key('streamChatCore');
const childKey = Key('child');
final streamChatCore = StreamChatCore(
key: streamChatCoreKey,
client: mockClient,
child: Offstage(key: childKey),
);
await tester.pumpWidget(streamChatCore);
expect(find.byKey(streamChatCoreKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
when(mockClient.state.userStream)
.thenAnswer((_) => userController.stream);
final streamChatCoreState = tester.state<StreamChatCoreState>(
find.byKey(streamChatCoreKey),
);
final ownUser = OwnUser(id: 'testUserId');
userController.add(ownUser);
await expectLater(
streamChatCoreState.userStream,
emits(ownUser),
);
addTearDown(() {
userController.close();
});
});
},
);
}
@@ -0,0 +1,572 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:stream_chat_flutter_core/src/user_list_core.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'mocks.dart';
void main() {
const alphabets = 'abcdefghijklmnopqrstuvwxyz';
List<User> _generateUsers({
int count = 3,
int offset = 0,
}) {
return List.generate(
count,
(index) {
index = index + offset;
return User(
id: 'testId$index',
role: 'testRole$index',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
lastActive: DateTime.now(),
online: true,
banned: false,
extraData: {
'name': '${alphabets[index]}-testName',
},
);
},
);
}
test(
'should throw assertion error in case listBuilder is null',
() {
final userListCore = () => UserListCore(
listBuilder: null,
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (Object error) => Offstage(),
);
expect(userListCore, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error in case loadingBuilder is null',
() {
final userListCore = () => UserListCore(
listBuilder: (_, __) => Offstage(),
loadingBuilder: null,
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (Object error) => Offstage(),
);
expect(userListCore, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error in case emptyBuilder is null',
() {
final userListCore = () => UserListCore(
listBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: null,
errorBuilder: (Object error) => Offstage(),
);
expect(userListCore, throwsA(isA<AssertionError>()));
},
);
test(
'should throw assertion error in case errorBuilder is null',
() {
final userListCore = () => UserListCore(
listBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: null,
);
expect(userListCore, throwsA(isA<AssertionError>()));
},
);
testWidgets(
'should throw if UserListCore is used where UsersBloc is not present '
'in the widget tree',
(tester) async {
const userListCoreKey = Key('userListCore');
final userListCore = UserListCore(
key: userListCoreKey,
listBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (Object error) => Offstage(),
);
await tester.pumpWidget(userListCore);
expect(find.byKey(userListCoreKey), findsNothing);
expect(tester.takeException(), isInstanceOf<Exception>());
},
);
testWidgets(
'should render UserListCore if used with UsersBloc as an ancestor',
(tester) async {
const userListCoreKey = Key('userListCore');
final userListCore = UserListCore(
key: userListCoreKey,
listBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (Object error) => Offstage(),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: UsersBloc(
child: userListCore,
),
),
);
expect(find.byKey(userListCoreKey), findsOneWidget);
},
);
testWidgets(
'should assign loadData and paginateData callback to '
'UserListController if passed',
(tester) async {
const userListCoreKey = Key('userListCore');
final controller = UserListController();
final userListCore = UserListCore(
key: userListCoreKey,
listBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (Object error) => Offstage(),
userListController: controller,
);
expect(controller.loadData, isNull);
expect(controller.paginateData, isNull);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: UsersBloc(
child: userListCore,
),
),
);
expect(find.byKey(userListCoreKey), findsOneWidget);
expect(controller.loadData, isNotNull);
expect(controller.paginateData, isNotNull);
},
);
testWidgets(
'should build error widget if usersBlocState.usersStream emits error',
(tester) async {
const userListCoreKey = Key('userListCore');
const errorWidgetKey = Key('errorWidget');
final userListCore = UserListCore(
key: userListCoreKey,
listBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (Object error) => Container(key: errorWidgetKey),
);
final mockClient = MockClient();
const error = 'Error! Error! Error!';
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).thenThrow(error);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: UsersBloc(
child: userListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(errorWidgetKey), findsOneWidget);
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).called(1);
},
);
testWidgets(
'should build empty widget if usersBlocState.usersStream emits empty data',
(tester) async {
const userListCoreKey = Key('userListCore');
const emptyWidgetKey = Key('emptyWidget');
final userListCore = UserListCore(
key: userListCoreKey,
listBuilder: (_, __) => Offstage(),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Container(key: emptyWidgetKey),
errorBuilder: (Object error) => Offstage(),
);
final mockClient = MockClient();
const users = <User>[];
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).thenAnswer((_) async => QueryUsersResponse()..users = users);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: UsersBloc(
child: userListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(emptyWidgetKey), findsOneWidget);
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).called(1);
},
);
testWidgets(
'should build list widget if usersBlocState.usersStream emits some data',
(tester) async {
const userListCoreKey = Key('userListCore');
const listWidgetKey = Key('listWidget');
final userListCore = UserListCore(
key: userListCoreKey,
listBuilder: (_, __) => Container(key: listWidgetKey),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (Object error) => Offstage(),
);
final mockClient = MockClient();
final users = _generateUsers();
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).thenAnswer((_) async => QueryUsersResponse()..users = users);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: UsersBloc(
child: userListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).called(1);
},
);
testWidgets(
'should build list widget with grouped data if groupAlphabetically is true',
(tester) async {
const userListCoreKey = Key('userListCore');
const listWidgetKey = Key('listWidget');
final userListCore = UserListCore(
key: userListCoreKey,
listBuilder: (_, items) => Container(
key: listWidgetKey,
child: ListView(
children: items.map((e) {
return Container(
key: Key(e.key),
child: e.when(
headerItem: (heading) => Text(heading),
userItem: (user) => Text(user.id),
),
);
}).toList(growable: false),
),
),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (Object error) => Offstage(),
groupAlphabetically: true,
);
final mockClient = MockClient();
final users = _generateUsers();
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).thenAnswer((_) async => QueryUsersResponse()..users = users);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChatCore(
client: mockClient,
child: UsersBloc(
child: userListCore,
),
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
for (final user in users) {
expect(find.byKey(Key('HEADER-${user.name[0]}')), findsOneWidget);
expect(find.byKey(Key('USER-${user.id}')), findsOneWidget);
}
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).called(1);
},
);
testWidgets(
'should build list widget with paginated data '
'on calling channelListCoreState.paginateData',
(tester) async {
const userListCoreKey = Key('userListCore');
const listWidgetKey = Key('listWidget');
const pagination = PaginationParams();
final userListCore = UserListCore(
key: userListCoreKey,
listBuilder: (_, items) => Container(
key: listWidgetKey,
child: ListView(
children: items.map((e) {
return Container(
key: Key(e.key),
child: e.when(
headerItem: (heading) => Text(heading),
userItem: (user) => Text(user.id),
),
);
}).toList(growable: false),
),
),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (Object error) => Offstage(),
pagination: pagination,
groupAlphabetically: true,
);
final mockClient = MockClient();
final users = _generateUsers();
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).thenAnswer((_) async => QueryUsersResponse()..users = users);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChatCore(
client: mockClient,
child: UsersBloc(
child: userListCore,
),
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
for (final user in users) {
expect(find.byKey(Key('HEADER-${user.name[0]}')), findsOneWidget);
expect(find.byKey(Key('USER-${user.id}')), findsOneWidget);
}
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).called(1);
final userListCoreState = tester.state<UserListCoreState>(
find.byKey(userListCoreKey),
);
final offset = users.length;
final paginatedUsers = _generateUsers(offset: offset);
final updatedPagination = pagination.copyWith(offset: offset);
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: updatedPagination,
)).thenAnswer((_) async => QueryUsersResponse()..users = paginatedUsers);
await userListCoreState.paginateData();
await tester.pumpAndSettle();
for (final user in users + paginatedUsers) {
expect(find.byKey(Key('HEADER-${user.name[0]}')), findsOneWidget);
expect(find.byKey(Key('USER-${user.id}')), findsOneWidget);
}
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: updatedPagination,
)).called(1);
},
);
testWidgets(
'should rebuild UserListCore with updated widget data '
'on calling setState()',
(tester) async {
const pagination = PaginationParams();
StateSetter _stateSetter;
int limit = pagination.limit;
const userListCoreKey = Key('userListCore');
const listWidgetKey = Key('listWidget');
UserListCore userListCoreBuilder(int limit) => UserListCore(
key: userListCoreKey,
listBuilder: (_, items) => Container(
key: listWidgetKey,
child: ListView(
children: items.map((e) {
return Container(
key: Key(e.key),
child: e.when(
headerItem: (heading) => Text(heading),
userItem: (user) => Text(user.id),
),
);
}).toList(growable: false),
),
),
loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (Object error) => Offstage(),
pagination: pagination.copyWith(limit: limit),
groupAlphabetically: true,
);
final mockClient = MockClient();
final users = _generateUsers();
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).thenAnswer((_) async => QueryUsersResponse()..users = users);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChatCore(
client: mockClient,
child: UsersBloc(
child: StatefulBuilder(builder: (context, stateSetter) {
// Assigning stateSetter for rebuilding UserListCore
_stateSetter = stateSetter;
return userListCoreBuilder(limit);
}),
),
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
for (final user in users) {
expect(find.byKey(Key('HEADER-${user.name[0]}')), findsOneWidget);
expect(find.byKey(Key('USER-${user.id}')), findsOneWidget);
}
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).called(1);
// Rebuilding UserListCore with new pagination limit
_stateSetter(() => limit = 6);
final updatedUsers = _generateUsers(count: limit);
final updatedPagination = pagination.copyWith(limit: limit);
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: updatedPagination,
)).thenAnswer((_) async => QueryUsersResponse()..users = updatedUsers);
await tester.pumpAndSettle();
for (final user in updatedUsers) {
expect(find.byKey(Key('HEADER-${user.name[0]}')), findsOneWidget);
expect(find.byKey(Key('USER-${user.id}')), findsOneWidget);
}
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: updatedPagination,
)).called(1);
},
);
}
@@ -0,0 +1,327 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter_core/src/stream_chat_core.dart';
import 'package:stream_chat_flutter_core/src/users_bloc.dart';
import 'package:mockito/mockito.dart';
import 'matchers/users_matcher.dart';
import 'mocks.dart';
void main() {
List<User> _generateUsers({
int count = 3,
int offset = 0,
}) {
return List.generate(
count,
(index) {
index = index + offset;
return User(
id: 'testId$index',
role: 'testRole$index',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
lastActive: DateTime.now(),
online: true,
banned: false,
extraData: {'extra_data_key': 'extraDataValue'},
);
},
);
}
test(
'should throw assertion error if child is null',
() async {
const usersBlocKey = Key('usersBloc');
final usersBloc = () => UsersBloc(
key: usersBlocKey,
child: null,
);
expect(usersBloc, throwsA(isA<AssertionError>()));
},
);
testWidgets(
'usersBlocState.queryUsers() should throw if used where '
'StreamChat is not present in the widget tree',
(tester) async {
const usersBlocKey = Key('usersBloc');
const childKey = Key('child');
final usersBloc = UsersBloc(
key: usersBlocKey,
child: Offstage(key: childKey),
);
await tester.pumpWidget(usersBloc);
expect(find.byKey(usersBlocKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
final usersBlocState = tester.state<UsersBlocState>(
find.byKey(usersBlocKey),
);
try {
await usersBlocState.queryUsers();
} catch (e) {
expect(e, isInstanceOf<Exception>());
}
},
);
testWidgets(
'usersBlocState.queryUsers() should emit data through usersStream',
(tester) async {
const usersBlocKey = Key('usersBloc');
const childKey = Key('child');
final usersBloc = UsersBloc(
key: usersBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: usersBloc,
),
);
final usersBlocState = tester.state<UsersBlocState>(
find.byKey(usersBlocKey),
);
final users = _generateUsers();
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).thenAnswer((_) async => QueryUsersResponse()..users = users);
usersBlocState.queryUsers();
await expectLater(
usersBlocState.usersStream,
emits(isSameUserListAs(users)),
);
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).called(1);
},
);
testWidgets(
'usersBlocState.usersStream should emit error '
'if client.queryUsers() throws',
(tester) async {
const usersBlocKey = Key('usersBloc');
const childKey = Key('child');
final usersBloc = UsersBloc(
key: usersBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: usersBloc,
),
);
final usersBlocState = tester.state<UsersBlocState>(
find.byKey(usersBlocKey),
);
final error = 'Error! Error! Error!';
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).thenThrow(error);
usersBlocState.queryUsers();
await expectLater(
usersBlocState.usersStream,
emitsError(error),
);
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).called(1);
},
);
testWidgets(
'calling usersBlocState.queryUsers() again with an offset '
'should emit new data through usersStream and also emit loading state '
'through queryUsersLoading',
(tester) async {
const usersBlocKey = Key('usersBloc');
const childKey = Key('child');
final usersBloc = UsersBloc(
key: usersBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: usersBloc,
),
);
final usersBlocState = tester.state<UsersBlocState>(
find.byKey(usersBlocKey),
);
final users = _generateUsers();
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).thenAnswer((_) async => QueryUsersResponse()..users = users);
usersBlocState.queryUsers();
await expectLater(
usersBlocState.usersStream,
emits(isSameUserListAs(users)),
);
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).called(1);
final offset = users.length;
final paginatedUsers = _generateUsers(offset: offset);
final pagination = PaginationParams(offset: offset);
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: pagination,
)).thenAnswer((_) async => QueryUsersResponse()..users = paginatedUsers);
usersBlocState.queryUsers(pagination: pagination);
await Future.wait([
expectLater(
usersBlocState.queryUsersLoading,
emitsInOrder([true, false]),
),
expectLater(
usersBlocState.usersStream,
emits(isSameUserListAs(users + paginatedUsers)),
),
]);
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: pagination,
)).called(1);
},
);
testWidgets(
'calling usersBlocState.queryUsers() again with an offset '
'should emit error through queryUsersLoading if '
'client.queryUsers() throws',
(tester) async {
const usersBlocKey = Key('usersBloc');
const childKey = Key('child');
final usersBloc = UsersBloc(
key: usersBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: usersBloc,
),
);
final usersBlocState = tester.state<UsersBlocState>(
find.byKey(usersBlocKey),
);
final users = _generateUsers();
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).thenAnswer((_) async => QueryUsersResponse()..users = users);
usersBlocState.queryUsers();
await expectLater(
usersBlocState.usersStream,
emits(isSameUserListAs(users)),
);
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: anyNamed('pagination'),
)).called(1);
final offset = users.length;
final pagination = PaginationParams(offset: offset);
final error = 'Error! Error! Error!';
when(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: pagination,
)).thenThrow(error);
usersBlocState.queryUsers(pagination: pagination);
await expectLater(
usersBlocState.queryUsersLoading,
emitsError(error),
);
verify(mockClient.queryUsers(
filter: anyNamed('filter'),
sort: anyNamed('sort'),
options: anyNamed('options'),
pagination: pagination,
)).called(1);
},
);
}
@@ -1,3 +1,8 @@
## 1.5.1
* Improved test coverage to > 95%
* Minor fixes and improvements
## 1.5.0
* Update llc dependency
@@ -32,7 +32,7 @@ class ChannelQueryDao extends DatabaseAccessor<MoorChatDatabase>
Future<void> updateChannelQueries(
Map<String, dynamic> filter,
List<String> cids, {
bool clearQueryCache,
bool clearQueryCache = false,
}) async =>
transaction(() async {
final hash = _computeHash(filter);
@@ -57,6 +57,14 @@ class ChannelQueryDao extends DatabaseAccessor<MoorChatDatabase>
});
});
///
Future<List<String>> getCachedChannelCids(Map<String, dynamic> filter) {
final hash = _computeHash(filter);
return (select(channelQueries)..where((c) => c.queryHash.equals(hash)))
.map((c) => c.channelCid)
.get();
}
/// Get list of channels by filter, sort and paginationParams
Future<List<ChannelModel>> getChannels({
Map<String, dynamic> filter,
@@ -72,12 +80,7 @@ class ChannelQueryDao extends DatabaseAccessor<MoorChatDatabase>
return true;
}(), '');
final hash = _computeHash(filter);
final cachedChannelCids = await (select(channelQueries)
..where((c) => c.queryHash.equals(hash)))
.map((c) => c.channelCid)
.get();
final cachedChannelCids = await getCachedChannelCids(filter);
final query = select(channels)..where((c) => c.cid.isIn(cachedChannelCids));
final cachedChannels = await (query.join([
@@ -2,14 +2,13 @@ import 'package:moor/moor.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/entity/connection_events.dart';
import 'package:stream_chat_persistence/src/entity/users.dart';
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, Users])
@UseDao(tables: [ConnectionEvents])
class ConnectionEventDao extends DatabaseAccessor<MoorChatDatabase>
with _$ConnectionEventDaoMixin {
/// Creates a new connection event dao instance
@@ -9,5 +9,4 @@ part of 'connection_event_dao.dart';
mixin _$ConnectionEventDaoMixin on DatabaseAccessor<MoorChatDatabase> {
$ConnectionEventsTable get connectionEvents =>
attachedDatabase.connectionEvents;
$UsersTable get users => attachedDatabase.users;
}
@@ -72,7 +72,7 @@ class MessageDao extends DatabaseAccessor<MoorChatDatabase>
/// [Messages.channelCid] with [cid]
Future<List<Message>> getThreadMessages(String cid) async =>
Future.wait(await (select(messages).join([
leftOuterJoin(users, messages.userId.equalsExp(_users.id)),
leftOuterJoin(_users, messages.userId.equalsExp(_users.id)),
leftOuterJoin(_pinnedByUsers,
messages.pinnedByUserId.equalsExp(_pinnedByUsers.id)),
])
@@ -89,18 +89,36 @@ class MessageDao extends DatabaseAccessor<MoorChatDatabase>
PaginationParams options,
}) async {
final msgList = await Future.wait(await (select(messages).join([
innerJoin(_users, messages.userId.equalsExp(_users.id)),
innerJoin(
leftOuterJoin(_users, messages.userId.equalsExp(_users.id)),
leftOuterJoin(
_pinnedByUsers, messages.pinnedByUserId.equalsExp(_pinnedByUsers.id)),
])
..where(isNotNull(messages.parentId))
..where(messages.parentId.equals(parentId))
..orderBy([OrderingTerm.asc(messages.createdAt)]))
.map(_messageFromJoinRow)
.get());
if (options?.lessThan != null && msgList.isNotEmpty) {
final lessThanIndex = msgList.indexWhere((m) => m.id == options.lessThan);
msgList.removeRange(lessThanIndex, msgList.length);
if (msgList.isNotEmpty) {
if (options?.lessThan != null) {
final lessThanIndex = msgList.indexWhere(
(m) => m.id == options.lessThan,
);
if (lessThanIndex != -1) {
msgList.removeRange(lessThanIndex, msgList.length);
}
}
if (options?.greaterThanOrEqual != null) {
final greaterThanIndex = msgList.indexWhere(
(m) => m.id == options.greaterThanOrEqual,
);
if (greaterThanIndex != -1) {
msgList.removeRange(0, greaterThanIndex);
}
}
if (options?.limit != null) {
return msgList.take(options.limit).toList();
}
}
return msgList;
}
@@ -123,24 +141,26 @@ class MessageDao extends DatabaseAccessor<MoorChatDatabase>
.map(_messageFromJoinRow)
.get());
if (messagePagination?.lessThan != null) {
final lessThanIndex = msgList.indexWhere(
(m) => m.id == messagePagination.lessThan,
);
if (lessThanIndex != -1) {
msgList.removeRange(lessThanIndex, msgList.length);
if (msgList.isNotEmpty) {
if (messagePagination?.lessThan != null) {
final lessThanIndex = msgList.indexWhere(
(m) => m.id == messagePagination.lessThan,
);
if (lessThanIndex != -1) {
msgList.removeRange(lessThanIndex, msgList.length);
}
}
}
if (messagePagination?.greaterThanOrEqual != null) {
final greaterThanIndex = msgList.indexWhere(
(m) => m.id == messagePagination.greaterThanOrEqual,
);
if (greaterThanIndex != -1) {
msgList.removeRange(0, greaterThanIndex);
if (messagePagination?.greaterThanOrEqual != null) {
final greaterThanIndex = msgList.indexWhere(
(m) => m.id == messagePagination.greaterThanOrEqual,
);
if (greaterThanIndex != -1) {
msgList.removeRange(0, greaterThanIndex);
}
}
if (messagePagination?.limit != null) {
return msgList.take(messagePagination.limit).toList();
}
}
if (messagePagination?.limit != null) {
return msgList.take(messagePagination.limit).toList();
}
return msgList;
}
@@ -89,18 +89,36 @@ class PinnedMessageDao extends DatabaseAccessor<MoorChatDatabase>
PaginationParams options,
}) async {
final msgList = await Future.wait(await (select(pinnedMessages).join([
innerJoin(_users, pinnedMessages.userId.equalsExp(_users.id)),
innerJoin(_pinnedByUsers,
leftOuterJoin(_users, pinnedMessages.userId.equalsExp(_users.id)),
leftOuterJoin(_pinnedByUsers,
pinnedMessages.pinnedByUserId.equalsExp(_pinnedByUsers.id)),
])
..where(isNotNull(pinnedMessages.parentId))
..where(pinnedMessages.parentId.equals(parentId))
..orderBy([OrderingTerm.asc(pinnedMessages.createdAt)]))
.map(_messageFromJoinRow)
.get());
if (options?.lessThan != null) {
final lessThanIndex = msgList.indexWhere((m) => m.id == options.lessThan);
msgList.removeRange(lessThanIndex, msgList.length);
if (msgList.isNotEmpty) {
if (options?.lessThan != null) {
final lessThanIndex = msgList.indexWhere(
(m) => m.id == options.lessThan,
);
if (lessThanIndex != -1) {
msgList.removeRange(lessThanIndex, msgList.length);
}
}
if (options?.greaterThanOrEqual != null) {
final greaterThanIndex = msgList.indexWhere(
(m) => m.id == options.greaterThanOrEqual,
);
if (greaterThanIndex != -1) {
msgList.removeRange(0, greaterThanIndex);
}
}
if (options?.limit != null) {
return msgList.take(options.limit).toList();
}
}
return msgList;
}
@@ -123,24 +141,26 @@ class PinnedMessageDao extends DatabaseAccessor<MoorChatDatabase>
.map(_messageFromJoinRow)
.get());
if (messagePagination?.lessThan != null) {
final lessThanIndex = msgList.indexWhere(
(m) => m.id == messagePagination.lessThan,
);
if (lessThanIndex != -1) {
msgList.removeRange(lessThanIndex, msgList.length);
if (msgList.isNotEmpty) {
if (messagePagination?.lessThan != null) {
final lessThanIndex = msgList.indexWhere(
(m) => m.id == messagePagination.lessThan,
);
if (lessThanIndex != -1) {
msgList.removeRange(lessThanIndex, msgList.length);
}
}
}
if (messagePagination?.greaterThanOrEqual != null) {
final greaterThanIndex = msgList.indexWhere(
(m) => m.id == messagePagination.greaterThanOrEqual,
);
if (greaterThanIndex != -1) {
msgList.removeRange(0, greaterThanIndex);
if (messagePagination?.greaterThanOrEqual != null) {
final greaterThanIndex = msgList.indexWhere(
(m) => m.id == messagePagination.greaterThanOrEqual,
);
if (greaterThanIndex != -1) {
msgList.removeRange(0, greaterThanIndex);
}
}
if (messagePagination?.limit != null) {
return msgList.take(messagePagination.limit).toList();
}
}
if (messagePagination?.limit != null) {
return msgList.take(messagePagination.limit).toList();
}
return msgList;
}
@@ -20,4 +20,10 @@ class UserDao extends DatabaseAccessor<MoorChatDatabase> with _$UserDaoMixin {
mode: InsertMode.insertOrReplace,
),
);
/// Returns the list of all the users stored in db
Future<List<User>> getUsers() =>
(select(users)..orderBy([(u) => OrderingTerm.desc(u.createdAt)]))
.map((it) => it.toUser())
.get();
}
@@ -1,22 +1,15 @@
import 'package:meta/meta.dart';
import 'package:moor/ffi.dart';
import 'package:moor/moor.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_persistence/src/converter/converter.dart';
import 'package:stream_chat_persistence/src/dao/dao.dart';
import 'package:stream_chat_persistence/src/db/shared/shared_db.dart';
import 'package:stream_chat_persistence/src/entity/entity.dart';
part 'moor_chat_database.g.dart';
export 'shared/shared_db.dart';
LazyDatabase _openConnection(
String userId, {
bool logStatements = false,
bool persistOnDisk = true,
}) =>
LazyDatabase(() async => SharedDB.constructDatabase(
userId,
logStatements: logStatements,
persistOnDisk: persistOnDisk,
));
part 'moor_chat_database.g.dart';
/// A chat database implemented using moor
@UseMoor(tables: [
@@ -43,14 +36,9 @@ LazyDatabase _openConnection(
class MoorChatDatabase extends _$MoorChatDatabase {
/// Creates a new moor chat database instance
MoorChatDatabase(
this._userId, {
logStatements = false,
bool persistOnDisk = true,
}) : super(_openConnection(
_userId,
logStatements: logStatements,
persistOnDisk: persistOnDisk,
));
this._userId,
QueryExecutor executor,
) : super(executor);
/// Instantiate a new database instance
MoorChatDatabase.connect(
@@ -58,6 +46,10 @@ class MoorChatDatabase extends _$MoorChatDatabase {
DatabaseConnection connection,
) : super.connect(connection);
/// Custom constructor used only for testing
@visibleForTesting
MoorChatDatabase.testable(this._userId) : super(VmDatabase.memory());
final String _userId;
/// User id to which the database is connected
@@ -1,3 +1,4 @@
// coverage:ignore-file
import 'dart:io';
import 'dart:isolate';
@@ -14,27 +15,49 @@ import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
/// A Helper class to construct new instances of [MoorChatDatabase] specifically
/// for native platform applications
class SharedDB {
/// Returns a new instance of [VmDatabase] created using [userId]
/// on a regular isolate.
///
/// Generally used with [ConnectionMode.regular].
static Future<VmDatabase> constructDatabase(
/// Returns a new instance of [MoorChatDatabase].
static MoorChatDatabase constructDatabase(
String userId, {
bool logStatements = false,
bool persistOnDisk = true,
}) async {
ConnectionMode connectionMode = ConnectionMode.regular,
}) {
final dbName = 'db_$userId';
if (persistOnDisk) {
if (Platform.isIOS || Platform.isAndroid) {
final dir = await getApplicationDocumentsDirectory();
final path = join(dir.path, '$dbName.sqlite');
final file = File(path);
return VmDatabase(file, logStatements: logStatements);
}
if (Platform.isMacOS || Platform.isLinux) {
final file = File('$dbName.sqlite');
return VmDatabase(file, logStatements: logStatements);
}
if (connectionMode == ConnectionMode.background) {
return MoorChatDatabase.connect(
userId,
DatabaseConnection.delayed(Future(() async {
final isolate = await _createMoorIsolate(
dbName,
logStatements: logStatements,
);
return isolate.connect();
})),
);
}
return MoorChatDatabase(
userId,
LazyDatabase(
() async => _constructDatabase(
dbName,
logStatements: logStatements,
),
),
);
}
static Future<VmDatabase> _constructDatabase(
String dbName, {
bool logStatements = false,
}) async {
if (Platform.isIOS || Platform.isAndroid) {
final dir = await getApplicationDocumentsDirectory();
final path = join(dir.path, '$dbName.sqlite');
final file = File(path);
return VmDatabase(file, logStatements: logStatements);
}
if (Platform.isMacOS || Platform.isLinux) {
final file = File('$dbName.sqlite');
return VmDatabase(file, logStatements: logStatements);
}
return VmDatabase.memory(logStatements: logStatements);
}
@@ -69,27 +92,6 @@ class SharedDB {
return await receivePort.first as MoorIsolate;
}
/// Returns a new instance of [MoorChatDatabase] using the factory constructor
/// [MoorChatDatabase.connect] created on a background isolate.
///
/// Generally used with [ConnectionMode.background].
static MoorChatDatabase constructMoorChatDatabase(
String userId, {
bool logStatements = false,
}) {
final dbName = 'db_$userId';
return MoorChatDatabase.connect(
userId,
DatabaseConnection.delayed(Future(() async {
final isolate = await _createMoorIsolate(
dbName,
logStatements: logStatements,
);
return isolate.connect();
})),
);
}
}
class _IsolateStartRequest {
@@ -1,29 +1,17 @@
import 'package:moor/backends.dart';
// coverage:ignore-file
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/stream_chat_persistence.dart';
/// A Helper class to construct new instances of [MoorChatDatabase]
class SharedDB {
/// Returns a new instance of database.
///
/// Generally used with [ConnectionMode.regular].
static Future<DelegatedDatabase> constructDatabase(
/// Returns a new instance of [MoorChatDatabase].
static MoorChatDatabase constructDatabase(
String userId, {
bool logStatements = false,
bool persistOnDisk = true,
ConnectionMode connectionMode = ConnectionMode.regular,
}) {
throw UnsupportedError(
'No implementation of the constructDatabase api provided');
}
/// Return a new instance of moor chat database.
///
/// Generally used with [ConnectionMode.background].
static MoorChatDatabase constructMoorChatDatabase(
String userId, {
bool logStatements = false,
}) {
throw UnsupportedError(
'No implementation of the constructMoorChatDatabase api provided');
'No implementation of the constructDatabase api provided',
);
}
}
@@ -1,3 +1,4 @@
// coverage:ignore-file
import 'package:moor/moor_web.dart';
import 'package:stream_chat_persistence/src/stream_chat_persistence_client.dart';
@@ -6,27 +7,14 @@ import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
/// A Helper class to construct new instances of [MoorChatDatabase] specifically
/// for Web applications
class SharedDB {
/// Returns a new instance of [WebDatabase] created using [userId].
///
/// Generally used with [ConnectionMode.regular].
static Future<WebDatabase> constructDatabase(
String userId, {
bool logStatements = false,
bool persistOnDisk = true, // ignored on web
}) async {
final dbName = 'db_$userId';
return WebDatabase(dbName, logStatements: logStatements);
}
/// Returns a new instance of [MoorChatDatabase] creating using the
/// default constructor.
///
/// Generally used with [ConnectionMode.background].
static MoorChatDatabase constructMoorChatDatabase(
/// Returns a new instance of [MoorChatDatabase].
static MoorChatDatabase constructDatabase(
String userId, {
bool logStatements = false,
ConnectionMode connectionMode = ConnectionMode.regular, // Ignored on web
}) {
final dbName = 'db_$userId';
return MoorChatDatabase(dbName, logStatements: logStatements);
final queryExecutor = WebDatabase(dbName, logStatements: logStatements);
return MoorChatDatabase(userId, queryExecutor);
}
}
@@ -1,3 +1,4 @@
// coverage:ignore-file
import 'package:moor/moor.dart';
/// Represents a [ChannelQueries] table in [MoorChatDatabase].
@@ -1,3 +1,4 @@
// coverage:ignore-file
import 'package:moor/moor.dart';
import 'package:stream_chat_persistence/src/converter/map_converter.dart';
@@ -1,3 +1,4 @@
// coverage:ignore-file
import 'package:moor/moor.dart';
import 'package:stream_chat_persistence/src/converter/map_converter.dart';
@@ -1,3 +1,4 @@
// coverage:ignore-file
import 'package:moor/moor.dart';
/// Represents a [Members] table in [MoorChatDatabase].
@@ -1,3 +1,4 @@
// coverage:ignore-file
import 'package:moor/moor.dart';
import 'package:stream_chat_persistence/src/converter/list_converter.dart';
import 'package:stream_chat_persistence/src/converter/map_converter.dart';
@@ -1,3 +1,4 @@
// coverage:ignore-file
import 'package:moor/moor.dart';
import 'package:stream_chat_persistence/src/entity/messages.dart';
@@ -1,3 +1,4 @@
// coverage:ignore-file
import 'package:moor/moor.dart';
import 'package:stream_chat_persistence/src/converter/map_converter.dart';
@@ -1,3 +1,4 @@
// coverage:ignore-file
import 'package:moor/moor.dart';
/// Represents a [Reads] table in [MoorChatDatabase].
@@ -1,3 +1,4 @@
// coverage:ignore-file
import 'package:moor/moor.dart';
import 'package:stream_chat_persistence/src/converter/map_converter.dart';
@@ -46,14 +46,14 @@ extension ChannelModelX on ChannelModel {
id: id,
type: type,
cid: cid,
config: config.toJson(),
config: config?.toJson(),
frozen: frozen,
lastMessageAt: lastMessageAt,
createdAt: createdAt,
updatedAt: updatedAt,
deletedAt: deletedAt,
memberCount: memberCount,
createdById: createdBy.id,
createdById: createdBy?.id,
extraData: extraData,
);
}
@@ -4,7 +4,6 @@ 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/shared/shared_db.dart';
/// Various connection modes on which [StreamChatPersistenceClient] can work
enum ConnectionMode {
@@ -15,6 +14,9 @@ enum ConnectionMode {
background,
}
/// Signature for a function which provides instance of [MoorChatDatabase]
typedef DatabaseProvider = MoorChatDatabase Function(String, ConnectionMode);
final _levelEmojiMapper = {
Level.INFO: '',
Level.WARNING: '⚠️',
@@ -36,26 +38,6 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
_logger.onRecord.listen(logHandlerFunction ?? _defaultLogHandler);
}
/// A function that has a parameter of type [LogRecord].
/// This is called on every new log record.
/// By default the client will use the handler returned by
/// [_getDefaultLogHandler].
/// Setting it you can handle the log messages directly instead of have them
/// written to stdout,
/// this is very convenient if you use an error tracking tool or if you want
/// to centralize your logs into one facility.
///
/// ```dart
/// myLogHandlerFunction = (LogRecord record) {
/// // do something with the record (ie. send it to Sentry or Fabric)
/// }
///
/// final client = StreamChatPersistenceClient(
/// logHandlerFunction: myLogHandlerFunction,
/// );
///```
LogHandlerFunction logHandlerFunction;
/// [MoorChatDatabase] instance used by this client.
@visibleForTesting
MoorChatDatabase db;
@@ -84,24 +66,25 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
return ret;
}
MoorChatDatabase _defaultDatabaseProvider(
String userId,
ConnectionMode mode,
) =>
SharedDB.constructDatabase(userId, connectionMode: mode);
@override
Future<void> connect(String userId) async {
Future<void> connect(
String userId, {
DatabaseProvider databaseProvider, // Used only for testing
}) async {
if (db != null) {
throw Exception(
'An instance of StreamChatDatabase is already connected.\n'
'disconnect the previous instance before connecting again.',
);
}
switch (_connectionMode) {
case ConnectionMode.regular:
_logger.info('Connecting on a regular isolate');
db = MoorChatDatabase(userId);
return;
case ConnectionMode.background:
_logger.info('Connecting on background isolate');
db = SharedDB.constructMoorChatDatabase(userId);
return;
}
db = databaseProvider?.call(userId, _connectionMode) ??
_defaultDatabaseProvider(userId, _connectionMode);
}
@override
@@ -259,9 +242,9 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
@override
Future<void> updateChannelQueries(
Map<String, dynamic> filter,
List<String> cids,
bool clearQueryCache,
) =>
List<String> cids, {
bool clearQueryCache = false,
}) =>
_readProtected(() async {
_logger.info('updateChannelQueries');
return db.channelQueryDao.updateChannelQueries(
@@ -0,0 +1,798 @@
{
"_meta": {
"description": "This file contains a serialized version of schema entities for moor.",
"version": "0.1.0-dev-preview"
},
"entities": [
{
"id": 0,
"references": [],
"type": "table",
"data": {
"name": "channels",
"was_declared_in_moor": false,
"columns": [
{
"name": "id",
"moor_type": "ColumnType.text",
"nullable": false,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "type",
"moor_type": "ColumnType.text",
"nullable": false,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "cid",
"moor_type": "ColumnType.text",
"nullable": false,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "config",
"moor_type": "ColumnType.text",
"nullable": false,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": [],
"type_converter": {
"dart_expr": "MapConverter<Object>()",
"dart_type_name": "Map<String, Object>"
}
},
{
"name": "frozen",
"moor_type": "ColumnType.boolean",
"nullable": false,
"customConstraints": null,
"default_dart": "Constant(false)",
"default_client_dart": null,
"dsl_features": []
},
{
"name": "last_message_at",
"moor_type": "ColumnType.datetime",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "created_at",
"moor_type": "ColumnType.datetime",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "updated_at",
"moor_type": "ColumnType.datetime",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "deleted_at",
"moor_type": "ColumnType.datetime",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "member_count",
"moor_type": "ColumnType.integer",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "created_by_id",
"moor_type": "ColumnType.text",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "extra_data",
"moor_type": "ColumnType.text",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": [],
"type_converter": {
"dart_expr": "MapConverter<Object>()",
"dart_type_name": "Map<String, Object>"
}
}
],
"is_virtual": false,
"explicit_pk": [
"cid"
]
}
},
{
"id": 1,
"references": [],
"type": "table",
"data": {
"name": "messages",
"was_declared_in_moor": false,
"columns": [
{
"name": "id",
"moor_type": "ColumnType.text",
"nullable": false,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "message_text",
"moor_type": "ColumnType.text",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "attachments",
"moor_type": "ColumnType.text",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": [],
"type_converter": {
"dart_expr": "ListConverter<String>()",
"dart_type_name": "List<String>"
}
},
{
"name": "status",
"moor_type": "ColumnType.integer",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": [],
"type_converter": {
"dart_expr": "MessageSendingStatusConverter()",
"dart_type_name": "MessageSendingStatus"
}
},
{
"name": "type",
"moor_type": "ColumnType.text",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "mentioned_users",
"moor_type": "ColumnType.text",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": [],
"type_converter": {
"dart_expr": "ListConverter<String>()",
"dart_type_name": "List<String>"
}
},
{
"name": "reaction_counts",
"moor_type": "ColumnType.text",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": [],
"type_converter": {
"dart_expr": "MapConverter<int>()",
"dart_type_name": "Map<String, int>"
}
},
{
"name": "reaction_scores",
"moor_type": "ColumnType.text",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": [],
"type_converter": {
"dart_expr": "MapConverter<int>()",
"dart_type_name": "Map<String, int>"
}
},
{
"name": "parent_id",
"moor_type": "ColumnType.text",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "quoted_message_id",
"moor_type": "ColumnType.text",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "reply_count",
"moor_type": "ColumnType.integer",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "show_in_channel",
"moor_type": "ColumnType.boolean",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "shadowed",
"moor_type": "ColumnType.boolean",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "command",
"moor_type": "ColumnType.text",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "created_at",
"moor_type": "ColumnType.datetime",
"nullable": false,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "updated_at",
"moor_type": "ColumnType.datetime",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "deleted_at",
"moor_type": "ColumnType.datetime",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "user_id",
"moor_type": "ColumnType.text",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "channel_cid",
"moor_type": "ColumnType.text",
"nullable": true,
"customConstraints": "NULLABLE REFERENCES channels(cid) ON DELETE CASCADE",
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "extra_data",
"moor_type": "ColumnType.text",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": [],
"type_converter": {
"dart_expr": "MapConverter<Object>()",
"dart_type_name": "Map<String, Object>"
}
}
],
"is_virtual": false,
"explicit_pk": [
"id"
]
}
},
{
"id": 2,
"references": [],
"type": "table",
"data": {
"name": "reactions",
"was_declared_in_moor": false,
"columns": [
{
"name": "user_id",
"moor_type": "ColumnType.text",
"nullable": false,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "message_id",
"moor_type": "ColumnType.text",
"nullable": false,
"customConstraints": "REFERENCES messages(id) ON DELETE CASCADE",
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "type",
"moor_type": "ColumnType.text",
"nullable": false,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "created_at",
"moor_type": "ColumnType.datetime",
"nullable": false,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "score",
"moor_type": "ColumnType.integer",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "extra_data",
"moor_type": "ColumnType.text",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": [],
"type_converter": {
"dart_expr": "MapConverter<Object>()",
"dart_type_name": "Map<String, Object>"
}
}
],
"is_virtual": false,
"explicit_pk": [
"message_id",
"type",
"user_id"
]
}
},
{
"id": 3,
"references": [],
"type": "table",
"data": {
"name": "users",
"was_declared_in_moor": false,
"columns": [
{
"name": "id",
"moor_type": "ColumnType.text",
"nullable": false,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "role",
"moor_type": "ColumnType.text",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "created_at",
"moor_type": "ColumnType.datetime",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "updated_at",
"moor_type": "ColumnType.datetime",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "last_active",
"moor_type": "ColumnType.datetime",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "online",
"moor_type": "ColumnType.boolean",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "banned",
"moor_type": "ColumnType.boolean",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "extra_data",
"moor_type": "ColumnType.text",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": [],
"type_converter": {
"dart_expr": "MapConverter<Object>()",
"dart_type_name": "Map<String, Object>"
}
}
],
"is_virtual": false,
"explicit_pk": [
"id"
]
}
},
{
"id": 4,
"references": [],
"type": "table",
"data": {
"name": "members",
"was_declared_in_moor": false,
"columns": [
{
"name": "user_id",
"moor_type": "ColumnType.text",
"nullable": false,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "channel_cid",
"moor_type": "ColumnType.text",
"nullable": false,
"customConstraints": "REFERENCES channels(cid) ON DELETE CASCADE",
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "role",
"moor_type": "ColumnType.text",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "invite_accepted_at",
"moor_type": "ColumnType.datetime",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "invite_rejected_at",
"moor_type": "ColumnType.datetime",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "invited",
"moor_type": "ColumnType.boolean",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "banned",
"moor_type": "ColumnType.boolean",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "shadow_banned",
"moor_type": "ColumnType.boolean",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "is_moderator",
"moor_type": "ColumnType.boolean",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "created_at",
"moor_type": "ColumnType.datetime",
"nullable": false,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "updated_at",
"moor_type": "ColumnType.datetime",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
}
],
"is_virtual": false,
"explicit_pk": [
"user_id",
"channel_cid"
]
}
},
{
"id": 5,
"references": [],
"type": "table",
"data": {
"name": "reads",
"was_declared_in_moor": false,
"columns": [
{
"name": "last_read",
"moor_type": "ColumnType.datetime",
"nullable": false,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "user_id",
"moor_type": "ColumnType.text",
"nullable": false,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "channel_cid",
"moor_type": "ColumnType.text",
"nullable": false,
"customConstraints": "REFERENCES channels(cid) ON DELETE CASCADE",
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "unread_messages",
"moor_type": "ColumnType.integer",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
}
],
"is_virtual": false,
"explicit_pk": [
"user_id",
"channel_cid"
]
}
},
{
"id": 6,
"references": [],
"type": "table",
"data": {
"name": "channel_queries",
"was_declared_in_moor": false,
"columns": [
{
"name": "query_hash",
"moor_type": "ColumnType.text",
"nullable": false,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "channel_cid",
"moor_type": "ColumnType.text",
"nullable": false,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
}
],
"is_virtual": false,
"explicit_pk": [
"query_hash",
"channel_cid"
]
}
},
{
"id": 7,
"references": [],
"type": "table",
"data": {
"name": "connection_events",
"was_declared_in_moor": false,
"columns": [
{
"name": "id",
"moor_type": "ColumnType.integer",
"nullable": false,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "own_user",
"moor_type": "ColumnType.text",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": [],
"type_converter": {
"dart_expr": "MapConverter<Object>()",
"dart_type_name": "Map<String, Object>"
}
},
{
"name": "total_unread_count",
"moor_type": "ColumnType.integer",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "unread_channels",
"moor_type": "ColumnType.integer",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "last_event_at",
"moor_type": "ColumnType.datetime",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "last_sync_at",
"moor_type": "ColumnType.datetime",
"nullable": true,
"customConstraints": null,
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
}
],
"is_virtual": false,
"explicit_pk": [
"id"
]
}
}
]
}
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,7 @@
name: stream_chat_persistence
homepage: https://github.com/GetStream/stream-chat-flutter
description: Official Stream Chat Persistence library. Build your own chat experience using Dart and Flutter.
version: 1.5.0
version: 1.5.1
repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -18,10 +18,10 @@ dependencies:
path: ^1.7.0
path_provider: ^1.6.27
sqlite3_flutter_libs: ^0.4.0+1
stream_chat: ^1.5.0
stream_chat: ^1.5.1
dev_dependencies:
build_runner: ^1.11.0
mockito: ^4.1.3
moor_generator: ^3.4.1
pedantic: ^1.9.2
test: ^1.15.7
@@ -0,0 +1,71 @@
import 'package:mockito/mockito.dart';
import 'package:stream_chat_persistence/src/dao/dao.dart';
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
class MockChatDatabase extends Mock implements MoorChatDatabase {
UserDao _userDao;
@override
UserDao get userDao => _userDao ??= MockUserDao();
ChannelDao _channelDao;
@override
ChannelDao get channelDao => _channelDao ??= MockChannelDao();
MessageDao _messageDao;
@override
MessageDao get messageDao => _messageDao ??= MockMessageDao();
PinnedMessageDao _pinnedMessageDao;
@override
PinnedMessageDao get pinnedMessageDao =>
_pinnedMessageDao ??= MockPinnedMessageDao();
MemberDao _memberDao;
@override
MemberDao get memberDao => _memberDao ??= MockMemberDao();
ReactionDao _reactionDao;
@override
ReactionDao get reactionDao => _reactionDao ??= MockReactionDao();
ReadDao _readDao;
@override
ReadDao get readDao => _readDao ??= MockReadDao();
ChannelQueryDao _channelQueryDao;
@override
ChannelQueryDao get channelQueryDao =>
_channelQueryDao ??= MockChannelQueryDao();
ConnectionEventDao _connectionEventDao;
@override
ConnectionEventDao get connectionEventDao =>
_connectionEventDao ??= MockConnectionEventDao();
}
class MockUserDao extends Mock implements UserDao {}
class MockChannelDao extends Mock implements ChannelDao {}
class MockMessageDao extends Mock implements MessageDao {}
class MockPinnedMessageDao extends Mock implements PinnedMessageDao {}
class MockMemberDao extends Mock implements MemberDao {}
class MockReactionDao extends Mock implements ReactionDao {}
class MockReadDao extends Mock implements ReadDao {}
class MockChannelQueryDao extends Mock implements ChannelQueryDao {}
class MockConnectionEventDao extends Mock implements ConnectionEventDao {}
@@ -0,0 +1,53 @@
import 'dart:convert';
import 'package:test/test.dart';
import 'package:stream_chat_persistence/src/converter/list_converter.dart';
void main() {
group('mapToDart', () {
final listConverter = ListConverter<String>();
test('should return null if nothing is provided', () {
final res = listConverter.mapToDart(null);
expect(res, isNull);
});
test('should throw type error if the provided json is not a list', () {
final json = {'test_key': 'testData'};
expect(
() => listConverter.mapToDart(jsonEncode(json)),
throwsA(isA<TypeError>()),
);
});
test('should throw type error if the provided json is not a list of String',
() {
final json = [22, 33, 44];
expect(
() => listConverter.mapToDart(jsonEncode(json)),
throwsA(isA<TypeError>()),
);
});
test('should return list of String if json data list is provided', () {
final data = ['data1', 'data2', 'data3'];
final res = listConverter.mapToDart(jsonEncode(data));
expect(res.length, data.length);
});
});
group('mapToSql', () {
final listConverter = ListConverter<String>();
test('should return null if nothing is provided', () {
final res = listConverter.mapToSql(null);
expect(res, isNull);
});
test('should return json string if data list is provided', () {
final data = ['data1', 'data2', 'data3'];
final res = listConverter.mapToSql(data);
expect(res, jsonEncode(data));
});
});
}
@@ -0,0 +1,64 @@
import 'dart:convert';
import 'package:test/test.dart';
import 'package:stream_chat_persistence/src/converter/map_converter.dart';
void main() {
group('mapToDart', () {
final mapConverter = MapConverter<String>();
test('should return null if nothing is provided', () {
final res = mapConverter.mapToDart(null);
expect(res, isNull);
});
test('should throw type error if the provided json is not a map', () {
const json = ['testData1', 'testData2', 'testData3'];
expect(
() => mapConverter.mapToDart(jsonEncode(json)),
throwsA(isA<TypeError>()),
);
});
test(
'should throw type error if the provided json is not a '
'map of String, String',
() {
const json = {'test_key': 22, 'test_key2': 33, 'test_key3': 44};
expect(
() => mapConverter.mapToDart(jsonEncode(json)),
throwsA(isA<TypeError>()),
);
},
);
test('should return map of String, String if json data is provided', () {
const data = {
'test_key': 'testValue',
'test_key2': 'testValue2',
'test_key3': 'testValue3',
};
final res = mapConverter.mapToDart(jsonEncode(data));
expect(res, data);
});
});
group('mapToSql', () {
final mapConverter = MapConverter<String>();
test('should return null if nothing is provided', () {
final res = mapConverter.mapToSql(null);
expect(res, isNull);
});
test('should return json string if data map is provided', () {
const data = {
'test_key': 'testValue',
'test_key2': 'testValue2',
'test_key3': 'testValue3',
};
final res = mapConverter.mapToSql(data);
expect(res, jsonEncode(data));
});
});
}
@@ -0,0 +1,33 @@
import 'package:test/test.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_persistence/src/converter/message_sending_status_converter.dart';
void main() {
group('mapToDart', () {
final statusConverter = MessageSendingStatusConverter();
test('should return null if nothing is provided', () {
final res = statusConverter.mapToDart(null);
expect(res, isNull);
});
test('should return expected status if status code is provided', () {
final res = statusConverter.mapToDart(3);
expect(res, MessageSendingStatus.updating);
});
});
group('mapToSql', () {
final statusConverter = MessageSendingStatusConverter();
test('should return null if nothing is provided', () {
final res = statusConverter.mapToSql(null);
expect(res, isNull);
});
test('should return expected code if the status is provided', () {
final res = statusConverter.mapToSql(MessageSendingStatus.updating);
expect(res, 3);
});
});
}
@@ -0,0 +1,130 @@
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:test/test.dart';
void main() {
ChannelDao channelDao;
MoorChatDatabase database;
setUp(() {
database = MoorChatDatabase.testable('testUserId');
channelDao = database.channelDao;
});
test('getChannelByCid', () async {
const id = 'testId';
const cid = 'testCid';
const type = 'testType';
// Should be null initially
final channel = await channelDao.getChannelByCid(cid);
expect(channel, isNull);
// Saving a dummy channel
final dummyChannel = ChannelModel(
id: id,
type: type,
cid: cid,
config: ChannelConfig(),
);
await channelDao.updateChannels([dummyChannel]);
// Should match the dummy channel
final updatedChannel = await channelDao.getChannelByCid(cid);
expect(updatedChannel.id, id);
expect(updatedChannel.cid, cid);
expect(updatedChannel.type, type);
});
test('deleteChannelByCids', () async {
const id = 'testId';
const cid = 'testCid';
const type = 'testType';
// Saving a dummy channel
final dummyChannel = ChannelModel(
id: id,
type: type,
cid: cid,
config: ChannelConfig(),
);
await channelDao.updateChannels([dummyChannel]);
// Should match the dummy channel
final updatedChannel = await channelDao.getChannelByCid(cid);
expect(updatedChannel.id, id);
expect(updatedChannel.cid, cid);
expect(updatedChannel.type, type);
// Deleting the dummyChannel using cid
await channelDao.deleteChannelByCids([cid]);
// Fetched channel Should be null
final channel = await channelDao.getChannelByCid(cid);
expect(channel, isNull);
});
test('cids', () async {
// Should be empty initially
final cids = await channelDao.cids;
expect(cids, []);
const id = 'testId';
const cid = 'testCid';
const type = 'testType';
// Saving a dummy channel
final dummyChannel = ChannelModel(
id: id,
type: type,
cid: cid,
config: ChannelConfig(),
);
await channelDao.updateChannels([dummyChannel]);
// Should return the cid of the dummy channel
final updatedCids = await channelDao.cids;
expect(updatedCids, [cid]);
});
test('updateChannels', () async {
const id = 'testId';
const cid = 'testCid';
const type = 'testType';
// Should be null initially
final channel = await channelDao.getChannelByCid(cid);
expect(channel, isNull);
// Saving a dummy channel
final dummyChannel = ChannelModel(
id: id,
type: type,
cid: cid,
config: ChannelConfig(),
);
await channelDao.updateChannels([dummyChannel]);
// Should match the dummy channel
final updatedChannel = await channelDao.getChannelByCid(cid);
expect(updatedChannel.id, id);
expect(updatedChannel.cid, cid);
expect(updatedChannel.type, type);
// Updating the previously saved channel
const newType = 'newTestType';
final newChannel = dummyChannel.copyWith(type: newType);
await channelDao.updateChannels([newChannel]);
// Should match the new channel
final newUpdatedChannel = await channelDao.getChannelByCid(cid);
expect(newUpdatedChannel.id, id);
expect(newUpdatedChannel.cid, cid);
expect(newUpdatedChannel.type, newType);
});
tearDown(() async {
await database.disconnect();
});
}
@@ -0,0 +1,273 @@
import 'dart:math' as math;
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:test/test.dart';
import '../utils/date_matcher.dart';
void main() {
MoorChatDatabase database;
ChannelQueryDao channelQueryDao;
setUp(() {
database = MoorChatDatabase.testable('testUserId');
channelQueryDao = database.channelQueryDao;
});
test('updateChannelQueries', () async {
const filter = {
'members': {
r'$in': ['testUserId'],
},
};
const cids = ['testCid1', 'testCid2', 'testCid3'];
final cachedCids = await channelQueryDao.getCachedChannelCids(filter);
expect(cachedCids, isEmpty);
// Updating channel queries
await channelQueryDao.updateChannelQueries(filter, cids);
final updatedCids = await channelQueryDao.getCachedChannelCids(filter);
expect(updatedCids, cids);
});
test('clear queryCache before updateChannelQueries', () async {
const filter = {
'members': {
r'$in': ['testUserId'],
},
};
const cids = ['testCid1', 'testCid2', 'testCid3'];
final cachedCids = await channelQueryDao.getCachedChannelCids(filter);
expect(cachedCids, isEmpty);
// Updating channel queries
await channelQueryDao.updateChannelQueries(
filter,
cids,
clearQueryCache: true,
);
final updatedCids = await channelQueryDao.getCachedChannelCids(filter);
expect(updatedCids, cids);
});
test('getCachedChannelCids', () async {
const filter = {
'members': {
r'$in': ['testUserId'],
},
};
const cids = ['testCid1', 'testCid2', 'testCid3'];
final cachedCids = await channelQueryDao.getCachedChannelCids(filter);
expect(cachedCids, isEmpty);
// Updating channel queries
await channelQueryDao.updateChannelQueries(filter, cids);
final updatedCids = await channelQueryDao.getCachedChannelCids(filter);
expect(updatedCids, cids);
});
Future<List<ChannelModel>> _insertTestDataForGetChannel(
Map<String, Object> filter, {
int count = 3,
}) async {
final now = DateTime.now();
final userDao = database.userDao;
final channelDao = database.channelDao;
final cids = List.generate(count, (index) => 'testCid$index');
final users = List.generate(count, (index) => User(id: 'testId$index'));
final channels = List.generate(
count,
(index) => ChannelModel(
id: 'testId$index',
type: 'testType$index',
cid: cids[index],
createdBy: users[index],
config: ChannelConfig(),
extraData: {'test_custom_field': math.Random().nextInt(100)},
createdAt: now,
memberCount: math.Random().nextInt(100),
lastMessageAt: now.add(Duration(hours: index)),
),
).reversed.toList(growable: false);
await userDao.updateUsers(users);
await channelDao.updateChannels(channels);
await channelQueryDao.updateChannelQueries(filter, cids);
return channels;
}
group('getChannels', () {
const filter = {
'members': {
r'$in': ['testUserId'],
},
};
test('should return empty list of channels', () async {
final channels = await channelQueryDao.getChannels(filter: filter);
expect(channels, isEmpty);
});
test('should return all the inserted channels', () async {
// Inserting test data for get channels
final insertedChannels = await _insertTestDataForGetChannel(filter);
// Should match with the inserted channels
final updatedChannels = await channelQueryDao.getChannels(filter: filter);
expect(updatedChannels.length, insertedChannels.length);
for (var i = 0; i < updatedChannels.length; i++) {
final updatedChannel = updatedChannels[i];
final insertedChannel = insertedChannels[i];
// Should match all the basic details
expect(updatedChannel.id, insertedChannel.id);
expect(updatedChannel.type, insertedChannel.type);
expect(updatedChannel.cid, insertedChannel.cid);
expect(updatedChannel.memberCount, insertedChannel.memberCount);
// Should match createdAt date
expect(
updatedChannel.createdAt,
isSameDateAs(insertedChannel.createdAt),
);
// Should match lastMessageAt date
expect(
updatedChannel.lastMessageAt,
isSameDateAs(insertedChannel.lastMessageAt),
);
}
});
test(
'should return all the inserted channels along with pagination applied',
() async {
const offset = 5;
const limit = 15;
const pagination = PaginationParams(offset: offset, limit: limit);
// Inserting test data for get channels
final insertedChannels = await _insertTestDataForGetChannel(
filter,
count: 30,
);
// Should match with the inserted channels
final updatedChannels = await channelQueryDao.getChannels(
filter: filter,
paginationParams: pagination,
);
expect(updatedChannels.length, limit);
expect(updatedChannels.first.id, 'testId24');
expect(updatedChannels.first.cid, 'testCid24');
},
);
test('should return sorted channels using member count', () async {
int sortComparator(ChannelModel a, ChannelModel b) =>
b.memberCount.compareTo(a.memberCount);
// Inserting test data for get channels
final insertedChannels = await _insertTestDataForGetChannel(filter);
insertedChannels.sort(sortComparator);
// Should match with the inserted channels
final updatedChannels = await channelQueryDao.getChannels(
filter: filter,
sort: [SortOption('member_count', comparator: sortComparator)],
);
expect(updatedChannels.length, insertedChannels.length);
for (var i = 0; i < updatedChannels.length; i++) {
final updatedChannel = updatedChannels[i];
final insertedChannel = insertedChannels[i];
// Should match all the basic details
expect(updatedChannel.id, insertedChannel.id);
expect(updatedChannel.type, insertedChannel.type);
expect(updatedChannel.cid, insertedChannel.cid);
expect(updatedChannel.memberCount, insertedChannel.memberCount);
// Should match createdAt date
expect(
updatedChannel.createdAt,
isSameDateAs(insertedChannel.createdAt),
);
// Should match lastMessageAt date
expect(
updatedChannel.lastMessageAt,
isSameDateAs(insertedChannel.lastMessageAt),
);
}
});
test('should throw if comparator is not provided in sort list', () {
expect(
() => channelQueryDao.getChannels(
sort: [const SortOption('test_custom_field')],
),
throwsArgumentError,
);
});
test('should return sorted channels using custom field', () async {
int sortComparator(ChannelModel a, ChannelModel b) {
final aData = a.extraData['test_custom_field'] as int;
final bData = b.extraData['test_custom_field'] as int;
return bData.compareTo(aData);
}
// Inserting test data for get channels
final insertedChannels = await _insertTestDataForGetChannel(filter);
insertedChannels.sort(sortComparator);
// Should match with the inserted channels
final updatedChannels = await channelQueryDao.getChannels(
filter: filter,
sort: [SortOption('test_custom_field', comparator: sortComparator)],
);
expect(updatedChannels.length, insertedChannels.length);
for (var i = 0; i < updatedChannels.length; i++) {
final updatedChannel = updatedChannels[i];
final insertedChannel = insertedChannels[i];
// Should match all the basic details
expect(updatedChannel.id, insertedChannel.id);
expect(updatedChannel.type, insertedChannel.type);
expect(updatedChannel.cid, insertedChannel.cid);
expect(updatedChannel.memberCount, insertedChannel.memberCount);
// Should match createdAt date
expect(
updatedChannel.createdAt,
isSameDateAs(insertedChannel.createdAt),
);
// Should match lastMessageAt date
expect(
updatedChannel.lastMessageAt,
isSameDateAs(insertedChannel.lastMessageAt),
);
}
});
});
tearDown(() async {
await database.disconnect();
});
}

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