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:
co-authored by
Sahil Kumar
Neevash Ramdial
Deven Joshi
parent
aeb6da8d38
commit
87ca818a9a
Executable
+6
@@ -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$'
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
## 1.5.1
|
||||
|
||||
- Minor fixes and improvements
|
||||
|
||||
## 1.5.0
|
||||
|
||||
- Minor fixes and improvements
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
+33
@@ -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
Reference in New Issue
Block a user