From 9e200313202dfd7fdcc5414f451073562ef4f269 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Fri, 21 May 2021 14:18:21 +0530 Subject: [PATCH 001/150] fix: Channel list builder perf fix --- .../stream_chat_flutter/lib/src/channel_list_view.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/channel_list_view.dart b/packages/stream_chat_flutter/lib/src/channel_list_view.dart index 766fe4ba..44962dd4 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -450,12 +450,13 @@ class _ChannelListViewState extends State { Widget _listItemBuilder(BuildContext context, int i, List channels) { final channelsBloc = ChannelsBloc.of(context); + final onTap = _getChannelTap(context); + final chatThemeData = StreamChatTheme.of(context); + final backgroundColor = chatThemeData.colorTheme.whiteSmoke; + if (i < channels.length) { final channel = channels[i]; - final onTap = _getChannelTap(context); - final chatThemeData = StreamChatTheme.of(context); - final backgroundColor = chatThemeData.colorTheme.whiteSmoke; return StreamChannel( key: ValueKey('CHANNEL-${channel.id}'), channel: channel, From 847e5120af9d4b1ef1c80c6cc6655d0f8085dfff Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 21 May 2021 15:04:25 +0200 Subject: [PATCH 002/150] minor fixes --- .../lib/src/message_list_view.dart | 1 + .../lib/src/message_widget.dart | 337 +++++++++--------- 2 files changed, 166 insertions(+), 172 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index e07d9358..a17dccf0 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -432,6 +432,7 @@ class _MessageListViewState extends State { physics: widget.scrollPhysics, itemScrollController: _scrollController, reverse: true, + addAutomaticKeepAlives: false, itemCount: messages.length + 2 + (_isThreadConversation ? 1 : 0), separatorBuilder: (context, i) { diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index 97e061ae..b18a0532 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -449,189 +449,182 @@ class _MessageWidgetState extends State final leftPadding = widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 0.5; - return Material( - type: MaterialType.transparency, - child: Portal( - child: InkWell( - onTap: () { - widget.onMessageTap!(widget.message); - }, - onLongPress: widget.message.isDeleted && !isFailedState - ? null - : () => onLongPress(context), - child: Padding( - padding: widget.padding ?? const EdgeInsets.all(8), - child: FractionallySizedBox( - alignment: - widget.reverse ? Alignment.centerRight : Alignment.centerLeft, - widthFactor: 0.78, - child: Column( - crossAxisAlignment: widget.reverse - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Stack( - clipBehavior: Clip.none, - alignment: widget.reverse - ? AlignmentDirectional.bottomEnd - : AlignmentDirectional.bottomStart, - children: [ - Column( - crossAxisAlignment: widget.reverse - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - if (widget.showUserAvatar == DisplayWidget.show && - widget.message.user != null) ...[ - _buildUserAvatar(), - const SizedBox(width: 4), - ], - if (widget.showUserAvatar == DisplayWidget.hide) - SizedBox(width: avatarWidth + 4), - Flexible( - child: PortalEntry( - portal: Container( - transform: Matrix4.translationValues( - widget.reverse ? 12 : -12, 0, 0), - constraints: const BoxConstraints( - maxWidth: 22 * 6.0), - child: _buildReactionIndicator(context), - ), - portalAnchor: - Alignment(widget.reverse ? 1 : -1, -1), - childAnchor: - Alignment(widget.reverse ? -1 : 1, -1), - child: Stack( - clipBehavior: Clip.none, - children: [ - Padding( - padding: widget.showReactions - ? EdgeInsets.only( - top: widget - .message - .reactionCounts - ?.isNotEmpty == - true - ? 18 - : 0, - ) - : EdgeInsets.zero, - child: (widget.message.isDeleted && - !isFailedState) - ? Container( - // ignore: lines_longer_than_80_chars - margin: EdgeInsets.symmetric( - horizontal: - // ignore: lines_longer_than_80_chars - widget.showUserAvatar == - // ignore: lines_longer_than_80_chars - DisplayWidget - .gone - ? 0 - : 4.0), - child: DeletedMessage( - borderRadiusGeometry: widget - .borderRadiusGeometry, - borderSide: widget.borderSide, - shape: widget.shape, - messageTheme: - widget.messageTheme, - ), - ) - : Card( - clipBehavior: Clip.antiAlias, - elevation: 0, - margin: EdgeInsets.symmetric( - horizontal: (isFailedState - ? 15.0 - : 0.0) + + return Portal( + child: GestureDetector( + onTap: () { + widget.onMessageTap!(widget.message); + }, + onLongPress: widget.message.isDeleted && !isFailedState + ? null + : () => onLongPress(context), + child: Padding( + padding: widget.padding ?? const EdgeInsets.all(8), + child: FractionallySizedBox( + alignment: + widget.reverse ? Alignment.centerRight : Alignment.centerLeft, + widthFactor: 0.78, + child: Column( + crossAxisAlignment: widget.reverse + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Stack( + clipBehavior: Clip.none, + alignment: widget.reverse + ? AlignmentDirectional.bottomEnd + : AlignmentDirectional.bottomStart, + children: [ + Column( + crossAxisAlignment: widget.reverse + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.showUserAvatar == DisplayWidget.show && + widget.message.user != null) ...[ + _buildUserAvatar(), + const SizedBox(width: 4), + ], + if (widget.showUserAvatar == DisplayWidget.hide) + SizedBox(width: avatarWidth + 4), + Flexible( + child: PortalEntry( + portal: Container( + transform: Matrix4.translationValues( + widget.reverse ? 12 : -12, 0, 0), + constraints: + const BoxConstraints(maxWidth: 22 * 6.0), + child: _buildReactionIndicator(context), + ), + portalAnchor: + Alignment(widget.reverse ? 1 : -1, -1), + childAnchor: + Alignment(widget.reverse ? -1 : 1, -1), + child: Stack( + clipBehavior: Clip.none, + children: [ + Padding( + padding: widget.showReactions + ? EdgeInsets.only( + top: widget.message.reactionCounts + ?.isNotEmpty == + true + ? 18 + : 0, + ) + : EdgeInsets.zero, + child: (widget.message.isDeleted && + !isFailedState) + ? Container( + // ignore: lines_longer_than_80_chars + margin: EdgeInsets.symmetric( + horizontal: // ignore: lines_longer_than_80_chars - (widget.showUserAvatar == + widget.showUserAvatar == + // ignore: lines_longer_than_80_chars DisplayWidget.gone ? 0 : 4.0), - ), - shape: widget.shape ?? - RoundedRectangleBorder( - side: widget.borderSide ?? - BorderSide( - color: widget - // ignore: lines_longer_than_80_chars - .messageTheme - // ignore: lines_longer_than_80_chars - .messageBorderColor ?? - Colors.grey, - ), - borderRadius: widget - // ignore: lines_longer_than_80_chars - .borderRadiusGeometry ?? - BorderRadius.zero, - ), - color: _getBackgroundColor(), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.end, - mainAxisSize: - MainAxisSize.min, - children: [ - if (hasQuotedMessage) - _buildQuotedMessage(), - if (hasNonUrlAttachments) - _parseAttachments(), - if (!isGiphy) - _buildTextBubble(), - ], - ), + child: DeletedMessage( + borderRadiusGeometry: + widget.borderRadiusGeometry, + borderSide: widget.borderSide, + shape: widget.shape, + messageTheme: + widget.messageTheme, + ), + ) + : Card( + clipBehavior: Clip.antiAlias, + elevation: 0, + margin: EdgeInsets.symmetric( + horizontal: (isFailedState + ? 15.0 + : 0.0) + + // ignore: lines_longer_than_80_chars + (widget.showUserAvatar == + DisplayWidget.gone + ? 0 + : 4.0), + ), + shape: widget.shape ?? + RoundedRectangleBorder( + side: widget.borderSide ?? + BorderSide( + color: widget + // ignore: lines_longer_than_80_chars + .messageTheme + // ignore: lines_longer_than_80_chars + .messageBorderColor ?? + Colors.grey, + ), + borderRadius: widget + // ignore: lines_longer_than_80_chars + .borderRadiusGeometry ?? + BorderRadius.zero, + ), + color: _getBackgroundColor(), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + if (hasQuotedMessage) + _buildQuotedMessage(), + if (hasNonUrlAttachments) + _parseAttachments(), + if (!isGiphy) + _buildTextBubble(), + ], ), - ), - if (widget.showReactionPickerIndicator) - Positioned( - right: widget.reverse ? null : 4, - left: widget.reverse ? 4 : null, - top: -8, - child: CustomPaint( - painter: ReactionBubblePainter( - StreamChatTheme.of(context) - .colorTheme - .white, - Colors.transparent, - Colors.transparent, - tailCirclesSpace: 1, ), + ), + if (widget.showReactionPickerIndicator) + Positioned( + right: widget.reverse ? null : 4, + left: widget.reverse ? 4 : null, + top: -8, + child: CustomPaint( + painter: ReactionBubblePainter( + StreamChatTheme.of(context) + .colorTheme + .white, + Colors.transparent, + Colors.transparent, + tailCirclesSpace: 1, ), ), - ], - ), + ), + ], ), ), - ], - ), - if (showBottomRow) - SizedBox(height: context.textScaleFactor * 18.0), - ], + ), + ], + ), + if (showBottomRow) + SizedBox(height: context.textScaleFactor * 18.0), + ], + ), + if (showBottomRow) + Padding( + padding: EdgeInsets.only(left: leftPadding), + child: _bottomRow, ), - if (showBottomRow) - Padding( - padding: EdgeInsets.only(left: leftPadding), - child: _bottomRow, - ), - if (isFailedState) - Positioned( - left: widget.reverse ? 0 : null, - right: widget.reverse ? null : 0, - bottom: showBottomRow ? 18 : -2, - child: StreamSvgIcon.error(size: 20), - ), - ], - ), - ], - ), + if (isFailedState) + Positioned( + left: widget.reverse ? 0 : null, + right: widget.reverse ? null : 0, + bottom: showBottomRow ? 18 : -2, + child: StreamSvgIcon.error(size: 20), + ), + ], + ), + ], ), ), ), From 839ced618bf8c0478d8e2c31ebfadad55a1d44c6 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 27 May 2021 19:59:34 +0530 Subject: [PATCH 003/150] major refactoring Signed-off-by: Sahil Kumar --- packages/stream_chat/lib/src/api/channel.dart | 306 +-- .../stream_chat/lib/src/api/retry_queue.dart | 2 +- .../lib/src/api/web_socket_channel_html.dart | 7 - .../lib/src/api/web_socket_channel_io.dart | 7 - .../lib/src/api/web_socket_channel_stub.dart | 9 - .../stream_chat/lib/src/api/websocket.dart | 321 --- packages/stream_chat/lib/src/client.dart | 1761 ++++++++--------- .../api}/attachment_file_uploader.dart | 16 +- .../lib/src/core/api/channel_api.dart | 293 +++ .../lib/src/core/api/device_api.dart | 59 + .../lib/src/core/api/general_api.dart | 94 + .../lib/src/core/api/guest_api.dart | 20 + .../lib/src/core/api/message_api.dart | 160 ++ .../lib/src/core/api/moderation_api.dart | 128 ++ .../lib/src/{ => core}/api/requests.dart | 43 +- .../lib/src/{ => core}/api/requests.g.dart | 18 + .../lib/src/{ => core}/api/responses.dart | 48 +- .../lib/src/{ => core}/api/responses.g.dart | 9 + .../lib/src/core/api/stream_chat_api.dart | 79 + .../lib/src/core/api/user_api.dart | 49 + .../src/core/http/connection_id_manager.dart | 27 + .../http/interceptor/auth_interceptor.dart | 91 + .../connection_id_interceptor.dart | 25 + .../http/interceptor/logging_interceptor.dart | 338 ++++ .../src/core/http/stream_chat_dio_error.dart | 21 + .../lib/src/core/http/stream_http_client.dart | 241 +++ .../core/http/stream_http_client_options.dart | 69 + .../stream_chat/lib/src/core/http/token.dart | 85 + .../lib/src/core/http/token_manager.dart | 80 + .../lib/src/{ => core}/models/action.dart | 0 .../lib/src/{ => core}/models/action.g.dart | 0 .../lib/src/{ => core}/models/attachment.dart | 6 +- .../src/{ => core}/models/attachment.g.dart | 0 .../{ => core}/models/attachment_file.dart | 0 .../models/attachment_file.freezed.dart | 0 .../{ => core}/models/attachment_file.g.dart | 0 .../src/{ => core}/models/channel_config.dart | 2 +- .../{ => core}/models/channel_config.g.dart | 0 .../src/{ => core}/models/channel_model.dart | 6 +- .../{ => core}/models/channel_model.g.dart | 0 .../src/{ => core}/models/channel_state.dart | 10 +- .../{ => core}/models/channel_state.g.dart | 0 .../lib/src/{ => core}/models/command.dart | 0 .../lib/src/{ => core}/models/command.g.dart | 0 .../lib/src/{ => core}/models/device.dart | 0 .../lib/src/{ => core}/models/device.g.dart | 0 .../lib/src/{ => core}/models/event.dart | 18 +- .../lib/src/{ => core}/models/event.g.dart | 4 +- .../lib/src/{ => core}/models/filter.dart | 0 .../lib/src/{ => core}/models/member.dart | 2 +- .../lib/src/{ => core}/models/member.g.dart | 0 .../lib/src/{ => core}/models/message.dart | 8 +- .../lib/src/{ => core}/models/message.g.dart | 0 .../lib/src/{ => core}/models/mute.dart | 6 +- .../lib/src/{ => core}/models/mute.g.dart | 0 .../lib/src/{ => core}/models/own_user.dart | 20 +- .../lib/src/{ => core}/models/own_user.g.dart | 0 .../lib/src/{ => core}/models/reaction.dart | 4 +- .../lib/src/{ => core}/models/reaction.g.dart | 0 .../lib/src/{ => core}/models/read.dart | 2 +- .../lib/src/{ => core}/models/read.g.dart | 0 .../src/{ => core}/models/serialization.dart | 2 +- .../lib/src/{ => core}/models/user.dart | 2 +- .../lib/src/{ => core}/models/user.g.dart | 0 packages/stream_chat/lib/src/core/utils.dart | 16 + .../lib/src/db/chat_persistence_client.dart | 20 +- .../lib/src/errors/chat_error_code.dart | 91 + .../lib/src/errors/stream_chat_error.dart | 77 + packages/stream_chat/lib/src/event_type.dart | 3 + .../src/{api => ws}/connection_status.dart | 0 .../stream_chat/lib/src/ws/socket_error.dart | 25 + .../stream_chat/lib/src/ws/timer_helper.dart | 51 + .../stream_chat/lib/src/ws/websocket.dart | 395 ++++ packages/stream_chat/lib/stream_chat.dart | 45 +- packages/stream_chat/pubspec.yaml | 1 + .../test/src/api/channel_test.dart | 10 +- .../test/src/api/responses_test.dart | 12 +- .../test/src/api/web_socket_stub_test.dart | 2 +- .../test/src/api/websocket_test.dart | 24 +- .../stream_chat/test/src/client_test.dart | 22 +- .../test/src/models/filter_test.dart | 2 +- packages/stream_chat/test/version_test.dart | 43 +- .../stream_chat_flutter/example/pubspec.yaml | 4 + .../lib/src/channel_info.dart | 7 +- .../lib/src/channel_list_header.dart | 7 +- .../lib/src/channel_list_view.dart | 42 +- .../lib/src/user_list_view.dart | 13 +- packages/stream_chat_flutter/pubspec.yaml | 6 + .../lib/src/channel_list_core.dart | 45 +- .../lib/src/channels_bloc.dart | 14 +- .../lib/src/stream_chat_core.dart | 6 +- .../lib/src/user_list_core.dart | 21 +- .../lib/src/users_bloc.dart | 4 +- .../stream_chat_flutter_core/pubspec.yaml | 4 + .../test/channel_list_core_test.dart | 84 +- .../test/channels_bloc_test.dart | 145 +- .../test/stream_chat_core_test.dart | 22 +- .../test/user_list_core_test.dart | 32 +- .../test/users_bloc_test.dart | 24 +- .../lib/src/dao/connection_event_dao.dart | 3 +- .../lib/src/db/moor_chat_database.g.dart | 356 ++-- .../lib/src/entity/connection_events.dart | 3 + .../lib/src/mapper/event_mapper.dart | 2 + packages/stream_chat_persistence/pubspec.yaml | 4 + .../test/src/mapper/event_mapper_test.dart | 11 +- .../stream_chat_persistence_client_test.dart | 4 +- 106 files changed, 4263 insertions(+), 1937 deletions(-) delete mode 100644 packages/stream_chat/lib/src/api/web_socket_channel_html.dart delete mode 100644 packages/stream_chat/lib/src/api/web_socket_channel_io.dart delete mode 100644 packages/stream_chat/lib/src/api/web_socket_channel_stub.dart delete mode 100644 packages/stream_chat/lib/src/api/websocket.dart rename packages/stream_chat/lib/src/{ => core/api}/attachment_file_uploader.dart (90%) create mode 100644 packages/stream_chat/lib/src/core/api/channel_api.dart create mode 100644 packages/stream_chat/lib/src/core/api/device_api.dart create mode 100644 packages/stream_chat/lib/src/core/api/general_api.dart create mode 100644 packages/stream_chat/lib/src/core/api/guest_api.dart create mode 100644 packages/stream_chat/lib/src/core/api/message_api.dart create mode 100644 packages/stream_chat/lib/src/core/api/moderation_api.dart rename packages/stream_chat/lib/src/{ => core}/api/requests.dart (77%) rename packages/stream_chat/lib/src/{ => core}/api/requests.g.dart (63%) rename packages/stream_chat/lib/src/{ => core}/api/responses.dart (90%) rename packages/stream_chat/lib/src/{ => core}/api/responses.g.dart (97%) create mode 100644 packages/stream_chat/lib/src/core/api/stream_chat_api.dart create mode 100644 packages/stream_chat/lib/src/core/api/user_api.dart create mode 100644 packages/stream_chat/lib/src/core/http/connection_id_manager.dart create mode 100644 packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart create mode 100644 packages/stream_chat/lib/src/core/http/interceptor/connection_id_interceptor.dart create mode 100644 packages/stream_chat/lib/src/core/http/interceptor/logging_interceptor.dart create mode 100644 packages/stream_chat/lib/src/core/http/stream_chat_dio_error.dart create mode 100644 packages/stream_chat/lib/src/core/http/stream_http_client.dart create mode 100644 packages/stream_chat/lib/src/core/http/stream_http_client_options.dart create mode 100644 packages/stream_chat/lib/src/core/http/token.dart create mode 100644 packages/stream_chat/lib/src/core/http/token_manager.dart rename packages/stream_chat/lib/src/{ => core}/models/action.dart (100%) rename packages/stream_chat/lib/src/{ => core}/models/action.g.dart (100%) rename packages/stream_chat/lib/src/{ => core}/models/attachment.dart (97%) rename packages/stream_chat/lib/src/{ => core}/models/attachment.g.dart (100%) rename packages/stream_chat/lib/src/{ => core}/models/attachment_file.dart (100%) rename packages/stream_chat/lib/src/{ => core}/models/attachment_file.freezed.dart (100%) rename packages/stream_chat/lib/src/{ => core}/models/attachment_file.g.dart (100%) rename packages/stream_chat/lib/src/{ => core}/models/channel_config.dart (97%) rename packages/stream_chat/lib/src/{ => core}/models/channel_config.g.dart (100%) rename packages/stream_chat/lib/src/{ => core}/models/channel_model.dart (96%) rename packages/stream_chat/lib/src/{ => core}/models/channel_model.g.dart (100%) rename packages/stream_chat/lib/src/{ => core}/models/channel_state.dart (87%) rename packages/stream_chat/lib/src/{ => core}/models/channel_state.g.dart (100%) rename packages/stream_chat/lib/src/{ => core}/models/command.dart (100%) rename packages/stream_chat/lib/src/{ => core}/models/command.g.dart (100%) rename packages/stream_chat/lib/src/{ => core}/models/device.dart (100%) rename packages/stream_chat/lib/src/{ => core}/models/device.g.dart (100%) rename packages/stream_chat/lib/src/{ => core}/models/event.dart (93%) rename packages/stream_chat/lib/src/{ => core}/models/event.g.dart (97%) rename packages/stream_chat/lib/src/{ => core}/models/filter.dart (100%) rename packages/stream_chat/lib/src/{ => core}/models/member.dart (98%) rename packages/stream_chat/lib/src/{ => core}/models/member.g.dart (100%) rename packages/stream_chat/lib/src/{ => core}/models/message.dart (98%) rename packages/stream_chat/lib/src/{ => core}/models/message.g.dart (100%) rename packages/stream_chat/lib/src/{ => core}/models/mute.dart (85%) rename packages/stream_chat/lib/src/{ => core}/models/mute.g.dart (100%) rename packages/stream_chat/lib/src/{ => core}/models/own_user.dart (79%) rename packages/stream_chat/lib/src/{ => core}/models/own_user.g.dart (100%) rename packages/stream_chat/lib/src/{ => core}/models/reaction.dart (95%) rename packages/stream_chat/lib/src/{ => core}/models/reaction.g.dart (100%) rename packages/stream_chat/lib/src/{ => core}/models/read.dart (94%) rename packages/stream_chat/lib/src/{ => core}/models/read.g.dart (100%) rename packages/stream_chat/lib/src/{ => core}/models/serialization.dart (96%) rename packages/stream_chat/lib/src/{ => core}/models/user.dart (98%) rename packages/stream_chat/lib/src/{ => core}/models/user.g.dart (100%) create mode 100644 packages/stream_chat/lib/src/core/utils.dart create mode 100644 packages/stream_chat/lib/src/errors/chat_error_code.dart create mode 100644 packages/stream_chat/lib/src/errors/stream_chat_error.dart rename packages/stream_chat/lib/src/{api => ws}/connection_status.dart (100%) create mode 100644 packages/stream_chat/lib/src/ws/socket_error.dart create mode 100644 packages/stream_chat/lib/src/ws/timer_helper.dart create mode 100644 packages/stream_chat/lib/src/ws/websocket.dart diff --git a/packages/stream_chat/lib/src/api/channel.dart b/packages/stream_chat/lib/src/api/channel.dart index cb3cfe37..abdae6ce 100644 --- a/packages/stream_chat/lib/src/api/channel.dart +++ b/packages/stream_chat/lib/src/api/channel.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:convert'; import 'dart:math'; import 'package:collection/collection.dart' @@ -10,9 +9,9 @@ import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/src/api/retry_queue.dart'; import 'package:stream_chat/src/event_type.dart'; import 'package:stream_chat/src/extensions/rate_limit.dart'; -import 'package:stream_chat/src/models/attachment_file.dart'; -import 'package:stream_chat/src/models/channel_state.dart'; -import 'package:stream_chat/src/models/user.dart'; +import 'package:stream_chat/src/core/models/attachment_file.dart'; +import 'package:stream_chat/src/core/models/channel_state.dart'; +import 'package:stream_chat/src/core/models/user.dart'; import 'package:stream_chat/stream_chat.dart'; /// This a the class that manages a specific channel. @@ -214,8 +213,6 @@ class Channel { StreamChatClient get client => _client; final StreamChatClient _client; - String get _channelURL => '/channels/$type/$id'; - final Completer _initializedCompleter = Completer(); /// True if this is initialized @@ -492,7 +489,7 @@ class Channel { state?.addMessage(message); - final response = await _client.deleteMessage(message); + final response = await _client.deleteMessage(message.id); state?.addMessage(message.copyWith(status: MessageSendingStatus.sent)); @@ -507,9 +504,9 @@ class Channel { /// Pins provided message Future pinMessage( - Message message, - Object? timeoutOrExpirationDate, - ) { + Message message, { + Object? /*num|DateTime*/ timeoutOrExpirationDate, + }) { assert(() { if (timeoutOrExpirationDate is! DateTime && timeoutOrExpirationDate != null && @@ -517,7 +514,7 @@ class Channel { throw ArgumentError('Invalid timeout or Expiration date'); } return true; - }(), 'Check for invalid token or expiration date'); + }(), 'Check for invalid timeout or expiration date'); DateTime? pinExpires; if (timeoutOrExpirationDate is DateTime) { @@ -619,10 +616,7 @@ class Channel { /// Send an event on this channel Future sendEvent(Event event) { _checkInitialized(); - return _client.post( - '$_channelURL/event', - data: {'event': event.toJson()}, - ).then((res) => _client.decode(res.data, EmptyResponse.fromJson)!); + return _client.sendEvent(id!, type, event); } /// Send a reaction to this channel @@ -674,21 +668,13 @@ class Channel { state?.addMessage(newMessage); - final data = Map.from(extraData) - ..addAll({ - 'type': type, - }); - try { - final res = await _client.post( - '/messages/$messageId/reaction', - data: { - 'reaction': data, - 'enforce_unique': enforceUnique, - }, + final reactionResp = await _client.sendReaction( + messageId, + type, + extraData: extraData, + enforceUnique: enforceUnique, ); - final reactionResp = - _client.decode(res.data, SendReactionResponse.fromJson); return reactionResp; } catch (_) { // Reset the message if the update fails @@ -731,9 +717,11 @@ class Channel { state?.addMessage(newMessage); try { - final res = await client - .delete('/messages/${message.id}/reaction/${reaction.type}'); - return _client.decode(res.data, EmptyResponse.fromJson); + final deleteResponse = await _client.deleteReaction( + message.id, + reaction.type, + ); + return deleteResponse; } catch (_) { // Reset the message if the update fails state?.addMessage(message); @@ -746,45 +734,45 @@ class Channel { Map channelData, [ Message? updateMessage, ]) async { - final response = await _client.post(_channelURL, data: { - if (updateMessage != null) - 'message': updateMessage.copyWith(updatedAt: DateTime.now()).toJson(), - 'data': channelData, - }); - return _client.decode(response.data, UpdateChannelResponse.fromJson); + _checkInitialized(); + return _client.updateChannel( + id!, + type, + channelData, + message: updateMessage, + ); } /// Edit the channel custom data Future updatePartial( - Map channelData) async { - final response = await _client.patch(_channelURL, data: channelData); - return _client.decode(response.data, PartialUpdateChannelResponse.fromJson); + Map channelData, + ) async { + _checkInitialized(); + return _client.updateChannelPartial(id!, type, channelData); } /// Delete this channel. Messages are permanently removed. Future delete() async { - final response = await _client.delete(_channelURL); - return _client.decode(response.data, EmptyResponse.fromJson); + _checkInitialized(); + return _client.deleteChannel(id!, type); } /// Removes all messages from the channel Future truncate() async { - final response = await _client.post('$_channelURL/truncate'); - return _client.decode(response.data, EmptyResponse.fromJson); + _checkInitialized(); + return _client.truncateChannel(id!, type); } /// Accept invitation to the channel Future acceptInvite([Message? message]) async { - final res = await _client.post(_channelURL, - data: {'accept_invite': true, 'message': message?.toJson()}); - return _client.decode(res.data, AcceptInviteResponse.fromJson); + _checkInitialized(); + return _client.acceptChannelInvite(id!, type, message: message); } /// Reject invitation to the channel Future rejectInvite([Message? message]) async { - final res = await _client.post(_channelURL, - data: {'reject_invite': true, 'message': message?.toJson()}); - return _client.decode(res.data, RejectInviteResponse.fromJson); + _checkInitialized(); + return _client.rejectChannelInvite(id!, type, message: message); } /// Add members to the channel @@ -792,11 +780,8 @@ class Channel { List memberIds, [ Message? message, ]) async { - final res = await _client.post(_channelURL, data: { - 'add_members': memberIds, - 'message': message?.toJson(), - }); - return _client.decode(res.data, AddMembersResponse.fromJson); + _checkInitialized(); + return _client.addChannelMembers(id!, type, memberIds, message: message); } /// Invite members to the channel @@ -804,11 +789,8 @@ class Channel { List memberIds, [ Message? message, ]) async { - final res = await _client.post(_channelURL, data: { - 'invites': memberIds, - 'message': message?.toJson(), - }); - return _client.decode(res.data, InviteMembersResponse.fromJson); + _checkInitialized(); + return _client.inviteChannelMembers(id!, type, memberIds, message: message); } /// Remove members from the channel @@ -816,11 +798,8 @@ class Channel { List memberIds, [ Message? message, ]) async { - final res = await _client.post(_channelURL, data: { - 'remove_members': memberIds, - 'message': message?.toJson(), - }); - return _client.decode(res.data, RemoveMembersResponse.fromJson); + _checkInitialized(); + return _client.removeChannelMembers(id!, type, memberIds, message: message); } /// Send action for a specific message of this channel @@ -829,16 +808,8 @@ class Channel { Map formData, ) async { _checkInitialized(); - final messageId = message.id; - final response = await _client.post('/messages/$messageId/action', data: { - 'id': id, - 'type': type, - 'form_data': formData, - 'message_id': messageId, - }); - - final res = _client.decode(response.data, SendActionResponse.fromJson); + final res = await _client.sendAction(id!, type, messageId, formData); if (res.message != null) { state!.addMessage(res.message!); @@ -867,21 +838,20 @@ class Channel { state!.threads[oldMessage.parentId!]!..remove(oldMessage)); } } - await _client.chatPersistenceClient?.deleteMessageById(messageId); } - return res; } - /// Mark all channel messages as read - Future markRead() async { + /// Mark all messages as read + /// Optionally provide a [messageId] if you want to mark a + /// particular message as read + Future markRead({String? messageId}) async { _checkInitialized(); client.state.totalUnreadCount = max( 0, (client.state.totalUnreadCount ?? 0) - (state!.unreadCount ?? 0)); state!._unreadCountController.add(0); - final response = await _client.post('$_channelURL/read', data: {}); - return _client.decode(response.data, EmptyResponse.fromJson); + return _client.markChannelRead(id!, type, messageId: messageId); } /// Loads the initial channel state and watches for changes @@ -924,11 +894,8 @@ class Channel { /// Stop watching the channel Future stopWatching() async { - final response = await _client.post( - '$_channelURL/stop-watching', - data: {}, - ); - return _client.decode(response.data, EmptyResponse.fromJson); + _checkInitialized(); + return _client.stopChannelWatching(id!, type); } /// List the message replies for a parent message @@ -949,50 +916,29 @@ class Channel { return QueryRepliesResponse()..messages = cachedReplies; } } - - final response = await _client.get('/messages/$parentId/replies', - queryParameters: options.toJson()); - - final repliesResponse = _client.decode( - response.data, - QueryRepliesResponse.fromJson, - ); - + final repliesResponse = await _client.getReplies(parentId, options); state?.updateThreadInfo(parentId, repliesResponse.messages); - return repliesResponse; } /// List the reactions for a message in the channel Future getReactions( - String messageID, + String messageId, PaginationParams options, - ) async { - final response = await _client.get( - '/messages/$messageID/reactions', - queryParameters: options.toJson(), - ); - return _client.decode( - response.data, QueryReactionsResponse.fromJson); - } + ) => + _client.getReactions( + messageId, + options, + ); /// Retrieves a list of messages by ID Future getMessagesById( - List messageIDs) async { - final response = await _client.get( - '$_channelURL/messages', - queryParameters: {'ids': messageIDs.join(',')}, - ); - - final res = _client.decode( - response.data, - GetMessagesByIdResponse.fromJson, - ); - + List messageIDs, + ) async { + _checkInitialized(); + final res = await _client.getMessagesById(id!, type, messageIDs); final messages = res.messages; - state?.updateChannelState(ChannelState(messages: messages)); - return res; } @@ -1000,18 +946,11 @@ class Channel { Future translateMessage( String messageId, String language, - ) async { - final response = await _client.post( - '/messages/$messageId/translate', - data: { - 'language': language, - }, - ); - return _client.decode( - response.data, - TranslateMessageResponse.fromJson, - ); - } + ) => + _client.translateMessage( + messageId, + language, + ); /// Creates a new channel Future create() async => query(options: { @@ -1030,36 +969,10 @@ class Channel { PaginationParams? watchersPagination, bool preferOffline = false, }) async { - var path = '/channels/$type'; - if (id != null) path = '$path/$id'; - path = '$path/query'; - - final payload = Map.from({ - 'state': true, - }) - ..addAll(options); - - if (_extraData.isNotEmpty) { - payload['data'] = _extraData; - } - - if (messagesPagination != null) { - payload['messages'] = messagesPagination.toJson(); - } - if (membersPagination != null) { - payload['members'] = membersPagination.toJson(); - } - if (watchersPagination != null) { - payload['watchers'] = watchersPagination.toJson(); - } - if (preferOffline && cid != null) { - final updatedState = - (await _client.chatPersistenceClient?.getChannelStateByCid( - cid!, - messagePagination: messagesPagination, - ))!; - if (updatedState.messages.isNotEmpty) { + final updatedState = await _client.chatPersistenceClient + ?.getChannelStateByCid(cid!, messagePagination: messagesPagination); + if (updatedState != null && updatedState.messages.isNotEmpty) { if (state == null) { _initState(updatedState); } else { @@ -1070,8 +983,14 @@ class Channel { } try { - final response = await _client.post(path, data: payload); - final updatedState = _client.decode(response.data, ChannelState.fromJson); + final updatedState = await _client.queryChannel( + type, + channelId: id, + channelData: _extraData, + messagesPagination: messagesPagination, + membersPagination: membersPagination, + watchersPagination: watchersPagination, + ); if (_id == null) { _id = updatedState.channel!.id; @@ -1096,45 +1015,26 @@ class Channel { Filter? filter, List? sort, PaginationParams? pagination, - }) async { - final payload = { - 'sort': sort, - 'filter_conditions': filter ?? {}, - 'type': type, - }; - - if (pagination != null) { - payload.addAll(pagination.toJson()); - } - - if (id != null) { - payload['id'] = id; - } else if (state?.members.isNotEmpty == true) { - payload['members'] = state!.members; - } - - final rawRes = await _client.get('/members', queryParameters: { - 'payload': jsonEncode(payload), - }); - final response = _client.decode(rawRes.data, QueryMembersResponse.fromJson); - return response; - } + }) => + _client.queryMembers( + type, + channelId: id, + filter: filter, + members: state?.members, + sort: sort, + pagination: pagination, + ); /// Mutes the channel - Future mute({Duration? expiration}) async { - final response = await _client.post('/moderation/mute/channel', data: { - 'channel_cid': cid, - if (expiration != null) 'expiration': expiration.inMilliseconds, - }); - return _client.decode(response.data, EmptyResponse.fromJson); + Future mute({Duration? expiration}) { + _checkInitialized(); + return _client.muteChannel(cid!, expiration: expiration); } /// Unmutes the channel - Future unmute() async { - final response = await _client.post('/moderation/unmute/channel', data: { - 'channel_cid': cid, - }); - return _client.decode(response.data, EmptyResponse.fromJson); + Future unmute() { + _checkInitialized(); + return _client.unmuteChannel(cid!); } /// Bans a user from the channel @@ -1188,9 +1088,11 @@ class Channel { /// will be removed for the user Future hide({bool clearHistory = false}) async { _checkInitialized(); - final response = await _client - .post('$_channelURL/hide', data: {'clear_history': clearHistory}); - + final response = await _client.hideChannel( + id!, + type, + clearHistory: clearHistory, + ); if (clearHistory == true) { state!.truncate(); final cid = _cid; @@ -1198,15 +1100,13 @@ class Channel { await _client.chatPersistenceClient?.deleteMessageByCid(cid); } } - - return _client.decode(response.data, EmptyResponse.fromJson); + return response; } /// Removes the hidden status for the channel Future show() async { _checkInitialized(); - final response = await _client.post('$_channelURL/show'); - return _client.decode(response.data, EmptyResponse.fromJson); + return _client.showChannel(id!, type); } /// Stream of [Event] coming from websocket connection specific for the @@ -1588,7 +1488,7 @@ class ChannelClientState { } readList.add(Read( user: event.user!, - lastRead: event.createdAt!, + lastRead: event.createdAt, unreadMessages: event.totalUnreadCount ?? 0, )); _channelState = _channelState.copyWith(read: readList); @@ -1793,7 +1693,7 @@ class ChannelClientState { BehaviorSubject.seeded({}); set _threads(Map> v) { - _channel._client.chatPersistenceClient?.updateMessages( + _channel.client.chatPersistenceClient?.updateMessages( _channel.cid!, v.values.expand((v) => v).toList(), ); diff --git a/packages/stream_chat/lib/src/api/retry_queue.dart b/packages/stream_chat/lib/src/api/retry_queue.dart index c13db9c6..e6d883a9 100644 --- a/packages/stream_chat/lib/src/api/retry_queue.dart +++ b/packages/stream_chat/lib/src/api/retry_queue.dart @@ -6,7 +6,7 @@ import 'package:stream_chat/src/api/channel.dart'; import 'package:stream_chat/src/api/retry_policy.dart'; import 'package:stream_chat/src/event_type.dart'; import 'package:stream_chat/src/exceptions.dart'; -import 'package:stream_chat/src/models/message.dart'; +import 'package:stream_chat/src/core/models/message.dart'; import 'package:stream_chat/stream_chat.dart'; /// The retry queue associated to a channel diff --git a/packages/stream_chat/lib/src/api/web_socket_channel_html.dart b/packages/stream_chat/lib/src/api/web_socket_channel_html.dart deleted file mode 100644 index ab821ca9..00000000 --- a/packages/stream_chat/lib/src/api/web_socket_channel_html.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:web_socket_channel/html.dart'; -import 'package:web_socket_channel/web_socket_channel.dart'; - -/// Html version of websocket implementation -/// Used in Flutter web version -WebSocketChannel connectWebSocket(String url, {Iterable? protocols}) => - HtmlWebSocketChannel.connect(url, protocols: protocols); diff --git a/packages/stream_chat/lib/src/api/web_socket_channel_io.dart b/packages/stream_chat/lib/src/api/web_socket_channel_io.dart deleted file mode 100644 index 8402ca0c..00000000 --- a/packages/stream_chat/lib/src/api/web_socket_channel_io.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:web_socket_channel/io.dart'; -import 'package:web_socket_channel/web_socket_channel.dart'; - -/// IO version of websocket implementation -/// Used in Flutter mobile version -WebSocketChannel connectWebSocket(String url, {Iterable? protocols}) => - IOWebSocketChannel.connect(url, protocols: protocols); diff --git a/packages/stream_chat/lib/src/api/web_socket_channel_stub.dart b/packages/stream_chat/lib/src/api/web_socket_channel_stub.dart deleted file mode 100644 index e2efaee6..00000000 --- a/packages/stream_chat/lib/src/api/web_socket_channel_stub.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:web_socket_channel/web_socket_channel.dart'; - -/// Stub version of websocket implementation -/// Used just for conditional library import -WebSocketChannel connectWebSocket(String url, - {Iterable? protocols, - Map? headers, - Duration? pingInterval}) => - throw UnimplementedError(); diff --git a/packages/stream_chat/lib/src/api/websocket.dart b/packages/stream_chat/lib/src/api/websocket.dart deleted file mode 100644 index b5bbbb69..00000000 --- a/packages/stream_chat/lib/src/api/websocket.dart +++ /dev/null @@ -1,321 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:math'; - -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/models/event.dart'; -import 'package:stream_chat/src/models/user.dart'; -import 'package:web_socket_channel/web_socket_channel.dart'; - -/// Typedef which exposes an [Event] as the only parameter. -typedef EventHandler = void Function(Event); - -/// Typedef used for connecting to a websocket. Method returns a -/// [WebSocketChannel] and accepts a connection [url] and an optional -/// [Iterable] of `protocols`. -typedef ConnectWebSocket = WebSocketChannel Function(String? url, - {Iterable? protocols}); - -// TODO: parse error even -// TODO: if parsing an error into an event fails we should not hide the -// TODO: original error -/// A WebSocket connection that reconnects upon failure. -class WebSocket { - /// Creates a new websocket - /// To connect the WS call [connect] - WebSocket({ - required this.baseUrl, - required this.user, - required this.handler, - this.connectParams = const {}, - this.connectPayload = const {}, - this.logger, - this.connectFunc, - this.reconnectionMonitorInterval = 1, - this.healthCheckInterval = 20, - this.reconnectionMonitorTimeout = 40, - }) { - final qs = Map.from(connectParams); - - final data = Map.from(connectPayload); - - data['user_details'] = user.toJson(); - qs['json'] = json.encode(data); - - if (baseUrl.startsWith('https')) { - _path = baseUrl.replaceFirst('https://', ''); - _path = Uri.https(_path, 'connect', qs) - .toString() - .replaceFirst('https', 'wss'); - } else if (baseUrl.startsWith('http')) { - _path = baseUrl.replaceFirst('http://', ''); - _path = - Uri.http(_path, 'connect', qs).toString().replaceFirst('http', 'ws'); - } else { - _path = Uri.https(baseUrl, 'connect', qs) - .toString() - .replaceFirst('https', 'wss'); - } - } - - /// WS base url - final String baseUrl; - - /// User performing the WS connection - final User user; - - /// Querystring connection parameters - final Map connectParams; - - /// WS connection payload - final Map connectPayload; - - /// Functions that will be called every time a new event is received from the - /// connection - final EventHandler handler; - - /// A WS specific logger instance - final Logger? logger; - - /// Connection function - /// Used only for testing purpose - @visibleForTesting - final ConnectWebSocket? connectFunc; - - /// Interval of the reconnection monitor timer - /// This checks that it received a new event in the last - /// [reconnectionMonitorTimeout] seconds, otherwise it considers the - /// connection unhealthy and reconnects the WS - final int reconnectionMonitorInterval; - - /// Interval of the health event sending timer - /// This sends a health event every [healthCheckInterval] seconds in order to - /// make the server aware that the client is still listening - final int healthCheckInterval; - - /// The timeout that uses the reconnection monitor timer to consider the - /// connection unhealthy - final int reconnectionMonitorTimeout; - - final _connectionStatusController = - BehaviorSubject.seeded(ConnectionStatus.disconnected); - - set _connectionStatus(ConnectionStatus status) => - _connectionStatusController.add(status); - - /// The current connection status value - ConnectionStatus? get connectionStatus => _connectionStatusController.value; - - /// This notifies of connection status changes - Stream get connectionStatusStream => - _connectionStatusController.stream; - - late String _path; - int _retryAttempt = 1; - late WebSocketChannel _channel; - Timer? _healthCheck, _reconnectionMonitor; - DateTime? _lastEventAt; - bool _manuallyDisconnected = false; - bool _connecting = false; - bool _reconnecting = false; - - Event _decodeEvent(String source) => Event.fromJson(json.decode(source)); - - Completer _connectionCompleter = Completer(); - - /// Connect the WS using the parameters passed in the constructor - Future connect() async { - _manuallyDisconnected = false; - - if (_connecting) { - logger?.severe('already connecting'); - return null; - } - - _connecting = true; - _connectionStatus = ConnectionStatus.connecting; - - logger?.info('connecting to $_path'); - - _channel = - connectFunc?.call(_path) ?? WebSocketChannel.connect(Uri.parse(_path)); - _channel.stream.listen( - (data) async { - final jsonData = json.decode(data); - if (jsonData['error'] != null) { - return _onConnectionError(jsonData['error']); - } - _onData(data); - }, - onError: (error, stacktrace) { - _onConnectionError(error, stacktrace); - }, - onDone: _onDone, - ); - return _connectionCompleter.future; - } - - void _onDone() { - _connecting = false; - if (_manuallyDisconnected) { - return; - } - - logger?.info('connection closed | closeCode: ${_channel.closeCode} | ' - 'closedReason: ${_channel.closeReason}'); - - if (!_reconnecting) { - _reconnect(); - } - } - - void _onData(data) { - if (_manuallyDisconnected) { - return; - } - - final event = _decodeEvent(data); - logger?.info('received new event: $data'); - - if (_lastEventAt == null) { - logger?.info('connection estabilished'); - _connecting = false; - _reconnecting = false; - _lastEventAt = DateTime.now(); - - _connectionStatus = ConnectionStatus.connected; - _retryAttempt = 1; - - if (!_connectionCompleter.isCompleted) { - _connectionCompleter.complete(event); - } - - _startReconnectionMonitor(); - _startHealthCheck(); - } - - handler(event); - _lastEventAt = DateTime.now(); - } - - Future _onConnectionError(error, [stacktrace]) async { - logger?..severe('error connecting')..severe(error); - if (stacktrace != null) { - logger?.severe(stacktrace); - } - _connecting = false; - - if (!_reconnecting) { - _connectionStatus = ConnectionStatus.disconnected; - } - - if (!_connectionCompleter.isCompleted) { - _cancelTimers(); - _connectionCompleter.completeError(error, stacktrace); - } else if (!_reconnecting) { - return _reconnect(); - } - } - - void _reconnectionTimer(_) { - final now = DateTime.now(); - if (_lastEventAt != null && - now.difference(_lastEventAt!).inSeconds > reconnectionMonitorTimeout) { - _channel.sink.close(); - } - } - - void _startReconnectionMonitor() { - _reconnectionMonitor = Timer.periodic( - Duration(seconds: reconnectionMonitorInterval), - _reconnectionTimer, - ); - - _reconnectionTimer(_reconnectionMonitor); - } - - void _reconnectTimer() async { - if (!_reconnecting) { - return; - } - if (_connecting) { - logger?.info('already connecting'); - return; - } - - logger?.info('reconnecting..'); - - _cancelTimers(); - - try { - await connect(); - } catch (e) { - logger?.log(Level.SEVERE, e.toString()); - } - await Future.delayed( - Duration(seconds: min(_retryAttempt * 5, 25)), - () { - _reconnectTimer(); - _retryAttempt++; - }, - ); - } - - Future _reconnect() async { - logger?.info('reconnect'); - if (!_reconnecting) { - _reconnecting = true; - _connectionStatus = ConnectionStatus.connecting; - } - - _reconnectTimer(); - } - - void _cancelTimers() { - _lastEventAt = null; - if (_healthCheck != null) { - _healthCheck!.cancel(); - } - if (_reconnectionMonitor != null) { - _reconnectionMonitor!.cancel(); - } - } - - void _healthCheckTimer(_) { - logger?.info('sending health.check'); - _channel.sink.add("{'type': 'health.check'}"); - } - - void _startHealthCheck() { - logger?.info('start health check monitor'); - - _healthCheck = Timer.periodic( - Duration(seconds: healthCheckInterval), - _healthCheckTimer, - ); - - _healthCheckTimer(_healthCheck); - } - - /// Disconnects the WS and releases eventual resources - Future disconnect() async { - _connecting = false; - if (!_connectionCompleter.isCompleted) { - _connectionCompleter.complete(); - } - if (_manuallyDisconnected) { - return; - } - logger?.info('disconnecting'); - _connectionCompleter = Completer(); - _cancelTimers(); - _reconnecting = false; - _manuallyDisconnected = true; - _connectionStatus = ConnectionStatus.disconnected; - await _connectionStatusController.close(); - await _channel.sink.close(); - } -} diff --git a/packages/stream_chat/lib/src/client.dart b/packages/stream_chat/lib/src/client.dart index 3cabe274..936ccfc0 100644 --- a/packages/stream_chat/lib/src/client.dart +++ b/packages/stream_chat/lib/src/client.dart @@ -5,61 +5,46 @@ import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:logging/logging.dart'; -import 'package:meta/meta.dart'; import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/src/api/channel.dart'; -import 'package:stream_chat/src/api/connection_status.dart'; -import 'package:stream_chat/src/api/requests.dart'; -import 'package:stream_chat/src/api/responses.dart'; +import 'package:stream_chat/src/ws/connection_status.dart'; +import 'package:stream_chat/src/core/api/attachment_file_uploader.dart'; +import 'package:stream_chat/src/core/api/requests.dart'; +import 'package:stream_chat/src/core/api/responses.dart'; import 'package:stream_chat/src/api/retry_policy.dart'; -import 'package:stream_chat/src/api/websocket.dart'; -import 'package:stream_chat/src/attachment_file_uploader.dart'; +import 'package:stream_chat/src/ws/websocket.dart'; +import 'package:stream_chat/src/core/api/stream_chat_api.dart'; +import 'package:stream_chat/src/core/http/connection_id_manager.dart'; +import 'package:stream_chat/src/core/http/stream_http_client.dart'; +import 'package:stream_chat/src/core/http/token.dart'; +import 'package:stream_chat/src/core/http/token_manager.dart'; import 'package:stream_chat/src/db/chat_persistence_client.dart'; import 'package:stream_chat/src/event_type.dart'; -import 'package:stream_chat/src/exceptions.dart'; -import 'package:stream_chat/src/extensions/map_extension.dart'; -import 'package:stream_chat/src/models/attachment_file.dart'; -import 'package:stream_chat/src/models/channel_model.dart'; -import 'package:stream_chat/src/models/channel_state.dart'; -import 'package:stream_chat/src/models/event.dart'; -import 'package:stream_chat/src/models/filter.dart'; -import 'package:stream_chat/src/models/message.dart'; -import 'package:stream_chat/src/models/own_user.dart'; -import 'package:stream_chat/src/models/user.dart'; -import 'package:stream_chat/src/platform_detector/platform_detector.dart'; -import 'package:stream_chat/version.dart'; -import 'package:uuid/uuid.dart'; +import 'package:stream_chat/src/core/models/attachment_file.dart'; +import 'package:stream_chat/src/core/models/channel_model.dart'; +import 'package:stream_chat/src/core/models/channel_state.dart'; +import 'package:stream_chat/src/core/models/event.dart'; +import 'package:stream_chat/src/core/models/message.dart'; +import 'package:stream_chat/src/core/models/own_user.dart'; +import 'package:stream_chat/src/core/models/user.dart'; + +import 'package:stream_chat/src/core/models/filter.dart'; + +import 'package:stream_chat/src/core/models/member.dart'; /// Handler function used for logging records. Function requires a single /// [LogRecord] as the only parameter. typedef LogHandlerFunction = void Function(LogRecord record); -/// Used for decoding [Map] data to a generic type `T`. -typedef DecoderFunction = T Function(Map); - /// A function which can be used to request a Stream Chat API token from your /// own backend server. Function requires a single [userId]. -typedef TokenProvider = Future Function(String? userId); +typedef TokenProvider = Future Function(String userId); -/// Provider used to send push notifications. -enum PushProvider { - /// Send notifications using Google's Firebase Cloud Messaging - firebase, - - /// Send notifications using Apple's Push Notification service - apn -} - -extension on PushProvider { - /// Returns the string notion for [PushProvider]. - String get name { - if (this == PushProvider.apn) { - return 'apn'; - } else { - return 'firebase'; - } - } -} +final _levelEmojiMapper = { + Level.INFO: 'ℹï¸', + Level.WARNING: 'âš ï¸', + Level.SEVERE: '🚨', +}; /// The official Dart client for Stream Chat, /// a service for building chat applications. @@ -79,37 +64,64 @@ class StreamChatClient { /// You should only create the client once and re-use it across your /// application. StreamChatClient( - this.apiKey, { - this.tokenProvider, - this.baseURL = _defaultBaseURL, - this.logLevel = Level.WARNING, + String apiKey, { + this.logLevel = Level.ALL, LogHandlerFunction? logHandlerFunction, + RetryPolicy? retryPolicy, + Location? location, + String? baseURL, Duration connectTimeout = const Duration(seconds: 6), Duration receiveTimeout = const Duration(seconds: 6), - Dio? httpClient, - RetryPolicy? retryPolicy, - this.attachmentFileUploader, + StreamChatApi? chatApi, + AttachmentFileUploader? attachmentFileUploader, }) { - _retryPolicy = retryPolicy ?? - RetryPolicy( - retryTimeout: - (StreamChatClient client, int attempt, ApiError? error) => - Duration(seconds: 1 * attempt), - shouldRetry: - (StreamChatClient client, int attempt, ApiError? error) => - attempt < 5, + _setupLogger(logHandlerFunction); + + final options = StreamHttpClientOptions( + baseUrl: baseURL, + location: location, + connectTimeout: connectTimeout, + receiveTimeout: receiveTimeout, + ); + + _chatApi = chatApi ?? + StreamChatApi( + apiKey, + options: options, + tokenManager: _tokenManager, + connectionIdManager: _connectionIdManager, + attachmentFileUploader: attachmentFileUploader, + logger: _detachedLogger('🕸ï¸'), ); - attachmentFileUploader ??= StreamAttachmentFileUploader(this); + _ws = WebSocket( + apiKey: apiKey, + baseUrl: options.baseUrl, + tokenManager: _tokenManager, + handler: handleEvent, + logger: _detachedLogger('🔌'), + ); + + _retryPolicy = retryPolicy ?? + RetryPolicy( + retryTimeout: (_, int attempt, __) => Duration(seconds: 1 * attempt), + shouldRetry: (_, int attempt, __) => attempt < 5, + ); state = ClientState(this); - _setupLogger(logHandlerFunction); - _setupDio(httpClient, receiveTimeout, connectTimeout); - logger.info('instantiating new client'); } + late final StreamChatApi _chatApi; + late final WebSocket _ws; + + /// This client state + late ClientState state; + + final _tokenManager = TokenManager(); + final _connectionIdManager = ConnectionIdManager(); + set chatPersistenceClient(ChatPersistenceClient? value) { _originalChatPersistenceClient = value; } @@ -121,22 +133,20 @@ class StreamChatClient { ChatPersistenceClient? _chatPersistenceClient; - /// Attachment uploader - AttachmentFileUploader? attachmentFileUploader; - /// Whether the chat persistence is available or not bool get persistenceEnabled => _chatPersistenceClient != null; RetryPolicy? _retryPolicy; + // sync state of the channels present inside state, defaults to false bool _synced = false; + // the last dateTime at the which all the channels were synced + DateTime? _lastSyncedAt; + /// The retry policy options getter RetryPolicy? get retryPolicy => _retryPolicy; - /// This client state - late ClientState state; - /// By default the Chat client will write all messages with level Warn or /// Error to stdout. /// @@ -173,38 +183,13 @@ class StreamChatClient { ///``` late LogHandlerFunction logHandlerFunction; - /// Your project Stream Chat api key. - /// Find your API keys here https://getstream.io/dashboard/ - String apiKey; - - /// Your project Stream Chat base url. - final String baseURL; - - /// A function in which you send a request to your own backend to get a Stream - /// Chat API token. - /// - /// The token will be the return value of the function. - /// It's used by the client to refresh the token once expired or to connect - /// the user without a predefined token using [connectUserWithProvider]. - final TokenProvider? tokenProvider; - - /// [Dio] httpClient - /// It's be chosen because it's easy to use and supports interesting features - /// out of the box (Interceptors, Global configuration, FormData, - /// File downloading etc.) - @visibleForTesting - Dio httpClient = Dio(); - - static const _defaultBaseURL = 'chat-us-east-1.stream-io-api.com'; - static const _tokenExpiredErrorCode = 40; StreamSubscription? _connectionStatusSubscription; - Future Function(ConnectionStatus)? _connectionStatusHandler; - final BehaviorSubject _controller = BehaviorSubject(); + final _eventController = BehaviorSubject(); /// Stream of [Event] coming from websocket connection /// Listen to this or use the [on] method to filter specific event types - Stream get stream => _controller.stream; + Stream get eventStream => _eventController.stream; final _wsConnectionStatusController = BehaviorSubject.seeded(ConnectionStatus.disconnected); @@ -219,153 +204,17 @@ class StreamChatClient { /// This notifies the connection status of the websocket connection. /// Listen to this to get notified when the websocket tries to reconnect. Stream get wsConnectionStatusStream => - _wsConnectionStatusController.stream; + _wsConnectionStatusController.stream.distinct(); - /// The current user token - String? token; - - /// The id of the current websocket connection - String? get connectionId => _connectionId; - - bool _anonymous = false; - String? _connectionId; - late WebSocket _ws; - - bool get _hasConnectionId => _connectionId != null; - - void _setupDio( - Dio? httpClient, - Duration receiveTimeout, - Duration connectTimeout, - ) { - logger.info('http client setup'); - - this.httpClient = httpClient ?? Dio(); - - String url; - if (!baseURL.startsWith('https') && !baseURL.startsWith('http')) { - url = Uri.https(baseURL, '').toString(); - } else { - url = baseURL; - } - - this.httpClient.options.baseUrl = url; - this.httpClient.options.receiveTimeout = receiveTimeout.inMilliseconds; - this.httpClient.options.connectTimeout = connectTimeout.inMilliseconds; - this.httpClient.interceptors.add( - InterceptorsWrapper( - onRequest: (options, handler) async { - options.queryParameters.addAll(_commonQueryParams); - options.headers.addAll(_httpHeaders); - - if (_connectionId != null && - (options.data is Map || options.data == null)) { - options.data = { - 'connection_id': _connectionId, - ...options.data ?? {}, - }; - } - - var stringData = options.data.toString(); - - if (options.data is FormData) { - final multiPart = (options.data as FormData).files[0].value; - stringData = '${multiPart.filename} - ${multiPart.contentType}'; - } - - logger.info(''' - - method: ${options.method} - url: ${options.uri} - headers: ${options.headers} - data: $stringData - - '''); - handler.next(options); - }, - onError: _tokenExpiredInterceptor, - ), + LogHandlerFunction get _defaultLogHandler => (LogRecord record) { + print( + '${record.time} ' + '${_levelEmojiMapper[record.level] ?? record.level.name} ' + '${record.loggerName} ${record.message} ', ); - } - - Future _tokenExpiredInterceptor( - DioError err, - ErrorInterceptorHandler handler, - ) async { - final apiError = ApiError( - err.response?.data, - err.response?.statusCode, - ); - - if (apiError.code == _tokenExpiredErrorCode) { - logger.info('token expired'); - - if (tokenProvider != null) { - httpClient.lock(); - final userId = state.user!.id; - - await _disconnect(); - - final newToken = await tokenProvider!(userId); - await Future.delayed(const Duration(seconds: 4)); - token = newToken; - - httpClient.unlock(); - - await connectUser(User(id: userId), newToken); - - try { - handler.resolve( - await httpClient.request( - err.requestOptions.path, - cancelToken: err.requestOptions.cancelToken, - data: err.requestOptions.data, - onReceiveProgress: err.requestOptions.onReceiveProgress, - onSendProgress: err.requestOptions.onSendProgress, - queryParameters: err.requestOptions.queryParameters, - options: Options( - method: err.requestOptions.method, - sendTimeout: err.requestOptions.sendTimeout, - receiveTimeout: err.requestOptions.receiveTimeout, - extra: err.requestOptions.extra, - headers: err.requestOptions.headers, - responseType: err.requestOptions.responseType, - contentType: err.requestOptions.contentType, - validateStatus: err.requestOptions.validateStatus, - receiveDataWhenStatusError: - err.requestOptions.receiveDataWhenStatusError, - followRedirects: err.requestOptions.followRedirects, - maxRedirects: err.requestOptions.maxRedirects, - requestEncoder: err.requestOptions.requestEncoder, - responseDecoder: err.requestOptions.responseDecoder, - listFormat: err.requestOptions.listFormat, - ), - ), - ); - } on DioError { - handler.reject(err); - } - } - } - } - - LogHandlerFunction _getDefaultLogHandler() { - final levelEmojiMapper = { - Level.INFO.name: 'ℹï¸', - Level.WARNING.name: 'âš ï¸', - Level.SEVERE.name: '🚨', - }; - return (LogRecord record) { - print( - '(${record.time}) ' - '${levelEmojiMapper[record.level.name] ?? record.level.name} ' - '${record.loggerName} ${record.message}', - ); - if (record.stackTrace != null) { - print(record.stackTrace); - } - }; - } + if (record.error != null) print(record.error); + if (record.stackTrace != null) print(record.stackTrace); + }; Logger _detachedLogger( String name, @@ -376,265 +225,272 @@ class StreamChatClient { void _setupLogger(LogHandlerFunction? logHandlerFunction) { logger.level = logLevel; - - this.logHandlerFunction = logHandlerFunction ?? _getDefaultLogHandler(); - + this.logHandlerFunction = logHandlerFunction ?? _defaultLogHandler; logger.onRecord.listen(this.logHandlerFunction); - logger.info('logger setup'); } - /// Call this function to dispose the client - void dispose() async { - await _chatPersistenceClient?.disconnect(); - await _disconnect(); - httpClient.close(); - await _controller.close(); - state.dispose(); - await _wsConnectionStatusController.close(); - } - - Map get _httpHeaders => { - 'Authorization': token, - 'stream-auth-type': _authType, - 'X-Stream-Client': _userAgent, - 'Content-Encoding': 'gzip', - }; - + /// Set the current user, this triggers a connection to the API. /// Set the current user, this triggers a connection to the API. /// It returns a [Future] that resolves when the connection is setup. @Deprecated('Use `connectUser` instead. Will be removed in Future releases') - Future setUser(User user, String token) => connectUser(user, token); + Future setUser(User user, String token) => connectUser(user, token); /// Connects the current user, this triggers a connection to the API. /// It returns a [Future] that resolves when the connection is setup. - Future connectUser(User? user, String? token) async { - if (_connectCompleter != null && !_connectCompleter!.isCompleted) { - logger.warning('Already connecting'); - throw Exception('Already connecting'); - } - - _connectCompleter = Completer(); - - logger.info('connect user'); - - if (user == null) { - final e = Error(); - _connectCompleter! - .completeError(e, StackTrace.fromString('No user provided.')); - throw e; - } - - state.user = OwnUser.fromJson(user.toJson()); - this.token = token; - _anonymous = false; - - return connect().then((event) { - _connectCompleter!.complete(event); - return event; - }).catchError((e, s) { - _connectCompleter!.completeError(e, s); - throw e; - }); - } + Future connectUser(User user, String token) => + _connectUser(user, token: Token.fromRawValue(token)); /// Set the current user using the [tokenProvider] to fetch the token. /// It returns a [Future] that resolves when the connection is setup. @Deprecated( 'Use `connectUserWithProvider` instead. Will be removed in Future releases', ) - Future setUserWithProvider(User user) => - connectUserWithProvider(user); + Future setUserWithProvider(User user, TokenProvider tokenProvider) => + connectUserWithProvider(user, tokenProvider); /// Connects the current user using the [tokenProvider] to fetch the token. /// It returns a [Future] that resolves when the connection is setup. - Future connectUserWithProvider(User user) async { - if (tokenProvider == null) { - throw Exception(''' - TokenProvider must be provided in the constructor in order to use `connectUserWithProvider` method. - Use `connectUser` providing a token. - '''); - } - final token = await tokenProvider!(user.id); - return connectUser(user, token); + Future connectUserWithProvider( + User user, TokenProvider tokenProvider) => + _connectUser(user, provider: tokenProvider); + + /// Set the current user with an anonymous id, this triggers a connection to + /// the API. It returns a [Future] that resolves when the connection is setup. + @Deprecated( + 'Use `connectAnonymousUser` instead. Will be removed in Future releases', + ) + Future setAnonymousUser() => connectAnonymousUser(); + + /// Connects the current user with an anonymous id, this triggers a connection + /// to the API. It returns a [Future] that resolves when the connection is + /// setup. + Future connectAnonymousUser() async { + final token = Token.anonymous(); + final user = OwnUser(id: token.userId); + return _connectUser(user, token: token); } - /// Stream of [Event] coming from websocket connection + /// Set the current user as guest, this triggers a connection to the API. + /// It returns a [Future] that resolves when the connection is setup. + @Deprecated( + 'Use `connectGuestUser` instead. Will be removed in Future releases', + ) + Future setGuestUser(User user) => connectGuestUser(user); + + /// Connects the current user as guest, this triggers a connection to the API. + /// It returns a [Future] that resolves when the connection is setup. + Future connectGuestUser(User user) async { + final userId = user.id; + final anonymousToken = Token.anonymous(userId: userId); + + // setting anonymous token so that getGuestUser works + _tokenManager.setTokenOrProvider(userId, token: anonymousToken); + + final guestUser = await _chatApi.guest.getGuestUser(user); + + // resetting tokenManager after successful request + _tokenManager.reset(); + + final guestUserToken = Token.fromRawValue(guestUser.accessToken); + return _connectUser(guestUser.user, token: guestUserToken); + } + + Future _connectUser( + User user, { + Token? token, + TokenProvider? provider, + }) async { + if (_ws.connectionCompleter?.isCompleted == false) { + throw Exception( + 'User already getting connected, try calling `disconnectUser` ' + 'before trying to connect again', + ); + } + + logger.info('connecting user : ${user.id}'); + + await _tokenManager.setTokenOrProvider( + user.id, + token: token, + provider: provider, + ); + + final ownUser = OwnUser.fromUser(user); + state.user = ownUser; + + try { + if (_originalChatPersistenceClient != null) { + _chatPersistenceClient = _originalChatPersistenceClient; + await _chatPersistenceClient!.connect(ownUser.id); + } + final event = await openConnection(); + return event; + } catch (e, stk) { + logger.severe('error connecting user : ${ownUser.id}', e, stk); + rethrow; + } + } + + /// Creates a new WebSocket connection with the current user. + Future openConnection() async { + assert( + state.user != null, + 'User is not set on client, ' + 'use `connectUser` or `connectAnonymousUser` instead', + ); + + final user = state.user!; + + logger.info('Opening web-socket connection for ${user.id}'); + + if (wsConnectionStatus == ConnectionStatus.connecting) { + throw Exception('Connection already in progress for ${user.id}'); + } + + if (wsConnectionStatus == ConnectionStatus.connected) { + throw Exception('Connection already connected for ${user.id}'); + } + + _wsConnectionStatus = ConnectionStatus.connecting; + + _connectionStatusSubscription = _ws.connectionStatusStream.listen( + _connectionStatusHandler, + ); + + try { + return await _ws.connect(user); + } catch (e, stk) { + logger.severe('error connecting ws', e, stk); + rethrow; + } + } + + /// Disconnects the [_ws] connection, + /// without removing the user set on client. + /// + /// This will not trigger default auto-retry mechanism for reconnection. + /// You need to call [openConnection] to reconnect to [_ws]. + void closeConnection() { + _wsConnectionStatus = ConnectionStatus.disconnected; + + _connectionStatusSubscription?.cancel(); + _connectionStatusSubscription = null; + + _ws.disconnect(); + } + + void _handleHealthCheckEvent(Event event) { + final user = event.me; + if (user != null) state.user = user; + + final connectionId = event.connectionId; + if (connectionId != null) { + _connectionIdManager.setConnectionId(connectionId); + _chatPersistenceClient?.updateConnectionInfo(event); + } + } + + /// Method called to add a new event to the [_eventController]. + void handleEvent(Event event) { + if (event.type == EventType.healthCheck) { + return _handleHealthCheckEvent(event); + } + if (!event.isLocal && _synced) { + _lastSyncedAt = event.createdAt; + _chatPersistenceClient?.updateLastSyncAt(event.createdAt); + } + state.updateUser(event.user); + return _eventController.add(event); + } + + void _connectionStatusHandler(ConnectionStatus status) async { + final currentState = _wsConnectionStatus = status; + + handleEvent(Event( + type: EventType.connectionChanged, + online: status == ConnectionStatus.connected, + )); + + if (currentState == ConnectionStatus.connected) { + // connection recovered + final cids = state.channels.keys.toList(growable: false); + if (cids.isNotEmpty) { + await queryChannelsOnline( + filter: Filter.in_('cid', cids), + paginationParams: const PaginationParams(limit: 30), + ).then((_) => sync(cids: cids, lastSyncAt: _lastSyncedAt)); + } + handleEvent(Event( + type: EventType.connectionRecovered, + online: true, + )); + } else { + _synced = false; + } + } + + /// Stream of [Event] coming from [_ws] connection /// Pass an eventType as parameter in order to filter just a type of event Stream on([ String? eventType, String? eventType2, String? eventType3, String? eventType4, - ]) => - stream.where((event) => - eventType == null || - (event.type != null && - (event.type == eventType || - event.type == eventType2 || - event.type == eventType3 || - event.type == eventType4))); - - /// Method called to add a new event to the [_controller]. - void handleEvent(Event event) async { - logger.info('handle new event: ${event.toJson()}'); - if (event.connectionId != null) { - _connectionId = event.connectionId; - } - - if (!event.isLocal) { - final createdAt = event.createdAt; - if (_synced && createdAt != null) { - await _chatPersistenceClient?.updateConnectionInfo(event); - await _chatPersistenceClient?.updateLastSyncAt(createdAt); - } - } - - if (event.user != null) { - state._updateUser(event.user); - } - - if (event.me != null) { - state.user = event.me; - } - _controller.add(event); - } - - Completer? _connectCompleter; - - /// Connect the client websocket - Future connect() async { - logger.info('connecting'); - if (wsConnectionStatus == ConnectionStatus.connecting) { - logger.warning('Already connecting'); - throw Exception('Already connecting'); - } - - if (wsConnectionStatus == ConnectionStatus.connected) { - logger.warning('Already connected'); - throw Exception('Already connected'); - } - - _wsConnectionStatus = ConnectionStatus.connecting; - - if (_originalChatPersistenceClient != null) { - _chatPersistenceClient = _originalChatPersistenceClient; - await _chatPersistenceClient!.connect(state.user!.id); - } - - _ws = WebSocket( - baseUrl: baseURL, - user: state.user!, - connectParams: { - 'api_key': apiKey, - 'authorization': token!, - 'stream-auth-type': _authType, - 'X-Stream-Client': _userAgent, - }, - connectPayload: { - 'user_id': state.user!.id, - 'server_determines_connection_id': true, - }, - handler: handleEvent, - logger: _detachedLogger('🔌'), - ); - - _connectionStatusHandler = (ConnectionStatus status) async { - _wsConnectionStatus = status; - handleEvent( - Event( - type: EventType.connectionChanged, - online: status == ConnectionStatus.connected, - ), - ); - - if (status == ConnectionStatus.connected) { - handleEvent(const Event( - type: EventType.connectionRecovered, - online: true, - )); - if (state.channels.isNotEmpty == true) { - // ignore: unawaited_futures - queryChannelsOnline( - filter: Filter.in_('cid', state.channels.keys.toList()), - ).then( - (_) async { - await resync(); - }, - ); - } else { - _synced = false; - } - } - }; - - _connectionStatusSubscription = - _ws.connectionStatusStream.listen(_connectionStatusHandler); - - var event = await _chatPersistenceClient?.getConnectionInfo(); - - await _ws.connect().then((e) async { - if (e != null) { - _chatPersistenceClient?.updateConnectionInfo(e); - event = e; - } - resync(); - }).catchError((err, stacktrace) { - logger.severe('error connecting ws', err, stacktrace); - if (err is Map) { - // ignore: only_throw_errors - throw err; - } - }); - - return event; + ]) { + if (eventType == null) return eventStream; + return eventStream.where((event) => + event.type == eventType || + event.type == eventType2 || + event.type == eventType3 || + event.type == eventType4); } /// Get the events missed while offline to sync the offline storage - Future resync([List? cids]) async { - final lastSyncAt = await _chatPersistenceClient?.getLastSyncAt(); + /// Will automatically fetch [cids] and [lastSyncedAt] if [persistenceEnabled] + Future sync({List? cids, DateTime? lastSyncAt}) async { + cids ??= await _chatPersistenceClient?.getChannelCids(); + if (cids == null || cids.isEmpty) { + _synced = true; + return; + } + lastSyncAt ??= await _chatPersistenceClient?.getLastSyncAt(); if (lastSyncAt == null) { _synced = true; return; } - cids ??= await _chatPersistenceClient?.getChannelCids(); - - if (cids?.isEmpty == true) { - return; - } - try { - final rawRes = await post('/sync', data: { - 'channel_cids': cids, - 'last_sync_at': lastSyncAt.toUtc().toIso8601String(), - }); - logger.fine('rawRes: $rawRes'); + final res = await _chatApi.general.sync(cids, lastSyncAt); + final events = res.events + ..sort((a, b) => a.createdAt.compareTo(b.createdAt)); - final res = decode( - rawRes.data, - SyncResponse.fromJson, - ); + for (final event in events) { + logger.fine('event.type: ${event.type}'); + final messageText = event.message?.text; + if (messageText != null) { + logger.fine('event.message.text: $messageText'); + } + handleEvent(event); + } - res.events.sort((a, b) => a.createdAt!.compareTo(b.createdAt!)); - - res.events.forEach((element) { - logger - ..fine('element.type: ${element.type}') - ..fine('element.message.text: ${element.message?.text}'); - }); - - res.events.forEach(handleEvent); - - await _chatPersistenceClient?.updateLastSyncAt(DateTime.now()); _synced = true; - } catch (error) { - logger.severe('Error during resync $error'); + final now = DateTime.now(); + _lastSyncedAt = now; + _chatPersistenceClient?.updateLastSyncAt(now); + } catch (e, stk) { + _synced = false; + logger.severe('Error during sync', e, stk); } } - String? _asMap(sort) => sort?.map((s) => s.toJson().toString())?.join(''); + String _generateHash(List objects) { + final payload = json.encode(objects); + final payloadBytes = utf8.encode(payload); + final payloadB64 = base64.encode(payloadBytes); + return payloadB64; + } final _queryChannelsStreams = >>{}; @@ -642,15 +498,29 @@ class StreamChatClient { Stream> queryChannels({ Filter? filter, List>? sort, - Map? options, - PaginationParams paginationParams = const PaginationParams(), + bool state = true, + bool watch = true, + bool presence = false, + int? memberLimit, int? messageLimit, + PaginationParams paginationParams = const PaginationParams(), bool waitForConnect = true, }) async* { - final hash = base64.encode(utf8.encode( - '$filter${_asMap(sort)}$options${paginationParams.toJson()}' - '$messageLimit', - )); + if (!_connectionIdManager.hasConnectionId) { + // ignore: parameter_assignments + watch = false; + } + + final hash = _generateHash([ + filter, + sort, + state, + watch, + presence, + memberLimit, + messageLimit, + paginationParams, + ]); if (_queryChannelsStreams.containsKey(hash)) { yield await _queryChannelsStreams[hash]!; @@ -666,9 +536,12 @@ class StreamChatClient { final newQueryChannelsFuture = queryChannelsOnline( filter: filter, sort: sort, - options: options, - paginationParams: paginationParams, + state: state, + watch: watch, + presence: presence, + memberLimit: memberLimit, messageLimit: messageLimit, + paginationParams: paginationParams, waitForConnect: waitForConnect, ).whenComplete(() { _queryChannelsStreams.remove(hash); @@ -687,67 +560,52 @@ class StreamChatClient { Future> queryChannelsOnline({ Filter? filter, List>? sort, - Map? options, + bool state = true, + bool watch = true, + bool presence = false, + int? memberLimit, int? messageLimit, - PaginationParams paginationParams = const PaginationParams(), bool waitForConnect = true, + PaginationParams paginationParams = const PaginationParams(), }) async { if (waitForConnect) { - if (_connectCompleter != null && !_connectCompleter!.isCompleted) { + if (_ws.connectionCompleter?.isCompleted == false) { logger.info('awaiting connection completer'); - await _connectCompleter!.future; + await _ws.connectionCompleter?.future; } if (wsConnectionStatus != ConnectionStatus.connected) { throw Exception( - 'You cannot use queryChannels without an active connection.' - ' Please call `connectUser` to connect the client.', + 'You cannot use queryChannels without an active connection. ' + 'Please call `connectUser` to connect the client.', ); } } + if (!_connectionIdManager.hasConnectionId) { + // ignore: parameter_assignments + watch = false; + } + logger.info('Query channel start'); - final defaultOptions = { - 'state': true, - 'watch': true, - 'presence': false, - }; - - final payload = { - 'filter_conditions': filter, - 'sort': sort, - }; - - if (messageLimit != null) { - payload['message_limit'] = messageLimit; - } - - payload.addAll(defaultOptions); - - if (options != null) { - payload.addAll(options); - } - - payload.addAll(paginationParams.toJson()); - - final response = await get( - '/channels', - queryParameters: { - 'payload': jsonEncode(payload), - }, - ); - - final res = decode( - response.data, - QueryChannelsResponse.fromJson, + print('Query Channel Started : ${DateTime.now()}'); + final res = await _chatApi.channel.queryChannels( + filter: filter, + sort: sort, + state: state, + watch: watch, + presence: presence, + memberLimit: memberLimit, + messageLimit: messageLimit, + paginationParams: paginationParams, ); + print('Query Channel Completed : ${DateTime.now()}'); if (res.channels.isEmpty && paginationParams.offset == 0) { - logger.warning( - ''' + logger.warning(''' We could not find any channel for this query. - Please make sure to take a look at the Flutter tutorial: https://getstream.io/chat/flutter/tutorial - If your application already has users and channels, you might need to adjust your query channel as explained in the docs https://getstream.io/chat/docs/query_channels/?language=dart''', - ); + Please make sure to take a look at the Flutter tutorial: https://getstream.io/chat/flutter/tutorial + If your application already has users and channels, you might need to adjust your query channel as explained in the docs https://getstream.io/chat/docs/query_channels/?language=dart + '''); return []; } @@ -758,7 +616,7 @@ class StreamChatClient { .map((it) => it.user) .toList(growable: false); - state._updateUsers(users); + this.state.updateUsers(users); logger.info('Got ${res.channels.length} channels from api'); @@ -770,7 +628,7 @@ class StreamChatClient { clearQueryCache: paginationParams.offset == 0, ); - state.channels = updateData.key; + this.state.channels = updateData.key; return updateData.value; } @@ -812,253 +670,20 @@ class StreamChatClient { return MapEntry(channels, newChannels); } - Object _parseError(DioError error) { - if (error.type == DioErrorType.response) { - final apiError = - ApiError(error.response?.data, error.response?.statusCode); - logger.severe('apiError: ${apiError.toString()}'); - return apiError; - } - - return error; - } - - /// Handy method to make http GET request with error parsing. - Future> get( - String path, { - Map? queryParameters, - }) async { - try { - final response = await httpClient.get( - path, - queryParameters: queryParameters, - ); - return response; - } on DioError catch (error) { - // ignore: only_throw_errors - throw _parseError(error); - } - } - - /// Handy method to make http POST request with error parsing. - Future> post( - String path, { - dynamic data, - ProgressCallback? onSendProgress, - CancelToken? cancelToken, - }) async { - try { - final response = await httpClient.post( - path, - data: data, - onSendProgress: onSendProgress, - cancelToken: cancelToken, - ); - return response; - } on DioError catch (error) { - // ignore: only_throw_errors - throw _parseError(error); - } - } - - /// Handy method to make http DELETE request with error parsing. - Future> delete( - String path, { - Map? queryParameters, - CancelToken? cancelToken, - }) async { - try { - final response = await httpClient.delete( - path, - queryParameters: queryParameters, - cancelToken: cancelToken, - ); - return response; - } on DioError catch (error) { - // ignore: only_throw_errors - throw _parseError(error); - } - } - - /// Handy method to make http PATCH request with error parsing. - Future> patch( - String path, { - Map? queryParameters, - dynamic data, - }) async { - try { - final response = await httpClient.patch( - path, - queryParameters: queryParameters, - data: data, - ); - return response; - } on DioError catch (error) { - // ignore: only_throw_errors - throw _parseError(error); - } - } - - /// Handy method to make http PUT request with error parsing. - Future> put( - String path, { - Map? queryParameters, - dynamic data, - }) async { - try { - final response = await httpClient.put( - path, - queryParameters: queryParameters, - data: data, - ); - return response; - } on DioError catch (error) { - // ignore: only_throw_errors - throw _parseError(error); - } - } - - /// Used to log errors and stacktrace in case of bad json deserialization - T decode(String? j, DecoderFunction decoderFunction) { - try { - final data = j ?? '{}'; - return decoderFunction(json.decode(data)); - } catch (error, stacktrace) { - logger.severe('Error decoding response', error, stacktrace); - rethrow; - } - } - - String get _authType => _anonymous ? 'anonymous' : 'jwt'; - - String get _userAgent => 'stream-chat-dart-client-${CurrentPlatform.name}-' - '${PACKAGE_VERSION.split('+')[0]}'; - - Map get _commonQueryParams => { - 'user_id': state.user?.id, - 'api_key': apiKey, - 'connection_id': _connectionId, - }; - - /// Set the current user with an anonymous id, this triggers a connection to - /// the API. It returns a [Future] that resolves when the connection is setup. - @Deprecated( - 'Use `connectAnonymousUser` instead. Will be removed in Future releases') - Future setAnonymousUser() => connectAnonymousUser(); - - /// Connects the current user with an anonymous id, this triggers a connection - /// to the API. It returns a [Future] that resolves when the connection is - /// setup. - Future connectAnonymousUser() async { - if (_connectCompleter != null && !_connectCompleter!.isCompleted) { - logger.warning('Already connecting'); - throw Exception('Already connecting'); - } - - _connectCompleter = Completer(); - - _anonymous = true; - const uuid = Uuid(); - state.user = OwnUser(id: uuid.v4()); - - return connect().then((event) { - _connectCompleter!.complete(event); - return event; - }).catchError((e, s) { - _connectCompleter!.completeError(e, s); - throw e; - }); - } - - /// Set the current user as guest, this triggers a connection to the API. - /// It returns a [Future] that resolves when the connection is setup. - @Deprecated( - 'Use `connectGuestUser` instead. Will be removed in Future releases') - Future setGuestUser(User user) => connectGuestUser(user); - - /// Connects the current user as guest, this triggers a connection to the API. - /// It returns a [Future] that resolves when the connection is setup. - Future connectGuestUser(User user) async { - _anonymous = true; - final response = await post('/guest', data: {'user': user.toJson()}) - .then((res) => decode( - res.data, ConnectGuestUserResponse.fromJson)) - .whenComplete(() => _anonymous = false); - - return connectUser( - response.user, - response.accessToken, - ); - } - - /// Closes the websocket connection and resets the client - /// If [flushChatPersistence] is true the client deletes all offline - /// user's data. If [clearUser] is true the client unsets the current user - Future disconnect({ - bool flushChatPersistence = false, - bool clearUser = false, - }) async { - logger.info('Disconnecting flushOfflineStorage: $flushChatPersistence; ' - 'clearUser: $clearUser'); - - await _chatPersistenceClient?.disconnect(flush: flushChatPersistence); - _chatPersistenceClient = null; - - _connectCompleter = null; - - if (clearUser == true) { - state.dispose(); - state = ClientState(this); - } - - await _disconnect(); - } - - Future _disconnect() async { - logger.info('Client disconnecting'); - - await _ws.disconnect(); - await _connectionStatusSubscription?.cancel(); - } - /// Requests users with a given query. Future queryUsers({ + bool? presence, Filter? filter, List? sort, - Map? options, PaginationParams? pagination, }) async { - final defaultOptions = { - 'presence': _hasConnectionId, - }; - - final payload = { - 'filter_conditions': filter, - 'sort': sort, - }..addAll(defaultOptions); - - if (pagination != null) { - payload.addAll(pagination.toJson()); - } - - if (options != null) { - payload.addAll(options); - } - - final rawRes = await get( - '/users', - queryParameters: { - 'payload': jsonEncode(payload), - }, + final response = await _chatApi.user.queryUsers( + presence: presence ?? _connectionIdManager.hasConnectionId, + filter: filter, + sort: sort, + pagination: pagination, ); - - final response = decode( - rawRes.data, - QueryUsersResponse.fromJson, - ); - - state._updateUsers(response.users); - + state.updateUsers(response.users); return response; } @@ -1069,34 +694,14 @@ class StreamChatClient { List? sort, PaginationParams? paginationParams, Filter? messageFilters, - }) async { - assert(() { - if (query == null && messageFilters == null) { - throw ArgumentError('Provide at least `query` or `messageFilters`'); - } - if (query != null && messageFilters != null) { - throw ArgumentError( - "Can't provide both `query` and `messageFilters` at the same time", - ); - } - return true; - }(), 'Check incoming params.'); - - final payload = { - 'filter_conditions': filter, - 'message_filter_conditions': messageFilters, - 'query': query, - 'sort': sort, - if (paginationParams != null) ...paginationParams.toJson(), - }.nullProtected; - - final response = await get('/search', queryParameters: { - 'payload': json.encode(payload), - }); - - return decode( - response.data, SearchMessagesResponse.fromJson); - } + }) => + _chatApi.general.searchMessages( + filter, + query: query, + sort: sort, + pagination: paginationParams, + messageFilters: messageFilters, + ); /// Send a [file] to the [channelId] of type [channelType] Future sendFile( @@ -1106,7 +711,7 @@ class StreamChatClient { ProgressCallback? onSendProgress, CancelToken? cancelToken, }) => - attachmentFileUploader!.sendFile( + _chatApi.fileUploader.sendFile( file, channelId, channelType, @@ -1122,7 +727,7 @@ class StreamChatClient { ProgressCallback? onSendProgress, CancelToken? cancelToken, }) => - attachmentFileUploader!.sendImage( + _chatApi.fileUploader.sendImage( image, channelId, channelType, @@ -1137,7 +742,7 @@ class StreamChatClient { String channelType, { CancelToken? cancelToken, }) => - attachmentFileUploader!.deleteFile( + _chatApi.fileUploader.deleteFile( url, channelId, channelType, @@ -1151,44 +756,52 @@ class StreamChatClient { String channelType, { CancelToken? cancelToken, }) => - attachmentFileUploader!.deleteImage( + _chatApi.fileUploader.deleteImage( url, channelId, channelType, cancelToken: cancelToken, ); + /// Replaces the [channelId] of type [ChannelType] data with [data] + Future updateChannel( + String channelId, + String channelType, + Map data, { + Message? message, + }) => + _chatApi.channel.updateChannel( + channelId, + channelType, + data, + message: message, + ); + + /// Updates the [channelId] of type [ChannelType] data with [data] + Future updateChannelPartial( + String channelId, + String channelType, + Map data, + ) => + _chatApi.channel.updateChannelPartial( + channelId, + channelType, + data, + ); + /// Add a device for Push Notifications. - Future addDevice(String id, PushProvider pushProvider) async { - final response = await post('/devices', data: { - 'id': id, - 'push_provider': pushProvider.name, - }); - return decode(response.data, EmptyResponse.fromJson); - } + Future addDevice(String id, PushProvider pushProvider) => + _chatApi.device.addDevice(id, pushProvider); /// Gets a list of user devices. - Future getDevices() async { - final response = await get('/devices'); - return decode( - response.data, ListDevicesResponse.fromJson); - } + Future getDevices() => _chatApi.device.getDevices(); /// Remove a user's device. - Future removeDevice(String id) async { - final response = await delete('/devices', queryParameters: { - 'id': id, - }); - return decode(response.data, EmptyResponse.fromJson); - } + Future removeDevice(String id) => + _chatApi.device.removeDevice(id); /// Get a development token - String devToken(String userId) { - final payload = json.encode({'user_id': userId}); - final payloadBytes = utf8.encode(payload); - final payloadB64 = base64.encode(payloadBytes); - return 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.$payloadB64.devtoken'; - } + String devToken(String userId) => Token.development(userId).rawValue; /// Returns a channel client with the given type, id and custom data. Channel channel( @@ -1202,58 +815,268 @@ class StreamChatClient { return Channel(this, type, id, extraData: extraData); } + /// Creates a new channel + Future createChannel( + String channelType, { + String? channelId, + Map? channelData, + }) => + queryChannel( + channelType, + channelId: channelId, + state: false, + channelData: channelData, + ); + + /// watches the provided channel + Future watchChannel( + String channelType, { + String? channelId, + Map? channelData, + }) => + queryChannel( + channelType, + channelId: channelId, + watch: true, + channelData: channelData, + ); + + /// Query the API, get messages, members or other channel fields + Future queryChannel( + String channelType, { + bool state = true, + bool watch = false, + bool presence = false, + String? channelId, + Map? channelData, + PaginationParams? messagesPagination, + PaginationParams? membersPagination, + PaginationParams? watchersPagination, + }) => + _chatApi.channel.queryChannel( + channelType, + channelId: channelId, + channelData: channelData, + state: state, + watch: watch, + presence: presence, + messagesPagination: messagesPagination, + membersPagination: membersPagination, + watchersPagination: watchersPagination, + ); + + /// Query channel members + Future queryMembers( + String channelType, { + Filter? filter, + String? channelId, + List? members, + List? sort, + PaginationParams? pagination, + }) => + _chatApi.general.queryMembers( + channelType, + channelId: channelId, + filter: filter, + members: members, + sort: sort, + pagination: pagination, + ); + + /// Hides the channel from [queryChannels] for the user + /// until a message is added If [clearHistory] is set to true - all messages + /// will be removed for the user + Future hideChannel( + String channelId, + String channelType, { + bool clearHistory = false, + }) => + _chatApi.channel.hideChannel( + channelId, + channelType, + clearHistory: clearHistory, + ); + + /// Removes the hidden status for the channel + Future showChannel( + String channelId, + String channelType, + ) => + _chatApi.channel.showChannel( + channelId, + channelType, + ); + + /// Delete this channel. Messages are permanently removed. + Future deleteChannel( + String channelId, + String channelType, + ) => + _chatApi.channel.deleteChannel( + channelId, + channelType, + ); + + /// Removes all messages from the channel + Future truncateChannel( + String channelId, + String channelType, + ) => + _chatApi.channel.truncateChannel( + channelId, + channelType, + ); + + /// Mutes the channel + Future muteChannel( + String channelCid, { + Duration? expiration, + }) => + _chatApi.moderation.muteChannel( + channelCid, + expiration: expiration, + ); + + /// Unmutes the channel + Future unmuteChannel(String channelCid) => + _chatApi.moderation.unmuteChannel(channelCid); + + /// Accept invitation to the channel + Future acceptChannelInvite( + String channelId, + String channelType, { + Message? message, + }) => + _chatApi.channel.acceptChannelInvite( + channelId, + channelType, + message: message, + ); + + /// Reject invitation to the channel + Future rejectChannelInvite( + String channelId, + String channelType, { + Message? message, + }) => + _chatApi.channel.rejectChannelInvite( + channelId, + channelType, + message: message, + ); + + /// Add members to the channel + Future addChannelMembers( + String channelId, + String channelType, + List memberIds, { + Message? message, + }) => + _chatApi.channel.addMembers( + channelId, + channelType, + memberIds, + message: message, + ); + + /// Remove members from the channel + Future removeChannelMembers( + String channelId, + String channelType, + List memberIds, { + Message? message, + }) => + _chatApi.channel.removeMembers( + channelId, + channelType, + memberIds, + message: message, + ); + + /// Invite members to the channel + Future inviteChannelMembers( + String channelId, + String channelType, + List memberIds, { + Message? message, + }) => + _chatApi.channel.inviteChannelMembers( + channelId, + channelType, + memberIds, + message: message, + ); + + /// Stop watching the channel + Future stopChannelWatching( + String channelId, + String channelType, + ) => + _chatApi.channel.stopWatching( + channelId, + channelType, + ); + + /// Send action for a specific message of this channel + Future sendAction( + String channelId, + String channelType, + String messageId, + Map formData, + ) => + _chatApi.message.sendAction( + channelId, + channelType, + messageId, + formData, + ); + + /// Mark [channelId] of type [channelType] all messages as read + /// Optionally provide a [messageId] if you want to mark a + /// particular message as read + Future markChannelRead( + String channelId, + String channelType, { + String? messageId, + }) => + _chatApi.channel.markRead( + channelId, + channelType, + messageId: messageId, + ); + /// Update or Create the given user object. - Future updateUser(User user) async => - updateUsers([user]); + Future updateUser(User user) => updateUsers([user]); /// Batch update a list of users - Future updateUsers(List users) async { - final response = await post('/users', data: { - 'users': users.asMap().map((_, u) => MapEntry(u.id, u.toJson())), - }); - return decode( - response.data, - UpdateUsersResponse.fromJson, - ); - } + Future updateUsers(List users) => + _chatApi.user.updateUsers(users); /// Bans a user from all channels Future banUser( - String targetUserID, [ + String targetUserId, [ Map options = const {}, - ]) async { - final data = Map.from(options) - ..addAll({ - 'target_user_id': targetUserID, - }); - final response = await post( - '/moderation/ban', - data: data, - ); - return decode(response.data, EmptyResponse.fromJson); - } + ]) => + _chatApi.moderation.banUser( + targetUserId, + options: options, + ); /// Remove global ban for a user Future unbanUser( - String targetUserID, [ + String targetUserId, [ Map options = const {}, - ]) async { - final data = Map.from(options) - ..addAll({ - 'target_user_id': targetUserID, - }); - final response = await delete( - '/moderation/ban', - queryParameters: data, - ); - return decode(response.data, EmptyResponse.fromJson); - } + ]) => + _chatApi.moderation.unbanUser( + targetUserId, + options: options, + ); /// Shadow bans a user Future shadowBan( String targetID, [ Map options = const {}, - ]) async => + ]) => banUser(targetID, { 'shadow': true, ...options, @@ -1263,124 +1086,166 @@ class StreamChatClient { Future removeShadowBan( String targetID, [ Map options = const {}, - ]) async => + ]) => unbanUser(targetID, { 'shadow': true, ...options, }); /// Mutes a user - Future muteUser(String targetID) async { - final response = await post('/moderation/mute', data: { - 'target_id': targetID, - }); - return decode(response.data, EmptyResponse.fromJson); - } + Future muteUser(String userId) => + _chatApi.moderation.muteUser(userId); /// Unmutes a user - Future unmuteUser(String targetID) async { - final response = await post('/moderation/unmute', data: { - 'target_id': targetID, - }); - return decode(response.data, EmptyResponse.fromJson); - } + Future unmuteUser(String userId) => + _chatApi.moderation.unmuteUser(userId); /// Flag a message - Future flagMessage(String messageID) async { - final response = await post('/moderation/flag', data: { - 'target_message_id': messageID, - }); - return decode(response.data, EmptyResponse.fromJson); - } + Future flagMessage(String messageId) => + _chatApi.moderation.flagMessage(messageId); /// Unflag a message - Future unflagMessage(String messageId) async { - final response = await post('/moderation/unflag', data: { - 'target_message_id': messageId, - }); - return decode(response.data, EmptyResponse.fromJson); - } + Future unflagMessage(String messageId) => + _chatApi.moderation.unflagMessage(messageId); /// Flag a user - Future flagUser(String userId) async { - final response = await post('/moderation/flag', data: { - 'target_user_id': userId, - }); - return decode(response.data, EmptyResponse.fromJson); - } + Future flagUser(String userId) => + _chatApi.moderation.flagUser(userId); /// Unflag a message - Future unflagUser(String userId) async { - final response = await post('/moderation/unflag', data: { - 'target_user_id': userId, - }); - return decode(response.data, EmptyResponse.fromJson); - } + Future unflagUser(String userId) => + _chatApi.moderation.unflagUser(userId); /// Mark all channels for this user as read - Future markAllRead() async { - final response = await post('/channels/read'); - return decode(response.data, EmptyResponse.fromJson); - } + Future markAllRead() => _chatApi.channel.markAllRead(); + + /// Send an event to a particular channel + Future sendEvent( + String channelId, + String channelType, + Event event, + ) => + _chatApi.channel.sendEvent( + channelId, + channelType, + event, + ); + + /// Send a [reactionType] for this [messageId] + /// Set [enforceUnique] to true to remove the existing user reaction + Future sendReaction( + String messageId, + String reactionType, { + Map extraData = const {}, + bool enforceUnique = false, + }) => + _chatApi.message.sendReaction( + messageId, + reactionType, + extraData: extraData, + enforceUnique: enforceUnique, + ); + + /// Delete a [reactionType] from this [messageId] + Future deleteReaction( + String messageId, + String reactionType, + ) => + _chatApi.message.deleteReaction( + messageId, + reactionType, + ); /// Sends the message to the given channel Future sendMessage( Message message, String channelId, String channelType, - ) async { - final response = await post( - '/channels/$channelType/$channelId/message', - data: {'message': message.toJson()}, - ); - return decode(response.data, SendMessageResponse.fromJson); - } + ) => + _chatApi.message.sendMessage( + channelId, + channelType, + message, + ); + + /// Lists all the message replies for the [parentId] + Future getReplies( + String parentId, + PaginationParams options, + ) => + _chatApi.message.getReplies( + parentId, + options, + ); + + /// Get all the reactions for a [messageId] + Future getReactions( + String messageId, + PaginationParams options, + ) => + _chatApi.message.getReactions( + messageId, + options, + ); /// Update the given message - Future updateMessage(Message message) async { - final response = await post( - '/messages/${message.id}', - data: {'message': message.toJson()}, - ); - return decode(response.data, UpdateMessageResponse.fromJson); - } + Future updateMessage(Message message) => + _chatApi.message.updateMessage(message); /// Deletes the given message - Future deleteMessage(Message message) async { - final response = await delete('/messages/${message.id}'); - return decode(response.data, EmptyResponse.fromJson); - } + Future deleteMessage(String messageId) => + _chatApi.message.deleteMessage(messageId); - /// Get a message by id - Future getMessage(String messageId) async { - final response = await get('/messages/$messageId'); - return decode(response.data, GetMessageResponse.fromJson); - } + /// Get a message by [messageId] + Future getMessage(String messageId) => + _chatApi.message.getMessage(messageId); + + /// Retrieves a list of messages by [messageIDs] + /// from the given [channelId] of type [channelType] + Future getMessagesById( + String channelId, + String channelType, + List messageIDs, + ) => + _chatApi.message.getMessagesById( + channelId, + channelType, + messageIDs, + ); + + /// Translates the [messageId] in provided [language] + Future translateMessage( + String messageId, + String language, + ) => + _chatApi.message.translateMessage( + messageId, + language, + ); /// Pins provided message /// [timeoutOrExpirationDate] can either be a [DateTime] or a value in seconds /// to be added to [DateTime.now] Future pinMessage( - Message message, - Object timeoutOrExpirationDate, - ) { + Message message, { + Object? /*num|DateTime*/ timeoutOrExpirationDate, + }) { assert(() { if (timeoutOrExpirationDate is! DateTime && + timeoutOrExpirationDate != null && timeoutOrExpirationDate is! num) { throw ArgumentError('Invalid timeout or Expiration date'); } return true; - }(), 'Check whether time out is valid'); + }(), 'Check for invalid timeout or expiration date'); DateTime? pinExpires; if (timeoutOrExpirationDate is DateTime) { - pinExpires = timeoutOrExpirationDate.toUtc(); + pinExpires = timeoutOrExpirationDate; } else if (timeoutOrExpirationDate is num) { - pinExpires = DateTime.now() - .add( - Duration(seconds: timeoutOrExpirationDate.toInt()), - ) - .toUtc(); + pinExpires = DateTime.now().add( + Duration(seconds: timeoutOrExpirationDate.toInt()), + ); } return updateMessage( message.copyWith(pinned: true, pinExpires: pinExpires), @@ -1390,6 +1255,39 @@ class StreamChatClient { /// Unpins provided message Future unpinMessage(Message message) => updateMessage(message.copyWith(pinned: false)); + + /// Closes the [_ws] connection and resets the [state] + /// If [flushChatPersistence] is true the client deletes all offline + /// user's data. + Future disconnectUser({bool flushChatPersistence = false}) async { + // resetting state + state.dispose(); + state = ClientState(this); + + // resetting credentials + _tokenManager.reset(); + _connectionIdManager.reset(); + + // disconnecting persistence client + await _chatPersistenceClient?.disconnect(flush: flushChatPersistence); + _chatPersistenceClient = null; + + // closing web-socket connection + closeConnection(); + } + + /// Call this function to dispose the client + Future dispose() async { + state.dispose(); + await _eventController.close(); + await _wsConnectionStatusController.close(); + + // disconnecting persistence client + await _chatPersistenceClient?.disconnect(); + + // closing web-socket connection + closeConnection(); + } } /// The class that handles the state of the channel listening to the events @@ -1453,7 +1351,7 @@ class ClientState { if (event.user!.id == user!.id) { user = OwnUser.fromJson(event.user!.toJson()); } - _updateUser(event.user); + updateUser(event.user); })); } @@ -1478,15 +1376,16 @@ class ClientState { _userController.add(user); } - void _updateUsers(List userList) { + void updateUsers(List userList) { final newUsers = { ...users, - for (var user in userList) user!.id: user, + for (var user in userList) + if (user != null) user.id: user, }; _usersController.add(newUsers); } - void _updateUser(User? user) => _updateUsers([user]); + void updateUser(User? user) => updateUsers([user]); /// The current user OwnUser? get user => _userController.value; @@ -1495,10 +1394,10 @@ class ClientState { Stream get userStream => _userController.stream; /// The current user - Map get users => _usersController.value!; + Map get users => _usersController.value!; /// The current user as a stream - Stream> get usersStream => _usersController.stream; + Stream> get usersStream => _usersController.stream; /// The current unread channels count int? get unreadChannels => _unreadChannelsController.value; @@ -1513,23 +1412,21 @@ class ClientState { Stream get totalUnreadCountStream => _totalUnreadCountController.stream; /// The current list of channels in memory as a stream - Stream?> get channelsStream => - _channelsController.stream; + Stream> get channelsStream => _channelsController.stream; /// The current list of channels in memory Map get channels => _channelsController.value!; - set channels(Map v) { - _channelsController.add(v); + set channels(Map channelMap) { + final newChannels = {...channels, ...channelMap}; + _channelsController.add(newChannels); } - final BehaviorSubject> _channelsController = - BehaviorSubject.seeded({}); - final BehaviorSubject _userController = BehaviorSubject(); - final BehaviorSubject> _usersController = - BehaviorSubject.seeded({}); - final BehaviorSubject _unreadChannelsController = BehaviorSubject(); - final BehaviorSubject _totalUnreadCountController = BehaviorSubject(); + final _channelsController = BehaviorSubject>.seeded({}); + final _userController = BehaviorSubject(); + final _usersController = BehaviorSubject>.seeded({}); + final _unreadChannelsController = BehaviorSubject(); + final _totalUnreadCountController = BehaviorSubject(); /// Call this method to dispose this object void dispose() { diff --git a/packages/stream_chat/lib/src/attachment_file_uploader.dart b/packages/stream_chat/lib/src/core/api/attachment_file_uploader.dart similarity index 90% rename from packages/stream_chat/lib/src/attachment_file_uploader.dart rename to packages/stream_chat/lib/src/core/api/attachment_file_uploader.dart index d00dd4e7..272188af 100644 --- a/packages/stream_chat/lib/src/attachment_file_uploader.dart +++ b/packages/stream_chat/lib/src/core/api/attachment_file_uploader.dart @@ -1,8 +1,8 @@ import 'package:dio/dio.dart'; -import 'package:stream_chat/src/api/responses.dart'; -import 'package:stream_chat/src/client.dart'; +import 'package:stream_chat/src/core/api/responses.dart'; +import 'package:stream_chat/src/core/http/stream_http_client.dart'; import 'package:stream_chat/src/extensions/string_extension.dart'; -import 'package:stream_chat/src/models/attachment_file.dart'; +import 'package:stream_chat/src/core/models/attachment_file.dart'; /// Class responsible for uploading images and files to a given channel abstract class AttachmentFileUploader { @@ -60,7 +60,7 @@ class StreamAttachmentFileUploader implements AttachmentFileUploader { /// Creates a new [StreamAttachmentFileUploader] instance. const StreamAttachmentFileUploader(this._client); - final StreamChatClient _client; + final StreamHttpClient _client; @override Future sendImage( @@ -96,7 +96,7 @@ class StreamAttachmentFileUploader implements AttachmentFileUploader { onSendProgress: onSendProgress, cancelToken: cancelToken, ); - return _client.decode(response.data, SendImageResponse.fromJson); + return SendImageResponse.fromJson(response.data); } @override @@ -133,7 +133,7 @@ class StreamAttachmentFileUploader implements AttachmentFileUploader { onSendProgress: onSendProgress, cancelToken: cancelToken, ); - return _client.decode(response.data, SendFileResponse.fromJson); + return SendFileResponse.fromJson(response.data); } @override @@ -148,7 +148,7 @@ class StreamAttachmentFileUploader implements AttachmentFileUploader { queryParameters: {'url': url}, cancelToken: cancelToken, ); - return _client.decode(response.data, EmptyResponse.fromJson); + return EmptyResponse.fromJson(response.data); } @override @@ -163,6 +163,6 @@ class StreamAttachmentFileUploader implements AttachmentFileUploader { queryParameters: {'url': url}, cancelToken: cancelToken, ); - return _client.decode(response.data, EmptyResponse.fromJson); + return EmptyResponse.fromJson(response.data); } } diff --git a/packages/stream_chat/lib/src/core/api/channel_api.dart b/packages/stream_chat/lib/src/core/api/channel_api.dart new file mode 100644 index 00000000..7407bcb5 --- /dev/null +++ b/packages/stream_chat/lib/src/core/api/channel_api.dart @@ -0,0 +1,293 @@ +import 'dart:convert'; + +import 'package:stream_chat/src/core/api/requests.dart'; +import 'package:stream_chat/src/core/api/responses.dart'; +import 'package:stream_chat/src/core/http/stream_http_client.dart'; +import 'package:stream_chat/src/core/models/channel_model.dart'; +import 'package:stream_chat/src/core/models/channel_state.dart'; +import 'package:stream_chat/src/core/models/event.dart'; +import 'package:stream_chat/src/core/models/filter.dart'; +import 'package:stream_chat/src/core/models/message.dart'; + +/// +class ChannelApi { + /// + ChannelApi(this._client); + + final StreamHttpClient _client; + + String _getChannelUrl(String channelId, String channelType) => + '/channels/$channelType/$channelId'; + + /// Query the API, get messages, members or other channel fields + Future queryChannel( + String channelType, { + bool state = true, + bool watch = false, + bool presence = false, + String? channelId, + Map? channelData, + PaginationParams? messagesPagination, + PaginationParams? membersPagination, + PaginationParams? watchersPagination, + }) async { + var channelPath = '/channels/$channelType'; + if (channelId != null) channelPath = '$channelPath/$channelId'; + final response = await _client.post( + '$channelPath/query', + data: { + 'state': state, + 'watch': watch, + 'presence': presence, + if (channelData != null) 'data': channelData, + if (messagesPagination != null) 'messages': messagesPagination, + if (membersPagination != null) 'members': membersPagination, + if (watchersPagination != null) 'watchers': watchersPagination, + }, + ); + return ChannelState.fromJson(response.data); + } + + /// Requests channels with a given query from the API. + Future queryChannels({ + Filter? filter, + List>? sort, + int? memberLimit, + int? messageLimit, + bool state = true, + bool watch = true, + bool presence = false, + PaginationParams paginationParams = const PaginationParams(), + }) async { + print('Query Channel Started 2 : ${DateTime.now()}'); + final response = await _client.get( + '/channels', + queryParameters: { + 'payload': jsonEncode({ + // default options + 'state': state, + 'watch': watch, + 'presence': presence, + + // passed options + if (sort != null) 'sort': sort, + if (filter != null) 'filter_conditions': filter, + if (memberLimit != null) 'member_limit': memberLimit, + if (messageLimit != null) 'message_limit': messageLimit, + + // pagination + ...paginationParams.toJson() + }) + }, + ); + print('Query Channel Completed 2 : ${DateTime.now()}'); + return QueryChannelsResponse.fromJson(response.data); + } + + /// Mark all channels for this user as read + Future markAllRead() async { + final response = await _client.post('channels/read'); + return EmptyResponse.fromJson(response.data); + } + + /// Replaces the [channelId] of type [ChannelType] data with [data] + Future updateChannel( + String channelId, + String channelType, + Map data, { + Message? message, + }) async { + final response = await _client.post( + _getChannelUrl(channelId, channelType), + data: { + 'data': data, + if (message != null) + 'message': message.copyWith(updatedAt: DateTime.now()), + }, + ); + return UpdateChannelResponse.fromJson(response.data); + } + + /// Updates the [channelId] of type [ChannelType] data with [data] + Future updateChannelPartial( + String channelId, + String channelType, + Map data, + ) async { + final response = await _client.patch( + _getChannelUrl(channelId, channelType), + data: data, + ); + return PartialUpdateChannelResponse.fromJson(response.data); + } + + /// Accept invitation to the channel + Future acceptChannelInvite( + String channelId, + String channelType, { + Message? message, + }) async { + final response = await _client.post( + _getChannelUrl(channelId, channelType), + data: { + 'accept_invite': true, + 'message': message, + }, + ); + return AcceptInviteResponse.fromJson(response.data); + } + + /// Reject invitation to the channel + Future rejectChannelInvite( + String channelId, + String channelType, { + Message? message, + }) async { + final response = await _client.post( + _getChannelUrl(channelId, channelType), + data: { + 'reject_invite': true, + 'message': message, + }, + ); + return RejectInviteResponse.fromJson(response.data); + } + + /// Invite members to the channel + Future inviteChannelMembers( + String channelId, + String channelType, + List memberIds, { + Message? message, + }) async { + final response = await _client.post( + _getChannelUrl(channelId, channelType), + data: { + 'invites': memberIds, + 'message': message, + }, + ); + return InviteMembersResponse.fromJson(response.data); + } + + /// Add members to the channel + Future addMembers( + String channelId, + String channelType, + List memberIds, { + Message? message, + }) async { + final response = await _client.post( + _getChannelUrl(channelId, channelType), + data: { + 'add_members': memberIds, + 'message': message, + }, + ); + return AddMembersResponse.fromJson(response.data); + } + + /// Remove members from the channel + Future removeMembers( + String channelId, + String channelType, + List memberIds, { + Message? message, + }) async { + final response = await _client.post( + _getChannelUrl(channelId, channelType), + data: { + 'remove_members': memberIds, + 'message': message, + }, + ); + return RemoveMembersResponse.fromJson(response.data); + } + + /// Send an event on this channel + Future sendEvent( + String channelId, + String channelType, + Event event, + ) async { + final response = await _client.post( + '${_getChannelUrl(channelId, channelType)}/event', + data: {'event': event}, + ); + return EmptyResponse.fromJson(response.data); + } + + /// Delete this channel. Messages are permanently removed. + Future deleteChannel( + String channelId, + String channelType, + ) async { + final response = await _client.delete( + _getChannelUrl(channelId, channelType), + ); + return EmptyResponse.fromJson(response.data); + } + + /// Removes all messages from the channel + Future truncateChannel( + String channelId, + String channelType, + ) async { + final response = await _client.post( + '${_getChannelUrl(channelId, channelType)}/truncate', + ); + return EmptyResponse.fromJson(response.data); + } + + /// Hides the channel from [StreamChatClient.queryChannels] for the user + /// until a message is added If [clearHistory] is set to true - all messages + /// will be removed for the user + Future hideChannel( + String channelId, + String channelType, { + bool clearHistory = false, + }) async { + final response = await _client.post( + '${_getChannelUrl(channelId, channelType)}/hide', + data: {'clear_history': clearHistory}, + ); + return EmptyResponse.fromJson(response.data); + } + + /// Removes the hidden status for the channel + Future showChannel( + String channelId, + String channelType, + ) async { + final response = await _client.post( + '${_getChannelUrl(channelId, channelType)}/show', + ); + return EmptyResponse.fromJson(response.data); + } + + /// Mark [channelId] of type [channelType] all messages as read + /// Optionally provide a [messageId] if you want to mark a + /// particular message as read + Future markRead( + String channelId, + String channelType, { + String? messageId, + }) async { + final response = await _client.post( + '${_getChannelUrl(channelId, channelType)}/read', + data: {if (messageId != null) 'message_id': messageId}, + ); + return EmptyResponse.fromJson(response.data); + } + + /// Stop watching the channel + Future stopWatching( + String channelId, + String channelType, + ) async { + final response = await _client.post( + '${_getChannelUrl(channelId, channelType)}/stop-watching', + ); + return EmptyResponse.fromJson(response.data); + } +} diff --git a/packages/stream_chat/lib/src/core/api/device_api.dart b/packages/stream_chat/lib/src/core/api/device_api.dart new file mode 100644 index 00000000..2d5e0fea --- /dev/null +++ b/packages/stream_chat/lib/src/core/api/device_api.dart @@ -0,0 +1,59 @@ +import 'package:stream_chat/src/core/api/responses.dart'; +import 'package:stream_chat/src/core/http/stream_http_client.dart'; + +/// Provider used to send push notifications. +enum PushProvider { + /// Send notifications using Google's Firebase Cloud Messaging + firebase, + + /// Send notifications using Apple's Push Notification service + apn +} + +extension on PushProvider { + /// Returns the string notion for [PushProvider]. + String get name => { + PushProvider.apn: 'apn', + PushProvider.firebase: 'firebase', + }[this]!; +} + +/// +class DeviceApi { + /// + DeviceApi(this._client); + + final StreamHttpClient _client; + + /// Add a device for Push Notifications. + Future addDevice( + String deviceId, + PushProvider pushProvider, + ) async { + final response = await _client.post( + '/devices', + data: { + 'id': deviceId, + 'push_provider': pushProvider.name, + }, + ); + return EmptyResponse.fromJson(response.data); + } + + /// Gets a list of user devices. + Future getDevices() async { + final response = await _client.get('/devices'); + return ListDevicesResponse.fromJson(response.data); + } + + /// Remove a user's device. + Future removeDevice( + String deviceId, + ) async { + final response = await _client.delete( + '/devices', + queryParameters: {'id': deviceId}, + ); + return EmptyResponse.fromJson(response.data); + } +} diff --git a/packages/stream_chat/lib/src/core/api/general_api.dart b/packages/stream_chat/lib/src/core/api/general_api.dart new file mode 100644 index 00000000..d26ffc7d --- /dev/null +++ b/packages/stream_chat/lib/src/core/api/general_api.dart @@ -0,0 +1,94 @@ +import 'dart:convert'; + +import 'package:stream_chat/src/core/api/requests.dart'; +import 'package:stream_chat/src/core/api/responses.dart'; +import 'package:stream_chat/src/core/http/stream_http_client.dart'; +import 'package:stream_chat/src/core/models/filter.dart'; +import 'package:stream_chat/src/core/models/member.dart'; + +/// +class GeneralApi { + /// + GeneralApi(this._client); + + final StreamHttpClient _client; + + /// Get all the missed events + Future sync( + List cids, + DateTime lastSyncAt, + ) async { + final response = await _client.post( + '/sync', + data: { + 'channel_cids': cids, + 'last_sync_at': lastSyncAt.toUtc().toIso8601String(), + }, + ); + return SyncResponse.fromJson(response.data); + } + + /// A message search. + Future searchMessages( + Filter filter, { + String? query, + List? sort, + PaginationParams? pagination, + Filter? messageFilters, + }) async { + assert(() { + if (query == null && messageFilters == null) { + throw ArgumentError('Provide at least `query` or `messageFilters`'); + } + if (query != null && messageFilters != null) { + throw ArgumentError( + "Can't provide both `query` and `messageFilters` at the same time", + ); + } + return true; + }(), 'Check incoming params.'); + + final response = await _client.get( + '/search', + queryParameters: { + 'payload': jsonEncode({ + 'filter_conditions': filter, + if (sort != null) 'sort': sort, + if (query != null) 'query': query, + if (messageFilters != null) + 'message_filter_conditions': messageFilters, + if (pagination != null) ...pagination.toJson(), + }), + }, + ); + + return SearchMessagesResponse.fromJson(response.data); + } + + /// Query channel members + Future queryMembers( + String channelType, { + Filter? filter, + String? channelId, + List? members, + List? sort, + PaginationParams? pagination, + }) async { + final response = await _client.get( + '/members', + queryParameters: { + 'payload': jsonEncode({ + 'type': channelType, + if (channelId != null) + 'id': channelId + else if (members != null) + 'members': members, + if (sort != null) 'sort': sort, + if (filter != null) 'filter': filter, + if (pagination != null) ...pagination.toJson(), + }), + }, + ); + return QueryMembersResponse.fromJson(response.data); + } +} diff --git a/packages/stream_chat/lib/src/core/api/guest_api.dart b/packages/stream_chat/lib/src/core/api/guest_api.dart new file mode 100644 index 00000000..438a637f --- /dev/null +++ b/packages/stream_chat/lib/src/core/api/guest_api.dart @@ -0,0 +1,20 @@ +import 'package:stream_chat/src/core/api/responses.dart'; +import 'package:stream_chat/src/core/http/stream_http_client.dart'; +import 'package:stream_chat/src/core/models/user.dart'; + +/// +class GuestApi { + /// + GuestApi(this._client); + + final StreamHttpClient _client; + + /// + Future getGuestUser(User user) async { + final response = await _client.post( + '/guest', + data: {'user': user}, + ); + return ConnectGuestUserResponse.fromJson(response.data); + } +} diff --git a/packages/stream_chat/lib/src/core/api/message_api.dart b/packages/stream_chat/lib/src/core/api/message_api.dart new file mode 100644 index 00000000..eccd2f44 --- /dev/null +++ b/packages/stream_chat/lib/src/core/api/message_api.dart @@ -0,0 +1,160 @@ +import 'package:stream_chat/src/core/api/requests.dart'; +import 'package:stream_chat/src/core/api/responses.dart'; +import 'package:stream_chat/src/core/http/stream_http_client.dart'; +import 'package:stream_chat/src/core/models/message.dart'; + +/// +class MessageApi { + /// + MessageApi(this._client); + + final StreamHttpClient _client; + + /// Sends the [message] to the given [channelId] of given [channelType] + Future sendMessage( + String channelId, + String channelType, + Message message, + ) async { + final response = await _client.post( + '/channels/$channelType/$channelId/message', + data: {'message': message}, + ); + return SendMessageResponse.fromJson(response.data); + } + + /// Retrieves a list of messages by [messageIDs] + /// from the given [channelId] of type [channelType] + Future getMessagesById( + String channelId, + String channelType, + List messageIDs, + ) async { + final response = await _client.get( + '/channels/$channelType/$channelId/messages', + queryParameters: {'ids': messageIDs.join(',')}, + ); + return GetMessagesByIdResponse.fromJson(response.data); + } + + /// Get a message by [messageId] + Future getMessage(String messageId) async { + final response = await _client.get( + '/messages/$messageId', + ); + return GetMessageResponse.fromJson(response.data); + } + + /// Updates the given [message] + Future updateMessage( + Message message, + ) async { + final response = await _client.post( + '/messages/${message.id}', + data: {'message': message}, + ); + return UpdateMessageResponse.fromJson(response.data); + } + + /// Deletes the given [messageId] + Future deleteMessage( + String messageId, + ) async { + final response = await _client.delete( + '/messages/$messageId', + ); + return EmptyResponse.fromJson(response.data); + } + + /// Send action for a specific [messageId] + /// of the given [channelId] of given [channelType] + Future sendAction( + String channelId, + String channelType, + String messageId, + Map formData, + ) async { + final response = await _client.post( + '/messages/$messageId/action', + data: { + 'id': channelId, + 'type': channelType, + 'form_data': formData, + 'message_id': messageId, + }, + ); + return SendActionResponse.fromJson(response.data); + } + + /// Send a [reactionType] for this [messageId] + /// Set [enforceUnique] to true to remove the existing user reaction + Future sendReaction( + String messageId, + String reactionType, { + Map extraData = const {}, + bool enforceUnique = false, + }) async { + final reaction = Map.from(extraData) + ..addAll({'type': reactionType}); + + final response = await _client.post( + '/messages/$messageId/reaction', + data: { + 'reaction': reaction, + 'enforce_unique': enforceUnique, + }, + ); + return SendReactionResponse.fromJson(response.data); + } + + /// Delete a [reactionType] from this [messageId] + Future deleteReaction( + String messageId, + String reactionType, + ) async { + final response = await _client.delete( + '/messages/$messageId/reaction/$reactionType', + ); + return EmptyResponse.fromJson(response.data); + } + + /// Get all the reactions for a [messageId] + Future getReactions( + String messageId, + PaginationParams options, + ) async { + final response = await _client.get( + '/messages/$messageId/reactions', + queryParameters: { + ...options.toJson(), + }, + ); + return QueryReactionsResponse.fromJson(response.data); + } + + /// Translates the [messageId] in provided [language] + Future translateMessage( + String messageId, + String language, + ) async { + final response = await _client.post( + '/messages/$messageId/translate', + data: {'language': language}, + ); + return TranslateMessageResponse.fromJson(response.data); + } + + /// Lists all the message replies for the [parentId] + Future getReplies( + String parentId, + PaginationParams options, + ) async { + final response = await _client.get( + '/messages/$parentId/replies', + queryParameters: { + ...options.toJson(), + }, + ); + return QueryRepliesResponse.fromJson(response.data); + } +} diff --git a/packages/stream_chat/lib/src/core/api/moderation_api.dart b/packages/stream_chat/lib/src/core/api/moderation_api.dart new file mode 100644 index 00000000..22c58c35 --- /dev/null +++ b/packages/stream_chat/lib/src/core/api/moderation_api.dart @@ -0,0 +1,128 @@ +import 'package:stream_chat/src/core/api/responses.dart'; +import 'package:stream_chat/src/core/http/stream_http_client.dart'; + +/// +class ModerationApi { + /// + ModerationApi(this._client); + + final StreamHttpClient _client; + + /// Mutes a user + Future muteUser(String userId) async { + final response = await _client.post( + '/moderation/mute', + data: {'target_id': userId}, + ); + return EmptyResponse.fromJson(response.data); + } + + /// Unmutes a user + Future unmuteUser(String userId) async { + final response = await _client.post( + '/moderation/unmute', + data: {'target_id': userId}, + ); + return EmptyResponse.fromJson(response.data); + } + + /// Mutes the channel + Future muteChannel( + String channelCid, { + Duration? expiration, + }) async { + final response = await _client.post( + '/moderation/mute/channel', + data: { + 'channel_cid': channelCid, + if (expiration != null) 'expiration': expiration.inMilliseconds, + }, + ); + return EmptyResponse.fromJson(response.data); + } + + /// Unmutes the channel + Future unmuteChannel( + String channelCid, + ) async { + final response = await _client.post( + '/moderation/unmute/channel', + data: {'channel_cid': channelCid}, + ); + return EmptyResponse.fromJson(response.data); + } + + /// Flag a message + Future flagMessage( + String messageId, + ) async { + final response = await _client.post( + '/moderation/flag', + data: {'target_message_id': messageId}, + ); + return EmptyResponse.fromJson(response.data); + } + + /// Unflag a message + Future unflagMessage( + String messageId, + ) async { + final response = await _client.post( + '/moderation/unflag', + data: {'target_message_id': messageId}, + ); + return EmptyResponse.fromJson(response.data); + } + + /// Flag a user + Future flagUser( + String userId, + ) async { + final response = await _client.post( + '/moderation/flag', + data: {'target_user_id': userId}, + ); + return EmptyResponse.fromJson(response.data); + } + + /// Unflag a user + Future unflagUser( + String userId, + ) async { + final response = await _client.post( + '/moderation/unflag', + data: {'target_user_id': userId}, + ); + return EmptyResponse.fromJson(response.data); + } + + /// Bans a user from all channels + Future banUser( + String targetUserId, { + Map? options, + }) async { + final response = await _client.post( + '/moderation/ban', + data: { + 'target_user_id': targetUserId, + if (options != null) ...options, + }, + ); + return EmptyResponse.fromJson(response.data); + } + + /// Remove global ban for a user + Future unbanUser( + String targetUserId, { + Map? options, + }) async { + final response = await _client.delete( + '/moderation/ban', + queryParameters: { + 'target_user_id': targetUserId, + if (options != null) ...options, + }, + ); + return EmptyResponse.fromJson(response.data); + } +} diff --git a/packages/stream_chat/lib/src/api/requests.dart b/packages/stream_chat/lib/src/core/api/requests.dart similarity index 77% rename from packages/stream_chat/lib/src/api/requests.dart rename to packages/stream_chat/lib/src/core/api/requests.dart index e78a80ec..29112d72 100644 --- a/packages/stream_chat/lib/src/api/requests.dart +++ b/packages/stream_chat/lib/src/core/api/requests.dart @@ -1,9 +1,10 @@ +import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; part 'requests.g.dart'; /// Sorting options -@JsonSerializable(createFactory: false) +@JsonSerializable(includeIfNull: false) class SortOption { /// Creates a new SortOption instance /// @@ -18,6 +19,10 @@ class SortOption { this.comparator, }); + /// Create a new instance from a json + factory SortOption.fromJson(Map json) => + _$SortOptionFromJson(json); + /// Ascending order // ignore: constant_identifier_names static const ASC = 1; @@ -41,8 +46,8 @@ class SortOption { } /// Pagination options. -@JsonSerializable(createFactory: false, includeIfNull: false) -class PaginationParams { +@JsonSerializable(includeIfNull: false) +class PaginationParams extends Equatable { /// Creates a new PaginationParams instance /// /// For example: @@ -62,6 +67,10 @@ class PaginationParams { this.lessThanOrEqual, }); + /// Create a new instance from a json + factory PaginationParams.fromJson(Map json) => + _$PaginationParamsFromJson(json); + /// The amount of items requested from the APIs. final int limit; @@ -106,24 +115,12 @@ class PaginationParams { ); @override - @JsonKey(ignore: true) - 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; + List get props => [ + limit, + offset, + greaterThan, + greaterThanOrEqual, + lessThan, + lessThanOrEqual, + ]; } diff --git a/packages/stream_chat/lib/src/api/requests.g.dart b/packages/stream_chat/lib/src/core/api/requests.g.dart similarity index 63% rename from packages/stream_chat/lib/src/api/requests.g.dart rename to packages/stream_chat/lib/src/core/api/requests.g.dart index 1ec7b5fc..6cd935b8 100644 --- a/packages/stream_chat/lib/src/api/requests.g.dart +++ b/packages/stream_chat/lib/src/core/api/requests.g.dart @@ -6,12 +6,30 @@ part of 'requests.dart'; // JsonSerializableGenerator // ************************************************************************** +SortOption _$SortOptionFromJson(Map json) { + return SortOption( + json['field'] as String, + direction: json['direction'] as int, + ); +} + Map _$SortOptionToJson(SortOption instance) => { 'field': instance.field, 'direction': instance.direction, }; +PaginationParams _$PaginationParamsFromJson(Map json) { + return PaginationParams( + limit: json['limit'] as int, + offset: json['offset'] as int, + greaterThan: json['id_gt'] as String?, + greaterThanOrEqual: json['id_gte'] as String?, + lessThan: json['id_lt'] as String?, + lessThanOrEqual: json['id_lte'] as String?, + ); +} + Map _$PaginationParamsToJson(PaginationParams instance) { final val = { 'limit': instance.limit, diff --git a/packages/stream_chat/lib/src/api/responses.dart b/packages/stream_chat/lib/src/core/api/responses.dart similarity index 90% rename from packages/stream_chat/lib/src/api/responses.dart rename to packages/stream_chat/lib/src/core/api/responses.dart index 92493e8a..c639a960 100644 --- a/packages/stream_chat/lib/src/api/responses.dart +++ b/packages/stream_chat/lib/src/core/api/responses.dart @@ -1,14 +1,15 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/client.dart'; -import 'package:stream_chat/src/models/channel_model.dart'; -import 'package:stream_chat/src/models/channel_state.dart'; -import 'package:stream_chat/src/models/device.dart'; -import 'package:stream_chat/src/models/event.dart'; -import 'package:stream_chat/src/models/member.dart'; -import 'package:stream_chat/src/models/message.dart'; -import 'package:stream_chat/src/models/reaction.dart'; -import 'package:stream_chat/src/models/read.dart'; -import 'package:stream_chat/src/models/user.dart'; +import 'package:stream_chat/src/core/models/channel_model.dart'; +import 'package:stream_chat/src/core/models/channel_state.dart'; +import 'package:stream_chat/src/core/models/device.dart'; +import 'package:stream_chat/src/core/models/event.dart'; +import 'package:stream_chat/src/core/models/member.dart'; +import 'package:stream_chat/src/core/models/message.dart'; +import 'package:stream_chat/src/core/models/reaction.dart'; +import 'package:stream_chat/src/core/models/read.dart'; +import 'package:stream_chat/src/core/models/user.dart'; +import 'package:stream_chat/src/errors/stream_chat_error.dart'; part 'responses.g.dart'; @@ -16,7 +17,34 @@ class _BaseResponse { String? duration; } -/// Model response for [StreamChatClient.resync] api call +/// Model response for [StreamChatNetworkError] data +@JsonSerializable(createToJson: false) +class ErrorResponse extends _BaseResponse { + /// + int? code; + + /// + String? message; + + /// + @JsonKey(name: 'StatusCode') + int? statusCode; + + /// + String? moreInfo; + + /// Create a new instance from a json + static ErrorResponse fromJson(Map json) => + _$ErrorResponseFromJson(json); + + @override + String toString() => 'ErrorResponse(code: $code, ' + 'message: $message, ' + 'statusCode: $statusCode, ' + 'moreInfo: $moreInfo)'; +} + +/// Model response for [StreamChatClient.sync] api call @JsonSerializable(createToJson: false) class SyncResponse extends _BaseResponse { /// The list of events diff --git a/packages/stream_chat/lib/src/api/responses.g.dart b/packages/stream_chat/lib/src/core/api/responses.g.dart similarity index 97% rename from packages/stream_chat/lib/src/api/responses.g.dart rename to packages/stream_chat/lib/src/core/api/responses.g.dart index 2358593d..c1c1f302 100644 --- a/packages/stream_chat/lib/src/api/responses.g.dart +++ b/packages/stream_chat/lib/src/core/api/responses.g.dart @@ -6,6 +6,15 @@ part of 'responses.dart'; // JsonSerializableGenerator // ************************************************************************** +ErrorResponse _$ErrorResponseFromJson(Map json) { + return ErrorResponse() + ..duration = json['duration'] as String? + ..code = json['code'] as int? + ..message = json['message'] as String? + ..statusCode = json['StatusCode'] as int? + ..moreInfo = json['more_info'] as String?; +} + SyncResponse _$SyncResponseFromJson(Map json) { return SyncResponse() ..duration = json['duration'] as String? diff --git a/packages/stream_chat/lib/src/core/api/stream_chat_api.dart b/packages/stream_chat/lib/src/core/api/stream_chat_api.dart new file mode 100644 index 00000000..13342160 --- /dev/null +++ b/packages/stream_chat/lib/src/core/api/stream_chat_api.dart @@ -0,0 +1,79 @@ +import 'package:logging/logging.dart'; +import 'package:stream_chat/src/core/api/channel_api.dart'; +import 'package:stream_chat/src/core/api/device_api.dart'; +import 'package:stream_chat/src/core/api/general_api.dart'; +import 'package:stream_chat/src/core/api/guest_api.dart'; +import 'package:stream_chat/src/core/api/message_api.dart'; +import 'package:stream_chat/src/core/api/moderation_api.dart'; +import 'package:stream_chat/src/core/http/connection_id_manager.dart'; +import 'package:stream_chat/src/core/http/stream_http_client.dart'; +import 'package:stream_chat/src/core/api/user_api.dart'; +import 'package:stream_chat/src/core/http/token_manager.dart'; +import 'package:stream_chat/src/core/api/attachment_file_uploader.dart'; + +export 'device_api.dart' show PushProvider; + +/// +class StreamChatApi { + /// + StreamChatApi( + String apiKey, { + StreamHttpClient? client, + StreamHttpClientOptions? options, + TokenManager? tokenManager, + ConnectionIdManager? connectionIdManager, + AttachmentFileUploader? attachmentFileUploader, + Logger? logger, + }) : _fileUploader = attachmentFileUploader, + _client = client ?? + StreamHttpClient( + apiKey, + options: options, + tokenManager: tokenManager, + connectionIdManager: connectionIdManager, + logger: logger, + ); + + final StreamHttpClient _client; + + UserApi? _user; + + /// + UserApi get user => _user ??= UserApi(_client); + + GuestApi? _guest; + + /// + GuestApi get guest => _guest ??= GuestApi(_client); + + MessageApi? _message; + + /// + MessageApi get message => _message ??= MessageApi(_client); + + ChannelApi? _channel; + + /// + ChannelApi get channel => _channel ??= ChannelApi(_client); + + DeviceApi? _device; + + /// + DeviceApi get device => _device ??= DeviceApi(_client); + + ModerationApi? _moderation; + + /// + ModerationApi get moderation => _moderation ??= ModerationApi(_client); + + GeneralApi? _general; + + /// + GeneralApi get general => _general ??= GeneralApi(_client); + + AttachmentFileUploader? _fileUploader; + + /// + AttachmentFileUploader get fileUploader => + _fileUploader ??= StreamAttachmentFileUploader(_client); +} diff --git a/packages/stream_chat/lib/src/core/api/user_api.dart b/packages/stream_chat/lib/src/core/api/user_api.dart new file mode 100644 index 00000000..04c72284 --- /dev/null +++ b/packages/stream_chat/lib/src/core/api/user_api.dart @@ -0,0 +1,49 @@ +import 'dart:convert'; + +import 'package:stream_chat/src/core/api/requests.dart'; +import 'package:stream_chat/src/core/api/responses.dart'; +import 'package:stream_chat/src/core/http/stream_http_client.dart'; +import 'package:stream_chat/src/core/models/filter.dart'; +import 'package:stream_chat/src/core/models/user.dart'; + +/// +class UserApi { + /// + UserApi(this._client); + + final StreamHttpClient _client; + + /// Requests users with a given query. + Future queryUsers({ + bool presence = false, + Filter? filter, + List? sort, + PaginationParams? pagination, + }) async { + final response = await _client.get( + '/users', + queryParameters: { + 'payload': jsonEncode({ + 'presence': presence, + if (sort != null) 'sort': sort, + if (filter != null) 'filter_conditions': filter, + if (pagination != null) ...pagination.toJson(), + }), + }, + ); + return QueryUsersResponse.fromJson(response.data); + } + + /// Batch update a list of users + Future updateUsers( + List users, + ) async { + final response = await _client.post( + '/users', + data: { + 'users': {for (final user in users) user.id: user}, + }, + ); + return UpdateUsersResponse.fromJson(response.data); + } +} diff --git a/packages/stream_chat/lib/src/core/http/connection_id_manager.dart b/packages/stream_chat/lib/src/core/http/connection_id_manager.dart new file mode 100644 index 00000000..55fa1896 --- /dev/null +++ b/packages/stream_chat/lib/src/core/http/connection_id_manager.dart @@ -0,0 +1,27 @@ +// ignore_for_file: use_setters_to_change_properties + +/// +class ConnectionIdManager { + /// + ConnectionIdManager({ + String? connectionId, + }) : _connectionId = connectionId; + + String? _connectionId; + + /// + String? get connectionId => _connectionId; + + /// + bool get hasConnectionId => _connectionId != null; + + /// + void setConnectionId(String connectionId) { + _connectionId = connectionId; + } + + /// + void reset() { + _connectionId = null; + } +} diff --git a/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart b/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart new file mode 100644 index 00000000..19d0293b --- /dev/null +++ b/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart @@ -0,0 +1,91 @@ +import 'package:dio/dio.dart'; +import 'package:stream_chat/src/core/api/responses.dart'; +import 'package:stream_chat/src/core/http/stream_chat_dio_error.dart'; +import 'package:stream_chat/src/core/http/token.dart'; + +import 'package:stream_chat/src/core/http/token_manager.dart'; +import 'package:stream_chat/src/errors/chat_error_code.dart'; +import 'package:stream_chat/src/errors/stream_chat_error.dart'; + +/// +class AuthInterceptor extends Interceptor { + /// + AuthInterceptor(this._httpClient, this._tokenManager); + + final Dio _httpClient; + + /// + final TokenManager _tokenManager; + + @override + Future onRequest( + RequestOptions options, + RequestInterceptorHandler handler, + ) async { + late Token token; + try { + token = await _tokenManager.loadToken(); + } catch (_) { + final error = StreamChatError(ChatErrorCode.undefinedToken); + final dioError = StreamChatDioError( + error: error, + requestOptions: options, + ); + return handler.reject(dioError); + } + final params = {'user_id': token.userId}; + final headers = { + 'Authorization': token.rawValue, + 'stream-auth-type': token.authType.raw, + }; + options..queryParameters.addAll(params)..headers.addAll(headers); + return handler.next(options); + } + + @override + void onError( + DioError err, + ErrorInterceptorHandler handler, + ) async { + ErrorResponse? error; + final data = err.response?.data; + if (data != null) error = ErrorResponse.fromJson(data); + if (error?.code == ChatErrorCode.tokenExpired.code) { + if (_tokenManager.isStatic) return handler.next(err); + _httpClient.lock(); + await _tokenManager.loadToken(refresh: true); + _httpClient.unlock(); + try { + final options = err.requestOptions; + final response = await _httpClient.request( + options.path, + cancelToken: options.cancelToken, + data: options.data, + onReceiveProgress: options.onReceiveProgress, + onSendProgress: options.onSendProgress, + queryParameters: options.queryParameters, + options: Options( + method: options.method, + sendTimeout: options.sendTimeout, + receiveTimeout: options.receiveTimeout, + extra: options.extra, + headers: options.headers, + responseType: options.responseType, + contentType: options.contentType, + validateStatus: options.validateStatus, + receiveDataWhenStatusError: options.receiveDataWhenStatusError, + followRedirects: options.followRedirects, + maxRedirects: options.maxRedirects, + requestEncoder: options.requestEncoder, + responseDecoder: options.responseDecoder, + listFormat: options.listFormat, + ), + ); + return handler.resolve(response); + } on DioError catch (error) { + return handler.reject(error); + } + } + return handler.next(err); + } +} diff --git a/packages/stream_chat/lib/src/core/http/interceptor/connection_id_interceptor.dart b/packages/stream_chat/lib/src/core/http/interceptor/connection_id_interceptor.dart new file mode 100644 index 00000000..78a3147f --- /dev/null +++ b/packages/stream_chat/lib/src/core/http/interceptor/connection_id_interceptor.dart @@ -0,0 +1,25 @@ +import 'package:dio/dio.dart'; + +import 'package:stream_chat/src/core/http/connection_id_manager.dart'; + +/// +class ConnectionIdInterceptor extends Interceptor { + /// + ConnectionIdInterceptor(this.connectionIdManager); + + /// + final ConnectionIdManager connectionIdManager; + + @override + void onRequest( + RequestOptions options, + RequestInterceptorHandler handler, + ) async { + if (connectionIdManager.hasConnectionId) { + options.queryParameters.addAll({ + 'connection_id': connectionIdManager.connectionId, + }); + } + handler.next(options); + } +} diff --git a/packages/stream_chat/lib/src/core/http/interceptor/logging_interceptor.dart b/packages/stream_chat/lib/src/core/http/interceptor/logging_interceptor.dart new file mode 100644 index 00000000..a9075e34 --- /dev/null +++ b/packages/stream_chat/lib/src/core/http/interceptor/logging_interceptor.dart @@ -0,0 +1,338 @@ +// ignore_for_file: lines_longer_than_80_chars + +import 'dart:math' as math; + +import 'package:dio/dio.dart'; + +/// +enum InterceptStep { + /// + request, + + /// + response, + + /// + error, +} + +/// +typedef LogPrint = void Function(InterceptStep step, Object object); + +void _defaultLogPrint(InterceptStep step, Object object) => print(object); + +/// +class LoggingInterceptor extends Interceptor { + /// + LoggingInterceptor({ + this.request = true, + this.requestHeader = false, + this.requestBody = true, + this.responseHeader = false, + this.responseBody = true, + this.error = true, + this.maxWidth = 120, + this.compact = true, + this.logPrint = _defaultLogPrint, + }); + + /// Print request [Options] + final bool request; + + /// Print request header [Options.headers] + final bool requestHeader; + + /// Print request data [Options.data] + final bool requestBody; + + /// Print [Response.data] + final bool responseBody; + + /// Print [Response.headers] + final bool responseHeader; + + /// Print error message + final bool error; + + /// InitialTab count to logPrint json response + static const int initialTab = 1; + + /// 1 tab length + static const String tabStep = ' '; + + /// Print compact json response + final bool compact; + + /// Width size per logPrint + final int maxWidth; + + /// Log printer; defaults logPrint log to console. + /// In flutter, you'd better use debugPrint. + /// you can also write log in a file. + void Function(InterceptStep step, Object object) logPrint; + + @override + void onRequest(RequestOptions options, RequestInterceptorHandler handler) { + if (request) { + _printRequestHeader(_logPrintRequest, options); + } + if (requestHeader) { + _printMapAsTable( + _logPrintRequest, + options.queryParameters, + header: 'Query Parameters', + ); + final requestHeaders = {...options.headers}; + requestHeaders['contentType'] = options.contentType?.toString(); + requestHeaders['responseType'] = options.responseType.toString(); + requestHeaders['followRedirects'] = options.followRedirects; + requestHeaders['connectTimeout'] = options.connectTimeout; + requestHeaders['receiveTimeout'] = options.receiveTimeout; + _printMapAsTable(_logPrintRequest, requestHeaders, header: 'Headers'); + _printMapAsTable(_logPrintRequest, options.extra, header: 'Extras'); + } + if (requestBody && options.method != 'GET') { + final dynamic data = options.data; + if (data != null) { + if (data is Map) { + _printMapAsTable( + _logPrintRequest, + options.data as Map?, + header: 'Body', + ); + } else if (data is FormData) { + final formDataMap = {} + ..addEntries(data.fields) + ..addEntries(data.files); + _printMapAsTable(_logPrintRequest, formDataMap, + header: 'Form data | ${data.boundary}'); + } else { + _printBlock(_logPrintRequest, data.toString()); + } + } + } + super.onRequest(options, handler); + } + + @override + void onError(DioError err, ErrorInterceptorHandler handler) { + if (error) { + if (err.type == DioErrorType.response) { + final uri = err.response?.requestOptions.uri; + _printBoxed( + _logPrintError, + header: + 'DioError â•‘ Status: ${err.response?.statusCode} ${err.response?.statusMessage}', + text: uri.toString(), + ); + if (err.response != null && err.response?.data != null) { + _logPrintError('â•” ${err.type.toString()}'); + _printResponse(_logPrintError, err.response!); + } + _printLine(_logPrintError, '╚'); + _logPrintError(''); + } else { + _printBoxed( + _logPrintError, + header: 'DioError â•‘ ${err.type}', + text: err.message, + ); + _printRequestHeader(_logPrintError, err.requestOptions); + } + } + super.onError(err, handler); + } + + @override + void onResponse(Response response, ResponseInterceptorHandler handler) { + _printResponseHeader(_logPrintResponse, response); + if (responseHeader) { + final responseHeaders = {}; + response.headers + .forEach((k, list) => responseHeaders[k] = list.toString()); + _printMapAsTable(_logPrintResponse, responseHeaders, header: 'Headers'); + } + + if (responseBody) { + _logPrintResponse('â•” Body'); + _logPrintResponse('â•‘'); + _printResponse(_logPrintResponse, response); + _logPrintResponse('â•‘'); + _printLine(_logPrintResponse, '╚'); + } + super.onResponse(response, handler); + } + + void _printBoxed( + void Function(Object) logPrint, { + String? header, + String? text, + }) { + logPrint(''); + logPrint('╔╣ $header'); + logPrint('â•‘ $text'); + _printLine(logPrint, '╚'); + } + + void _printResponse(void Function(Object) logPrint, Response response) { + if (response.data != null) { + if (response.data is Map) { + _printPrettyMap(logPrint, response.data as Map); + } else if (response.data is List) { + logPrint('â•‘${_indent()}['); + _printList(logPrint, response.data as List); + logPrint('â•‘${_indent()}['); + } else { + _printBlock(logPrint, response.data.toString()); + } + } + } + + void _printResponseHeader(void Function(Object) logPrint, Response response) { + final uri = response.requestOptions.uri; + final method = response.requestOptions.method; + _printBoxed( + logPrint, + header: + 'Response â•‘ $method â•‘ Status: ${response.statusCode} ${response.statusMessage}', + text: uri.toString(), + ); + } + + void _printRequestHeader( + void Function(Object) logPrint, RequestOptions options) { + final uri = options.uri; + final method = options.method; + _printBoxed(logPrint, header: 'Request â•‘ $method ', text: uri.toString()); + } + + void _printLine(void Function(Object) logPrint, + [String pre = '', String suf = 'â•']) => + logPrint('$pre${'â•' * maxWidth}$suf'); + + void _printKV(void Function(Object) logPrint, String? key, Object? v) { + final pre = '╟ $key: '; + final msg = v.toString(); + + if (pre.length + msg.length > maxWidth) { + logPrint(pre); + _printBlock(logPrint, msg); + } else { + logPrint('$pre$msg'); + } + } + + void _printBlock(void Function(Object) logPrint, String msg) { + final lines = (msg.length / maxWidth).ceil(); + for (var i = 0; i < lines; ++i) { + logPrint((i >= 0 ? 'â•‘ ' : '') + + msg.substring(i * maxWidth, + math.min(i * maxWidth + maxWidth, msg.length))); + } + } + + String _indent([int tabCount = initialTab]) => tabStep * tabCount; + + void _printPrettyMap( + void Function(Object) logPrint, + Map data, { + int tabs = initialTab, + bool isListItem = false, + bool isLast = false, + }) { + var _tabs = tabs; + final isRoot = _tabs == initialTab; + final initialIndent = _indent(_tabs); + _tabs++; + + if (isRoot || isListItem) logPrint('â•‘$initialIndent{'); + + data.keys.toList().asMap().forEach((index, dynamic key) { + final isLast = index == data.length - 1; + dynamic value = data[key]; + if (value is String) { + value = '"${value.toString().replaceAll(RegExp(r'(\r|\n)+'), " ")}"'; + } + if (value is Map) { + if (compact) { + logPrint('â•‘${_indent(_tabs)} $key: $value${!isLast ? ',' : ''}'); + } else { + logPrint('â•‘${_indent(_tabs)} $key: {'); + _printPrettyMap(logPrint, value, tabs: _tabs); + } + } else if (value is List) { + if (compact) { + logPrint('â•‘${_indent(_tabs)} $key: ${value.toString()}'); + } else { + logPrint('â•‘${_indent(_tabs)} $key: ['); + _printList(logPrint, value, tabs: _tabs); + logPrint('â•‘${_indent(_tabs)} ]${isLast ? '' : ','}'); + } + } else { + final msg = value.toString().replaceAll('\n', ''); + final indent = _indent(_tabs); + final linWidth = maxWidth - indent.length; + if (msg.length + indent.length > linWidth) { + final lines = (msg.length / linWidth).ceil(); + for (var i = 0; i < lines; ++i) { + logPrint('â•‘${_indent(_tabs)} ${msg.substring( + i * linWidth, + math.min(i * linWidth + linWidth, msg.length), + )}'); + } + } else { + logPrint('â•‘${_indent(_tabs)} $key: $msg${!isLast ? ',' : ''}'); + } + } + }); + + logPrint('â•‘$initialIndent}${isListItem && !isLast ? ',' : ''}'); + } + + void _printList( + void Function(Object) logPrint, + List list, { + int tabs = initialTab, + }) { + list.asMap().forEach((i, dynamic e) { + final isLast = i == list.length - 1; + if (e is Map) { + if (compact) { + logPrint('â•‘${_indent(tabs)} $e${!isLast ? ',' : ''}'); + } else { + _printPrettyMap(logPrint, e, + tabs: tabs + 1, isListItem: true, isLast: isLast); + } + } else { + logPrint('â•‘${_indent(tabs + 2)} $e${isLast ? '' : ','}'); + } + }); + } + + bool _canFlattenMap(Map map) => + map.values.where((dynamic val) => val is Map || val is List).isEmpty && + map.toString().length < maxWidth; + + bool _canFlattenList(List list) => + list.length < 10 && list.toString().length < maxWidth; + + void _printMapAsTable( + void Function(Object) logPrint, + Map? map, { + String? header, + }) { + if (map == null || map.isEmpty) return; + logPrint('â•” $header '); + map.forEach((dynamic key, dynamic value) => + _printKV(logPrint, key.toString(), value)); + _printLine(logPrint, '╚'); + } + + void _logPrintRequest(Object object) => + logPrint(InterceptStep.request, object); + + void _logPrintResponse(Object object) => + logPrint(InterceptStep.response, object); + + void _logPrintError(Object object) => logPrint(InterceptStep.error, object); +} diff --git a/packages/stream_chat/lib/src/core/http/stream_chat_dio_error.dart b/packages/stream_chat/lib/src/core/http/stream_chat_dio_error.dart new file mode 100644 index 00000000..dd2c2535 --- /dev/null +++ b/packages/stream_chat/lib/src/core/http/stream_chat_dio_error.dart @@ -0,0 +1,21 @@ +import 'package:dio/dio.dart'; +import 'package:stream_chat/src/errors/stream_chat_error.dart'; + +/// +class StreamChatDioError extends DioError { + /// + StreamChatDioError({ + required this.error, + required RequestOptions requestOptions, + Response? response, + DioErrorType type = DioErrorType.other, + }) : super( + error: error, + requestOptions: requestOptions, + response: response, + type: type, + ); + + @override + final StreamChatError error; +} diff --git a/packages/stream_chat/lib/src/core/http/stream_http_client.dart b/packages/stream_chat/lib/src/core/http/stream_http_client.dart new file mode 100644 index 00000000..ae0a5f72 --- /dev/null +++ b/packages/stream_chat/lib/src/core/http/stream_http_client.dart @@ -0,0 +1,241 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:dio/dio.dart'; +import 'package:logging/logging.dart'; +import 'package:meta/meta.dart'; +import 'package:stream_chat/src/core/http/interceptor/auth_interceptor.dart'; +import 'package:stream_chat/src/core/http/interceptor/connection_id_interceptor.dart'; +import 'package:stream_chat/src/core/http/interceptor/logging_interceptor.dart'; +import 'package:stream_chat/src/core/http/stream_chat_dio_error.dart'; +import 'package:stream_chat/src/core/http/token_manager.dart'; +import 'package:stream_chat/src/errors/stream_chat_error.dart'; +import 'package:stream_chat/src/platform_detector/platform_detector.dart'; +import 'package:stream_chat/version.dart'; + +import 'package:stream_chat/src/core/http/connection_id_manager.dart'; + +part 'stream_http_client_options.dart'; + +/// This is where we configure the base url, headers, +/// query parameters and convenient methods for http verbs with error parsing. +class StreamHttpClient { + /// [StreamHttpClient] constructor + StreamHttpClient( + this.apiKey, { + Dio? dio, + StreamHttpClientOptions? options, + TokenManager? tokenManager, + ConnectionIdManager? connectionIdManager, + Logger? logger, + }) : _options = options ?? const StreamHttpClientOptions(), + httpClient = dio ?? Dio() { + httpClient + ..options.baseUrl = _options.baseUrl + ..options.receiveTimeout = _options.receiveTimeout.inMilliseconds + ..options.connectTimeout = _options.connectTimeout.inMilliseconds + ..options.queryParameters = {'api_key': apiKey} + ..options.headers = { + 'Content-Type': 'application/json', + 'X-Stream-Client': _options.userAgent, + 'Content-Encoding': 'application/gzip', + } + ..interceptors.addAll([ + if (tokenManager != null) AuthInterceptor(httpClient, tokenManager), + if (connectionIdManager != null) + ConnectionIdInterceptor(connectionIdManager), + if (logger != null && logger.level != Level.OFF) + LoggingInterceptor( + requestHeader: true, + logPrint: (step, message) { + switch (step) { + case InterceptStep.request: + return logger.info(message); + case InterceptStep.response: + return logger.info(message); + case InterceptStep.error: + return logger.severe(message); + } + }, + ), + ]); + } + + /// Your project Stream Chat api key. + /// Find your API keys here https://getstream.io/dashboard/ + final String apiKey; + + /// Your project Stream Chat ClientOptions + final StreamHttpClientOptions _options; + + /// [Dio] httpClient + /// It's been chosen because it's easy to use + /// and supports interesting features out of the box + /// (Interceptors, Global configuration, FormData, File downloading etc.) + @visibleForTesting + final Dio httpClient; + + /// Shuts down the [httpClient]. + /// + /// If [force] is `false` (the default) the [httpClient] will be kept alive + /// until all active connections are done. If [force] is `true` any active + /// connections will be closed to immediately release all resources. These + /// closed connections will receive an error event to indicate that the client + /// was shut down. In both cases trying to establish a new connection after + /// calling [close] will throw an exception. + void close({bool force = false}) => httpClient.close(force: force); + + StreamChatNetworkError _parseError(DioError err) { + // locally thrown dio error + if (err is StreamChatDioError) { + final code = err.error.code; + final message = err.error.message; + return StreamChatNetworkError.raw(code: code, message: message); + } + // real network request dio error + return StreamChatNetworkError.fromDioError(err); + } + + /// Handy method to make http GET request with error parsing. + Future> get( + String path, { + Map? queryParameters, + Map? headers, + ProgressCallback? onReceiveProgress, + CancelToken? cancelToken, + }) async { + try { + final response = await httpClient.get( + path, + queryParameters: queryParameters, + options: Options(headers: headers), + onReceiveProgress: onReceiveProgress, + cancelToken: cancelToken, + ); + return response; + } on DioError catch (error) { + throw _parseError(error); + } + } + + /// Handy method to make http POST request with error parsing. + Future> post( + String path, { + Object? data, + Map? queryParameters, + Map? headers, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + CancelToken? cancelToken, + }) async { + try { + final response = await httpClient.post( + path, + queryParameters: queryParameters, + data: data, + options: Options(headers: headers), + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + cancelToken: cancelToken, + ); + return response; + } on DioError catch (error) { + throw _parseError(error); + } + } + + /// Handy method to make http DELETE request with error parsing. + Future> delete( + String path, { + Map? queryParameters, + Map? headers, + CancelToken? cancelToken, + }) async { + try { + final response = await httpClient.delete( + path, + queryParameters: queryParameters, + options: Options(headers: headers), + cancelToken: cancelToken, + ); + return response; + } on DioError catch (error) { + throw _parseError(error); + } + } + + /// Handy method to make http PATCH request with error parsing. + Future> patch( + String path, { + Object? data, + Map? queryParameters, + Map? headers, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + CancelToken? cancelToken, + }) async { + try { + final response = await httpClient.patch( + path, + queryParameters: queryParameters, + data: data, + options: Options(headers: headers), + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + cancelToken: cancelToken, + ); + return response; + } on DioError catch (error) { + throw _parseError(error); + } + } + + /// Handy method to make http PUT request with error parsing. + Future> put( + String path, { + Object? data, + Map? queryParameters, + Map? headers, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + CancelToken? cancelToken, + }) async { + try { + final response = await httpClient.put( + path, + queryParameters: queryParameters, + data: data, + options: Options(headers: headers), + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + cancelToken: cancelToken, + ); + return response; + } on DioError catch (error) { + throw _parseError(error); + } + } + + /// Handy method to post files with error parsing. + Future> postFile( + String path, + MultipartFile file, { + Map? queryParameters, + Map? headers, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + CancelToken? cancelToken, + }) async { + final formData = FormData.fromMap({'file': file}); + final response = await post( + path, + data: formData, + queryParameters: queryParameters, + headers: headers, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + cancelToken: cancelToken, + ); + return response; + } +} diff --git a/packages/stream_chat/lib/src/core/http/stream_http_client_options.dart b/packages/stream_chat/lib/src/core/http/stream_http_client_options.dart new file mode 100644 index 00000000..0f06e4d0 --- /dev/null +++ b/packages/stream_chat/lib/src/core/http/stream_http_client_options.dart @@ -0,0 +1,69 @@ +part of 'stream_http_client.dart'; + +/// +enum Location { + /// + usEast, + + /// + euWest, + + /// + mumbai, + + /// + sydney, + + /// + singapore, +} + +/// +extension LocationX on Location { + /// + String get name => { + Location.usEast: 'us-east', + Location.euWest: 'dublin', + Location.mumbai: 'mumbai', + Location.sydney: 'sydney', + Location.singapore: 'singapore', + }[this]!; +} + +const _defaultBaseURL = 'https://chat-us-east-1.stream-io-api.com'; + +/// Client options to modify [StreamHttpClient] +class StreamHttpClientOptions { + /// Instantiates a new [StreamHttpClientOptions] + const StreamHttpClientOptions({ + String? baseUrl, + this.location, + this.connectTimeout = const Duration(seconds: 6), + this.receiveTimeout = const Duration(seconds: 6), + }) : _baseUrl = baseUrl ?? _defaultBaseURL; + + final String _baseUrl; + + /// base url to use with client. + String get baseUrl { + if (location == null) return _baseUrl; + const serviceName = 'chat'; + final locationName = location!.name; + const baseDomainName = 'stream-io-api.com'; + return 'https://$serviceName-proxy-$locationName.$baseDomainName'; + } + + /// data center to use with client + final Location? location; + + /// connect timeout, default to 6s + final Duration connectTimeout; + + /// received timeout, default to 6s + final Duration receiveTimeout; + + /// Get the current user agent + String get userAgent => 'stream-chat-dart-client-' + '${CurrentPlatform.name}-' + '${PACKAGE_VERSION.split('+')[0]}'; +} diff --git a/packages/stream_chat/lib/src/core/http/token.dart b/packages/stream_chat/lib/src/core/http/token.dart new file mode 100644 index 00000000..a914521c --- /dev/null +++ b/packages/stream_chat/lib/src/core/http/token.dart @@ -0,0 +1,85 @@ +import 'dart:convert'; + +import 'package:equatable/equatable.dart'; +import 'package:jose/jose.dart'; +import 'package:stream_chat/src/core/models/user.dart'; +import 'package:stream_chat/src/core/utils.dart'; + +/// +typedef GuestTokenProvider = Future Function(User user); + +/// +enum AuthType { + /// + jwt, + + /// + anonymous, +} + +/// +extension AuthTypeX on AuthType { + /// + String get raw => { + AuthType.jwt: 'jwt', + AuthType.anonymous: 'anonymous', + }[this]!; +} + +/// Token designed to store the JWT and the user it is related to. +class Token extends Equatable { + const Token._({ + required this.rawValue, + required this.userId, + required this.authType, + }); + + /// The token that can be used when user is unknown. + /// Is used by `anonymous` token provider. + factory Token.anonymous({String? userId}) => Token._( + rawValue: '', + userId: userId ?? randomId(), + authType: AuthType.anonymous, + ); + + /// Creates a [Token] instance from the provided [rawValue] if it's valid. + factory Token.fromRawValue(String rawValue) { + final jwtBody = JsonWebToken.unverified(rawValue); + final userId = jwtBody.claims.getTyped('user_id'); + assert( + userId != null, + 'Invalid `token`, It should contain `user_id`', + ); + return Token._(rawValue: rawValue, userId: userId!, authType: AuthType.jwt); + } + + /// The token which can be used during the development. + /// Is used by `development(userId:)` token provider. + factory Token.development(String userId) { + const devSignature = 'devtoken'; + const header = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'; + final payload = json.encode({'user_id': userId}); + final payloadBytes = utf8.encode(payload); + final payloadB64 = base64.encode(payloadBytes); + final jwt = '$header.$payloadB64.$devSignature'; + return Token._(rawValue: jwt, userId: userId, authType: AuthType.jwt); + } + + /// The token which designed to be used for guest users. + static Future guest(User user, GuestTokenProvider provider) async { + final rawToken = await provider(user); + return Token.fromRawValue(rawToken); + } + + /// + final AuthType authType; + + /// + final String rawValue; + + /// + final String userId; + + @override + List get props => [authType, rawValue, userId]; +} diff --git a/packages/stream_chat/lib/src/core/http/token_manager.dart b/packages/stream_chat/lib/src/core/http/token_manager.dart new file mode 100644 index 00000000..88e9746d --- /dev/null +++ b/packages/stream_chat/lib/src/core/http/token_manager.dart @@ -0,0 +1,80 @@ +import 'package:stream_chat/src/core/http/token.dart'; + +/// A function which can be used to request a Stream Chat API token from your +/// own backend server. Function requires a single [userId]. +typedef TokenProvider = Future Function(String userId); + +/// +class TokenManager { + /// + TokenManager({ + String? userId, + Token? token, + TokenProvider? tokenProvider, + }) : _userId = userId, + _token = token, + _provider = tokenProvider; + + String? _type; + Token? _token; + + TokenProvider? _provider; + + String? _userId; + + /// User id to which this TokenManager is configured to + String? get userId => _userId; + + /// + bool get isStatic => _type == 'static'; + + /// + Future setTokenOrProvider( + String userId, { + Token? token, + TokenProvider? provider, + }) async { + assert(() { + if (token == null && provider == null) { + throw AssertionError('Provide at-least token or provider'); + } + if (token != null && provider != null) { + throw AssertionError("Can't set both token and provider"); + } + return true; + }(), ''); + + _userId = userId; + + if (token != null) { + _type = 'static'; + _token = token; + } + if (provider != null) { + _type = 'provider'; + _provider = provider; + } + + return loadToken(); + } + + /// + Future loadToken({bool refresh = false}) async { + assert( + _userId != null && _type != null, + 'Please call `setTokenOrProvider` before calling `loadToken`', + ); + if (refresh || _token == null) { + final rawValue = await _provider!(_userId!); + _token = Token.fromRawValue(rawValue); + } + return _token!; + } + + /// + void reset() { + _userId = null; + _token = null; + _provider = null; + } +} diff --git a/packages/stream_chat/lib/src/models/action.dart b/packages/stream_chat/lib/src/core/models/action.dart similarity index 100% rename from packages/stream_chat/lib/src/models/action.dart rename to packages/stream_chat/lib/src/core/models/action.dart diff --git a/packages/stream_chat/lib/src/models/action.g.dart b/packages/stream_chat/lib/src/core/models/action.g.dart similarity index 100% rename from packages/stream_chat/lib/src/models/action.g.dart rename to packages/stream_chat/lib/src/core/models/action.g.dart diff --git a/packages/stream_chat/lib/src/models/attachment.dart b/packages/stream_chat/lib/src/core/models/attachment.dart similarity index 97% rename from packages/stream_chat/lib/src/models/attachment.dart rename to packages/stream_chat/lib/src/core/models/attachment.dart index b68a1bae..1df785a8 100644 --- a/packages/stream_chat/lib/src/models/attachment.dart +++ b/packages/stream_chat/lib/src/core/models/attachment.dart @@ -2,9 +2,9 @@ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; -import 'package:stream_chat/src/models/action.dart'; -import 'package:stream_chat/src/models/attachment_file.dart'; -import 'package:stream_chat/src/models/serialization.dart'; +import 'package:stream_chat/src/core/models/action.dart'; +import 'package:stream_chat/src/core/models/attachment_file.dart'; +import 'package:stream_chat/src/core/models/serialization.dart'; import 'package:uuid/uuid.dart'; part 'attachment.g.dart'; diff --git a/packages/stream_chat/lib/src/models/attachment.g.dart b/packages/stream_chat/lib/src/core/models/attachment.g.dart similarity index 100% rename from packages/stream_chat/lib/src/models/attachment.g.dart rename to packages/stream_chat/lib/src/core/models/attachment.g.dart diff --git a/packages/stream_chat/lib/src/models/attachment_file.dart b/packages/stream_chat/lib/src/core/models/attachment_file.dart similarity index 100% rename from packages/stream_chat/lib/src/models/attachment_file.dart rename to packages/stream_chat/lib/src/core/models/attachment_file.dart diff --git a/packages/stream_chat/lib/src/models/attachment_file.freezed.dart b/packages/stream_chat/lib/src/core/models/attachment_file.freezed.dart similarity index 100% rename from packages/stream_chat/lib/src/models/attachment_file.freezed.dart rename to packages/stream_chat/lib/src/core/models/attachment_file.freezed.dart diff --git a/packages/stream_chat/lib/src/models/attachment_file.g.dart b/packages/stream_chat/lib/src/core/models/attachment_file.g.dart similarity index 100% rename from packages/stream_chat/lib/src/models/attachment_file.g.dart rename to packages/stream_chat/lib/src/core/models/attachment_file.g.dart diff --git a/packages/stream_chat/lib/src/models/channel_config.dart b/packages/stream_chat/lib/src/core/models/channel_config.dart similarity index 97% rename from packages/stream_chat/lib/src/models/channel_config.dart rename to packages/stream_chat/lib/src/core/models/channel_config.dart index 9ba36e4f..2d5182b3 100644 --- a/packages/stream_chat/lib/src/models/channel_config.dart +++ b/packages/stream_chat/lib/src/core/models/channel_config.dart @@ -1,5 +1,5 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:stream_chat/src/models/command.dart'; +import 'package:stream_chat/src/core/models/command.dart'; part 'channel_config.g.dart'; diff --git a/packages/stream_chat/lib/src/models/channel_config.g.dart b/packages/stream_chat/lib/src/core/models/channel_config.g.dart similarity index 100% rename from packages/stream_chat/lib/src/models/channel_config.g.dart rename to packages/stream_chat/lib/src/core/models/channel_config.g.dart diff --git a/packages/stream_chat/lib/src/models/channel_model.dart b/packages/stream_chat/lib/src/core/models/channel_model.dart similarity index 96% rename from packages/stream_chat/lib/src/models/channel_model.dart rename to packages/stream_chat/lib/src/core/models/channel_model.dart index 41230bf7..892713e7 100644 --- a/packages/stream_chat/lib/src/models/channel_model.dart +++ b/packages/stream_chat/lib/src/core/models/channel_model.dart @@ -1,7 +1,7 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:stream_chat/src/models/channel_config.dart'; -import 'package:stream_chat/src/models/serialization.dart'; -import 'package:stream_chat/src/models/user.dart'; +import 'package:stream_chat/src/core/models/channel_config.dart'; +import 'package:stream_chat/src/core/models/serialization.dart'; +import 'package:stream_chat/src/core/models/user.dart'; part 'channel_model.g.dart'; diff --git a/packages/stream_chat/lib/src/models/channel_model.g.dart b/packages/stream_chat/lib/src/core/models/channel_model.g.dart similarity index 100% rename from packages/stream_chat/lib/src/models/channel_model.g.dart rename to packages/stream_chat/lib/src/core/models/channel_model.g.dart diff --git a/packages/stream_chat/lib/src/models/channel_state.dart b/packages/stream_chat/lib/src/core/models/channel_state.dart similarity index 87% rename from packages/stream_chat/lib/src/models/channel_state.dart rename to packages/stream_chat/lib/src/core/models/channel_state.dart index 9d18f434..998efa98 100644 --- a/packages/stream_chat/lib/src/models/channel_state.dart +++ b/packages/stream_chat/lib/src/core/models/channel_state.dart @@ -1,9 +1,9 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:stream_chat/src/models/channel_model.dart'; -import 'package:stream_chat/src/models/member.dart'; -import 'package:stream_chat/src/models/message.dart'; -import 'package:stream_chat/src/models/read.dart'; -import 'package:stream_chat/src/models/user.dart'; +import 'package:stream_chat/src/core/models/channel_model.dart'; +import 'package:stream_chat/src/core/models/member.dart'; +import 'package:stream_chat/src/core/models/message.dart'; +import 'package:stream_chat/src/core/models/read.dart'; +import 'package:stream_chat/src/core/models/user.dart'; part 'channel_state.g.dart'; diff --git a/packages/stream_chat/lib/src/models/channel_state.g.dart b/packages/stream_chat/lib/src/core/models/channel_state.g.dart similarity index 100% rename from packages/stream_chat/lib/src/models/channel_state.g.dart rename to packages/stream_chat/lib/src/core/models/channel_state.g.dart diff --git a/packages/stream_chat/lib/src/models/command.dart b/packages/stream_chat/lib/src/core/models/command.dart similarity index 100% rename from packages/stream_chat/lib/src/models/command.dart rename to packages/stream_chat/lib/src/core/models/command.dart diff --git a/packages/stream_chat/lib/src/models/command.g.dart b/packages/stream_chat/lib/src/core/models/command.g.dart similarity index 100% rename from packages/stream_chat/lib/src/models/command.g.dart rename to packages/stream_chat/lib/src/core/models/command.g.dart diff --git a/packages/stream_chat/lib/src/models/device.dart b/packages/stream_chat/lib/src/core/models/device.dart similarity index 100% rename from packages/stream_chat/lib/src/models/device.dart rename to packages/stream_chat/lib/src/core/models/device.dart diff --git a/packages/stream_chat/lib/src/models/device.g.dart b/packages/stream_chat/lib/src/core/models/device.g.dart similarity index 100% rename from packages/stream_chat/lib/src/models/device.g.dart rename to packages/stream_chat/lib/src/core/models/device.g.dart diff --git a/packages/stream_chat/lib/src/models/event.dart b/packages/stream_chat/lib/src/core/models/event.dart similarity index 93% rename from packages/stream_chat/lib/src/models/event.dart rename to packages/stream_chat/lib/src/core/models/event.dart index b4ee9c1d..d0d17aa0 100644 --- a/packages/stream_chat/lib/src/models/event.dart +++ b/packages/stream_chat/lib/src/core/models/event.dart @@ -1,7 +1,7 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:stream_chat/src/models/channel_model.dart'; -import 'package:stream_chat/src/models/message.dart'; -import 'package:stream_chat/src/models/serialization.dart'; +import 'package:stream_chat/src/core/models/channel_model.dart'; +import 'package:stream_chat/src/core/models/message.dart'; +import 'package:stream_chat/src/core/models/serialization.dart'; import 'package:stream_chat/stream_chat.dart'; part 'event.g.dart'; @@ -10,11 +10,11 @@ part 'event.g.dart'; @JsonSerializable() class Event { /// Constructor used for json serialization - const Event({ - this.type, + Event({ + this.type = 'local.event', this.cid, this.connectionId, - this.createdAt, + DateTime? createdAt, this.me, this.user, this.message, @@ -29,7 +29,7 @@ class Event { this.parentId, this.extraData = const {}, this.isLocal = true, - }); + }) : createdAt = createdAt ?? DateTime.now(); /// Create a new instance from a json factory Event.fromJson(Map json) => @@ -40,7 +40,7 @@ class Event { /// The type of the event /// [EventType] contains some predefined constant types - final String? type; + final String type; /// The channel cid to which the event belongs final String? cid; @@ -55,7 +55,7 @@ class Event { final String? connectionId; /// The date of creation of the event - final DateTime? createdAt; + final DateTime createdAt; /// User object of the health check user final OwnUser? me; diff --git a/packages/stream_chat/lib/src/models/event.g.dart b/packages/stream_chat/lib/src/core/models/event.g.dart similarity index 97% rename from packages/stream_chat/lib/src/models/event.g.dart rename to packages/stream_chat/lib/src/core/models/event.g.dart index aba142f8..90abeb71 100644 --- a/packages/stream_chat/lib/src/models/event.g.dart +++ b/packages/stream_chat/lib/src/core/models/event.g.dart @@ -8,7 +8,7 @@ part of 'event.dart'; Event _$EventFromJson(Map json) { return Event( - type: json['type'] as String?, + type: json['type'] as String, cid: json['cid'] as String?, connectionId: json['connection_id'] as String?, createdAt: json['created_at'] == null @@ -49,7 +49,7 @@ Map _$EventToJson(Event instance) => { 'channel_id': instance.channelId, 'channel_type': instance.channelType, 'connection_id': instance.connectionId, - 'created_at': instance.createdAt?.toIso8601String(), + 'created_at': instance.createdAt.toIso8601String(), 'me': instance.me?.toJson(), 'user': instance.user?.toJson(), 'message': instance.message?.toJson(), diff --git a/packages/stream_chat/lib/src/models/filter.dart b/packages/stream_chat/lib/src/core/models/filter.dart similarity index 100% rename from packages/stream_chat/lib/src/models/filter.dart rename to packages/stream_chat/lib/src/core/models/filter.dart diff --git a/packages/stream_chat/lib/src/models/member.dart b/packages/stream_chat/lib/src/core/models/member.dart similarity index 98% rename from packages/stream_chat/lib/src/models/member.dart rename to packages/stream_chat/lib/src/core/models/member.dart index 49df170c..87c82473 100644 --- a/packages/stream_chat/lib/src/models/member.dart +++ b/packages/stream_chat/lib/src/core/models/member.dart @@ -1,5 +1,5 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:stream_chat/src/models/user.dart'; +import 'package:stream_chat/src/core/models/user.dart'; part 'member.g.dart'; diff --git a/packages/stream_chat/lib/src/models/member.g.dart b/packages/stream_chat/lib/src/core/models/member.g.dart similarity index 100% rename from packages/stream_chat/lib/src/models/member.g.dart rename to packages/stream_chat/lib/src/core/models/member.g.dart diff --git a/packages/stream_chat/lib/src/models/message.dart b/packages/stream_chat/lib/src/core/models/message.dart similarity index 98% rename from packages/stream_chat/lib/src/models/message.dart rename to packages/stream_chat/lib/src/core/models/message.dart index f928ce3a..f3bf100f 100644 --- a/packages/stream_chat/lib/src/models/message.dart +++ b/packages/stream_chat/lib/src/core/models/message.dart @@ -1,9 +1,9 @@ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; -import 'package:stream_chat/src/models/attachment.dart'; -import 'package:stream_chat/src/models/reaction.dart'; -import 'package:stream_chat/src/models/serialization.dart'; -import 'package:stream_chat/src/models/user.dart'; +import 'package:stream_chat/src/core/models/attachment.dart'; +import 'package:stream_chat/src/core/models/reaction.dart'; +import 'package:stream_chat/src/core/models/serialization.dart'; +import 'package:stream_chat/src/core/models/user.dart'; import 'package:uuid/uuid.dart'; part 'message.g.dart'; diff --git a/packages/stream_chat/lib/src/models/message.g.dart b/packages/stream_chat/lib/src/core/models/message.g.dart similarity index 100% rename from packages/stream_chat/lib/src/models/message.g.dart rename to packages/stream_chat/lib/src/core/models/message.g.dart diff --git a/packages/stream_chat/lib/src/models/mute.dart b/packages/stream_chat/lib/src/core/models/mute.dart similarity index 85% rename from packages/stream_chat/lib/src/models/mute.dart rename to packages/stream_chat/lib/src/core/models/mute.dart index 3ba26230..8c3bef89 100644 --- a/packages/stream_chat/lib/src/models/mute.dart +++ b/packages/stream_chat/lib/src/core/models/mute.dart @@ -1,7 +1,7 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:stream_chat/src/models/channel_model.dart'; -import 'package:stream_chat/src/models/serialization.dart'; -import 'package:stream_chat/src/models/user.dart'; +import 'package:stream_chat/src/core/models/channel_model.dart'; +import 'package:stream_chat/src/core/models/serialization.dart'; +import 'package:stream_chat/src/core/models/user.dart'; part 'mute.g.dart'; diff --git a/packages/stream_chat/lib/src/models/mute.g.dart b/packages/stream_chat/lib/src/core/models/mute.g.dart similarity index 100% rename from packages/stream_chat/lib/src/models/mute.g.dart rename to packages/stream_chat/lib/src/core/models/mute.g.dart diff --git a/packages/stream_chat/lib/src/models/own_user.dart b/packages/stream_chat/lib/src/core/models/own_user.dart similarity index 79% rename from packages/stream_chat/lib/src/models/own_user.dart rename to packages/stream_chat/lib/src/core/models/own_user.dart index a93af19a..6f975a94 100644 --- a/packages/stream_chat/lib/src/models/own_user.dart +++ b/packages/stream_chat/lib/src/core/models/own_user.dart @@ -1,8 +1,8 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:stream_chat/src/models/device.dart'; -import 'package:stream_chat/src/models/mute.dart'; -import 'package:stream_chat/src/models/serialization.dart'; -import 'package:stream_chat/src/models/user.dart'; +import 'package:stream_chat/src/core/models/device.dart'; +import 'package:stream_chat/src/core/models/mute.dart'; +import 'package:stream_chat/src/core/models/serialization.dart'; +import 'package:stream_chat/src/core/models/user.dart'; part 'own_user.g.dart'; @@ -40,6 +40,18 @@ class OwnUser extends User { factory OwnUser.fromJson(Map json) => _$OwnUserFromJson( Serialization.moveToExtraDataFromRoot(json, topLevelFields)); + /// Create a new instance from [User] object + factory OwnUser.fromUser(User user) => OwnUser( + id: user.id, + role: user.role, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + lastActive: user.lastActive, + online: user.online, + banned: user.banned, + extraData: user.extraData, + ); + /// List of user devices @JsonKey( includeIfNull: false, diff --git a/packages/stream_chat/lib/src/models/own_user.g.dart b/packages/stream_chat/lib/src/core/models/own_user.g.dart similarity index 100% rename from packages/stream_chat/lib/src/models/own_user.g.dart rename to packages/stream_chat/lib/src/core/models/own_user.g.dart diff --git a/packages/stream_chat/lib/src/models/reaction.dart b/packages/stream_chat/lib/src/core/models/reaction.dart similarity index 95% rename from packages/stream_chat/lib/src/models/reaction.dart rename to packages/stream_chat/lib/src/core/models/reaction.dart index 85697204..ace4a15e 100644 --- a/packages/stream_chat/lib/src/models/reaction.dart +++ b/packages/stream_chat/lib/src/core/models/reaction.dart @@ -1,6 +1,6 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:stream_chat/src/models/serialization.dart'; -import 'package:stream_chat/src/models/user.dart'; +import 'package:stream_chat/src/core/models/serialization.dart'; +import 'package:stream_chat/src/core/models/user.dart'; part 'reaction.g.dart'; diff --git a/packages/stream_chat/lib/src/models/reaction.g.dart b/packages/stream_chat/lib/src/core/models/reaction.g.dart similarity index 100% rename from packages/stream_chat/lib/src/models/reaction.g.dart rename to packages/stream_chat/lib/src/core/models/reaction.g.dart diff --git a/packages/stream_chat/lib/src/models/read.dart b/packages/stream_chat/lib/src/core/models/read.dart similarity index 94% rename from packages/stream_chat/lib/src/models/read.dart rename to packages/stream_chat/lib/src/core/models/read.dart index cbd47dd1..55912762 100644 --- a/packages/stream_chat/lib/src/models/read.dart +++ b/packages/stream_chat/lib/src/core/models/read.dart @@ -1,5 +1,5 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:stream_chat/src/models/user.dart'; +import 'package:stream_chat/src/core/models/user.dart'; part 'read.g.dart'; diff --git a/packages/stream_chat/lib/src/models/read.g.dart b/packages/stream_chat/lib/src/core/models/read.g.dart similarity index 100% rename from packages/stream_chat/lib/src/models/read.g.dart rename to packages/stream_chat/lib/src/core/models/read.g.dart diff --git a/packages/stream_chat/lib/src/models/serialization.dart b/packages/stream_chat/lib/src/core/models/serialization.dart similarity index 96% rename from packages/stream_chat/lib/src/models/serialization.dart rename to packages/stream_chat/lib/src/core/models/serialization.dart index d584912e..692ad01a 100644 --- a/packages/stream_chat/lib/src/models/serialization.dart +++ b/packages/stream_chat/lib/src/core/models/serialization.dart @@ -1,4 +1,4 @@ -import 'package:stream_chat/src/models/user.dart'; +import 'package:stream_chat/src/core/models/user.dart'; /// Used to avoid to serialize properties to json // ignore: prefer_void_to_null diff --git a/packages/stream_chat/lib/src/models/user.dart b/packages/stream_chat/lib/src/core/models/user.dart similarity index 98% rename from packages/stream_chat/lib/src/models/user.dart rename to packages/stream_chat/lib/src/core/models/user.dart index 9335f8c8..ae1dd742 100644 --- a/packages/stream_chat/lib/src/models/user.dart +++ b/packages/stream_chat/lib/src/core/models/user.dart @@ -1,5 +1,5 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:stream_chat/src/models/serialization.dart'; +import 'package:stream_chat/src/core/models/serialization.dart'; part 'user.g.dart'; diff --git a/packages/stream_chat/lib/src/models/user.g.dart b/packages/stream_chat/lib/src/core/models/user.g.dart similarity index 100% rename from packages/stream_chat/lib/src/models/user.g.dart rename to packages/stream_chat/lib/src/core/models/user.g.dart diff --git a/packages/stream_chat/lib/src/core/utils.dart b/packages/stream_chat/lib/src/core/utils.dart new file mode 100644 index 00000000..9366e34e --- /dev/null +++ b/packages/stream_chat/lib/src/core/utils.dart @@ -0,0 +1,16 @@ +import 'dart:math' as math; + +// This alphabet uses `A-Za-z0-9_-` symbols. The genetic algorithm helped +// optimize the gzip compression for this alphabet. +const _alphabet = + 'ModuleSymbhasOwnPr0123456789ABCDEFGHNRVfgctiUvzKqYTJkLxpZXIjQW'; + +/// Generates a random String id +/// Adopted from: https://github.com/ai/nanoid/blob/main/non-secure/index.js +String randomId({int size = 21}) { + var id = ''; + for (var i = 0; i < size; i++) { + id += _alphabet[(math.Random().nextInt(32) * 64) | 0]; + } + return id; +} diff --git a/packages/stream_chat/lib/src/db/chat_persistence_client.dart b/packages/stream_chat/lib/src/db/chat_persistence_client.dart index 8f60482f..d7e0f765 100644 --- a/packages/stream_chat/lib/src/db/chat_persistence_client.dart +++ b/packages/stream_chat/lib/src/db/chat_persistence_client.dart @@ -1,13 +1,13 @@ -import 'package:stream_chat/src/api/requests.dart'; -import 'package:stream_chat/src/models/channel_model.dart'; -import 'package:stream_chat/src/models/channel_state.dart'; -import 'package:stream_chat/src/models/event.dart'; -import 'package:stream_chat/src/models/filter.dart'; -import 'package:stream_chat/src/models/member.dart'; -import 'package:stream_chat/src/models/message.dart'; -import 'package:stream_chat/src/models/reaction.dart'; -import 'package:stream_chat/src/models/read.dart'; -import 'package:stream_chat/src/models/user.dart'; +import 'package:stream_chat/src/core/api/requests.dart'; +import 'package:stream_chat/src/core/models/channel_model.dart'; +import 'package:stream_chat/src/core/models/channel_state.dart'; +import 'package:stream_chat/src/core/models/event.dart'; +import 'package:stream_chat/src/core/models/filter.dart'; +import 'package:stream_chat/src/core/models/member.dart'; +import 'package:stream_chat/src/core/models/message.dart'; +import 'package:stream_chat/src/core/models/reaction.dart'; +import 'package:stream_chat/src/core/models/read.dart'; +import 'package:stream_chat/src/core/models/user.dart'; import 'package:stream_chat/src/extensions/iterable_extension.dart'; /// A simple client used for persisting chat data locally. diff --git a/packages/stream_chat/lib/src/errors/chat_error_code.dart b/packages/stream_chat/lib/src/errors/chat_error_code.dart new file mode 100644 index 00000000..3dbb600c --- /dev/null +++ b/packages/stream_chat/lib/src/errors/chat_error_code.dart @@ -0,0 +1,91 @@ +/// +enum ChatErrorCode { + // client error codes + networkFailed, + parserError, + socketClosed, + socketFailure, + cantParseConnectionEvent, + cantParseEvent, + invalidToken, + undefinedToken, + unableToParseSocketEvent, + noErrorBody, + + // server error codes + authenticationError, + tokenExpired, + tokenNotValid, + tokenDateIncorrect, + tokenSignatureIncorrect, + apiKeyNotFound, +} + +/// +extension ChatErrorCodeX on ChatErrorCode { + /// + String get message => { + // client error message + ChatErrorCode.networkFailed: 'Response is failed. See cause', + ChatErrorCode.parserError: 'Unable to parse error', + ChatErrorCode.socketClosed: 'Server closed connection', + ChatErrorCode.socketFailure: 'See stack trace in logs. ' + 'Intercept error in error handler of setUser', + ChatErrorCode.cantParseConnectionEvent: + 'Unable to parse connection event', + ChatErrorCode.cantParseEvent: 'Unable to parse event', + ChatErrorCode.invalidToken: 'Invalid token', + ChatErrorCode.undefinedToken: + 'No defined token. Check if client.setUser was called and finished', + ChatErrorCode.unableToParseSocketEvent: + 'Socket event payload either invalid or null', + ChatErrorCode.noErrorBody: 'No error body. See http status code', + + // server error message + ChatErrorCode.authenticationError: + 'Unauthenticated, problem with authentication', + ChatErrorCode.tokenExpired: 'Token expired, new one must be requested.', + ChatErrorCode.tokenNotValid: 'Unauthenticated, token not valid yet', + ChatErrorCode.tokenDateIncorrect: + 'Unauthenticated, token date incorrect', + ChatErrorCode.tokenSignatureIncorrect: + 'Unauthenticated, token signature invalid', + ChatErrorCode.apiKeyNotFound: + "Api key is not found, verify it if it's correct or was created.", + }[this]!; + + /// + int get code => { + // client error codes + ChatErrorCode.networkFailed: 1000, + ChatErrorCode.parserError: 1001, + ChatErrorCode.socketClosed: 1002, + ChatErrorCode.socketFailure: 1003, + ChatErrorCode.cantParseConnectionEvent: 1004, + ChatErrorCode.cantParseEvent: 1005, + ChatErrorCode.invalidToken: 1006, + ChatErrorCode.undefinedToken: 1007, + ChatErrorCode.unableToParseSocketEvent: 1008, + ChatErrorCode.noErrorBody: 1009, + + // server error codes + ChatErrorCode.authenticationError: 5, + ChatErrorCode.tokenExpired: 40, + ChatErrorCode.tokenNotValid: 41, + ChatErrorCode.tokenDateIncorrect: 42, + ChatErrorCode.tokenSignatureIncorrect: 43, + ChatErrorCode.apiKeyNotFound: 2, + }[this]!; + + /// + Set get authenticationErrors => { + ChatErrorCode.authenticationError, + ChatErrorCode.tokenExpired, + ChatErrorCode.tokenNotValid, + ChatErrorCode.tokenDateIncorrect, + ChatErrorCode.tokenSignatureIncorrect, + }.map((it) => it.code).toSet(); + + /// + bool isAuthenticationError(int code) => authenticationErrors.contains(code); +} diff --git a/packages/stream_chat/lib/src/errors/stream_chat_error.dart b/packages/stream_chat/lib/src/errors/stream_chat_error.dart new file mode 100644 index 00000000..280ab6c7 --- /dev/null +++ b/packages/stream_chat/lib/src/errors/stream_chat_error.dart @@ -0,0 +1,77 @@ +import 'package:equatable/equatable.dart'; +import 'package:stream_chat/src/errors/chat_error_code.dart'; +import 'package:stream_chat/stream_chat.dart'; + +/// +class StreamChatError extends Equatable implements Exception { + /// + StreamChatError(ChatErrorCode errorCode) + : code = errorCode.code, + message = errorCode.message; + + /// + const StreamChatError.raw(this.code, this.message); + + /// Error code + final int code; + + /// Error message + final String message; + + @override + List get props => [code, message]; + + @override + String toString() => 'StreamChatError(code: $code, message: $message)'; +} + +/// +class StreamChatNetworkError extends StreamChatError { + /// + StreamChatNetworkError({ + required ChatErrorCode errorCode, + int? statusCode, + this.data, + }) : statusCode = statusCode ?? data?.statusCode, + super(errorCode); + + /// + const StreamChatNetworkError.raw({ + required int code, + required String message, + this.statusCode, + this.data, + }) : super.raw(code, message); + + /// + factory StreamChatNetworkError.fromDioError(DioError error) { + final response = error.response; + ErrorResponse? errorResponse; + final data = response?.data; + if (data != null) { + errorResponse = ErrorResponse.fromJson(data); + } + return StreamChatNetworkError.raw( + code: errorResponse?.code ?? -1, + message: errorResponse?.message ?? response?.statusMessage ?? '', + statusCode: errorResponse?.statusCode ?? response?.statusCode, + data: errorResponse, + ); + } + + /// HTTP status code + final int? statusCode; + + /// Response body. please refer to [ErrorResponse]. + final ErrorResponse? data; + + @override + List get props => [...super.props, statusCode]; + + @override + String toString() => 'StreamChatNetworkError(' + 'code: $code, ' + 'message: $message, ' + 'statusCode: $statusCode, ' + 'data: $data)'; +} diff --git a/packages/stream_chat/lib/src/event_type.dart b/packages/stream_chat/lib/src/event_type.dart index fd93af2a..87529399 100644 --- a/packages/stream_chat/lib/src/event_type.dart +++ b/packages/stream_chat/lib/src/event_type.dart @@ -3,6 +3,9 @@ class EventType { /// Indicates any type of events static const String any = '*'; + /// + static const String healthCheck = 'health.check'; + /// Event sent when a user starts typing a message static const String typingStart = 'typing.start'; diff --git a/packages/stream_chat/lib/src/api/connection_status.dart b/packages/stream_chat/lib/src/ws/connection_status.dart similarity index 100% rename from packages/stream_chat/lib/src/api/connection_status.dart rename to packages/stream_chat/lib/src/ws/connection_status.dart diff --git a/packages/stream_chat/lib/src/ws/socket_error.dart b/packages/stream_chat/lib/src/ws/socket_error.dart new file mode 100644 index 00000000..00b052b9 --- /dev/null +++ b/packages/stream_chat/lib/src/ws/socket_error.dart @@ -0,0 +1,25 @@ +/// +class SocketError { + /// + const SocketError({ + this.code = -1, + this.statusCode = -1, + this.message = '', + }); + + /// + factory SocketError.fromJson(Map json) => SocketError( + code: json['code'], + statusCode: json['StatusCode'], + message: json['message'], + ); + + /// + final int code; + + /// + final int statusCode; + + /// + final String message; +} diff --git a/packages/stream_chat/lib/src/ws/timer_helper.dart b/packages/stream_chat/lib/src/ws/timer_helper.dart new file mode 100644 index 00000000..0974ee80 --- /dev/null +++ b/packages/stream_chat/lib/src/ws/timer_helper.dart @@ -0,0 +1,51 @@ +import 'dart:async'; +import 'package:uuid/uuid.dart'; + +/// +class TimerHelper { + final _uuid = const Uuid(); + late final _timers = {}; + + /// + String setTimer( + Duration duration, + void Function() callback, { + bool immediate = false, + }) { + final id = _uuid.v1(); + final timer = Timer(duration, callback); + if (immediate) callback(); + _timers[id] = timer; + return id; + } + + /// + String setPeriodicTimer( + Duration duration, + void Function(Timer) callback, { + bool immediate = false, + }) { + final id = _uuid.v1(); + final timer = Timer.periodic(duration, callback); + if (immediate) callback.call(timer); + _timers[id] = timer; + return id; + } + + /// + void cancelTimer(String id) { + final timer = _timers.remove(id); + return timer?.cancel(); + } + + /// + void cancelAllTimers() { + for (final t in _timers.values) { + t.cancel(); + } + _timers.clear(); + } + + /// + bool get hasTimers => _timers.isNotEmpty; +} diff --git a/packages/stream_chat/lib/src/ws/websocket.dart b/packages/stream_chat/lib/src/ws/websocket.dart new file mode 100644 index 00000000..0e19f9a6 --- /dev/null +++ b/packages/stream_chat/lib/src/ws/websocket.dart @@ -0,0 +1,395 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:math' as math; + +import 'package:logging/logging.dart'; +import 'package:meta/meta.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:stream_chat/src/ws/connection_status.dart'; +import 'package:stream_chat/src/core/http/token.dart'; +import 'package:stream_chat/src/core/http/token_manager.dart'; +import 'package:stream_chat/src/core/models/event.dart'; +import 'package:stream_chat/src/core/models/user.dart'; +import 'package:stream_chat/src/event_type.dart'; +import 'package:stream_chat/src/ws/socket_error.dart'; +import 'package:stream_chat/src/ws/timer_helper.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; +import 'package:web_socket_channel/status.dart' as status; + +/// Typedef which exposes an [Event] as the only parameter. +typedef EventHandler = void Function(Event); + +/// Typedef used for connecting to a websocket. Method returns a +/// [WebSocketChannel] and accepts a connection [url] and an optional +/// [Iterable] of `protocols`. +typedef WebSocketChannelProvider = WebSocketChannel Function( + Uri uri, { + Iterable? protocols, +}); + +const _tokenExpiredErrorCode = 40; + +/// A WebSocket connection that reconnects upon failure. +class WebSocket with TimerHelper { + /// Creates a new websocket + /// To connect the WS call [connect] + WebSocket({ + required this.apiKey, + required this.baseUrl, + required this.tokenManager, + this.handler, + Logger? logger, + this.webSocketChannelProvider, + this.reconnectionMonitorInterval = 10, + this.healthCheckInterval = 20, + this.reconnectionMonitorTimeout = 40, + }) : _logger = logger; + + /// + final String apiKey; + + /// WS base url + final String baseUrl; + + /// + final TokenManager tokenManager; + + /// Functions that will be called every time a new event is received from the + /// connection + final EventHandler? handler; + + final Logger? _logger; + + /// Connection function + /// Used only for testing purpose + @visibleForTesting + final WebSocketChannelProvider? webSocketChannelProvider; + + /// Interval of the reconnection monitor timer + /// This checks that it received a new event in the last + /// [reconnectionMonitorTimeout] seconds, otherwise it considers the + /// connection unhealthy and reconnects the WS + final int reconnectionMonitorInterval; + + /// Interval of the health event sending timer + /// This sends a health event every [healthCheckInterval] seconds in order to + /// make the server aware that the client is still listening + final int healthCheckInterval; + + /// The timeout that uses the reconnection monitor timer to consider the + /// connection unhealthy + final int reconnectionMonitorTimeout; + + User? _user; + String? _connectionId; + DateTime? _lastEventAt; + WebSocketChannel? _webSocketChannel; + StreamSubscription? _webSocketChannelSubscription; + + /// + Completer? connectionCompleter; + + /// + String? get connectionId => _connectionId; + + final _connectionStatusController = + BehaviorSubject.seeded(ConnectionStatus.disconnected); + + set _connectionStatus(ConnectionStatus status) => + _connectionStatusController.add(status); + + /// The current connection status value + ConnectionStatus get connectionStatus => _connectionStatusController.value!; + + /// This notifies of connection status changes + Stream get connectionStatusStream => + _connectionStatusController.stream.distinct(); + + void _initWebSocketChannel(Uri uri) { + _logger?.info('Initiating connection with $baseUrl'); + if (_webSocketChannel != null) { + _closeWebSocketChannel(); + } + _webSocketChannel = + webSocketChannelProvider?.call(uri) ?? WebSocketChannel.connect(uri); + _subscribeToWebSocketChannel(); + } + + void _closeWebSocketChannel() { + _logger?.info('Closing connection with $baseUrl'); + if (_webSocketChannel != null) { + _unsubscribeFromWebSocketChannel(); + _webSocketChannel?.sink.close(status.goingAway); + _webSocketChannel = null; + } + } + + void _subscribeToWebSocketChannel() { + _logger?.info('Started listening to $baseUrl'); + if (_webSocketChannelSubscription != null) { + _unsubscribeFromWebSocketChannel(); + } + _webSocketChannelSubscription = _webSocketChannel?.stream.listen( + _onDataReceived, + onError: _onConnectionError, + onDone: _onConnectionClosed, + ); + } + + void _unsubscribeFromWebSocketChannel() { + _logger?.info('Stopped listening to $baseUrl'); + if (_webSocketChannelSubscription != null) { + _webSocketChannelSubscription?.cancel(); + _webSocketChannelSubscription = null; + } + } + + Future _buildUri({bool refreshToken = false}) async { + final user = _user!; + final token = await tokenManager.loadToken(refresh: refreshToken); + final params = { + 'user_id': user.id, + 'user_details': user, + 'user_token': token.rawValue, + 'server_determines_connection_id': true, + }; + final qs = { + 'json': jsonEncode(params), + 'api_key': apiKey, + 'authorization': token.rawValue, + 'stream-auth-type': token.authType.raw, + }; + final scheme = baseUrl.startsWith('https') ? 'wss' : 'ws'; + final host = baseUrl.replaceAll(RegExp(r'(^\w+:|^)\/\/'), ''); + return Uri( + scheme: scheme, + host: host, + pathSegments: ['connect'], + queryParameters: qs, + ); + } + + bool _connectRequestInProgress = false; + + /// Connect the WS using the parameters passed in the constructor + Future connect(User user) async { + if (_connectRequestInProgress) { + throw Exception(''' + You've called connect twice, + can only attempt 1 connection at the time, + '''); + } + _connectRequestInProgress = true; + _manuallyClosed = false; + + _user = user; + _connectionStatus = ConnectionStatus.connecting; + connectionCompleter = Completer(); + + final uri = await _buildUri(); + _initWebSocketChannel(uri); + + return connectionCompleter!.future; + } + + int _reconnectAttempt = 0; + bool _reconnectRequestInProgress = false; + + Future _reconnect({bool refreshToken = false}) async { + _logger?.info('Retrying connection : $_reconnectAttempt'); + if (_reconnectRequestInProgress) return; + _reconnectRequestInProgress = true; + + _stopMonitoringEvents(); + // Closing any previously opened web-socket + _closeWebSocketChannel(); + + _reconnectAttempt += 1; + _connectionStatus = ConnectionStatus.connecting; + + final delay = _getReconnectInterval(_reconnectAttempt); + setTimer( + Duration(milliseconds: delay), + () async { + final uri = await _buildUri(refreshToken: refreshToken); + _initWebSocketChannel(uri); + }, + ); + } + + // returns the reconnect interval based on `reconnectAttempt` in milliseconds + int _getReconnectInterval(int reconnectAttempt) { + // try to reconnect in 0.25-25 seconds + // (random to spread out the load from failures) + final max = math.min(500 + reconnectAttempt * 2000, 25000); + final min = math.min( + math.max(250, (reconnectAttempt - 1) * 2000), + 25000, + ); + return (math.Random().nextDouble() * (max - min) + min).floor(); + } + + void _startMonitoringEvents() { + _logger?.info('Starting monitoring events'); + // cancel all previous timers + cancelAllTimers(); + + _startHealthCheck(); + _startReconnectionMonitor(); + } + + void _stopMonitoringEvents() { + _logger?.info('Stopped monitoring events'); + // reset lastEvent + _lastEventAt = null; + + cancelAllTimers(); + } + + void _handleConnectedEvent(Event event) { + // updating connectionId and status + _connectionId = event.connectionId; + _connectionStatus = ConnectionStatus.connected; + + _logger?.info('Connection successful: $_connectionId'); + + // notify user that connection is completed + final completer = connectionCompleter; + if (completer != null && !completer.isCompleted) { + completer.complete(event); + } + + // start monitoring health-check events + _startMonitoringEvents(); + } + + void _handleHealthCheckEvent(Event event) { + _logger?.info('HealthCheck received : ${event.connectionId}'); + + _connectionId = event.connectionId; + _connectionStatus = ConnectionStatus.connected; + } + + void _onDataReceived(dynamic data) { + final jsonData = json.decode(data); + final error = jsonData['error']; + if (error != null) return _onConnectionError(error); + + // resetting connect, reconnect request flag + _resetRequestFlags(resetAttempts: true); + + Event? event; + try { + event = Event.fromJson(jsonData); + } catch (_) {} + + if (event == null) return; + + _lastEventAt = DateTime.now(); + _logger?.info('Event received: ${event.type}'); + + if (event.type == EventType.healthCheck) { + if (event.me != null) { + _handleConnectedEvent(event); + } else { + _handleHealthCheckEvent(event); + } + } + + return handler?.call(event); + } + + void _onConnectionError(error, [stacktrace]) { + _logger?.severe('Error occurred', error, stacktrace); + + // resetting connect, reconnect request flag + _resetRequestFlags(); + + var refreshToken = false; + try { + final socketError = SocketError.fromJson(json.decode(error)); + refreshToken = socketError.code == _tokenExpiredErrorCode; + } catch (_) {} + + // refresh token in case it is expired + _reconnect(refreshToken: refreshToken); + } + + bool _manuallyClosed = false; + + void _onConnectionClosed() { + _logger?.info('Connection closed : $connectionId'); + + // resetting connect, reconnect request flag + _resetRequestFlags(); + + // resetting connection + _connectionId = null; + + // check if we manually closed the connection + if (_manuallyClosed) return; + _reconnect(); + } + + bool get _needsToReconnect { + final lastEventAt = _lastEventAt; + // means not yet connected or disconnected + if (lastEventAt == null) return false; + + // means we missed a health check + final now = DateTime.now(); + return now.difference(lastEventAt).inSeconds > reconnectionMonitorTimeout; + } + + void _resetRequestFlags({bool resetAttempts = false}) { + _connectRequestInProgress = false; + _reconnectRequestInProgress = false; + if (resetAttempts) _reconnectAttempt = 0; + } + + void _startReconnectionMonitor() { + _logger?.info('Starting reconnection monitor'); + setPeriodicTimer( + Duration(seconds: reconnectionMonitorInterval), + (_) { + final needsToReconnect = _needsToReconnect; + _logger?.info('Needs to reconnect : $needsToReconnect'); + if (needsToReconnect) _reconnect(); + }, + immediate: true, + ); + } + + void _startHealthCheck() { + _logger?.info('Starting health check monitor'); + setPeriodicTimer( + Duration(seconds: healthCheckInterval), + (_) { + _logger?.info('Sending Event: ${EventType.healthCheck}'); + final event = Event( + type: EventType.healthCheck, + connectionId: connectionId, + ); + _webSocketChannel?.sink.add(jsonEncode(event)); + }, + immediate: true, + ); + } + + /// Disconnects the WS and releases eventual resources + void disconnect() { + if (connectionStatus == ConnectionStatus.disconnected) return; + _connectionStatus = ConnectionStatus.disconnected; + + _logger?.info('Disconnecting $connectionId'); + + // resetting user + _user = null; + connectionCompleter = null; + + _stopMonitoringEvents(); + + _manuallyClosed = true; + _closeWebSocketChannel(); + } +} diff --git a/packages/stream_chat/lib/stream_chat.dart b/packages/stream_chat/lib/stream_chat.dart index 2ac56b10..de49dcdd 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -8,31 +8,32 @@ export 'package:dio/src/options.dart' show ProgressCallback; export 'package:logging/logging.dart' show Logger, Level; export './src/api/channel.dart'; -export './src/api/connection_status.dart'; -export './src/api/requests.dart'; -export './src/api/requests.dart'; -export './src/api/responses.dart'; -export './src/attachment_file_uploader.dart' show AttachmentFileUploader; export './src/client.dart'; +export './src/core/api/attachment_file_uploader.dart' + show AttachmentFileUploader; +export './src/core/models/action.dart'; +export './src/core/models/attachment.dart'; +export './src/core/models/attachment_file.dart'; +export './src/core/models/channel_config.dart'; +export './src/core/models/channel_model.dart'; +export './src/core/models/channel_state.dart'; +export './src/core/models/command.dart'; +export './src/core/models/device.dart'; +export './src/core/models/event.dart'; +export './src/core/models/filter.dart' show Filter; +export './src/core/models/member.dart'; +export './src/core/models/message.dart'; +export './src/core/models/mute.dart'; +export './src/core/models/own_user.dart'; +export './src/core/models/reaction.dart'; +export './src/core/models/read.dart'; +export './src/core/models/user.dart'; export './src/db/chat_persistence_client.dart'; export './src/event_type.dart'; export './src/exceptions.dart'; export './src/extensions/rate_limit.dart'; export './src/extensions/string_extension.dart'; -export './src/models/action.dart'; -export './src/models/attachment.dart'; -export './src/models/attachment_file.dart'; -export './src/models/channel_config.dart'; -export './src/models/channel_model.dart'; -export './src/models/channel_state.dart'; -export './src/models/command.dart'; -export './src/models/device.dart'; -export './src/models/event.dart'; -export './src/models/filter.dart' show Filter; -export './src/models/member.dart'; -export './src/models/message.dart'; -export './src/models/mute.dart'; -export './src/models/own_user.dart'; -export './src/models/reaction.dart'; -export './src/models/read.dart'; -export './src/models/user.dart'; +export 'src/core/api/requests.dart'; +export 'src/core/api/requests.dart'; +export 'src/core/api/responses.dart'; +export 'src/ws/connection_status.dart'; diff --git a/packages/stream_chat/pubspec.yaml b/packages/stream_chat/pubspec.yaml index 6b8ee0aa..4fd82f3f 100644 --- a/packages/stream_chat/pubspec.yaml +++ b/packages/stream_chat/pubspec.yaml @@ -15,6 +15,7 @@ dependencies: equatable: ^2.0.0 freezed_annotation: ^0.14.0 http_parser: ^4.0.0 + jose: ^0.3.2 json_annotation: ^4.0.1 logging: ^1.0.1 meta: ^1.3.0 diff --git a/packages/stream_chat/test/src/api/channel_test.dart b/packages/stream_chat/test/src/api/channel_test.dart index c3409b7e..901eb4af 100644 --- a/packages/stream_chat/test/src/api/channel_test.dart +++ b/packages/stream_chat/test/src/api/channel_test.dart @@ -3,13 +3,13 @@ import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:dio/native_imp.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat/src/api/requests.dart'; +import 'package:stream_chat/src/core/api/requests.dart'; import 'package:stream_chat/src/client.dart'; import 'package:stream_chat/src/event_type.dart'; -import 'package:stream_chat/src/models/event.dart'; -import 'package:stream_chat/src/models/message.dart'; -import 'package:stream_chat/src/models/own_user.dart'; -import 'package:stream_chat/src/models/reaction.dart'; +import 'package:stream_chat/src/core/models/event.dart'; +import 'package:stream_chat/src/core/models/message.dart'; +import 'package:stream_chat/src/core/models/own_user.dart'; +import 'package:stream_chat/src/core/models/reaction.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:test/test.dart'; diff --git a/packages/stream_chat/test/src/api/responses_test.dart b/packages/stream_chat/test/src/api/responses_test.dart index f91248af..77e7abc3 100644 --- a/packages/stream_chat/test/src/api/responses_test.dart +++ b/packages/stream_chat/test/src/api/responses_test.dart @@ -1,12 +1,12 @@ import 'dart:convert'; import 'package:test/test.dart'; -import 'package:stream_chat/src/api/responses.dart'; -import 'package:stream_chat/src/models/device.dart'; -import 'package:stream_chat/src/models/member.dart'; -import 'package:stream_chat/src/models/message.dart'; -import 'package:stream_chat/src/models/reaction.dart'; -import 'package:stream_chat/src/models/read.dart'; +import 'package:stream_chat/src/core/api/responses.dart'; +import 'package:stream_chat/src/core/models/device.dart'; +import 'package:stream_chat/src/core/models/member.dart'; +import 'package:stream_chat/src/core/models/message.dart'; +import 'package:stream_chat/src/core/models/reaction.dart'; +import 'package:stream_chat/src/core/models/read.dart'; import 'package:stream_chat/stream_chat.dart'; void main() { diff --git a/packages/stream_chat/test/src/api/web_socket_stub_test.dart b/packages/stream_chat/test/src/api/web_socket_stub_test.dart index 7f5bf4b1..77a67421 100644 --- a/packages/stream_chat/test/src/api/web_socket_stub_test.dart +++ b/packages/stream_chat/test/src/api/web_socket_stub_test.dart @@ -4,7 +4,7 @@ import 'package:stream_chat/src/api/web_socket_channel_stub.dart'; void main() { test('src/api/web_socket_stub_test', () { expect( - () => connectWebSocket('fakeurl'), + () => openConnection('fakeurl'), throwsA(isA()), ); }); diff --git a/packages/stream_chat/test/src/api/websocket_test.dart b/packages/stream_chat/test/src/api/websocket_test.dart index b9c897a6..11270a9e 100644 --- a/packages/stream_chat/test/src/api/websocket_test.dart +++ b/packages/stream_chat/test/src/api/websocket_test.dart @@ -2,10 +2,10 @@ import 'dart:async'; import 'package:logging/logging.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat/src/api/connection_status.dart'; -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/src/ws/connection_status.dart'; +import 'package:stream_chat/src/ws/websocket.dart'; +import 'package:stream_chat/src/core/models/event.dart'; +import 'package:stream_chat/src/core/models/user.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:test/test.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; @@ -39,7 +39,7 @@ void main() { final ws = WebSocket( baseUrl: 'baseurl', user: User(id: 'testid'), - logger: Logger('ws'), + _logger: Logger('ws'), connectParams: {'test': 'true'}, connectPayload: {'payload': 'test'}, handler: print, @@ -77,7 +77,7 @@ void main() { final ws = WebSocket( baseUrl: 'baseurl', user: User(id: 'testid'), - logger: Logger('ws'), + _logger: Logger('ws'), connectParams: {'test': 'true'}, connectPayload: {'payload': 'test'}, handler: handleFunc, @@ -113,7 +113,7 @@ void main() { final ws = WebSocket( baseUrl: 'baseurl', user: User(id: 'testid'), - logger: Logger('ws'), + _logger: Logger('ws'), connectParams: {'test': 'true'}, connectPayload: {'payload': 'test'}, handler: handleFunc, @@ -153,7 +153,7 @@ void main() { final ws = WebSocket( baseUrl: 'baseurl', user: User(id: 'testid'), - logger: Logger('ws'), + _logger: Logger('ws'), connectParams: {'test': 'true'}, connectPayload: {'payload': 'test'}, handler: handleFunc, @@ -189,7 +189,7 @@ void main() { final ws = WebSocket( baseUrl: 'baseurl', user: User(id: 'testid'), - logger: Logger('ws'), + _logger: Logger('ws'), connectParams: {'test': 'true'}, connectPayload: {'payload': 'test'}, handler: handleFunc, @@ -235,7 +235,7 @@ void main() { final ws = WebSocket( baseUrl: 'baseurl', user: User(id: 'testid'), - logger: Logger('ws'), + _logger: Logger('ws'), connectParams: {'test': 'true'}, connectPayload: {'payload': 'test'}, handler: handleFunc, @@ -280,7 +280,7 @@ void main() { final ws = WebSocket( baseUrl: 'baseurl', user: User(id: 'testid'), - logger: Logger('ws'), + _logger: Logger('ws'), connectParams: {'test': 'true'}, connectPayload: {'payload': 'test'}, handler: handleFunc, @@ -318,7 +318,7 @@ void main() { final ws = WebSocket( baseUrl: 'baseurl', user: User(id: 'testid'), - logger: Logger('ws'), + _logger: Logger('ws'), connectParams: {'test': 'true'}, connectPayload: {'payload': 'test'}, handler: print, diff --git a/packages/stream_chat/test/src/client_test.dart b/packages/stream_chat/test/src/client_test.dart index b2a5bbac..751d7a50 100644 --- a/packages/stream_chat/test/src/client_test.dart +++ b/packages/stream_chat/test/src/client_test.dart @@ -6,13 +6,13 @@ import 'package:dio/dio.dart'; import 'package:dio/native_imp.dart'; import 'package:logging/logging.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat/src/api/requests.dart'; +import 'package:stream_chat/src/core/api/requests.dart'; import 'package:stream_chat/src/client.dart'; import 'package:stream_chat/src/exceptions.dart'; -import 'package:stream_chat/src/models/channel_model.dart'; -import 'package:stream_chat/src/models/filter.dart'; -import 'package:stream_chat/src/models/message.dart'; -import 'package:stream_chat/src/models/user.dart'; +import 'package:stream_chat/src/core/models/channel_model.dart'; +import 'package:stream_chat/src/core/models/filter.dart'; +import 'package:stream_chat/src/core/models/message.dart'; +import 'package:stream_chat/src/core/models/user.dart'; import 'package:test/test.dart'; class MockDio extends Mock implements DioForNative {} @@ -55,8 +55,8 @@ void main() { expect(client.baseURL, 'chat-us-east-1.stream-io-api.com'); expect(client.apiKey, 'api-key'); expect(client.logLevel, Level.WARNING); - expect(client.httpClient.options.connectTimeout, 6000); - expect(client.httpClient.options.receiveTimeout, 6000); + expect(client.httpClient._options.connectTimeout, 6000); + expect(client.httpClient._options.receiveTimeout, 6000); }); test('should create the object correctly', overridePrint(() { @@ -76,8 +76,8 @@ void main() { expect(client.baseURL, 'test.com'); expect(client.apiKey, 'api-key'); expect(Logger.root.level, Level.INFO); - expect(client.httpClient.options.connectTimeout, 10000); - expect(client.httpClient.options.receiveTimeout, 12000); + expect(client.httpClient._options.connectTimeout, 10000); + expect(client.httpClient._options.receiveTimeout, 12000); client.logger.warning('test'); client.logger.config('test config'); @@ -170,7 +170,7 @@ void main() { await client.queryChannelsOnline( filter: queryFilter, sort: sortOptions, - options: options, + _options: options, paginationParams: paginationParams, waitForConnect: false, ); @@ -419,7 +419,7 @@ void main() { await client.queryUsers( filter: queryFilter, sort: sortOptions, - options: options, + _options: options, ); verify(() => diff --git a/packages/stream_chat/test/src/models/filter_test.dart b/packages/stream_chat/test/src/models/filter_test.dart index ac026c6c..663ab702 100644 --- a/packages/stream_chat/test/src/models/filter_test.dart +++ b/packages/stream_chat/test/src/models/filter_test.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'package:test/test.dart'; -import 'package:stream_chat/src/models/filter.dart'; +import 'package:stream_chat/src/core/models/filter.dart'; void main() { group('operators', () { diff --git a/packages/stream_chat/test/version_test.dart b/packages/stream_chat/test/version_test.dart index 34ecc750..8e7112a4 100644 --- a/packages/stream_chat/test/version_test.dart +++ b/packages/stream_chat/test/version_test.dart @@ -1,5 +1,7 @@ +import 'dart:async'; import 'dart:io'; +import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat/version.dart'; import 'package:test/test.dart'; @@ -12,13 +14,38 @@ void prepareTest() { void main() { prepareTest(); - test('stream chat version matches pubspec', () { - final pubspecPath = '${Directory.current.path}/pubspec.yaml'; - final pubspec = File(pubspecPath).readAsStringSync(); - // ignore: unnecessary_string_escapes - final regex = RegExp('version:\s*(.*)'); - final match = regex.firstMatch(pubspec); - expect(match, isNotNull); - expect(PACKAGE_VERSION, match?.group(1)?.trim()); + test('stream chat version matches pubspec', () async { + /// Create a new instance of [StreamChatClient] + /// by passing the apikey obtained from your project dashboard. + final client = StreamChatClient('b67pax5b2wdq', logLevel: Level.INFO); + + /// Set the current user. In a production scenario, this should be done using + /// a backend to generate a user token using our server SDK. + /// Please see the following for more information: + /// https://getstream.io/chat/docs/ios_user_setup_and_tokens/ + await client.connectUser( + User( + id: 'cool-shadow-7', + extraData: { + 'image': + 'https://getstream.io/random_png/?id=cool-shadow-7&name=Cool+shadow', + }, + ), + '''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiY29vbC1zaGFkb3ctNyJ9.gkOlCRb1qgy4joHPaxFwPOdXcGvSPvp6QY0S4mpRkVo''', + ); + + try { + await client.banUser('asdasdas'); + } catch (e) { + print(e); + } + + // final pubspecPath = '${Directory.current.path}/pubspec.yaml'; + // final pubspec = File(pubspecPath).readAsStringSync(); + // // ignore: unnecessary_string_escapes + // final regex = RegExp('version:\s*(.*)'); + // final match = regex.firstMatch(pubspec); + // expect(match, isNotNull); + // expect(PACKAGE_VERSION, match?.group(1)?.trim()); }); } diff --git a/packages/stream_chat_flutter/example/pubspec.yaml b/packages/stream_chat_flutter/example/pubspec.yaml index 3feec144..408bf9e1 100644 --- a/packages/stream_chat_flutter/example/pubspec.yaml +++ b/packages/stream_chat_flutter/example/pubspec.yaml @@ -23,6 +23,10 @@ environment: dependencies: flutter: sdk: flutter +# stream_chat: +# path: ../../stream_chat +# stream_chat_flutter_core: +# path: ../../stream_chat_flutter_core stream_chat_flutter: path: ../ stream_chat_persistence: diff --git a/packages/stream_chat_flutter/lib/src/channel_info.dart b/packages/stream_chat_flutter/lib/src/channel_info.dart index f59ac2e0..05622812 100644 --- a/packages/stream_chat_flutter/lib/src/channel_info.dart +++ b/packages/stream_chat_flutter/lib/src/channel_info.dart @@ -132,10 +132,9 @@ class ChannelInfo extends StatelessWidget { vertical: VisualDensity.minimumDensity, ), ), - onPressed: () async { - await client.disconnect(); - await client.connect(); - }, + onPressed: () => client + ..closeConnection() + ..openConnection(), child: Text( 'Try Again', style: textStyle?.copyWith( diff --git a/packages/stream_chat_flutter/lib/src/channel_list_header.dart b/packages/stream_chat_flutter/lib/src/channel_list_header.dart index 325736f1..1c7befcf 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_header.dart @@ -253,10 +253,9 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { ), ), TextButton( - onPressed: () async { - await client.disconnect(); - await client.connect(); - }, + onPressed: () => client + ..closeConnection() + ..openConnection(), child: Text( 'Try Again', style: chatThemeData.channelListHeaderTheme.title?.copyWith( diff --git a/packages/stream_chat_flutter/lib/src/channel_list_view.dart b/packages/stream_chat_flutter/lib/src/channel_list_view.dart index 766fe4ba..0fa7bca4 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -58,9 +58,15 @@ class ChannelListView extends StatefulWidget { const ChannelListView({ Key? key, this.filter, - this.options, this.sort, - this.pagination, + this.state = true, + this.watch = true, + this.presence = false, + this.memberLimit, + this.messageLimit, + this.pagination = const PaginationParams( + limit: 25, + ), this.onChannelTap, this.onChannelLongPress, this.channelWidget, @@ -88,12 +94,6 @@ class ChannelListView extends StatefulWidget { /// You can also filter other built-in channel fields. final Filter? filter; - /// Query channels options. - /// - /// state: if true returns the Channel state - /// watch: if true listen to changes to this Channel in real time. - final Map? options; - /// The sorting used for the channels matching the filters. /// Sorting is based on field and direction, multiple sorting options /// can be provided. @@ -102,11 +102,22 @@ class ChannelListView extends StatefulWidget { /// Direction can be ascending or descending. final List>? sort; + /// If true returns the Channel state + final bool state; + + /// If true listen to changes to this Channel in real time. + final bool watch; + + final bool presence; + + final int? memberLimit; + final int? messageLimit; + /// Pagination parameters /// limit: the number of channels to return (max is 30) /// offset: the offset (max is 1000) /// message_limit: how many messages should be included to each channel - final PaginationParams? pagination; + final PaginationParams pagination; /// Function called when tapping on a channel /// By default it calls [Navigator.push] building a [MaterialPageRoute] @@ -170,13 +181,14 @@ class _ChannelListViewState extends State { @override Widget build(BuildContext context) { Widget child = ChannelListCore( - pagination: widget.pagination ?? - const PaginationParams( - limit: 25, - ), - options: widget.options, - sort: widget.sort, filter: widget.filter, + sort: widget.sort, + state: widget.state, + watch: widget.watch, + presence: widget.presence, + memberLimit: widget.memberLimit, + messageLimit: widget.messageLimit, + pagination: widget.pagination, channelListController: _channelListController, listBuilder: widget.listBuilder ?? _buildListView, emptyBuilder: widget.emptyBuilder ?? _buildEmptyWidget, diff --git a/packages/stream_chat_flutter/lib/src/user_list_view.dart b/packages/stream_chat_flutter/lib/src/user_list_view.dart index 3b1efd04..aa1886b7 100644 --- a/packages/stream_chat_flutter/lib/src/user_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/user_list_view.dart @@ -47,8 +47,8 @@ class UserListView extends StatefulWidget { const UserListView({ Key? key, this.filter, - this.options, this.sort, + this.presence, this.pagination, this.onUserTap, this.onUserLongPress, @@ -75,12 +75,6 @@ class UserListView extends StatefulWidget { /// You can also filter other built-in channel fields. final Filter? filter; - /// Query channels options. - /// - /// state: if true returns the Channel state - /// watch: if true listen to changes to this Channel in real time. - final Map? options; - /// The sorting used for the channels matching the filters. /// Sorting is based on field and direction, multiple sorting options can /// be provided. @@ -89,6 +83,9 @@ class UserListView extends StatefulWidget { /// Direction can be ascending or descending. final List? sort; + /// + final bool? presence; + /// Pagination parameters /// limit: the number of users to return (max is 30) /// offset: the offset (max is 1000) @@ -177,9 +174,9 @@ class _UserListViewState extends State listBuilder: widget.listBuilder ?? (context, list) => _buildListView(list), pagination: widget.pagination, - options: widget.options, sort: widget.sort, filter: widget.filter, + presence: widget.presence, groupAlphabetically: widget.groupAlphabetically, userListController: _userListController, ); diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index 1b2dbe2f..9d3e7bc2 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -53,6 +53,12 @@ flutter: - animations/ uses-material-design: true +dependency_overrides: + stream_chat: + path: ../stream_chat + stream_chat_flutter_core: + path: ../stream_chat_flutter_core + dev_dependencies: flutter_test: sdk: flutter diff --git a/packages/stream_chat_flutter_core/lib/src/channel_list_core.dart b/packages/stream_chat_flutter_core/lib/src/channel_list_core.dart index a3be273d..684fd630 100644 --- a/packages/stream_chat_flutter_core/lib/src/channel_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/channel_list_core.dart @@ -63,7 +63,11 @@ class ChannelListCore extends StatefulWidget { required this.loadingBuilder, required this.listBuilder, this.filter, - this.options, + this.state = true, + this.watch = true, + this.presence = false, + this.memberLimit, + this.messageLimit, this.sort, this.pagination = const PaginationParams( limit: 25, @@ -94,11 +98,11 @@ class ChannelListCore extends StatefulWidget { /// You can also filter other built-in channel fields. final Filter? filter; - /// Query channels options. - /// - /// state: if true returns the Channel state - /// watch: if true listen to changes to this Channel in real time. - final Map? options; + // bool state = true, + // bool watch = true, + // bool presence = false, + // int? memberLimit, + // int? messageLimit, /// The sorting used for the channels matching the filters. /// Sorting is based on field and direction, multiple sorting options can be @@ -107,6 +111,17 @@ class ChannelListCore extends StatefulWidget { /// _at or member_count. Direction can be ascending or descending. final List>? sort; + /// If true returns the Channel state + final bool state; + + /// If true listen to changes to this Channel in real time. + final bool watch; + + final bool presence; + + final int? memberLimit; + final int? messageLimit; + /// Pagination parameters /// limit: the number of channels to return (max is 30) /// offset: the offset (max is 1000) @@ -149,18 +164,26 @@ class ChannelListCoreState extends State { Future loadData() => _channelsBloc.queryChannels( filter: widget.filter, sortOptions: widget.sort, + state: widget.state, + watch: widget.watch, + presence: widget.presence, + memberLimit: widget.memberLimit, + messageLimit: widget.messageLimit, paginationParams: widget.pagination, - options: widget.options, ); /// Fetches more channels with updated pagination and updates the widget Future paginateData() => _channelsBloc.queryChannels( filter: widget.filter, sortOptions: widget.sort, + state: widget.state, + watch: widget.watch, + presence: widget.presence, + memberLimit: widget.memberLimit, + messageLimit: widget.messageLimit, paginationParams: widget.pagination.copyWith( offset: _channelsBloc.channels?.length ?? 0, ), - options: widget.options, ); StreamSubscription? _subscription; @@ -200,7 +223,11 @@ class ChannelListCoreState extends State { if (widget.filter?.toString() != oldWidget.filter?.toString() || jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) || - widget.options?.toString() != oldWidget.options?.toString() || + widget.state != oldWidget.state || + widget.watch != oldWidget.watch || + widget.presence != oldWidget.presence || + widget.messageLimit != oldWidget.messageLimit || + widget.memberLimit != oldWidget.memberLimit || widget.pagination.toJson().toString() != oldWidget.pagination.toJson().toString()) { loadData(); diff --git a/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart b/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart index d46ee4bd..5e2475b4 100644 --- a/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart +++ b/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart @@ -95,8 +95,13 @@ class ChannelsBlocState extends State Future queryChannels({ Filter? filter, List>? sortOptions, + bool state = true, + bool watch = true, + bool presence = false, + int? memberLimit, + int? messageLimit, + bool waitForConnect = true, PaginationParams paginationParams = const PaginationParams(limit: 30), - Map? options, }) async { final client = _streamChatCoreState!.client; @@ -117,7 +122,12 @@ class ChannelsBlocState extends State await for (final channels in client.queryChannels( filter: filter, sort: sortOptions, - options: options, + state: state, + watch: watch, + presence: presence, + memberLimit: memberLimit, + messageLimit: messageLimit, + waitForConnect: waitForConnect, paginationParams: paginationParams, )) { newChannels = channels; diff --git a/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart b/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart index 89d4853a..0bf78b2e 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart @@ -109,7 +109,7 @@ class StreamChatCoreState extends State if (user != null) { if (state == AppLifecycleState.paused) { if (widget.onBackgroundEventReceived == null) { - client.disconnect(); + client.closeConnection(); return; } _eventSubscription = client.on().listen( @@ -118,7 +118,7 @@ class StreamChatCoreState extends State void onTimerComplete() { _eventSubscription?.cancel(); - client.disconnect(); + client.closeConnection(); } _disconnectTimer = Timer(widget.backgroundKeepAlive, onTimerComplete); @@ -128,7 +128,7 @@ class StreamChatCoreState extends State _disconnectTimer?.cancel(); } else { if (client.wsConnectionStatus == ConnectionStatus.disconnected) { - client.connect(); + client.openConnection(); } } } diff --git a/packages/stream_chat_flutter_core/lib/src/user_list_core.dart b/packages/stream_chat_flutter_core/lib/src/user_list_core.dart index d34c563c..6377686b 100644 --- a/packages/stream_chat_flutter_core/lib/src/user_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/user_list_core.dart @@ -63,8 +63,8 @@ class UserListCore extends StatefulWidget { required this.listBuilder, Key? key, this.filter, - this.options, this.sort, + this.presence, this.pagination, this.groupAlphabetically = false, this.userListController, @@ -92,18 +92,15 @@ class UserListCore extends StatefulWidget { /// You can also filter other built-in channel fields. final Filter? filter; - /// Query channels options. - /// - /// state: if true returns the Channel state - /// watch: if true listen to changes to this Channel in real time. - final Map? 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. final List? sort; + /// + final bool? presence; + /// Pagination parameters /// limit: the number of users to return (max is 30) /// offset: the offset (max is 1000) @@ -186,22 +183,22 @@ class UserListCoreState extends State }, ); - // ignore: public_member_api_docs + /// Fetches initial users and updates the widget Future loadData() => _usersBloc!.queryUsers( filter: widget.filter, sort: widget.sort, + presence: widget.presence, pagination: widget.pagination, - options: widget.options, ); - // ignore: public_member_api_docs + /// Fetches more users with updated pagination and updates the widget Future paginateData() => _usersBloc!.queryUsers( filter: widget.filter, sort: widget.sort, + presence: widget.presence, pagination: widget.pagination!.copyWith( offset: _usersBloc!.users?.length ?? 0, ), - options: widget.options, ); @override @@ -209,7 +206,7 @@ class UserListCoreState extends State super.didUpdateWidget(oldWidget); if (widget.filter?.toString() != oldWidget.filter?.toString() || jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) || - widget.options?.toString() != oldWidget.options?.toString() || + widget.presence != oldWidget.presence || widget.pagination?.toJson().toString() != oldWidget.pagination?.toJson().toString()) { loadData(); diff --git a/packages/stream_chat_flutter_core/lib/src/users_bloc.dart b/packages/stream_chat_flutter_core/lib/src/users_bloc.dart index 57edfecb..6a02904d 100644 --- a/packages/stream_chat_flutter_core/lib/src/users_bloc.dart +++ b/packages/stream_chat_flutter_core/lib/src/users_bloc.dart @@ -63,7 +63,7 @@ class UsersBlocState extends State Future queryUsers({ Filter? filter, List? sort, - Map? options, + bool? presence, PaginationParams? pagination, }) async { final client = _streamChatCore.client; @@ -82,7 +82,7 @@ class UsersBlocState extends State final usersResponse = await client.queryUsers( filter: filter, sort: sort, - options: options, + presence: presence, pagination: pagination, ); diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml index c84242b8..f26bb2cf 100644 --- a/packages/stream_chat_flutter_core/pubspec.yaml +++ b/packages/stream_chat_flutter_core/pubspec.yaml @@ -17,6 +17,10 @@ dependencies: rxdart: ^0.26.0 stream_chat: ^2.0.0-nullsafety.1 +dependency_overrides: + stream_chat: + path: ../stream_chat + dev_dependencies: fake_async: ^1.2.0 flutter_test: diff --git a/packages/stream_chat_flutter_core/test/channel_list_core_test.dart b/packages/stream_chat_flutter_core/test/channel_list_core_test.dart index 14cbc232..a69f81c9 100644 --- a/packages/stream_chat_flutter_core/test/channel_list_core_test.dart +++ b/packages/stream_chat_flutter_core/test/channel_list_core_test.dart @@ -142,7 +142,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: pagination, )).thenThrow(error); @@ -162,7 +166,11 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: pagination, )).called(1); }, @@ -191,7 +199,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: pagination, )).thenAnswer((_) => Stream.value(channels)); @@ -211,7 +223,11 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: pagination, )).called(1); }, @@ -240,7 +256,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: pagination, )).thenAnswer((_) => Stream.value(channels)); @@ -260,7 +280,11 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: pagination, )).called(1); }, @@ -297,7 +321,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: pagination, )).thenAnswer((_) => Stream.value(channels)); @@ -321,7 +349,11 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: pagination, )).called(1); @@ -335,7 +367,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: updatedPagination, )).thenAnswer((_) => Stream.value(paginatedChannels)); @@ -355,7 +391,11 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: updatedPagination, )).called(1); }, @@ -396,7 +436,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: pagination, )).thenAnswer((_) => Stream.value(channels)); @@ -424,7 +468,11 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: pagination, )).called(1); @@ -436,7 +484,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: updatedPagination, )).thenAnswer((_) => Stream.value(updatedChannels)); @@ -451,7 +503,11 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: updatedPagination, )).called(1); }, diff --git a/packages/stream_chat_flutter_core/test/channels_bloc_test.dart b/packages/stream_chat_flutter_core/test/channels_bloc_test.dart index ff373878..3c0286a0 100644 --- a/packages/stream_chat_flutter_core/test/channels_bloc_test.dart +++ b/packages/stream_chat_flutter_core/test/channels_bloc_test.dart @@ -114,7 +114,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), )).thenAnswer( (_) => Stream.fromIterable([offlineChannels, onlineChannels]), @@ -133,7 +137,11 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), )).called(1); }, @@ -176,7 +184,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), )).thenThrow(error); @@ -190,7 +202,11 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), )).called(1); }, @@ -228,7 +244,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), )).thenAnswer((_) => Stream.value(channels)); @@ -245,7 +265,11 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), )).called(1); @@ -257,7 +281,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: paginationParams, )).thenAnswer( (_) => Stream.value(newChannels), @@ -279,7 +307,11 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: paginationParams, )).called(1); }, @@ -320,7 +352,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: paginationParams, )).thenAnswer((_) => Stream.value(channels)); @@ -336,7 +372,11 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: paginationParams, )).called(1); @@ -345,7 +385,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: paginationParams, )).thenThrow(error); @@ -359,7 +403,11 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: paginationParams, )).called(1); }, @@ -404,7 +452,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), )).thenAnswer( (_) => Stream.value(channels), @@ -415,7 +467,11 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), )).called(1); @@ -476,7 +532,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), )).thenAnswer( (_) => Stream.value(channels), @@ -487,11 +547,16 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), )).called(1); final channelDeletedOrNotificationRemovedEvent = Event( + type: EventType.channelDeleted, channel: EventChannel( cid: channels.first.cid!, updatedAt: DateTime.now(), @@ -557,7 +622,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), )).thenAnswer( (_) => Stream.value(channels), @@ -568,7 +637,11 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), )).called(1); @@ -648,7 +721,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), )).thenAnswer( (_) => Stream.value(channels), @@ -659,7 +736,11 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), )).called(1); @@ -735,7 +816,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), )).thenAnswer( (_) => Stream.value(channels), @@ -746,7 +831,11 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), )).called(1); @@ -813,7 +902,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), )).thenAnswer( (_) => Stream.value(channels), @@ -824,7 +917,11 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), )).called(1); diff --git a/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart b/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart index bc5324b8..94b38238 100644 --- a/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart +++ b/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart @@ -51,7 +51,7 @@ void main() { ); testWidgets( - 'didChangeAppLifecycleState should call client.disconnect() and return ' + 'didChangeAppLifecycleState should call client.closeConnection and return ' 'if onBackgroundEventReceived is null and the widget lifestyle changes to ' 'AppLifecycleState.paused', (tester) async { @@ -69,7 +69,7 @@ void main() { expect(find.byKey(streamChatCoreKey), findsOneWidget); expect(find.byKey(childKey), findsOneWidget); - when(() => mockClient.disconnect()).thenAnswer((_) async { + when(() => mockClient.closeConnection()).thenAnswer((_) async { return; }); @@ -79,7 +79,7 @@ void main() { streamChatCoreState.didChangeAppLifecycleState(AppLifecycleState.paused); - verify(() => mockClient.disconnect()).called(1); + verify(() => mockClient.closeConnection()).called(1); }, ); @@ -107,9 +107,9 @@ void main() { expect(find.byKey(streamChatCoreKey), findsOneWidget); expect(find.byKey(childKey), findsOneWidget); - final event = Event(); + final event = Event(type: EventType.any); when(() => mockClient.on()).thenAnswer((_) => Stream.value(event)); - when(() => mockClient.disconnect()).thenAnswer((_) async { + when(() => mockClient.closeConnection()).thenAnswer((_) async { return; }); @@ -126,7 +126,7 @@ void main() { await Future.delayed(backgroundKeepAlive); - verify(() => mockClient.disconnect()).called(1); + verify(() => mockClient.closeConnection()).called(1); verifyNever(() => mockOnBackgroundEventReceived.call(event)); }); }, @@ -156,7 +156,7 @@ void main() { expect(find.byKey(streamChatCoreKey), findsOneWidget); expect(find.byKey(childKey), findsOneWidget); - final event = Event(); + final event = Event(type: EventType.any); when(() => mockClient.on()).thenAnswer((_) => Stream.value(event)); final streamChatCoreState = tester.state( @@ -198,10 +198,10 @@ void main() { expect(find.byKey(streamChatCoreKey), findsOneWidget); expect(find.byKey(childKey), findsOneWidget); - final event = Event(); + final event = Event(type: EventType.any); when(() => mockClient.on()).thenAnswer((_) => Stream.value(event)); - when(() => mockClient.connect()).thenAnswer((_) async => event); - when(mockClient.disconnect).thenAnswer((_) async => null); + when(() => mockClient.openConnection()).thenAnswer((_) async => event); + when(() => mockClient.closeConnection()).thenAnswer((_) async => null); when(() => mockClient.wsConnectionStatus) .thenReturn(ConnectionStatus.disconnected); @@ -217,7 +217,7 @@ void main() { streamChatCoreState .didChangeAppLifecycleState(AppLifecycleState.resumed); - verify(() => mockClient.connect()).called(1); + verify(() => mockClient.openConnection()).called(1); }); }, ); diff --git a/packages/stream_chat_flutter_core/test/user_list_core_test.dart b/packages/stream_chat_flutter_core/test/user_list_core_test.dart index 820e479f..6f21123f 100644 --- a/packages/stream_chat_flutter_core/test/user_list_core_test.dart +++ b/packages/stream_chat_flutter_core/test/user_list_core_test.dart @@ -134,7 +134,7 @@ void main() { when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).thenThrow(error); @@ -154,7 +154,7 @@ void main() { verify(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).called(1); }, @@ -179,7 +179,7 @@ void main() { when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).thenAnswer((_) async => QueryUsersResponse()..users = users); @@ -199,7 +199,7 @@ void main() { verify(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).called(1); }, @@ -224,7 +224,7 @@ void main() { when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).thenAnswer((_) async => QueryUsersResponse()..users = users); @@ -244,7 +244,7 @@ void main() { verify(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).called(1); }, @@ -283,7 +283,7 @@ void main() { when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).thenAnswer((_) async => QueryUsersResponse()..users = users); @@ -310,7 +310,7 @@ void main() { verify(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).called(1); }, @@ -352,7 +352,7 @@ void main() { when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).thenAnswer((_) async => QueryUsersResponse()..users = users); @@ -379,7 +379,7 @@ void main() { verify(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).called(1); @@ -393,7 +393,7 @@ void main() { when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: updatedPagination, )) .thenAnswer( @@ -411,7 +411,7 @@ void main() { verify(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: updatedPagination, )).called(1); }, @@ -457,7 +457,7 @@ void main() { when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).thenAnswer((_) async => QueryUsersResponse()..users = users); @@ -488,7 +488,7 @@ void main() { verify(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).called(1); @@ -500,7 +500,7 @@ void main() { when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: updatedPagination, )) .thenAnswer((_) async => QueryUsersResponse()..users = updatedUsers); @@ -515,7 +515,7 @@ void main() { verify(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: updatedPagination, )).called(1); }, diff --git a/packages/stream_chat_flutter_core/test/users_bloc_test.dart b/packages/stream_chat_flutter_core/test/users_bloc_test.dart index 2722d08a..b659ca34 100644 --- a/packages/stream_chat_flutter_core/test/users_bloc_test.dart +++ b/packages/stream_chat_flutter_core/test/users_bloc_test.dart @@ -72,7 +72,7 @@ void main() { when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).thenAnswer((_) async => QueryUsersResponse()..users = users); @@ -86,7 +86,7 @@ void main() { verify(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).called(1); }, @@ -121,7 +121,7 @@ void main() { when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).thenThrow(error); @@ -135,7 +135,7 @@ void main() { verify(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).called(1); }, @@ -171,7 +171,7 @@ void main() { when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).thenAnswer((_) async => QueryUsersResponse()..users = users); @@ -185,7 +185,7 @@ void main() { verify(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).called(1); @@ -196,7 +196,7 @@ void main() { when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: pagination, )) .thenAnswer( @@ -218,7 +218,7 @@ void main() { verify(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: pagination, )).called(1); }, @@ -254,7 +254,7 @@ void main() { when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).thenAnswer((_) async => QueryUsersResponse()..users = users); @@ -268,7 +268,7 @@ void main() { verify(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).called(1); @@ -280,7 +280,7 @@ void main() { when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: pagination, )).thenThrow(error); @@ -294,7 +294,7 @@ void main() { verify(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: pagination, )).called(1); }, diff --git a/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.dart b/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.dart index 4a169cd2..0cef23b0 100644 --- a/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.dart @@ -29,8 +29,9 @@ class ConnectionEventDao extends DatabaseAccessor return into(connectionEvents).insert( ConnectionEventEntity( id: 1, + type: event.type, lastSyncAt: connectionInfo?.lastSyncAt, - lastEventAt: event.createdAt ?? connectionInfo?.lastEventAt, + lastEventAt: event.createdAt, totalUnreadCount: event.totalUnreadCount ?? connectionInfo?.totalUnreadCount, ownUser: event.me?.toJson() ?? connectionInfo?.ownUser, diff --git a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.g.dart b/packages/stream_chat_persistence/lib/src/db/moor_chat_database.g.dart index b8d404b2..09156642 100644 --- a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.g.dart +++ b/packages/stream_chat_persistence/lib/src/db/moor_chat_database.g.dart @@ -60,31 +60,30 @@ class ChannelEntity extends DataClass implements Insertable { Map data, GeneratedDatabase db, {String? prefix}) { final effectivePrefix = prefix ?? ''; - final stringType = db.typeSystem.forDartType(); - final boolType = db.typeSystem.forDartType(); - final dateTimeType = db.typeSystem.forDartType(); - final intType = db.typeSystem.forDartType(); return ChannelEntity( - id: stringType.mapFromDatabaseResponse(data['${effectivePrefix}id'])!, - type: stringType.mapFromDatabaseResponse(data['${effectivePrefix}type'])!, - cid: stringType.mapFromDatabaseResponse(data['${effectivePrefix}cid'])!, - config: $ChannelsTable.$converter0.mapToDart(stringType + id: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}id'])!, + type: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}type'])!, + cid: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}cid'])!, + config: $ChannelsTable.$converter0.mapToDart(const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}config']))!, - frozen: - boolType.mapFromDatabaseResponse(data['${effectivePrefix}frozen'])!, - lastMessageAt: dateTimeType + frozen: const BoolType() + .mapFromDatabaseResponse(data['${effectivePrefix}frozen'])!, + lastMessageAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}last_message_at']), - createdAt: dateTimeType + createdAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}created_at'])!, - updatedAt: dateTimeType + updatedAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}updated_at'])!, - deletedAt: dateTimeType + deletedAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}deleted_at']), - memberCount: intType + memberCount: const IntType() .mapFromDatabaseResponse(data['${effectivePrefix}member_count'])!, - createdById: stringType + createdById: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}created_by_id']), - extraData: $ChannelsTable.$converter1.mapToDart(stringType + extraData: $ChannelsTable.$converter1.mapToDart(const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}extra_data'])), ); } @@ -226,7 +225,7 @@ class ChannelEntity extends DataClass implements Insertable { $mrjc(createdById.hashCode, extraData.hashCode)))))))))))); @override - bool operator ==(dynamic other) => + bool operator ==(Object other) => identical(this, other) || (other is ChannelEntity && other.id == this.id && @@ -742,56 +741,54 @@ class MessageEntity extends DataClass implements Insertable { Map data, GeneratedDatabase db, {String? prefix}) { final effectivePrefix = prefix ?? ''; - final stringType = db.typeSystem.forDartType(); - final intType = db.typeSystem.forDartType(); - final boolType = db.typeSystem.forDartType(); - final dateTimeType = db.typeSystem.forDartType(); return MessageEntity( - id: stringType.mapFromDatabaseResponse(data['${effectivePrefix}id'])!, - messageText: stringType + id: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}id'])!, + messageText: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}message_text']), - attachments: $MessagesTable.$converter0.mapToDart(stringType + attachments: $MessagesTable.$converter0.mapToDart(const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}attachments']))!, - status: $MessagesTable.$converter1.mapToDart( - intType.mapFromDatabaseResponse(data['${effectivePrefix}status']))!, - type: stringType.mapFromDatabaseResponse(data['${effectivePrefix}type'])!, - mentionedUsers: $MessagesTable.$converter2.mapToDart(stringType + status: $MessagesTable.$converter1.mapToDart(const IntType() + .mapFromDatabaseResponse(data['${effectivePrefix}status']))!, + type: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}type'])!, + mentionedUsers: $MessagesTable.$converter2.mapToDart(const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}mentioned_users']))!, - reactionCounts: $MessagesTable.$converter3.mapToDart(stringType + reactionCounts: $MessagesTable.$converter3.mapToDart(const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}reaction_counts'])), - reactionScores: $MessagesTable.$converter4.mapToDart(stringType + reactionScores: $MessagesTable.$converter4.mapToDart(const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}reaction_scores'])), - parentId: stringType + parentId: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}parent_id']), - quotedMessageId: stringType + quotedMessageId: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}quoted_message_id']), - replyCount: intType + replyCount: const IntType() .mapFromDatabaseResponse(data['${effectivePrefix}reply_count']), - showInChannel: boolType + showInChannel: const BoolType() .mapFromDatabaseResponse(data['${effectivePrefix}show_in_channel']), - shadowed: - boolType.mapFromDatabaseResponse(data['${effectivePrefix}shadowed'])!, - command: - stringType.mapFromDatabaseResponse(data['${effectivePrefix}command']), - createdAt: dateTimeType + shadowed: const BoolType() + .mapFromDatabaseResponse(data['${effectivePrefix}shadowed'])!, + command: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}command']), + createdAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}created_at'])!, - updatedAt: dateTimeType + updatedAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}updated_at'])!, - deletedAt: dateTimeType + deletedAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}deleted_at']), - userId: - stringType.mapFromDatabaseResponse(data['${effectivePrefix}user_id']), - pinned: - boolType.mapFromDatabaseResponse(data['${effectivePrefix}pinned'])!, - pinnedAt: dateTimeType + userId: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}user_id']), + pinned: const BoolType() + .mapFromDatabaseResponse(data['${effectivePrefix}pinned'])!, + pinnedAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}pinned_at']), - pinExpires: dateTimeType + pinExpires: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}pin_expires']), - pinnedByUserId: stringType + pinnedByUserId: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}pinned_by_user_id']), - channelCid: stringType + channelCid: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}channel_cid']), - extraData: $MessagesTable.$converter5.mapToDart(stringType + extraData: $MessagesTable.$converter5.mapToDart(const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}extra_data'])), ); } @@ -1066,7 +1063,7 @@ class MessageEntity extends DataClass implements Insertable { pinned.hashCode, $mrjc(pinnedAt.hashCode, $mrjc(pinExpires.hashCode, $mrjc(pinnedByUserId.hashCode, $mrjc(channelCid.hashCode, extraData.hashCode)))))))))))))))))))))))); @override - bool operator ==(dynamic other) => + bool operator ==(Object other) => identical(this, other) || (other is MessageEntity && other.id == this.id && @@ -1923,56 +1920,57 @@ class PinnedMessageEntity extends DataClass Map data, GeneratedDatabase db, {String? prefix}) { final effectivePrefix = prefix ?? ''; - final stringType = db.typeSystem.forDartType(); - final intType = db.typeSystem.forDartType(); - final boolType = db.typeSystem.forDartType(); - final dateTimeType = db.typeSystem.forDartType(); return PinnedMessageEntity( - id: stringType.mapFromDatabaseResponse(data['${effectivePrefix}id'])!, - messageText: stringType + id: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}id'])!, + messageText: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}message_text']), - attachments: $PinnedMessagesTable.$converter0.mapToDart(stringType + attachments: $PinnedMessagesTable.$converter0.mapToDart(const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}attachments']))!, - status: $PinnedMessagesTable.$converter1.mapToDart( - intType.mapFromDatabaseResponse(data['${effectivePrefix}status']))!, - type: stringType.mapFromDatabaseResponse(data['${effectivePrefix}type'])!, - mentionedUsers: $PinnedMessagesTable.$converter2.mapToDart(stringType - .mapFromDatabaseResponse(data['${effectivePrefix}mentioned_users']))!, - reactionCounts: $PinnedMessagesTable.$converter3.mapToDart(stringType - .mapFromDatabaseResponse(data['${effectivePrefix}reaction_counts'])), - reactionScores: $PinnedMessagesTable.$converter4.mapToDart(stringType - .mapFromDatabaseResponse(data['${effectivePrefix}reaction_scores'])), - parentId: stringType + status: $PinnedMessagesTable.$converter1.mapToDart(const IntType() + .mapFromDatabaseResponse(data['${effectivePrefix}status']))!, + type: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}type'])!, + mentionedUsers: $PinnedMessagesTable.$converter2.mapToDart( + const StringType().mapFromDatabaseResponse( + data['${effectivePrefix}mentioned_users']))!, + reactionCounts: $PinnedMessagesTable.$converter3.mapToDart( + const StringType().mapFromDatabaseResponse( + data['${effectivePrefix}reaction_counts'])), + reactionScores: $PinnedMessagesTable.$converter4.mapToDart( + const StringType().mapFromDatabaseResponse( + data['${effectivePrefix}reaction_scores'])), + parentId: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}parent_id']), - quotedMessageId: stringType + quotedMessageId: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}quoted_message_id']), - replyCount: intType + replyCount: const IntType() .mapFromDatabaseResponse(data['${effectivePrefix}reply_count']), - showInChannel: boolType + showInChannel: const BoolType() .mapFromDatabaseResponse(data['${effectivePrefix}show_in_channel']), - shadowed: - boolType.mapFromDatabaseResponse(data['${effectivePrefix}shadowed'])!, - command: - stringType.mapFromDatabaseResponse(data['${effectivePrefix}command']), - createdAt: dateTimeType + shadowed: const BoolType() + .mapFromDatabaseResponse(data['${effectivePrefix}shadowed'])!, + command: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}command']), + createdAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}created_at'])!, - updatedAt: dateTimeType + updatedAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}updated_at'])!, - deletedAt: dateTimeType + deletedAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}deleted_at']), - userId: - stringType.mapFromDatabaseResponse(data['${effectivePrefix}user_id']), - pinned: - boolType.mapFromDatabaseResponse(data['${effectivePrefix}pinned'])!, - pinnedAt: dateTimeType + userId: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}user_id']), + pinned: const BoolType() + .mapFromDatabaseResponse(data['${effectivePrefix}pinned'])!, + pinnedAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}pinned_at']), - pinExpires: dateTimeType + pinExpires: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}pin_expires']), - pinnedByUserId: stringType + pinnedByUserId: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}pinned_by_user_id']), - channelCid: stringType + channelCid: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}channel_cid']), - extraData: $PinnedMessagesTable.$converter5.mapToDart(stringType + extraData: $PinnedMessagesTable.$converter5.mapToDart(const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}extra_data'])), ); } @@ -2247,7 +2245,7 @@ class PinnedMessageEntity extends DataClass pinned.hashCode, $mrjc(pinnedAt.hashCode, $mrjc(pinExpires.hashCode, $mrjc(pinnedByUserId.hashCode, $mrjc(channelCid.hashCode, extraData.hashCode)))))))))))))))))))))))); @override - bool operator ==(dynamic other) => + bool operator ==(Object other) => identical(this, other) || (other is PinnedMessageEntity && other.id == this.id && @@ -3031,19 +3029,18 @@ class ReactionEntity extends DataClass implements Insertable { Map data, GeneratedDatabase db, {String? prefix}) { final effectivePrefix = prefix ?? ''; - final stringType = db.typeSystem.forDartType(); - final dateTimeType = db.typeSystem.forDartType(); - final intType = db.typeSystem.forDartType(); return ReactionEntity( - userId: stringType + userId: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}user_id'])!, - messageId: stringType + messageId: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}message_id'])!, - type: stringType.mapFromDatabaseResponse(data['${effectivePrefix}type'])!, - createdAt: dateTimeType + type: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}type'])!, + createdAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}created_at'])!, - score: intType.mapFromDatabaseResponse(data['${effectivePrefix}score'])!, - extraData: $ReactionsTable.$converter0.mapToDart(stringType + score: const IntType() + .mapFromDatabaseResponse(data['${effectivePrefix}score'])!, + extraData: $ReactionsTable.$converter0.mapToDart(const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}extra_data'])), ); } @@ -3125,7 +3122,7 @@ class ReactionEntity extends DataClass implements Insertable { $mrjc(createdAt.hashCode, $mrjc(score.hashCode, extraData.hashCode)))))); @override - bool operator ==(dynamic other) => + bool operator ==(Object other) => identical(this, other) || (other is ReactionEntity && other.userId == this.userId && @@ -3395,23 +3392,22 @@ class UserEntity extends DataClass implements Insertable { factory UserEntity.fromData(Map data, GeneratedDatabase db, {String? prefix}) { final effectivePrefix = prefix ?? ''; - final stringType = db.typeSystem.forDartType(); - final dateTimeType = db.typeSystem.forDartType(); - final boolType = db.typeSystem.forDartType(); return UserEntity( - id: stringType.mapFromDatabaseResponse(data['${effectivePrefix}id'])!, - role: stringType.mapFromDatabaseResponse(data['${effectivePrefix}role']), - createdAt: dateTimeType + id: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}id'])!, + role: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}role']), + createdAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}created_at'])!, - updatedAt: dateTimeType + updatedAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}updated_at'])!, - lastActive: dateTimeType + lastActive: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}last_active']), - online: - boolType.mapFromDatabaseResponse(data['${effectivePrefix}online'])!, - banned: - boolType.mapFromDatabaseResponse(data['${effectivePrefix}banned'])!, - extraData: $UsersTable.$converter0.mapToDart(stringType + online: const BoolType() + .mapFromDatabaseResponse(data['${effectivePrefix}online'])!, + banned: const BoolType() + .mapFromDatabaseResponse(data['${effectivePrefix}banned'])!, + extraData: $UsersTable.$converter0.mapToDart(const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}extra_data']))!, ); } @@ -3513,7 +3509,7 @@ class UserEntity extends DataClass implements Insertable { $mrjc(online.hashCode, $mrjc(banned.hashCode, extraData.hashCode)))))))); @override - bool operator ==(dynamic other) => + bool operator ==(Object other) => identical(this, other) || (other is UserEntity && other.id == this.id && @@ -3841,30 +3837,28 @@ class MemberEntity extends DataClass implements Insertable { factory MemberEntity.fromData(Map data, GeneratedDatabase db, {String? prefix}) { final effectivePrefix = prefix ?? ''; - final stringType = db.typeSystem.forDartType(); - final dateTimeType = db.typeSystem.forDartType(); - final boolType = db.typeSystem.forDartType(); return MemberEntity( - userId: stringType + userId: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}user_id'])!, - channelCid: stringType + channelCid: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}channel_cid'])!, - role: stringType.mapFromDatabaseResponse(data['${effectivePrefix}role']), - inviteAcceptedAt: dateTimeType.mapFromDatabaseResponse( + role: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}role']), + inviteAcceptedAt: const DateTimeType().mapFromDatabaseResponse( data['${effectivePrefix}invite_accepted_at']), - inviteRejectedAt: dateTimeType.mapFromDatabaseResponse( + inviteRejectedAt: const DateTimeType().mapFromDatabaseResponse( data['${effectivePrefix}invite_rejected_at']), - invited: - boolType.mapFromDatabaseResponse(data['${effectivePrefix}invited'])!, - banned: - boolType.mapFromDatabaseResponse(data['${effectivePrefix}banned'])!, - shadowBanned: boolType + invited: const BoolType() + .mapFromDatabaseResponse(data['${effectivePrefix}invited'])!, + banned: const BoolType() + .mapFromDatabaseResponse(data['${effectivePrefix}banned'])!, + shadowBanned: const BoolType() .mapFromDatabaseResponse(data['${effectivePrefix}shadow_banned'])!, - isModerator: boolType + isModerator: const BoolType() .mapFromDatabaseResponse(data['${effectivePrefix}is_moderator'])!, - createdAt: dateTimeType + createdAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}created_at'])!, - updatedAt: dateTimeType + updatedAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}updated_at'])!, ); } @@ -3997,7 +3991,7 @@ class MemberEntity extends DataClass implements Insertable { $mrjc(createdAt.hashCode, updatedAt.hashCode))))))))))); @override - bool operator ==(dynamic other) => + bool operator ==(Object other) => identical(this, other) || (other is MemberEntity && other.userId == this.userId && @@ -4396,17 +4390,14 @@ class ReadEntity extends DataClass implements Insertable { factory ReadEntity.fromData(Map data, GeneratedDatabase db, {String? prefix}) { final effectivePrefix = prefix ?? ''; - final dateTimeType = db.typeSystem.forDartType(); - final stringType = db.typeSystem.forDartType(); - final intType = db.typeSystem.forDartType(); return ReadEntity( - lastRead: dateTimeType + lastRead: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}last_read'])!, - userId: stringType + userId: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}user_id'])!, - channelCid: stringType + channelCid: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}channel_cid'])!, - unreadMessages: intType + unreadMessages: const IntType() .mapFromDatabaseResponse(data['${effectivePrefix}unread_messages'])!, ); } @@ -4469,7 +4460,7 @@ class ReadEntity extends DataClass implements Insertable { $mrjc(userId.hashCode, $mrjc(channelCid.hashCode, unreadMessages.hashCode)))); @override - bool operator ==(dynamic other) => + bool operator ==(Object other) => identical(this, other) || (other is ReadEntity && other.lastRead == this.lastRead && @@ -4666,11 +4657,10 @@ class ChannelQueryEntity extends DataClass Map data, GeneratedDatabase db, {String? prefix}) { final effectivePrefix = prefix ?? ''; - final stringType = db.typeSystem.forDartType(); return ChannelQueryEntity( - queryHash: stringType + queryHash: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}query_hash'])!, - channelCid: stringType + channelCid: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}channel_cid'])!, ); } @@ -4716,7 +4706,7 @@ class ChannelQueryEntity extends DataClass @override int get hashCode => $mrjf($mrjc(queryHash.hashCode, channelCid.hashCode)); @override - bool operator ==(dynamic other) => + bool operator ==(Object other) => identical(this, other) || (other is ChannelQueryEntity && other.queryHash == this.queryHash && @@ -4851,6 +4841,9 @@ class ConnectionEventEntity extends DataClass /// event id final int id; + /// event type + final String type; + /// User object of the current user final Map? ownUser; @@ -4867,6 +4860,7 @@ class ConnectionEventEntity extends DataClass final DateTime? lastSyncAt; ConnectionEventEntity( {required this.id, + required this.type, this.ownUser, this.totalUnreadCount, this.unreadChannels, @@ -4876,20 +4870,20 @@ class ConnectionEventEntity extends DataClass Map data, GeneratedDatabase db, {String? prefix}) { final effectivePrefix = prefix ?? ''; - final intType = db.typeSystem.forDartType(); - final stringType = db.typeSystem.forDartType(); - final dateTimeType = db.typeSystem.forDartType(); return ConnectionEventEntity( - id: intType.mapFromDatabaseResponse(data['${effectivePrefix}id'])!, - ownUser: $ConnectionEventsTable.$converter0.mapToDart(stringType + id: const IntType() + .mapFromDatabaseResponse(data['${effectivePrefix}id'])!, + type: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}type'])!, + ownUser: $ConnectionEventsTable.$converter0.mapToDart(const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}own_user'])), - totalUnreadCount: intType.mapFromDatabaseResponse( + totalUnreadCount: const IntType().mapFromDatabaseResponse( data['${effectivePrefix}total_unread_count']), - unreadChannels: intType + unreadChannels: const IntType() .mapFromDatabaseResponse(data['${effectivePrefix}unread_channels']), - lastEventAt: dateTimeType + lastEventAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}last_event_at']), - lastSyncAt: dateTimeType + lastSyncAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}last_sync_at']), ); } @@ -4897,6 +4891,7 @@ class ConnectionEventEntity extends DataClass Map toColumns(bool nullToAbsent) { final map = {}; map['id'] = Variable(id); + map['type'] = Variable(type); if (!nullToAbsent || ownUser != null) { final converter = $ConnectionEventsTable.$converter0; map['own_user'] = Variable(converter.mapToSql(ownUser)); @@ -4921,6 +4916,7 @@ class ConnectionEventEntity extends DataClass serializer ??= moorRuntimeOptions.defaultSerializer; return ConnectionEventEntity( id: serializer.fromJson(json['id']), + type: serializer.fromJson(json['type']), ownUser: serializer.fromJson?>(json['ownUser']), totalUnreadCount: serializer.fromJson(json['totalUnreadCount']), unreadChannels: serializer.fromJson(json['unreadChannels']), @@ -4933,6 +4929,7 @@ class ConnectionEventEntity extends DataClass serializer ??= moorRuntimeOptions.defaultSerializer; return { 'id': serializer.toJson(id), + 'type': serializer.toJson(type), 'ownUser': serializer.toJson?>(ownUser), 'totalUnreadCount': serializer.toJson(totalUnreadCount), 'unreadChannels': serializer.toJson(unreadChannels), @@ -4943,6 +4940,7 @@ class ConnectionEventEntity extends DataClass ConnectionEventEntity copyWith( {int? id, + String? type, Value?> ownUser = const Value.absent(), Value totalUnreadCount = const Value.absent(), Value unreadChannels = const Value.absent(), @@ -4950,6 +4948,7 @@ class ConnectionEventEntity extends DataClass Value lastSyncAt = const Value.absent()}) => ConnectionEventEntity( id: id ?? this.id, + type: type ?? this.type, ownUser: ownUser.present ? ownUser.value : this.ownUser, totalUnreadCount: totalUnreadCount.present ? totalUnreadCount.value @@ -4963,6 +4962,7 @@ class ConnectionEventEntity extends DataClass String toString() { return (StringBuffer('ConnectionEventEntity(') ..write('id: $id, ') + ..write('type: $type, ') ..write('ownUser: $ownUser, ') ..write('totalUnreadCount: $totalUnreadCount, ') ..write('unreadChannels: $unreadChannels, ') @@ -4976,16 +4976,19 @@ class ConnectionEventEntity extends DataClass int get hashCode => $mrjf($mrjc( id.hashCode, $mrjc( - ownUser.hashCode, + type.hashCode, $mrjc( - totalUnreadCount.hashCode, - $mrjc(unreadChannels.hashCode, - $mrjc(lastEventAt.hashCode, lastSyncAt.hashCode)))))); + ownUser.hashCode, + $mrjc( + totalUnreadCount.hashCode, + $mrjc(unreadChannels.hashCode, + $mrjc(lastEventAt.hashCode, lastSyncAt.hashCode))))))); @override - bool operator ==(dynamic other) => + bool operator ==(Object other) => identical(this, other) || (other is ConnectionEventEntity && other.id == this.id && + other.type == this.type && other.ownUser == this.ownUser && other.totalUnreadCount == this.totalUnreadCount && other.unreadChannels == this.unreadChannels && @@ -4995,6 +4998,7 @@ class ConnectionEventEntity extends DataClass class ConnectionEventsCompanion extends UpdateCompanion { final Value id; + final Value type; final Value?> ownUser; final Value totalUnreadCount; final Value unreadChannels; @@ -5002,6 +5006,7 @@ class ConnectionEventsCompanion extends UpdateCompanion { final Value lastSyncAt; const ConnectionEventsCompanion({ this.id = const Value.absent(), + this.type = const Value.absent(), this.ownUser = const Value.absent(), this.totalUnreadCount = const Value.absent(), this.unreadChannels = const Value.absent(), @@ -5010,14 +5015,16 @@ class ConnectionEventsCompanion extends UpdateCompanion { }); ConnectionEventsCompanion.insert({ this.id = const Value.absent(), + required String type, this.ownUser = const Value.absent(), this.totalUnreadCount = const Value.absent(), this.unreadChannels = const Value.absent(), this.lastEventAt = const Value.absent(), this.lastSyncAt = const Value.absent(), - }); + }) : type = Value(type); static Insertable custom({ Expression? id, + Expression? type, Expression?>? ownUser, Expression? totalUnreadCount, Expression? unreadChannels, @@ -5026,6 +5033,7 @@ class ConnectionEventsCompanion extends UpdateCompanion { }) { return RawValuesInsertable({ if (id != null) 'id': id, + if (type != null) 'type': type, if (ownUser != null) 'own_user': ownUser, if (totalUnreadCount != null) 'total_unread_count': totalUnreadCount, if (unreadChannels != null) 'unread_channels': unreadChannels, @@ -5036,6 +5044,7 @@ class ConnectionEventsCompanion extends UpdateCompanion { ConnectionEventsCompanion copyWith( {Value? id, + Value? type, Value?>? ownUser, Value? totalUnreadCount, Value? unreadChannels, @@ -5043,6 +5052,7 @@ class ConnectionEventsCompanion extends UpdateCompanion { Value? lastSyncAt}) { return ConnectionEventsCompanion( id: id ?? this.id, + type: type ?? this.type, ownUser: ownUser ?? this.ownUser, totalUnreadCount: totalUnreadCount ?? this.totalUnreadCount, unreadChannels: unreadChannels ?? this.unreadChannels, @@ -5057,6 +5067,9 @@ class ConnectionEventsCompanion extends UpdateCompanion { if (id.present) { map['id'] = Variable(id.value); } + if (type.present) { + map['type'] = Variable(type.value); + } if (ownUser.present) { final converter = $ConnectionEventsTable.$converter0; map['own_user'] = Variable(converter.mapToSql(ownUser.value)); @@ -5080,6 +5093,7 @@ class ConnectionEventsCompanion extends UpdateCompanion { String toString() { return (StringBuffer('ConnectionEventsCompanion(') ..write('id: $id, ') + ..write('type: $type, ') ..write('ownUser: $ownUser, ') ..write('totalUnreadCount: $totalUnreadCount, ') ..write('unreadChannels: $unreadChannels, ') @@ -5106,6 +5120,17 @@ class $ConnectionEventsTable extends ConnectionEvents ); } + final VerificationMeta _typeMeta = const VerificationMeta('type'); + @override + late final GeneratedTextColumn type = _constructType(); + GeneratedTextColumn _constructType() { + return GeneratedTextColumn( + 'type', + $tableName, + false, + ); + } + final VerificationMeta _ownUserMeta = const VerificationMeta('ownUser'); @override late final GeneratedTextColumn ownUser = _constructOwnUser(); @@ -5165,8 +5190,15 @@ class $ConnectionEventsTable extends ConnectionEvents } @override - List get $columns => - [id, ownUser, totalUnreadCount, unreadChannels, lastEventAt, lastSyncAt]; + List get $columns => [ + id, + type, + ownUser, + totalUnreadCount, + unreadChannels, + lastEventAt, + lastSyncAt + ]; @override $ConnectionEventsTable get asDslTable => this; @override @@ -5182,6 +5214,12 @@ class $ConnectionEventsTable extends ConnectionEvents if (data.containsKey('id')) { context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); } + if (data.containsKey('type')) { + context.handle( + _typeMeta, type.isAcceptableOrUnknown(data['type']!, _typeMeta)); + } else if (isInserting) { + context.missing(_typeMeta); + } context.handle(_ownUserMeta, const VerificationResult.success()); if (data.containsKey('total_unread_count')) { context.handle( diff --git a/packages/stream_chat_persistence/lib/src/entity/connection_events.dart b/packages/stream_chat_persistence/lib/src/entity/connection_events.dart index cf0819ae..9e91a9cb 100644 --- a/packages/stream_chat_persistence/lib/src/entity/connection_events.dart +++ b/packages/stream_chat_persistence/lib/src/entity/connection_events.dart @@ -8,6 +8,9 @@ class ConnectionEvents extends Table { /// event id IntColumn get id => integer()(); + /// event type + TextColumn get type => text()(); + /// User object of the current user TextColumn get ownUser => text().nullable().map(MapConverter())(); diff --git a/packages/stream_chat_persistence/lib/src/mapper/event_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/event_mapper.dart index c5cc4198..f0350797 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/event_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/event_mapper.dart @@ -5,6 +5,8 @@ import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; extension ConnectionEventX on ConnectionEventEntity { /// Maps a [ConnectionEventEntity] into [Event] Event toEvent() => Event( + type: type, + createdAt: lastEventAt, me: ownUser != null ? OwnUser.fromJson(ownUser!) : null, totalUnreadCount: totalUnreadCount, unreadChannels: unreadChannels, diff --git a/packages/stream_chat_persistence/pubspec.yaml b/packages/stream_chat_persistence/pubspec.yaml index 5d1927c3..a512c69a 100644 --- a/packages/stream_chat_persistence/pubspec.yaml +++ b/packages/stream_chat_persistence/pubspec.yaml @@ -20,6 +20,10 @@ dependencies: sqlite3_flutter_libs: ^0.4.1 stream_chat: ^2.0.0-nullsafety.0 +dependency_overrides: + stream_chat: + path: ../stream_chat + dev_dependencies: build_runner: ^2.0.1 mocktail: ^0.1.1 diff --git a/packages/stream_chat_persistence/test/src/mapper/event_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/event_mapper_test.dart index cb977aa5..ffa8ee62 100644 --- a/packages/stream_chat_persistence/test/src/mapper/event_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/event_mapper_test.dart @@ -3,19 +3,26 @@ import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; import 'package:stream_chat_persistence/src/mapper/event_mapper.dart'; +import '../utils/date_matcher.dart'; + void main() { test('toEvent should map entity into Event', () { + const type = 'dummy.type'; + final now = DateTime.now(); final ownUser = OwnUser(id: 'testUserId'); final entity = ConnectionEventEntity( id: 3, + type: type, ownUser: ownUser.toJson(), totalUnreadCount: 33, unreadChannels: 33, - lastSyncAt: DateTime.now(), - lastEventAt: DateTime.now(), + lastSyncAt: now, + lastEventAt: now, ); final event = entity.toEvent(); expect(event, isA()); + expect(event.type, type); + expect(event.createdAt, isSameDateAs(now)); expect(event.me!.id, ownUser.id); expect(event.totalUnreadCount, entity.totalUnreadCount); expect(event.unreadChannels, entity.unreadChannels); diff --git a/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart b/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart index 8a275201..e771954b 100644 --- a/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart +++ b/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart @@ -79,7 +79,7 @@ void main() { }); test('getConnectionInfo', () async { - const event = Event(type: 'testEvent'); + final event = Event(); when(() => mockDatabase.connectionEventDao.connectionEvent) .thenAnswer((_) async => event); @@ -100,7 +100,7 @@ void main() { }); test('updateConnectionInfo', () async { - const event = Event(type: 'testEvent'); + final event = Event(); when(() => mockDatabase.connectionEventDao.updateConnectionEvent(event)) .thenAnswer((_) async => 1); From 73aa8f58e3545c2f408edecacc3dee16d0015b47 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 27 May 2021 20:10:45 +0530 Subject: [PATCH 004/150] merge fixes Signed-off-by: Sahil Kumar --- .../lib/src/core/http/interceptor/logging_interceptor.dart | 4 ++-- .../stream_chat/lib/src/core/http/stream_http_client.dart | 1 - packages/stream_chat/lib/src/ws/websocket.dart | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/stream_chat/lib/src/core/http/interceptor/logging_interceptor.dart b/packages/stream_chat/lib/src/core/http/interceptor/logging_interceptor.dart index a9075e34..41c36326 100644 --- a/packages/stream_chat/lib/src/core/http/interceptor/logging_interceptor.dart +++ b/packages/stream_chat/lib/src/core/http/interceptor/logging_interceptor.dart @@ -309,12 +309,12 @@ class LoggingInterceptor extends Interceptor { }); } - bool _canFlattenMap(Map map) => +/* bool _canFlattenMap(Map map) => map.values.where((dynamic val) => val is Map || val is List).isEmpty && map.toString().length < maxWidth; bool _canFlattenList(List list) => - list.length < 10 && list.toString().length < maxWidth; + list.length < 10 && list.toString().length < maxWidth;*/ void _printMapAsTable( void Function(Object) logPrint, diff --git a/packages/stream_chat/lib/src/core/http/stream_http_client.dart b/packages/stream_chat/lib/src/core/http/stream_http_client.dart index ae0a5f72..43149714 100644 --- a/packages/stream_chat/lib/src/core/http/stream_http_client.dart +++ b/packages/stream_chat/lib/src/core/http/stream_http_client.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:math'; import 'package:dio/dio.dart'; import 'package:logging/logging.dart'; diff --git a/packages/stream_chat/lib/src/ws/websocket.dart b/packages/stream_chat/lib/src/ws/websocket.dart index 0e19f9a6..6ebb7a7b 100644 --- a/packages/stream_chat/lib/src/ws/websocket.dart +++ b/packages/stream_chat/lib/src/ws/websocket.dart @@ -99,7 +99,7 @@ class WebSocket with TimerHelper { _connectionStatusController.add(status); /// The current connection status value - ConnectionStatus get connectionStatus => _connectionStatusController.value!; + ConnectionStatus get connectionStatus => _connectionStatusController.value; /// This notifies of connection status changes Stream get connectionStatusStream => From 9201ebb2f3e01c7d09ddb6135846e09da1b0bccd Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 27 May 2021 20:26:52 +0530 Subject: [PATCH 005/150] improvements Signed-off-by: Sahil Kumar --- .../http/interceptor/auth_interceptor.dart | 11 +++-- .../lib/src/core/http/stream_http_client.dart | 46 +++++++++++++++++-- 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart b/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart index 19d0293b..a75b2d2c 100644 --- a/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart +++ b/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart @@ -1,6 +1,7 @@ import 'package:dio/dio.dart'; import 'package:stream_chat/src/core/api/responses.dart'; import 'package:stream_chat/src/core/http/stream_chat_dio_error.dart'; +import 'package:stream_chat/src/core/http/stream_http_client.dart'; import 'package:stream_chat/src/core/http/token.dart'; import 'package:stream_chat/src/core/http/token_manager.dart'; @@ -10,9 +11,9 @@ import 'package:stream_chat/src/errors/stream_chat_error.dart'; /// class AuthInterceptor extends Interceptor { /// - AuthInterceptor(this._httpClient, this._tokenManager); + AuthInterceptor(this._client, this._tokenManager); - final Dio _httpClient; + final StreamHttpClient _client; /// final TokenManager _tokenManager; @@ -52,12 +53,12 @@ class AuthInterceptor extends Interceptor { if (data != null) error = ErrorResponse.fromJson(data); if (error?.code == ChatErrorCode.tokenExpired.code) { if (_tokenManager.isStatic) return handler.next(err); - _httpClient.lock(); + _client.lock(); await _tokenManager.loadToken(refresh: true); - _httpClient.unlock(); + _client.unlock(); try { final options = err.requestOptions; - final response = await _httpClient.request( + final response = await _client.request( options.path, cancelToken: options.cancelToken, data: options.data, diff --git a/packages/stream_chat/lib/src/core/http/stream_http_client.dart b/packages/stream_chat/lib/src/core/http/stream_http_client.dart index 43149714..e5c1e82b 100644 --- a/packages/stream_chat/lib/src/core/http/stream_http_client.dart +++ b/packages/stream_chat/lib/src/core/http/stream_http_client.dart @@ -40,7 +40,7 @@ class StreamHttpClient { 'Content-Encoding': 'application/gzip', } ..interceptors.addAll([ - if (tokenManager != null) AuthInterceptor(httpClient, tokenManager), + if (tokenManager != null) AuthInterceptor(this, tokenManager), if (connectionIdManager != null) ConnectionIdInterceptor(connectionIdManager), if (logger != null && logger.level != Level.OFF) @@ -74,9 +74,23 @@ class StreamHttpClient { @visibleForTesting final Dio httpClient; - /// Shuts down the [httpClient]. + /// Lock the current [StreamHttpClient] instance. /// - /// If [force] is `false` (the default) the [httpClient] will be kept alive + /// [StreamHttpClient] will enqueue the incoming request tasks instead + /// send them directly when [interceptor.requestOptions] is locked. + void lock() => httpClient.lock(); + + /// Unlock the current [StreamHttpClient] instance. + /// + /// [StreamHttpClient] instance dequeue the request task。 + void unlock() => httpClient.unlock(); + + ///Clear the current [StreamHttpClient] instance waiting queue. + void clear() => httpClient.close(); + + /// Shuts down the [StreamHttpClient]. + /// + /// If [force] is `false` the [StreamHttpClient] will be kept alive /// until all active connections are done. If [force] is `true` any active /// connections will be closed to immediately release all resources. These /// closed connections will receive an error event to indicate that the client @@ -237,4 +251,30 @@ class StreamHttpClient { ); return response; } + + /// Handy method to make generic http request with error parsing. + Future> request( + String path, { + Object? data, + Map? queryParameters, + Options? options, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + CancelToken? cancelToken, + }) async { + try { + final response = await httpClient.request( + path, + data: data, + queryParameters: queryParameters, + options: options, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + cancelToken: cancelToken, + ); + return response; + } on DioError catch (error) { + throw _parseError(error); + } + } } From 927eb3c4518673bbf85d0191383f39b6753f30ca Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 28 May 2021 10:35:13 +0530 Subject: [PATCH 006/150] extract location outside of client options Signed-off-by: Sahil Kumar --- packages/stream_chat/lib/src/client.dart | 1 + .../lib/src/core/http/stream_http_client.dart | 1 + .../core/http/stream_http_client_options.dart | 30 ------------------- packages/stream_chat/lib/src/location.dart | 29 ++++++++++++++++++ packages/stream_chat/lib/stream_chat.dart | 9 +++--- 5 files changed, 36 insertions(+), 34 deletions(-) create mode 100644 packages/stream_chat/lib/src/location.dart diff --git a/packages/stream_chat/lib/src/client.dart b/packages/stream_chat/lib/src/client.dart index d008e211..cd986b2e 100644 --- a/packages/stream_chat/lib/src/client.dart +++ b/packages/stream_chat/lib/src/client.dart @@ -7,6 +7,7 @@ import 'package:dio/dio.dart'; import 'package:logging/logging.dart'; import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/src/api/channel.dart'; +import 'package:stream_chat/src/location.dart'; import 'package:stream_chat/src/ws/connection_status.dart'; import 'package:stream_chat/src/core/api/attachment_file_uploader.dart'; import 'package:stream_chat/src/core/api/requests.dart'; diff --git a/packages/stream_chat/lib/src/core/http/stream_http_client.dart b/packages/stream_chat/lib/src/core/http/stream_http_client.dart index e5c1e82b..d814d705 100644 --- a/packages/stream_chat/lib/src/core/http/stream_http_client.dart +++ b/packages/stream_chat/lib/src/core/http/stream_http_client.dart @@ -9,6 +9,7 @@ import 'package:stream_chat/src/core/http/interceptor/logging_interceptor.dart'; import 'package:stream_chat/src/core/http/stream_chat_dio_error.dart'; import 'package:stream_chat/src/core/http/token_manager.dart'; import 'package:stream_chat/src/errors/stream_chat_error.dart'; +import 'package:stream_chat/src/location.dart'; import 'package:stream_chat/src/platform_detector/platform_detector.dart'; import 'package:stream_chat/version.dart'; diff --git a/packages/stream_chat/lib/src/core/http/stream_http_client_options.dart b/packages/stream_chat/lib/src/core/http/stream_http_client_options.dart index 0f06e4d0..faad46a1 100644 --- a/packages/stream_chat/lib/src/core/http/stream_http_client_options.dart +++ b/packages/stream_chat/lib/src/core/http/stream_http_client_options.dart @@ -1,35 +1,5 @@ part of 'stream_http_client.dart'; -/// -enum Location { - /// - usEast, - - /// - euWest, - - /// - mumbai, - - /// - sydney, - - /// - singapore, -} - -/// -extension LocationX on Location { - /// - String get name => { - Location.usEast: 'us-east', - Location.euWest: 'dublin', - Location.mumbai: 'mumbai', - Location.sydney: 'sydney', - Location.singapore: 'singapore', - }[this]!; -} - const _defaultBaseURL = 'https://chat-us-east-1.stream-io-api.com'; /// Client options to modify [StreamHttpClient] diff --git a/packages/stream_chat/lib/src/location.dart b/packages/stream_chat/lib/src/location.dart new file mode 100644 index 00000000..86813a0a --- /dev/null +++ b/packages/stream_chat/lib/src/location.dart @@ -0,0 +1,29 @@ +/// +enum Location { + /// + usEast, + + /// + euWest, + + /// + mumbai, + + /// + sydney, + + /// + singapore, +} + +/// +extension LocationX on Location { + /// + String get name => { + Location.usEast: 'us-east', + Location.euWest: 'dublin', + Location.mumbai: 'mumbai', + Location.sydney: 'sydney', + Location.singapore: 'singapore', + }[this]!; +} diff --git a/packages/stream_chat/lib/stream_chat.dart b/packages/stream_chat/lib/stream_chat.dart index de49dcdd..991e721b 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -11,6 +11,9 @@ export './src/api/channel.dart'; export './src/client.dart'; export './src/core/api/attachment_file_uploader.dart' show AttachmentFileUploader; +export './src/core/api/requests.dart'; +export './src/core/api/requests.dart'; +export './src/core/api/responses.dart'; export './src/core/models/action.dart'; export './src/core/models/attachment.dart'; export './src/core/models/attachment_file.dart'; @@ -33,7 +36,5 @@ export './src/event_type.dart'; export './src/exceptions.dart'; export './src/extensions/rate_limit.dart'; export './src/extensions/string_extension.dart'; -export 'src/core/api/requests.dart'; -export 'src/core/api/requests.dart'; -export 'src/core/api/responses.dart'; -export 'src/ws/connection_status.dart'; +export './src/location.dart'; +export './src/ws/connection_status.dart'; From 06bf2cdfe5f69016b7f955dada999421536b46de Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 1 Jun 2021 15:42:45 +0200 Subject: [PATCH 007/150] fix rebuild message list view --- packages/stream_chat/lib/src/api/channel.dart | 15 ++++++++++----- packages/stream_chat/lib/src/client.dart | 2 +- .../stream_chat/lib/src/models/member.dart | 18 +++++++++++++++++- packages/stream_chat/lib/src/models/user.dart | 16 +++++++++++++++- .../stream_chat_flutter/analysis_options.yaml | 4 ++-- .../lib/src/channel_info.dart | 1 - .../lib/src/connection_status_builder.dart | 2 +- .../lib/src/message_list_view.dart | 6 ++++-- .../lib/src/typing_indicator.dart | 17 +++++++++-------- .../lib/src/message_list_core.dart | 10 ++++++---- 10 files changed, 65 insertions(+), 26 deletions(-) diff --git a/packages/stream_chat/lib/src/api/channel.dart b/packages/stream_chat/lib/src/api/channel.dart index 69d967fe..4a5476ee 100644 --- a/packages/stream_chat/lib/src/api/channel.dart +++ b/packages/stream_chat/lib/src/api/channel.dart @@ -70,8 +70,11 @@ class Channel { true; /// Returns true if the channel is muted as a stream - Stream? get isMutedStream => _client.state.userStream.map((event) => - event!.channelMutes.any((element) => element.channel.cid == cid) == true); + Stream? get isMutedStream => _client.state.userStream + .map((event) => + event!.channelMutes.any((element) => element.channel.cid == cid) == + true) + .distinct(); /// True if the channel is a group bool get isGroup => memberCount != 2; @@ -1604,7 +1607,7 @@ class ChannelClientState { /// Channel message list as a stream Stream?> get messagesStream => channelStateStream .map((cs) => cs.messages) - .distinct((prev, next) => const ListEquality().equals(prev, next)); + .distinct(const ListEquality().equals); /// Channel pinned message list List? get pinnedMessages => _channelState.pinnedMessages.toList(); @@ -1634,7 +1637,7 @@ class ChannelClientState { _channel.client.state.usersStream, (members, users) => members!.map((e) => e!.copyWith(user: users[e.user!.id])).toList(), - ); + ).distinct(const ListEquality().equals); /// Channel watcher count int? get watcherCount => _channelState.watcherCount; @@ -1804,7 +1807,9 @@ class ChannelClientState { List get typingEvents => _typingEventsController.value; /// Channel related typing users stream - Stream> get typingEventsStream => _typingEventsController.stream; + Stream> get typingEventsStream => + _typingEventsController.stream.distinct(const ListEquality().equals); + final BehaviorSubject> _typingEventsController = BehaviorSubject.seeded([]); diff --git a/packages/stream_chat/lib/src/client.dart b/packages/stream_chat/lib/src/client.dart index 05f2559b..52374239 100644 --- a/packages/stream_chat/lib/src/client.dart +++ b/packages/stream_chat/lib/src/client.dart @@ -219,7 +219,7 @@ class StreamChatClient { /// This notifies the connection status of the websocket connection. /// Listen to this to get notified when the websocket tries to reconnect. Stream get wsConnectionStatusStream => - _wsConnectionStatusController.stream; + _wsConnectionStatusController.stream.distinct(); /// The current user token String? token; diff --git a/packages/stream_chat/lib/src/models/member.dart b/packages/stream_chat/lib/src/models/member.dart index 49df170c..aa576cd2 100644 --- a/packages/stream_chat/lib/src/models/member.dart +++ b/packages/stream_chat/lib/src/models/member.dart @@ -1,3 +1,4 @@ +import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/models/user.dart'; @@ -6,7 +7,7 @@ part 'member.g.dart'; /// The class that contains the information about the user membership /// in a channel @JsonSerializable() -class Member { +class Member extends Equatable { /// Constructor used for json serialization Member({ this.user, @@ -98,4 +99,19 @@ class Member { /// Serialize to json Map toJson() => _$MemberToJson(this); + + @override + List get props => [ + user, + inviteAcceptedAt, + inviteRejectedAt, + invited, + role, + userId, + isModerator, + banned, + shadowBanned, + createdAt, + updatedAt, + ]; } diff --git a/packages/stream_chat/lib/src/models/user.dart b/packages/stream_chat/lib/src/models/user.dart index 9335f8c8..08f61b91 100644 --- a/packages/stream_chat/lib/src/models/user.dart +++ b/packages/stream_chat/lib/src/models/user.dart @@ -1,3 +1,4 @@ +import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/models/serialization.dart'; @@ -5,7 +6,7 @@ part 'user.g.dart'; /// The class that defines the user model @JsonSerializable() -class User { +class User extends Equatable { /// Constructor used for json serialization User({ required this.id, @@ -125,4 +126,17 @@ class User { banned: banned ?? this.banned, teams: teams ?? this.teams, ); + + @override + List get props => [ + id, + role, + teams, + createdAt, + updatedAt, + lastActive, + online, + banned, + extraData, + ]; } diff --git a/packages/stream_chat_flutter/analysis_options.yaml b/packages/stream_chat_flutter/analysis_options.yaml index f0a87ea5..a37a0fb7 100644 --- a/packages/stream_chat_flutter/analysis_options.yaml +++ b/packages/stream_chat_flutter/analysis_options.yaml @@ -3,10 +3,10 @@ analyzer: - extension-methods exclude: - lib/**/*.g.dart - - example/** +# - example/** - lib/src/emoji - lib/**/*.freezed.dart - - test/** +# - test/** linter: rules: diff --git a/packages/stream_chat_flutter/lib/src/channel_info.dart b/packages/stream_chat_flutter/lib/src/channel_info.dart index f59ac2e0..e2067f1b 100644 --- a/packages/stream_chat_flutter/lib/src/channel_info.dart +++ b/packages/stream_chat_flutter/lib/src/channel_info.dart @@ -27,7 +27,6 @@ class ChannelInfo extends StatelessWidget { final client = StreamChat.of(context).client; return StreamBuilder>( stream: channel.state?.membersStream, - initialData: channel.state?.members, builder: (context, snapshot) => ConnectionStatusBuilder( statusBuilder: (context, status) { switch (status) { diff --git a/packages/stream_chat_flutter/lib/src/connection_status_builder.dart b/packages/stream_chat_flutter/lib/src/connection_status_builder.dart index 7e4e7f8e..cfc1b937 100644 --- a/packages/stream_chat_flutter/lib/src/connection_status_builder.dart +++ b/packages/stream_chat_flutter/lib/src/connection_status_builder.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; @@ -39,7 +40,6 @@ class ConnectionStatusBuilder extends StatelessWidget { final stream = connectionStatusStream ?? StreamChat.of(context).client.wsConnectionStatusStream; return StreamBuilder( - initialData: initialStatus, stream: stream, builder: (context, snapshot) { if (snapshot.hasError) { diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index a17dccf0..c13a87a7 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -690,7 +690,7 @@ class _MessageListViewState extends State { ? streamChannel.queryTopMessages : streamChannel.queryBottomMessages; return StreamBuilder( - key: const Key('LOADING-INDICATOR'), + key: Key('LOADING-INDICATOR $direction'), stream: stream, initialData: false, builder: (context, snapshot) { @@ -789,7 +789,9 @@ class _MessageListViewState extends State { } } if (mounted) { - setState(() => _showScrollToBottom = !isVisible); + if (_showScrollToBottom == isVisible) { + setState(() => _showScrollToBottom = !isVisible); + } } }, child: messageWidget, diff --git a/packages/stream_chat_flutter/lib/src/typing_indicator.dart b/packages/stream_chat_flutter/lib/src/typing_indicator.dart index e9377674..010b2c07 100644 --- a/packages/stream_chat_flutter/lib/src/typing_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/typing_indicator.dart @@ -33,8 +33,15 @@ class TypingIndicator extends StatelessWidget { Widget build(BuildContext context) { final channelState = channel?.state ?? StreamChannel.of(context).channel.state!; + + final altWidget = Align( + key: const Key('alternative'), + alignment: alignment, + child: Container( + child: alternativeWidget ?? const Offstage(), + ), + ); return StreamBuilder>( - initialData: channelState.typingEvents, stream: channelState.typingEventsStream, builder: (context, snapshot) => AnimatedSwitcher( duration: const Duration(milliseconds: 300), @@ -62,13 +69,7 @@ class TypingIndicator extends StatelessWidget { ), ), ) - : Align( - key: const Key('alternative'), - alignment: alignment, - child: Container( - child: alternativeWidget ?? const Offstage(), - ), - ), + : altWidget, ), ); } diff --git a/packages/stream_chat_flutter_core/lib/src/message_list_core.dart b/packages/stream_chat_flutter_core/lib/src/message_list_core.dart index cf11ef5f..9b0d3d38 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_list_core.dart @@ -135,10 +135,12 @@ class MessageListCoreState extends State { } return StreamBuilder?>( - stream: messagesStream?.map((messages) => - messages?.where(widget.messageFilter ?? defaultFilter).toList( - growable: false, - )), + stream: messagesStream?.map( + (messages) => + messages?.where(widget.messageFilter ?? defaultFilter).toList( + growable: false, + ), + ), builder: (context, snapshot) { if (snapshot.hasError) { return widget.errorWidgetBuilder(context, snapshot.error!); From c10166dbffce35ce36bca5de554b9ac0dc3cc810 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 1 Jun 2021 17:24:26 +0200 Subject: [PATCH 008/150] experiment with streambuilder --- packages/stream_chat/lib/src/client.dart | 2 +- .../lib/src/channel_image.dart | 10 +- .../lib/src/channel_info.dart | 7 +- .../lib/src/channel_name.dart | 9 +- .../lib/src/connection_status_builder.dart | 28 +- .../lib/src/message_list_view.dart | 29 +- .../lib/src/message_widget.dart | 337 +++++++++--------- .../lib/src/typing_indicator.dart | 8 +- .../lib/src/unread_indicator.dart | 8 +- .../stream_chat_flutter/lib/src/utils.dart | 2 + .../lib/src/better_stream_builder.dart | 100 ++++++ .../lib/src/message_list_core.dart | 35 +- .../lib/stream_chat_flutter_core.dart | 1 + 13 files changed, 342 insertions(+), 234 deletions(-) create mode 100644 packages/stream_chat_flutter_core/lib/src/better_stream_builder.dart diff --git a/packages/stream_chat/lib/src/client.dart b/packages/stream_chat/lib/src/client.dart index 52374239..0b759a53 100644 --- a/packages/stream_chat/lib/src/client.dart +++ b/packages/stream_chat/lib/src/client.dart @@ -213,7 +213,7 @@ class StreamChatClient { _wsConnectionStatusController.add(status); /// The current status value of the websocket connection - ConnectionStatus? get wsConnectionStatus => + ConnectionStatus get wsConnectionStatus => _wsConnectionStatusController.value; /// This notifies the connection status of the websocket connection. diff --git a/packages/stream_chat_flutter/lib/src/channel_image.dart b/packages/stream_chat_flutter/lib/src/channel_image.dart index 19495809..aefdb44e 100644 --- a/packages/stream_chat_flutter/lib/src/channel_image.dart +++ b/packages/stream_chat_flutter/lib/src/channel_image.dart @@ -83,14 +83,14 @@ class ChannelImage extends StatelessWidget { Widget build(BuildContext context) { final streamChat = StreamChat.of(context); final channel = this.channel ?? StreamChannel.of(context).channel; - return StreamBuilder>( + return BetterStreamBuilder>( stream: channel.extraDataStream, initialData: channel.extraData, builder: (context, snapshot) { String? image; final chatThemeData = StreamChatTheme.of(context); - if (snapshot.data!.containsKey('image') == true) { - image = snapshot.data!['image']; + if (snapshot.containsKey('image') == true) { + image = snapshot['image']; } else if (channel.state?.members.length == 2) { final otherMember = channel.state?.members .firstWhere((member) => member.user?.id != streamChat.user?.id); @@ -153,9 +153,7 @@ class ChannelImage extends StatelessWidget { imageUrl: image, errorWidget: (_, __, ___) => Center( child: Text( - snapshot.data?.containsKey('name') ?? false - ? snapshot.data!['name'][0] - : '', + snapshot.containsKey('name') ? snapshot['name'][0] : '', style: TextStyle( color: chatThemeData.colorTheme.white, fontWeight: FontWeight.bold, diff --git a/packages/stream_chat_flutter/lib/src/channel_info.dart b/packages/stream_chat_flutter/lib/src/channel_info.dart index e2067f1b..91e7aceb 100644 --- a/packages/stream_chat_flutter/lib/src/channel_info.dart +++ b/packages/stream_chat_flutter/lib/src/channel_info.dart @@ -25,13 +25,14 @@ class ChannelInfo extends StatelessWidget { @override Widget build(BuildContext context) { final client = StreamChat.of(context).client; - return StreamBuilder>( - stream: channel.state?.membersStream, + return BetterStreamBuilder>( + stream: channel.state!.membersStream, + initialData: channel.state!.members, builder: (context, snapshot) => ConnectionStatusBuilder( statusBuilder: (context, status) { switch (status) { case ConnectionStatus.connected: - return _buildConnectedTitleState(context, snapshot.data); + return _buildConnectedTitleState(context, snapshot); case ConnectionStatus.connecting: return _buildConnectingTitleState(context); case ConnectionStatus.disconnected: diff --git a/packages/stream_chat_flutter/lib/src/channel_name.dart b/packages/stream_chat_flutter/lib/src/channel_name.dart index 2cd804c9..4d560bed 100644 --- a/packages/stream_chat_flutter/lib/src/channel_name.dart +++ b/packages/stream_chat_flutter/lib/src/channel_name.dart @@ -26,11 +26,14 @@ class ChannelName extends StatelessWidget { final client = StreamChat.of(context); final channel = StreamChannel.of(context).channel; - return StreamBuilder>( + return BetterStreamBuilder>( stream: channel.extraDataStream, initialData: channel.extraData, - builder: (context, snapshot) => - _buildName(snapshot.data!, channel.state?.members, client), + builder: (context, snapshot) => _buildName( + snapshot, + channel.state?.members, + client, + ), ); } diff --git a/packages/stream_chat_flutter/lib/src/connection_status_builder.dart b/packages/stream_chat_flutter/lib/src/connection_status_builder.dart index cfc1b937..9137265f 100644 --- a/packages/stream_chat_flutter/lib/src/connection_status_builder.dart +++ b/packages/stream_chat_flutter/lib/src/connection_status_builder.dart @@ -13,14 +13,14 @@ class ConnectionStatusBuilder extends StatelessWidget { const ConnectionStatusBuilder({ Key? key, required this.statusBuilder, - this.initialStatus = ConnectionStatus.disconnected, + this.initialStatus, this.connectionStatusStream, this.errorBuilder, this.loadingBuilder, }) : super(key: key); /// The connection status that will be used to create the initial snapshot. - final ConnectionStatus initialStatus; + final ConnectionStatus? initialStatus; /// The asynchronous computation to which this builder is currently connected. final Stream? connectionStatusStream; @@ -37,23 +37,19 @@ class ConnectionStatusBuilder extends StatelessWidget { @override Widget build(BuildContext context) { - final stream = connectionStatusStream ?? - StreamChat.of(context).client.wsConnectionStatusStream; - return StreamBuilder( + final client = StreamChat.of(context).client; + final stream = connectionStatusStream ?? client.wsConnectionStatusStream; + return BetterStreamBuilder( + initialData: initialStatus ?? client.wsConnectionStatus, stream: stream, - builder: (context, snapshot) { - if (snapshot.hasError) { - if (errorBuilder != null) { - return errorBuilder!(context, snapshot.error); - } - return const Offstage(); + loadingBuilder: loadingBuilder, + errorBuilder: (context, error) { + if (errorBuilder != null) { + return errorBuilder!(context, error); } - if (!snapshot.hasData) { - if (loadingBuilder != null) return loadingBuilder!(context); - return const Offstage(); - } - return statusBuilder(context, snapshot.data!); + return const Offstage(); }, + builder: statusBuilder, ); } } diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index c13a87a7..25cf9941 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -606,8 +606,8 @@ class _MessageListViewState extends State { Widget _buildScrollToBottom() => StreamBuilder>( stream: Rx.combineLatest2( - streamChannel!.channel.state!.isUpToDateStream, - streamChannel!.channel.state!.unreadCountStream, + streamChannel!.channel.state!.isUpToDateStream.distinct(), + streamChannel!.channel.state!.unreadCountStream.distinct(), (bool isUpToDate, int unreadCount) => Tuple2(isUpToDate, unreadCount), ), builder: (_, snapshot) { @@ -689,23 +689,18 @@ class _MessageListViewState extends State { final stream = direction == QueryDirection.top ? streamChannel.queryTopMessages : streamChannel.queryBottomMessages; - return StreamBuilder( + return BetterStreamBuilder( key: Key('LOADING-INDICATOR $direction'), stream: stream, initialData: false, + errorBuilder: (context, error) => Container( + color: StreamChatTheme.of(context).colorTheme.accentRed.withOpacity(.2), + child: const Center( + child: Text('Error loading messages'), + ), + ), builder: (context, snapshot) { - if (snapshot.hasError) { - return Container( - color: StreamChatTheme.of(context) - .colorTheme - .accentRed - .withOpacity(.2), - child: const Center( - child: Text('Error loading messages'), - ), - ); - } - if (!snapshot.data!) { + if (!snapshot) { if (!_isThreadConversation && direction == QueryDirection.top) { return const SizedBox( height: 52, @@ -1163,14 +1158,14 @@ class _MessageListViewState extends State { Navigator.push( context, MaterialPageRoute( - builder: (_) => StreamBuilder( + builder: (_) => BetterStreamBuilder( stream: streamChannel!.channel.state!.messagesStream.map( (messages) => messages!.firstWhere((m) => m.id == message.id)), initialData: message, builder: (_, snapshot) => StreamChannel( channel: streamChannel!.channel, - child: widget.threadBuilder!(context, snapshot.data), + child: widget.threadBuilder!(context, snapshot), ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index b18a0532..97e061ae 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -449,182 +449,189 @@ class _MessageWidgetState extends State final leftPadding = widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 0.5; - return Portal( - child: GestureDetector( - onTap: () { - widget.onMessageTap!(widget.message); - }, - onLongPress: widget.message.isDeleted && !isFailedState - ? null - : () => onLongPress(context), - child: Padding( - padding: widget.padding ?? const EdgeInsets.all(8), - child: FractionallySizedBox( - alignment: - widget.reverse ? Alignment.centerRight : Alignment.centerLeft, - widthFactor: 0.78, - child: Column( - crossAxisAlignment: widget.reverse - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Stack( - clipBehavior: Clip.none, - alignment: widget.reverse - ? AlignmentDirectional.bottomEnd - : AlignmentDirectional.bottomStart, - children: [ - Column( - crossAxisAlignment: widget.reverse - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - if (widget.showUserAvatar == DisplayWidget.show && - widget.message.user != null) ...[ - _buildUserAvatar(), - const SizedBox(width: 4), - ], - if (widget.showUserAvatar == DisplayWidget.hide) - SizedBox(width: avatarWidth + 4), - Flexible( - child: PortalEntry( - portal: Container( - transform: Matrix4.translationValues( - widget.reverse ? 12 : -12, 0, 0), - constraints: - const BoxConstraints(maxWidth: 22 * 6.0), - child: _buildReactionIndicator(context), - ), - portalAnchor: - Alignment(widget.reverse ? 1 : -1, -1), - childAnchor: - Alignment(widget.reverse ? -1 : 1, -1), - child: Stack( - clipBehavior: Clip.none, - children: [ - Padding( - padding: widget.showReactions - ? EdgeInsets.only( - top: widget.message.reactionCounts - ?.isNotEmpty == - true - ? 18 - : 0, - ) - : EdgeInsets.zero, - child: (widget.message.isDeleted && - !isFailedState) - ? Container( - // ignore: lines_longer_than_80_chars - margin: EdgeInsets.symmetric( - horizontal: + return Material( + type: MaterialType.transparency, + child: Portal( + child: InkWell( + onTap: () { + widget.onMessageTap!(widget.message); + }, + onLongPress: widget.message.isDeleted && !isFailedState + ? null + : () => onLongPress(context), + child: Padding( + padding: widget.padding ?? const EdgeInsets.all(8), + child: FractionallySizedBox( + alignment: + widget.reverse ? Alignment.centerRight : Alignment.centerLeft, + widthFactor: 0.78, + child: Column( + crossAxisAlignment: widget.reverse + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Stack( + clipBehavior: Clip.none, + alignment: widget.reverse + ? AlignmentDirectional.bottomEnd + : AlignmentDirectional.bottomStart, + children: [ + Column( + crossAxisAlignment: widget.reverse + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.showUserAvatar == DisplayWidget.show && + widget.message.user != null) ...[ + _buildUserAvatar(), + const SizedBox(width: 4), + ], + if (widget.showUserAvatar == DisplayWidget.hide) + SizedBox(width: avatarWidth + 4), + Flexible( + child: PortalEntry( + portal: Container( + transform: Matrix4.translationValues( + widget.reverse ? 12 : -12, 0, 0), + constraints: const BoxConstraints( + maxWidth: 22 * 6.0), + child: _buildReactionIndicator(context), + ), + portalAnchor: + Alignment(widget.reverse ? 1 : -1, -1), + childAnchor: + Alignment(widget.reverse ? -1 : 1, -1), + child: Stack( + clipBehavior: Clip.none, + children: [ + Padding( + padding: widget.showReactions + ? EdgeInsets.only( + top: widget + .message + .reactionCounts + ?.isNotEmpty == + true + ? 18 + : 0, + ) + : EdgeInsets.zero, + child: (widget.message.isDeleted && + !isFailedState) + ? Container( + // ignore: lines_longer_than_80_chars + margin: EdgeInsets.symmetric( + horizontal: + // ignore: lines_longer_than_80_chars + widget.showUserAvatar == + // ignore: lines_longer_than_80_chars + DisplayWidget + .gone + ? 0 + : 4.0), + child: DeletedMessage( + borderRadiusGeometry: widget + .borderRadiusGeometry, + borderSide: widget.borderSide, + shape: widget.shape, + messageTheme: + widget.messageTheme, + ), + ) + : Card( + clipBehavior: Clip.antiAlias, + elevation: 0, + margin: EdgeInsets.symmetric( + horizontal: (isFailedState + ? 15.0 + : 0.0) + // ignore: lines_longer_than_80_chars - widget.showUserAvatar == - // ignore: lines_longer_than_80_chars + (widget.showUserAvatar == DisplayWidget.gone ? 0 : 4.0), - child: DeletedMessage( - borderRadiusGeometry: - widget.borderRadiusGeometry, - borderSide: widget.borderSide, - shape: widget.shape, - messageTheme: - widget.messageTheme, - ), - ) - : Card( - clipBehavior: Clip.antiAlias, - elevation: 0, - margin: EdgeInsets.symmetric( - horizontal: (isFailedState - ? 15.0 - : 0.0) + - // ignore: lines_longer_than_80_chars - (widget.showUserAvatar == - DisplayWidget.gone - ? 0 - : 4.0), - ), - shape: widget.shape ?? - RoundedRectangleBorder( - side: widget.borderSide ?? - BorderSide( - color: widget - // ignore: lines_longer_than_80_chars - .messageTheme - // ignore: lines_longer_than_80_chars - .messageBorderColor ?? - Colors.grey, - ), - borderRadius: widget - // ignore: lines_longer_than_80_chars - .borderRadiusGeometry ?? - BorderRadius.zero, - ), - color: _getBackgroundColor(), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - if (hasQuotedMessage) - _buildQuotedMessage(), - if (hasNonUrlAttachments) - _parseAttachments(), - if (!isGiphy) - _buildTextBubble(), - ], + ), + shape: widget.shape ?? + RoundedRectangleBorder( + side: widget.borderSide ?? + BorderSide( + color: widget + // ignore: lines_longer_than_80_chars + .messageTheme + // ignore: lines_longer_than_80_chars + .messageBorderColor ?? + Colors.grey, + ), + borderRadius: widget + // ignore: lines_longer_than_80_chars + .borderRadiusGeometry ?? + BorderRadius.zero, + ), + color: _getBackgroundColor(), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.end, + mainAxisSize: + MainAxisSize.min, + children: [ + if (hasQuotedMessage) + _buildQuotedMessage(), + if (hasNonUrlAttachments) + _parseAttachments(), + if (!isGiphy) + _buildTextBubble(), + ], + ), ), + ), + if (widget.showReactionPickerIndicator) + Positioned( + right: widget.reverse ? null : 4, + left: widget.reverse ? 4 : null, + top: -8, + child: CustomPaint( + painter: ReactionBubblePainter( + StreamChatTheme.of(context) + .colorTheme + .white, + Colors.transparent, + Colors.transparent, + tailCirclesSpace: 1, ), - ), - if (widget.showReactionPickerIndicator) - Positioned( - right: widget.reverse ? null : 4, - left: widget.reverse ? 4 : null, - top: -8, - child: CustomPaint( - painter: ReactionBubblePainter( - StreamChatTheme.of(context) - .colorTheme - .white, - Colors.transparent, - Colors.transparent, - tailCirclesSpace: 1, ), ), - ), - ], + ], + ), ), ), - ), - ], + ], + ), + if (showBottomRow) + SizedBox(height: context.textScaleFactor * 18.0), + ], + ), + if (showBottomRow) + Padding( + padding: EdgeInsets.only(left: leftPadding), + child: _bottomRow, ), - if (showBottomRow) - SizedBox(height: context.textScaleFactor * 18.0), - ], - ), - if (showBottomRow) - Padding( - padding: EdgeInsets.only(left: leftPadding), - child: _bottomRow, - ), - if (isFailedState) - Positioned( - left: widget.reverse ? 0 : null, - right: widget.reverse ? null : 0, - bottom: showBottomRow ? 18 : -2, - child: StreamSvgIcon.error(size: 20), - ), - ], - ), - ], + if (isFailedState) + Positioned( + left: widget.reverse ? 0 : null, + right: widget.reverse ? null : 0, + bottom: showBottomRow ? 18 : -2, + child: StreamSvgIcon.error(size: 20), + ), + ], + ), + ], + ), ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/typing_indicator.dart b/packages/stream_chat_flutter/lib/src/typing_indicator.dart index 010b2c07..8f974d6e 100644 --- a/packages/stream_chat_flutter/lib/src/typing_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/typing_indicator.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lottie/lottie.dart'; +import 'package:stream_chat_flutter/src/utils.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Widget to show the current list of typing users @@ -41,11 +42,12 @@ class TypingIndicator extends StatelessWidget { child: alternativeWidget ?? const Offstage(), ), ); - return StreamBuilder>( + return BetterStreamBuilder>( + initialData: channelState.typingEvents, stream: channelState.typingEventsStream, builder: (context, snapshot) => AnimatedSwitcher( duration: const Duration(milliseconds: 300), - child: snapshot.data?.isNotEmpty == true + child: snapshot.isNotEmpty == true ? Padding( padding: padding, child: Align( @@ -61,7 +63,7 @@ class TypingIndicator extends StatelessWidget { ), Text( // ignore: lines_longer_than_80_chars - ' ${snapshot.data![0].name}${snapshot.data!.length == 1 ? '' : ' and ${snapshot.data!.length - 1} more'} ${snapshot.data!.length == 1 ? 'is' : 'are'} typing', + ' ${snapshot[0].name}${snapshot.length == 1 ? '' : ' and ${snapshot.length - 1} more'} ${snapshot.length == 1 ? 'is' : 'are'} typing', maxLines: 1, style: style, ), diff --git a/packages/stream_chat_flutter/lib/src/unread_indicator.dart b/packages/stream_chat_flutter/lib/src/unread_indicator.dart index 9c3a5f88..8b235187 100644 --- a/packages/stream_chat_flutter/lib/src/unread_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/unread_indicator.dart @@ -17,7 +17,7 @@ class UnreadIndicator extends StatelessWidget { Widget build(BuildContext context) { final client = StreamChat.of(context).client; return IgnorePointer( - child: StreamBuilder( + child: BetterStreamBuilder( stream: cid != null ? client.state.channels[cid]?.state?.unreadCountStream : client.state.totalUnreadCountStream, @@ -25,8 +25,8 @@ class UnreadIndicator extends StatelessWidget { ? client.state.channels[cid]?.state?.unreadCount : client.state.totalUnreadCount, builder: (context, snapshot) { - if (!snapshot.hasData || snapshot.data == 0) { - return const SizedBox(); + if (snapshot == null || snapshot == 0) { + return const Offstage(); } return Material( borderRadius: BorderRadius.circular(8), @@ -42,7 +42,7 @@ class UnreadIndicator extends StatelessWidget { ), child: Center( child: Text( - '${snapshot.data! > 99 ? '99+' : snapshot.data}', + '${snapshot > 99 ? '99+' : snapshot}', style: const TextStyle( fontSize: 11, color: Colors.white, diff --git a/packages/stream_chat_flutter/lib/src/utils.dart b/packages/stream_chat_flutter/lib/src/utils.dart index 5a86d4ef..644ea99f 100644 --- a/packages/stream_chat_flutter/lib/src/utils.dart +++ b/packages/stream_chat_flutter/lib/src/utils.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; diff --git a/packages/stream_chat_flutter_core/lib/src/better_stream_builder.dart b/packages/stream_chat_flutter_core/lib/src/better_stream_builder.dart new file mode 100644 index 00000000..bf50966b --- /dev/null +++ b/packages/stream_chat_flutter_core/lib/src/better_stream_builder.dart @@ -0,0 +1,100 @@ +import 'dart:async'; + +import 'package:flutter/widgets.dart'; + +class BetterStreamBuilder extends StatefulWidget { + const BetterStreamBuilder({ + required this.stream, + required this.initialData, + required this.builder, + this.loadingBuilder, + this.errorBuilder, + this.comparator, + Key? key, + }) : super(key: key); + + final Stream? stream; + final T initialData; + final bool Function(T?, T)? comparator; + final Widget Function(BuildContext context, T data) builder; + final Widget Function(BuildContext context)? loadingBuilder; + final Widget Function(BuildContext context, Object error)? errorBuilder; + + @override + _BetterStreamBuilderState createState() => _BetterStreamBuilderState(); +} + +class _BetterStreamBuilderState extends State> { + Widget? _child; + T? _lastEvent; + StreamSubscription? _subscription; + Object? _lastError; + + @override + Widget build(BuildContext context) => _child ?? const Offstage(); + + bool _firstTime = true; + @override + void didChangeDependencies() { + if (_firstTime) { + if (widget.initialData == null && widget.loadingBuilder != null) { + _child = widget.loadingBuilder!(context); + } else { + _onEvent(widget.initialData); + } + _lastEvent = widget.initialData; + _subscription = widget.stream?.listen( + _onEvent, + onError: _onError, + ); + + _firstTime = false; + } + super.didChangeDependencies(); + } + + @override + void didUpdateWidget(covariant BetterStreamBuilder oldWidget) { + if (_lastError != null && oldWidget.errorBuilder != widget.errorBuilder) { + _onError(_lastError); + } else if (oldWidget.builder != widget.builder) { + _onEvent(_lastEvent); + } + + if (oldWidget.stream != widget.stream) { + _subscription?.cancel(); + _subscription = widget.stream?.listen( + _onEvent, + onError: _onError, + ); + } + super.didUpdateWidget(oldWidget); + } + + @override + void dispose() { + _subscription?.cancel(); + super.dispose(); + } + + void _onError(error) { + if (widget.errorBuilder != null && error != _lastError) { + setState(() { + _child = widget.errorBuilder!(context, error); + }); + _lastError = error; + } + } + + void _onEvent(event) { + _lastError = null; + if (widget.comparator != null + ? widget.comparator!(_lastEvent, event) + : event != _lastEvent) { + setState(() { + _child = widget.builder(context, event); + }); + _lastEvent = event; + } + } +} diff --git a/packages/stream_chat_flutter_core/lib/src/message_list_core.dart b/packages/stream_chat_flutter_core/lib/src/message_list_core.dart index 9b0d3d38..0da733bf 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_list_core.dart @@ -1,9 +1,11 @@ import 'dart:async'; +import 'package:collection/collection.dart'; 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/better_stream_builder.dart'; import 'package:stream_chat_flutter_core/src/stream_channel.dart'; import 'package:stream_chat_flutter_core/src/typedef.dart'; @@ -127,6 +129,10 @@ class MessageListCoreState extends State { .map((threads) => threads[widget.parentMessage!.id]) : _streamChannel!.channel.state?.messagesStream; + final initialData = _isThreadConversation + ? _streamChannel!.channel.state?.threads[widget.parentMessage!.id] + : _streamChannel!.channel.state?.messages; + bool defaultFilter(Message m) { final isMyMessage = m.user?.id == _currentUser?.id; final isDeletedOrShadowed = m.isDeleted == true || m.shadowed == true; @@ -134,30 +140,27 @@ class MessageListCoreState extends State { return true; } - return StreamBuilder?>( - stream: messagesStream?.map( + return BetterStreamBuilder?>( + initialData: initialData, + comparator: const ListEquality().equals, + stream: messagesStream!.map( (messages) => messages?.where(widget.messageFilter ?? defaultFilter).toList( growable: false, ), ), + errorBuilder: widget.errorWidgetBuilder, + loadingBuilder: widget.loadingBuilder, builder: (context, snapshot) { - if (snapshot.hasError) { - return widget.errorWidgetBuilder(context, snapshot.error!); - } else if (!snapshot.hasData) { - return widget.loadingBuilder(context); - } else { - final messageList = - snapshot.data?.reversed.toList(growable: false) ?? []; - if (messageList.isEmpty && !_isThreadConversation) { - if (_upToDate) { - return widget.emptyBuilder(context); - } - } else { - _messages = messageList; + final messageList = snapshot?.reversed.toList(growable: false) ?? []; + if (messageList.isEmpty && !_isThreadConversation) { + if (_upToDate) { + return widget.emptyBuilder(context); } - return widget.messageListBuilder(context, _messages); + } else { + _messages = messageList; } + return widget.messageListBuilder(context, _messages); }, ); } diff --git a/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart b/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart index 01463de0..dba20f40 100644 --- a/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart +++ b/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart @@ -2,6 +2,7 @@ library stream_chat_flutter_core; export 'package:stream_chat/stream_chat.dart'; +export 'src/better_stream_builder.dart'; export 'src/channel_list_core.dart' hide ChannelListCoreState; export 'src/channels_bloc.dart'; export 'src/lazy_load_scroll_view.dart'; From 12e6b9c500bfee4c5375a13deac3960473129bb5 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 2 Jun 2021 17:56:38 +0530 Subject: [PATCH 009/150] unify errors, refactor retry_queue Signed-off-by: Sahil Kumar --- packages/stream_chat/lib/src/api/channel.dart | 42 +-- .../stream_chat/lib/src/api/retry_policy.dart | 35 +-- .../stream_chat/lib/src/api/retry_queue.dart | 251 ++++++++++-------- packages/stream_chat/lib/src/client.dart | 34 ++- .../http/interceptor/auth_interceptor.dart | 4 +- .../src/core/http/stream_chat_dio_error.dart | 2 +- .../lib/src/core/http/stream_http_client.dart | 11 +- .../lib/src/core/models/event.dart | 2 +- packages/stream_chat/lib/src/core/utils.dart | 9 + .../lib/src/errors/chat_error_code.dart | 229 ++++++++++------ .../lib/src/errors/stream_chat_error.dart | 106 ++++++-- packages/stream_chat/lib/src/exceptions.dart | 53 ---- .../stream_chat/lib/src/ws/socket_error.dart | 25 -- .../stream_chat/lib/src/ws/websocket.dart | 55 ++-- packages/stream_chat/lib/stream_chat.dart | 3 +- .../stream_chat/test/src/client_test.dart | 1 - .../lib/src/message_actions_modal.dart | 3 +- .../lib/src/db/moor_chat_database.dart | 2 +- 18 files changed, 486 insertions(+), 381 deletions(-) delete mode 100644 packages/stream_chat/lib/src/exceptions.dart delete mode 100644 packages/stream_chat/lib/src/ws/socket_error.dart diff --git a/packages/stream_chat/lib/src/api/channel.dart b/packages/stream_chat/lib/src/api/channel.dart index 3fd38e41..3fb63c91 100644 --- a/packages/stream_chat/lib/src/api/channel.dart +++ b/packages/stream_chat/lib/src/api/channel.dart @@ -7,6 +7,8 @@ import 'package:dio/dio.dart'; import 'package:logging/logging.dart'; import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/src/api/retry_queue.dart'; +import 'package:stream_chat/src/core/utils.dart'; +import 'package:stream_chat/src/errors/stream_chat_error.dart'; import 'package:stream_chat/src/event_type.dart'; import 'package:stream_chat/src/extensions/rate_limit.dart'; import 'package:stream_chat/src/core/models/attachment_file.dart'; @@ -233,12 +235,14 @@ class Channel { }) { final cancelToken = _cancelableAttachmentUploadRequest[attachmentId]; if (cancelToken == null) { - throw Exception( - "Upload request for this Attachment hasn't started yet or else " + throw const StreamChatError( + "Upload request for this Attachment hasn't started yet or maybe " 'Already completed', ); } - if (cancelToken.isCancelled) throw Exception('Already cancelled'); + if (cancelToken.isCancelled) { + throw const StreamChatError('Upload request already cancelled'); + } cancelToken.cancel(reason); } @@ -255,7 +259,7 @@ class Channel { ); if (message == null) { - throw Exception('Error, Message not found'); + throw const StreamChatError('Error, Message not found'); } final attachments = message.attachments.where((it) { @@ -396,9 +400,9 @@ class Channel { final response = await _client.sendMessage(message, id!, type); state!.addMessage(response.message); return response; - } catch (error) { - if (error is DioError && error.type != DioErrorType.response) { - state!.retryQueue?.add([message]); + } catch (e) { + if (e is StreamChatNetworkError && e.isRetriable) { + state!._retryQueue.add([message]); } rethrow; } @@ -453,9 +457,9 @@ class Channel { state?.addMessage(m); return response; - } catch (error) { - if (error is DioError && error.type != DioErrorType.response) { - state?.retryQueue?.add([message]); + } catch (e) { + if (e is StreamChatNetworkError && e.isRetriable) { + state!._retryQueue.add([message]); } rethrow; } @@ -494,9 +498,9 @@ class Channel { state?.addMessage(message.copyWith(status: MessageSendingStatus.sent)); return response; - } catch (error) { - if (error is DioError && error.type != DioErrorType.response) { - state?.retryQueue?.add([message]); + } catch (e) { + if (e is StreamChatNetworkError && e.isRetriable) { + state!._retryQueue.add([message]); } rethrow; } @@ -1188,9 +1192,11 @@ class ChannelClientState { _channel._client.chatPersistenceClient ?.updateChannelState(state)) .debounced(const Duration(seconds: 1)) { - retryQueue = RetryQueue( + _retryQueue = RetryQueue( channel: _channel, - logger: Logger('RETRY QUEUE ${_channel.cid}'), + logger: _channel.client.detachedLogger( + '⟳ (${generateHash([_channel.cid])})', + ), ); _checkExpiredAttachmentMessages(channelState); @@ -1338,7 +1344,7 @@ class ChannelClientState { BehaviorSubject.seeded(true); /// The retry queue associated to this channel - RetryQueue? retryQueue; + late final RetryQueue _retryQueue; /// Retry failed message Future retryFailedMessages() async { @@ -1357,7 +1363,7 @@ class ChannelClientState { ) .toList(); - retryQueue!.add(failedMessages); + _retryQueue.add(failedMessages); } void _listenReactionDeleted() { @@ -1832,7 +1838,7 @@ class ChannelClientState { void dispose() { _debouncedUpdatePersistenceChannelState.cancel(); _unreadCountController.close(); - retryQueue!.dispose(); + _retryQueue.dispose(); _subscriptions.forEach((s) => s.cancel()); _channelStateController.close(); _isUpToDateController.close(); diff --git a/packages/stream_chat/lib/src/api/retry_policy.dart b/packages/stream_chat/lib/src/api/retry_policy.dart index f0f2c67b..14933fed 100644 --- a/packages/stream_chat/lib/src/api/retry_policy.dart +++ b/packages/stream_chat/lib/src/api/retry_policy.dart @@ -1,5 +1,5 @@ import 'package:stream_chat/src/client.dart'; -import 'package:stream_chat/src/exceptions.dart'; +import 'package:stream_chat/src/errors/stream_chat_error.dart'; /// The retry options class RetryPolicy { @@ -7,32 +7,25 @@ class RetryPolicy { RetryPolicy({ required this.shouldRetry, required this.retryTimeout, - this.attempt = 0, + this.maxRetryAttempts = 6, }); - /// The number of attempts tried so far - int attempt = 0; + /// Hard limit on maximum retry attempts before giving up, defaults to 6 + /// Resets once connection recovers. + final int maxRetryAttempts; /// This function evaluates if we should retry the failure - final bool Function(StreamChatClient client, int attempt, ApiError? apiError) - shouldRetry; + final bool Function( + StreamChatClient client, + int attempt, + StreamChatError? error, + ) shouldRetry; /// In the case that we want to retry a failed request the retryTimeout /// method is called to determine the timeout final Duration Function( - StreamChatClient client, int attempt, ApiError? apiError) retryTimeout; - - /// Creates a copy of [RetryPolicy] with specified attributes overridden. - RetryPolicy copyWith({ - bool Function(StreamChatClient client, int attempt, ApiError? apiError)? - shouldRetry, - Duration Function(StreamChatClient client, int attempt, ApiError? apiError)? - retryTimeout, - int? attempt, - }) => - RetryPolicy( - retryTimeout: retryTimeout ?? this.retryTimeout, - shouldRetry: shouldRetry ?? this.shouldRetry, - attempt: attempt ?? this.attempt, - ); + StreamChatClient client, + int attempt, + StreamChatError? error, + ) retryTimeout; } diff --git a/packages/stream_chat/lib/src/api/retry_queue.dart b/packages/stream_chat/lib/src/api/retry_queue.dart index e6d883a9..9d0b8502 100644 --- a/packages/stream_chat/lib/src/api/retry_queue.dart +++ b/packages/stream_chat/lib/src/api/retry_queue.dart @@ -2,10 +2,11 @@ import 'dart:async'; import 'package:collection/collection.dart'; import 'package:logging/logging.dart'; +import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/src/api/channel.dart'; import 'package:stream_chat/src/api/retry_policy.dart'; +import 'package:stream_chat/src/errors/stream_chat_error.dart'; import 'package:stream_chat/src/event_type.dart'; -import 'package:stream_chat/src/exceptions.dart'; import 'package:stream_chat/src/core/models/message.dart'; import 'package:stream_chat/stream_chat.dart'; @@ -15,116 +16,155 @@ class RetryQueue { RetryQueue({ required this.channel, this.logger, - }) { - _retryPolicy = channel.client.retryPolicy; - + }) : client = channel.client { + _retryPolicy = client.retryPolicy; _listenConnectionRecovered(); - _listenFailedEvents(); } /// The channel of this queue final Channel channel; + /// The client associated with this [channel] + final StreamChatClient client; + /// The logger associated to this queue final Logger? logger; - final _subscriptions = []; + late final RetryPolicy _retryPolicy; + + final _compositeSubscription = CompositeSubscription(); + + final _messageQueue = HeapPriorityQueue(_byDate); + bool _isRetrying = false; void _listenConnectionRecovered() { - _subscriptions - .add(channel.client.on(EventType.connectionRecovered).listen((event) { - if (!_isRetrying && event.online!) { + client.on(EventType.connectionRecovered).listen((event) { + if (event.online == true) { _startRetrying(); } - })); + }).addTo(_compositeSubscription); } - final HeapPriorityQueue _messageQueue = HeapPriorityQueue(_byDate); - bool _isRetrying = false; - RetryPolicy? _retryPolicy; + void _listenFailedEvents() { + channel.on().where((event) => event.message != null).listen((event) { + final message = event.message!; + final containsMessage = _messageQueue.containsMessage(message); + if (!containsMessage) return; + if (message.status == MessageSendingStatus.sent) { + logger?.info('Removing sent message from queue : ${message.id}'); + _messageQueue.removeMessage(message); + return; + } else { + if ([ + MessageSendingStatus.failed_update, + MessageSendingStatus.failed, + MessageSendingStatus.failed_delete, + ].contains(message.status)) { + logger?.info('Adding failed message from event : ${event.type}'); + add([message]); + } + } + }).addTo(_compositeSubscription); + } /// Add a list of messages void add(List messages) { - logger?.info('added ${messages.length} messages'); + if (messages.isEmpty) return; + if (_messageQueue.containsAllMessage(messages)) return; + + logger?.info('Adding ${messages.length} messages'); final messageList = _messageQueue.toList(); - - _messageQueue.addAll(messages - .where((element) => !messageList.any((m) => m.id == element.id))); - - if (_messageQueue.isNotEmpty && !_isRetrying) { - _startRetrying(); - } + // we should not add message if already available in the queue + _messageQueue.addAll(messages.where( + (it) => !messageList.any((m) => m.id == it.id), + )); + _startRetrying(); } Future _startRetrying() async { - logger?.info('start retrying'); + if (_isRetrying) return; _isRetrying = true; - final retryPolicy = _retryPolicy!.copyWith(attempt: 0); + logger?.info('Started retrying failed messages'); while (_messageQueue.isNotEmpty) { + logger?.info('${_messageQueue.length} messages remaining in the queue'); final message = _messageQueue.first; - try { - logger?.info('retry attempt ${retryPolicy.attempt}'); - await _sendMessage(message); - logger?.info('message sent - removing it from the queue'); - _messageQueue.remove(message); - logger?.info('now ${_messageQueue.length} messages in the queue'); - retryPolicy.attempt = 0; - } catch (error) { - ApiError? apiError; - if (error is DioError) { - if (error.type == DioErrorType.response) { - _messageQueue.remove(message); - return; - } - apiError = ApiError( - error.response?.data, - error.response?.statusCode, - ); - } else if (error is ApiError) { - apiError = error; - if (apiError.status?.toString().startsWith('4') == true) { - _messageQueue.remove(message); - return; - } - } - - if (!retryPolicy.shouldRetry( - channel.client, - retryPolicy.attempt, - apiError, - )) { - _messageQueue.toList().forEach(_sendFailedEvent); - _isRetrying = false; - return; - } - - retryPolicy.attempt++; - - final timeout = retryPolicy.retryTimeout( - channel.client, - retryPolicy.attempt, - apiError, - ); - await Future.delayed(timeout); - } + await _runAndRetry(message); } _isRetrying = false; } - void _sendFailedEvent(Message? message) { - final newStatus = message!.status == MessageSendingStatus.sending - ? MessageSendingStatus.failed - : (message.status == MessageSendingStatus.updating - ? MessageSendingStatus.failed_update - : MessageSendingStatus.failed_delete); - channel.state!.addMessage(message.copyWith( - status: newStatus, - )); + Future _runAndRetry(Message message) async { + var attempt = 1; + + final maxAttempt = _retryPolicy.maxRetryAttempts; + // early return in case maxAttempt is less than 0 + if (attempt > maxAttempt) return; + + // ignore: literal_only_boolean_expressions + while (true) { + try { + logger?.info('Message (${message.id}) retry attempt $attempt'); + await _retryMessage(message); + logger?.info('Message (${message.id}) sent successfully'); + _messageQueue.removeMessage(message); + break; + } on StreamChatError catch (e) { + // retry logic + final maxAttempt = _retryPolicy.maxRetryAttempts; + if (attempt < maxAttempt) { + final shouldRetry = _retryPolicy.shouldRetry(client, attempt, e); + if (shouldRetry) { + final timeout = _retryPolicy.retryTimeout(client, attempt, e); + // temporary failure, continue + logger?.info( + 'API call failed (attempt $attempt), ' + 'retrying in ${timeout.inSeconds} seconds. Error was $e', + ); + await Future.delayed(timeout); + attempt += 1; + } else { + logger?.info( + 'API call failed (attempt $attempt). ' + 'Giving up for now, will retry when connection recovers. ' + 'Error was $e', + ); + _sendFailedEvent(message); + break; + } + } else { + logger?.info( + 'API call failed (attempt $attempt). ' + 'Exceeds maxRetryAttempt : $maxAttempt ' + 'Giving up for now, will retry when connection recovers. ' + 'Error was $e', + ); + _sendFailedEvent(message); + break; + } + } catch (e) { + logger?.info( + 'API call failed due to unknown error (attempt $attempt). ' + 'Giving up for now, will retry when connection recovers. ' + 'Error was $e', + ); + _sendFailedEvent(message); + break; + } + } } - Future _sendMessage(Message message) async { + void _sendFailedEvent(Message message) { + final newStatus = message.status == MessageSendingStatus.sending + ? MessageSendingStatus.failed + : message.status == MessageSendingStatus.updating + ? MessageSendingStatus.failed_update + : MessageSendingStatus.failed_delete; + channel.state?.addMessage(message.copyWith(status: newStatus)); + } + + Future _retryMessage(Message message) async { if (message.status == MessageSendingStatus.failed_update || message.status == MessageSendingStatus.updating) { await channel.updateMessage(message); @@ -137,39 +177,10 @@ class RetryQueue { } } - void _listenFailedEvents() { - _subscriptions.add(channel.on().listen((event) { - final messageList = _messageQueue.toList(); - if (event.message != null) { - final messageIndex = - messageList.indexWhere((m) => m.id == event.message!.id); - if (messageIndex == -1 && - [ - MessageSendingStatus.failed_update, - MessageSendingStatus.failed, - MessageSendingStatus.failed_delete, - ].contains(event.message!.status)) { - logger?.info('add message from events'); - final m = event.message; - - if (m != null) { - add([m]); - } - } else if (messageIndex != -1 && - [ - MessageSendingStatus.sent, - null, - ].contains(event.message!.status)) { - _messageQueue.remove(messageList[messageIndex]); - } - } - })); - } - /// Call this method to dispose this object void dispose() { _messageQueue.clear(); - _subscriptions.forEach((s) => s.cancel()); + _compositeSubscription.dispose(); } static int _byDate(Message m1, Message m2) { @@ -201,3 +212,27 @@ class RetryQueue { } } } + +extension _MessageHeapPriorityQueue on HeapPriorityQueue { + void removeMessage(Message message) { + final list = toUnorderedList(); + final index = list.indexWhere((it) => it.id == message.id); + if (index == -1) return; + final element = list[index]; + remove(element); + } + + bool containsMessage(Message message) { + final list = toUnorderedList(); + final index = list.indexWhere((it) => it.id == message.id); + if (index == -1) return false; + return true; + } + + bool containsAllMessage(List messages) { + if (isEmpty) return false; + final list = toUnorderedList(); + final messageIds = messages.map((it) => it.id); + return list.every((it) => messageIds.contains(it.id)); + } +} diff --git a/packages/stream_chat/lib/src/client.dart b/packages/stream_chat/lib/src/client.dart index cd986b2e..e7e05b69 100644 --- a/packages/stream_chat/lib/src/client.dart +++ b/packages/stream_chat/lib/src/client.dart @@ -7,6 +7,8 @@ import 'package:dio/dio.dart'; import 'package:logging/logging.dart'; import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/src/api/channel.dart'; +import 'package:stream_chat/src/core/utils.dart'; +import 'package:stream_chat/src/errors/stream_chat_error.dart'; import 'package:stream_chat/src/location.dart'; import 'package:stream_chat/src/ws/connection_status.dart'; import 'package:stream_chat/src/core/api/attachment_file_uploader.dart'; @@ -92,7 +94,7 @@ class StreamChatClient { tokenManager: _tokenManager, connectionIdManager: _connectionIdManager, attachmentFileUploader: attachmentFileUploader, - logger: _detachedLogger('🕸ï¸'), + logger: detachedLogger('🕸ï¸'), ); _ws = WebSocket( @@ -100,13 +102,13 @@ class StreamChatClient { baseUrl: options.baseUrl, tokenManager: _tokenManager, handler: handleEvent, - logger: _detachedLogger('🔌'), + logger: detachedLogger('🔌'), ); _retryPolicy = retryPolicy ?? RetryPolicy( - retryTimeout: (_, int attempt, __) => Duration(seconds: 1 * attempt), - shouldRetry: (_, int attempt, __) => attempt < 5, + shouldRetry: (_, attempt, __) => attempt < 5, + retryTimeout: (_, attempt, __) => Duration(seconds: attempt), ); state = ClientState(this); @@ -137,7 +139,7 @@ class StreamChatClient { /// Whether the chat persistence is available or not bool get persistenceEnabled => _chatPersistenceClient != null; - RetryPolicy? _retryPolicy; + late final RetryPolicy _retryPolicy; // sync state of the channels present inside state, defaults to false bool _synced = false; @@ -146,7 +148,7 @@ class StreamChatClient { DateTime? _lastSyncedAt; /// The retry policy options getter - RetryPolicy? get retryPolicy => _retryPolicy; + RetryPolicy get retryPolicy => _retryPolicy; /// By default the Chat client will write all messages with level Warn or /// Error to stdout. @@ -217,7 +219,8 @@ class StreamChatClient { if (record.stackTrace != null) print(record.stackTrace); }; - Logger _detachedLogger( + /// + Logger detachedLogger( String name, ) => Logger.detached(name) @@ -303,7 +306,7 @@ class StreamChatClient { TokenProvider? provider, }) async { if (_ws.connectionCompleter?.isCompleted == false) { - throw Exception( + throw const StreamChatError( 'User already getting connected, try calling `disconnectUser` ' 'before trying to connect again', ); @@ -346,11 +349,11 @@ class StreamChatClient { logger.info('Opening web-socket connection for ${user.id}'); if (wsConnectionStatus == ConnectionStatus.connecting) { - throw Exception('Connection already in progress for ${user.id}'); + throw StreamChatError('Connection already in progress for ${user.id}'); } if (wsConnectionStatus == ConnectionStatus.connected) { - throw Exception('Connection already connected for ${user.id}'); + throw StreamChatError('Connection already connected for ${user.id}'); } _wsConnectionStatus = ConnectionStatus.connecting; @@ -486,13 +489,6 @@ class StreamChatClient { } } - String _generateHash(List objects) { - final payload = json.encode(objects); - final payloadBytes = utf8.encode(payload); - final payloadB64 = base64.encode(payloadBytes); - return payloadB64; - } - final _queryChannelsStreams = >>{}; /// Requests channels with a given query. @@ -512,7 +508,7 @@ class StreamChatClient { watch = false; } - final hash = _generateHash([ + final hash = generateHash([ filter, sort, state, @@ -575,7 +571,7 @@ class StreamChatClient { await _ws.connectionCompleter?.future; } if (wsConnectionStatus != ConnectionStatus.connected) { - throw Exception( + throw const StreamChatError( 'You cannot use queryChannels without an active connection. ' 'Please call `connectUser` to connect the client.', ); diff --git a/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart b/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart index a75b2d2c..9dafc6c9 100644 --- a/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart +++ b/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart @@ -27,12 +27,12 @@ class AuthInterceptor extends Interceptor { try { token = await _tokenManager.loadToken(); } catch (_) { - final error = StreamChatError(ChatErrorCode.undefinedToken); + final error = StreamChatNetworkError(ChatErrorCode.undefinedToken); final dioError = StreamChatDioError( error: error, requestOptions: options, ); - return handler.reject(dioError); + return handler.reject(dioError, true); } final params = {'user_id': token.userId}; final headers = { diff --git a/packages/stream_chat/lib/src/core/http/stream_chat_dio_error.dart b/packages/stream_chat/lib/src/core/http/stream_chat_dio_error.dart index dd2c2535..f0e61111 100644 --- a/packages/stream_chat/lib/src/core/http/stream_chat_dio_error.dart +++ b/packages/stream_chat/lib/src/core/http/stream_chat_dio_error.dart @@ -17,5 +17,5 @@ class StreamChatDioError extends DioError { ); @override - final StreamChatError error; + final StreamChatNetworkError error; } diff --git a/packages/stream_chat/lib/src/core/http/stream_http_client.dart b/packages/stream_chat/lib/src/core/http/stream_http_client.dart index d814d705..0daeaacd 100644 --- a/packages/stream_chat/lib/src/core/http/stream_http_client.dart +++ b/packages/stream_chat/lib/src/core/http/stream_http_client.dart @@ -100,14 +100,15 @@ class StreamHttpClient { void close({bool force = false}) => httpClient.close(force: force); StreamChatNetworkError _parseError(DioError err) { + StreamChatNetworkError error; // locally thrown dio error if (err is StreamChatDioError) { - final code = err.error.code; - final message = err.error.message; - return StreamChatNetworkError.raw(code: code, message: message); + error = err.error; + } else { + // real network request dio error + error = StreamChatNetworkError.fromDioError(err); } - // real network request dio error - return StreamChatNetworkError.fromDioError(err); + return error..stackTrace = err.stackTrace; } /// Handy method to make http GET request with error parsing. diff --git a/packages/stream_chat/lib/src/core/models/event.dart b/packages/stream_chat/lib/src/core/models/event.dart index d0d17aa0..6ed1d414 100644 --- a/packages/stream_chat/lib/src/core/models/event.dart +++ b/packages/stream_chat/lib/src/core/models/event.dart @@ -29,7 +29,7 @@ class Event { this.parentId, this.extraData = const {}, this.isLocal = true, - }) : createdAt = createdAt ?? DateTime.now(); + }) : createdAt = createdAt?.toUtc() ?? DateTime.now().toUtc(); /// Create a new instance from a json factory Event.fromJson(Map json) => diff --git a/packages/stream_chat/lib/src/core/utils.dart b/packages/stream_chat/lib/src/core/utils.dart index 9366e34e..c2d77927 100644 --- a/packages/stream_chat/lib/src/core/utils.dart +++ b/packages/stream_chat/lib/src/core/utils.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:math' as math; // This alphabet uses `A-Za-z0-9_-` symbols. The genetic algorithm helped @@ -14,3 +15,11 @@ String randomId({int size = 21}) { } return id; } + +/// Creates a hash string from the passed [objects] +String generateHash(List objects) { + final payload = json.encode(objects); + final payloadBytes = utf8.encode(payload); + final payloadB64 = base64.encode(payloadBytes); + return payloadB64; +} diff --git a/packages/stream_chat/lib/src/errors/chat_error_code.dart b/packages/stream_chat/lib/src/errors/chat_error_code.dart index 3dbb600c..6391428d 100644 --- a/packages/stream_chat/lib/src/errors/chat_error_code.dart +++ b/packages/stream_chat/lib/src/errors/chat_error_code.dart @@ -1,91 +1,166 @@ -/// -enum ChatErrorCode { - // client error codes - networkFailed, - parserError, - socketClosed, - socketFailure, - cantParseConnectionEvent, - cantParseEvent, - invalidToken, - undefinedToken, - unableToParseSocketEvent, - noErrorBody, +// ignore_for_file: lines_longer_than_80_chars - // server error codes +import 'package:collection/collection.dart'; + +/// Complete list of errors that are returned by the API +/// together with the description and API code. +enum ChatErrorCode { + // Client errors + + /// Unauthenticated, token not defined + undefinedToken, + + // Bad Request + + /// Wrong data/parameter is sent to the API + inputError, + + /// Duplicate username is sent while enforce_unique_usernames is enabled + duplicateUsername, + + /// Message is too long + messageTooLong, + + /// Event is not supported + eventNotSupported, + + /// The feature is currently disabled + /// on the dashboard (i.e. Reactions & Replies) + channelFeatureNotSupported, + + /// Multiple Levels Reply is not supported + /// the API only supports 1 level deep reply threads + multipleNestling, + + /// Custom Command handler returned an error + customCommandEndpointCall, + + /// App config does not have custom_action_handler_url + customCommandEndpointMissing, + + // Unauthorised + + /// Unauthenticated, problem with authentication authenticationError, + + /// Unauthenticated, token expired tokenExpired, + + /// Unauthenticated, token date incorrect + tokenBeforeIssuedAt, + + /// Unauthenticated, token not valid yet tokenNotValid, - tokenDateIncorrect, - tokenSignatureIncorrect, - apiKeyNotFound, + + /// Unauthenticated, token signature invalid + tokenSignatureInvalid, + + /// Access Key invalid + accessKeyError, + + // Forbidden + + /// Unauthorised / forbidden to make request + notAllowed, + + /// App suspended + appSuspended, + + /// User tried to post a message during the cooldown period + cooldownError, + + // Miscellaneous + + /// Resource not found + doesNotExist, + + /// Request timed out + requestTimeout, + + /// Payload too big + payloadTooBig, + + /// Too many requests in a certain time frame + rateLimitError, + + /// Request headers are too large + maximumHeaderSizeExceeded, + + /// Something goes wrong in the system + internalSystemError, + + /// No access to requested channels + noAccessToChannels } +const _errorCodeWithDescription = { + ChatErrorCode.undefinedToken: + MapEntry(1000, 'Unauthorised, token not defined'), + ChatErrorCode.inputError: + MapEntry(4, 'Wrong data/parameter is sent to the API'), + ChatErrorCode.duplicateUsername: MapEntry(6, + 'Duplicate username is sent while enforce_unique_usernames is enabled'), + ChatErrorCode.messageTooLong: MapEntry(20, 'Message is too long'), + ChatErrorCode.eventNotSupported: MapEntry(18, 'Event is not supported'), + ChatErrorCode.channelFeatureNotSupported: MapEntry(19, + 'The feature is currently disabled on the dashboard (i.e. Reactions & Replies)'), + ChatErrorCode.multipleNestling: MapEntry(21, + 'Multiple Levels Reply is not supported - the API only supports 1 level deep reply threads'), + ChatErrorCode.customCommandEndpointCall: + MapEntry(45, 'Custom Command handler returned an error'), + ChatErrorCode.customCommandEndpointMissing: + MapEntry(44, 'App config does not have custom_action_handler_url'), + ChatErrorCode.authenticationError: + MapEntry(5, 'Unauthenticated, problem with authentication'), + ChatErrorCode.tokenExpired: MapEntry(40, 'Unauthenticated, token expired'), + ChatErrorCode.tokenBeforeIssuedAt: + MapEntry(42, 'Unauthenticated, token date incorrect'), + ChatErrorCode.tokenNotValid: + MapEntry(41, 'Unauthenticated, token not valid yet'), + ChatErrorCode.tokenSignatureInvalid: + MapEntry(43, 'Unauthenticated, token signature invalid'), + ChatErrorCode.accessKeyError: MapEntry(2, 'Access Key invalid'), + ChatErrorCode.notAllowed: + MapEntry(17, 'Unauthorised / forbidden to make request'), + ChatErrorCode.appSuspended: MapEntry(99, 'App suspended'), + ChatErrorCode.cooldownError: + MapEntry(60, 'User tried to post a message during the cooldown period'), + ChatErrorCode.doesNotExist: MapEntry(16, 'Resource not found'), + ChatErrorCode.requestTimeout: MapEntry(23, 'Request timed out'), + ChatErrorCode.payloadTooBig: MapEntry(22, 'Payload too big'), + ChatErrorCode.rateLimitError: + MapEntry(9, 'Too many requests in a certain time frame'), + ChatErrorCode.maximumHeaderSizeExceeded: + MapEntry(24, 'Request headers are too large'), + ChatErrorCode.internalSystemError: + MapEntry(-1, 'Something goes wrong in the system'), + ChatErrorCode.noAccessToChannels: + MapEntry(70, 'No access to requested channels'), +}; + +const _authenticationErrors = [ + ChatErrorCode.undefinedToken, + ChatErrorCode.authenticationError, + ChatErrorCode.tokenExpired, + ChatErrorCode.tokenBeforeIssuedAt, + ChatErrorCode.tokenNotValid, + ChatErrorCode.tokenSignatureInvalid, + ChatErrorCode.accessKeyError, + ChatErrorCode.noAccessToChannels, +]; + +/// +ChatErrorCode? chatErrorCodeFromCode(int code) => _errorCodeWithDescription.keys + .firstWhereOrNull((key) => _errorCodeWithDescription[key]!.key == code); + /// extension ChatErrorCodeX on ChatErrorCode { /// - String get message => { - // client error message - ChatErrorCode.networkFailed: 'Response is failed. See cause', - ChatErrorCode.parserError: 'Unable to parse error', - ChatErrorCode.socketClosed: 'Server closed connection', - ChatErrorCode.socketFailure: 'See stack trace in logs. ' - 'Intercept error in error handler of setUser', - ChatErrorCode.cantParseConnectionEvent: - 'Unable to parse connection event', - ChatErrorCode.cantParseEvent: 'Unable to parse event', - ChatErrorCode.invalidToken: 'Invalid token', - ChatErrorCode.undefinedToken: - 'No defined token. Check if client.setUser was called and finished', - ChatErrorCode.unableToParseSocketEvent: - 'Socket event payload either invalid or null', - ChatErrorCode.noErrorBody: 'No error body. See http status code', - - // server error message - ChatErrorCode.authenticationError: - 'Unauthenticated, problem with authentication', - ChatErrorCode.tokenExpired: 'Token expired, new one must be requested.', - ChatErrorCode.tokenNotValid: 'Unauthenticated, token not valid yet', - ChatErrorCode.tokenDateIncorrect: - 'Unauthenticated, token date incorrect', - ChatErrorCode.tokenSignatureIncorrect: - 'Unauthenticated, token signature invalid', - ChatErrorCode.apiKeyNotFound: - "Api key is not found, verify it if it's correct or was created.", - }[this]!; + String get message => _errorCodeWithDescription[this]!.value; /// - int get code => { - // client error codes - ChatErrorCode.networkFailed: 1000, - ChatErrorCode.parserError: 1001, - ChatErrorCode.socketClosed: 1002, - ChatErrorCode.socketFailure: 1003, - ChatErrorCode.cantParseConnectionEvent: 1004, - ChatErrorCode.cantParseEvent: 1005, - ChatErrorCode.invalidToken: 1006, - ChatErrorCode.undefinedToken: 1007, - ChatErrorCode.unableToParseSocketEvent: 1008, - ChatErrorCode.noErrorBody: 1009, - - // server error codes - ChatErrorCode.authenticationError: 5, - ChatErrorCode.tokenExpired: 40, - ChatErrorCode.tokenNotValid: 41, - ChatErrorCode.tokenDateIncorrect: 42, - ChatErrorCode.tokenSignatureIncorrect: 43, - ChatErrorCode.apiKeyNotFound: 2, - }[this]!; + int get code => _errorCodeWithDescription[this]!.key; /// - Set get authenticationErrors => { - ChatErrorCode.authenticationError, - ChatErrorCode.tokenExpired, - ChatErrorCode.tokenNotValid, - ChatErrorCode.tokenDateIncorrect, - ChatErrorCode.tokenSignatureIncorrect, - }.map((it) => it.code).toSet(); - - /// - bool isAuthenticationError(int code) => authenticationErrors.contains(code); + bool get isAuthenticationError => _authenticationErrors.contains(this); } diff --git a/packages/stream_chat/lib/src/errors/stream_chat_error.dart b/packages/stream_chat/lib/src/errors/stream_chat_error.dart index 280ab6c7..a057783a 100644 --- a/packages/stream_chat/lib/src/errors/stream_chat_error.dart +++ b/packages/stream_chat/lib/src/errors/stream_chat_error.dart @@ -3,45 +3,74 @@ import 'package:stream_chat/src/errors/chat_error_code.dart'; import 'package:stream_chat/stream_chat.dart'; /// -class StreamChatError extends Equatable implements Exception { +class StreamChatError with EquatableMixin implements Exception { /// - StreamChatError(ChatErrorCode errorCode) - : code = errorCode.code, - message = errorCode.message; - - /// - const StreamChatError.raw(this.code, this.message); - - /// Error code - final int code; + const StreamChatError(this.message); /// Error message final String message; @override - List get props => [code, message]; + List get props => [message]; @override - String toString() => 'StreamChatError(code: $code, message: $message)'; + String toString() => 'StreamChatError(message: $message)'; +} + +/// +class StreamWebSocketError extends StreamChatError { + /// + const StreamWebSocketError( + String message, { + this.data, + }) : super(message); + + /// + factory StreamWebSocketError.fromStreamError(Map error) { + final data = ErrorResponse.fromJson(error); + final message = data.message ?? ''; + return StreamWebSocketError(message, data: data); + } + + /// + int? get code => data?.code; + + /// + ChatErrorCode? get errorCode { + final code = this.code; + if (code == null) return null; + return chatErrorCodeFromCode(code); + } + + /// Response body. please refer to [ErrorResponse]. + final ErrorResponse? data; + + @override + String toString() { + var params = 'message: $message'; + if (data != null) params += ', data: $data'; + return 'WebSocketError($params)'; + } } /// class StreamChatNetworkError extends StreamChatError { /// - StreamChatNetworkError({ - required ChatErrorCode errorCode, + StreamChatNetworkError( + ChatErrorCode errorCode, { int? statusCode, this.data, - }) : statusCode = statusCode ?? data?.statusCode, - super(errorCode); + }) : code = errorCode.code, + statusCode = statusCode ?? data?.statusCode, + super(errorCode.message); /// - const StreamChatNetworkError.raw({ - required int code, + StreamChatNetworkError.raw({ + required this.code, required String message, this.statusCode, this.data, - }) : super.raw(code, message); + }) : super(message); /// factory StreamChatNetworkError.fromDioError(DioError error) { @@ -53,25 +82,46 @@ class StreamChatNetworkError extends StreamChatError { } return StreamChatNetworkError.raw( code: errorResponse?.code ?? -1, - message: errorResponse?.message ?? response?.statusMessage ?? '', + message: + errorResponse?.message ?? response?.statusMessage ?? error.message, statusCode: errorResponse?.statusCode ?? response?.statusCode, data: errorResponse, - ); + )..stackTrace = error.stackTrace; } + /// Error code + final int code; + /// HTTP status code final int? statusCode; /// Response body. please refer to [ErrorResponse]. final ErrorResponse? data; - @override - List get props => [...super.props, statusCode]; + StackTrace? _stackTrace; + + /// + set stackTrace(StackTrace? stack) => _stackTrace = stack; + + /// + ChatErrorCode? get errorCode => chatErrorCodeFromCode(code); + + /// + bool get isRetriable => data == null; @override - String toString() => 'StreamChatNetworkError(' - 'code: $code, ' - 'message: $message, ' - 'statusCode: $statusCode, ' - 'data: $data)'; + List get props => [...super.props, code, statusCode]; + + @override + String toString({bool printStackTrace = false}) { + var params = 'code: $code, message: $message'; + if (statusCode != null) params += ', statusCode: $statusCode'; + if (data != null) params += ', data: $data'; + var msg = 'StreamChatNetworkError($params)'; + + if (printStackTrace && _stackTrace != null) { + msg += '\n$_stackTrace'; + } + return msg; + } } diff --git a/packages/stream_chat/lib/src/exceptions.dart b/packages/stream_chat/lib/src/exceptions.dart deleted file mode 100644 index 714bb386..00000000 --- a/packages/stream_chat/lib/src/exceptions.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'dart:convert'; - -/// Exception related to api calls -class ApiError extends Error { - /// Creates a new ApiError instance using the response body and status code - ApiError(this.body, this.status) : jsonData = _decode(body) { - if (jsonData != null && jsonData!.containsKey('code')) { - _code = jsonData!['code']; - } - } - - /// Raw body of the response - final String? body; - - /// Json parsed body - final Map? jsonData; - - /// Http status code of the response - final int? status; - - /// Stream specific error code - int? get code => _code; - int? _code; - - static Map? _decode(String? body) { - try { - if (body == null) { - return null; - } - return json.decode(body); - } on FormatException { - return null; - } - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is ApiError && - runtimeType == other.runtimeType && - body == other.body && - jsonData == other.jsonData && - status == other.status && - _code == other._code; - - @override - int get hashCode => - body.hashCode ^ jsonData.hashCode ^ status.hashCode ^ _code.hashCode; - - @override - String toString() => 'ApiError{body: $body, jsonData: $jsonData, ' - 'status: $status, code: $_code}'; -} diff --git a/packages/stream_chat/lib/src/ws/socket_error.dart b/packages/stream_chat/lib/src/ws/socket_error.dart deleted file mode 100644 index 00b052b9..00000000 --- a/packages/stream_chat/lib/src/ws/socket_error.dart +++ /dev/null @@ -1,25 +0,0 @@ -/// -class SocketError { - /// - const SocketError({ - this.code = -1, - this.statusCode = -1, - this.message = '', - }); - - /// - factory SocketError.fromJson(Map json) => SocketError( - code: json['code'], - statusCode: json['StatusCode'], - message: json['message'], - ); - - /// - final int code; - - /// - final int statusCode; - - /// - final String message; -} diff --git a/packages/stream_chat/lib/src/ws/websocket.dart b/packages/stream_chat/lib/src/ws/websocket.dart index 6ebb7a7b..6c36fd0b 100644 --- a/packages/stream_chat/lib/src/ws/websocket.dart +++ b/packages/stream_chat/lib/src/ws/websocket.dart @@ -5,13 +5,14 @@ import 'dart:math' as math; import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; import 'package:rxdart/rxdart.dart'; +import 'package:stream_chat/src/errors/chat_error_code.dart'; +import 'package:stream_chat/src/errors/stream_chat_error.dart'; import 'package:stream_chat/src/ws/connection_status.dart'; import 'package:stream_chat/src/core/http/token.dart'; import 'package:stream_chat/src/core/http/token_manager.dart'; import 'package:stream_chat/src/core/models/event.dart'; import 'package:stream_chat/src/core/models/user.dart'; import 'package:stream_chat/src/event_type.dart'; -import 'package:stream_chat/src/ws/socket_error.dart'; import 'package:stream_chat/src/ws/timer_helper.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/status.dart' as status; @@ -27,8 +28,6 @@ typedef WebSocketChannelProvider = WebSocketChannel Function( Iterable? protocols, }); -const _tokenExpiredErrorCode = 40; - /// A WebSocket connection that reconnects upon failure. class WebSocket with TimerHelper { /// Creates a new websocket @@ -174,7 +173,7 @@ class WebSocket with TimerHelper { /// Connect the WS using the parameters passed in the constructor Future connect(User user) async { if (_connectRequestInProgress) { - throw Exception(''' + throw const StreamWebSocketError(''' You've called connect twice, can only attempt 1 connection at the time, '''); @@ -195,7 +194,7 @@ class WebSocket with TimerHelper { int _reconnectAttempt = 0; bool _reconnectRequestInProgress = false; - Future _reconnect({bool refreshToken = false}) async { + void _reconnect({bool refreshToken = false}) async { _logger?.info('Retrying connection : $_reconnectAttempt'); if (_reconnectRequestInProgress) return; _reconnectRequestInProgress = true; @@ -270,10 +269,35 @@ class WebSocket with TimerHelper { _connectionStatus = ConnectionStatus.connected; } + void _handleStreamError(Map errorResponse) { + // resetting connect, reconnect request flag + _resetRequestFlags(); + + final error = StreamWebSocketError.fromStreamError(errorResponse); + final isTokenExpired = error.errorCode == ChatErrorCode.tokenExpired; + if (isTokenExpired && !tokenManager.isStatic) { + _logger?.warning('Connection failed, token expired'); + return _reconnect(refreshToken: true); + } + + _logger?.severe('Connection failed', error); + + final completer = connectionCompleter; + // complete with error if not yet completed + if (completer != null && !completer.isCompleted) { + // complete the connection with error + completer.completeError(error); + // disconnect the web-socket connection + return disconnect(); + } + + return _reconnect(); + } + void _onDataReceived(dynamic data) { - final jsonData = json.decode(data); - final error = jsonData['error']; - if (error != null) return _onConnectionError(error); + final jsonData = json.decode(data) as Map; + final error = jsonData['error'] as Map?; + if (error != null) return _handleStreamError(error); // resetting connect, reconnect request flag _resetRequestFlags(resetAttempts: true); @@ -300,25 +324,18 @@ class WebSocket with TimerHelper { } void _onConnectionError(error, [stacktrace]) { - _logger?.severe('Error occurred', error, stacktrace); + _logger?.warning('Error occurred', error, stacktrace); // resetting connect, reconnect request flag _resetRequestFlags(); - var refreshToken = false; - try { - final socketError = SocketError.fromJson(json.decode(error)); - refreshToken = socketError.code == _tokenExpiredErrorCode; - } catch (_) {} - - // refresh token in case it is expired - _reconnect(refreshToken: refreshToken); + _reconnect(); } bool _manuallyClosed = false; void _onConnectionClosed() { - _logger?.info('Connection closed : $connectionId'); + _logger?.warning('Connection closed : $connectionId'); // resetting connect, reconnect request flag _resetRequestFlags(); @@ -381,7 +398,7 @@ class WebSocket with TimerHelper { if (connectionStatus == ConnectionStatus.disconnected) return; _connectionStatus = ConnectionStatus.disconnected; - _logger?.info('Disconnecting $connectionId'); + _logger?.info('Disconnecting web-socket connection'); // resetting user _user = null; diff --git a/packages/stream_chat/lib/stream_chat.dart b/packages/stream_chat/lib/stream_chat.dart index 991e721b..66dace1d 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -32,8 +32,9 @@ export './src/core/models/reaction.dart'; export './src/core/models/read.dart'; export './src/core/models/user.dart'; export './src/db/chat_persistence_client.dart'; +export './src/errors/chat_error_code.dart'; +export './src/errors/stream_chat_error.dart'; export './src/event_type.dart'; -export './src/exceptions.dart'; export './src/extensions/rate_limit.dart'; export './src/extensions/string_extension.dart'; export './src/location.dart'; diff --git a/packages/stream_chat/test/src/client_test.dart b/packages/stream_chat/test/src/client_test.dart index 8da187a4..13506f66 100644 --- a/packages/stream_chat/test/src/client_test.dart +++ b/packages/stream_chat/test/src/client_test.dart @@ -8,7 +8,6 @@ import 'package:logging/logging.dart'; import 'package:mocktail/mocktail.dart'; import 'package:stream_chat/src/core/api/requests.dart'; import 'package:stream_chat/src/client.dart'; -import 'package:stream_chat/src/exceptions.dart'; import 'package:stream_chat/src/core/models/channel_model.dart'; import 'package:stream_chat/src/core/models/filter.dart'; import 'package:stream_chat/src/core/models/message.dart'; diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart index 5336ed48..e40cfc21 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -341,7 +341,8 @@ class _MessageActionsModalState extends State { okText: 'OK', ); } catch (err) { - if (err is ApiError && json.decode(err.body ?? '{}')['code'] == 4) { + if (err is StreamChatNetworkError && + err.errorCode == ChatErrorCode.inputError) { await showInfoDialog( context, icon: StreamSvgIcon.flag( diff --git a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart b/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart index efc8610b..447533e0 100644 --- a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart +++ b/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart @@ -51,7 +51,7 @@ class MoorChatDatabase extends _$MoorChatDatabase { // you should bump this number whenever you change or add a table definition. @override - int get schemaVersion => 3; + int get schemaVersion => 4; @override MigrationStrategy get migration => MigrationStrategy( From d758f3df1d5b0f79b3c7e6de0ba7a2c509558a5b Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 3 Jun 2021 13:38:32 +0530 Subject: [PATCH 010/150] change file structure, replace `rate_limit` with the pub dependency Signed-off-by: Sahil Kumar --- .../lib/src/{api => client}/channel.dart | 9 +- .../lib/src/{ => client}/client.dart | 9 +- .../lib/src/{api => client}/retry_policy.dart | 4 +- .../lib/src/{api => client}/retry_queue.dart | 6 +- .../core/api/attachment_file_uploader.dart | 2 +- .../lib/src/core/api/responses.dart | 4 +- .../error}/chat_error_code.dart | 0 .../stream_chat/lib/src/core/error/error.dart | 2 + .../error}/stream_chat_error.dart | 2 +- .../http/interceptor/auth_interceptor.dart | 4 +- .../src/core/http/stream_chat_dio_error.dart | 2 +- .../lib/src/core/http/stream_http_client.dart | 4 +- .../stream_chat/lib/src/core/http/token.dart | 2 +- .../lib/src/core/models/attachment.dart | 2 +- .../lib/src/core/models/channel_model.dart | 2 +- .../lib/src/core/models/event.dart | 2 +- .../lib/src/core/models/message.dart | 4 +- .../lib/src/core/models/message.g.dart | 2 +- .../stream_chat/lib/src/core/models/mute.dart | 2 +- .../lib/src/core/models/own_user.dart | 2 +- .../lib/src/core/models/reaction.dart | 2 +- .../stream_chat/lib/src/core/models/user.dart | 6 +- .../platform_detector/platform_detector.dart | 2 +- .../platform_detector_io.dart | 2 +- .../platform_detector_stub.dart | 2 +- .../platform_detector_web.dart | 2 +- .../lib/src/core/util/extension.dart | 30 ++ .../core/{models => util}/serialization.dart | 6 - .../lib/src/core/{ => util}/utils.dart | 4 +- .../lib/src/db/chat_persistence_client.dart | 2 +- .../src/extensions/iterable_extension.dart | 9 - .../lib/src/extensions/map_extension.dart | 6 - .../lib/src/extensions/rate_limit.dart | 335 ------------------ .../lib/src/extensions/string_extension.dart | 18 - .../stream_chat/lib/src/ws/websocket.dart | 3 +- packages/stream_chat/lib/stream_chat.dart | 11 +- packages/stream_chat/lib/version.dart | 2 +- packages/stream_chat/pubspec.yaml | 1 + packages/stream_chat/test/version_test.dart | 65 ++-- 39 files changed, 126 insertions(+), 448 deletions(-) rename packages/stream_chat/lib/src/{api => client}/channel.dart (99%) rename packages/stream_chat/lib/src/{ => client}/client.dart (99%) rename packages/stream_chat/lib/src/{api => client}/retry_policy.dart (87%) rename packages/stream_chat/lib/src/{api => client}/retry_queue.dart (97%) rename packages/stream_chat/lib/src/{errors => core/error}/chat_error_code.dart (100%) create mode 100644 packages/stream_chat/lib/src/core/error/error.dart rename packages/stream_chat/lib/src/{errors => core/error}/stream_chat_error.dart (97%) rename packages/stream_chat/lib/src/{ => core}/platform_detector/platform_detector.dart (95%) rename packages/stream_chat/lib/src/{ => core}/platform_detector/platform_detector_io.dart (82%) rename packages/stream_chat/lib/src/{ => core}/platform_detector/platform_detector_stub.dart (53%) rename packages/stream_chat/lib/src/{ => core}/platform_detector/platform_detector_web.dart (50%) create mode 100644 packages/stream_chat/lib/src/core/util/extension.dart rename packages/stream_chat/lib/src/core/{models => util}/serialization.dart (86%) rename packages/stream_chat/lib/src/core/{ => util}/utils.dart (82%) delete mode 100644 packages/stream_chat/lib/src/extensions/iterable_extension.dart delete mode 100644 packages/stream_chat/lib/src/extensions/map_extension.dart delete mode 100644 packages/stream_chat/lib/src/extensions/rate_limit.dart delete mode 100644 packages/stream_chat/lib/src/extensions/string_extension.dart diff --git a/packages/stream_chat/lib/src/api/channel.dart b/packages/stream_chat/lib/src/client/channel.dart similarity index 99% rename from packages/stream_chat/lib/src/api/channel.dart rename to packages/stream_chat/lib/src/client/channel.dart index 3fb63c91..5bc59ae8 100644 --- a/packages/stream_chat/lib/src/api/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -4,13 +4,12 @@ import 'dart:math'; import 'package:collection/collection.dart' show IterableExtension, ListEquality; import 'package:dio/dio.dart'; -import 'package:logging/logging.dart'; import 'package:rxdart/rxdart.dart'; -import 'package:stream_chat/src/api/retry_queue.dart'; -import 'package:stream_chat/src/core/utils.dart'; -import 'package:stream_chat/src/errors/stream_chat_error.dart'; +import 'package:stream_chat/src/client/retry_queue.dart'; +import 'package:stream_chat/src/core/util/utils.dart'; +import 'package:stream_chat/src/core/error/error.dart'; import 'package:stream_chat/src/event_type.dart'; -import 'package:stream_chat/src/extensions/rate_limit.dart'; +import 'package:rate_limiter/rate_limiter.dart'; import 'package:stream_chat/src/core/models/attachment_file.dart'; import 'package:stream_chat/src/core/models/channel_state.dart'; import 'package:stream_chat/src/core/models/user.dart'; diff --git a/packages/stream_chat/lib/src/client.dart b/packages/stream_chat/lib/src/client/client.dart similarity index 99% rename from packages/stream_chat/lib/src/client.dart rename to packages/stream_chat/lib/src/client/client.dart index e7e05b69..ddc6e335 100644 --- a/packages/stream_chat/lib/src/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -1,20 +1,19 @@ // ignore_for_file: unnecessary_getters_setters import 'dart:async'; -import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:logging/logging.dart'; import 'package:rxdart/rxdart.dart'; -import 'package:stream_chat/src/api/channel.dart'; -import 'package:stream_chat/src/core/utils.dart'; -import 'package:stream_chat/src/errors/stream_chat_error.dart'; +import 'package:stream_chat/src/client/channel.dart'; +import 'package:stream_chat/src/core/util/utils.dart'; +import 'package:stream_chat/src/core/error/error.dart'; import 'package:stream_chat/src/location.dart'; import 'package:stream_chat/src/ws/connection_status.dart'; import 'package:stream_chat/src/core/api/attachment_file_uploader.dart'; import 'package:stream_chat/src/core/api/requests.dart'; import 'package:stream_chat/src/core/api/responses.dart'; -import 'package:stream_chat/src/api/retry_policy.dart'; +import 'package:stream_chat/src/client/retry_policy.dart'; import 'package:stream_chat/src/ws/websocket.dart'; import 'package:stream_chat/src/core/api/stream_chat_api.dart'; import 'package:stream_chat/src/core/http/connection_id_manager.dart'; diff --git a/packages/stream_chat/lib/src/api/retry_policy.dart b/packages/stream_chat/lib/src/client/retry_policy.dart similarity index 87% rename from packages/stream_chat/lib/src/api/retry_policy.dart rename to packages/stream_chat/lib/src/client/retry_policy.dart index 14933fed..fbebc696 100644 --- a/packages/stream_chat/lib/src/api/retry_policy.dart +++ b/packages/stream_chat/lib/src/client/retry_policy.dart @@ -1,5 +1,5 @@ -import 'package:stream_chat/src/client.dart'; -import 'package:stream_chat/src/errors/stream_chat_error.dart'; +import 'package:stream_chat/src/client/client.dart'; +import 'package:stream_chat/src/core/error/error.dart'; /// The retry options class RetryPolicy { diff --git a/packages/stream_chat/lib/src/api/retry_queue.dart b/packages/stream_chat/lib/src/client/retry_queue.dart similarity index 97% rename from packages/stream_chat/lib/src/api/retry_queue.dart rename to packages/stream_chat/lib/src/client/retry_queue.dart index 9d0b8502..008270e5 100644 --- a/packages/stream_chat/lib/src/api/retry_queue.dart +++ b/packages/stream_chat/lib/src/client/retry_queue.dart @@ -3,9 +3,9 @@ import 'dart:async'; import 'package:collection/collection.dart'; import 'package:logging/logging.dart'; import 'package:rxdart/rxdart.dart'; -import 'package:stream_chat/src/api/channel.dart'; -import 'package:stream_chat/src/api/retry_policy.dart'; -import 'package:stream_chat/src/errors/stream_chat_error.dart'; +import 'package:stream_chat/src/client/channel.dart'; +import 'package:stream_chat/src/client/retry_policy.dart'; +import 'package:stream_chat/src/core/error/error.dart'; import 'package:stream_chat/src/event_type.dart'; import 'package:stream_chat/src/core/models/message.dart'; import 'package:stream_chat/stream_chat.dart'; diff --git a/packages/stream_chat/lib/src/core/api/attachment_file_uploader.dart b/packages/stream_chat/lib/src/core/api/attachment_file_uploader.dart index 272188af..8eacf716 100644 --- a/packages/stream_chat/lib/src/core/api/attachment_file_uploader.dart +++ b/packages/stream_chat/lib/src/core/api/attachment_file_uploader.dart @@ -1,7 +1,7 @@ import 'package:dio/dio.dart'; import 'package:stream_chat/src/core/api/responses.dart'; import 'package:stream_chat/src/core/http/stream_http_client.dart'; -import 'package:stream_chat/src/extensions/string_extension.dart'; +import 'package:stream_chat/src/core/util/extension.dart'; import 'package:stream_chat/src/core/models/attachment_file.dart'; /// Class responsible for uploading images and files to a given channel diff --git a/packages/stream_chat/lib/src/core/api/responses.dart b/packages/stream_chat/lib/src/core/api/responses.dart index c639a960..b6d6e801 100644 --- a/packages/stream_chat/lib/src/core/api/responses.dart +++ b/packages/stream_chat/lib/src/core/api/responses.dart @@ -1,5 +1,5 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:stream_chat/src/client.dart'; +import 'package:stream_chat/src/client/client.dart'; import 'package:stream_chat/src/core/models/channel_model.dart'; import 'package:stream_chat/src/core/models/channel_state.dart'; import 'package:stream_chat/src/core/models/device.dart'; @@ -9,7 +9,7 @@ import 'package:stream_chat/src/core/models/message.dart'; import 'package:stream_chat/src/core/models/reaction.dart'; import 'package:stream_chat/src/core/models/read.dart'; import 'package:stream_chat/src/core/models/user.dart'; -import 'package:stream_chat/src/errors/stream_chat_error.dart'; +import 'package:stream_chat/src/core/error/error.dart'; part 'responses.g.dart'; diff --git a/packages/stream_chat/lib/src/errors/chat_error_code.dart b/packages/stream_chat/lib/src/core/error/chat_error_code.dart similarity index 100% rename from packages/stream_chat/lib/src/errors/chat_error_code.dart rename to packages/stream_chat/lib/src/core/error/chat_error_code.dart diff --git a/packages/stream_chat/lib/src/core/error/error.dart b/packages/stream_chat/lib/src/core/error/error.dart new file mode 100644 index 00000000..6f2c39a3 --- /dev/null +++ b/packages/stream_chat/lib/src/core/error/error.dart @@ -0,0 +1,2 @@ +export 'chat_error_code.dart'; +export 'stream_chat_error.dart'; \ No newline at end of file diff --git a/packages/stream_chat/lib/src/errors/stream_chat_error.dart b/packages/stream_chat/lib/src/core/error/stream_chat_error.dart similarity index 97% rename from packages/stream_chat/lib/src/errors/stream_chat_error.dart rename to packages/stream_chat/lib/src/core/error/stream_chat_error.dart index a057783a..04214033 100644 --- a/packages/stream_chat/lib/src/errors/stream_chat_error.dart +++ b/packages/stream_chat/lib/src/core/error/stream_chat_error.dart @@ -1,5 +1,5 @@ import 'package:equatable/equatable.dart'; -import 'package:stream_chat/src/errors/chat_error_code.dart'; +import 'package:stream_chat/src/core/error/chat_error_code.dart'; import 'package:stream_chat/stream_chat.dart'; /// diff --git a/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart b/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart index 9dafc6c9..d689cce1 100644 --- a/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart +++ b/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart @@ -5,8 +5,8 @@ import 'package:stream_chat/src/core/http/stream_http_client.dart'; import 'package:stream_chat/src/core/http/token.dart'; import 'package:stream_chat/src/core/http/token_manager.dart'; -import 'package:stream_chat/src/errors/chat_error_code.dart'; -import 'package:stream_chat/src/errors/stream_chat_error.dart'; +import 'package:stream_chat/src/core/error/error.dart'; + /// class AuthInterceptor extends Interceptor { diff --git a/packages/stream_chat/lib/src/core/http/stream_chat_dio_error.dart b/packages/stream_chat/lib/src/core/http/stream_chat_dio_error.dart index f0e61111..987c9d67 100644 --- a/packages/stream_chat/lib/src/core/http/stream_chat_dio_error.dart +++ b/packages/stream_chat/lib/src/core/http/stream_chat_dio_error.dart @@ -1,5 +1,5 @@ import 'package:dio/dio.dart'; -import 'package:stream_chat/src/errors/stream_chat_error.dart'; +import 'package:stream_chat/src/core/error/error.dart'; /// class StreamChatDioError extends DioError { diff --git a/packages/stream_chat/lib/src/core/http/stream_http_client.dart b/packages/stream_chat/lib/src/core/http/stream_http_client.dart index 0daeaacd..3eacd11e 100644 --- a/packages/stream_chat/lib/src/core/http/stream_http_client.dart +++ b/packages/stream_chat/lib/src/core/http/stream_http_client.dart @@ -8,9 +8,9 @@ import 'package:stream_chat/src/core/http/interceptor/connection_id_interceptor. import 'package:stream_chat/src/core/http/interceptor/logging_interceptor.dart'; import 'package:stream_chat/src/core/http/stream_chat_dio_error.dart'; import 'package:stream_chat/src/core/http/token_manager.dart'; -import 'package:stream_chat/src/errors/stream_chat_error.dart'; +import 'package:stream_chat/src/core/error/error.dart'; import 'package:stream_chat/src/location.dart'; -import 'package:stream_chat/src/platform_detector/platform_detector.dart'; +import 'package:stream_chat/src/core/platform_detector/platform_detector.dart'; import 'package:stream_chat/version.dart'; import 'package:stream_chat/src/core/http/connection_id_manager.dart'; diff --git a/packages/stream_chat/lib/src/core/http/token.dart b/packages/stream_chat/lib/src/core/http/token.dart index a914521c..26532e80 100644 --- a/packages/stream_chat/lib/src/core/http/token.dart +++ b/packages/stream_chat/lib/src/core/http/token.dart @@ -3,7 +3,7 @@ import 'dart:convert'; import 'package:equatable/equatable.dart'; import 'package:jose/jose.dart'; import 'package:stream_chat/src/core/models/user.dart'; -import 'package:stream_chat/src/core/utils.dart'; +import 'package:stream_chat/src/core/util/utils.dart'; /// typedef GuestTokenProvider = Future Function(User user); diff --git a/packages/stream_chat/lib/src/core/models/attachment.dart b/packages/stream_chat/lib/src/core/models/attachment.dart index 1df785a8..a5f9f19d 100644 --- a/packages/stream_chat/lib/src/core/models/attachment.dart +++ b/packages/stream_chat/lib/src/core/models/attachment.dart @@ -4,7 +4,7 @@ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/core/models/action.dart'; import 'package:stream_chat/src/core/models/attachment_file.dart'; -import 'package:stream_chat/src/core/models/serialization.dart'; +import 'package:stream_chat/src/core/util/serialization.dart'; import 'package:uuid/uuid.dart'; part 'attachment.g.dart'; diff --git a/packages/stream_chat/lib/src/core/models/channel_model.dart b/packages/stream_chat/lib/src/core/models/channel_model.dart index 892713e7..9e50ed3e 100644 --- a/packages/stream_chat/lib/src/core/models/channel_model.dart +++ b/packages/stream_chat/lib/src/core/models/channel_model.dart @@ -1,6 +1,6 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/core/models/channel_config.dart'; -import 'package:stream_chat/src/core/models/serialization.dart'; +import 'package:stream_chat/src/core/util/serialization.dart'; import 'package:stream_chat/src/core/models/user.dart'; part 'channel_model.g.dart'; diff --git a/packages/stream_chat/lib/src/core/models/event.dart b/packages/stream_chat/lib/src/core/models/event.dart index 6ed1d414..7c1be679 100644 --- a/packages/stream_chat/lib/src/core/models/event.dart +++ b/packages/stream_chat/lib/src/core/models/event.dart @@ -1,7 +1,7 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/core/models/channel_model.dart'; import 'package:stream_chat/src/core/models/message.dart'; -import 'package:stream_chat/src/core/models/serialization.dart'; +import 'package:stream_chat/src/core/util/serialization.dart'; import 'package:stream_chat/stream_chat.dart'; part 'event.g.dart'; diff --git a/packages/stream_chat/lib/src/core/models/message.dart b/packages/stream_chat/lib/src/core/models/message.dart index f3bf100f..1266519c 100644 --- a/packages/stream_chat/lib/src/core/models/message.dart +++ b/packages/stream_chat/lib/src/core/models/message.dart @@ -2,7 +2,7 @@ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/core/models/attachment.dart'; import 'package:stream_chat/src/core/models/reaction.dart'; -import 'package:stream_chat/src/core/models/serialization.dart'; +import 'package:stream_chat/src/core/util/serialization.dart'; import 'package:stream_chat/src/core/models/user.dart'; import 'package:uuid/uuid.dart'; @@ -112,7 +112,7 @@ class Message extends Equatable { /// The list of user mentioned in the message @JsonKey( - toJson: Serialization.userIds, + toJson: User.toIds, defaultValue: [], ) final List mentionedUsers; diff --git a/packages/stream_chat/lib/src/core/models/message.g.dart b/packages/stream_chat/lib/src/core/models/message.g.dart index ca094a83..7d268d0e 100644 --- a/packages/stream_chat/lib/src/core/models/message.g.dart +++ b/packages/stream_chat/lib/src/core/models/message.g.dart @@ -85,7 +85,7 @@ Map _$MessageToJson(Message instance) { writeNotNull('type', readonly(instance.type)); val['attachments'] = instance.attachments.map((e) => e.toJson()).toList(); - val['mentioned_users'] = Serialization.userIds(instance.mentionedUsers); + val['mentioned_users'] = User.toIds(instance.mentionedUsers); writeNotNull('reaction_counts', readonly(instance.reactionCounts)); writeNotNull('reaction_scores', readonly(instance.reactionScores)); writeNotNull('latest_reactions', readonly(instance.latestReactions)); diff --git a/packages/stream_chat/lib/src/core/models/mute.dart b/packages/stream_chat/lib/src/core/models/mute.dart index 8c3bef89..7a82fc54 100644 --- a/packages/stream_chat/lib/src/core/models/mute.dart +++ b/packages/stream_chat/lib/src/core/models/mute.dart @@ -1,6 +1,6 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/core/models/channel_model.dart'; -import 'package:stream_chat/src/core/models/serialization.dart'; +import 'package:stream_chat/src/core/util/serialization.dart'; import 'package:stream_chat/src/core/models/user.dart'; part 'mute.g.dart'; diff --git a/packages/stream_chat/lib/src/core/models/own_user.dart b/packages/stream_chat/lib/src/core/models/own_user.dart index 6f975a94..b9cdf0fd 100644 --- a/packages/stream_chat/lib/src/core/models/own_user.dart +++ b/packages/stream_chat/lib/src/core/models/own_user.dart @@ -1,7 +1,7 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/core/models/device.dart'; import 'package:stream_chat/src/core/models/mute.dart'; -import 'package:stream_chat/src/core/models/serialization.dart'; +import 'package:stream_chat/src/core/util/serialization.dart'; import 'package:stream_chat/src/core/models/user.dart'; part 'own_user.g.dart'; diff --git a/packages/stream_chat/lib/src/core/models/reaction.dart b/packages/stream_chat/lib/src/core/models/reaction.dart index ace4a15e..997d833c 100644 --- a/packages/stream_chat/lib/src/core/models/reaction.dart +++ b/packages/stream_chat/lib/src/core/models/reaction.dart @@ -1,5 +1,5 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:stream_chat/src/core/models/serialization.dart'; +import 'package:stream_chat/src/core/util/serialization.dart'; import 'package:stream_chat/src/core/models/user.dart'; part 'reaction.g.dart'; diff --git a/packages/stream_chat/lib/src/core/models/user.dart b/packages/stream_chat/lib/src/core/models/user.dart index ae1dd742..b5fdec69 100644 --- a/packages/stream_chat/lib/src/core/models/user.dart +++ b/packages/stream_chat/lib/src/core/models/user.dart @@ -1,5 +1,5 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:stream_chat/src/core/models/serialization.dart'; +import 'package:stream_chat/src/core/util/serialization.dart'; part 'user.g.dart'; @@ -92,6 +92,10 @@ class User { return id; } + /// List of users to list of userIds + static List? toIds(List? users) => + users?.map((u) => u.id).toList(); + @override bool operator ==(Object other) => identical(this, other) || diff --git a/packages/stream_chat/lib/src/platform_detector/platform_detector.dart b/packages/stream_chat/lib/src/core/platform_detector/platform_detector.dart similarity index 95% rename from packages/stream_chat/lib/src/platform_detector/platform_detector.dart rename to packages/stream_chat/lib/src/core/platform_detector/platform_detector.dart index 58d86db4..cf817cac 100644 --- a/packages/stream_chat/lib/src/platform_detector/platform_detector.dart +++ b/packages/stream_chat/lib/src/core/platform_detector/platform_detector.dart @@ -1,4 +1,4 @@ -import 'package:stream_chat/src/platform_detector/platform_detector_stub.dart' +import 'package:stream_chat/src/core/platform_detector/platform_detector_stub.dart' if (dart.library.html) 'platform_detector_web.dart' if (dart.library.io) 'platform_detector_io.dart'; diff --git a/packages/stream_chat/lib/src/platform_detector/platform_detector_io.dart b/packages/stream_chat/lib/src/core/platform_detector/platform_detector_io.dart similarity index 82% rename from packages/stream_chat/lib/src/platform_detector/platform_detector_io.dart rename to packages/stream_chat/lib/src/core/platform_detector/platform_detector_io.dart index c7b4a0b7..da707eed 100644 --- a/packages/stream_chat/lib/src/platform_detector/platform_detector_io.dart +++ b/packages/stream_chat/lib/src/core/platform_detector/platform_detector_io.dart @@ -1,5 +1,5 @@ import 'dart:io'; -import 'package:stream_chat/src/platform_detector/platform_detector.dart'; +import 'package:stream_chat/src/core/platform_detector/platform_detector.dart'; /// Version running on native systems PlatformType get currentPlatform { diff --git a/packages/stream_chat/lib/src/platform_detector/platform_detector_stub.dart b/packages/stream_chat/lib/src/core/platform_detector/platform_detector_stub.dart similarity index 53% rename from packages/stream_chat/lib/src/platform_detector/platform_detector_stub.dart rename to packages/stream_chat/lib/src/core/platform_detector/platform_detector_stub.dart index b9e13c2e..9d1a7f66 100644 --- a/packages/stream_chat/lib/src/platform_detector/platform_detector_stub.dart +++ b/packages/stream_chat/lib/src/core/platform_detector/platform_detector_stub.dart @@ -1,4 +1,4 @@ -import 'package:stream_chat/src/platform_detector/platform_detector.dart'; +import 'package:stream_chat/src/core/platform_detector/platform_detector.dart'; /// Stub implementation PlatformType get currentPlatform { diff --git a/packages/stream_chat/lib/src/platform_detector/platform_detector_web.dart b/packages/stream_chat/lib/src/core/platform_detector/platform_detector_web.dart similarity index 50% rename from packages/stream_chat/lib/src/platform_detector/platform_detector_web.dart rename to packages/stream_chat/lib/src/core/platform_detector/platform_detector_web.dart index ba5d04fc..324b4145 100644 --- a/packages/stream_chat/lib/src/platform_detector/platform_detector_web.dart +++ b/packages/stream_chat/lib/src/core/platform_detector/platform_detector_web.dart @@ -1,4 +1,4 @@ -import 'package:stream_chat/src/platform_detector/platform_detector.dart'; +import 'package:stream_chat/src/core/platform_detector/platform_detector.dart'; /// Version running on web PlatformType get currentPlatform => PlatformType.web; diff --git a/packages/stream_chat/lib/src/core/util/extension.dart b/packages/stream_chat/lib/src/core/util/extension.dart new file mode 100644 index 00000000..5dd8ea15 --- /dev/null +++ b/packages/stream_chat/lib/src/core/util/extension.dart @@ -0,0 +1,30 @@ +import 'package:http_parser/http_parser.dart'; +import 'package:mime/mime.dart'; + +/// Useful extension functions for [Iterable] +extension IterableX on Iterable { + /// Removes all the null values + /// and converts `Iterable` into `Iterable` + Iterable get withNullifyer => whereType(); +} + +/// Useful extension functions for [Map] +extension MapX on Map { + /// Returns a new map with null keys or values removed + Map get nullProtected => + Map.from(this)..removeWhere((key, value) => key == null || value == null); +} + +/// Useful extension functions for [String] +extension StringX on String { + /// returns the mime type from the passed file name. + MediaType? get mimeType { + if (toLowerCase().endsWith('heic')) { + return MediaType.parse('image/heic'); + } else { + final mimeType = lookupMimeType(this); + if (mimeType == null) return null; + return MediaType.parse(mimeType); + } + } +} diff --git a/packages/stream_chat/lib/src/core/models/serialization.dart b/packages/stream_chat/lib/src/core/util/serialization.dart similarity index 86% rename from packages/stream_chat/lib/src/core/models/serialization.dart rename to packages/stream_chat/lib/src/core/util/serialization.dart index 692ad01a..1e933a32 100644 --- a/packages/stream_chat/lib/src/core/models/serialization.dart +++ b/packages/stream_chat/lib/src/core/util/serialization.dart @@ -1,5 +1,3 @@ -import 'package:stream_chat/src/core/models/user.dart'; - /// Used to avoid to serialize properties to json // ignore: prefer_void_to_null Null readonly(_) => null; @@ -9,10 +7,6 @@ class Serialization { /// Used to avoid to serialize properties to json static const Function readOnly = readonly; - /// List of users to list of userIds - static List? userIds(List? users) => - users?.map((u) => u.id).toList(); - /// Takes unknown json keys and puts them in the `extra_data` key static Map moveToExtraDataFromRoot( Map json, diff --git a/packages/stream_chat/lib/src/core/utils.dart b/packages/stream_chat/lib/src/core/util/utils.dart similarity index 82% rename from packages/stream_chat/lib/src/core/utils.dart rename to packages/stream_chat/lib/src/core/util/utils.dart index c2d77927..c2204991 100644 --- a/packages/stream_chat/lib/src/core/utils.dart +++ b/packages/stream_chat/lib/src/core/util/utils.dart @@ -4,14 +4,14 @@ import 'dart:math' as math; // This alphabet uses `A-Za-z0-9_-` symbols. The genetic algorithm helped // optimize the gzip compression for this alphabet. const _alphabet = - 'ModuleSymbhasOwnPr0123456789ABCDEFGHNRVfgctiUvzKqYTJkLxpZXIjQW'; + 'ModuleSymbhasOwnPr-0123456789ABCDEFGHNRVfgctiUvz_KqYTJkLxpZXIjQW'; /// Generates a random String id /// Adopted from: https://github.com/ai/nanoid/blob/main/non-secure/index.js String randomId({int size = 21}) { var id = ''; for (var i = 0; i < size; i++) { - id += _alphabet[(math.Random().nextInt(32) * 64) | 0]; + id += _alphabet[(math.Random().nextDouble() * 64).floor() | 0]; } return id; } diff --git a/packages/stream_chat/lib/src/db/chat_persistence_client.dart b/packages/stream_chat/lib/src/db/chat_persistence_client.dart index d7e0f765..2470e403 100644 --- a/packages/stream_chat/lib/src/db/chat_persistence_client.dart +++ b/packages/stream_chat/lib/src/db/chat_persistence_client.dart @@ -8,7 +8,7 @@ import 'package:stream_chat/src/core/models/message.dart'; import 'package:stream_chat/src/core/models/reaction.dart'; import 'package:stream_chat/src/core/models/read.dart'; import 'package:stream_chat/src/core/models/user.dart'; -import 'package:stream_chat/src/extensions/iterable_extension.dart'; +import 'package:stream_chat/src/core/util/extension.dart'; /// A simple client used for persisting chat data locally. abstract class ChatPersistenceClient { diff --git a/packages/stream_chat/lib/src/extensions/iterable_extension.dart b/packages/stream_chat/lib/src/extensions/iterable_extension.dart deleted file mode 100644 index a7d30f76..00000000 --- a/packages/stream_chat/lib/src/extensions/iterable_extension.dart +++ /dev/null @@ -1,9 +0,0 @@ -/// Useful extension functions for [Iterable] -extension IterableX on Iterable { - /// Removes all the null values - /// and converts `Iterable` into `Iterable` - Iterable get withNullifyer => [ - for (final item in this) - if (item != null) item - ]; -} diff --git a/packages/stream_chat/lib/src/extensions/map_extension.dart b/packages/stream_chat/lib/src/extensions/map_extension.dart deleted file mode 100644 index d99ab96a..00000000 --- a/packages/stream_chat/lib/src/extensions/map_extension.dart +++ /dev/null @@ -1,6 +0,0 @@ -/// Useful extension functions for [Map] -extension MapX on Map { - /// Returns a new map with null keys or values removed - Map get nullProtected => - Map.from(this)..removeWhere((key, value) => key == null || value == null); -} diff --git a/packages/stream_chat/lib/src/extensions/rate_limit.dart b/packages/stream_chat/lib/src/extensions/rate_limit.dart deleted file mode 100644 index bf54fe5c..00000000 --- a/packages/stream_chat/lib/src/extensions/rate_limit.dart +++ /dev/null @@ -1,335 +0,0 @@ -// ignore_for_file: lines_longer_than_80_chars - -import 'dart:async' show Timer; -import 'dart:math' as math; - -/// Useful rate limiter extensions for [Function] class. -extension RateLimit on Function { - /// Converts this into a [Debounce] function. - Debounce debounced( - Duration wait, { - bool leading = false, - bool trailing = true, - Duration? maxWait, - }) => - Debounce( - this, - wait, - leading: leading, - trailing: trailing, - maxWait: maxWait, - ); - - /// Converts this into a [Throttle] function. - Throttle throttled( - Duration wait, { - bool leading = true, - bool trailing = true, - }) => - Throttle( - this, - wait, - leading: leading, - trailing: trailing, - ); -} - -/// TopLevel lambda to create [Debounce] functions. -Debounce debounce( - Function func, - Duration wait, { - bool leading = false, - bool trailing = true, - Duration? maxWait, -}) => - Debounce( - func, - wait, - leading: leading, - trailing: trailing, - maxWait: maxWait, - ); - -/// TopLevel lambda to create [Throttle] functions. -Throttle throttle( - Function func, - Duration wait, { - bool leading = true, - bool trailing = true, -}) => - Throttle( - func, - wait, - leading: leading, - trailing: trailing, - ); - -/// Creates a debounced function that delays invoking `func` until after `wait` -/// milliseconds have elapsed since the last time the debounced function was -/// invoked. The debounced function comes with a [Debounce.cancel] method to cancel -/// delayed `func` invocations and a [Debounce.flush] method to immediately invoke them. -/// Provide `leading` and/or `trailing` to indicate whether `func` should be -/// invoked on the `leading` and/or `trailing` edge of the `wait` interval. -/// The `func` is invoked with the last arguments provided to the [call] -/// function. Subsequent calls to the debounced function return the result of -/// the last `func` invocation. -/// -/// **Note:** If `leading` and `trailing` options are `true`, `func` is -/// invoked on the trailing edge of the timeout only if the debounced function -/// is invoked more than once during the `wait` timeout. -/// -/// If `wait` is [Duration.zero] and `leading` is `false`, -/// `func` invocation is deferred until the next tick. -/// -/// See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) -/// for details over the differences between [Debounce] and [Throttle]. -/// -/// Some examples: -/// -/// Avoid calling costly network calls when user is typing something. -/// ```dart -/// void fetchData(String query) async { -/// final data = api.getData(query); -/// doSomethingWithTheData(data); -/// } -/// -/// final debouncedFetchData = Debounce( -/// fetchData, -/// const Duration(milliseconds: 350), -/// ); -/// -/// void onSearchQueryChanged(query) { -/// debouncedFetchData(query); -/// } -/// ``` -/// -/// Cancel the trailing debounced invocation. -/// ```dart -/// void dispose() { -/// debounced.cancel(); -/// } -/// ``` -/// -/// Check for pending invocations. -/// ```dart -/// final status = debounced.isPending ? "Pending..." : "Ready"; -/// ``` -class Debounce { - /// Creates a new instance of [Debounce]. - Debounce( - this._func, - Duration wait, { - bool leading = false, - bool trailing = true, - Duration? maxWait, - }) : _leading = leading, - _trailing = trailing, - _wait = wait.inMilliseconds, - _maxing = maxWait != null { - if (_maxing) { - _maxWait = math.max(maxWait!.inMilliseconds, _wait); - } - } - - final Function _func; - final bool _leading; - final bool _trailing; - final int _wait; - final bool _maxing; - - late int _maxWait; - List? _lastArgs; - Map? _lastNamedArgs; - Timer? _timer; - int? _lastCallTime; - Object? _result; - int? _lastInvokeTime = 0; - - Object? _invokeFunc(int? time) { - final args = _lastArgs; - final namedArgs = _lastNamedArgs; - _lastArgs = _lastNamedArgs = null; - _lastInvokeTime = time; - return _result = Function.apply(_func, args, namedArgs); - } - - Timer _startTimer(Function pendingFunc, int wait) => - Timer(Duration(milliseconds: wait), pendingFunc as void Function()); - - bool _shouldInvoke(int time) { - final timeSinceLastCall = time - (_lastCallTime ?? double.nan); - final timeSinceLastInvoke = time - _lastInvokeTime!; - - // Either this is the first call, activity has stopped and we're at the - // trailing edge, the system time has gone backwards and we're treating - // it as the trailing edge, or we've hit the `maxWait` limit. - return _lastCallTime == null || - (timeSinceLastCall >= _wait) || - (timeSinceLastCall < 0) || - (_maxing && timeSinceLastInvoke >= _maxWait); - } - - Object? _trailingEdge(int time) { - _timer = null; - - // Only invoke if we have `lastArgs` which means `func` has been - // debounced at least once. - if (_trailing && _lastArgs != null) { - return _invokeFunc(time); - } - _lastArgs = _lastNamedArgs = null; - return _result; - } - - int _remainingWait(int time) { - final timeSinceLastCall = time - _lastCallTime!; - final timeSinceLastInvoke = time - _lastInvokeTime!; - final timeWaiting = _wait - timeSinceLastCall; - - return _maxing - ? math.min(timeWaiting, _maxWait - timeSinceLastInvoke) - : timeWaiting; - } - - void _timerExpired() { - final time = DateTime.now().millisecondsSinceEpoch; - if (_shouldInvoke(time)) { - _trailingEdge(time); - } else { - // Restart the timer. - _timer = _startTimer(_timerExpired, _remainingWait(time)); - } - } - - Object? _leadingEdge(int? time) { - // Reset any `maxWait` timer. - _lastInvokeTime = time; - // Start the timer for the trailing edge. - _timer = _startTimer(_timerExpired, _wait); - // Invoke the leading edge. - return _leading ? _invokeFunc(time) : _result; - } - - /// Cancels all the remaining delayed functions. - void cancel() { - _timer?.cancel(); - _lastInvokeTime = 0; - _lastArgs = _lastNamedArgs = _lastCallTime = _timer = null; - } - - /// Immediately invokes all the remaining delayed functions. - Object? flush() { - final now = DateTime.now().millisecondsSinceEpoch; - return _timer == null ? _result : _trailingEdge(now); - } - - /// True if there are functions remaining to get invoked. - bool get isPending => _timer != null; - - /// Calls/invokes this class like a function. - /// Pass [args] and [namedArgs] to be used while invoking [_func]. - Object? call( - List args, { - Map? namedArgs, - }) { - final time = DateTime.now().millisecondsSinceEpoch; - final isInvoking = _shouldInvoke(time); - - _lastArgs = args; - _lastNamedArgs = namedArgs as Map?; - _lastCallTime = time; - - if (isInvoking) { - if (_timer == null) { - return _leadingEdge(_lastCallTime); - } - if (_maxing) { - // Handle invocations in a tight loop. - _timer = _startTimer(_timerExpired, _wait); - return _invokeFunc(_lastCallTime); - } - } - _timer ??= _startTimer(_timerExpired, _wait); - return _result; - } -} - -/// Creates a throttled function that only invokes `func` at most once per -/// every `wait` milliseconds. The throttled function comes with a [Throttle.cancel] -/// method to cancel delayed `func` invocations and a [Throttle.flush] method to -/// immediately invoke them. Provide `leading` and/or `trailing` to indicate -/// whether `func` should be invoked on the `leading` and/or `trailing` edge of the `wait` timeout. -/// The `func` is invoked with the last arguments provided to the -/// throttled function. Subsequent calls to the throttled function return the -/// result of the last `func` invocation. -/// -/// **Note:** If `leading` and `trailing` options are `true`, `func` is -/// invoked on the trailing edge of the timeout only if the throttled function -/// is invoked more than once during the `wait` timeout. -/// -/// If `wait` is [Duration.zero] and `leading` is `false`, `func` invocation is deferred -/// until the next tick. -/// -/// See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) -/// for details over the differences between [Throttle] and [Debounce]. -/// -/// Some examples: -/// -/// Avoid excessively rebuilding UI progress while uploading data to server. -/// ```dart -/// void updateUI(Data data) { -/// updateProgress(data); -/// } -/// -/// final throttledUpdateUI = Throttle( -/// updateUI, -/// const Duration(milliseconds: 350), -/// ); -/// -/// void onUploadProgressChanged(progress) { -/// throttledUpdateUI(progress); -/// } -/// ``` -/// -/// Cancel the trailing throttled invocation. -/// ```dart -/// void dispose() { -/// throttled.cancel(); -/// } -/// ``` -/// -/// Check for pending invocations. -/// ```dart -/// final status = throttled.isPending ? "Pending..." : "Ready"; -/// ``` -class Throttle { - /// Creates a new instance of [Throttle] - Throttle( - Function func, - Duration wait, { - bool leading = true, - bool trailing = true, - }) : _debounce = Debounce( - func, - wait, - leading: leading, - trailing: trailing, - maxWait: wait, - ); - - final Debounce _debounce; - - /// Cancels all the remaining delayed functions. - void cancel() => _debounce.cancel(); - - /// Immediately invokes all the remaining delayed functions. - Object? flush() => _debounce.flush(); - - /// True if there are functions remaining to get invoked. - bool get isPending => _debounce.isPending; - - /// Calls/invokes this class like a function. - /// Pass [args] and [namedArgs] to be used while invoking `func`. - Object? call(List args, {Map? namedArgs}) => - _debounce.call(args, namedArgs: namedArgs); -} diff --git a/packages/stream_chat/lib/src/extensions/string_extension.dart b/packages/stream_chat/lib/src/extensions/string_extension.dart deleted file mode 100644 index 440e2cb6..00000000 --- a/packages/stream_chat/lib/src/extensions/string_extension.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:http_parser/http_parser.dart' as http_parser; -import 'package:mime/mime.dart'; - -/// Useful extension functions for [String] -extension StringX on String { - /// Returns the mime type from the passed file name. - http_parser.MediaType? get mimeType { - if (toLowerCase().endsWith('heic')) { - return http_parser.MediaType.parse('image/heic'); - } else { - final mimeType = lookupMimeType(this); - if (mimeType == null) { - return null; - } - return http_parser.MediaType.parse(mimeType); - } - } -} diff --git a/packages/stream_chat/lib/src/ws/websocket.dart b/packages/stream_chat/lib/src/ws/websocket.dart index 6c36fd0b..4c7c91ed 100644 --- a/packages/stream_chat/lib/src/ws/websocket.dart +++ b/packages/stream_chat/lib/src/ws/websocket.dart @@ -5,8 +5,7 @@ import 'dart:math' as math; import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; import 'package:rxdart/rxdart.dart'; -import 'package:stream_chat/src/errors/chat_error_code.dart'; -import 'package:stream_chat/src/errors/stream_chat_error.dart'; +import 'package:stream_chat/src/core/error/error.dart'; import 'package:stream_chat/src/ws/connection_status.dart'; import 'package:stream_chat/src/core/http/token.dart'; import 'package:stream_chat/src/core/http/token_manager.dart'; diff --git a/packages/stream_chat/lib/stream_chat.dart b/packages/stream_chat/lib/stream_chat.dart index 66dace1d..3c2e3d21 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -6,14 +6,14 @@ export 'package:dio/src/multipart_file.dart'; export 'package:dio/src/options.dart'; export 'package:dio/src/options.dart' show ProgressCallback; export 'package:logging/logging.dart' show Logger, Level; +export 'package:rate_limiter/rate_limiter.dart'; -export './src/api/channel.dart'; -export './src/client.dart'; export './src/core/api/attachment_file_uploader.dart' show AttachmentFileUploader; export './src/core/api/requests.dart'; export './src/core/api/requests.dart'; export './src/core/api/responses.dart'; +export './src/core/error/error.dart'; export './src/core/models/action.dart'; export './src/core/models/attachment.dart'; export './src/core/models/attachment_file.dart'; @@ -31,11 +31,10 @@ export './src/core/models/own_user.dart'; export './src/core/models/reaction.dart'; export './src/core/models/read.dart'; export './src/core/models/user.dart'; +export './src/core/util/extension.dart'; export './src/db/chat_persistence_client.dart'; -export './src/errors/chat_error_code.dart'; -export './src/errors/stream_chat_error.dart'; export './src/event_type.dart'; -export './src/extensions/rate_limit.dart'; -export './src/extensions/string_extension.dart'; export './src/location.dart'; export './src/ws/connection_status.dart'; +export 'src/client/channel.dart'; +export 'src/client/client.dart'; diff --git a/packages/stream_chat/lib/version.dart b/packages/stream_chat/lib/version.dart index 6c68818a..6bf106a2 100644 --- a/packages/stream_chat/lib/version.dart +++ b/packages/stream_chat/lib/version.dart @@ -1,4 +1,4 @@ -import 'package:stream_chat/src/client.dart'; +import 'package:stream_chat/src/client/client.dart'; /// Current package version /// Used in [StreamChatClient] to build the `x-stream-client` header diff --git a/packages/stream_chat/pubspec.yaml b/packages/stream_chat/pubspec.yaml index 23f6c114..f4554959 100644 --- a/packages/stream_chat/pubspec.yaml +++ b/packages/stream_chat/pubspec.yaml @@ -20,6 +20,7 @@ dependencies: logging: ^1.0.1 meta: ^1.3.0 mime: ^1.0.0 + rate_limiter: ^0.1.1 rxdart: ^0.27.0 uuid: ^3.0.4 web_socket_channel: ^2.0.0 diff --git a/packages/stream_chat/test/version_test.dart b/packages/stream_chat/test/version_test.dart index 8e7112a4..29076db0 100644 --- a/packages/stream_chat/test/version_test.dart +++ b/packages/stream_chat/test/version_test.dart @@ -4,6 +4,23 @@ import 'dart:io'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat/version.dart'; import 'package:test/test.dart'; +import 'dart:math' as math; + +// This alphabet uses `A-Za-z0-9_-` symbols. The genetic algorithm helped +// optimize the gzip compression for this alphabet. +const _alphabet = + 'ModuleSymbhasOwnPr-0123456789ABCDEFGHNRVfgctiUvz_KqYTJkLxpZXIjQW'; + +/// Generates a random String id +/// Adopted from: https://github.com/ai/nanoid/blob/main/non-secure/index.js +String randomId({int size = 21}) { + var id = ''; + for (var i = 0; i < size; i++) { + id += _alphabet[(math.Random().nextDouble() * 64).floor() | 0]; + } + return id; +} + void prepareTest() { // https://github.com/flutter/flutter/issues/20907 @@ -15,30 +32,32 @@ void prepareTest() { void main() { prepareTest(); test('stream chat version matches pubspec', () async { - /// Create a new instance of [StreamChatClient] - /// by passing the apikey obtained from your project dashboard. - final client = StreamChatClient('b67pax5b2wdq', logLevel: Level.INFO); + print(randomId()); - /// Set the current user. In a production scenario, this should be done using - /// a backend to generate a user token using our server SDK. - /// Please see the following for more information: - /// https://getstream.io/chat/docs/ios_user_setup_and_tokens/ - await client.connectUser( - User( - id: 'cool-shadow-7', - extraData: { - 'image': - 'https://getstream.io/random_png/?id=cool-shadow-7&name=Cool+shadow', - }, - ), - '''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiY29vbC1zaGFkb3ctNyJ9.gkOlCRb1qgy4joHPaxFwPOdXcGvSPvp6QY0S4mpRkVo''', - ); - - try { - await client.banUser('asdasdas'); - } catch (e) { - print(e); - } + // /// Create a new instance of [StreamChatClient] + // /// by passing the apikey obtained from your project dashboard. + // final client = StreamChatClient('b67pax5b2wdq', logLevel: Level.INFO); + // + // /// Set the current user. In a production scenario, this should be done using + // /// a backend to generate a user token using our server SDK. + // /// Please see the following for more information: + // /// https://getstream.io/chat/docs/ios_user_setup_and_tokens/ + // await client.connectUser( + // User( + // id: 'cool-shadow-7', + // extraData: { + // 'image': + // 'https://getstream.io/random_png/?id=cool-shadow-7&name=Cool+shadow', + // }, + // ), + // '''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiY29vbC1zaGFkb3ctNyJ9.gkOlCRb1qgy4joHPaxFwPOdXcGvSPvp6QY0S4mpRkVo''', + // ); + // + // try { + // await client.banUser('asdasdas'); + // } catch (e) { + // print(e); + // } // final pubspecPath = '${Directory.current.path}/pubspec.yaml'; // final pubspec = File(pubspecPath).readAsStringSync(); From 1626d7432cbd3d11d16311b866294dba842b4132 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 3 Jun 2021 18:06:56 +0530 Subject: [PATCH 011/150] add websocket_test.dart --- packages/stream_chat/analysis_options.yaml | 2 +- .../lib/src/core/api/responses.dart | 5 +- .../lib/src/core/api/responses.g.dart | 9 + packages/stream_chat/pubspec.yaml | 2 +- packages/stream_chat/test/src/fakes.dart | 27 ++ packages/stream_chat/test/src/mocks.dart | 6 + .../test/src/ws/websocket_test.dart | 344 ++++++++++++++++++ packages/stream_chat/test/version_test.dart | 62 +--- 8 files changed, 400 insertions(+), 57 deletions(-) create mode 100644 packages/stream_chat/test/src/fakes.dart create mode 100644 packages/stream_chat/test/src/mocks.dart create mode 100644 packages/stream_chat/test/src/ws/websocket_test.dart diff --git a/packages/stream_chat/analysis_options.yaml b/packages/stream_chat/analysis_options.yaml index f0a87ea5..ed0ce983 100644 --- a/packages/stream_chat/analysis_options.yaml +++ b/packages/stream_chat/analysis_options.yaml @@ -6,7 +6,7 @@ analyzer: - example/** - lib/src/emoji - lib/**/*.freezed.dart - - test/** +# - test/** linter: rules: diff --git a/packages/stream_chat/lib/src/core/api/responses.dart b/packages/stream_chat/lib/src/core/api/responses.dart index b6d6e801..ccf03822 100644 --- a/packages/stream_chat/lib/src/core/api/responses.dart +++ b/packages/stream_chat/lib/src/core/api/responses.dart @@ -18,7 +18,7 @@ class _BaseResponse { } /// Model response for [StreamChatNetworkError] data -@JsonSerializable(createToJson: false) +@JsonSerializable() class ErrorResponse extends _BaseResponse { /// int? code; @@ -37,6 +37,9 @@ class ErrorResponse extends _BaseResponse { static ErrorResponse fromJson(Map json) => _$ErrorResponseFromJson(json); + /// Serialize to json + Map toJson() => _$ErrorResponseToJson(this); + @override String toString() => 'ErrorResponse(code: $code, ' 'message: $message, ' diff --git a/packages/stream_chat/lib/src/core/api/responses.g.dart b/packages/stream_chat/lib/src/core/api/responses.g.dart index c1c1f302..deba55f0 100644 --- a/packages/stream_chat/lib/src/core/api/responses.g.dart +++ b/packages/stream_chat/lib/src/core/api/responses.g.dart @@ -15,6 +15,15 @@ ErrorResponse _$ErrorResponseFromJson(Map json) { ..moreInfo = json['more_info'] as String?; } +Map _$ErrorResponseToJson(ErrorResponse instance) => + { + 'duration': instance.duration, + 'code': instance.code, + 'message': instance.message, + 'StatusCode': instance.statusCode, + 'more_info': instance.moreInfo, + }; + SyncResponse _$SyncResponseFromJson(Map json) { return SyncResponse() ..duration = json['duration'] as String? diff --git a/packages/stream_chat/pubspec.yaml b/packages/stream_chat/pubspec.yaml index f4554959..e6259b42 100644 --- a/packages/stream_chat/pubspec.yaml +++ b/packages/stream_chat/pubspec.yaml @@ -30,4 +30,4 @@ dev_dependencies: freezed: ^0.14.1+3 json_serializable: ^4.1.0 mocktail: ^0.1.1 - test: ^1.16.8 + test: ^1.16.8 \ No newline at end of file diff --git a/packages/stream_chat/test/src/fakes.dart b/packages/stream_chat/test/src/fakes.dart new file mode 100644 index 00000000..e96cbd12 --- /dev/null +++ b/packages/stream_chat/test/src/fakes.dart @@ -0,0 +1,27 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/core/http/token.dart'; +import 'package:stream_chat/src/core/http/token_manager.dart'; + +class FakeTokenManager extends Fake implements TokenManager { + final token = Token.development('test-user-id'); + + @override + bool get isStatic => true; + + @override + String? get userId => token.userId; + + @override + Future loadToken({bool refresh = false}) async => token; + + @override + Future setTokenOrProvider( + String userId, { + Token? token, + TokenProvider? provider, + }) async => + this.token; + + @override + void reset() {} +} diff --git a/packages/stream_chat/test/src/mocks.dart b/packages/stream_chat/test/src/mocks.dart new file mode 100644 index 00000000..98e84d77 --- /dev/null +++ b/packages/stream_chat/test/src/mocks.dart @@ -0,0 +1,6 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; + +class MockWebSocketChannel extends Mock implements WebSocketChannel {} + +class MockWebSocketSink extends Mock implements WebSocketSink {} diff --git a/packages/stream_chat/test/src/ws/websocket_test.dart b/packages/stream_chat/test/src/ws/websocket_test.dart new file mode 100644 index 00000000..73eecf3f --- /dev/null +++ b/packages/stream_chat/test/src/ws/websocket_test.dart @@ -0,0 +1,344 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:stream_chat/src/core/http/token_manager.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:test/test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/ws/websocket.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; + +import '../fakes.dart'; +import '../mocks.dart'; + +void main() { + late TokenManager tokenManager; + late WebSocketChannel webSocketChannel; + late WebSocketSink webSocketSink; + late WebSocket webSocket; + + setUp(() { + tokenManager = FakeTokenManager(); + webSocketChannel = MockWebSocketChannel(); + + WebSocketChannel channelProvider( + Uri uri, { + Iterable? protocols, + }) => + webSocketChannel; + + webSocket = WebSocket( + apiKey: 'api-key', + baseUrl: 'base-url', + tokenManager: tokenManager, + webSocketChannelProvider: channelProvider, + ); + + webSocketSink = MockWebSocketSink(); + when(() => webSocketChannel.sink).thenReturn(webSocketSink); + + var webSocketController = StreamController.broadcast(); + when(() => webSocketChannel.stream).thenAnswer( + (_) => webSocketController.stream, + ); + when(() => webSocketSink.add(any())).thenAnswer((invocation) { + webSocketController.add(invocation.positionalArguments.first); + }); + when(() => webSocketSink.close(any(), any())).thenAnswer( + (_) { + final res = webSocketController.close(); + // re-initializing for future events + webSocketController = StreamController.broadcast(); + return res; + }, + ); + }); + + tearDown(() { + tokenManager.reset(); + webSocket.disconnect(); + }); + + test('`connect` successfully with the provided user', () async { + final user = OwnUser(id: 'test-user'); + const connectionId = 'test-connection-id'; + // Sends connect event to web-socket stream + final timer = Timer(const Duration(milliseconds: 300), () { + final event = Event( + type: EventType.healthCheck, + connectionId: connectionId, + me: user, + ); + webSocketSink.add(json.encode(event)); + }); + + expectLater( + webSocket.connectionStatusStream, + emitsInOrder([ + ConnectionStatus.disconnected, + ConnectionStatus.connecting, + ConnectionStatus.connected, + ]), + ); + + final event = await webSocket.connect(user); + + expect(event.type, EventType.healthCheck); + expect(event.connectionId, connectionId); + expect(event.me, isNotNull); + expect(event.me!.id, user.id); + + addTearDown(timer.cancel); + }); + + test('`connect` should throw if already in connection attempt', () async { + final user = OwnUser(id: 'test-user'); + webSocket.connect(user); + try { + // calling again before previous attempt finishes + await webSocket.connect(user); + } catch (e) { + expect(e, isA()); + } + }); + + test('`connect` should throw if `onMessage` contains error', () async { + final user = OwnUser(id: 'test-user'); + final error = ErrorResponse() + ..code = 333 + ..message = 'Invalid request'; + // Sends error event to web-socket stream + final timer = Timer(const Duration(milliseconds: 300), () { + webSocketSink.add(json.encode({'error': error})); + }); + + expectLater( + webSocket.connectionStatusStream, + emitsInOrder([ + ConnectionStatus.disconnected, + ConnectionStatus.connecting, + ConnectionStatus.disconnected, + ]), + ); + + try { + await webSocket.connect(user); + } catch (e) { + expect(e, isA()); + final err = e as StreamWebSocketError; + expect(err.code, error.code); + expect(err.message, error.message); + } + + addTearDown(timer.cancel); + }); + + test( + 'should `reconnect` automatically ' + 'if `onMessage` throws error after getting connected', + () async { + final user = OwnUser(id: 'test-user'); + const connectionId = 'test-connection-id'; + // Sends connect event to web-socket stream + final timer = Timer(const Duration(milliseconds: 300), () { + final event = Event( + type: EventType.healthCheck, + connectionId: connectionId, + me: user, + ); + webSocketSink.add(json.encode(event)); + }); + + expectLater( + webSocket.connectionStatusStream, + emitsInOrder([ + ConnectionStatus.disconnected, + ConnectionStatus.connecting, + ConnectionStatus.connected, + // starts reconnecting + ConnectionStatus.connecting, + ConnectionStatus.connected, + ]), + ); + + await webSocket.connect(user); + + final error = ErrorResponse() + ..code = 333 + ..message = 'Invalid request'; + // Sends error event to web-socket stream + webSocketSink.add(json.encode({'error': error})); + + final reconnectTimer = Timer(const Duration(seconds: 3), () { + final event = Event( + type: EventType.healthCheck, + connectionId: connectionId, + me: user, + ); + webSocketSink.add(json.encode(event)); + }); + + expect(webSocket.connectionId, connectionId); + + addTearDown(() { + timer.cancel(); + reconnectTimer.cancel(); + }); + }, + ); + + test( + '`onMessage` should handle `health.check` event if `me` is null', + () async { + final user = OwnUser(id: 'test-user'); + const connectionId = 'test-connection-id'; + // Sends connect event to web-socket stream + final timer = Timer(const Duration(milliseconds: 300), () { + final event = Event( + type: EventType.healthCheck, + connectionId: connectionId, + me: user, + ); + webSocketSink.add(json.encode(event)); + }); + + expectLater( + webSocket.connectionStatusStream, + emitsInOrder([ + ConnectionStatus.disconnected, + ConnectionStatus.connecting, + ConnectionStatus.connected, + ]), + ); + + final event = await webSocket.connect(user); + + expect(event.type, EventType.healthCheck); + expect(event.connectionId, connectionId); + expect(event.me, isNotNull); + expect(event.me!.id, user.id); + + const newConnectionId = 'new-connection-id'; + final healthCheckEvent = Event( + type: EventType.healthCheck, + connectionId: newConnectionId, + ); + webSocketSink.add(json.encode(healthCheckEvent)); + + await Future.delayed(const Duration(milliseconds: 300)); + + expectLater(webSocket.connectionId, newConnectionId); + + addTearDown(timer.cancel); + }, + ); + + test('should call `onConnectionError` if web-socket stream throws', () async { + final user = OwnUser(id: 'test-user'); + // Sends connect event to web-socket stream + final timer = Timer(const Duration(milliseconds: 300), () { + const error = StreamWebSocketError('test-error'); + webSocketSink.addError(error); + }); + + expectLater( + webSocket.connectionStatusStream, + emitsInOrder([ + ConnectionStatus.disconnected, + ConnectionStatus.connecting, + // throws error, reconnects + ConnectionStatus.connected, + ]), + ); + + webSocket.connect(user); + + // Assuming web-socket stream will add error + // and web-socket now trying to reconnect + await Future.delayed(const Duration(seconds: 3)); + + const connectionId = 'test-connection-id'; + // Sends connect event to web-socket stream + final event = Event( + type: EventType.healthCheck, + connectionId: connectionId, + me: user, + ); + webSocketSink.add(json.encode(event)); + + addTearDown(timer.cancel); + }); + + test( + 'should call `onConnectionClosed` if web-socket stream throws', + () async { + final user = OwnUser(id: 'test-user'); + // Sends connect event to web-socket stream + final timer = Timer(const Duration(milliseconds: 300), () { + webSocketSink.close(); + }); + + expectLater( + webSocket.connectionStatusStream, + emitsInOrder([ + ConnectionStatus.disconnected, + ConnectionStatus.connecting, + // throws error, reconnects + ConnectionStatus.connected, + ]), + ); + + webSocket.connect(user); + + // Assuming web-socket stream will add error + // and web-socket now trying to reconnect + await Future.delayed(const Duration(seconds: 3)); + + const connectionId = 'test-connection-id'; + // Sends connect event to web-socket stream + final event = Event( + type: EventType.healthCheck, + connectionId: connectionId, + me: user, + ); + webSocketSink.add(json.encode(event)); + + addTearDown(timer.cancel); + }, + ); + + test('`disconnect` successfully disconnects the current user', () async { + final user = OwnUser(id: 'test-user'); + const connectionId = 'test-connection-id'; + // Sends connect event to web-socket stream + final timer = Timer(const Duration(milliseconds: 300), () { + final event = Event( + type: EventType.healthCheck, + connectionId: connectionId, + me: user, + ); + webSocketSink.add(json.encode(event)); + }); + + expectLater( + webSocket.connectionStatusStream, + emitsInOrder([ + ConnectionStatus.disconnected, + ConnectionStatus.connecting, + ConnectionStatus.connected, + // after disconnect + ConnectionStatus.disconnected, + ]), + ); + + final event = await webSocket.connect(user); + + expect(event.type, EventType.healthCheck); + expect(event.connectionId, connectionId); + expect(event.me?.id, user.id); + + webSocket.disconnect(); + + addTearDown(timer.cancel); + }); +} diff --git a/packages/stream_chat/test/version_test.dart b/packages/stream_chat/test/version_test.dart index 29076db0..34ecc750 100644 --- a/packages/stream_chat/test/version_test.dart +++ b/packages/stream_chat/test/version_test.dart @@ -1,26 +1,7 @@ -import 'dart:async'; import 'dart:io'; -import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat/version.dart'; import 'package:test/test.dart'; -import 'dart:math' as math; - -// This alphabet uses `A-Za-z0-9_-` symbols. The genetic algorithm helped -// optimize the gzip compression for this alphabet. -const _alphabet = - 'ModuleSymbhasOwnPr-0123456789ABCDEFGHNRVfgctiUvz_KqYTJkLxpZXIjQW'; - -/// Generates a random String id -/// Adopted from: https://github.com/ai/nanoid/blob/main/non-secure/index.js -String randomId({int size = 21}) { - var id = ''; - for (var i = 0; i < size; i++) { - id += _alphabet[(math.Random().nextDouble() * 64).floor() | 0]; - } - return id; -} - void prepareTest() { // https://github.com/flutter/flutter/issues/20907 @@ -31,40 +12,13 @@ void prepareTest() { void main() { prepareTest(); - test('stream chat version matches pubspec', () async { - print(randomId()); - - // /// Create a new instance of [StreamChatClient] - // /// by passing the apikey obtained from your project dashboard. - // final client = StreamChatClient('b67pax5b2wdq', logLevel: Level.INFO); - // - // /// Set the current user. In a production scenario, this should be done using - // /// a backend to generate a user token using our server SDK. - // /// Please see the following for more information: - // /// https://getstream.io/chat/docs/ios_user_setup_and_tokens/ - // await client.connectUser( - // User( - // id: 'cool-shadow-7', - // extraData: { - // 'image': - // 'https://getstream.io/random_png/?id=cool-shadow-7&name=Cool+shadow', - // }, - // ), - // '''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiY29vbC1zaGFkb3ctNyJ9.gkOlCRb1qgy4joHPaxFwPOdXcGvSPvp6QY0S4mpRkVo''', - // ); - // - // try { - // await client.banUser('asdasdas'); - // } catch (e) { - // print(e); - // } - - // final pubspecPath = '${Directory.current.path}/pubspec.yaml'; - // final pubspec = File(pubspecPath).readAsStringSync(); - // // ignore: unnecessary_string_escapes - // final regex = RegExp('version:\s*(.*)'); - // final match = regex.firstMatch(pubspec); - // expect(match, isNotNull); - // expect(PACKAGE_VERSION, match?.group(1)?.trim()); + test('stream chat version matches pubspec', () { + final pubspecPath = '${Directory.current.path}/pubspec.yaml'; + final pubspec = File(pubspecPath).readAsStringSync(); + // ignore: unnecessary_string_escapes + final regex = RegExp('version:\s*(.*)'); + final match = regex.firstMatch(pubspec); + expect(match, isNotNull); + expect(PACKAGE_VERSION, match?.group(1)?.trim()); }); } From 63d391ba7aba208444c2646fd2c836c08916081c Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 3 Jun 2021 20:04:32 +0530 Subject: [PATCH 012/150] add chat_persistence_client_test.dart --- .../src/db/chat_persistence_client_test.dart | 191 ++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 packages/stream_chat/test/src/db/chat_persistence_client_test.dart diff --git a/packages/stream_chat/test/src/db/chat_persistence_client_test.dart b/packages/stream_chat/test/src/db/chat_persistence_client_test.dart new file mode 100644 index 00000000..0ab7695f --- /dev/null +++ b/packages/stream_chat/test/src/db/chat_persistence_client_test.dart @@ -0,0 +1,191 @@ +import 'package:stream_chat/src/core/api/requests.dart'; +import 'package:stream_chat/src/core/models/channel_model.dart'; +import 'package:stream_chat/src/core/models/channel_state.dart'; +import 'package:stream_chat/src/core/models/event.dart'; +import 'package:stream_chat/src/core/models/filter.dart'; +import 'package:stream_chat/src/core/models/member.dart'; +import 'package:stream_chat/src/core/models/message.dart'; +import 'package:stream_chat/src/core/models/reaction.dart'; +import 'package:stream_chat/src/core/models/read.dart'; +import 'package:stream_chat/src/core/models/user.dart'; +import 'package:stream_chat/src/db/chat_persistence_client.dart'; +import 'package:test/test.dart'; + +class TestPersistenceClient extends ChatPersistenceClient { + @override + Future connect(String userId) => throw UnimplementedError(); + + @override + Future deleteChannels(List cids) => throw UnimplementedError(); + + @override + Future deleteMembersByCids(List cids) => Future.value(); + + @override + Future deleteMessageByCids(List cids) => Future.value(); + + @override + Future deleteMessageByIds(List messageIds) => Future.value(); + + @override + Future deletePinnedMessageByCids(List cids) => Future.value(); + + @override + Future deletePinnedMessageByIds(List messageIds) => + Future.value(); + + @override + Future deleteReactionsByMessageId(List messageIds) => + Future.value(); + + @override + Future disconnect({bool flush = false}) => throw UnimplementedError(); + + @override + Future getChannelByCid(String cid) async => + ChannelModel(cid: cid); + + @override + Future> getChannelCids() => throw UnimplementedError(); + + @override + Future> getChannelStates( + {Filter? filter, + List>? sort, + PaginationParams? paginationParams}) => + throw UnimplementedError(); + + @override + Future>> getChannelThreads(String cid) => + throw UnimplementedError(); + + @override + Future getConnectionInfo() => throw UnimplementedError(); + + @override + Future getLastSyncAt() => throw UnimplementedError(); + + @override + Future> getMembersByCid(String cid) async => []; + + @override + Future> getMessagesByCid(String cid, + {PaginationParams? messagePagination}) async => + []; + + @override + Future> getPinnedMessagesByCid(String cid, + {PaginationParams? messagePagination}) async => + []; + + @override + Future> getReadsByCid(String cid) async => []; + + @override + Future> getReplies(String parentId, + {PaginationParams? options}) => + throw UnimplementedError(); + + @override + Future updateChannelQueries(Filter? filter, List cids, + {bool clearQueryCache = false}) => + throw UnimplementedError(); + + @override + Future updateChannels(List channels) => Future.value(); + + @override + Future updateConnectionInfo(Event event) => throw UnimplementedError(); + + @override + Future updateLastSyncAt(DateTime lastSyncAt) => + throw UnimplementedError(); + + @override + Future updateMembers(String cid, List members) => + Future.value(); + + @override + Future updateMessages(String cid, List messages) => + Future.value(); + + @override + Future updatePinnedMessages(String cid, List messages) => + Future.value(); + + @override + Future updateReactions(List reactions) => Future.value(); + + @override + Future updateReads(String cid, List reads) => Future.value(); + + @override + Future updateUsers(List users) => Future.value(); +} + +void main() { + group('chatPersistenceClient', () { + final persistenceClient = TestPersistenceClient(); + + test('deleteMessageById', () { + const messageId = 'message-id'; + persistenceClient.deleteMessageById(messageId); + }); + + test('deleteMessageByCid', () { + const messageId = 'message-id'; + persistenceClient.deleteMessageByCid(messageId); + }); + + test('deletePinnedMessageById', () { + const messageId = 'message-id'; + persistenceClient.deletePinnedMessageById(messageId); + }); + + test('deletePinnedMessageByCid', () { + const messageId = 'message-id'; + persistenceClient.deletePinnedMessageByCid(messageId); + }); + + test('getChannelStateByCid', () async { + const cid = 'test:cid'; + final channelState = await persistenceClient.getChannelStateByCid(cid); + expect(channelState, isNotNull); + }); + + test('updateChannelState', () async { + const cid = 'test:cid'; + final channelState = ChannelState(); + persistenceClient.updateChannelState(channelState); + }); + + test('updateChannelStates', () async { + const cid = 'test:cid'; + final user = User(id: 'test-user-id'); + final channelState = ChannelState( + channel: ChannelModel(cid: cid, createdBy: user), + messages: [ + Message( + id: 'test-message', + text: 'test-message', + user: user, + ownReactions: [Reaction(type: 'test', user: user)], + latestReactions: [Reaction(type: 'test', user: user)], + ) + ], + pinnedMessages: [ + Message( + id: 'test-message', + text: 'test-message', + user: user, + ownReactions: [Reaction(type: 'test', user: user)], + latestReactions: [Reaction(type: 'test', user: user)], + ) + ], + read: [Read(lastRead: DateTime.now(), user: user)], + members: [Member(user: user)], + ); + persistenceClient.updateChannelStates([channelState]); + }); + }); +} From 7a951920938be04d5e885b15ca7234688a68ce00 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 3 Jun 2021 21:44:12 +0530 Subject: [PATCH 013/150] add extension_test.dart, serializer_test.dart and utils_test.dart --- .../lib/src/core/models/attachment.dart | 14 +++--- .../lib/src/core/models/channel_model.dart | 26 +++++----- .../lib/src/core/models/event.dart | 14 +++--- .../lib/src/core/models/message.dart | 46 +++++++++--------- .../stream_chat/lib/src/core/models/mute.dart | 10 ++-- .../lib/src/core/models/own_user.dart | 18 +++---- .../lib/src/core/models/reaction.dart | 12 ++--- .../stream_chat/lib/src/core/models/user.dart | 22 ++++----- .../lib/src/core/util/extension.dart | 9 ++-- .../{serialization.dart => serializer.dart} | 2 +- .../test/src/core/util/extension_test.dart | 48 +++++++++++++++++++ .../test/src/core/util/serializer_test.dart | 43 +++++++++++++++++ .../test/src/core/util/utils_test.dart | 15 ++++++ 13 files changed, 194 insertions(+), 85 deletions(-) rename packages/stream_chat/lib/src/core/util/{serialization.dart => serializer.dart} (98%) create mode 100644 packages/stream_chat/test/src/core/util/extension_test.dart create mode 100644 packages/stream_chat/test/src/core/util/serializer_test.dart create mode 100644 packages/stream_chat/test/src/core/util/utils_test.dart diff --git a/packages/stream_chat/lib/src/core/models/attachment.dart b/packages/stream_chat/lib/src/core/models/attachment.dart index a5f9f19d..8973aee2 100644 --- a/packages/stream_chat/lib/src/core/models/attachment.dart +++ b/packages/stream_chat/lib/src/core/models/attachment.dart @@ -4,7 +4,7 @@ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/core/models/action.dart'; import 'package:stream_chat/src/core/models/attachment_file.dart'; -import 'package:stream_chat/src/core/util/serialization.dart'; +import 'package:stream_chat/src/core/util/serializer.dart'; import 'package:uuid/uuid.dart'; part 'attachment.g.dart'; @@ -49,11 +49,11 @@ class Attachment extends Equatable { /// Create a new instance from a json factory Attachment.fromJson(Map json) => _$AttachmentFromJson( - Serialization.moveToExtraDataFromRoot(json, topLevelFields)); + Serializer.moveToExtraDataFromRoot(json, topLevelFields)); /// Create a new instance from a db data factory Attachment.fromData(Map json) => - _$AttachmentFromJson(Serialization.moveToExtraDataFromRoot( + _$AttachmentFromJson(Serializer.moveToExtraDataFromRoot( json, topLevelFields + dbSpecificTopLevelFields)); ///The attachment type based on the URL resource. This can be: audio, @@ -122,7 +122,7 @@ class Attachment extends Equatable { final String id; /// Known top level fields. - /// Useful for [Serialization] methods. + /// Useful for [Serializer] methods. static const topLevelFields = [ 'type', 'title_link', @@ -145,7 +145,7 @@ class Attachment extends Equatable { ]; /// Known db specific top level fields. - /// Useful for [Serialization] methods. + /// Useful for [Serializer] methods. static const dbSpecificTopLevelFields = [ 'id', 'upload_state', @@ -154,12 +154,12 @@ class Attachment extends Equatable { /// Serialize to json Map toJson() => - Serialization.moveFromExtraDataToRoot(_$AttachmentToJson(this)) + Serializer.moveFromExtraDataToRoot(_$AttachmentToJson(this)) ..removeWhere((key, value) => dbSpecificTopLevelFields.contains(key)); /// Serialize to db data Map toData() => - Serialization.moveFromExtraDataToRoot(_$AttachmentToJson(this)); + Serializer.moveFromExtraDataToRoot(_$AttachmentToJson(this)); Attachment copyWith({ String? id, diff --git a/packages/stream_chat/lib/src/core/models/channel_model.dart b/packages/stream_chat/lib/src/core/models/channel_model.dart index 9e50ed3e..74195c71 100644 --- a/packages/stream_chat/lib/src/core/models/channel_model.dart +++ b/packages/stream_chat/lib/src/core/models/channel_model.dart @@ -1,6 +1,6 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/core/models/channel_config.dart'; -import 'package:stream_chat/src/core/util/serialization.dart'; +import 'package:stream_chat/src/core/util/serializer.dart'; import 'package:stream_chat/src/core/models/user.dart'; part 'channel_model.g.dart'; @@ -37,7 +37,7 @@ class ChannelModel { /// Create a new instance from a json factory ChannelModel.fromJson(Map json) => _$ChannelModelFromJson( - Serialization.moveToExtraDataFromRoot(json, topLevelFields)); + Serializer.moveToExtraDataFromRoot(json, topLevelFields)); /// The id of this channel final String id; @@ -46,15 +46,15 @@ class ChannelModel { final String type; /// The cid of this channel - @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final String cid; /// The channel configuration data - @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final ChannelConfig config; /// The user that created this channel - @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final User? createdBy; /// True if this channel is frozen @@ -62,24 +62,24 @@ class ChannelModel { final bool frozen; /// The date of the last message - @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final DateTime? lastMessageAt; /// The date of channel creation - @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final DateTime createdAt; /// The date of the last channel update - @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final DateTime updatedAt; /// The date of channel deletion - @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final DateTime? deletedAt; /// The count of this channel members @JsonKey( - includeIfNull: false, toJson: Serialization.readOnly, defaultValue: 0) + includeIfNull: false, toJson: Serializer.readOnly, defaultValue: 0) final int memberCount; /// Map of custom channel extraData @@ -90,11 +90,11 @@ class ChannelModel { final Map extraData; /// The team the channel belongs to - @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final String? team; /// Known top level fields. - /// Useful for [Serialization] methods. + /// Useful for [Serializer] methods. static const topLevelFields = [ 'id', 'type', @@ -115,7 +115,7 @@ class ChannelModel { extraData.containsKey('name') ? extraData['name']! as String : cid; /// Serialize to json - Map toJson() => Serialization.moveFromExtraDataToRoot( + Map toJson() => Serializer.moveFromExtraDataToRoot( _$ChannelModelToJson(this), ); diff --git a/packages/stream_chat/lib/src/core/models/event.dart b/packages/stream_chat/lib/src/core/models/event.dart index 7c1be679..33e696c4 100644 --- a/packages/stream_chat/lib/src/core/models/event.dart +++ b/packages/stream_chat/lib/src/core/models/event.dart @@ -1,7 +1,7 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/core/models/channel_model.dart'; import 'package:stream_chat/src/core/models/message.dart'; -import 'package:stream_chat/src/core/util/serialization.dart'; +import 'package:stream_chat/src/core/util/serializer.dart'; import 'package:stream_chat/stream_chat.dart'; part 'event.g.dart'; @@ -33,7 +33,7 @@ class Event { /// Create a new instance from a json factory Event.fromJson(Map json) => - _$EventFromJson(Serialization.moveToExtraDataFromRoot( + _$EventFromJson(Serializer.moveToExtraDataFromRoot( json, topLevelFields, )); @@ -96,7 +96,7 @@ class Event { final Map extraData; /// Known top level fields. - /// Useful for [Serialization] methods. + /// Useful for [Serializer] methods. static final topLevelFields = [ 'type', 'cid', @@ -118,7 +118,7 @@ class Event { ]; /// Serialize to json - Map toJson() => Serialization.moveFromExtraDataToRoot( + Map toJson() => Serializer.moveFromExtraDataToRoot( _$EventToJson(this), ); @@ -198,7 +198,7 @@ class EventChannel extends ChannelModel { /// Create a new instance from a json factory EventChannel.fromJson(Map json) => - _$EventChannelFromJson(Serialization.moveToExtraDataFromRoot( + _$EventChannelFromJson(Serializer.moveToExtraDataFromRoot( json, topLevelFields, )); @@ -207,7 +207,7 @@ class EventChannel extends ChannelModel { final List? members; /// Known top level fields. - /// Useful for [Serialization] methods. + /// Useful for [Serializer] methods. static final topLevelFields = [ 'members', ...ChannelModel.topLevelFields, @@ -215,7 +215,7 @@ class EventChannel extends ChannelModel { /// Serialize to json @override - Map toJson() => Serialization.moveFromExtraDataToRoot( + Map toJson() => Serializer.moveFromExtraDataToRoot( _$EventChannelToJson(this), ); } diff --git a/packages/stream_chat/lib/src/core/models/message.dart b/packages/stream_chat/lib/src/core/models/message.dart index 1266519c..426b863a 100644 --- a/packages/stream_chat/lib/src/core/models/message.dart +++ b/packages/stream_chat/lib/src/core/models/message.dart @@ -2,7 +2,7 @@ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/core/models/attachment.dart'; import 'package:stream_chat/src/core/models/reaction.dart'; -import 'package:stream_chat/src/core/util/serialization.dart'; +import 'package:stream_chat/src/core/util/serializer.dart'; import 'package:stream_chat/src/core/models/user.dart'; import 'package:uuid/uuid.dart'; @@ -81,7 +81,7 @@ class Message extends Equatable { /// Create a new instance from a json factory Message.fromJson(Map json) => _$MessageFromJson( - Serialization.moveToExtraDataFromRoot(json, topLevelFields)); + Serializer.moveToExtraDataFromRoot(json, topLevelFields)); /// The message ID. This is either created by Stream or set client side when /// the message is added. @@ -97,7 +97,7 @@ class Message extends Equatable { /// The message type @JsonKey( includeIfNull: false, - toJson: Serialization.readOnly, + toJson: Serializer.readOnly, defaultValue: 'regular', ) final String type; @@ -118,37 +118,37 @@ class Message extends Equatable { final List mentionedUsers; /// A map describing the count of number of every reaction - @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final Map? reactionCounts; /// A map describing the count of score of every reaction - @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final Map? reactionScores; /// The latest reactions to the message created by any user. - @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final List? latestReactions; /// The reactions added to the message by the current user. - @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final List? ownReactions; /// The ID of the parent message, if the message is a thread reply. final String? parentId; /// A quoted reply message - @JsonKey(toJson: Serialization.readOnly) + @JsonKey(toJson: Serializer.readOnly) final Message? quotedMessage; /// The ID of the quoted message, if the message is a quoted reply. final String? quotedMessageId; /// Reserved field indicating the number of replies for this message. - @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final int? replyCount; /// Reserved field indicating the thread participants for this message. - @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final List? threadParticipants; /// Check if this message needs to show in the channel. @@ -165,25 +165,25 @@ class Message extends Equatable { /// If true the message is shadowed @JsonKey( includeIfNull: false, - toJson: Serialization.readOnly, + toJson: Serializer.readOnly, defaultValue: false, ) final bool shadowed; /// A used command name. - @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final String? command; /// Reserved field indicating when the message was created. - @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final DateTime createdAt; /// Reserved field indicating when the message was updated last time. - @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final DateTime updatedAt; /// User who sent the message - @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final User? user; /// If true the message is pinned @@ -191,7 +191,7 @@ class Message extends Equatable { final bool pinned; /// Reserved field indicating when the message was pinned - @JsonKey(toJson: Serialization.readOnly) + @JsonKey(toJson: Serializer.readOnly) final DateTime? pinnedAt; /// Reserved field indicating when the message will expire @@ -200,7 +200,7 @@ class Message extends Equatable { final DateTime? pinExpires; /// Reserved field indicating who pinned the message - @JsonKey(toJson: Serialization.readOnly) + @JsonKey(toJson: Serializer.readOnly) final User? pinnedBy; /// Message custom extraData @@ -220,11 +220,11 @@ class Message extends Equatable { bool get isEphemeral => type == 'ephemeral'; /// Reserved field indicating when the message was deleted. - @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final DateTime? deletedAt; /// Known top level fields. - /// Useful for [Serialization] methods. + /// Useful for [Serializer] methods. static const topLevelFields = [ 'id', 'text', @@ -257,7 +257,7 @@ class Message extends Equatable { ]; /// Serialize to json - Map toJson() => Serialization.moveFromExtraDataToRoot( + Map toJson() => Serializer.moveFromExtraDataToRoot( _$MessageToJson(this), ); @@ -412,14 +412,14 @@ class TranslatedMessage extends Message { /// Create a new instance from a json factory TranslatedMessage.fromJson(Map json) => _$TranslatedMessageFromJson( - Serialization.moveToExtraDataFromRoot(json, topLevelFields), + Serializer.moveToExtraDataFromRoot(json, topLevelFields), ); /// A Map of final Map? i18n; /// Known top level fields. - /// Useful for [Serialization] methods. + /// Useful for [Serializer] methods. static final topLevelFields = [ 'i18n', ...Message.topLevelFields, @@ -427,7 +427,7 @@ class TranslatedMessage extends Message { /// Serialize to json @override - Map toJson() => Serialization.moveFromExtraDataToRoot( + Map toJson() => Serializer.moveFromExtraDataToRoot( _$TranslatedMessageToJson(this), ); } diff --git a/packages/stream_chat/lib/src/core/models/mute.dart b/packages/stream_chat/lib/src/core/models/mute.dart index 7a82fc54..9166d3e4 100644 --- a/packages/stream_chat/lib/src/core/models/mute.dart +++ b/packages/stream_chat/lib/src/core/models/mute.dart @@ -1,6 +1,6 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/core/models/channel_model.dart'; -import 'package:stream_chat/src/core/util/serialization.dart'; +import 'package:stream_chat/src/core/util/serializer.dart'; import 'package:stream_chat/src/core/models/user.dart'; part 'mute.g.dart'; @@ -20,19 +20,19 @@ class Mute { factory Mute.fromJson(Map json) => _$MuteFromJson(json); /// The user that performed the muting action - @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final User user; /// The target user - @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final ChannelModel channel; /// The date in which the use was muted - @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final DateTime createdAt; /// The date of the last update - @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final DateTime updatedAt; /// Serialize to json diff --git a/packages/stream_chat/lib/src/core/models/own_user.dart b/packages/stream_chat/lib/src/core/models/own_user.dart index b9cdf0fd..70529687 100644 --- a/packages/stream_chat/lib/src/core/models/own_user.dart +++ b/packages/stream_chat/lib/src/core/models/own_user.dart @@ -1,7 +1,7 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/core/models/device.dart'; import 'package:stream_chat/src/core/models/mute.dart'; -import 'package:stream_chat/src/core/util/serialization.dart'; +import 'package:stream_chat/src/core/util/serializer.dart'; import 'package:stream_chat/src/core/models/user.dart'; part 'own_user.g.dart'; @@ -38,7 +38,7 @@ class OwnUser extends User { /// Create a new instance from a json factory OwnUser.fromJson(Map json) => _$OwnUserFromJson( - Serialization.moveToExtraDataFromRoot(json, topLevelFields)); + Serializer.moveToExtraDataFromRoot(json, topLevelFields)); /// Create a new instance from [User] object factory OwnUser.fromUser(User user) => OwnUser( @@ -55,35 +55,35 @@ class OwnUser extends User { /// List of user devices @JsonKey( includeIfNull: false, - toJson: Serialization.readOnly, + toJson: Serializer.readOnly, defaultValue: []) final List devices; /// List of users muted by the user @JsonKey( includeIfNull: false, - toJson: Serialization.readOnly, + toJson: Serializer.readOnly, defaultValue: []) final List mutes; /// List of users muted by the user @JsonKey( includeIfNull: false, - toJson: Serialization.readOnly, + toJson: Serializer.readOnly, defaultValue: []) final List channelMutes; /// Total unread messages by the user @JsonKey( - includeIfNull: false, toJson: Serialization.readOnly, defaultValue: 0) + includeIfNull: false, toJson: Serializer.readOnly, defaultValue: 0) final int totalUnreadCount; /// Total unread channels by the user - @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final int? unreadChannels; /// Known top level fields. - /// Useful for [Serialization] methods. + /// Useful for [Serializer] methods. static final topLevelFields = [ 'devices', 'mutes', @@ -95,7 +95,7 @@ class OwnUser extends User { /// Serialize to json @override - Map toJson() => Serialization.moveFromExtraDataToRoot( + Map toJson() => Serializer.moveFromExtraDataToRoot( _$OwnUserToJson(this), ); } diff --git a/packages/stream_chat/lib/src/core/models/reaction.dart b/packages/stream_chat/lib/src/core/models/reaction.dart index 997d833c..57d0b48d 100644 --- a/packages/stream_chat/lib/src/core/models/reaction.dart +++ b/packages/stream_chat/lib/src/core/models/reaction.dart @@ -1,5 +1,5 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:stream_chat/src/core/util/serialization.dart'; +import 'package:stream_chat/src/core/util/serializer.dart'; import 'package:stream_chat/src/core/models/user.dart'; part 'reaction.g.dart'; @@ -21,7 +21,7 @@ class Reaction { /// Create a new instance from a json factory Reaction.fromJson(Map json) => - _$ReactionFromJson(Serialization.moveToExtraDataFromRoot( + _$ReactionFromJson(Serializer.moveToExtraDataFromRoot( json, topLevelFields, )); @@ -33,11 +33,11 @@ class Reaction { final String type; /// The date of the reaction - @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final DateTime createdAt; /// The user that sent the reaction - @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final User? user; /// The score of the reaction (ie. number of reactions sent) @@ -45,7 +45,7 @@ class Reaction { final int score; /// The userId that sent the reaction - @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final String? userId; /// Reaction custom extraData @@ -66,7 +66,7 @@ class Reaction { ]; /// Serialize to json - Map toJson() => Serialization.moveFromExtraDataToRoot( + Map toJson() => Serializer.moveFromExtraDataToRoot( _$ReactionToJson(this), ); diff --git a/packages/stream_chat/lib/src/core/models/user.dart b/packages/stream_chat/lib/src/core/models/user.dart index b5fdec69..b1840ba2 100644 --- a/packages/stream_chat/lib/src/core/models/user.dart +++ b/packages/stream_chat/lib/src/core/models/user.dart @@ -1,5 +1,5 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:stream_chat/src/core/util/serialization.dart'; +import 'package:stream_chat/src/core/util/serializer.dart'; part 'user.g.dart'; @@ -22,10 +22,10 @@ class User { /// Create a new instance from a json factory User.fromJson(Map json) => _$UserFromJson( - Serialization.moveToExtraDataFromRoot(json, topLevelFields)); + Serializer.moveToExtraDataFromRoot(json, topLevelFields)); /// Known top level fields. - /// Useful for [Serialization] methods. + /// Useful for [Serializer] methods. static const topLevelFields = [ 'id', 'role', @@ -41,36 +41,36 @@ class User { final String id; /// User role - @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final String? role; /// User role @JsonKey( includeIfNull: false, - toJson: Serialization.readOnly, + toJson: Serializer.readOnly, defaultValue: []) final List teams; /// Date of user creation - @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final DateTime createdAt; /// Date of last user update - @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final DateTime updatedAt; /// Date of last user connection - @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final DateTime? lastActive; /// True if user is online @JsonKey( - includeIfNull: false, toJson: Serialization.readOnly, defaultValue: false) + includeIfNull: false, toJson: Serializer.readOnly, defaultValue: false) final bool online; /// True if user is banned from the chat @JsonKey( - includeIfNull: false, toJson: Serialization.readOnly, defaultValue: false) + includeIfNull: false, toJson: Serializer.readOnly, defaultValue: false) final bool banned; /// Map of custom user extraData @@ -102,7 +102,7 @@ class User { other is User && runtimeType == other.runtimeType && id == other.id; /// Serialize to json - Map toJson() => Serialization.moveFromExtraDataToRoot( + Map toJson() => Serializer.moveFromExtraDataToRoot( _$UserToJson(this), ); diff --git a/packages/stream_chat/lib/src/core/util/extension.dart b/packages/stream_chat/lib/src/core/util/extension.dart index 5dd8ea15..a28dc12b 100644 --- a/packages/stream_chat/lib/src/core/util/extension.dart +++ b/packages/stream_chat/lib/src/core/util/extension.dart @@ -9,10 +9,13 @@ extension IterableX on Iterable { } /// Useful extension functions for [Map] -extension MapX on Map { +extension MapX on Map { /// Returns a new map with null keys or values removed - Map get nullProtected => - Map.from(this)..removeWhere((key, value) => key == null || value == null); + Map get nullProtected { + final nullProtected = {...this} + ..removeWhere((key, value) => key == null || value == null); + return nullProtected.cast(); + } } /// Useful extension functions for [String] diff --git a/packages/stream_chat/lib/src/core/util/serialization.dart b/packages/stream_chat/lib/src/core/util/serializer.dart similarity index 98% rename from packages/stream_chat/lib/src/core/util/serialization.dart rename to packages/stream_chat/lib/src/core/util/serializer.dart index 1e933a32..2bf92e39 100644 --- a/packages/stream_chat/lib/src/core/util/serialization.dart +++ b/packages/stream_chat/lib/src/core/util/serializer.dart @@ -3,7 +3,7 @@ Null readonly(_) => null; /// Helper class for serialization to and from json -class Serialization { +class Serializer { /// Used to avoid to serialize properties to json static const Function readOnly = readonly; diff --git a/packages/stream_chat/test/src/core/util/extension_test.dart b/packages/stream_chat/test/src/core/util/extension_test.dart new file mode 100644 index 00000000..98f9f784 --- /dev/null +++ b/packages/stream_chat/test/src/core/util/extension_test.dart @@ -0,0 +1,48 @@ +import 'package:test/test.dart'; +import 'package:stream_chat/src/core/util/extension.dart'; + +void main() { + test('`.withNullifyer` converts the type into non-nullable', () { + final items = ['A', 'B', null, 'D']; + expect(items, isA>()); + expect(items.length, 4); + + final nullifiedItems = items.withNullifyer; + expect(nullifiedItems, isA>()); + expect(nullifiedItems.length, 3); + }); + + test('`.nullProtected should remove all the null keys, value`', () { + final map = {'name': 'sahil', 'age': null, null: 'India'}; + expect(map, isA>()); + expect(map.length, 3); + + final nullProtectedMap = map.nullProtected; + expect(nullProtectedMap, isA>()); + expect(nullProtectedMap.length, 1); + }); + + group('mimeType', () { + test('should return null if `String` is not a filename', () { + const fileName = 'not-a-file-name'; + final mimeType = fileName.mimeType; + expect(mimeType, isNull); + }); + + test('should return mimeType if string is a filename', () { + const fileName = 'dummyFileName.jpeg'; + final mimeType = fileName.mimeType; + expect(mimeType, isNotNull); + expect(mimeType!.type, 'image'); + expect(mimeType.subtype, 'jpeg'); + }); + + test('should return `image/heic` if ends with `heic`', () { + const fileName = 'dummyFileName.heic'; + final mimeType = fileName.mimeType; + expect(mimeType, isNotNull); + expect(mimeType!.type, 'image'); + expect(mimeType.subtype, 'heic'); + }); + }); +} diff --git a/packages/stream_chat/test/src/core/util/serializer_test.dart b/packages/stream_chat/test/src/core/util/serializer_test.dart new file mode 100644 index 00000000..fbbb96a2 --- /dev/null +++ b/packages/stream_chat/test/src/core/util/serializer_test.dart @@ -0,0 +1,43 @@ +import 'package:stream_chat/src/core/util/serializer.dart'; +import 'package:test/test.dart'; + +void main() { + group('Serializer', () { + test('moveKeysToMapInPlace', () { + final serializer = Serializer.moveToExtraDataFromRoot( + { + 'test': 'test', + 'name': 'Sahil', + 'age': 22, + 'country': 'India', + }, + ['test'], + ); + expect(serializer, { + 'test': 'test', + 'extra_data': { + 'name': 'Sahil', + 'age': 22, + 'country': 'India', + } + }); + }); + + test('moveKeysToMapInPlace', () { + final serializer = Serializer.moveFromExtraDataToRoot({ + 'test': 'test', + 'extra_data': { + 'name': 'Sahil', + 'age': 22, + 'country': 'India', + } + }); + expect(serializer, { + 'test': 'test', + 'name': 'Sahil', + 'age': 22, + 'country': 'India', + }); + }); + }); +} diff --git a/packages/stream_chat/test/src/core/util/utils_test.dart b/packages/stream_chat/test/src/core/util/utils_test.dart new file mode 100644 index 00000000..5d4c272f --- /dev/null +++ b/packages/stream_chat/test/src/core/util/utils_test.dart @@ -0,0 +1,15 @@ +import 'package:stream_chat/src/core/util/utils.dart'; +import 'package:test/test.dart'; + +void main() { + test('should generate a `randomId` of length 33', () { + final id = randomId(size: 33); + expect(id.length, 33); + }); + + test('should `generateHash` for the passed objects', () { + final objects = ['Sahil', 23, 'Flutter Engineer', 'India']; + final hash = generateHash(objects); + expect(hash, 'WyJTYWhpbCIsMjMsIkZsdXR0ZXIgRW5naW5lZXIiLCJJbmRpYSJd'); + }); +} From 473851684bd033d6dc7ae8f077b877c30869b15e Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 3 Jun 2021 22:29:52 +0530 Subject: [PATCH 014/150] add platform_detector_test.dart Signed-off-by: Sahil Kumar --- .../platform_detector_test.dart | 25 +++++++++++++++++++ .../test/src/ws/websocket_test.dart | 5 ++-- 2 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 packages/stream_chat/test/src/core/platform_detector/platform_detector_test.dart diff --git a/packages/stream_chat/test/src/core/platform_detector/platform_detector_test.dart b/packages/stream_chat/test/src/core/platform_detector/platform_detector_test.dart new file mode 100644 index 00000000..110fb64e --- /dev/null +++ b/packages/stream_chat/test/src/core/platform_detector/platform_detector_test.dart @@ -0,0 +1,25 @@ +@TestOn('linux') +import 'package:stream_chat/src/core/platform_detector/platform_detector.dart'; +import 'package:test/test.dart'; + +void main() { + test('`.type` should return current platform', () { + final type = CurrentPlatform.type; + expect(type, PlatformType.linux); + }); + + test('`.name` should return current platform name', () { + final name = CurrentPlatform.name; + expect(name, 'linux'); + }); + + test('flags', () { + expect(CurrentPlatform.isWeb, isFalse); + expect(CurrentPlatform.isIos, isFalse); + expect(CurrentPlatform.isLinux, isTrue); + expect(CurrentPlatform.isAndroid, isFalse); + expect(CurrentPlatform.isMacOS, isFalse); + expect(CurrentPlatform.isWindows, isFalse); + expect(CurrentPlatform.isFuchsia, isFalse); + }); +} diff --git a/packages/stream_chat/test/src/ws/websocket_test.dart b/packages/stream_chat/test/src/ws/websocket_test.dart index 73eecf3f..94033bf0 100644 --- a/packages/stream_chat/test/src/ws/websocket_test.dart +++ b/packages/stream_chat/test/src/ws/websocket_test.dart @@ -45,11 +45,10 @@ void main() { webSocketController.add(invocation.positionalArguments.first); }); when(() => webSocketSink.close(any(), any())).thenAnswer( - (_) { - final res = webSocketController.close(); + (_) async { + webSocketController.close(); // re-initializing for future events webSocketController = StreamController.broadcast(); - return res; }, ); }); From 214d1bcf1dae9702d16a4716ab7803d8217a89ca Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 4 Jun 2021 16:24:52 +0530 Subject: [PATCH 015/150] add core/http tests Signed-off-by: Sahil Kumar --- .../http/interceptor/auth_interceptor.dart | 3 +- .../lib/src/core/http/stream_http_client.dart | 4 +- .../core/http/connection_id_manager_test.dart | 38 ++ .../interceptor/auth_interceptor_test.dart | 270 +++++++++ .../connection_id_interceptor.dart | 67 +++ .../core/http/stream_chat_dio_error_test.dart | 20 + .../http/stream_http_client_options_test.dart | 53 ++ .../core/http/stream_http_client_test.dart | 528 ++++++++++++++++++ .../src/core/http/token_manager_test.dart | 141 +++++ .../test/src/core/http/token_test.dart | 57 ++ 10 files changed, 1177 insertions(+), 4 deletions(-) create mode 100644 packages/stream_chat/test/src/core/http/connection_id_manager_test.dart create mode 100644 packages/stream_chat/test/src/core/http/interceptor/auth_interceptor_test.dart create mode 100644 packages/stream_chat/test/src/core/http/interceptor/connection_id_interceptor.dart create mode 100644 packages/stream_chat/test/src/core/http/stream_chat_dio_error_test.dart create mode 100644 packages/stream_chat/test/src/core/http/stream_http_client_options_test.dart create mode 100644 packages/stream_chat/test/src/core/http/stream_http_client_test.dart create mode 100644 packages/stream_chat/test/src/core/http/token_manager_test.dart create mode 100644 packages/stream_chat/test/src/core/http/token_test.dart diff --git a/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart b/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart index d689cce1..3097c2b1 100644 --- a/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart +++ b/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart @@ -7,7 +7,6 @@ import 'package:stream_chat/src/core/http/token.dart'; import 'package:stream_chat/src/core/http/token_manager.dart'; import 'package:stream_chat/src/core/error/error.dart'; - /// class AuthInterceptor extends Interceptor { /// @@ -84,7 +83,7 @@ class AuthInterceptor extends Interceptor { ); return handler.resolve(response); } on DioError catch (error) { - return handler.reject(error); + return handler.next(error); } } return handler.next(err); diff --git a/packages/stream_chat/lib/src/core/http/stream_http_client.dart b/packages/stream_chat/lib/src/core/http/stream_http_client.dart index 3eacd11e..4cf5b0e0 100644 --- a/packages/stream_chat/lib/src/core/http/stream_http_client.dart +++ b/packages/stream_chat/lib/src/core/http/stream_http_client.dart @@ -86,8 +86,8 @@ class StreamHttpClient { /// [StreamHttpClient] instance dequeue the request task。 void unlock() => httpClient.unlock(); - ///Clear the current [StreamHttpClient] instance waiting queue. - void clear() => httpClient.close(); + /// Clear the current [StreamHttpClient] instance waiting queue. + void clear() => httpClient.clear(); /// Shuts down the [StreamHttpClient]. /// diff --git a/packages/stream_chat/test/src/core/http/connection_id_manager_test.dart b/packages/stream_chat/test/src/core/http/connection_id_manager_test.dart new file mode 100644 index 00000000..ad70283f --- /dev/null +++ b/packages/stream_chat/test/src/core/http/connection_id_manager_test.dart @@ -0,0 +1,38 @@ +import 'package:stream_chat/src/core/http/connection_id_manager.dart'; +import 'package:test/test.dart'; + +void main() { + late ConnectionIdManager connectionIdManager; + + setUp(() { + connectionIdManager = ConnectionIdManager(); + }); + + tearDown(() { + connectionIdManager.reset(); + }); + + test('`setConnectionId` should set connectionId', () { + expect(connectionIdManager.connectionId, isNull); + expect(connectionIdManager.hasConnectionId, isFalse); + + const connectionId = 'test-connection-id'; + connectionIdManager.setConnectionId(connectionId); + + expect(connectionIdManager.connectionId, connectionId); + expect(connectionIdManager.hasConnectionId, isTrue); + }); + + test('`reset` should clear the connectionId', () { + const connectionId = 'test-connection-id'; + connectionIdManager.setConnectionId(connectionId); + + expect(connectionIdManager.connectionId, connectionId); + expect(connectionIdManager.hasConnectionId, isTrue); + + connectionIdManager.reset(); + + expect(connectionIdManager.connectionId, isNull); + expect(connectionIdManager.hasConnectionId, isFalse); + }); +} diff --git a/packages/stream_chat/test/src/core/http/interceptor/auth_interceptor_test.dart b/packages/stream_chat/test/src/core/http/interceptor/auth_interceptor_test.dart new file mode 100644 index 00000000..82dcdfb9 --- /dev/null +++ b/packages/stream_chat/test/src/core/http/interceptor/auth_interceptor_test.dart @@ -0,0 +1,270 @@ +import 'package:dio/dio.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/core/http/interceptor/auth_interceptor.dart'; +import 'package:stream_chat/src/core/http/stream_chat_dio_error.dart'; +import 'package:stream_chat/src/core/http/stream_http_client.dart'; +import 'package:stream_chat/src/core/http/token.dart'; +import 'package:stream_chat/src/core/http/token_manager.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:test/test.dart'; + +import '../../../mocks.dart'; + +void main() { + late StreamHttpClient client; + late TokenManager tokenManager; + late AuthInterceptor authInterceptor; + + setUp(() { + client = MockHttpClient(); + tokenManager = MockTokenManager(); + authInterceptor = AuthInterceptor(client, tokenManager); + }); + + test( + '`onRequest` should add userId, authToken, authType in the request', + () async { + final options = RequestOptions(path: 'test-path'); + final handler = RequestInterceptorHandler(); + + final headers = options.headers; + final queryParams = options.queryParameters; + expect(headers.containsKey('Authorization'), isFalse); + expect(headers.containsKey('stream-auth-type'), isFalse); + expect(queryParams.containsKey('user_id'), isFalse); + + final token = Token.development('test-user-id'); + when(() => tokenManager.loadToken(refresh: any(named: 'refresh'))) + .thenAnswer((_) async => token); + + authInterceptor.onRequest(options, handler); + + final updatedOptions = (await handler.future).data as RequestOptions; + final updateHeaders = updatedOptions.headers; + final updatedQueryParams = updatedOptions.queryParameters; + + expect(updateHeaders.containsKey('Authorization'), isTrue); + expect(updateHeaders['Authorization'], token.rawValue); + expect(updateHeaders.containsKey('stream-auth-type'), isTrue); + expect(updateHeaders['stream-auth-type'], token.authType.raw); + expect(updatedQueryParams.containsKey('user_id'), isTrue); + expect(updatedQueryParams['user_id'], token.userId); + + verify(() => tokenManager.loadToken(refresh: any(named: 'refresh'))) + .called(1); + verifyNoMoreInteractions(tokenManager); + }, + ); + + test( + '`onRequest` should reject with error if `tokenManager.loadToken` throws', + () async { + final options = RequestOptions(path: 'test-path'); + final handler = RequestInterceptorHandler(); + + authInterceptor.onRequest(options, handler); + + try { + await handler.future; + } catch (e) { + // need to cast it as the type is private in dio + var error = (e as dynamic).data; + expect(error, isA()); + error = (error as StreamChatDioError).error; + expect(error.code, ChatErrorCode.undefinedToken.code); + expect(error.message, ChatErrorCode.undefinedToken.message); + } + }, + ); + + test('`onError` should retry the request with refreshed token', () async { + const path = 'test-request-path'; + final options = RequestOptions(path: path); + const code = ChatErrorCode.tokenExpired; + final errorResponse = ErrorResponse() + ..code = code.code + ..message = code.message; + final response = Response( + requestOptions: options, + data: errorResponse.toJson(), + ); + final err = DioError(requestOptions: options, response: response); + final handler = ErrorInterceptorHandler(); + + when(() => tokenManager.isStatic).thenReturn(false); + + when(() => client.lock()).thenReturn(() {}); + + final token = Token.development('test-user-id'); + when(() => tokenManager.loadToken(refresh: true)) + .thenAnswer((_) async => token); + + when(() => client.unlock()).thenReturn(() {}); + + when(() => client.request( + path, + data: options.data, + onReceiveProgress: options.onReceiveProgress, + onSendProgress: options.onSendProgress, + queryParameters: options.queryParameters, + cancelToken: options.cancelToken, + options: any(named: 'options'), + )).thenAnswer((_) async => Response( + requestOptions: options, + statusCode: 200, + )); + + authInterceptor.onError(err, handler); + + final res = await handler.future; + + var data = res.data; + expect(data, isA()); + data = data as Response; + expect(data, isNotNull); + expect(data.statusCode, 200); + expect(data.requestOptions.path, path); + + verify(() => tokenManager.isStatic).called(1); + + verify(() => client.lock()).called(1); + + verify(() => tokenManager.loadToken(refresh: true)).called(1); + verifyNoMoreInteractions(tokenManager); + + verify(() => client.unlock()).called(1); + verify(() => client.request( + path, + data: options.data, + onReceiveProgress: options.onReceiveProgress, + onSendProgress: options.onSendProgress, + queryParameters: options.queryParameters, + cancelToken: options.cancelToken, + options: any(named: 'options'), + )).called(1); + verifyNoMoreInteractions(client); + }); + + test( + '`onError` should reject with error if retried request throws', + () async { + const path = 'test-request-path'; + final options = RequestOptions(path: path); + const code = ChatErrorCode.tokenExpired; + final errorResponse = ErrorResponse() + ..code = code.code + ..message = code.message; + final response = Response( + requestOptions: options, + data: errorResponse.toJson(), + ); + final err = DioError(requestOptions: options, response: response); + final handler = ErrorInterceptorHandler(); + + when(() => tokenManager.isStatic).thenReturn(false); + + when(() => client.lock()).thenReturn(() {}); + + final token = Token.development('test-user-id'); + when(() => tokenManager.loadToken(refresh: true)) + .thenAnswer((_) async => token); + + when(() => client.unlock()).thenReturn(() {}); + + when(() => client.request( + path, + data: options.data, + onReceiveProgress: options.onReceiveProgress, + onSendProgress: options.onSendProgress, + queryParameters: options.queryParameters, + cancelToken: options.cancelToken, + options: any(named: 'options'), + )).thenThrow(err); + + authInterceptor.onError(err, handler); + + try { + await handler.future; + } catch (e) { + // need to cast it as the type is private in dio + final error = (e as dynamic).data; + expect(error, isA()); + } + + verify(() => tokenManager.isStatic).called(1); + + verify(() => client.lock()).called(1); + + verify(() => tokenManager.loadToken(refresh: true)).called(1); + verifyNoMoreInteractions(tokenManager); + + verify(() => client.unlock()).called(1); + verify(() => client.request( + path, + data: options.data, + onReceiveProgress: options.onReceiveProgress, + onSendProgress: options.onSendProgress, + queryParameters: options.queryParameters, + cancelToken: options.cancelToken, + options: any(named: 'options'), + )).called(1); + verifyNoMoreInteractions(client); + }, + ); + + test( + '`onError` should reject with error if `tokenManager.isStatic` is true', + () async { + const path = 'test-request-path'; + final options = RequestOptions(path: path); + const code = ChatErrorCode.tokenExpired; + final errorResponse = ErrorResponse() + ..code = code.code + ..message = code.message; + final response = Response( + requestOptions: options, + data: errorResponse.toJson(), + ); + final err = DioError(requestOptions: options, response: response); + final handler = ErrorInterceptorHandler(); + + when(() => tokenManager.isStatic).thenReturn(true); + + authInterceptor.onError(err, handler); + + try { + await handler.future; + } catch (e) { + // need to cast it as the type is private in dio + final error = (e as dynamic).data; + expect(error, isA()); + final response = StreamChatNetworkError.fromDioError(error); + expect(response.errorCode, code); + } + + verify(() => tokenManager.isStatic).called(1); + verifyNoMoreInteractions(tokenManager); + }, + ); + + test( + '`onError` should reject with error if error is not a `tokenExpired error`', + () async { + const path = 'test-request-path'; + final options = RequestOptions(path: path); + final response = Response(requestOptions: options); + final err = DioError(requestOptions: options, response: response); + final handler = ErrorInterceptorHandler(); + + authInterceptor.onError(err, handler); + + try { + await handler.future; + } catch (e) { + // need to cast it as the type is private in dio + final error = (e as dynamic).data; + expect(error, isA()); + } + }, + ); +} diff --git a/packages/stream_chat/test/src/core/http/interceptor/connection_id_interceptor.dart b/packages/stream_chat/test/src/core/http/interceptor/connection_id_interceptor.dart new file mode 100644 index 00000000..9acabccf --- /dev/null +++ b/packages/stream_chat/test/src/core/http/interceptor/connection_id_interceptor.dart @@ -0,0 +1,67 @@ +import 'package:dio/dio.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/core/http/connection_id_manager.dart'; +import 'package:stream_chat/src/core/http/interceptor/connection_id_interceptor.dart'; +import 'package:test/test.dart'; + +import '../../../mocks.dart'; + +void main() { + late ConnectionIdManager connectionIdManager; + late ConnectionIdInterceptor connectionIdInterceptor; + + setUp(() { + connectionIdManager = MockConnectionIdManager(); + connectionIdInterceptor = ConnectionIdInterceptor(connectionIdManager); + }); + + test( + '`onRequest` should add connectionId in the request', + () async { + final options = RequestOptions(path: 'test-path'); + final handler = RequestInterceptorHandler(); + + final queryParams = options.queryParameters; + expect(queryParams.containsKey('connection_id'), isFalse); + + const connectionId = 'test-connection-id'; + when(() => connectionIdManager.hasConnectionId).thenReturn(true); + when(() => connectionIdManager.connectionId).thenReturn(connectionId); + + connectionIdInterceptor.onRequest(options, handler); + + final updatedOptions = (await handler.future).data as RequestOptions; + final updatedQueryParams = updatedOptions.queryParameters; + + expect(updatedQueryParams.containsKey('connection_id'), isTrue); + expect(updatedQueryParams['connection_id'], connectionId); + + verify(() => connectionIdManager.hasConnectionId).called(1); + verify(() => connectionIdManager.connectionId).called(1); + verifyNoMoreInteractions(connectionIdManager); + }, + ); + + test( + '`onRequest` should not add connectionId if `hasConnectionId` is false', + () async { + final options = RequestOptions(path: 'test-path'); + final handler = RequestInterceptorHandler(); + + final queryParams = options.queryParameters; + expect(queryParams.containsKey('connection_id'), isFalse); + + when(() => connectionIdManager.hasConnectionId).thenReturn(false); + + connectionIdInterceptor.onRequest(options, handler); + + final updatedOptions = (await handler.future).data as RequestOptions; + final updatedQueryParams = updatedOptions.queryParameters; + + expect(updatedQueryParams.containsKey('connection_id'), isFalse); + + verify(() => connectionIdManager.hasConnectionId).called(1); + verifyNoMoreInteractions(connectionIdManager); + }, + ); +} diff --git a/packages/stream_chat/test/src/core/http/stream_chat_dio_error_test.dart b/packages/stream_chat/test/src/core/http/stream_chat_dio_error_test.dart new file mode 100644 index 00000000..f13cf50d --- /dev/null +++ b/packages/stream_chat/test/src/core/http/stream_chat_dio_error_test.dart @@ -0,0 +1,20 @@ +import 'package:dio/dio.dart'; +import 'package:stream_chat/src/core/error/error.dart'; +import 'package:stream_chat/src/core/http/stream_chat_dio_error.dart'; +import 'package:test/test.dart'; + +void main() { + test('should create a new instance of StreamChatDioError', () { + final error = StreamChatNetworkError(ChatErrorCode.inputError); + final options = RequestOptions(path: 'test-path'); + final dioError = StreamChatDioError( + error: error, + requestOptions: options, + ); + + expect(dioError, isA()); + expect(dioError, isNotNull); + expect(dioError.error, error); + expect(dioError.requestOptions, options); + }); +} diff --git a/packages/stream_chat/test/src/core/http/stream_http_client_options_test.dart b/packages/stream_chat/test/src/core/http/stream_http_client_options_test.dart new file mode 100644 index 00000000..a03434fa --- /dev/null +++ b/packages/stream_chat/test/src/core/http/stream_http_client_options_test.dart @@ -0,0 +1,53 @@ +import 'package:stream_chat/src/core/http/stream_http_client.dart'; +import 'package:stream_chat/src/location.dart'; +import 'package:test/test.dart'; + +void main() { + test('should return the all default set params', () { + const options = StreamHttpClientOptions(); + expect(options.location, isNull); + expect(options.baseUrl, 'https://chat-us-east-1.stream-io-api.com'); + expect(options.connectTimeout, const Duration(seconds: 6)); + expect(options.receiveTimeout, const Duration(seconds: 6)); + }); + + test('should override all the default set params', () { + const options = StreamHttpClientOptions( + baseUrl: 'base-url', + connectTimeout: Duration(seconds: 3), + receiveTimeout: Duration(seconds: 3), + ); + expect(options.location, isNull); + expect(options.baseUrl, 'base-url'); + expect(options.connectTimeout, const Duration(seconds: 3)); + expect(options.receiveTimeout, const Duration(seconds: 3)); + }); + + group('should create baseUrl according to provided location', () { + test('us-east', () { + const options = StreamHttpClientOptions(location: Location.usEast); + expect(options.location, isNotNull); + expect(options.baseUrl, 'https://chat-proxy-us-east.stream-io-api.com'); + }); + test('eu-west', () { + const options = StreamHttpClientOptions(location: Location.euWest); + expect(options.location, isNotNull); + expect(options.baseUrl, 'https://chat-proxy-dublin.stream-io-api.com'); + }); + test('mumbai', () { + const options = StreamHttpClientOptions(location: Location.mumbai); + expect(options.location, isNotNull); + expect(options.baseUrl, 'https://chat-proxy-mumbai.stream-io-api.com'); + }); + test('sydney', () { + const options = StreamHttpClientOptions(location: Location.sydney); + expect(options.location, isNotNull); + expect(options.baseUrl, 'https://chat-proxy-sydney.stream-io-api.com'); + }); + test('singapore', () { + const options = StreamHttpClientOptions(location: Location.singapore); + expect(options.location, isNotNull); + expect(options.baseUrl, 'https://chat-proxy-singapore.stream-io-api.com'); + }); + }); +} diff --git a/packages/stream_chat/test/src/core/http/stream_http_client_test.dart b/packages/stream_chat/test/src/core/http/stream_http_client_test.dart new file mode 100644 index 00000000..ece00efd --- /dev/null +++ b/packages/stream_chat/test/src/core/http/stream_http_client_test.dart @@ -0,0 +1,528 @@ +import 'package:dio/dio.dart'; +import 'package:logging/logging.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/core/api/responses.dart'; +import 'package:stream_chat/src/core/error/error.dart'; +import 'package:stream_chat/src/core/http/connection_id_manager.dart'; +import 'package:stream_chat/src/core/http/interceptor/auth_interceptor.dart'; +import 'package:stream_chat/src/core/http/interceptor/connection_id_interceptor.dart'; +import 'package:stream_chat/src/core/http/interceptor/logging_interceptor.dart'; +import 'package:stream_chat/src/core/http/stream_chat_dio_error.dart'; +import 'package:stream_chat/src/core/http/stream_http_client.dart'; +import 'package:stream_chat/src/core/http/token_manager.dart'; +import 'package:test/test.dart'; + +import '../../mocks.dart'; + +void main() { + Response successResponse(String path) => Response( + requestOptions: RequestOptions(path: path), + statusCode: 200, + ); + + DioError throwableError( + String path, { + StreamChatNetworkError? error, + bool streamChatDioError = false, + }) { + if (streamChatDioError) assert(error != null, ''); + final options = RequestOptions(path: path); + final data = ErrorResponse() + ..code = error?.code + ..statusCode = error?.statusCode + ..message = error?.message; + DioError? dioError; + if (streamChatDioError) { + dioError = StreamChatDioError(error: error!, requestOptions: options); + } else { + dioError = DioError( + error: error, + requestOptions: options, + response: Response( + requestOptions: options, + statusCode: data.statusCode, + data: data.toJson(), + ), + ); + } + return dioError; + } + + test('AuthInterceptor should be added if tokenManager is provided', () { + const apiKey = 'api-key'; + final client = StreamHttpClient(apiKey, tokenManager: TokenManager()); + + expect(client.httpClient.interceptors.length, 1); + expect(client.httpClient.interceptors.first, isA()); + }); + + test( + 'connectionIdInterceptor should be added if connectionIdManager is provided', + () { + const apiKey = 'api-key'; + final client = StreamHttpClient( + apiKey, + connectionIdManager: ConnectionIdManager(), + ); + + expect(client.httpClient.interceptors.length, 1); + expect( + client.httpClient.interceptors.first, + isA(), + ); + }, + ); + + test('loggingInterceptor should be added if logger is provided', () { + const apiKey = 'api-key'; + final client = StreamHttpClient( + apiKey, + logger: Logger('test-logger'), + ); + + expect(client.httpClient.interceptors.length, 1); + expect( + client.httpClient.interceptors.first, + isA(), + ); + }); + + test('loggingInterceptor should log requests', () async { + const apiKey = 'api-key'; + final logger = MockLogger(); + final client = StreamHttpClient(apiKey, logger: logger); + + try { + await client.get('path'); + } catch (_) {} + + verify(() => logger.info(any())).called(16); + }); + + test('loggingInterceptor should log error', () async { + const apiKey = 'api-key'; + final logger = MockLogger(); + final client = StreamHttpClient(apiKey, logger: logger); + + try { + await client.get('path'); + } catch (_) {} + + verify(() => logger.severe(any())).called(8); + }); + + test('`.lock` should lock the dio client', () async { + final client = StreamHttpClient('api-key'); + expect(client.httpClient.interceptors.requestLock.locked, isFalse); + client.lock(); + expect(client.httpClient.interceptors.requestLock.locked, isTrue); + }); + + test('`.unlock` should unlock the dio client', () async { + final client = StreamHttpClient('api-key'); + expect(client.httpClient.interceptors.requestLock.locked, isFalse); + client.lock(); + expect(client.httpClient.interceptors.requestLock.locked, isTrue); + client.unlock(); + expect(client.httpClient.interceptors.requestLock.locked, isFalse); + }); + + test('`.clear` should clear and unlock the dio client', () async { + final client = StreamHttpClient('api-key')..clear(); + expect(client.httpClient.interceptors.requestLock.locked, isFalse); + }); + + test('`.close` should close the dio client', () async { + final client = StreamHttpClient('api-key')..close(force: true); + try { + await client.get('path'); + } on StreamChatNetworkError catch (e) { + expect(e, isA()); + expect(e.message, "Dio can't establish new connection after closed."); + } + }); + + test('`.get` should return response successfully', () async { + final dio = MockDio(); + final client = StreamHttpClient('api-key', dio: dio); + + const path = 'test-get-api-path'; + when(() => dio.get( + path, + options: any(named: 'options'), + )).thenAnswer((_) async => successResponse(path)); + + final res = await client.get(path); + + expect(res, isNotNull); + expect(res.statusCode, 200); + expect(res.requestOptions.path, path); + + verify(() => dio.get( + path, + options: any(named: 'options'), + )).called(1); + verifyNoMoreInteractions(dio); + }); + + test('`.get` should throw an instance of `StreamChatNetworkError`', () async { + final dio = MockDio(); + final client = StreamHttpClient('api-key', dio: dio); + + const path = 'test-get-api-path'; + final error = throwableError( + path, + error: StreamChatNetworkError(ChatErrorCode.internalSystemError), + ); + when(() => dio.get( + path, + options: any(named: 'options'), + )).thenThrow(error); + + try { + await client.get(path); + } catch (e) { + expect(e, isA()); + expect(e, StreamChatNetworkError.fromDioError(error)); + } + + verify(() => dio.get( + path, + options: any(named: 'options'), + )).called(1); + verifyNoMoreInteractions(dio); + }); + + test('`.post` should return response successfully', () async { + final dio = MockDio(); + final client = StreamHttpClient('api-key', dio: dio); + + const path = 'test-post-api-path'; + when(() => dio.post( + path, + options: any(named: 'options'), + )).thenAnswer((_) async => successResponse(path)); + + final res = await client.post(path); + + expect(res, isNotNull); + expect(res.statusCode, 200); + expect(res.requestOptions.path, path); + + verify(() => dio.post( + path, + options: any(named: 'options'), + )).called(1); + verifyNoMoreInteractions(dio); + }); + + test( + '`.post` should throw an instance of `StreamChatNetworkError`', + () async { + final dio = MockDio(); + final client = StreamHttpClient('api-key', dio: dio); + + const path = 'test-post-api-path'; + final error = throwableError( + path, + error: StreamChatNetworkError(ChatErrorCode.internalSystemError), + ); + when(() => dio.post( + path, + options: any(named: 'options'), + )).thenThrow(error); + + try { + await client.post(path); + } catch (e) { + expect(e, isA()); + expect(e, StreamChatNetworkError.fromDioError(error)); + } + + verify(() => dio.post( + path, + options: any(named: 'options'), + )).called(1); + verifyNoMoreInteractions(dio); + }, + ); + + test('`.delete` should return response successfully', () async { + final dio = MockDio(); + final client = StreamHttpClient('api-key', dio: dio); + + const path = 'test-delete-api-path'; + when(() => dio.delete( + path, + options: any(named: 'options'), + )).thenAnswer((_) async => successResponse(path)); + + final res = await client.delete(path); + + expect(res, isNotNull); + expect(res.statusCode, 200); + expect(res.requestOptions.path, path); + + verify(() => dio.delete( + path, + options: any(named: 'options'), + )).called(1); + verifyNoMoreInteractions(dio); + }); + + test( + '`.delete` should throw an instance of `StreamChatNetworkError`', + () async { + final dio = MockDio(); + final client = StreamHttpClient('api-key', dio: dio); + + const path = 'test-delete-api-path'; + final error = throwableError( + path, + error: StreamChatNetworkError(ChatErrorCode.internalSystemError), + ); + when(() => dio.delete( + path, + options: any(named: 'options'), + )).thenThrow(error); + + try { + await client.delete(path); + } catch (e) { + expect(e, isA()); + expect(e, StreamChatNetworkError.fromDioError(error)); + } + + verify(() => dio.delete( + path, + options: any(named: 'options'), + )).called(1); + verifyNoMoreInteractions(dio); + }, + ); + + test('`.patch` should return response successfully', () async { + final dio = MockDio(); + final client = StreamHttpClient('api-key', dio: dio); + + const path = 'test-patch-api-path'; + when(() => dio.patch( + path, + options: any(named: 'options'), + )).thenAnswer((_) async => successResponse(path)); + + final res = await client.patch(path); + + expect(res, isNotNull); + expect(res.statusCode, 200); + expect(res.requestOptions.path, path); + + verify(() => dio.patch( + path, + options: any(named: 'options'), + )).called(1); + verifyNoMoreInteractions(dio); + }); + + test( + '`.patch` should throw an instance of `StreamChatNetworkError`', + () async { + final dio = MockDio(); + final client = StreamHttpClient('api-key', dio: dio); + + const path = 'test-patch-api-path'; + final error = throwableError( + path, + error: StreamChatNetworkError(ChatErrorCode.internalSystemError), + ); + when(() => dio.patch( + path, + options: any(named: 'options'), + )).thenThrow(error); + + try { + await client.patch(path); + } catch (e) { + expect(e, isA()); + expect(e, StreamChatNetworkError.fromDioError(error)); + } + + verify(() => dio.patch( + path, + options: any(named: 'options'), + )).called(1); + verifyNoMoreInteractions(dio); + }, + ); + + test('`.put` should return response successfully', () async { + final dio = MockDio(); + final client = StreamHttpClient('api-key', dio: dio); + + const path = 'test-put-api-path'; + when(() => dio.put( + path, + options: any(named: 'options'), + )).thenAnswer((_) async => successResponse(path)); + + final res = await client.put(path); + + expect(res, isNotNull); + expect(res.statusCode, 200); + expect(res.requestOptions.path, path); + + verify(() => dio.put( + path, + options: any(named: 'options'), + )).called(1); + verifyNoMoreInteractions(dio); + }); + + test( + '`.put` should throw an instance of `StreamChatNetworkError`', + () async { + final dio = MockDio(); + final client = StreamHttpClient('api-key', dio: dio); + + const path = 'test-put-api-path'; + final error = throwableError( + path, + error: StreamChatNetworkError(ChatErrorCode.internalSystemError), + ); + when(() => dio.put( + path, + options: any(named: 'options'), + )).thenThrow(error); + + try { + await client.put(path); + } catch (e) { + expect(e, isA()); + expect(e, StreamChatNetworkError.fromDioError(error)); + } + + verify(() => dio.put( + path, + options: any(named: 'options'), + )).called(1); + verifyNoMoreInteractions(dio); + }, + ); + + test('`.postFile` should return response successfully', () async { + final dio = MockDio(); + final client = StreamHttpClient('api-key', dio: dio); + + const path = 'test-delete-api-path'; + final file = MultipartFile.fromBytes([]); + + when(() => dio.post( + path, + data: any(named: 'data'), + options: any(named: 'options'), + )).thenAnswer((_) async => successResponse(path)); + + final res = await client.postFile(path, file); + + expect(res, isNotNull); + expect(res.statusCode, 200); + expect(res.requestOptions.path, path); + + verify(() => dio.post( + path, + data: any(named: 'data'), + options: any(named: 'options'), + )).called(1); + verifyNoMoreInteractions(dio); + }); + + test( + '`.postFile` should throw an instance of `StreamChatNetworkError`', + () async { + final dio = MockDio(); + final client = StreamHttpClient('api-key', dio: dio); + + const path = 'test-post-file-api-path'; + final file = MultipartFile.fromBytes([]); + + final error = throwableError( + path, + error: StreamChatNetworkError(ChatErrorCode.internalSystemError), + ); + when(() => dio.post( + path, + data: any(named: 'data'), + options: any(named: 'options'), + )).thenThrow(error); + + try { + await client.postFile(path, file); + } catch (e) { + expect(e, isA()); + expect(e, StreamChatNetworkError.fromDioError(error)); + } + + verify(() => dio.post( + path, + data: any(named: 'data'), + options: any(named: 'options'), + )).called(1); + verifyNoMoreInteractions(dio); + }, + ); + + test('`.request` should return response successfully', () async { + final dio = MockDio(); + final client = StreamHttpClient('api-key', dio: dio); + + const path = 'test-request-api-path'; + when(() => dio.request( + path, + options: any(named: 'options'), + )).thenAnswer((_) async => successResponse(path)); + + final res = await client.request(path); + + expect(res, isNotNull); + expect(res.statusCode, 200); + expect(res.requestOptions.path, path); + + verify(() => dio.request( + path, + options: any(named: 'options'), + )).called(1); + verifyNoMoreInteractions(dio); + }); + + test( + '`.request` should throw an instance of `StreamChatNetworkError`', + () async { + final dio = MockDio(); + final client = StreamHttpClient('api-key', dio: dio); + + const path = 'test-put-api-path'; + final error = throwableError( + path, + streamChatDioError: true, + error: StreamChatNetworkError(ChatErrorCode.internalSystemError), + ); + when(() => dio.request( + path, + options: any(named: 'options'), + )).thenThrow(error); + + try { + await client.request(path); + } catch (e) { + expect(e, isA()); + expect(e, error.error); + } + + verify(() => dio.request( + path, + options: any(named: 'options'), + )).called(1); + verifyNoMoreInteractions(dio); + }, + ); +} diff --git a/packages/stream_chat/test/src/core/http/token_manager_test.dart b/packages/stream_chat/test/src/core/http/token_manager_test.dart new file mode 100644 index 00000000..a1a38fa5 --- /dev/null +++ b/packages/stream_chat/test/src/core/http/token_manager_test.dart @@ -0,0 +1,141 @@ +import 'package:stream_chat/src/core/http/token.dart'; +import 'package:stream_chat/src/core/http/token_manager.dart'; +import 'package:test/test.dart'; + +void main() { + late TokenManager tokenManager; + + setUp(() { + tokenManager = TokenManager(); + }); + + tearDown(() { + tokenManager.reset(); + }); + + test('`setTokenOrProvider` should set token', () async { + expect(tokenManager.userId, isNull); + + const userId = 'test-user-id'; + final token = Token.development(userId); + final returnedToken = await tokenManager.setTokenOrProvider( + userId, + token: token, + ); + + expect(returnedToken, token); + expect(tokenManager.userId, userId); + expect(tokenManager.isStatic, isTrue); + }); + + test('`setTokenOrProvider` should set tokenProvider', () async { + expect(tokenManager.userId, isNull); + + const userId = 'test-user-id'; + Future tokenProvider(String userId) async => + Token.development(userId).rawValue; + final returnedToken = await tokenManager.setTokenOrProvider( + userId, + provider: tokenProvider, + ); + + expect(returnedToken, isNotNull); + expect(tokenManager.userId, userId); + expect(tokenManager.isStatic, isFalse); + }); + + test( + '`setTokenOrProvider` should throw if both token and provider is not provided', + () async { + expect(tokenManager.userId, isNull); + + const userId = 'test-user-id'; + try { + await tokenManager.setTokenOrProvider(userId); + } catch (e) { + expect(e, isA()); + } + }, + ); + + test( + '`setTokenOrProvider` should throw if both token and provider is provided', + () async { + expect(tokenManager.userId, isNull); + + const userId = 'test-user-id'; + final token = Token.development(userId); + Future tokenProvider(String userId) async => + Token.development(userId).rawValue; + try { + await tokenManager.setTokenOrProvider( + userId, + token: token, + provider: tokenProvider, + ); + } catch (e) { + expect(e, isA()); + } + }, + ); + + test( + '`.loadToken` should return token set via `setToken`', + () async { + const userId = 'test-user-id'; + final token = Token.development(userId); + await tokenManager.setTokenOrProvider(userId, token: token); + + final returnedToken = await tokenManager.loadToken(); + expect(returnedToken, token); + }, + ); + + test( + '`.loadToken` should return token set via `setProvider`', + () async { + const userId = 'test-user-id'; + final token = Token.development(userId); + Future tokenProvider(String userId) async => token.rawValue; + await tokenManager.setTokenOrProvider(userId, provider: tokenProvider); + + final returnedToken = await tokenManager.loadToken(); + expect(returnedToken, token); + }, + ); + + test( + '`.loadToken` should return refreshed token set via `setProvider`', + () async { + const userId = 'test-user-id'; + final token = Token.development(userId); + final refreshToken = Token.development(userId); + + var refresh = false; + + Future tokenProvider(String userId) async { + if (refresh) return refreshToken.rawValue; + return token.rawValue; + } + + await tokenManager.setTokenOrProvider(userId, provider: tokenProvider); + + final returnedToken = await tokenManager.loadToken(); + expect(returnedToken, token); + + refresh = true; + final returnedRefreshToken = await tokenManager.loadToken(refresh: true); + expect(returnedRefreshToken, refreshToken); + }, + ); + + test('`.reset` should reset the tokenManager', () async { + const userId = 'test-user-id'; + final token = Token.development(userId); + await tokenManager.setTokenOrProvider(userId, token: token); + expect(tokenManager.userId, userId); + + tokenManager.reset(); + expect(tokenManager.userId, isNull); + }); +} diff --git a/packages/stream_chat/test/src/core/http/token_test.dart b/packages/stream_chat/test/src/core/http/token_test.dart new file mode 100644 index 00000000..b448884e --- /dev/null +++ b/packages/stream_chat/test/src/core/http/token_test.dart @@ -0,0 +1,57 @@ +import 'package:stream_chat/src/core/http/token.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:test/test.dart'; + +void main() { + test('`.anonymous` should create anonymous token with passed userId', () { + const userId = 'test-user-id'; + final token = Token.anonymous(userId: userId); + expect(token, isNotNull); + expect(token.userId, userId); + expect(token.rawValue, isEmpty); + expect(token.authType, AuthType.anonymous); + expect(token.authType.raw, AuthType.anonymous.raw); + }); + + test('`.fromRawValue` should create token from rawValue', () { + const userId = 'test-user-id'; + final devToken = Token.development(userId); + final token = Token.fromRawValue(devToken.rawValue); + expect(token, devToken); + }); + + test('`.fromRawValue` should throw if does not contain `user_id`', () { + const badToken = 'bad-token-without-a-user-id'; + try { + Token.fromRawValue(badToken); + } catch (e) { + expect(e, isA()); + } + }); + + test('`.development` should create a dev-token with provided user-id', () { + const userId = 'test-user-id'; + final token = Token.development(userId); + expect(token, isNotNull); + expect(token.userId, userId); + expect(token.rawValue, isNotEmpty); + expect(token.authType, AuthType.jwt); + expect(token.authType.raw, AuthType.jwt.raw); + }); + + test( + '`.guest` should create a guest-token with provided user and provider', + () async { + final user = User(id: 'test-user-id'); + Future provider(User user) async => + Token.development(user.id).rawValue; + + final token = await Token.guest(user, provider); + expect(token, isNotNull); + expect(token.userId, user.id); + expect(token.rawValue, isNotEmpty); + expect(token.authType, AuthType.jwt); + expect(token.authType.raw, AuthType.jwt.raw); + }, + ); +} From cb9a8a23a97e2073d808d5f407ac76f6e5a524af Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 4 Jun 2021 16:25:12 +0530 Subject: [PATCH 016/150] add timer_helper_test.dart Signed-off-by: Sahil Kumar --- .../test/src/ws/timer_helper_test.dart | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 packages/stream_chat/test/src/ws/timer_helper_test.dart diff --git a/packages/stream_chat/test/src/ws/timer_helper_test.dart b/packages/stream_chat/test/src/ws/timer_helper_test.dart new file mode 100644 index 00000000..ecafd6d2 --- /dev/null +++ b/packages/stream_chat/test/src/ws/timer_helper_test.dart @@ -0,0 +1,98 @@ +import 'package:stream_chat/src/ws/timer_helper.dart'; +import 'package:test/test.dart'; + +void main() { + late TimerHelper timerHelper; + + setUp(() { + timerHelper = TimerHelper(); + }); + + tearDown(() { + timerHelper.cancelAllTimers(); + }); + + test('setTimer', () async { + expect(timerHelper.hasTimers, isFalse); + + var count = 0; + void callback() => count += 1; + + timerHelper.setTimer( + const Duration(milliseconds: 500), + callback, + ); + + expect(count, 0); + await Future.delayed(const Duration(milliseconds: 500)); + expect(count, 1); + + expect(timerHelper.hasTimers, isTrue); + }); + + test('setImmediateTimer', () async { + var count = 0; + void callback() => count += 1; + + timerHelper.setTimer( + const Duration(milliseconds: 500), + callback, + immediate: true, + ); + + expect(count, 1); + await Future.delayed(const Duration(milliseconds: 500)); + expect(count, 2); + }); + + test('setPeriodicTimer', () async { + expect(timerHelper.hasTimers, isFalse); + var count = 0; + void callback() => count += 1; + + timerHelper.setTimer( + const Duration(milliseconds: 500), + callback, + ); + + expect(count, 0); + await Future.delayed(const Duration(milliseconds: 500)); + expect(count, 1); + + expect(timerHelper.hasTimers, isTrue); + }); + + test('setImmediatePeriodicTimer', () async { + var count = 0; + void callback(_) => count += 1; + + timerHelper.setPeriodicTimer( + const Duration(milliseconds: 500), + callback, + immediate: true, + ); + + expect(count, 1); + await Future.delayed(const Duration(milliseconds: 500)); + expect(count, 2); + }); + + test('cancelTimer', () { + expect(timerHelper.hasTimers, isFalse); + final id = timerHelper.setTimer(const Duration(seconds: 3), () {}); + expect(timerHelper.hasTimers, isTrue); + timerHelper.cancelTimer(id); + expect(timerHelper.hasTimers, isFalse); + }); + + test('cancelAllTimers', () { + expect(timerHelper.hasTimers, isFalse); + timerHelper + ..setTimer(const Duration(seconds: 3), () {}) + ..setTimer(const Duration(seconds: 6), () {}) + ..setTimer(const Duration(seconds: 9), () {}); + expect(timerHelper.hasTimers, isTrue); + timerHelper.cancelAllTimers(); + expect(timerHelper.hasTimers, isFalse); + }); +} From dbb98811ab2e56df1949c69dea2e1a2e936449ab Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 4 Jun 2021 16:26:35 +0530 Subject: [PATCH 017/150] push mocks.dart Signed-off-by: Sahil Kumar --- packages/stream_chat/test/src/mocks.dart | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/stream_chat/test/src/mocks.dart b/packages/stream_chat/test/src/mocks.dart index 98e84d77..be4f15e5 100644 --- a/packages/stream_chat/test/src/mocks.dart +++ b/packages/stream_chat/test/src/mocks.dart @@ -1,6 +1,34 @@ +import 'package:dio/dio.dart'; +import 'package:logging/logging.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/core/http/connection_id_manager.dart'; +import 'package:stream_chat/src/core/http/stream_http_client.dart'; +import 'package:stream_chat/src/core/http/token_manager.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; class MockWebSocketChannel extends Mock implements WebSocketChannel {} class MockWebSocketSink extends Mock implements WebSocketSink {} + +class MockDio extends Mock implements Dio { + BaseOptions? _options; + + @override + BaseOptions get options => _options ??= BaseOptions(); + + Interceptors? _interceptors; + + @override + Interceptors get interceptors => _interceptors ??= Interceptors(); +} + +class MockLogger extends Mock implements Logger { + @override + Level get level => Level.ALL; +} + +class MockHttpClient extends Mock implements StreamHttpClient {} + +class MockTokenManager extends Mock implements TokenManager {} + +class MockConnectionIdManager extends Mock implements ConnectionIdManager {} From b64aa70b73aad969f49575d160a530ec8124838e Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 4 Jun 2021 16:27:37 +0530 Subject: [PATCH 018/150] remove, refactor old tests Signed-off-by: Sahil Kumar --- .../test/src/api/channel_test.dart | 2724 ----------------- .../test/src/api/web_socket_stub_test.dart | 11 - .../test/src/api/websocket_test.dart | 348 --- .../stream_chat/test/src/client_test.dart | 1205 -------- .../test/src/models/action_test.dart | 2 +- .../test/src/models/attachment_test.dart | 4 +- .../test/src/models/channel_state_test.dart | 10 +- .../test/src/models/channel_test.dart | 2 +- .../test/src/models/command_test.dart | 2 +- .../test/src/models/device_test.dart | 2 +- .../test/src/models/event_test.dart | 4 +- .../test/src/models/member_test.dart | 4 +- .../test/src/models/message_test.dart | 8 +- .../test/src/models/reaction_test.dart | 4 +- .../test/src/models/read_test.dart | 4 +- .../test/src/models/serialization_test.dart | 8 +- .../test/src/models/user_test.dart | 2 +- 17 files changed, 28 insertions(+), 4316 deletions(-) delete mode 100644 packages/stream_chat/test/src/api/channel_test.dart delete mode 100644 packages/stream_chat/test/src/api/web_socket_stub_test.dart delete mode 100644 packages/stream_chat/test/src/api/websocket_test.dart delete mode 100644 packages/stream_chat/test/src/client_test.dart diff --git a/packages/stream_chat/test/src/api/channel_test.dart b/packages/stream_chat/test/src/api/channel_test.dart deleted file mode 100644 index 12e84fbc..00000000 --- a/packages/stream_chat/test/src/api/channel_test.dart +++ /dev/null @@ -1,2724 +0,0 @@ -import 'dart:convert'; - -import 'package:dio/dio.dart'; -import 'package:dio/native_imp.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat/src/core/api/requests.dart'; -import 'package:stream_chat/src/client.dart'; -import 'package:stream_chat/src/event_type.dart'; -import 'package:stream_chat/src/core/models/event.dart'; -import 'package:stream_chat/src/core/models/message.dart'; -import 'package:stream_chat/src/core/models/own_user.dart'; -import 'package:stream_chat/src/core/models/reaction.dart'; -import 'package:stream_chat/stream_chat.dart'; -import 'package:test/test.dart'; - -class MockDio extends Mock implements DioForNative {} - -class FakeRequestOptions extends Fake implements RequestOptions {} - -class MockAttachmentUploader extends Mock implements AttachmentFileUploader {} - -class MockHttpClientAdapter extends Mock implements HttpClientAdapter {} - -void main() { - group('src/api/channel', () { - group('message', () { - test('sendMessage', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - tokenProvider: (_) async => '', - ); - final channelClient = client.channel('messaging', id: 'testid'); - final message = Message(text: 'hey', id: 'test'); - - when(() => mockDio.post( - any(), - data: any(named: 'data'), - )).thenAnswer((_) async => Response( - data: jsonEncode(ChannelState()), - statusCode: 200, - requestOptions: FakeRequestOptions(), - )); - - await channelClient.watch(); - - when( - () => mockDio.post( - '/channels/messaging/testid/message', - data: {'message': message.toJson()}, - ), - ).thenAnswer( - (_) async => Response( - data: jsonEncode({'message': message}), - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await channelClient.sendMessage(message); - - verify(() => - mockDio.post('/channels/messaging/testid/message', data: { - 'message': message.toJson(), - })).called(1); - }); - - test('markRead', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - tokenProvider: (_) async => '', - ); - - final channelClient = client.channel('messaging', id: 'testid'); - - when(() => mockDio.post( - any(), - data: any(named: 'data'), - )).thenAnswer((_) async => Response( - data: jsonEncode(ChannelState()), - statusCode: 200, - requestOptions: FakeRequestOptions(), - )); - await channelClient.watch(); - - when( - () => mockDio.post( - '/channels/messaging/testid/read', - data: {}, - ), - ).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await channelClient.markRead(); - - verify(() => mockDio.post('/channels/messaging/testid/read', - data: {})).called(1); - }); - - test('getReplies', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - tokenProvider: (_) async => '', - ); - final channelClient = client.channel('messaging', id: 'testid'); - const pagination = PaginationParams(); - - when(() => mockDio.get('/messages/messageid/replies', - queryParameters: pagination.toJson())).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await channelClient.getReplies('messageid', pagination); - - verify(() => mockDio.get('/messages/messageid/replies', - queryParameters: pagination.toJson())).called(1); - }); - - test('sendAction', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - tokenProvider: (_) async => '', - ); - final channelClient = client.channel('messaging', id: 'testid'); - - when(() => mockDio.post( - any(), - data: any(named: 'data'), - )).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - await channelClient.watch(); - - final data = {'test': true}; - - when(() => mockDio.post('/messages/messageid/action', data: { - 'id': 'testid', - 'type': 'messaging', - 'form_data': data, - 'message_id': 'messageid', - })).thenAnswer((_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - )); - - await channelClient.sendAction(Message(id: 'messageid'), data); - - verify(() => mockDio.post('/messages/messageid/action', data: { - 'id': 'testid', - 'type': 'messaging', - 'form_data': data, - 'message_id': 'messageid', - })).called(1); - }); - - test('getMessagesById', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - tokenProvider: (_) async => '', - ); - final channelClient = client.channel('messaging', id: 'testid'); - final messageIds = ['a', 'b']; - - when(() => mockDio.get('/channels/messaging/testid/messages', - queryParameters: {'ids': messageIds.join(',')})).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await channelClient.getMessagesById(messageIds); - - verify(() => mockDio.get('/channels/messaging/testid/messages', - queryParameters: {'ids': messageIds.join(',')})).called(1); - }); - - test('sendFile', () async { - final mockDio = MockDio(); - final mockUploader = MockAttachmentUploader(); - - const file = AttachmentFile( - path: 'filePath/fileName.pdf', - size: 100, - ); - const channelId = 'testId'; - const channelType = 'messaging'; - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - tokenProvider: (_) async => '', - attachmentFileUploader: mockUploader, - ); - final channelClient = client.channel(channelType, id: channelId); - - when(() => mockDio.post( - any(), - data: any(named: 'data'), - )).thenAnswer((_) async => Response( - data: jsonEncode(ChannelState()), - statusCode: 200, - requestOptions: FakeRequestOptions(), - )); - - await channelClient.watch(); - - when(() => mockUploader.sendFile(file, channelId, channelType)) - .thenAnswer((_) async => SendFileResponse()); - - await channelClient.sendFile(file); - - verify(() => mockUploader.sendFile(file, channelId, channelType)) - .called(1); - }); - - test('sendImage', () async { - final mockDio = MockDio(); - final mockUploader = MockAttachmentUploader(); - - const image = AttachmentFile( - path: 'imagePath/imageName.jpeg', - size: 100, - ); - const channelId = 'testId'; - const channelType = 'messaging'; - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - tokenProvider: (_) async => '', - attachmentFileUploader: mockUploader, - ); - final channelClient = client.channel(channelType, id: channelId); - - when(() => mockDio.post( - any(), - data: any(named: 'data'), - )).thenAnswer((_) async => Response( - data: jsonEncode(ChannelState()), - statusCode: 200, - requestOptions: FakeRequestOptions(), - )); - - await channelClient.watch(); - - when(() => mockUploader.sendImage(image, channelId, channelType)) - .thenAnswer((_) async => SendImageResponse()); - - await channelClient.sendImage(image); - - verify(() => mockUploader.sendImage(image, channelId, channelType)) - .called(1); - }); - - test('deleteFile', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - tokenProvider: (_) async => '', - ); - final channelClient = client.channel('messaging', id: 'testid'); - const url = 'url'; - - when(() => mockDio.post( - any(), - data: any(named: 'data'), - )).thenAnswer((_) async => Response( - data: jsonEncode(ChannelState()), - statusCode: 200, - requestOptions: FakeRequestOptions(), - )); - - await channelClient.watch(); - - when( - () => mockDio.delete( - '/channels/messaging/testid/file', - queryParameters: {'url': url}, - ), - ).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await channelClient.deleteFile(url); - - verify(() => mockDio.delete('/channels/messaging/testid/file', - queryParameters: {'url': url})).called(1); - }); - - test('deleteImage', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - tokenProvider: (_) async => '', - ); - final channelClient = client.channel('messaging', id: 'testid'); - const url = 'url'; - - when(() => mockDio.post( - any(), - data: any(named: 'data'), - )).thenAnswer((_) async => Response( - data: jsonEncode(ChannelState()), - statusCode: 200, - requestOptions: FakeRequestOptions(), - )); - - await channelClient.watch(); - - when( - () => mockDio.delete( - '/channels/messaging/testid/image', - queryParameters: {'url': url}, - ), - ).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await channelClient.deleteImage(url); - - verify(() => mockDio.delete('/channels/messaging/testid/image', - queryParameters: {'url': url})).called(1); - }); - - test('pinMessage should throw argument error', () { - final client = StreamChatClient('api-key'); - - final channelClient = client.channel('messaging', id: 'testid'); - - final message = Message(text: 'Hello'); - - expect( - () => channelClient.pinMessage(message, 'InvalidType'), - throwsArgumentError, - ); - }); - - test('should be pinned successfully', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - tokenProvider: (_) async => '', - ); - final channelClient = client.channel('messaging', id: 'testid'); - final message = Message(text: 'Hello', id: 'test'); - - when(() => mockDio.post( - any(), - data: any(named: 'data'), - )).thenAnswer((_) async => Response( - data: jsonEncode(ChannelState()), - statusCode: 200, - requestOptions: FakeRequestOptions(), - )); - - await channelClient.watch(); - - when( - () => mockDio.post( - '/messages/${message.id}', - data: anything, - ), - ).thenAnswer( - (_) async => Response( - data: jsonEncode({'message': message}), - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await channelClient.pinMessage(message, 30); - - verify(() => - mockDio.post('/messages/${message.id}', data: anything)) - .called(1); - }); - - test('should be pinned successfully with null timeout', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - tokenProvider: (_) async => '', - ); - final channelClient = client.channel('messaging', id: 'testid'); - final message = Message(text: 'Hello', id: 'test'); - - when(() => mockDio.post( - any(), - data: any(named: 'data'), - )).thenAnswer((_) async => Response( - data: jsonEncode(ChannelState()), - statusCode: 200, - requestOptions: FakeRequestOptions(), - )); - - await channelClient.watch(); - - when( - () => mockDio.post( - '/messages/${message.id}', - data: anything, - ), - ).thenAnswer( - (_) async => Response( - data: jsonEncode({'message': message}), - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await channelClient.pinMessage(message); - - verify(() => - mockDio.post('/messages/${message.id}', data: anything)) - .called(1); - }); - - test('should be unpinned successfully', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - tokenProvider: (_) async => '', - ); - final channelClient = client.channel('messaging', id: 'testid'); - final message = Message(text: 'Hello', id: 'test'); - - when(() => mockDio.post( - any(), - data: any(named: 'data'), - )).thenAnswer((_) async => Response( - data: jsonEncode(ChannelState()), - statusCode: 200, - requestOptions: FakeRequestOptions(), - )); - - await channelClient.watch(); - - when( - () => mockDio.post( - '/messages/${message.id}', - data: anything, - ), - ).thenAnswer( - (_) async => Response( - data: jsonEncode({'message': message}), - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await channelClient.unpinMessage(message); - - verify(() => - mockDio.post('/messages/${message.id}', data: anything)) - .called(1); - }); - }); - - test('sendEvent', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - tokenProvider: (_) async => '', - ); - final channelClient = client.channel('messaging', id: 'testid'); - - when( - () => mockDio.post( - any(), - data: any(named: 'data'), - ), - ).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - await channelClient.watch(); - - const event = Event(type: EventType.any); - - when( - () => mockDio.post( - '/channels/messaging/testid/event', - data: {'event': event.toJson()}, - ), - ).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await channelClient.sendEvent(event); - - verify(() => mockDio.post('/channels/messaging/testid/event', - data: {'event': event.toJson()})).called(1); - }); - - test('keyStroke', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - tokenProvider: (_) async => '', - ); - final channelClient = client.channel('messaging', id: 'testid'); - - when( - () => mockDio.post( - any(), - data: any(named: 'data'), - ), - ).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - await channelClient.watch(); - - const event = Event(type: EventType.typingStart); - - when( - () => mockDio.post( - '/channels/messaging/testid/event', - data: {'event': event.toJson()}, - ), - ).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await channelClient.keyStroke(); - - verify(() => mockDio.post('/channels/messaging/testid/event', - data: {'event': event.toJson()})).called(1); - }); - - test('stopTyping', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - tokenProvider: (_) async => '', - ); - final channelClient = client.channel('messaging', id: 'testid'); - - when(() => mockDio.post( - any(), - data: any(named: 'data'), - )).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - await channelClient.watch(); - - const event = Event(type: EventType.typingStop); - - when( - () => mockDio.post( - '/channels/messaging/testid/event', - data: {'event': event.toJson()}, - ), - ).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await channelClient.stopTyping(); - - verify(() => mockDio.post('/channels/messaging/testid/event', - data: {'event': event.toJson()})).called(1); - }); - - group('reactions', () { - test('sendReaction', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - tokenProvider: (_) async => '', - ); - - final user = OwnUser(id: 'test-id'); - - client.state.user = user; - - final message = Message(id: 'messageid'); - const reactionType = 'test'; - final reaction = Reaction(type: reactionType); - final channelClient = client.channel('messaging', id: 'testid'); - - when(() => mockDio.post( - any(), - data: any(named: 'data'), - )).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - await channelClient.watch(); - - when( - () => mockDio.post( - '/messages/${message.id}/reaction', - data: { - 'reaction': { - 'type': reactionType, - }, - 'enforce_unique': false, - }, - ), - ).thenAnswer( - (_) async => Response( - data: jsonEncode({ - 'message': message, - 'reaction': reaction, - }), - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await channelClient.sendReaction( - message, - reactionType, - ); - - verify( - () => mockDio.post('/messages/messageid/reaction', data: { - 'reaction': { - 'type': reactionType, - }, - 'enforce_unique': false, - })).called(1); - }); - - test('deleteReaction', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - tokenProvider: (_) async => '', - ); - - client.state.user = OwnUser(id: 'test-id'); - - final channelClient = client.channel('messaging', id: 'testid'); - - when( - () => mockDio.delete('/messages/messageid/reaction/test'), - ).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await channelClient.deleteReaction( - Message( - id: 'messageid', - ), - Reaction( - type: 'test', - createdAt: DateTime.now(), - user: User( - id: client.state.user?.id ?? '', - ), - ), - ); - - verify(() => - mockDio.delete('/messages/messageid/reaction/test')) - .called(1); - }); - - test('getReactions', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - tokenProvider: (_) async => '', - ); - final channelClient = client.channel('messaging', id: 'testid'); - const pagination = PaginationParams(); - - when( - () => mockDio.get( - '/messages/messageid/reactions', - queryParameters: pagination.toJson(), - ), - ).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await channelClient.getReactions('messageid', pagination); - - verify(() => mockDio.get('/messages/messageid/reactions', - queryParameters: pagination.toJson())).called(1); - }); - }); - - group('channel', () { - test('addMembers', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - tokenProvider: (_) async => '', - ); - final channelClient = client.channel('messaging', id: 'testid'); - final channelModel = ChannelModel(cid: 'messaging:testid'); - - when(() => mockDio.post( - any(), - data: any(named: 'data'), - )).thenAnswer((_) async => Response( - data: jsonEncode(ChannelState(channel: channelModel)), - statusCode: 200, - requestOptions: FakeRequestOptions(), - )); - - await channelClient.watch(); - - final members = [Member(userId: 'vishal')]; - final memberIds = members.map((e) => e.userId!).toList(); - final message = Message(text: 'test'); - - when( - () => mockDio.post( - '/channels/messaging/testid', - data: {'add_members': memberIds, 'message': message.toJson()}, - ), - ).thenAnswer( - (_) async => Response( - data: jsonEncode({ - 'members': members, - 'message': message, - 'channel': channelModel, - }), - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await channelClient.addMembers(memberIds, message); - - verify(() => mockDio.post('/channels/messaging/testid', - data: {'add_members': memberIds, 'message': message.toJson()})) - .called(1); - }); - - test('acceptInvite', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - tokenProvider: (_) async => '', - ); - final channelClient = client.channel('messaging', id: 'testid'); - final channelModel = ChannelModel(cid: 'messaging:testid'); - - when(() => mockDio.post( - any(), - data: any(named: 'data'), - )).thenAnswer((_) async => Response( - data: jsonEncode(ChannelState(channel: channelModel)), - statusCode: 200, - requestOptions: FakeRequestOptions(), - )); - - await channelClient.watch(); - - final message = Message(text: 'test'); - - when( - () => mockDio.post( - '/channels/messaging/testid', - data: {'accept_invite': true, 'message': message.toJson()}, - ), - ).thenAnswer( - (_) async => Response( - data: jsonEncode({ - 'message': message, - 'channel': channelModel, - }), - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await channelClient.acceptInvite(message); - - verify(() => mockDio.post('/channels/messaging/testid', - data: {'accept_invite': true, 'message': message.toJson()})) - .called(1); - }); - - group('query', () { - test('without id', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - tokenProvider: (_) async => '', - ); - final channelClient = client.channel('messaging'); - final options = { - 'watch': true, - 'state': false, - 'presence': true, - }; - - when(() => mockDio.post( - '/channels/messaging/query', - data: options, - )).thenAnswer( - (_) async => Response( - data: r''' - { - "channel": { - "id": "!members-0LOcD0mZtTan60zHobLmELjdndXsonnBVNzZnB5mTt0", - "type": "messaging", - "cid": "messaging:!members-0LOcD0mZtTan60zHobLmELjdndXsonnBVNzZnB5mTt0", - "last_message_at": "2020-01-28T22:17:31.204287Z", - "created_at": "2020-01-28T22:17:31.00187Z", - "updated_at": "2020-01-28T22:17:31.00187Z", - "created_by": { - "id": "spring-voice-7", - "role": "user", - "created_at": "2020-01-28T22:17:30.834135Z", - "updated_at": "2020-01-28T22:17:31.186771Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", - "name": "Spring voice" - }, - "frozen": false, - "member_count": 2, - "config": { - "created_at": "2020-01-29T12:59:14.291912835Z", - "updated_at": "2020-01-29T12:59:14.291912991Z", - "name": "messaging", - "typing_events": true, - "read_events": true, - "connect_events": true, - "search": true, - "reactions": true, - "replies": true, - "mutes": true, - "uploads": true, - "url_enrichment": true, - "message_retention": "infinite", - "max_message_length": 5000, - "automod": "disabled", - "automod_behavior": "flag", - "commands": [ - { - "name": "giphy", - "description": "Post a random gif to the channel", - "args": "[text]", - "set": "fun_set" - } - ] - }, - "name": "Mia Denys", - "image": "https://randomuser.me/api/portraits/women/2.jpg" - }, - "messages": [ - { - "id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f", - "text": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA", - "html": "\u003cp\u003e\u003ca href=\"https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA\" rel=\"nofollow\"\u003ehttps://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA\u003c/a\u003e\u003c/p\u003e\n", - "type": "regular", - "user": { - "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", - "role": "user", - "created_at": "2020-01-28T22:17:30.83015Z", - "updated_at": "2020-01-28T22:17:31.19435Z", - "banned": false, - "online": false, - "image": "https://randomuser.me/api/portraits/women/2.jpg", - "name": "Mia Denys" - }, - "attachments": [ - { - "type": "video", - "author_name": "GIPHY", - "title": "The Lion King Disney GIF - Find \u0026 Share on GIPHY", - "title_link": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", - "text": "Discover \u0026 share this Lion King Live Action GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.", - "image_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", - "thumb_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", - "asset_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.mp4", - "og_scrape_url": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA" - } - ], - "latest_reactions": [ - { - "message_id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f", - "user_id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", - "user": { - "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", - "role": "user", - "created_at": "2020-01-28T22:17:30.83015Z", - "updated_at": "2020-01-28T22:17:31.19435Z", - "banned": false, - "online": false, - "image": "https://randomuser.me/api/portraits/women/2.jpg", - "name": "Mia Denys" - }, - "type": "love", - "score": 1, - "created_at": "2020-01-28T22:17:31.128376Z", - "updated_at": "2020-01-28T22:17:31.128376Z" - } - ], - "own_reactions": [], - "reaction_counts": { - "love": 1 - }, - "reaction_scores": { - "love": 1 - }, - "reply_count": 0, - "created_at": "2020-01-28T22:17:31.107978Z", - "updated_at": "2020-01-28T22:17:31.130506Z", - "mentioned_users": [] - }, - { - "id": "16e19c46-fb96-4f89-8031-d5bd2ce3bd64", - "text": "Few can name a topfull mother that isn't a breezeless damage.", - "html": "\u003cp\u003eFew can name a topfull mother that isn’t a breezeless damage.\u003c/p\u003e\n", - "type": "regular", - "user": { - "id": "spring-voice-7", - "role": "user", - "created_at": "2020-01-28T22:17:30.834135Z", - "updated_at": "2020-01-28T22:17:31.186771Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", - "name": "Spring voice" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 0, - "created_at": "2020-01-28T22:17:31.153518Z", - "updated_at": "2020-01-28T22:17:31.153518Z", - "mentioned_users": [] - }, - { - "id": "38b1b252-c9a6-4aea-a39e-8de3b7f7604b", - "text": "https://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog", - "html": "\u003cp\u003e\u003ca href=\"https://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog\" rel=\"nofollow\"\u003ehttps://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog\u003c/a\u003e\u003c/p\u003e\n", - "type": "regular", - "user": { - "id": "spring-voice-7", - "role": "user", - "created_at": "2020-01-28T22:17:30.834135Z", - "updated_at": "2020-01-28T22:17:31.186771Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", - "name": "Spring voice" - }, - "attachments": [ - { - "type": "video", - "author_name": "GIPHY", - "title": "Moustache Thumbs Up GIF - Find \u0026 Share on GIPHY", - "title_link": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.gif", - "text": "Discover \u0026 share this Pouce Leve GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.", - "image_url": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.gif", - "thumb_url": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.gif", - "asset_url": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.mp4", - "og_scrape_url": "https://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog" - } - ], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 0, - "created_at": "2020-01-28T22:17:31.155428Z", - "updated_at": "2020-01-28T22:17:31.155428Z", - "mentioned_users": [] - }, - { - "id": "5c7d0c68-72d5-4027-b6b7-711b342d0fc4", - "text": "The carbons could be said to resemble smartish hoods.", - "html": "\u003cp\u003eThe carbons could be said to resemble smartish hoods.\u003c/p\u003e\n", - "type": "regular", - "user": { - "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", - "role": "user", - "created_at": "2020-01-28T22:17:30.83015Z", - "updated_at": "2020-01-28T22:17:31.19435Z", - "banned": false, - "online": false, - "image": "https://randomuser.me/api/portraits/women/2.jpg", - "name": "Mia Denys" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 1, - "created_at": "2020-01-28T22:17:31.157811Z", - "updated_at": "2020-01-28T22:17:31.157811Z", - "mentioned_users": [] - }, - { - "id": "5b02535a-0c3c-45fa-9cf8-16f840c5123f", - "text": "Their software was, in this moment, a prolix feature.", - "html": "\u003cp\u003eTheir software was, in this moment, a prolix feature.\u003c/p\u003e\n", - "type": "regular", - "user": { - "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", - "role": "user", - "created_at": "2020-01-28T22:17:30.83015Z", - "updated_at": "2020-01-28T22:17:31.19435Z", - "banned": false, - "online": false, - "image": "https://randomuser.me/api/portraits/women/2.jpg", - "name": "Mia Denys" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 1, - "created_at": "2020-01-28T22:17:31.158391Z", - "updated_at": "2020-01-28T22:17:31.158391Z", - "mentioned_users": [] - } - ], - "watcher_count": 1, - "read": [ - { - "user": { - "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", - "role": "user", - "created_at": "2020-01-28T22:17:30.83015Z", - "updated_at": "2020-01-28T22:17:31.19435Z", - "banned": false, - "online": false, - "image": "https://randomuser.me/api/portraits/women/2.jpg", - "name": "Mia Denys" - }, - "last_read": "2020-01-28T22:17:31.016937728Z" - }, - { - "user": { - "id": "spring-voice-7", - "role": "user", - "created_at": "2020-01-28T22:17:30.834135Z", - "updated_at": "2020-01-28T22:17:31.186771Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", - "name": "Spring voice" - }, - "last_read": "2020-01-28T22:17:31.018856448Z" - } - ], - "members": [ - { - "user": { - "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", - "role": "user", - "created_at": "2020-01-28T22:17:30.83015Z", - "updated_at": "2020-01-28T22:17:31.19435Z", - "banned": false, - "online": false, - "image": "https://randomuser.me/api/portraits/women/2.jpg", - "name": "Mia Denys" - }, - "role": "member", - "created_at": "2020-01-28T22:17:31.005135Z", - "updated_at": "2020-01-28T22:17:31.005135Z" - }, - { - "user": { - "id": "spring-voice-7", - "role": "user", - "created_at": "2020-01-28T22:17:30.834135Z", - "updated_at": "2020-01-28T22:17:31.186771Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", - "name": "Spring voice" - }, - "role": "owner", - "created_at": "2020-01-28T22:17:31.005135Z", - "updated_at": "2020-01-28T22:17:31.005135Z" - } - ] - } - ''', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - final response = await channelClient.query(options: options); - - verify(() => mockDio.post( - '/channels/messaging/query', - data: options, - )).called(1); - expect(channelClient.id, response.channel?.id); - expect(channelClient.cid, response.channel?.cid); - }); - - test('with id', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - tokenProvider: (_) async => '', - ); - final channelClient = client.channel('messaging', id: 'testid'); - final options = {'state': false}; - - when(() => mockDio.post('/channels/messaging/testid/query', - data: options)).thenAnswer( - (_) async => Response( - data: r''' - { - "channel": { - "id": "!members-0LOcD0mZtTan60zHobLmELjdndXsonnBVNzZnB5mTt0", - "type": "messaging", - "cid": "messaging:!members-0LOcD0mZtTan60zHobLmELjdndXsonnBVNzZnB5mTt0", - "last_message_at": "2020-01-28T22:17:31.204287Z", - "created_at": "2020-01-28T22:17:31.00187Z", - "updated_at": "2020-01-28T22:17:31.00187Z", - "created_by": { - "id": "spring-voice-7", - "role": "user", - "created_at": "2020-01-28T22:17:30.834135Z", - "updated_at": "2020-01-28T22:17:31.186771Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", - "name": "Spring voice" - }, - "frozen": false, - "member_count": 2, - "config": { - "created_at": "2020-01-29T12:59:14.291912835Z", - "updated_at": "2020-01-29T12:59:14.291912991Z", - "name": "messaging", - "typing_events": true, - "read_events": true, - "connect_events": true, - "search": true, - "reactions": true, - "replies": true, - "mutes": true, - "uploads": true, - "url_enrichment": true, - "message_retention": "infinite", - "max_message_length": 5000, - "automod": "disabled", - "automod_behavior": "flag", - "commands": [ - { - "name": "giphy", - "description": "Post a random gif to the channel", - "args": "[text]", - "set": "fun_set" - } - ] - }, - "name": "Mia Denys", - "image": "https://randomuser.me/api/portraits/women/2.jpg" - }, - "messages": [ - { - "id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f", - "text": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA", - "html": "\u003cp\u003e\u003ca href=\"https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA\" rel=\"nofollow\"\u003ehttps://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA\u003c/a\u003e\u003c/p\u003e\n", - "type": "regular", - "user": { - "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", - "role": "user", - "created_at": "2020-01-28T22:17:30.83015Z", - "updated_at": "2020-01-28T22:17:31.19435Z", - "banned": false, - "online": false, - "image": "https://randomuser.me/api/portraits/women/2.jpg", - "name": "Mia Denys" - }, - "attachments": [ - { - "type": "video", - "author_name": "GIPHY", - "title": "The Lion King Disney GIF - Find \u0026 Share on GIPHY", - "title_link": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", - "text": "Discover \u0026 share this Lion King Live Action GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.", - "image_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", - "thumb_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", - "asset_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.mp4", - "og_scrape_url": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA" - } - ], - "latest_reactions": [ - { - "message_id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f", - "user_id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", - "user": { - "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", - "role": "user", - "created_at": "2020-01-28T22:17:30.83015Z", - "updated_at": "2020-01-28T22:17:31.19435Z", - "banned": false, - "online": false, - "image": "https://randomuser.me/api/portraits/women/2.jpg", - "name": "Mia Denys" - }, - "type": "love", - "score": 1, - "created_at": "2020-01-28T22:17:31.128376Z", - "updated_at": "2020-01-28T22:17:31.128376Z" - } - ], - "own_reactions": [], - "reaction_counts": { - "love": 1 - }, - "reaction_scores": { - "love": 1 - }, - "reply_count": 0, - "created_at": "2020-01-28T22:17:31.107978Z", - "updated_at": "2020-01-28T22:17:31.130506Z", - "mentioned_users": [] - }, - { - "id": "16e19c46-fb96-4f89-8031-d5bd2ce3bd64", - "text": "Few can name a topfull mother that isn't a breezeless damage.", - "html": "\u003cp\u003eFew can name a topfull mother that isn’t a breezeless damage.\u003c/p\u003e\n", - "type": "regular", - "user": { - "id": "spring-voice-7", - "role": "user", - "created_at": "2020-01-28T22:17:30.834135Z", - "updated_at": "2020-01-28T22:17:31.186771Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", - "name": "Spring voice" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 0, - "created_at": "2020-01-28T22:17:31.153518Z", - "updated_at": "2020-01-28T22:17:31.153518Z", - "mentioned_users": [] - }, - { - "id": "38b1b252-c9a6-4aea-a39e-8de3b7f7604b", - "text": "https://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog", - "html": "\u003cp\u003e\u003ca href=\"https://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog\" rel=\"nofollow\"\u003ehttps://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog\u003c/a\u003e\u003c/p\u003e\n", - "type": "regular", - "user": { - "id": "spring-voice-7", - "role": "user", - "created_at": "2020-01-28T22:17:30.834135Z", - "updated_at": "2020-01-28T22:17:31.186771Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", - "name": "Spring voice" - }, - "attachments": [ - { - "type": "video", - "author_name": "GIPHY", - "title": "Moustache Thumbs Up GIF - Find \u0026 Share on GIPHY", - "title_link": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.gif", - "text": "Discover \u0026 share this Pouce Leve GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.", - "image_url": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.gif", - "thumb_url": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.gif", - "asset_url": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.mp4", - "og_scrape_url": "https://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog" - } - ], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 0, - "created_at": "2020-01-28T22:17:31.155428Z", - "updated_at": "2020-01-28T22:17:31.155428Z", - "mentioned_users": [] - }, - { - "id": "5c7d0c68-72d5-4027-b6b7-711b342d0fc4", - "text": "The carbons could be said to resemble smartish hoods.", - "html": "\u003cp\u003eThe carbons could be said to resemble smartish hoods.\u003c/p\u003e\n", - "type": "regular", - "user": { - "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", - "role": "user", - "created_at": "2020-01-28T22:17:30.83015Z", - "updated_at": "2020-01-28T22:17:31.19435Z", - "banned": false, - "online": false, - "image": "https://randomuser.me/api/portraits/women/2.jpg", - "name": "Mia Denys" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 1, - "created_at": "2020-01-28T22:17:31.157811Z", - "updated_at": "2020-01-28T22:17:31.157811Z", - "mentioned_users": [] - }, - { - "id": "5b02535a-0c3c-45fa-9cf8-16f840c5123f", - "text": "Their software was, in this moment, a prolix feature.", - "html": "\u003cp\u003eTheir software was, in this moment, a prolix feature.\u003c/p\u003e\n", - "type": "regular", - "user": { - "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", - "role": "user", - "created_at": "2020-01-28T22:17:30.83015Z", - "updated_at": "2020-01-28T22:17:31.19435Z", - "banned": false, - "online": false, - "image": "https://randomuser.me/api/portraits/women/2.jpg", - "name": "Mia Denys" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 1, - "created_at": "2020-01-28T22:17:31.158391Z", - "updated_at": "2020-01-28T22:17:31.158391Z", - "mentioned_users": [] - } - ], - "watcher_count": 1, - "read": [ - { - "user": { - "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", - "role": "user", - "created_at": "2020-01-28T22:17:30.83015Z", - "updated_at": "2020-01-28T22:17:31.19435Z", - "banned": false, - "online": false, - "image": "https://randomuser.me/api/portraits/women/2.jpg", - "name": "Mia Denys" - }, - "last_read": "2020-01-28T22:17:31.016937728Z" - }, - { - "user": { - "id": "spring-voice-7", - "role": "user", - "created_at": "2020-01-28T22:17:30.834135Z", - "updated_at": "2020-01-28T22:17:31.186771Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", - "name": "Spring voice" - }, - "last_read": "2020-01-28T22:17:31.018856448Z" - } - ], - "members": [ - { - "user": { - "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", - "role": "user", - "created_at": "2020-01-28T22:17:30.83015Z", - "updated_at": "2020-01-28T22:17:31.19435Z", - "banned": false, - "online": false, - "image": "https://randomuser.me/api/portraits/women/2.jpg", - "name": "Mia Denys" - }, - "role": "member", - "created_at": "2020-01-28T22:17:31.005135Z", - "updated_at": "2020-01-28T22:17:31.005135Z" - }, - { - "user": { - "id": "spring-voice-7", - "role": "user", - "created_at": "2020-01-28T22:17:30.834135Z", - "updated_at": "2020-01-28T22:17:31.186771Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", - "name": "Spring voice" - }, - "role": "owner", - "created_at": "2020-01-28T22:17:31.005135Z", - "updated_at": "2020-01-28T22:17:31.005135Z" - } - ] - } - ''', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await channelClient.query(options: options); - - verify(() => mockDio.post('/channels/messaging/testid/query', - data: options)).called(1); - }); - }); - - test('create', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - tokenProvider: (_) async => '', - ); - final channelClient = client.channel('messaging'); - final options = { - 'watch': false, - 'state': false, - 'presence': false, - }; - - when(() => mockDio.post('/channels/messaging/query', - data: options)).thenAnswer( - (_) async => Response( - data: r''' - { - "channel": { - "id": "!members-0LOcD0mZtTan60zHobLmELjdndXsonnBVNzZnB5mTt0", - "type": "messaging", - "cid": "messaging:!members-0LOcD0mZtTan60zHobLmELjdndXsonnBVNzZnB5mTt0", - "last_message_at": "2020-01-28T22:17:31.204287Z", - "created_at": "2020-01-28T22:17:31.00187Z", - "updated_at": "2020-01-28T22:17:31.00187Z", - "created_by": { - "id": "spring-voice-7", - "role": "user", - "created_at": "2020-01-28T22:17:30.834135Z", - "updated_at": "2020-01-28T22:17:31.186771Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", - "name": "Spring voice" - }, - "frozen": false, - "member_count": 2, - "config": { - "created_at": "2020-01-29T12:59:14.291912835Z", - "updated_at": "2020-01-29T12:59:14.291912991Z", - "name": "messaging", - "typing_events": true, - "read_events": true, - "connect_events": true, - "search": true, - "reactions": true, - "replies": true, - "mutes": true, - "uploads": true, - "url_enrichment": true, - "message_retention": "infinite", - "max_message_length": 5000, - "automod": "disabled", - "automod_behavior": "flag", - "commands": [ - { - "name": "giphy", - "description": "Post a random gif to the channel", - "args": "[text]", - "set": "fun_set" - } - ] - }, - "name": "Mia Denys", - "image": "https://randomuser.me/api/portraits/women/2.jpg" - }, - "messages": [ - { - "id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f", - "text": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA", - "html": "\u003cp\u003e\u003ca href=\"https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA\" rel=\"nofollow\"\u003ehttps://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA\u003c/a\u003e\u003c/p\u003e\n", - "type": "regular", - "user": { - "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", - "role": "user", - "created_at": "2020-01-28T22:17:30.83015Z", - "updated_at": "2020-01-28T22:17:31.19435Z", - "banned": false, - "online": false, - "image": "https://randomuser.me/api/portraits/women/2.jpg", - "name": "Mia Denys" - }, - "attachments": [ - { - "type": "video", - "author_name": "GIPHY", - "title": "The Lion King Disney GIF - Find \u0026 Share on GIPHY", - "title_link": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", - "text": "Discover \u0026 share this Lion King Live Action GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.", - "image_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", - "thumb_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", - "asset_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.mp4", - "og_scrape_url": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA" - } - ], - "latest_reactions": [ - { - "message_id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f", - "user_id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", - "user": { - "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", - "role": "user", - "created_at": "2020-01-28T22:17:30.83015Z", - "updated_at": "2020-01-28T22:17:31.19435Z", - "banned": false, - "online": false, - "image": "https://randomuser.me/api/portraits/women/2.jpg", - "name": "Mia Denys" - }, - "type": "love", - "score": 1, - "created_at": "2020-01-28T22:17:31.128376Z", - "updated_at": "2020-01-28T22:17:31.128376Z" - } - ], - "own_reactions": [], - "reaction_counts": { - "love": 1 - }, - "reaction_scores": { - "love": 1 - }, - "reply_count": 0, - "created_at": "2020-01-28T22:17:31.107978Z", - "updated_at": "2020-01-28T22:17:31.130506Z", - "mentioned_users": [] - }, - { - "id": "16e19c46-fb96-4f89-8031-d5bd2ce3bd64", - "text": "Few can name a topfull mother that isn't a breezeless damage.", - "html": "\u003cp\u003eFew can name a topfull mother that isn’t a breezeless damage.\u003c/p\u003e\n", - "type": "regular", - "user": { - "id": "spring-voice-7", - "role": "user", - "created_at": "2020-01-28T22:17:30.834135Z", - "updated_at": "2020-01-28T22:17:31.186771Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", - "name": "Spring voice" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 0, - "created_at": "2020-01-28T22:17:31.153518Z", - "updated_at": "2020-01-28T22:17:31.153518Z", - "mentioned_users": [] - }, - { - "id": "38b1b252-c9a6-4aea-a39e-8de3b7f7604b", - "text": "https://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog", - "html": "\u003cp\u003e\u003ca href=\"https://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog\" rel=\"nofollow\"\u003ehttps://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog\u003c/a\u003e\u003c/p\u003e\n", - "type": "regular", - "user": { - "id": "spring-voice-7", - "role": "user", - "created_at": "2020-01-28T22:17:30.834135Z", - "updated_at": "2020-01-28T22:17:31.186771Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", - "name": "Spring voice" - }, - "attachments": [ - { - "type": "video", - "author_name": "GIPHY", - "title": "Moustache Thumbs Up GIF - Find \u0026 Share on GIPHY", - "title_link": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.gif", - "text": "Discover \u0026 share this Pouce Leve GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.", - "image_url": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.gif", - "thumb_url": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.gif", - "asset_url": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.mp4", - "og_scrape_url": "https://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog" - } - ], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 0, - "created_at": "2020-01-28T22:17:31.155428Z", - "updated_at": "2020-01-28T22:17:31.155428Z", - "mentioned_users": [] - }, - { - "id": "5c7d0c68-72d5-4027-b6b7-711b342d0fc4", - "text": "The carbons could be said to resemble smartish hoods.", - "html": "\u003cp\u003eThe carbons could be said to resemble smartish hoods.\u003c/p\u003e\n", - "type": "regular", - "user": { - "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", - "role": "user", - "created_at": "2020-01-28T22:17:30.83015Z", - "updated_at": "2020-01-28T22:17:31.19435Z", - "banned": false, - "online": false, - "image": "https://randomuser.me/api/portraits/women/2.jpg", - "name": "Mia Denys" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 1, - "created_at": "2020-01-28T22:17:31.157811Z", - "updated_at": "2020-01-28T22:17:31.157811Z", - "mentioned_users": [] - }, - { - "id": "5b02535a-0c3c-45fa-9cf8-16f840c5123f", - "text": "Their software was, in this moment, a prolix feature.", - "html": "\u003cp\u003eTheir software was, in this moment, a prolix feature.\u003c/p\u003e\n", - "type": "regular", - "user": { - "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", - "role": "user", - "created_at": "2020-01-28T22:17:30.83015Z", - "updated_at": "2020-01-28T22:17:31.19435Z", - "banned": false, - "online": false, - "image": "https://randomuser.me/api/portraits/women/2.jpg", - "name": "Mia Denys" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 1, - "created_at": "2020-01-28T22:17:31.158391Z", - "updated_at": "2020-01-28T22:17:31.158391Z", - "mentioned_users": [] - } - ], - "watcher_count": 1, - "read": [ - { - "user": { - "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", - "role": "user", - "created_at": "2020-01-28T22:17:30.83015Z", - "updated_at": "2020-01-28T22:17:31.19435Z", - "banned": false, - "online": false, - "image": "https://randomuser.me/api/portraits/women/2.jpg", - "name": "Mia Denys" - }, - "last_read": "2020-01-28T22:17:31.016937728Z" - }, - { - "user": { - "id": "spring-voice-7", - "role": "user", - "created_at": "2020-01-28T22:17:30.834135Z", - "updated_at": "2020-01-28T22:17:31.186771Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", - "name": "Spring voice" - }, - "last_read": "2020-01-28T22:17:31.018856448Z" - } - ], - "members": [ - { - "user": { - "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", - "role": "user", - "created_at": "2020-01-28T22:17:30.83015Z", - "updated_at": "2020-01-28T22:17:31.19435Z", - "banned": false, - "online": false, - "image": "https://randomuser.me/api/portraits/women/2.jpg", - "name": "Mia Denys" - }, - "role": "member", - "created_at": "2020-01-28T22:17:31.005135Z", - "updated_at": "2020-01-28T22:17:31.005135Z" - }, - { - "user": { - "id": "spring-voice-7", - "role": "user", - "created_at": "2020-01-28T22:17:30.834135Z", - "updated_at": "2020-01-28T22:17:31.186771Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", - "name": "Spring voice" - }, - "role": "owner", - "created_at": "2020-01-28T22:17:31.005135Z", - "updated_at": "2020-01-28T22:17:31.005135Z" - } - ] - } - ''', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - final response = await channelClient.create(); - - verify(() => mockDio.post('/channels/messaging/query', - data: options)).called(1); - expect(channelClient.id, response.channel?.id); - expect(channelClient.cid, response.channel?.cid); - }); - - test('watch', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - tokenProvider: (_) async => '', - ); - final channelClient = client.channel('messaging'); - final options = { - 'watch': true, - 'state': true, - 'presence': true, - }; - - when(() => mockDio.post('/channels/messaging/query', - data: options)).thenAnswer( - (_) async => Response( - data: r''' - { - "channel": { - "id": "!members-0LOcD0mZtTan60zHobLmELjdndXsonnBVNzZnB5mTt0", - "type": "messaging", - "cid": "messaging:!members-0LOcD0mZtTan60zHobLmELjdndXsonnBVNzZnB5mTt0", - "last_message_at": "2020-01-28T22:17:31.204287Z", - "created_at": "2020-01-28T22:17:31.00187Z", - "updated_at": "2020-01-28T22:17:31.00187Z", - "created_by": { - "id": "spring-voice-7", - "role": "user", - "created_at": "2020-01-28T22:17:30.834135Z", - "updated_at": "2020-01-28T22:17:31.186771Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", - "name": "Spring voice" - }, - "frozen": false, - "member_count": 2, - "config": { - "created_at": "2020-01-29T12:59:14.291912835Z", - "updated_at": "2020-01-29T12:59:14.291912991Z", - "name": "messaging", - "typing_events": true, - "read_events": true, - "connect_events": true, - "search": true, - "reactions": true, - "replies": true, - "mutes": true, - "uploads": true, - "url_enrichment": true, - "message_retention": "infinite", - "max_message_length": 5000, - "automod": "disabled", - "automod_behavior": "flag", - "commands": [ - { - "name": "giphy", - "description": "Post a random gif to the channel", - "args": "[text]", - "set": "fun_set" - } - ] - }, - "name": "Mia Denys", - "image": "https://randomuser.me/api/portraits/women/2.jpg" - }, - "messages": [ - { - "id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f", - "text": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA", - "html": "\u003cp\u003e\u003ca href=\"https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA\" rel=\"nofollow\"\u003ehttps://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA\u003c/a\u003e\u003c/p\u003e\n", - "type": "regular", - "user": { - "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", - "role": "user", - "created_at": "2020-01-28T22:17:30.83015Z", - "updated_at": "2020-01-28T22:17:31.19435Z", - "banned": false, - "online": false, - "image": "https://randomuser.me/api/portraits/women/2.jpg", - "name": "Mia Denys" - }, - "attachments": [ - { - "type": "video", - "author_name": "GIPHY", - "title": "The Lion King Disney GIF - Find \u0026 Share on GIPHY", - "title_link": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", - "text": "Discover \u0026 share this Lion King Live Action GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.", - "image_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", - "thumb_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", - "asset_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.mp4", - "og_scrape_url": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA" - } - ], - "latest_reactions": [ - { - "message_id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f", - "user_id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", - "user": { - "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", - "role": "user", - "created_at": "2020-01-28T22:17:30.83015Z", - "updated_at": "2020-01-28T22:17:31.19435Z", - "banned": false, - "online": false, - "image": "https://randomuser.me/api/portraits/women/2.jpg", - "name": "Mia Denys" - }, - "type": "love", - "score": 1, - "created_at": "2020-01-28T22:17:31.128376Z", - "updated_at": "2020-01-28T22:17:31.128376Z" - } - ], - "own_reactions": [], - "reaction_counts": { - "love": 1 - }, - "reaction_scores": { - "love": 1 - }, - "reply_count": 0, - "created_at": "2020-01-28T22:17:31.107978Z", - "updated_at": "2020-01-28T22:17:31.130506Z", - "mentioned_users": [] - }, - { - "id": "16e19c46-fb96-4f89-8031-d5bd2ce3bd64", - "text": "Few can name a topfull mother that isn't a breezeless damage.", - "html": "\u003cp\u003eFew can name a topfull mother that isn’t a breezeless damage.\u003c/p\u003e\n", - "type": "regular", - "user": { - "id": "spring-voice-7", - "role": "user", - "created_at": "2020-01-28T22:17:30.834135Z", - "updated_at": "2020-01-28T22:17:31.186771Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", - "name": "Spring voice" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 0, - "created_at": "2020-01-28T22:17:31.153518Z", - "updated_at": "2020-01-28T22:17:31.153518Z", - "mentioned_users": [] - }, - { - "id": "38b1b252-c9a6-4aea-a39e-8de3b7f7604b", - "text": "https://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog", - "html": "\u003cp\u003e\u003ca href=\"https://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog\" rel=\"nofollow\"\u003ehttps://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog\u003c/a\u003e\u003c/p\u003e\n", - "type": "regular", - "user": { - "id": "spring-voice-7", - "role": "user", - "created_at": "2020-01-28T22:17:30.834135Z", - "updated_at": "2020-01-28T22:17:31.186771Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", - "name": "Spring voice" - }, - "attachments": [ - { - "type": "video", - "author_name": "GIPHY", - "title": "Moustache Thumbs Up GIF - Find \u0026 Share on GIPHY", - "title_link": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.gif", - "text": "Discover \u0026 share this Pouce Leve GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.", - "image_url": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.gif", - "thumb_url": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.gif", - "asset_url": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.mp4", - "og_scrape_url": "https://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog" - } - ], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 0, - "created_at": "2020-01-28T22:17:31.155428Z", - "updated_at": "2020-01-28T22:17:31.155428Z", - "mentioned_users": [] - }, - { - "id": "5c7d0c68-72d5-4027-b6b7-711b342d0fc4", - "text": "The carbons could be said to resemble smartish hoods.", - "html": "\u003cp\u003eThe carbons could be said to resemble smartish hoods.\u003c/p\u003e\n", - "type": "regular", - "user": { - "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", - "role": "user", - "created_at": "2020-01-28T22:17:30.83015Z", - "updated_at": "2020-01-28T22:17:31.19435Z", - "banned": false, - "online": false, - "image": "https://randomuser.me/api/portraits/women/2.jpg", - "name": "Mia Denys" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 1, - "created_at": "2020-01-28T22:17:31.157811Z", - "updated_at": "2020-01-28T22:17:31.157811Z", - "mentioned_users": [] - }, - { - "id": "5b02535a-0c3c-45fa-9cf8-16f840c5123f", - "text": "Their software was, in this moment, a prolix feature.", - "html": "\u003cp\u003eTheir software was, in this moment, a prolix feature.\u003c/p\u003e\n", - "type": "regular", - "user": { - "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", - "role": "user", - "created_at": "2020-01-28T22:17:30.83015Z", - "updated_at": "2020-01-28T22:17:31.19435Z", - "banned": false, - "online": false, - "image": "https://randomuser.me/api/portraits/women/2.jpg", - "name": "Mia Denys" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 1, - "created_at": "2020-01-28T22:17:31.158391Z", - "updated_at": "2020-01-28T22:17:31.158391Z", - "mentioned_users": [] - } - ], - "watcher_count": 1, - "read": [ - { - "user": { - "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", - "role": "user", - "created_at": "2020-01-28T22:17:30.83015Z", - "updated_at": "2020-01-28T22:17:31.19435Z", - "banned": false, - "online": false, - "image": "https://randomuser.me/api/portraits/women/2.jpg", - "name": "Mia Denys" - }, - "last_read": "2020-01-28T22:17:31.016937728Z" - }, - { - "user": { - "id": "spring-voice-7", - "role": "user", - "created_at": "2020-01-28T22:17:30.834135Z", - "updated_at": "2020-01-28T22:17:31.186771Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", - "name": "Spring voice" - }, - "last_read": "2020-01-28T22:17:31.018856448Z" - } - ], - "members": [ - { - "user": { - "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", - "role": "user", - "created_at": "2020-01-28T22:17:30.83015Z", - "updated_at": "2020-01-28T22:17:31.19435Z", - "banned": false, - "online": false, - "image": "https://randomuser.me/api/portraits/women/2.jpg", - "name": "Mia Denys" - }, - "role": "member", - "created_at": "2020-01-28T22:17:31.005135Z", - "updated_at": "2020-01-28T22:17:31.005135Z" - }, - { - "user": { - "id": "spring-voice-7", - "role": "user", - "created_at": "2020-01-28T22:17:30.834135Z", - "updated_at": "2020-01-28T22:17:31.186771Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", - "name": "Spring voice" - }, - "role": "owner", - "created_at": "2020-01-28T22:17:31.005135Z", - "updated_at": "2020-01-28T22:17:31.005135Z" - } - ] - } - ''', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - final response = await channelClient.watch({'presence': true}); - - verify(() => mockDio.post('/channels/messaging/query', - data: options)).called(1); - expect(channelClient.id, response.channel?.id); - expect(channelClient.cid, response.channel?.cid); - }); - - test('stopWatching', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - tokenProvider: (_) async => '', - ); - final channelClient = client.channel('messaging', id: 'testid'); - - when( - () => mockDio.post( - '/channels/messaging/testid/stop-watching', - data: {}, - ), - ).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await channelClient.stopWatching(); - - verify(() => mockDio.post( - '/channels/messaging/testid/stop-watching', - data: {}, - )).called(1); - }); - - test('update', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - tokenProvider: (_) async => '', - ); - final channelClient = client.channel('messaging', id: 'testid'); - final channelModel = ChannelModel(cid: 'messaging:testid'); - - when(() => mockDio.post( - any(), - data: any(named: 'data'), - )).thenAnswer((_) async => Response( - data: jsonEncode(ChannelState(channel: channelModel)), - statusCode: 200, - requestOptions: FakeRequestOptions(), - )); - - await channelClient.watch(); - - final message = Message(text: 'test'); - - when( - () => mockDio.post( - '/channels/messaging/testid', - data: { - 'message': message.toJson(), - 'data': {'test': true}, - }, - ), - ).thenAnswer( - (_) async => Response( - data: jsonEncode({ - 'channel': channelModel, - 'message': message, - }), - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await channelClient.update({'test': true}, message); - - verify( - () => mockDio.post('/channels/messaging/testid', data: { - 'message': message.toJson(), - 'data': {'test': true}, - }), - ).called(1); - }); - - test('delete', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - tokenProvider: (_) async => '', - ); - final channelClient = client.channel('messaging', id: 'testid'); - - when( - () => mockDio.delete('/channels/messaging/testid'), - ).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await channelClient.delete(); - - verify(() => mockDio.delete('/channels/messaging/testid')) - .called(1); - }); - - test('truncate', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - tokenProvider: (_) async => '', - ); - final channelClient = client.channel('messaging', id: 'testid'); - - when( - () => mockDio.post('/channels/messaging/testid/truncate'), - ).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await channelClient.truncate(); - - verify(() => - mockDio.post('/channels/messaging/testid/truncate')) - .called(1); - }); - - test('rejectInvite', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - tokenProvider: (_) async => '', - ); - final channelClient = client.channel('messaging', id: 'testid'); - final channelModel = ChannelModel(cid: 'messaging:testid'); - - when(() => mockDio.post( - any(), - data: any(named: 'data'), - )).thenAnswer((_) async => Response( - data: jsonEncode(ChannelState(channel: channelModel)), - statusCode: 200, - requestOptions: FakeRequestOptions(), - )); - - await channelClient.watch(); - - final message = Message(text: 'test'); - - when( - () => mockDio.post( - '/channels/messaging/testid', - data: {'reject_invite': true, 'message': message.toJson()}, - ), - ).thenAnswer( - (_) async => Response( - data: jsonEncode({ - 'message': message, - 'channel': channelModel, - }), - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await channelClient.rejectInvite(message); - - verify(() => mockDio.post('/channels/messaging/testid', - data: {'reject_invite': true, 'message': message.toJson()})) - .called(1); - }); - - test('inviteMembers', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - tokenProvider: (_) async => '', - ); - final channelClient = client.channel('messaging', id: 'testid'); - final channelModel = ChannelModel(cid: 'messaging:testid'); - - when(() => mockDio.post( - any(), - data: any(named: 'data'), - )).thenAnswer((_) async => Response( - data: jsonEncode(ChannelState(channel: channelModel)), - statusCode: 200, - requestOptions: FakeRequestOptions(), - )); - - await channelClient.watch(); - - final members = [Member(userId: 'vishal')]; - final memberIds = members.map((e) => e.userId!).toList(); - final message = Message(text: 'test'); - - when( - () => mockDio.post( - '/channels/messaging/testid', - data: {'invites': memberIds, 'message': message.toJson()}, - ), - ).thenAnswer( - (_) async => Response( - data: jsonEncode({ - 'members': members, - 'message': message, - 'channel': channelModel, - }), - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await channelClient.inviteMembers(memberIds, message); - - verify(() => mockDio.post('/channels/messaging/testid', - data: {'invites': memberIds, 'message': message.toJson()})) - .called(1); - }); - - test('removeMembers', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - tokenProvider: (_) async => '', - ); - final channelClient = client.channel('messaging', id: 'testid'); - final channelModel = ChannelModel(cid: 'messaging:testid'); - - when(() => mockDio.post( - any(), - data: any(named: 'data'), - )).thenAnswer((_) async => Response( - data: jsonEncode(ChannelState(channel: channelModel)), - statusCode: 200, - requestOptions: FakeRequestOptions(), - )); - - await channelClient.watch(); - - final members = [Member(userId: 'vishal')]; - final memberIds = members.map((e) => e.userId!).toList(); - final message = Message(text: 'test'); - - when( - () => mockDio.post( - '/channels/messaging/testid', - data: {'remove_members': memberIds, 'message': message.toJson()}, - ), - ).thenAnswer( - (_) async => Response( - data: jsonEncode({ - 'members': members, - 'message': message, - 'channel': channelModel, - }), - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await channelClient.removeMembers(memberIds, message); - - verify(() => mockDio.post('/channels/messaging/testid', data: { - 'remove_members': memberIds, - 'message': message.toJson() - })).called(1); - }); - - test('hide', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - tokenProvider: (_) async => '', - ); - final channelClient = client.channel('messaging', id: 'testid'); - - when( - () => mockDio.post( - any(), - data: any(named: 'data'), - ), - ).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - await channelClient.watch(); - - when( - () => mockDio.post( - '/channels/messaging/testid/hide', - data: {'clear_history': true}, - ), - ).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await channelClient.hide(clearHistory: true); - - verify(() => mockDio.post('/channels/messaging/testid/hide', - data: {'clear_history': true})).called(1); - }); - - test('show', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - tokenProvider: (_) async => '', - ); - final channelClient = client.channel('messaging', id: 'testid'); - - when( - () => mockDio.post( - any(), - data: any(named: 'data'), - ), - ).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - await channelClient.watch(); - - when( - () => mockDio.post('/channels/messaging/testid/show'), - ).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await channelClient.show(); - - verify(() => mockDio.post('/channels/messaging/testid/show')) - .called(1); - }); - - test('banUser', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - tokenProvider: (_) async => '', - ); - final channelClient = client.channel('messaging', id: 'testid'); - - when( - () => mockDio.post( - any(), - data: any(named: 'data'), - ), - ).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - await channelClient.watch(); - - when( - () => mockDio.post('/moderation/ban', data: { - 'test': true, - 'target_user_id': 'test-id', - 'type': 'messaging', - 'id': 'testid', - }), - ).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - final options = {'test': true}; - await channelClient.banUser('test-id', options); - - verify(() => mockDio.post('/moderation/ban', data: { - 'test': true, - 'target_user_id': 'test-id', - 'type': 'messaging', - 'id': 'testid', - })).called(1); - }); - - test('unbanUser', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - tokenProvider: (_) async => '', - ); - final channelClient = client.channel('messaging', id: 'testid'); - - when( - () => mockDio.post( - any(), - data: any(named: 'data'), - ), - ).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - await channelClient.watch(); - - when( - () => mockDio.delete( - '/moderation/ban', - queryParameters: { - 'target_user_id': 'test-id', - 'type': 'messaging', - 'id': 'testid', - }, - ), - ).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await channelClient.unbanUser('test-id'); - - verify( - () => mockDio.delete('/moderation/ban', queryParameters: { - 'target_user_id': 'test-id', - 'type': 'messaging', - 'id': 'testid', - }), - ).called(1); - }); - }); - }); -} diff --git a/packages/stream_chat/test/src/api/web_socket_stub_test.dart b/packages/stream_chat/test/src/api/web_socket_stub_test.dart deleted file mode 100644 index 77a67421..00000000 --- a/packages/stream_chat/test/src/api/web_socket_stub_test.dart +++ /dev/null @@ -1,11 +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( - () => openConnection('fakeurl'), - throwsA(isA()), - ); - }); -} diff --git a/packages/stream_chat/test/src/api/websocket_test.dart b/packages/stream_chat/test/src/api/websocket_test.dart deleted file mode 100644 index 11270a9e..00000000 --- a/packages/stream_chat/test/src/api/websocket_test.dart +++ /dev/null @@ -1,348 +0,0 @@ -import 'dart:async'; - -import 'package:logging/logging.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat/src/ws/connection_status.dart'; -import 'package:stream_chat/src/ws/websocket.dart'; -import 'package:stream_chat/src/core/models/event.dart'; -import 'package:stream_chat/src/core/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 { - WebSocketChannel connectFunc( - String? url, { - Iterable? protocols, - }) => - WebSocketChannel.connect(Uri()); - - void handleFunc(Event event) {} -} - -class MockFunctions extends Mock implements Functions {} - -class MockWSChannel extends Mock implements WebSocketChannel {} - -class MockWSSink extends Mock implements WebSocketSink {} - -class FakeEvent extends Fake implements Event {} - -void main() { - group('src/api/websocket', () { - setUpAll(() { - registerFallbackValue(FakeEvent()); - }); - - test('should connect with correct parameters', () async { - final connectFunc = MockFunctions().connectFunc; - final ws = WebSocket( - baseUrl: 'baseurl', - user: User(id: 'testid'), - _logger: Logger('ws'), - connectParams: {'test': 'true'}, - connectPayload: {'payload': 'test'}, - handler: print, - connectFunc: connectFunc, - ); - final mockWSChannel = MockWSChannel(); - final streamController = StreamController.broadcast(); - 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); - when(() => mockWSChannel.sink).thenAnswer((_) => MockWSSink()); - when(() => mockWSChannel.stream).thenAnswer( - (_) => streamController.stream, - ); - - final timer = Timer.periodic( - const Duration(milliseconds: 100), - (_) => streamController.sink.add('{}'), - ); - - await ws.connect(); - - verify(() => connectFunc(computedUrl)).called(1); - expect(ws.connectionStatus, ConnectionStatus.connected); - - await streamController.close(); - timer.cancel(); - }); - }); - - test('should connect with correct parameters and handle events', () async { - final handleFunc = MockFunctions().handleFunc; - final 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 = StreamController.broadcast(); - 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); - when(() => mockWSChannel.sink).thenAnswer((_) => MockWSSink()); - when(() => mockWSChannel.stream).thenAnswer((_) => streamController.stream); - - final connect = ws.connect().then((_) { - streamController.sink.add('{}'); - return Future.delayed(const Duration(milliseconds: 200)); - }).then((value) { - verify(() => connectFunc(computedUrl)).called(1); - verify(() => handleFunc(any())).called(greaterThan(0)); - - return streamController.close(); - }); - - streamController.sink.add('{}'); - - return connect; - }); - - test('should close correctly the controller', () async { - final handleFunc = MockFunctions().handleFunc; - final 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 = StreamController.broadcast(); - 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'; - - final mockWSSink = MockWSSink(); - when(() => mockWSChannel.sink).thenAnswer((_) => mockWSSink); - when(() => mockWSSink.close(any(), any())).thenAnswer((_) async => null); - when(() => connectFunc(computedUrl)).thenAnswer((_) => mockWSChannel); - when(() => mockWSChannel.stream).thenAnswer((_) => streamController.stream); - - final connect = ws.connect().then((_) { - streamController.sink.add('{}'); - return Future.delayed(const Duration(milliseconds: 200)); - }).then((value) { - verify(() => connectFunc(computedUrl)).called(1); - verify(() => handleFunc(any())).called(greaterThan(0)); - - return streamController.close(); - }); - - streamController.sink.add('{}'); - - return connect; - }); - - test('should close correctly the controller while connecting', () async { - final handleFunc = MockFunctions().handleFunc; - - final 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 = StreamController.broadcast(); - - 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'; - - final mockWSSink = MockWSSink(); - when(() => mockWSChannel.sink).thenAnswer((_) => mockWSSink); - when(() => mockWSSink.close(any(), any())).thenAnswer((_) async => null); - when(() => connectFunc(computedUrl)).thenAnswer((_) => mockWSChannel); - when(() => mockWSChannel.stream).thenAnswer((_) => streamController.stream); - - ws.connect(); - await ws.disconnect(); - streamController.add('{}'); - - verify(() => connectFunc(computedUrl)).called(1); - verifyNever(() => handleFunc(any())); - - addTearDown(streamController.close); - }); - - test('should run correctly health check', () async { - final handleFunc = MockFunctions().handleFunc; - final 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 mockWSSink = MockWSSink(); - final streamController = StreamController.broadcast(); - 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); - when(() => mockWSChannel.stream).thenAnswer((_) => streamController.stream); - when(() => mockWSChannel.sink).thenAnswer((_) => mockWSSink); - when(() => mockWSSink.close(any(), any())).thenAnswer((_) async => null); - - final timer = Timer.periodic( - const Duration(milliseconds: 1000), - (_) => streamController.sink.add('{}'), - ); - - final connect = ws.connect().then((_) { - streamController.sink.add('{}'); - return Future.delayed(const Duration(milliseconds: 200)); - }).then((value) async { - verify(() => mockWSSink.add("{'type': 'health.check'}")) - .called(greaterThan(0)); - - timer.cancel(); - await streamController.close(); - return mockWSSink.close(); - }); - - streamController.sink.add('{}'); - - return connect; - }); - - test('should run correctly reconnection check', () async { - final handleFunc = MockFunctions().handleFunc; - final connectFunc = MockFunctions().connectFunc; - Logger.root.level = Level.ALL; - final ws = WebSocket( - baseUrl: 'baseurl', - user: User(id: 'testid'), - _logger: Logger('ws'), - connectParams: {'test': 'true'}, - connectPayload: {'payload': 'test'}, - handler: handleFunc, - connectFunc: connectFunc, - reconnectionMonitorTimeout: 1, - ); - final mockWSChannel = MockWSChannel(); - final mockWSSink = MockWSSink(); - var streamController = StreamController.broadcast(); - 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); - when(() => mockWSChannel.stream).thenAnswer((_) => streamController.stream); - when(() => mockWSChannel.sink).thenAnswer((_) => mockWSSink); - when(() => mockWSSink.close(any(), any())).thenAnswer((_) async => null); - - final connect = ws.connect().then((_) { - streamController.sink.add('{}'); - streamController.close(); - streamController = StreamController.broadcast(); - streamController.sink.add('{}'); - return Future.delayed(const Duration(milliseconds: 200)); - }).then((value) async { - verify(() => mockWSSink.add("{'type': 'health.check'}")) - .called(greaterThan(0)); - - verify(() => connectFunc(computedUrl)).called(2); - - await streamController.close(); - return mockWSSink.close(); - }); - - streamController.sink.add('{}'); - - return connect; - }); - - test('should close correctly the controller', () async { - final handleFunc = MockFunctions().handleFunc; - final 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 mockWSSink = MockWSSink(); - final streamController = StreamController.broadcast(); - 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); - when(() => mockWSChannel.stream).thenAnswer((_) => streamController.stream); - when(() => mockWSChannel.sink).thenAnswer((_) => mockWSSink); - when(() => mockWSSink.close(any(), any())).thenAnswer((_) async => null); - - final connect = ws.connect().then((_) { - streamController.sink.add('{}'); - return Future.delayed(const Duration(milliseconds: 200)); - }).then((value) async { - await ws.disconnect(); - verify(mockWSSink.close).called(greaterThan(0)); - - await streamController.close(); - await mockWSSink.close(); - }); - - streamController.sink.add('{}'); - - return connect; - }); - - test('should throw an error', () async { - final connectFunc = MockFunctions().connectFunc; - final ws = WebSocket( - baseUrl: 'baseurl', - user: User(id: 'testid'), - _logger: Logger('ws'), - connectParams: {'test': 'true'}, - connectPayload: {'payload': 'test'}, - handler: print, - connectFunc: connectFunc, - ); - final mockWSChannel = MockWSChannel(); - final streamController = StreamController.broadcast(); - 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); - when(() => mockWSChannel.sink).thenAnswer((_) => MockWSSink()); - when(() => mockWSChannel.stream).thenAnswer((_) => streamController.stream); - - Future.delayed( - const Duration(milliseconds: 1000), - () => streamController.sink.addError('test error'), - ); - - try { - expect(await ws.connect(), throwsA(isA())); - } catch (e) { - verify(() => connectFunc(computedUrl)).called(greaterThanOrEqualTo(1)); - streamController.close(); - } - }); -} diff --git a/packages/stream_chat/test/src/client_test.dart b/packages/stream_chat/test/src/client_test.dart deleted file mode 100644 index 13506f66..00000000 --- a/packages/stream_chat/test/src/client_test.dart +++ /dev/null @@ -1,1205 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:dio/dio.dart'; -import 'package:dio/native_imp.dart'; -import 'package:logging/logging.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat/src/core/api/requests.dart'; -import 'package:stream_chat/src/client.dart'; -import 'package:stream_chat/src/core/models/channel_model.dart'; -import 'package:stream_chat/src/core/models/filter.dart'; -import 'package:stream_chat/src/core/models/message.dart'; -import 'package:stream_chat/src/core/models/user.dart'; -import 'package:test/test.dart'; - -class MockDio extends Mock implements DioForNative {} - -class FakeRequestOptions extends Fake implements RequestOptions {} - -class MockHttpClientAdapter extends Mock implements HttpClientAdapter {} - -class Functions { - Future tokenProvider(String userId) async => ''; -} - -class MockFunctions extends Mock implements Functions {} - -void main() { - group('src/client', () { - setUpAll(() { - registerFallbackValue(FakeRequestOptions()); - registerFallbackValue>(const Stream.empty()); - registerFallbackValue>(Future.value()); - }); - - group('constructor', () { - final log = []; - - dynamic overridePrint(testFn()) => () { - log.clear(); - final spec = ZoneSpecification(print: (_, __, ___, String msg) { - // Add to log instead of printing to stdout - log.add(msg); - }); - return Zone.current.fork(specification: spec).run(testFn); - }; - - tearDown(log.clear); - - test('should create the object correctly', () { - final client = StreamChatClient('api-key'); - - expect(client.baseURL, 'chat-us-east-1.stream-io-api.com'); - expect(client.apiKey, 'api-key'); - expect(client.logLevel, Level.WARNING); - expect(client.httpClient._options.connectTimeout, 6000); - expect(client.httpClient._options.receiveTimeout, 6000); - }); - - test('should create the object correctly', overridePrint(() { - void logHandler(LogRecord record) { - print(record.message); - } - - final client = StreamChatClient( - 'api-key', - connectTimeout: const Duration(seconds: 10), - receiveTimeout: const Duration(seconds: 12), - logLevel: Level.INFO, - baseURL: 'test.com', - logHandlerFunction: logHandler, - ); - - expect(client.baseURL, 'test.com'); - expect(client.apiKey, 'api-key'); - expect(Logger.root.level, Level.INFO); - expect(client.httpClient._options.connectTimeout, 10000); - expect(client.httpClient._options.receiveTimeout, 12000); - - client.logger.warning('test'); - client.logger.config('test config'); - - expect( - [log[log.length - 2], log[log.length - 1]], - ['instantiating new client', 'test'], - ); - })); - - test('Channel', () { - final client = StreamChatClient('test'); - final data = {'test': 1}; - final channelClient = client.channel('type', id: 'id', extraData: data); - expect(channelClient.type, 'type'); - expect(channelClient.id, 'id'); - }); - }); - - group('queryChannelsOnline', () { - test('should pass right default parameters', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient('api-key', httpClient: mockDio); - final queryParams = { - 'payload': json.encode({ - 'filter_conditions': null, - 'sort': null, - 'state': true, - 'watch': true, - 'presence': false, - 'limit': 10, - 'offset': 0, - }), - }; - - when( - () => mockDio.get('/channels', queryParameters: queryParams), - ).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await client.queryChannelsOnline(waitForConnect: false); - - verify(() => - mockDio.get('/channels', queryParameters: queryParams)) - .called(1); - }); - - test('should pass right parameters', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient('api-key', httpClient: mockDio); - final queryFilter = Filter.in_('id', const ['test']); - final sortOptions = >[]; - final options = {'state': false, 'watch': false, 'presence': true}; - const paginationParams = PaginationParams(offset: 2); - - final queryParams = { - 'payload': json.encode({ - 'filter_conditions': queryFilter, - 'sort': sortOptions, - } - ..addAll(options) - ..addAll(paginationParams - .toJson() - .map((key, value) => MapEntry(key, value as Object)))), - }; - - when( - () => mockDio.get('/channels', queryParameters: queryParams), - ).thenAnswer( - (_) async => Response( - data: '{"channels":[]}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await client.queryChannelsOnline( - filter: queryFilter, - sort: sortOptions, - _options: options, - paginationParams: paginationParams, - waitForConnect: false, - ); - - verify(() => - mockDio.get('/channels', queryParameters: queryParams)) - .called(1); - }); - }); - - group('search', () { - test('should pass right default parameters', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient('api-key', httpClient: mockDio); - final filter = Filter.in_('cid', const ['messaging:testId']); - const query = 'hello'; - final queryParams = { - 'payload': json.encode({ - 'filter_conditions': filter, - 'query': query, - }), - }; - - when( - () => mockDio.get('/search', queryParameters: queryParams), - ).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await client.search(filter, query: query); - - verify(() => - mockDio.get('/search', queryParameters: queryParams)) - .called(1); - }); - - test('should pass right parameters', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient('api-key', httpClient: mockDio); - final filters = Filter.in_('id', const ['test']); - const sortOptions = [SortOption('name')]; - const query = 'query'; - final queryParams = { - 'payload': json.encode({ - 'filter_conditions': filters, - 'query': query, - 'sort': sortOptions, - 'limit': 10, - 'offset': 0, - }), - }; - - when( - () => mockDio.get('/search', queryParameters: queryParams), - ).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await client.search( - filters, - sort: sortOptions, - query: query, - paginationParams: const PaginationParams(), - ); - - verify( - () => mockDio.get('/search', queryParameters: queryParams), - ).called(1); - }); - }); - - group('devices', () { - test('addDevice', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient('api-key', httpClient: mockDio); - - when( - () => mockDio.post('/devices', data: { - 'id': 'test-id', - 'push_provider': 'firebase', - }), - ).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await client.addDevice('test-id', PushProvider.firebase); - - verify( - () => mockDio.post( - '/devices', - data: {'id': 'test-id', 'push_provider': 'firebase'}, - ), - ).called(1); - }); - - test('getDevices', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient('api-key', httpClient: mockDio); - - when(() => mockDio.get('/devices')).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await client.getDevices(); - - verify(() => mockDio.get('/devices')).called(1); - }); - - test('removeDevice', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient('api-key', httpClient: mockDio); - - when( - () => mockDio.delete( - '/devices', - queryParameters: {'id': 'test-id'}, - ), - ).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await client.removeDevice('test-id'); - - verify( - () => mockDio.delete( - '/devices', - queryParameters: {'id': 'test-id'}, - ), - ).called(1); - }); - }); - - test('devToken', () { - final client = StreamChatClient('api-key'); - final token = client.devToken('test'); - - expect( - token, - '''eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidGVzdCJ9.devtoken''', - ); - }); - - group('queryUsers', () { - test('should pass right default parameters', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient('api-key', httpClient: mockDio); - final queryParams = { - 'payload': json.encode({ - 'filter_conditions': null, - 'sort': null, - 'presence': false, - }), - }; - - when( - () => mockDio.get('/users', queryParameters: queryParams), - ).thenAnswer( - (_) async => Response( - data: '{"users":[]}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await client.queryUsers(); - - verify( - () => mockDio.get( - '/users', - queryParameters: queryParams, - ), - ).called(1); - }); - - test('should pass right parameters', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient('api-key', httpClient: mockDio); - final queryFilter = Filter.in_('id', const ['test']); - const sortOptions = []; - final options = {'presence': true}; - final queryParams = { - 'payload': json.encode({ - 'filter_conditions': queryFilter, - 'sort': sortOptions, - }..addAll(options)), - }; - - when( - () => mockDio.get('/users', queryParameters: queryParams), - ).thenAnswer( - (_) async => Response( - data: '{"users":[]}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await client.queryUsers( - filter: queryFilter, - sort: sortOptions, - _options: options, - ); - - verify(() => - mockDio.get('/users', queryParameters: queryParams)) - .called(1); - }); - }); - - group('user', () { - test('connectUser should throw exception', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient('api-key', httpClient: mockDio); - - when( - () => mockDio.post( - '/moderation/flag', - data: {'target_user_id': 'test-id'}, - ), - ).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await client.flagUser('test-id'); - - verify(() => mockDio.post('/moderation/flag', - data: {'target_user_id': 'test-id'})).called(1); - }); - - test('flagUser', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient('api-key', httpClient: mockDio); - - expect(() => client.connectUserWithProvider(User(id: 'test-id')), - throwsA(isA())); - }); - - test('unflagUser', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient('api-key', httpClient: mockDio); - - when( - () => mockDio.post( - '/moderation/unflag', - data: {'target_user_id': 'test-id'}, - ), - ).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await client.unflagUser('test-id'); - - verify(() => mockDio.post('/moderation/unflag', - data: {'target_user_id': 'test-id'})).called(1); - }); - - test('updateUser', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient('api-key', httpClient: mockDio); - final user = User(id: 'test-id'); - final data = { - 'users': {user.id: user.toJson()}, - }; - - when(() => mockDio.post('/users', data: data)).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await client.updateUser(user); - - verify(() => mockDio.post('/users', data: data)).called(1); - }); - - test('updateUsers', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient('api-key', httpClient: mockDio); - final user = User(id: 'test-id'); - final user2 = User(id: 'test-id2'); - - final data = { - 'users': { - user.id: user.toJson(), - user2.id: user2.toJson(), - }, - }; - - when(() => mockDio.post('/users', data: data)).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await client.updateUsers([user, user2]); - - verify(() => mockDio.post('/users', data: data)).called(1); - }); - - test('banUser', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient('api-key', httpClient: mockDio); - - when( - () => mockDio.post( - '/moderation/ban', - data: {'test': true, 'target_user_id': 'test-id'}, - ), - ).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await client.banUser('test-id', {'test': true}); - - verify(() => mockDio.post('/moderation/ban', - data: {'test': true, 'target_user_id': 'test-id'})).called(1); - }); - - test('unbanUser', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient('api-key', httpClient: mockDio); - - when( - () => mockDio.delete( - '/moderation/ban', - queryParameters: {'test': true, 'target_user_id': 'test-id'}, - ), - ).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await client.unbanUser('test-id', {'test': true}); - - verify(() => mockDio.delete('/moderation/ban', - queryParameters: {'test': true, 'target_user_id': 'test-id'})) - .called(1); - }); - - test('muteUser', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient('api-key', httpClient: mockDio); - - when( - () => mockDio.post( - '/moderation/mute', - data: {'target_id': 'test-id'}, - ), - ).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await client.muteUser('test-id'); - - verify(() => mockDio.post('/moderation/mute', - data: {'target_id': 'test-id'})).called(1); - }); - - test('unmuteUser', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient('api-key', httpClient: mockDio); - - when( - () => mockDio.post( - '/moderation/unmute', - data: {'target_id': 'test-id'}, - ), - ).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await client.unmuteUser('test-id'); - - verify(() => mockDio.post('/moderation/unmute', - data: {'target_id': 'test-id'})).called(1); - }); - }); - - group('message', () { - test('flagMessage', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient('api-key', httpClient: mockDio); - - when( - () => mockDio.post( - '/moderation/flag', - data: {'target_message_id': 'test-id'}, - ), - ).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await client.flagMessage('test-id'); - - verify(() => mockDio.post('/moderation/flag', - data: {'target_message_id': 'test-id'})).called(1); - }); - - test('unflagMessage', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient('api-key', httpClient: mockDio); - - when( - () => mockDio.post( - '/moderation/unflag', - data: {'target_message_id': 'test-id'}, - ), - ).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await client.unflagMessage('test-id'); - - verify(() => mockDio.post('/moderation/unflag', - data: {'target_message_id': 'test-id'})).called(1); - }); - - test('updateMessage', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient('api-key', httpClient: mockDio); - final message = Message( - id: 'test', - ); - - when( - () => mockDio.post( - '/messages/${message.id}', - data: {'message': anything}, - ), - ).thenAnswer( - (_) async => Response( - data: jsonEncode({'message': message}), - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await client.updateMessage(message); - - verify(() => mockDio.post('/messages/${message.id}', - data: {'message': anything})).called(1); - }); - - test('deleteMessage', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient('api-key', httpClient: mockDio); - const messageId = 'test'; - - when(() => mockDio.delete('/messages/$messageId')).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await client.deleteMessage(Message(id: messageId)); - - verify(() => mockDio.delete('/messages/$messageId')).called(1); - }); - - test('getMessage', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient('api-key', httpClient: mockDio); - const messageId = 'test'; - - when(() => mockDio.get('/messages/$messageId')).thenAnswer( - (_) async => Response( - data: jsonEncode({'message': Message(id: messageId)}), - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await client.getMessage(messageId); - - verify(() => mockDio.get('/messages/$messageId')).called(1); - }); - - test('markAllRead', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient('api-key', httpClient: mockDio); - - when(() => mockDio.post('/channels/read')).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await client.markAllRead(); - - verify(() => mockDio.post('/channels/read')).called(1); - }); - }); - - group('api methods', () { - group('get', () { - test('should put the correct parameters', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient('api-key', httpClient: mockDio); - final queryParams = {'test': 1}; - - when( - () => mockDio.get('/test', queryParameters: queryParams), - ).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await client.get('/test', queryParameters: queryParams); - - verify(() => - mockDio.get('/test', queryParameters: queryParams)) - .called(1); - }); - - test('should catch the error', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient('api-key', httpClient: mockDio); - - when(() => mockDio.get(any())).thenThrow( - DioError( - type: DioErrorType.response, - response: Response( - data: 'test error', - statusCode: 400, - requestOptions: FakeRequestOptions(), - ), - requestOptions: FakeRequestOptions(), - ), - ); - - expect( - client.get('/test'), - throwsA(ApiError('test error', 400)), - ); - }); - }); - - group('post', () { - test('should put the correct parameters', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient('api-key', httpClient: mockDio); - final data = {'test': 1}; - - when(() => mockDio.post('/test', data: data)).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await client.post('/test', data: data); - - verify(() => mockDio.post('/test', data: data)).called(1); - }); - - test('should catch the error', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - ); - - when(() => mockDio.post(any())).thenThrow( - DioError( - type: DioErrorType.response, - response: Response( - data: 'test error', - statusCode: 400, - requestOptions: FakeRequestOptions(), - ), - requestOptions: FakeRequestOptions(), - ), - ); - - expect(client.post('/test'), throwsA(ApiError('test error', 400))); - }); - }); - - group('put', () { - test('should put the correct parameters', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient('api-key', httpClient: mockDio); - final data = {'test': 1}; - - when(() => mockDio.put('/test', data: data)).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await client.put('/test', data: data); - - verify(() => mockDio.put('/test', data: data)).called(1); - }); - - test('should catch the error', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient('api-key', httpClient: mockDio); - - when(() => mockDio.put(any())).thenThrow( - DioError( - type: DioErrorType.response, - response: Response( - data: 'test error', - statusCode: 400, - requestOptions: FakeRequestOptions(), - ), - requestOptions: FakeRequestOptions(), - ), - ); - - expect(client.put('/test'), throwsA(ApiError('test error', 400))); - }); - }); - - group('patch', () { - test('should put the correct parameters', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient('api-key', httpClient: mockDio); - - final data = { - 'test': 1, - }; - - when(() => mockDio.patch('/test', data: data)).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await client.patch('/test', data: data); - - verify(() => mockDio.patch('/test', data: data)).called(1); - }); - - test('should catch the error', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient('api-key', httpClient: mockDio); - - when(() => mockDio.patch(any())).thenThrow( - DioError( - type: DioErrorType.response, - response: Response( - data: 'test error', - statusCode: 400, - requestOptions: FakeRequestOptions(), - ), - requestOptions: FakeRequestOptions(), - ), - ); - - expect(client.patch('/test'), throwsA(ApiError('test error', 400))); - }); - }); - - group('delete', () { - test('should put the correct parameters', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - ); - - final queryParams = { - 'test': 1, - }; - - when( - () => mockDio.delete('/test', queryParameters: queryParams), - ).thenAnswer( - (_) async => Response( - data: '{}', - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await client.delete('/test', queryParameters: queryParams); - - verify(() => - mockDio.delete('/test', queryParameters: queryParams)) - .called(1); - }); - - test('should catch the error', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient('api-key', httpClient: mockDio); - - when(() => mockDio.delete(any())).thenThrow( - DioError( - type: DioErrorType.response, - response: Response( - data: 'test error', - statusCode: 400, - requestOptions: FakeRequestOptions(), - ), - requestOptions: FakeRequestOptions(), - ), - ); - - expect(client.delete('/test'), throwsA(ApiError('test error', 400))); - }); - }); - - group('pin message', () { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - ); - - test('should throw argument error', () { - final message = Message(text: 'Hello'); - expect( - () => client.pinMessage(message, 'InvalidType'), - throwsArgumentError, - ); - }); - - test('should complete successfully', () async { - const timeout = 30; - final message = Message(text: 'Hello'); - - when( - () => mockDio.post( - '/messages/${message.id}', - data: anything, - ), - ).thenAnswer( - (_) async => Response( - data: jsonEncode({'message': message}), - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await client.pinMessage(message, timeout); - - verify(() => mockDio.post('/messages/${message.id}', - data: {'message': anything})).called(1); - }); - - test('should complete successfully with a null value', () async { - final message = Message(text: 'Hello'); - - when( - () => mockDio.post( - '/messages/${message.id}', - data: anything, - ), - ).thenAnswer( - (_) async => Response( - data: jsonEncode({'message': message}), - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await client.pinMessage(message); - - verify(() => mockDio.post('/messages/${message.id}', - data: {'message': anything})).called(1); - }); - - test('should unpin message successfully', () async { - final message = Message(text: 'Hello'); - - when( - () => mockDio.post( - '/messages/${message.id}', - data: anything, - ), - ).thenAnswer( - (_) async => Response( - data: jsonEncode({'message': message}), - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await client.unpinMessage(message); - - verify(() => mockDio.post('/messages/${message.id}', - data: anything)).called(1); - }); - }); - }); - - group('channel', () { - test('should update channel', () async { - final mockDio = MockDio(); - - when(() => mockDio.options).thenReturn(BaseOptions()); - when(() => mockDio.interceptors).thenReturn(Interceptors()); - - final client = StreamChatClient( - 'api-key', - httpClient: mockDio, - ); - - final channelClient = client.channel( - 'type', - id: 'id', - extraData: {'name': 'init'}, - ); - - const update = { - 'set': {'name': 'demo'} - }; - - when( - () => mockDio.patch( - '/channels/${channelClient.type}/${channelClient.id}', - data: update, - ), - ).thenAnswer( - (_) async => Response( - data: jsonEncode({'channel': ChannelModel(cid: 'messaging:test')}), - statusCode: 200, - requestOptions: FakeRequestOptions(), - ), - ); - - await channelClient.updatePartial(update); - verify( - () => mockDio.patch( - '/channels/${channelClient.type}/${channelClient.id}', - data: update, - ), - ).called(1); - }); - }); - }); -} diff --git a/packages/stream_chat/test/src/models/action_test.dart b/packages/stream_chat/test/src/models/action_test.dart index 22e66078..d0108948 100644 --- a/packages/stream_chat/test/src/models/action_test.dart +++ b/packages/stream_chat/test/src/models/action_test.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'package:test/test.dart'; -import 'package:stream_chat/src/models/action.dart'; +import 'package:stream_chat/src/core/models/action.dart'; void main() { group('src/models/action', () { diff --git a/packages/stream_chat/test/src/models/attachment_test.dart b/packages/stream_chat/test/src/models/attachment_test.dart index 1342e72e..a433bedd 100644 --- a/packages/stream_chat/test/src/models/attachment_test.dart +++ b/packages/stream_chat/test/src/models/attachment_test.dart @@ -1,7 +1,7 @@ import 'dart:convert'; -import 'package:stream_chat/src/models/action.dart'; -import 'package:stream_chat/src/models/attachment.dart'; +import 'package:stream_chat/src/core/models/action.dart'; +import 'package:stream_chat/src/core/models/attachment.dart'; import 'package:test/test.dart'; void main() { diff --git a/packages/stream_chat/test/src/models/channel_state_test.dart b/packages/stream_chat/test/src/models/channel_state_test.dart index a93166b4..8f8bdf45 100644 --- a/packages/stream_chat/test/src/models/channel_state_test.dart +++ b/packages/stream_chat/test/src/models/channel_state_test.dart @@ -1,10 +1,10 @@ import 'dart:convert'; -import 'package:stream_chat/src/models/channel_config.dart'; -import 'package:stream_chat/src/models/channel_state.dart'; -import 'package:stream_chat/src/models/command.dart'; -import 'package:stream_chat/src/models/message.dart'; -import 'package:stream_chat/src/models/user.dart'; +import 'package:stream_chat/src/core/models/channel_config.dart'; +import 'package:stream_chat/src/core/models/channel_state.dart'; +import 'package:stream_chat/src/core/models/command.dart'; +import 'package:stream_chat/src/core/models/message.dart'; +import 'package:stream_chat/src/core/models/user.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:test/test.dart'; diff --git a/packages/stream_chat/test/src/models/channel_test.dart b/packages/stream_chat/test/src/models/channel_test.dart index 1e69565c..1bb400a6 100644 --- a/packages/stream_chat/test/src/models/channel_test.dart +++ b/packages/stream_chat/test/src/models/channel_test.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:stream_chat/src/models/channel_model.dart'; +import 'package:stream_chat/src/core/models/channel_model.dart'; import 'package:test/test.dart'; void main() { diff --git a/packages/stream_chat/test/src/models/command_test.dart b/packages/stream_chat/test/src/models/command_test.dart index 8fafd192..3306fd0f 100644 --- a/packages/stream_chat/test/src/models/command_test.dart +++ b/packages/stream_chat/test/src/models/command_test.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:stream_chat/src/models/command.dart'; +import 'package:stream_chat/src/core/models/command.dart'; import 'package:test/test.dart'; void main() { diff --git a/packages/stream_chat/test/src/models/device_test.dart b/packages/stream_chat/test/src/models/device_test.dart index 5cbf015d..f4700b7e 100644 --- a/packages/stream_chat/test/src/models/device_test.dart +++ b/packages/stream_chat/test/src/models/device_test.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'package:test/test.dart'; -import 'package:stream_chat/src/models/device.dart'; +import 'package:stream_chat/src/core/models/device.dart'; void main() { group('src/models/device', () { diff --git a/packages/stream_chat/test/src/models/event_test.dart b/packages/stream_chat/test/src/models/event_test.dart index 96efa844..b05da962 100644 --- a/packages/stream_chat/test/src/models/event_test.dart +++ b/packages/stream_chat/test/src/models/event_test.dart @@ -1,7 +1,7 @@ import 'dart:convert'; -import 'package:stream_chat/src/models/event.dart'; -import 'package:stream_chat/src/models/own_user.dart'; +import 'package:stream_chat/src/core/models/event.dart'; +import 'package:stream_chat/src/core/models/own_user.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:test/test.dart'; diff --git a/packages/stream_chat/test/src/models/member_test.dart b/packages/stream_chat/test/src/models/member_test.dart index 25e1d78e..54c258e1 100644 --- a/packages/stream_chat/test/src/models/member_test.dart +++ b/packages/stream_chat/test/src/models/member_test.dart @@ -1,8 +1,8 @@ import 'dart:convert'; import 'package:test/test.dart'; -import 'package:stream_chat/src/models/member.dart'; -import 'package:stream_chat/src/models/user.dart'; +import 'package:stream_chat/src/core/models/member.dart'; +import 'package:stream_chat/src/core/models/user.dart'; void main() { group('src/models/member', () { diff --git a/packages/stream_chat/test/src/models/message_test.dart b/packages/stream_chat/test/src/models/message_test.dart index 1219e137..b3f28ff0 100644 --- a/packages/stream_chat/test/src/models/message_test.dart +++ b/packages/stream_chat/test/src/models/message_test.dart @@ -1,9 +1,9 @@ import 'dart:convert'; -import 'package:stream_chat/src/models/attachment.dart'; -import 'package:stream_chat/src/models/message.dart'; -import 'package:stream_chat/src/models/reaction.dart'; -import 'package:stream_chat/src/models/user.dart'; +import 'package:stream_chat/src/core/models/attachment.dart'; +import 'package:stream_chat/src/core/models/message.dart'; +import 'package:stream_chat/src/core/models/reaction.dart'; +import 'package:stream_chat/src/core/models/user.dart'; import 'package:test/test.dart'; void main() { diff --git a/packages/stream_chat/test/src/models/reaction_test.dart b/packages/stream_chat/test/src/models/reaction_test.dart index 795e2967..b69bd986 100644 --- a/packages/stream_chat/test/src/models/reaction_test.dart +++ b/packages/stream_chat/test/src/models/reaction_test.dart @@ -1,7 +1,7 @@ import 'dart:convert'; -import 'package:stream_chat/src/models/reaction.dart'; -import 'package:stream_chat/src/models/user.dart'; +import 'package:stream_chat/src/core/models/reaction.dart'; +import 'package:stream_chat/src/core/models/user.dart'; import 'package:test/test.dart'; void main() { diff --git a/packages/stream_chat/test/src/models/read_test.dart b/packages/stream_chat/test/src/models/read_test.dart index 66efe809..47c252ac 100644 --- a/packages/stream_chat/test/src/models/read_test.dart +++ b/packages/stream_chat/test/src/models/read_test.dart @@ -1,8 +1,8 @@ import 'dart:convert'; import 'package:test/test.dart'; -import 'package:stream_chat/src/models/read.dart'; -import 'package:stream_chat/src/models/user.dart'; +import 'package:stream_chat/src/core/models/read.dart'; +import 'package:stream_chat/src/core/models/user.dart'; void main() { group('src/models/read', () { diff --git a/packages/stream_chat/test/src/models/serialization_test.dart b/packages/stream_chat/test/src/models/serialization_test.dart index 9e64caad..605b5b58 100644 --- a/packages/stream_chat/test/src/models/serialization_test.dart +++ b/packages/stream_chat/test/src/models/serialization_test.dart @@ -1,5 +1,5 @@ import 'package:test/test.dart'; -import 'package:stream_chat/src/models/serialization.dart'; +import 'package:stream_chat/src/core/util/serializer.dart'; void main() { group('src/models/serialization', () { @@ -9,7 +9,7 @@ void main() { 'prop2': 123, 'prop3': true, }; - final result = Serialization.moveToExtraDataFromRoot(json, [ + final result = Serializer.moveToExtraDataFromRoot(json, [ 'prop1', 'prop2', ]); @@ -30,7 +30,7 @@ void main() { }); test('should have empty extraData', () { - final result = Serialization.moveToExtraDataFromRoot({ + final result = Serializer.moveToExtraDataFromRoot({ 'prop1': 'test', 'prop2': 123, 'prop3': true, @@ -49,7 +49,7 @@ void main() { }); test('should return null', () { - final result = Serialization.moveToExtraDataFromRoot({}, [ + final result = Serializer.moveToExtraDataFromRoot({}, [ 'prop1', 'prop2', ]); diff --git a/packages/stream_chat/test/src/models/user_test.dart b/packages/stream_chat/test/src/models/user_test.dart index 8f56f7e1..f4ff5ee0 100644 --- a/packages/stream_chat/test/src/models/user_test.dart +++ b/packages/stream_chat/test/src/models/user_test.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:stream_chat/src/models/user.dart'; +import 'package:stream_chat/src/core/models/user.dart'; import 'package:test/test.dart'; void main() { From bdfbc279173b4b9ec8eef7c062e57b441f251793 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 4 Jun 2021 17:09:41 +0530 Subject: [PATCH 019/150] add stream_chat_error_test.dart Signed-off-by: Sahil Kumar --- .../lib/src/core/error/stream_chat_error.dart | 3 + .../http/interceptor/logging_interceptor.dart | 1 + .../core/error/stream_chat_error_test.dart | 126 ++++++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 packages/stream_chat/test/src/core/error/stream_chat_error_test.dart diff --git a/packages/stream_chat/lib/src/core/error/stream_chat_error.dart b/packages/stream_chat/lib/src/core/error/stream_chat_error.dart index 04214033..a78e9390 100644 --- a/packages/stream_chat/lib/src/core/error/stream_chat_error.dart +++ b/packages/stream_chat/lib/src/core/error/stream_chat_error.dart @@ -45,6 +45,9 @@ class StreamWebSocketError extends StreamChatError { /// Response body. please refer to [ErrorResponse]. final ErrorResponse? data; + @override + List get props => [...super.props, code]; + @override String toString() { var params = 'message: $message'; diff --git a/packages/stream_chat/lib/src/core/http/interceptor/logging_interceptor.dart b/packages/stream_chat/lib/src/core/http/interceptor/logging_interceptor.dart index 41c36326..1f5687a8 100644 --- a/packages/stream_chat/lib/src/core/http/interceptor/logging_interceptor.dart +++ b/packages/stream_chat/lib/src/core/http/interceptor/logging_interceptor.dart @@ -1,4 +1,5 @@ // ignore_for_file: lines_longer_than_80_chars +// coverage:ignore-file import 'dart:math' as math; diff --git a/packages/stream_chat/test/src/core/error/stream_chat_error_test.dart b/packages/stream_chat/test/src/core/error/stream_chat_error_test.dart new file mode 100644 index 00000000..a32e0d71 --- /dev/null +++ b/packages/stream_chat/test/src/core/error/stream_chat_error_test.dart @@ -0,0 +1,126 @@ +import 'package:dio/dio.dart'; +import 'package:stream_chat/src/core/api/responses.dart'; +import 'package:stream_chat/src/core/error/error.dart'; +import 'package:test/test.dart'; + +void main() { + group('StreamChatError', () { + test('should match if message is same', () { + const message = 'test-error-message'; + const error = StreamChatError(message); + const error2 = StreamChatError(message); + + expect(error, error2); + }); + + test('`.toString`', () { + const message = 'test-error-message'; + const error = StreamChatError(message); + + expect(error.toString(), 'StreamChatError(message: $message)'); + }); + }); + + group('StreamWebSocketError', () { + test('.fromStreamError', () { + final data = ErrorResponse()..code = 333; + final error = StreamWebSocketError.fromStreamError(data.toJson()); + expect(error, isNotNull); + expect(error.code, data.code); + }); + + test('should match if message and data.code is same', () { + const message = 'test-error-message'; + final data = ErrorResponse()..code = 333; + final error = StreamWebSocketError(message, data: data); + final error2 = StreamWebSocketError(message, data: data); + + expect(error, error2); + }); + + test('`.toString`', () { + const message = 'test-error-message'; + final data = ErrorResponse()..code = 333; + final error = StreamWebSocketError(message, data: data); + + expect( + error.toString(), + 'WebSocketError(message: $message, data: $data)', + ); + }); + }); + + group('StreamChatNetworkError', () { + test('.raw', () { + const code = 333; + const message = 'test-error-message'; + final error = StreamChatNetworkError.raw(code: code, message: message); + expect(error, isNotNull); + expect(error.code, code); + expect(error.message, message); + }); + + test('.fromDioError', () { + const code = 333; + const statusCode = 666; + const message = 'test-error-message'; + final options = RequestOptions(path: 'test-path'); + final data = ErrorResponse() + ..code = code + ..statusCode = statusCode + ..message = message; + final dioError = DioError( + requestOptions: options, + response: Response( + requestOptions: options, + statusCode: data.statusCode, + data: data.toJson(), + ), + ); + final error = StreamChatNetworkError.fromDioError(dioError); + expect(error, isNotNull); + expect(error.code, code); + expect(error.message, message); + expect(error.statusCode, statusCode); + expect(error.data?.code, data.code); + expect(error.data?.statusCode, data.statusCode); + expect(error.data?.message, data.message); + }); + + test('should match if message, code and statusCode is same', () { + const code = 333; + const statusCode = 666; + const message = 'test-error-message'; + final error = StreamChatNetworkError.raw( + code: code, + statusCode: statusCode, + message: message, + ); + final error2 = StreamChatNetworkError.raw( + code: code, + statusCode: statusCode, + message: message, + ); + + expect(error, error2); + }); + + test('`.retriable` should return true if data is not present', () { + const errorCode = ChatErrorCode.tokenExpired; + final error = StreamChatNetworkError(errorCode); + + expect(error.isRetriable, isTrue); + }); + + test('`.toString`', () { + const errorCode = ChatErrorCode.tokenExpired; + final error = StreamChatNetworkError(errorCode); + expect( + error.toString(), + 'StreamChatNetworkError(' + 'code: ${errorCode.code}, ' + 'message: ${errorCode.message})', + ); + }); + }); +} From 89f989003d12598c45636cec76bcc7554c57db51 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 8 Jun 2021 16:09:17 +0530 Subject: [PATCH 020/150] add core/api tests Signed-off-by: Sahil Kumar --- .../stream_chat/lib/src/client/client.dart | 6 - .../core/api/attachment_file_uploader.dart | 51 +- .../lib/src/core/api/channel_api.dart | 2 - .../lib/src/core/api/device_api.dart | 3 +- .../lib/src/core/models/attachment_file.dart | 35 +- packages/stream_chat/test/assets/example.pdf | 57 ++ .../stream_chat/test/assets/test_image.jpeg | Bin 0 -> 4429 bytes .../api/attachment_file_uploader_test.dart | 136 +++++ .../test/src/core/api/channel_api_test.dart | 492 ++++++++++++++++++ .../test/src/core/api/device_api_test.dart | 84 +++ .../test/src/core/api/general_api_test.dart | 211 ++++++++ .../test/src/core/api/guest_api_test.dart | 44 ++ .../test/src/core/api/message_api_test.dart | 291 +++++++++++ .../src/core/api/moderation_api_test.dart | 190 +++++++ .../src/core/api/stream_chat_api_test.dart | 49 ++ .../test/src/core/api/user_api_test.dart | 77 +++ ...rt => connection_id_interceptor_test.dart} | 0 .../src/{ => core}/models/action_test.dart | 0 .../{ => core}/models/attachment_test.dart | 0 .../{ => core}/models/channel_state_test.dart | 0 .../src/{ => core}/models/channel_test.dart | 0 .../src/{ => core}/models/command_test.dart | 0 .../src/{ => core}/models/device_test.dart | 0 .../src/{ => core}/models/event_test.dart | 0 .../src/{ => core}/models/filter_test.dart | 0 .../src/{ => core}/models/member_test.dart | 0 .../src/{ => core}/models/message_test.dart | 0 .../src/{ => core}/models/reaction_test.dart | 0 .../test/src/{ => core}/models/read_test.dart | 0 .../{ => core}/models/serialization_test.dart | 0 .../test/src/{ => core}/models/user_test.dart | 0 packages/stream_chat/test/src/fakes.dart | 57 ++ packages/stream_chat/test/src/matchers.dart | 19 + packages/stream_chat/test/src/mocks.dart | 25 + packages/stream_chat/test/src/utils.dart | 15 + 35 files changed, 1788 insertions(+), 56 deletions(-) create mode 100644 packages/stream_chat/test/assets/example.pdf create mode 100644 packages/stream_chat/test/assets/test_image.jpeg create mode 100644 packages/stream_chat/test/src/core/api/attachment_file_uploader_test.dart create mode 100644 packages/stream_chat/test/src/core/api/channel_api_test.dart create mode 100644 packages/stream_chat/test/src/core/api/device_api_test.dart create mode 100644 packages/stream_chat/test/src/core/api/general_api_test.dart create mode 100644 packages/stream_chat/test/src/core/api/guest_api_test.dart create mode 100644 packages/stream_chat/test/src/core/api/message_api_test.dart create mode 100644 packages/stream_chat/test/src/core/api/moderation_api_test.dart create mode 100644 packages/stream_chat/test/src/core/api/stream_chat_api_test.dart create mode 100644 packages/stream_chat/test/src/core/api/user_api_test.dart rename packages/stream_chat/test/src/core/http/interceptor/{connection_id_interceptor.dart => connection_id_interceptor_test.dart} (100%) rename packages/stream_chat/test/src/{ => core}/models/action_test.dart (100%) rename packages/stream_chat/test/src/{ => core}/models/attachment_test.dart (100%) rename packages/stream_chat/test/src/{ => core}/models/channel_state_test.dart (100%) rename packages/stream_chat/test/src/{ => core}/models/channel_test.dart (100%) rename packages/stream_chat/test/src/{ => core}/models/command_test.dart (100%) rename packages/stream_chat/test/src/{ => core}/models/device_test.dart (100%) rename packages/stream_chat/test/src/{ => core}/models/event_test.dart (100%) rename packages/stream_chat/test/src/{ => core}/models/filter_test.dart (100%) rename packages/stream_chat/test/src/{ => core}/models/member_test.dart (100%) rename packages/stream_chat/test/src/{ => core}/models/message_test.dart (100%) rename packages/stream_chat/test/src/{ => core}/models/reaction_test.dart (100%) rename packages/stream_chat/test/src/{ => core}/models/read_test.dart (100%) rename packages/stream_chat/test/src/{ => core}/models/serialization_test.dart (100%) rename packages/stream_chat/test/src/{ => core}/models/user_test.dart (100%) create mode 100644 packages/stream_chat/test/src/matchers.dart create mode 100644 packages/stream_chat/test/src/utils.dart diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index ddc6e335..9ecc780b 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -38,10 +38,6 @@ import 'package:stream_chat/src/core/models/member.dart'; /// [LogRecord] as the only parameter. typedef LogHandlerFunction = void Function(LogRecord record); -/// A function which can be used to request a Stream Chat API token from your -/// own backend server. Function requires a single [userId]. -typedef TokenProvider = Future Function(String userId); - final _levelEmojiMapper = { Level.INFO: 'ℹï¸', Level.WARNING: 'âš ï¸', @@ -583,7 +579,6 @@ class StreamChatClient { } logger.info('Query channel start'); - print('Query Channel Started : ${DateTime.now()}'); final res = await _chatApi.channel.queryChannels( filter: filter, sort: sort, @@ -594,7 +589,6 @@ class StreamChatClient { messageLimit: messageLimit, paginationParams: paginationParams, ); - print('Query Channel Completed : ${DateTime.now()}'); if (res.channels.isEmpty && paginationParams.offset == 0) { logger.warning(''' diff --git a/packages/stream_chat/lib/src/core/api/attachment_file_uploader.dart b/packages/stream_chat/lib/src/core/api/attachment_file_uploader.dart index 8eacf716..d6dc249f 100644 --- a/packages/stream_chat/lib/src/core/api/attachment_file_uploader.dart +++ b/packages/stream_chat/lib/src/core/api/attachment_file_uploader.dart @@ -1,7 +1,6 @@ import 'package:dio/dio.dart'; import 'package:stream_chat/src/core/api/responses.dart'; import 'package:stream_chat/src/core/http/stream_http_client.dart'; -import 'package:stream_chat/src/core/util/extension.dart'; import 'package:stream_chat/src/core/models/attachment_file.dart'; /// Class responsible for uploading images and files to a given channel @@ -70,29 +69,10 @@ class StreamAttachmentFileUploader implements AttachmentFileUploader { ProgressCallback? onSendProgress, CancelToken? cancelToken, }) async { - final filename = file.path?.split('/').last ?? file.name; - final mimeType = filename?.mimeType; - - MultipartFile? multiPartFile; - if (file.path != null) { - multiPartFile = await MultipartFile.fromFile( - file.path!, - filename: filename, - contentType: mimeType, - ); - } else if (file.bytes != null) { - multiPartFile = MultipartFile.fromBytes( - file.bytes!, - filename: filename, - contentType: mimeType, - ); - } - - final response = await _client.post( + final multiPartFile = await file.toMultipartFile(); + final response = await _client.postFile( '/channels/$channelType/$channelId/image', - data: FormData.fromMap({ - 'file': multiPartFile, - }), + multiPartFile, onSendProgress: onSendProgress, cancelToken: cancelToken, ); @@ -107,29 +87,10 @@ class StreamAttachmentFileUploader implements AttachmentFileUploader { ProgressCallback? onSendProgress, CancelToken? cancelToken, }) async { - final filename = file.path?.split('/').last ?? file.name; - final mimeType = filename?.mimeType; - - MultipartFile? multiPartFile; - if (file.path != null) { - multiPartFile = await MultipartFile.fromFile( - file.path!, - filename: filename, - contentType: mimeType, - ); - } else if (file.bytes != null) { - multiPartFile = MultipartFile.fromBytes( - file.bytes!, - filename: filename, - contentType: mimeType, - ); - } - - final response = await _client.post( + final multiPartFile = await file.toMultipartFile(); + final response = await _client.postFile( '/channels/$channelType/$channelId/file', - data: FormData.fromMap({ - 'file': multiPartFile, - }), + multiPartFile, onSendProgress: onSendProgress, cancelToken: cancelToken, ); diff --git a/packages/stream_chat/lib/src/core/api/channel_api.dart b/packages/stream_chat/lib/src/core/api/channel_api.dart index 7407bcb5..761f10da 100644 --- a/packages/stream_chat/lib/src/core/api/channel_api.dart +++ b/packages/stream_chat/lib/src/core/api/channel_api.dart @@ -59,7 +59,6 @@ class ChannelApi { bool presence = false, PaginationParams paginationParams = const PaginationParams(), }) async { - print('Query Channel Started 2 : ${DateTime.now()}'); final response = await _client.get( '/channels', queryParameters: { @@ -80,7 +79,6 @@ class ChannelApi { }) }, ); - print('Query Channel Completed 2 : ${DateTime.now()}'); return QueryChannelsResponse.fromJson(response.data); } diff --git a/packages/stream_chat/lib/src/core/api/device_api.dart b/packages/stream_chat/lib/src/core/api/device_api.dart index 2d5e0fea..69f509f8 100644 --- a/packages/stream_chat/lib/src/core/api/device_api.dart +++ b/packages/stream_chat/lib/src/core/api/device_api.dart @@ -10,7 +10,8 @@ enum PushProvider { apn } -extension on PushProvider { +/// Helper extension for [PushProvider] +extension PushProviderX on PushProvider { /// Returns the string notion for [PushProvider]. String get name => { PushProvider.apn: 'apn', diff --git a/packages/stream_chat/lib/src/core/models/attachment_file.dart b/packages/stream_chat/lib/src/core/models/attachment_file.dart index 8dbb70ad..7bff5806 100644 --- a/packages/stream_chat/lib/src/core/models/attachment_file.dart +++ b/packages/stream_chat/lib/src/core/models/attachment_file.dart @@ -1,9 +1,13 @@ import 'dart:typed_data'; +import 'package:dio/dio.dart' show MultipartFile; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:meta/meta.dart'; +import 'package:stream_chat/src/core/platform_detector/platform_detector.dart'; +import 'package:stream_chat/src/core/util/extension.dart'; part 'attachment_file.freezed.dart'; + part 'attachment_file.g.dart'; /// Union class to hold various [UploadState] of a attachment. @@ -58,14 +62,18 @@ String? _toString(Uint8List? bytes) { @JsonSerializable() class AttachmentFile { /// Creates a new [AttachmentFile] instance. - const AttachmentFile({ + AttachmentFile({ required this.size, this.path, this.name, this.bytes, - }) : assert( + }) : assert( path != null || bytes != null, 'Either path or bytes should be != null', + ), + assert( + !CurrentPlatform.isWeb || bytes != null, + 'File by path is not supported in web, Please provide bytes', ); /// Create a new instance from a json @@ -95,4 +103,27 @@ class AttachmentFile { /// Serialize to json Map toJson() => _$AttachmentFileToJson(this); + + /// Converts this into a [MultipartFile] + Future toMultipartFile() async { + final filename = path?.split('/').last ?? name; + final mimeType = filename?.mimeType; + + late MultipartFile multiPartFile; + + if (CurrentPlatform.isWeb) { + multiPartFile = MultipartFile.fromBytes( + bytes!, + filename: filename, + contentType: mimeType, + ); + } else { + multiPartFile = await MultipartFile.fromFile( + path!, + filename: filename, + contentType: mimeType, + ); + } + return multiPartFile; + } } diff --git a/packages/stream_chat/test/assets/example.pdf b/packages/stream_chat/test/assets/example.pdf new file mode 100644 index 00000000..d736dedc --- /dev/null +++ b/packages/stream_chat/test/assets/example.pdf @@ -0,0 +1,57 @@ +%PDF-1.7 +%µí®û +3 0 obj +<< /Length 4 0 R >> +stream +/DeviceRGB cs /DeviceRGB CS +0 0 0.972549 SC +21.68 194 136.64 26 re +10 10 m 20 20 l S +BT +/F0 24 Tf +25.68 200 Td +(Hello World!) Tj +ET +endstream +endobj +4 0 obj +132 +endobj +5 0 obj +<< /Type /Font /Subtype /Type1 /BaseFont /Times-Roman /Encoding /WinAnsiEncoding >> +endobj +6 0 obj +<< /Type /Page + /Parent 2 0 R + /Resources << /Font << /F0 5 0 R >> >> + /MediaBox [ 0 0 180 240 ] + /Contents 3 0 R +>> +endobj +2 0 obj +<< /Type /Pages + /Count 1 + /Kids [ 6 0 R ] +>> +endobj +1 0 obj +<< /Type /Catalog + /Pages 2 0 R +>> +endobj +xref +0 7 +0000000000 65535 f +0000000522 00000 n +0000000457 00000 n +0000000015 00000 n +0000000199 00000 n +0000000218 00000 n +0000000317 00000 n +trailer +<< /Size 7 + /Root 1 0 R +>> +startxref +574 +%%EOF diff --git a/packages/stream_chat/test/assets/test_image.jpeg b/packages/stream_chat/test/assets/test_image.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..aeaccec269c66219d2abb3352e50eb959a1961ce GIT binary patch literal 4429 zcmeH|c{J4h9>;%UjKPEkgT^w}OtKV)WP6aw@E}XJETIW`>@;L6Q642F@{B!1i4a4n zWErwF${uPkD6-91Ct~h&?>)~w=l*q<|L*PcJ@3yy-{rhtpU>xbMmqzt+Z2FU^(a1a;{ zVvzw90DvHDw?7R3JwO~_PPR*K9$pyx0BJwK0Rn?L*#6l7?9;LAeSi}V5tKP<#3f|o z36%{IR=;xRKDV54O$)-dk1ns_6`H`qDhBCubM$bNKT6zKjU*_h&GFI2tH#WDJ-?o2nfdKH| zu>K_bCoVXfi-X-3PUsIV5C?%RFq{)2bCgTa$Oh^eA|$JRgIZo zk5@!qb4r2n1MRP5{~B1re?;~t*gv?&fPG*PyLn(ZUPqkLVWP)28`jP&E{E!!LsxnfBVBT3rsZ0|3)(`C_{SgDy;Jzbz!X0^B%3t{K08x zkJU%2<6hI!MVXP(lHyqRHzhW%Y3&p6ulXW$)80o#e9~Xte+wCZ@+d;`vjC z<*3!qBlGSR`=>WWR4gc+ixAJalid-n)S`_h1<7X6Ht}PJMF!fbLN5oe5&dYff;47* zUjX8}EdI?L}(3M5({s@<&e#8|D7x@M z7EJbIw~!h-HCg3Fuwt+~hs4N(c@JnD((h8gzlgg~-K|v}&SZ-Gt>oqz1lH>+v=oX6 z>hOSlhfMQX>{yTuBokKGeC>|+$Xhjscco?>lL+%LaZH)FMjUu#ks0l{p9G;ShftgM!P4;&|*TI$tsYrlT^ zrKIeH<4vdXfcjS`gHd*6{;WIy&#= zH@vdwF_f+rRI&8%ySHJB28uA3RK;V@EitinDXA6XnZpeiw;ENmw2BjRm9Vz8Pa6V) zqL=ngX2%Vb?GV~u3&ilgK7IjN{p`85*b-OZVV zs7*0F{mB*n$au%0jaWG=t*x=QO#$CKq9nRUy$guR?rzUq1v>4g*0ztiO8D8@+NJ84 z>Kh!Q=;%^e0548|r7uuta@Mvuqw@T~H-$1<3=5!pw8jZD6vnjn5ZLXc!0 zF)6M)*yGD8qi)Hxlk;p;W-5KcwLeCI4kPa!9M*JJQ_(Z26h~_^ef-1m+)jM+-DTU& zAtQmuk=qt+`Cxk4^%zY26h+*8N0s>S0Nd6|oY(xf4M`B|Sb>P_#< zxwfRcbxm8VV-xf@J@G>8ZpMR5!?xG--`UyGajQn$dXbpzW@IE|-=n5u>$)E&pE*hQ zf3L@bFN&O)-Xgq!I z)=9{S(vmwAuF5by##b(56Jb9y>88+%V(Cy|61kOjnFR>Q(+%6Q-jnVJL=fUNvqTwJ zn$+@)zs02e5pYUCtV_R~nn@>2m1b2Qtiey|nytv)HoMtxHg}WX$o@R~$fdI(&a}u{ z0SmaZUBDQw19vD4-EOK#kb3lD;l=ylo-b(ZF6oTR@@_$yNjbgp!S3~>^~aJ9vwUlf&j*ng7}x~lhrWv`X>qa_9W*`gFBE@2x};HQ4p;O7kdf6qWX GYxFNZ3vhY> literal 0 HcmV?d00001 diff --git a/packages/stream_chat/test/src/core/api/attachment_file_uploader_test.dart b/packages/stream_chat/test/src/core/api/attachment_file_uploader_test.dart new file mode 100644 index 00000000..27f4ce6a --- /dev/null +++ b/packages/stream_chat/test/src/core/api/attachment_file_uploader_test.dart @@ -0,0 +1,136 @@ +import 'package:dio/dio.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/core/api/attachment_file_uploader.dart'; +import 'package:stream_chat/src/core/models/attachment_file.dart'; +import 'package:test/test.dart'; + +import '../../fakes.dart'; +import '../../matchers.dart'; +import '../../mocks.dart'; +import '../../utils.dart'; + +void main() { + late final client = MockHttpClient(); + late StreamAttachmentFileUploader fileUploader; + + setUp(() { + fileUploader = StreamAttachmentFileUploader(client); + registerFallbackValue(FakeMultiPartFile()); + }); + + Response successResponse(String path, {Object? data}) => Response( + data: data, + requestOptions: RequestOptions(path: path), + statusCode: 200, + ); + + test('sendImage', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + + const path = '/channels/$channelType/$channelId/image'; + final file = assetFile('test_image.jpeg'); + final attachmentFile = AttachmentFile( + size: 333, + path: file.path, + bytes: file.readAsBytesSync(), + ); + final multipartFile = await attachmentFile.toMultipartFile(); + + when(() => client.postFile( + path, + any(that: isSameMultipartFileAs(multipartFile)), + )).thenAnswer((_) async => successResponse(path, data: { + 'file': 'test-file-url', + })); + + final res = await fileUploader.sendImage( + attachmentFile, + channelId, + channelType, + ); + + expect(res, isNotNull); + expect(res.file, isNotNull); + expect(res.file, isNotEmpty); + + verify(() => client.postFile( + path, + any(that: isSameMultipartFileAs(multipartFile)), + )).called(1); + verifyNoMoreInteractions(client); + }); + + test('sendFile', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + + const path = '/channels/$channelType/$channelId/file'; + final file = assetFile('example.pdf'); + final attachmentFile = AttachmentFile( + size: 333, + path: file.path, + bytes: file.readAsBytesSync(), + ); + final multipartFile = await attachmentFile.toMultipartFile(); + + when(() => client.postFile( + path, + any(that: isSameMultipartFileAs(multipartFile)), + )).thenAnswer((_) async => successResponse(path, data: { + 'file': 'test-file-url', + })); + + final res = await fileUploader.sendFile( + attachmentFile, + channelId, + channelType, + ); + + expect(res, isNotNull); + expect(res.file, isNotNull); + expect(res.file, isNotEmpty); + + verify(() => client.postFile( + path, + any(that: isSameMultipartFileAs(multipartFile)), + )).called(1); + verifyNoMoreInteractions(client); + }); + + test('deleteImage', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + const path = '/channels/$channelType/$channelId/image'; + + const url = 'test-image-url'; + + when(() => client.delete(path, queryParameters: {'url': url})).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await fileUploader.deleteImage(url, channelId, channelType); + + expect(res, isNotNull); + + verify(() => client.delete(path, queryParameters: {'url': url})).called(1); + verifyNoMoreInteractions(client); + }); + + test('deleteFile', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + const path = '/channels/$channelType/$channelId/file'; + + const url = 'test-file-url'; + + when(() => client.delete(path, queryParameters: {'url': url})).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await fileUploader.deleteFile(url, channelId, channelType); + + expect(res, isNotNull); + + verify(() => client.delete(path, queryParameters: {'url': url})).called(1); + verifyNoMoreInteractions(client); + }); +} diff --git a/packages/stream_chat/test/src/core/api/channel_api_test.dart b/packages/stream_chat/test/src/core/api/channel_api_test.dart new file mode 100644 index 00000000..8d5f5e76 --- /dev/null +++ b/packages/stream_chat/test/src/core/api/channel_api_test.dart @@ -0,0 +1,492 @@ +import 'package:dio/dio.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/core/api/channel_api.dart'; +import 'package:stream_chat/src/core/models/channel_model.dart'; +import 'package:stream_chat/src/core/models/channel_state.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:test/test.dart'; + +import '../../mocks.dart'; + +void main() { + String _getChannelUrl(String channelId, String channelType) => + '/channels/$channelType/$channelId'; + + ChannelState _generateChannelState( + String channelId, + String channelType, + ) { + final channel = ChannelModel(id: channelId, type: channelType); + final messages = List.generate( + 3, + (index) => Message( + id: 'test-message-id-$index', + text: 'test-message-text-$index', + ), + ); + final members = List.generate( + 3, + (index) => Member(userId: 'test-user-id-$index'), + ); + final reads = List.generate( + 3, + (index) => Read( + lastRead: DateTime.now(), + user: User(id: 'test-user-id-$index'), + ), + ); + final watchers = List.generate( + 3, + (index) => User(id: 'test-user-id-$index'), + ); + final state = ChannelState( + channel: channel, + messages: messages, + pinnedMessages: messages, + members: members, + read: reads, + watchers: watchers, + watcherCount: watchers.length, + ); + return state; + } + + Response successResponse(String path, {Object? data}) => Response( + data: data, + requestOptions: RequestOptions(path: path), + statusCode: 200, + ); + + late final client = MockHttpClient(); + late ChannelApi channelApi; + + setUp(() { + channelApi = ChannelApi(client); + }); + + test('queryChannel', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + const channelData = {'name': 'test-channel'}; + const messagePagination = PaginationParams(); + const membersPagination = PaginationParams(); + const watchersPagination = PaginationParams(); + + const channelPath = '/channels/$channelType/$channelId'; + const path = '$channelPath/query'; + + final channelState = _generateChannelState(channelId, channelType); + when(() => client.post(path, data: any(named: 'data'))) + .thenAnswer((_) async => successResponse( + path, + data: channelState.toJson(), + )); + + final res = await channelApi.queryChannel( + channelType, + channelId: channelId, + channelData: channelData, + messagesPagination: messagePagination, + membersPagination: membersPagination, + watchersPagination: watchersPagination, + ); + + expect(res, isNotNull); + expect(res.messages.length, channelState.messages.length); + expect(res.pinnedMessages.length, channelState.pinnedMessages.length); + expect(res.members.length, channelState.members.length); + expect(res.read.length, channelState.read.length); + expect(res.watchers.length, channelState.watchers.length); + expect(res.watcherCount, channelState.watcherCount); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('queryChannels', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + + final filter = Filter.in_('cid', const ['test-cid']); + const sort = [SortOption('test-field')]; + const memberLimit = 33; + const messageLimit = 33; + + const path = '/channels'; + + final channelState = _generateChannelState(channelId, channelType); + when(() => client.get(path, queryParameters: any(named: 'queryParameters'))) + .thenAnswer((_) async => successResponse( + path, + data: { + 'channels': [channelState.toJson()] + }, + )); + + final res = await channelApi.queryChannels( + filter: filter, + sort: sort, + memberLimit: memberLimit, + messageLimit: messageLimit, + ); + + expect(res, isNotNull); + expect(res.channels, isNotEmpty); + + verify( + () => client.get(path, queryParameters: any(named: 'queryParameters')), + ).called(1); + verifyNoMoreInteractions(client); + }); + + test('markAllRead', () async { + const path = 'channels/read'; + when(() => client.post(path)).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await channelApi.markAllRead(); + + expect(res, isNotNull); + + verify(() => client.post(path)).called(1); + verifyNoMoreInteractions(client); + }); + + test('updateChannel', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + const data = {'name': 'test-channel-name'}; + final message = Message(id: 'test-message-id', text: 'channel-updated'); + + final path = _getChannelUrl(channelId, channelType); + + final channelModel = ChannelModel( + id: channelId, + type: channelType, + extraData: data, + ); + + when(() => client.post(path, data: any(named: 'data'))) + .thenAnswer((_) async => successResponse(path, data: { + 'channel': channelModel.toJson(), + 'message': message.toJson(), + })); + + final res = await channelApi.updateChannel( + channelId, + channelType, + data, + message: message, + ); + + expect(res, isNotNull); + expect(res.channel.cid, channelModel.cid); + expect(res.message?.id, message.id); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('updateChannelPartial', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + const data = {'name': 'test-channel-name'}; + + final path = _getChannelUrl(channelId, channelType); + + final channelModel = ChannelModel( + id: channelId, + type: channelType, + extraData: data, + ); + + when(() => client.patch(path, data: any(named: 'data'))) + .thenAnswer((_) async => successResponse(path, data: { + 'channel': channelModel.toJson(), + })); + + final res = await channelApi.updateChannelPartial( + channelId, + channelType, + data, + ); + + expect(res, isNotNull); + + verify(() => client.patch(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('acceptChannelInvite', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + final message = Message(id: 'test-message-id', text: 'channel-accepted'); + + final channelModel = ChannelModel(id: channelId, type: channelType); + + final path = _getChannelUrl(channelId, channelType); + + when(() => client.post(path, data: any(named: 'data'))) + .thenAnswer((_) async => successResponse(path, data: { + 'channel': channelModel.toJson(), + 'message': message.toJson(), + })); + + final res = await channelApi.acceptChannelInvite( + channelId, + channelType, + message: message, + ); + + expect(res, isNotNull); + expect(res.channel.cid, channelModel.cid); + expect(res.message?.id, message.id); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('rejectChannelInvite', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + final message = Message(id: 'test-message-id', text: 'channel-rejected'); + + final channelModel = ChannelModel(id: channelId, type: channelType); + + final path = _getChannelUrl(channelId, channelType); + + when(() => client.post(path, data: any(named: 'data'))) + .thenAnswer((_) async => successResponse(path, data: { + 'channel': channelModel.toJson(), + 'message': message.toJson(), + })); + + final res = await channelApi.rejectChannelInvite( + channelId, + channelType, + message: message, + ); + + expect(res, isNotNull); + expect(res.channel.cid, channelModel.cid); + expect(res.message?.id, message.id); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('inviteChannelMembers', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + const memberIds = ['test-member-id-1', 'test-member-id-2']; + final channelModel = ChannelModel(id: channelId, type: channelType); + final message = Message(id: 'test-message-id', text: 'members-invited'); + + final path = _getChannelUrl(channelId, channelType); + + when(() => client.post(path, data: any(named: 'data'))) + .thenAnswer((_) async => successResponse(path, data: { + 'channel': channelModel.toJson(), + 'message': message.toJson(), + })); + + final res = await channelApi.inviteChannelMembers( + channelId, + channelType, + memberIds, + message: message, + ); + + expect(res, isNotNull); + expect(res.channel.cid, channelModel.cid); + expect(res.message?.id, message.id); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('addMembers', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + const memberIds = ['test-member-id-1', 'test-member-id-2']; + final channelModel = ChannelModel(id: channelId, type: channelType); + final message = Message(id: 'test-message-id', text: 'members-added'); + + final path = _getChannelUrl(channelId, channelType); + + when(() => client.post(path, data: any(named: 'data'))) + .thenAnswer((_) async => successResponse(path, data: { + 'channel': channelModel.toJson(), + 'message': message.toJson(), + })); + + final res = await channelApi.addMembers( + channelId, + channelType, + memberIds, + message: message, + ); + + expect(res, isNotNull); + expect(res.channel.cid, channelModel.cid); + expect(res.message?.id, message.id); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('removeMembers', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + const memberIds = ['test-member-id-1', 'test-member-id-2']; + final channelModel = ChannelModel(id: channelId, type: channelType); + final message = Message(id: 'test-message-id', text: 'members-removed'); + + final path = _getChannelUrl(channelId, channelType); + + when(() => client.post(path, data: any(named: 'data'))) + .thenAnswer((_) async => successResponse(path, data: { + 'channel': channelModel.toJson(), + 'message': message.toJson(), + })); + + final res = await channelApi.removeMembers( + channelId, + channelType, + memberIds, + message: message, + ); + + expect(res, isNotNull); + expect(res.channel.cid, channelModel.cid); + expect(res.message?.id, message.id); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('sendEvent', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + final event = Event(type: 'event.test'); + + final path = '${_getChannelUrl(channelId, channelType)}/event'; + + when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await channelApi.sendEvent(channelId, channelType, event); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('deleteChannel', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + + final path = _getChannelUrl(channelId, channelType); + + when(() => client.delete(path)).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await channelApi.deleteChannel(channelId, channelType); + + expect(res, isNotNull); + + verify(() => client.delete(path)).called(1); + verifyNoMoreInteractions(client); + }); + + test('truncateChannel', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + + final path = '${_getChannelUrl(channelId, channelType)}/truncate'; + + when(() => client.post(path)).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await channelApi.truncateChannel(channelId, channelType); + + expect(res, isNotNull); + + verify(() => client.post(path)).called(1); + verifyNoMoreInteractions(client); + }); + + test('hideChannel', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + + final path = '${_getChannelUrl(channelId, channelType)}/hide'; + + when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await channelApi.hideChannel(channelId, channelType); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('showChannel', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + + final path = '${_getChannelUrl(channelId, channelType)}/show'; + + when(() => client.post(path)).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await channelApi.showChannel(channelId, channelType); + + expect(res, isNotNull); + + verify(() => client.post(path)).called(1); + verifyNoMoreInteractions(client); + }); + + test('markRead', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + const messageId = 'test-message-id'; + + final path = '${_getChannelUrl(channelId, channelType)}/read'; + + when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await channelApi.markRead( + channelId, + channelType, + messageId: messageId, + ); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('stopWatching', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + + final path = '${_getChannelUrl(channelId, channelType)}/stop-watching'; + + when(() => client.post(path)).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await channelApi.stopWatching(channelId, channelType); + + expect(res, isNotNull); + + verify(() => client.post(path)).called(1); + verifyNoMoreInteractions(client); + }); +} diff --git a/packages/stream_chat/test/src/core/api/device_api_test.dart b/packages/stream_chat/test/src/core/api/device_api_test.dart new file mode 100644 index 00000000..f12f04f5 --- /dev/null +++ b/packages/stream_chat/test/src/core/api/device_api_test.dart @@ -0,0 +1,84 @@ +import 'package:dio/dio.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/core/api/device_api.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:test/test.dart'; + +import '../../mocks.dart'; + +void main() { + Response successResponse(String path, {Object? data}) => Response( + data: data, + requestOptions: RequestOptions(path: path), + statusCode: 200, + ); + + late final client = MockHttpClient(); + late DeviceApi deviceApi; + + setUp(() { + deviceApi = DeviceApi(client); + }); + + test('addDevice', () async { + const deviceId = 'test-device-id'; + const pushProvider = PushProvider.firebase; + + const path = '/devices'; + + when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await deviceApi.addDevice(deviceId, pushProvider); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('getDevices', () async { + const path = '/devices'; + + final devices = List.generate( + 3, + (index) => Device( + id: 'test-device-id-$index', + pushProvider: PushProvider.firebase.name, + ), + ); + + when(() => client.get(path)).thenAnswer( + (_) async => successResponse(path, data: { + 'devices': [...devices.map((it) => it.toJson())] + }), + ); + + final res = await deviceApi.getDevices(); + + expect(res, isNotNull); + expect(res.devices.length, devices.length); + + verify(() => client.get(path)).called(1); + verifyNoMoreInteractions(client); + }); + + test('removeDevice', () async { + const deviceId = 'test-device-id'; + + const path = '/devices'; + + when( + () => client.delete(path, queryParameters: any(named: 'queryParameters')), + ).thenAnswer((_) async => successResponse(path, data: {})); + + final res = await deviceApi.removeDevice(deviceId); + + expect(res, isNotNull); + + verify( + () => client.delete(path, queryParameters: any(named: 'queryParameters')), + ).called(1); + verifyNoMoreInteractions(client); + }); +} diff --git a/packages/stream_chat/test/src/core/api/general_api_test.dart b/packages/stream_chat/test/src/core/api/general_api_test.dart new file mode 100644 index 00000000..8844262f --- /dev/null +++ b/packages/stream_chat/test/src/core/api/general_api_test.dart @@ -0,0 +1,211 @@ +import 'package:dio/dio.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/core/api/general_api.dart'; +import 'package:stream_chat/src/core/api/requests.dart'; +import 'package:stream_chat/src/core/models/channel_model.dart'; +import 'package:stream_chat/src/core/models/event.dart'; +import 'package:stream_chat/src/core/models/filter.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:test/test.dart'; + +import '../../mocks.dart'; + +void main() { + Response successResponse(String path, {Object? data}) => Response( + data: data, + requestOptions: RequestOptions(path: path), + statusCode: 200, + ); + + late final client = MockHttpClient(); + late GeneralApi generalApi; + + setUp(() { + generalApi = GeneralApi(client); + }); + + test('sync', () async { + const cids = ['test-cid-1', 'test-cid-2', 'test-cid-3']; + final lastSyncAt = DateTime.now(); + + const path = '/sync'; + + final events = + List.generate(3, (index) => Event(type: 'test-event-type-$index')); + + when(() => client.post(path, data: any(named: 'data'))) + .thenAnswer((_) async => successResponse(path, data: { + 'events': [...events.map((it) => it.toJson())] + })); + + final res = await generalApi.sync(cids, lastSyncAt); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + group('searchMessages', () { + test( + 'should throw if `query` and `messageFilters` is not provided', + () async { + final filter = Filter.in_('cid', const ['test-cid-1', 'test-cid-2']); + try { + await generalApi.searchMessages(filter); + } catch (e) { + expect(e, isA()); + } + }, + ); + + test( + 'should throw if `query` and `messageFilters` both are provided', + () async { + final filter = Filter.in_('cid', const ['test-cid-1', 'test-cid-2']); + const query = 'test-query'; + final messageFilter = Filter.query('key', 'text'); + try { + await generalApi.searchMessages( + filter, + query: query, + messageFilters: messageFilter, + ); + } catch (e) { + expect(e, isA()); + } + }, + ); + + test('should run successfully with `query`', () async { + final filter = Filter.in_('cid', const ['test-cid-1', 'test-cid-2']); + const query = 'test-query'; + const sort = [SortOption('test-field')]; + const pagination = PaginationParams(); + + const path = '/search'; + + when( + () => client.get(path, queryParameters: any(named: 'queryParameters')), + ).thenAnswer((_) async => successResponse(path, data: {'results': []})); + + final res = await generalApi.searchMessages( + filter, + query: query, + sort: sort, + pagination: pagination, + ); + + expect(res, isNotNull); + expect(res.results, isEmpty); + + verify( + () => client.get(path, queryParameters: any(named: 'queryParameters')), + ).called(1); + verifyNoMoreInteractions(client); + }); + + test('should run successfully with `messageFilter`', () async { + final filter = Filter.in_('cid', const ['test-cid-1', 'test-cid-2']); + const sort = [SortOption('test-field')]; + final messageFilter = Filter.query('key', 'text'); + const pagination = PaginationParams(); + + const path = '/search'; + + when( + () => client.get(path, queryParameters: any(named: 'queryParameters')), + ).thenAnswer((_) async => successResponse(path, data: {'results': []})); + + final res = await generalApi.searchMessages( + filter, + messageFilters: messageFilter, + sort: sort, + pagination: pagination, + ); + + expect(res, isNotNull); + expect(res.results, isEmpty); + + verify( + () => client.get(path, queryParameters: any(named: 'queryParameters')), + ).called(1); + verifyNoMoreInteractions(client); + }); + }); + + group('queryMembers', () { + test('with `channelId`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + final filter = Filter.in_('cid', const ['test-cid-1', 'test-cid-2']); + const pagination = PaginationParams(); + const sort = [SortOption('test-field')]; + + const path = '/members'; + + final members = List.generate( + 3, + (index) => Member(userId: 'test-user-id=$index'), + ); + + when(() => + client.get(path, queryParameters: any(named: 'queryParameters'))) + .thenAnswer((_) async => successResponse(path, data: { + 'members': [...members.map((it) => it.toJson())] + })); + + final res = await generalApi.queryMembers( + channelType, + channelId: channelId, + filter: filter, + pagination: pagination, + sort: sort, + ); + + expect(res, isNotNull); + expect(res.members.length, members.length); + + verify( + () => client.get(path, queryParameters: any(named: 'queryParameters')), + ).called(1); + verifyNoMoreInteractions(client); + }); + + test('with `members`', () async { + const channelType = 'test-channel-type'; + final filter = Filter.in_('cid', const ['test-cid-1', 'test-cid-2']); + const pagination = PaginationParams(); + const sort = [SortOption('test-field')]; + + const path = '/members'; + + final members = List.generate( + 3, + (index) => Member(userId: 'test-user-id=$index'), + ); + + when(() => + client.get(path, queryParameters: any(named: 'queryParameters'))) + .thenAnswer((_) async => successResponse(path, data: { + 'members': [...members.map((it) => it.toJson())] + })); + + final res = await generalApi.queryMembers( + channelType, + filter: filter, + pagination: pagination, + sort: sort, + members: members, + ); + + expect(res, isNotNull); + expect(res.members.length, members.length); + + verify( + () => client.get(path, queryParameters: any(named: 'queryParameters')), + ).called(1); + verifyNoMoreInteractions(client); + }); + }); +} diff --git a/packages/stream_chat/test/src/core/api/guest_api_test.dart b/packages/stream_chat/test/src/core/api/guest_api_test.dart new file mode 100644 index 00000000..6c6386fa --- /dev/null +++ b/packages/stream_chat/test/src/core/api/guest_api_test.dart @@ -0,0 +1,44 @@ +import 'package:dio/dio.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/core/api/guest_api.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:test/test.dart'; + +import '../../mocks.dart'; + +void main() { + Response successResponse(String path, {Object? data}) => Response( + data: data, + requestOptions: RequestOptions(path: path), + statusCode: 200, + ); + + late final client = MockHttpClient(); + late GuestApi guestApi; + + setUp(() { + guestApi = GuestApi(client); + }); + + test('getGuestUser', () async { + const accessToken = 'test-guest-token'; + final user = User(id: 'test-user-id'); + + const path = '/guest'; + + when(() => client.post(path, data: any(named: 'data'))) + .thenAnswer((_) async => successResponse(path, data: { + 'access_token': accessToken, + 'user': user.toJson(), + })); + + final res = await guestApi.getGuestUser(user); + + expect(res, isNotNull); + expect(res.accessToken, accessToken); + expect(res.user.id, user.id); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); +} diff --git a/packages/stream_chat/test/src/core/api/message_api_test.dart b/packages/stream_chat/test/src/core/api/message_api_test.dart new file mode 100644 index 00000000..59319eca --- /dev/null +++ b/packages/stream_chat/test/src/core/api/message_api_test.dart @@ -0,0 +1,291 @@ +import 'package:dio/dio.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/core/api/message_api.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:test/test.dart'; + +import '../../mocks.dart'; + +void main() { + Response successResponse(String path, {Object? data}) => Response( + data: data, + requestOptions: RequestOptions(path: path), + statusCode: 200, + ); + + late final client = MockHttpClient(); + late MessageApi messageApi; + + setUp(() { + messageApi = MessageApi(client); + }); + + test('sendMessage', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + final message = Message(id: 'test-message-id', text: 'test-message-text'); + + const path = '/channels/$channelType/$channelId/message'; + + when(() => client.post(path, data: any(named: 'data'))) + .thenAnswer((_) async => successResponse(path, data: { + 'message': message.toJson(), + })); + + final res = await messageApi.sendMessage(channelId, channelType, message); + + expect(res, isNotNull); + expect(res.message.id, message.id); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('getMessagesById', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + const messageIds = ['test-message-id-1', 'test-message-id-2']; + + const path = '/channels/$channelType/$channelId/messages'; + + final messages = List.generate( + 3, + (index) => Message(id: 'test-message-id-$index'), + ); + + when(() => client.get(path, queryParameters: any(named: 'queryParameters'))) + .thenAnswer((_) async => successResponse(path, data: { + 'messages': [...messages.map((it) => it.toJson())], + })); + + final res = await messageApi.getMessagesById( + channelId, + channelType, + messageIds, + ); + + expect(res, isNotNull); + expect(res.messages.length, messages.length); + + verify( + () => client.get(path, queryParameters: any(named: 'queryParameters')), + ).called(1); + verifyNoMoreInteractions(client); + }); + + test('getMessage', () async { + const messageId = 'test-message-id'; + + const path = '/messages/$messageId'; + + final message = Message(id: messageId); + + when(() => client.get(path)).thenAnswer((_) async => + successResponse(path, data: {'message': message.toJson()})); + + final res = await messageApi.getMessage(messageId); + + expect(res, isNotNull); + expect(res.message.id, messageId); + + verify(() => client.get(path)).called(1); + verifyNoMoreInteractions(client); + }); + + test('updateMessage', () async { + final message = Message(id: 'test-message-id'); + + final path = '/messages/${message.id}'; + + when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + (_) async => successResponse(path, data: {'message': message.toJson()}), + ); + + final res = await messageApi.updateMessage(message); + + expect(res, isNotNull); + expect(res.message.id, message.id); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('deleteMessage', () async { + const messageId = 'test-message-id'; + + const path = '/messages/$messageId'; + + when(() => client.delete(path)).thenAnswer( + (_) async => successResponse(path, data: {}), + ); + + final res = await messageApi.deleteMessage(messageId); + + expect(res, isNotNull); + + verify(() => client.delete(path)).called(1); + verifyNoMoreInteractions(client); + }); + + test('sendAction', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + const messageId = 'test-message-id'; + const formData = {'test-key': 'test-data'}; + + const path = '/messages/$messageId/action'; + + when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await messageApi.sendAction( + channelId, + channelType, + messageId, + formData, + ); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('sendReaction', () async { + const messageId = 'test-message-id'; + const reactionType = 'test-reaction-type'; + const extraData = {'test-key': 'test-data'}; + + const path = '/messages/$messageId/reaction'; + + final message = Message(id: messageId); + final reaction = Reaction(type: reactionType, messageId: messageId); + + when(() => client.post(path, data: any(named: 'data'))) + .thenAnswer((_) async => successResponse(path, data: { + 'message': message.toJson(), + 'reaction': reaction.toJson(), + })); + + final res = await messageApi.sendReaction( + messageId, + reactionType, + extraData: extraData, + ); + + expect(res, isNotNull); + expect(res.message.id, messageId); + expect(res.reaction.messageId, messageId); + expect(res.reaction.type, reactionType); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('deleteReaction', () async { + const messageId = 'test-message-id'; + const reactionType = 'test-reaction-type'; + + const path = '/messages/$messageId/reaction/$reactionType'; + + when(() => client.delete(path)).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await messageApi.deleteReaction(messageId, reactionType); + + expect(res, isNotNull); + + verify(() => client.delete(path)).called(1); + verifyNoMoreInteractions(client); + }); + + test('getReactions', () async { + const messageId = 'test-message-id'; + const options = PaginationParams(); + + const path = '/messages/$messageId/reactions'; + + final reactions = List.generate( + 3, + (index) => Reaction( + type: 'test-reaction-type-$index', + messageId: messageId, + ), + ); + + when(() => client.get(path, queryParameters: any(named: 'queryParameters'))) + .thenAnswer((_) async => successResponse(path, data: { + 'reactions': [...reactions.map((it) => it.toJson())] + })); + + final res = await messageApi.getReactions(messageId, options); + + expect(res, isNotNull); + expect(res.reactions.length, reactions.length); + expect(res.reactions.every((it) => it.messageId == messageId), isTrue); + + verify( + () => client.get(path, queryParameters: any(named: 'queryParameters')), + ).called(1); + verifyNoMoreInteractions(client); + }); + + test('translateMessage', () async { + const messageId = 'test-message-id'; + const messageText = 'hello'; + const language = 'hi'; // Hindi + final message = Message(id: messageId, text: messageText); + + final path = '/messages/${message.id}/translate'; + + const translatedMessageText = 'नमसà¥à¤¤à¥‡'; + final translatedMessage = TranslatedMessage(const { + language: translatedMessageText, + }); + + when(() => client.post(path, data: any(named: 'data'))) + .thenAnswer((_) async => successResponse(path, data: { + 'message': translatedMessage.toJson(), + })); + + final res = await messageApi.translateMessage(messageId, language); + + expect(res, isNotNull); + expect(res.message.i18n?.containsKey(language), isTrue); + expect(res.message.i18n?[language], translatedMessageText); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('getReplies', () async { + const parentId = 'test-parent-id'; + const options = PaginationParams(); + + const path = '/messages/$parentId/replies'; + + final messages = List.generate( + 3, + (index) => Message( + id: 'test-message-id-$index', + parentId: parentId, + ), + ); + + when(() => client.get(path, queryParameters: any(named: 'queryParameters'))) + .thenAnswer((_) async => successResponse(path, data: { + 'messages': [...messages.map((it) => it.toJson())] + })); + + final res = await messageApi.getReplies(parentId, options); + + expect(res, isNotNull); + expect(res.messages.length, messages.length); + expect(res.messages.every((it) => it.parentId == parentId), isTrue); + + verify( + () => client.get(path, queryParameters: any(named: 'queryParameters')), + ).called(1); + verifyNoMoreInteractions(client); + }); +} diff --git a/packages/stream_chat/test/src/core/api/moderation_api_test.dart b/packages/stream_chat/test/src/core/api/moderation_api_test.dart new file mode 100644 index 00000000..fb2a1f29 --- /dev/null +++ b/packages/stream_chat/test/src/core/api/moderation_api_test.dart @@ -0,0 +1,190 @@ +import 'package:dio/dio.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/core/api/moderation_api.dart'; +import 'package:test/test.dart'; + +import '../../mocks.dart'; + +void main() { + Response successResponse(String path, {Object? data}) => Response( + data: data, + requestOptions: RequestOptions(path: path), + statusCode: 200, + ); + + late final client = MockHttpClient(); + late ModerationApi moderationApi; + + setUp(() { + moderationApi = ModerationApi(client); + }); + + test('muteUser', () async { + const userId = 'test-user-id'; + + const path = '/moderation/mute'; + + when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await moderationApi.muteUser(userId); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('unmuteUser', () async { + const userId = 'test-user-id'; + + const path = '/moderation/unmute'; + + when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await moderationApi.unmuteUser(userId); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('muteChannel', () async { + const channelCid = 'test-channel-cid'; + const expiration = Duration(days: 3); + + const path = '/moderation/mute/channel'; + + when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await moderationApi.muteChannel( + channelCid, + expiration: expiration, + ); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('unmuteChannel', () async { + const channelCid = 'test-channel-cid'; + + const path = '/moderation/unmute/channel'; + + when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await moderationApi.unmuteChannel(channelCid); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('flagMessage', () async { + const messageId = 'test-message-id'; + + const path = '/moderation/flag'; + + when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await moderationApi.flagMessage(messageId); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('unflagMessage', () async { + const messageId = 'test-message-id'; + + const path = '/moderation/unflag'; + + when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await moderationApi.unflagMessage(messageId); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('flagUser', () async { + const userId = 'test-message-id'; + + const path = '/moderation/flag'; + + when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await moderationApi.flagUser(userId); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('unflagUser', () async { + const userId = 'test-message-id'; + + const path = '/moderation/unflag'; + + when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await moderationApi.unflagUser(userId); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('banUser', () async { + const targetUserId = 'test-target-user-id'; + const options = {'key': 'value'}; + + const path = '/moderation/ban'; + + when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await moderationApi.banUser(targetUserId, options: options); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('unbanUser', () async { + const targetUserId = 'test-target-user-id'; + const options = {'key': 'value'}; + + const path = '/moderation/ban'; + + when( + () => client.delete(path, queryParameters: any(named: 'queryParameters')), + ).thenAnswer((_) async => successResponse(path, data: {})); + + final res = await moderationApi.unbanUser(targetUserId, options: options); + + expect(res, isNotNull); + + verify( + () => client.delete(path, queryParameters: any(named: 'queryParameters')), + ).called(1); + verifyNoMoreInteractions(client); + }); +} diff --git a/packages/stream_chat/test/src/core/api/stream_chat_api_test.dart b/packages/stream_chat/test/src/core/api/stream_chat_api_test.dart new file mode 100644 index 00000000..f61746cd --- /dev/null +++ b/packages/stream_chat/test/src/core/api/stream_chat_api_test.dart @@ -0,0 +1,49 @@ +import 'package:stream_chat/src/core/api/stream_chat_api.dart'; +import 'package:test/test.dart'; + +import '../../mocks.dart'; + +void main() { + const apiKey = 'test-api-key'; + late final client = MockHttpClient(); + late StreamChatApi streamChatApi; + + setUp(() { + streamChatApi = StreamChatApi( + apiKey, + client: client, + ); + }); + + test('`.user`', () { + expect(streamChatApi.user, isNotNull); + }); + + test('`.guest`', () { + expect(streamChatApi.guest, isNotNull); + }); + + test('`.message`', () { + expect(streamChatApi.message, isNotNull); + }); + + test('`.channel`', () { + expect(streamChatApi.channel, isNotNull); + }); + + test('`.device`', () { + expect(streamChatApi.device, isNotNull); + }); + + test('`.moderation`', () { + expect(streamChatApi.moderation, isNotNull); + }); + + test('`.general`', () { + expect(streamChatApi.general, isNotNull); + }); + + test('`.fileUploader`', () { + expect(streamChatApi.fileUploader, isNotNull); + }); +} diff --git a/packages/stream_chat/test/src/core/api/user_api_test.dart b/packages/stream_chat/test/src/core/api/user_api_test.dart new file mode 100644 index 00000000..ebf0e343 --- /dev/null +++ b/packages/stream_chat/test/src/core/api/user_api_test.dart @@ -0,0 +1,77 @@ +import 'package:dio/dio.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/core/api/requests.dart'; +import 'package:stream_chat/src/core/api/user_api.dart'; +import 'package:stream_chat/src/core/models/filter.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:test/test.dart'; + +import '../../mocks.dart'; + +void main() { + Response successResponse(String path, {Object? data}) => Response( + data: data, + requestOptions: RequestOptions(path: path), + statusCode: 200, + ); + + late final client = MockHttpClient(); + late UserApi userApi; + + setUp(() { + userApi = UserApi(client); + }); + + test('queryUsers', () async { + const presence = true; + final filter = Filter.in_('cid', const ['test-cid-1', 'test-cid-2']); + const sort = [SortOption('test-field')]; + const pagination = PaginationParams(); + + const path = '/users'; + + final users = List.generate(3, (index) => User(id: 'test-user-id-$index')); + + when(() => client.get(path, queryParameters: any(named: 'queryParameters'))) + .thenAnswer((_) async => successResponse(path, data: { + 'users': [...users.map((it) => it.toJson())] + })); + + final res = await userApi.queryUsers( + presence: presence, + filter: filter, + sort: sort, + pagination: pagination, + ); + + expect(res, isNotNull); + expect(res.users.length, users.length); + + verify( + () => client.get(path, queryParameters: any(named: 'queryParameters')), + ).called(1); + verifyNoMoreInteractions(client); + }); + + test('updateUsers', () async { + final users = List.generate(3, (index) => User(id: 'test-user-id-$index')); + + const path = '/users'; + + final updatedUsers = {for (final user in users) user.id: user}; + + when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + (_) async => successResponse(path, data: { + 'users': updatedUsers + .map((key, value) => MapEntry(key, value.toJson())) + })); + + final res = await userApi.updateUsers(users); + + expect(res, isNotNull); + expect(res.users.length, updatedUsers.length); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); +} diff --git a/packages/stream_chat/test/src/core/http/interceptor/connection_id_interceptor.dart b/packages/stream_chat/test/src/core/http/interceptor/connection_id_interceptor_test.dart similarity index 100% rename from packages/stream_chat/test/src/core/http/interceptor/connection_id_interceptor.dart rename to packages/stream_chat/test/src/core/http/interceptor/connection_id_interceptor_test.dart diff --git a/packages/stream_chat/test/src/models/action_test.dart b/packages/stream_chat/test/src/core/models/action_test.dart similarity index 100% rename from packages/stream_chat/test/src/models/action_test.dart rename to packages/stream_chat/test/src/core/models/action_test.dart diff --git a/packages/stream_chat/test/src/models/attachment_test.dart b/packages/stream_chat/test/src/core/models/attachment_test.dart similarity index 100% rename from packages/stream_chat/test/src/models/attachment_test.dart rename to packages/stream_chat/test/src/core/models/attachment_test.dart diff --git a/packages/stream_chat/test/src/models/channel_state_test.dart b/packages/stream_chat/test/src/core/models/channel_state_test.dart similarity index 100% rename from packages/stream_chat/test/src/models/channel_state_test.dart rename to packages/stream_chat/test/src/core/models/channel_state_test.dart diff --git a/packages/stream_chat/test/src/models/channel_test.dart b/packages/stream_chat/test/src/core/models/channel_test.dart similarity index 100% rename from packages/stream_chat/test/src/models/channel_test.dart rename to packages/stream_chat/test/src/core/models/channel_test.dart diff --git a/packages/stream_chat/test/src/models/command_test.dart b/packages/stream_chat/test/src/core/models/command_test.dart similarity index 100% rename from packages/stream_chat/test/src/models/command_test.dart rename to packages/stream_chat/test/src/core/models/command_test.dart diff --git a/packages/stream_chat/test/src/models/device_test.dart b/packages/stream_chat/test/src/core/models/device_test.dart similarity index 100% rename from packages/stream_chat/test/src/models/device_test.dart rename to packages/stream_chat/test/src/core/models/device_test.dart diff --git a/packages/stream_chat/test/src/models/event_test.dart b/packages/stream_chat/test/src/core/models/event_test.dart similarity index 100% rename from packages/stream_chat/test/src/models/event_test.dart rename to packages/stream_chat/test/src/core/models/event_test.dart diff --git a/packages/stream_chat/test/src/models/filter_test.dart b/packages/stream_chat/test/src/core/models/filter_test.dart similarity index 100% rename from packages/stream_chat/test/src/models/filter_test.dart rename to packages/stream_chat/test/src/core/models/filter_test.dart diff --git a/packages/stream_chat/test/src/models/member_test.dart b/packages/stream_chat/test/src/core/models/member_test.dart similarity index 100% rename from packages/stream_chat/test/src/models/member_test.dart rename to packages/stream_chat/test/src/core/models/member_test.dart diff --git a/packages/stream_chat/test/src/models/message_test.dart b/packages/stream_chat/test/src/core/models/message_test.dart similarity index 100% rename from packages/stream_chat/test/src/models/message_test.dart rename to packages/stream_chat/test/src/core/models/message_test.dart diff --git a/packages/stream_chat/test/src/models/reaction_test.dart b/packages/stream_chat/test/src/core/models/reaction_test.dart similarity index 100% rename from packages/stream_chat/test/src/models/reaction_test.dart rename to packages/stream_chat/test/src/core/models/reaction_test.dart diff --git a/packages/stream_chat/test/src/models/read_test.dart b/packages/stream_chat/test/src/core/models/read_test.dart similarity index 100% rename from packages/stream_chat/test/src/models/read_test.dart rename to packages/stream_chat/test/src/core/models/read_test.dart diff --git a/packages/stream_chat/test/src/models/serialization_test.dart b/packages/stream_chat/test/src/core/models/serialization_test.dart similarity index 100% rename from packages/stream_chat/test/src/models/serialization_test.dart rename to packages/stream_chat/test/src/core/models/serialization_test.dart diff --git a/packages/stream_chat/test/src/models/user_test.dart b/packages/stream_chat/test/src/core/models/user_test.dart similarity index 100% rename from packages/stream_chat/test/src/models/user_test.dart rename to packages/stream_chat/test/src/core/models/user_test.dart diff --git a/packages/stream_chat/test/src/fakes.dart b/packages/stream_chat/test/src/fakes.dart index e96cbd12..b7e431b1 100644 --- a/packages/stream_chat/test/src/fakes.dart +++ b/packages/stream_chat/test/src/fakes.dart @@ -1,6 +1,18 @@ +import 'package:dio/dio.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/core/api/channel_api.dart'; +import 'package:stream_chat/src/core/api/device_api.dart'; +import 'package:stream_chat/src/core/api/general_api.dart'; +import 'package:stream_chat/src/core/api/message_api.dart'; +import 'package:stream_chat/src/core/api/moderation_api.dart'; +import 'package:stream_chat/src/core/api/stream_chat_api.dart'; +import 'package:stream_chat/src/core/api/user_api.dart'; +import 'package:stream_chat/src/core/api/guest_api.dart'; import 'package:stream_chat/src/core/http/token.dart'; import 'package:stream_chat/src/core/http/token_manager.dart'; +import 'package:stream_chat/stream_chat.dart'; + +import 'mocks.dart'; class FakeTokenManager extends Fake implements TokenManager { final token = Token.development('test-user-id'); @@ -25,3 +37,48 @@ class FakeTokenManager extends Fake implements TokenManager { @override void reset() {} } + +class FakeMultiPartFile extends Fake implements MultipartFile {} + +class FakeChatApi extends Fake implements StreamChatApi { + UserApi? _user; + + @override + UserApi get user => _user ??= MockUserApi(); + + GuestApi? _guest; + + @override + GuestApi get guest => _guest ??= MockGuestApi(); + + MessageApi? _message; + + @override + MessageApi get message => _message ??= MockMessageApi(); + + ChannelApi? _channel; + + @override + ChannelApi get channel => _channel ??= MockChannelApi(); + + DeviceApi? _device; + + @override + DeviceApi get device => _device ??= MockDeviceApi(); + + ModerationApi? _moderation; + + @override + ModerationApi get moderation => _moderation ??= MockModerationApi(); + + GeneralApi? _general; + + @override + GeneralApi get general => _general ??= MockGeneralApi(); + + AttachmentFileUploader? _fileUploader; + + @override + AttachmentFileUploader get fileUploader => + _fileUploader ??= MockAttachmentFileUploader(); +} diff --git a/packages/stream_chat/test/src/matchers.dart b/packages/stream_chat/test/src/matchers.dart new file mode 100644 index 00000000..a2be680f --- /dev/null +++ b/packages/stream_chat/test/src/matchers.dart @@ -0,0 +1,19 @@ +import 'package:dio/dio.dart' show MultipartFile; +import 'package:test/test.dart'; + +Matcher isSameMultipartFileAs(MultipartFile targetFile) => + _IsSameMultipartFileAs(targetFile: targetFile); + +class _IsSameMultipartFileAs extends Matcher { + const _IsSameMultipartFileAs({required this.targetFile}); + + final MultipartFile targetFile; + + @override + Description describe(Description description) => + description.add('is same multipartFile as $targetFile'); + + @override + bool matches(covariant MultipartFile file, Map matchState) => + file.length == targetFile.length; +} diff --git a/packages/stream_chat/test/src/mocks.dart b/packages/stream_chat/test/src/mocks.dart index be4f15e5..411f06a3 100644 --- a/packages/stream_chat/test/src/mocks.dart +++ b/packages/stream_chat/test/src/mocks.dart @@ -1,6 +1,14 @@ import 'package:dio/dio.dart'; import 'package:logging/logging.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/core/api/attachment_file_uploader.dart'; +import 'package:stream_chat/src/core/api/channel_api.dart'; +import 'package:stream_chat/src/core/api/device_api.dart'; +import 'package:stream_chat/src/core/api/general_api.dart'; +import 'package:stream_chat/src/core/api/guest_api.dart'; +import 'package:stream_chat/src/core/api/message_api.dart'; +import 'package:stream_chat/src/core/api/moderation_api.dart'; +import 'package:stream_chat/src/core/api/user_api.dart'; import 'package:stream_chat/src/core/http/connection_id_manager.dart'; import 'package:stream_chat/src/core/http/stream_http_client.dart'; import 'package:stream_chat/src/core/http/token_manager.dart'; @@ -32,3 +40,20 @@ class MockHttpClient extends Mock implements StreamHttpClient {} class MockTokenManager extends Mock implements TokenManager {} class MockConnectionIdManager extends Mock implements ConnectionIdManager {} + +class MockUserApi extends Mock implements UserApi {} + +class MockGuestApi extends Mock implements GuestApi {} + +class MockMessageApi extends Mock implements MessageApi {} + +class MockChannelApi extends Mock implements ChannelApi {} + +class MockDeviceApi extends Mock implements DeviceApi {} + +class MockModerationApi extends Mock implements ModerationApi {} + +class MockGeneralApi extends Mock implements GeneralApi {} + +class MockAttachmentFileUploader extends Mock + implements AttachmentFileUploader {} diff --git a/packages/stream_chat/test/src/utils.dart b/packages/stream_chat/test/src/utils.dart new file mode 100644 index 00000000..b17e35a9 --- /dev/null +++ b/packages/stream_chat/test/src/utils.dart @@ -0,0 +1,15 @@ +import 'dart:io'; + +File assetFile(String name) { + final dir = currentDirectory.path; + return File('$dir/test/assets/$name'); +} + +// https://github.com/flutter/flutter/issues/20907 +Directory get currentDirectory { + var directory = Directory.current; + if (directory.path.endsWith('/test')) { + directory = directory.parent; + } + return directory; +} From dc6c3094e96f49cd182a72335f22f30762281f6e Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 9 Jun 2021 16:11:59 +0200 Subject: [PATCH 021/150] performance fixes --- packages/stream_chat/lib/src/api/channel.dart | 2 +- packages/stream_chat/lib/src/client.dart | 6 +- .../stream_chat_flutter/analysis_options.yaml | 4 +- .../lib/src/attachment/file_attachment.dart | 6 +- .../lib/src/attachment/giphy_attachment.dart | 2 +- .../lib/src/attachment/image_attachment.dart | 12 +- .../lib/src/channel_header.dart | 3 +- .../lib/src/channel_image.dart | 10 +- .../lib/src/channel_list_view.dart | 194 ++-- .../lib/src/channel_preview.dart | 35 +- .../lib/src/connection_status_builder.dart | 44 +- .../lib/src/emoji/emoji.dart | 3 + .../lib/src/extension.dart | 8 +- .../lib/src/info_tile.dart | 3 + .../lib/src/message_actions_modal.dart | 244 +++-- .../lib/src/message_input.dart | 945 +++++++++--------- .../lib/src/message_list_view.dart | 218 ++-- .../lib/src/message_reactions_modal.dart | 192 ++-- .../lib/src/message_widget.dart | 146 +-- .../lib/src/quoted_message_widget.dart | 6 +- .../lib/src/reaction_picker.dart | 171 ++-- .../lib/src/typing_indicator.dart | 21 +- .../lib/src/url_attachment.dart | 2 +- .../stream_chat_flutter/lib/src/utils.dart | 2 +- .../lib/src/better_stream_builder.dart | 43 +- .../lib/src/stream_channel.dart | 60 +- 26 files changed, 1224 insertions(+), 1158 deletions(-) diff --git a/packages/stream_chat/lib/src/api/channel.dart b/packages/stream_chat/lib/src/api/channel.dart index a2fc4ed3..1e7f824e 100644 --- a/packages/stream_chat/lib/src/api/channel.dart +++ b/packages/stream_chat/lib/src/api/channel.dart @@ -1709,7 +1709,7 @@ class ChannelClientState { final BehaviorSubject _unreadCountController = BehaviorSubject.seeded(0); /// Unread count getter as a stream - Stream get unreadCountStream => _unreadCountController.stream; + Stream get unreadCountStream => _unreadCountController.stream.distinct(); /// Unread count getter int? get unreadCount => _unreadCountController.value; diff --git a/packages/stream_chat/lib/src/client.dart b/packages/stream_chat/lib/src/client.dart index 1b98762d..17b5654c 100644 --- a/packages/stream_chat/lib/src/client.dart +++ b/packages/stream_chat/lib/src/client.dart @@ -1532,13 +1532,15 @@ class ClientState { int? get unreadChannels => _unreadChannelsController.valueOrNull; /// The current unread channels count as a stream - Stream get unreadChannelsStream => _unreadChannelsController.stream; + Stream get unreadChannelsStream => + _unreadChannelsController.stream.distinct(); /// The current total unread messages count int? get totalUnreadCount => _totalUnreadCountController.valueOrNull; /// The current total unread messages count as a stream - Stream get totalUnreadCountStream => _totalUnreadCountController.stream; + Stream get totalUnreadCountStream => + _totalUnreadCountController.stream.distinct(); /// The current list of channels in memory as a stream Stream?> get channelsStream => diff --git a/packages/stream_chat_flutter/analysis_options.yaml b/packages/stream_chat_flutter/analysis_options.yaml index a37a0fb7..f0a87ea5 100644 --- a/packages/stream_chat_flutter/analysis_options.yaml +++ b/packages/stream_chat_flutter/analysis_options.yaml @@ -3,10 +3,10 @@ analyzer: - extension-methods exclude: - lib/**/*.g.dart -# - example/** + - example/** - lib/src/emoji - lib/**/*.freezed.dart -# - test/** + - test/** linter: rules: diff --git a/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart index a676e576..7ec5fb04 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart @@ -3,10 +3,10 @@ import 'package:flutter/material.dart'; import 'package:shimmer/shimmer.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/upload_progress_indicator.dart'; import 'package:stream_chat_flutter/src/utils.dart'; import 'package:stream_chat_flutter/src/video_thumbnail_image.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -import 'package:stream_chat_flutter/src/upload_progress_indicator.dart'; // ignore: always_use_package_imports import 'attachment_widget.dart'; @@ -103,7 +103,7 @@ class FileAttachment extends AttachmentWidget { Widget _getFileTypeImage(BuildContext context) { if (isImageAttachment) { return Material( - clipBehavior: Clip.antiAlias, + clipBehavior: Clip.hardEdge, type: MaterialType.transparency, shape: _getDefaultShape(context), child: source.when( @@ -154,7 +154,7 @@ class FileAttachment extends AttachmentWidget { if (isVideoAttachment) { return Material( - clipBehavior: Clip.antiAlias, + clipBehavior: Clip.hardEdge, type: MaterialType.transparency, shape: _getDefaultShape(context), child: source.when( diff --git a/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart index 1bbbd54f..1572061e 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart @@ -53,7 +53,7 @@ class GiphyAttachment extends AttachmentWidget { Card( color: StreamChatTheme.of(context).colorTheme.white, elevation: 2, - clipBehavior: Clip.antiAlias, + clipBehavior: Clip.hardEdge, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( topRight: Radius.circular(16), diff --git a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart index 5c431d6e..c677435a 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart @@ -74,16 +74,16 @@ class ImageAttachment extends AttachmentWidget { if (imageUri.host == 'stream-io-cdn.com') { imageUri = imageUri.replace(queryParameters: { ...imageUri.queryParameters, - 'h': '500', - 'w': '500', + 'h': '400', + 'w': '400', 'crop': 'center', 'resize': 'crop', }); } else if (imageUri.host == 'stream-cloud-uploads.imgix.net') { imageUri = imageUri.replace(queryParameters: { ...imageUri.queryParameters, - 'height': '500', - 'width': '500', + 'height': '400', + 'width': '400', 'fit': 'crop', }); } @@ -92,10 +92,10 @@ class ImageAttachment extends AttachmentWidget { return _buildImageAttachment( context, CachedNetworkImage( - cacheKey: imageUri.path, + cacheKey: imageUrl, height: size?.height, width: size?.width, - placeholder: (_, __) { + placeholder: (context, __) { final image = Image.asset( 'images/placeholder.png', fit: BoxFit.cover, diff --git a/packages/stream_chat_flutter/lib/src/channel_header.dart b/packages/stream_chat_flutter/lib/src/channel_header.dart index 7773b36f..16c1f1b7 100644 --- a/packages/stream_chat_flutter/lib/src/channel_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_header.dart @@ -133,8 +133,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { } return InfoTile( - // ignore: avoid_bool_literals_in_conditional_expressions - showMessage: showConnectionStateTile ? showStatus : false, + showMessage: showConnectionStateTile && showStatus, message: statusString, child: AppBar( textTheme: Theme.of(context).textTheme, diff --git a/packages/stream_chat_flutter/lib/src/channel_image.dart b/packages/stream_chat_flutter/lib/src/channel_image.dart index aefdb44e..dd9bb314 100644 --- a/packages/stream_chat_flutter/lib/src/channel_image.dart +++ b/packages/stream_chat_flutter/lib/src/channel_image.dart @@ -94,15 +94,17 @@ class ChannelImage extends StatelessWidget { } else if (channel.state?.members.length == 2) { final otherMember = channel.state?.members .firstWhere((member) => member.user?.id != streamChat.user?.id); - return StreamBuilder( - stream: streamChat.client.state.usersStream.map( - (users) => users[otherMember?.userId] ?? otherMember!.user!), + return BetterStreamBuilder( + stream: streamChat.client.state.usersStream + .map((users) => + users[otherMember?.userId] ?? otherMember!.user!) + .distinct(), initialData: otherMember!.user, builder: (context, snapshot) => UserAvatar( borderRadius: borderRadius ?? chatThemeData .channelPreviewTheme.avatarTheme?.borderRadius, - user: snapshot.data ?? otherMember.user!, + user: snapshot ?? otherMember.user!, constraints: constraints ?? chatThemeData .channelPreviewTheme.avatarTheme?.constraints, diff --git a/packages/stream_chat_flutter/lib/src/channel_list_view.dart b/packages/stream_chat_flutter/lib/src/channel_list_view.dart index f5780d73..12b73f72 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -1,11 +1,10 @@ -import 'package:collection/collection.dart' show IterableExtension; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:shimmer/shimmer.dart'; import 'package:stream_chat_flutter/src/channel_bottom_sheet.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; -import 'package:stream_chat_flutter/src/utils.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; @@ -245,10 +244,7 @@ class _ChannelListViewState extends State { } } - return AnimatedSwitcher( - duration: const Duration(milliseconds: 500), - child: child, - ); + return child; } Widget _buildEmptyWidget(BuildContext context) => LayoutBuilder( @@ -473,97 +469,97 @@ class _ChannelListViewState extends State { final channel = channels[i]; return StreamChannel( - key: ValueKey('CHANNEL-${channel.id}'), + key: ValueKey('CHANNEL-${channel.cid}'), channel: channel, - child: Builder( - builder: (context) => Slidable( - controller: _slideController, - enabled: widget.swipeToAction, - actionPane: const SlidableBehindActionPane(), - actionExtentRatio: 0.12, - secondaryActions: widget.swipeActions - ?.map((e) => IconSlideAction( - color: e.color, - iconWidget: e.iconWidget, - onTap: () { - e.onTap?.call(channel); - }, - )) - .toList() ?? - [ + child: Slidable( + controller: _slideController, + enabled: widget.swipeToAction, + actionPane: const SlidableBehindActionPane(), + actionExtentRatio: 0.12, + secondaryActions: widget.swipeActions + ?.map((e) => IconSlideAction( + color: e.color, + iconWidget: e.iconWidget, + onTap: () { + e.onTap?.call(channel); + }, + )) + .toList() ?? + [ + IconSlideAction( + color: backgroundColor, + icon: Icons.more_horiz, + onTap: widget.onMoreDetailsPressed != null + ? () { + widget.onMoreDetailsPressed!(channel); + } + : () { + showModalBottomSheet( + clipBehavior: Clip.hardEdge, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(32), + topRight: Radius.circular(32), + ), + ), + context: context, + builder: (context) => StreamChannel( + channel: channel, + child: ChannelBottomSheet( + onViewInfoTap: () { + widget.onViewInfoTap?.call(channel); + }, + ), + ), + ); + }, + ), + if ([ + 'admin', + 'owner', + ].contains(channel.state!.members + .firstWhereOrNull( + (m) => m.userId == channel.client.state.user?.id) + ?.role)) IconSlideAction( color: backgroundColor, - icon: Icons.more_horiz, - onTap: widget.onMoreDetailsPressed != null + iconWidget: StreamSvgIcon.delete( + color: chatThemeData.colorTheme.accentRed, + ), + onTap: widget.onDeletePressed != null ? () { - widget.onMoreDetailsPressed!(channel); + widget.onDeletePressed!(channel); } - : () { - showModalBottomSheet( - clipBehavior: Clip.hardEdge, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(32), - topRight: Radius.circular(32), - ), - ), - context: context, - builder: (context) => StreamChannel( - channel: channel, - child: ChannelBottomSheet( - onViewInfoTap: () { - widget.onViewInfoTap?.call(channel); - }, - ), + : () async { + final res = await showConfirmationDialog( + context, + title: 'Delete Conversation', + okText: 'DELETE', + question: + // ignore: lines_longer_than_80_chars + 'Are you sure you want to delete this conversation?', + cancelText: 'CANCEL', + icon: StreamSvgIcon.delete( + color: chatThemeData.colorTheme.accentRed, ), ); + if (res == true) { + await channel.delete(); + } }, ), - if ([ - 'admin', - 'owner', - ].contains(channel.state!.members - .firstWhereOrNull( - (m) => m.userId == channel.client.state.user?.id) - ?.role)) - IconSlideAction( - color: backgroundColor, - iconWidget: StreamSvgIcon.delete( - color: chatThemeData.colorTheme.accentRed, - ), - onTap: widget.onDeletePressed != null - ? () { - widget.onDeletePressed!(channel); - } - : () async { - final res = await showConfirmationDialog( - context, - title: 'Delete Conversation', - okText: 'DELETE', - question: - // ignore: lines_longer_than_80_chars - 'Are you sure you want to delete this conversation?', - cancelText: 'CANCEL', - icon: StreamSvgIcon.delete( - color: chatThemeData.colorTheme.accentRed, - ), - ); - if (res == true) { - await channel.delete(); - } - }, - ), - ], - child: Container( + ], + child: DecoratedBox( + decoration: BoxDecoration( color: chatThemeData.colorTheme.whiteSnow, - child: widget.channelPreviewBuilder?.call(context, channel) ?? - ChannelPreview( - onLongPress: widget.onChannelLongPress, - channel: channel, - onImageTap: () => widget.onImageTap?.call(channel), - onTap: (channel) => onTap(channel, widget.channelWidget), - ), ), + child: widget.channelPreviewBuilder?.call(context, channel) ?? + ChannelPreview( + onLongPress: widget.onChannelLongPress, + channel: channel, + onImageTap: () => widget.onImageTap?.call(channel), + onTap: (channel) => onTap(channel, widget.channelWidget), + ), ), ), ); @@ -637,12 +633,10 @@ class _ChannelListViewState extends State { context, ChannelsBlocState channelsProvider, ) => - StreamBuilder( + BetterStreamBuilder( stream: channelsProvider.queryChannelsLoading, initialData: false, - builder: (context, snapshot) { - if (snapshot.hasError) { - return Container( + errorBuilder: (context, err) => Container( color: StreamChatTheme.of(context) .colorTheme .accentRed @@ -653,17 +647,15 @@ class _ChannelListViewState extends State { child: Text('Error loading channels'), ), ), - ); - } - return snapshot.data! - ? const Center( - child: Padding( - padding: EdgeInsets.all(16), - child: CircularProgressIndicator(), - ), - ) - : const Offstage(); - }); + ), + builder: (context, snapshot) => snapshot + ? const Center( + child: Padding( + padding: EdgeInsets.all(16), + child: CircularProgressIndicator(), + ), + ) + : const Offstage()); Widget _separatorBuilder(context, i) { final effect = StreamChatTheme.of(context).colorTheme.borderBottom; diff --git a/packages/stream_chat_flutter/lib/src/channel_preview.dart b/packages/stream_chat_flutter/lib/src/channel_preview.dart index 3189b0cb..bb41f652 100644 --- a/packages/stream_chat_flutter/lib/src/channel_preview.dart +++ b/packages/stream_chat_flutter/lib/src/channel_preview.dart @@ -1,4 +1,5 @@ -import 'package:collection/collection.dart' show IterableExtension; +import 'package:collection/collection.dart' + show IterableExtension, ListEquality; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:jiffy/jiffy.dart'; @@ -69,12 +70,12 @@ class ChannelPreview extends StatelessWidget { Widget build(BuildContext context) { final channelPreviewTheme = StreamChatTheme.of(context).channelPreviewTheme; final streamChatState = StreamChat.of(context); - - return StreamBuilder( + return BetterStreamBuilder( stream: channel.isMutedStream, initialData: channel.isMuted, - builder: (context, snapshot) => Opacity( - opacity: snapshot.data! ? 0.5 : 1, + builder: (context, snapshot) => AnimatedOpacity( + opacity: snapshot ? 0.5 : 1, + duration: const Duration(milliseconds: 300), child: ListTile( visualDensity: VisualDensity.compact, contentPadding: const EdgeInsets.symmetric( @@ -103,14 +104,16 @@ class ChannelPreview extends StatelessWidget { textStyle: channelPreviewTheme.title, ), ), - StreamBuilder>( + BetterStreamBuilder?>( stream: channel.state?.membersStream, initialData: channel.state?.members, + comparator: const ListEquality().equals, builder: (context, snapshot) { - if (!snapshot.hasData || - snapshot.data!.isEmpty || - !snapshot.data!.any((Member e) => - e.user!.id == channel.client.state.user?.id)) { + if (snapshot?.isEmpty == true || + snapshot?.any((Member e) => + e.user!.id == + channel.client.state.user?.id) != + true) { return const SizedBox(); } return UnreadIndicator( @@ -159,14 +162,14 @@ class ChannelPreview extends StatelessWidget { )); } - Widget _buildDate(BuildContext context) => StreamBuilder( + Widget _buildDate(BuildContext context) => BetterStreamBuilder( stream: channel.lastMessageAtStream, initialData: channel.lastMessageAt, builder: (context, snapshot) { - if (!snapshot.hasData) { - return const SizedBox(); + if (snapshot == null) { + return const Offstage(); } - final lastMessageAt = snapshot.data!.toLocal(); + final lastMessageAt = snapshot.toLocal(); String stringDate; final now = DateTime.now(); @@ -219,11 +222,11 @@ class ChannelPreview extends StatelessWidget { } Widget _buildLastMessage(BuildContext context) => - StreamBuilder?>( + BetterStreamBuilder?>( stream: channel.state!.messagesStream, initialData: channel.state!.messages, builder: (context, snapshot) { - final lastMessage = snapshot.data + final lastMessage = snapshot ?.lastWhereOrNull((m) => m.shadowed != true && !m.isDeleted); if (lastMessage == null) { return const SizedBox(); diff --git a/packages/stream_chat_flutter/lib/src/connection_status_builder.dart b/packages/stream_chat_flutter/lib/src/connection_status_builder.dart index 9137265f..98daa580 100644 --- a/packages/stream_chat_flutter/lib/src/connection_status_builder.dart +++ b/packages/stream_chat_flutter/lib/src/connection_status_builder.dart @@ -8,7 +8,7 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// /// The widget will use the closest [StreamChatClient.wsConnectionStatusStream] /// in case no stream is provided. -class ConnectionStatusBuilder extends StatelessWidget { +class ConnectionStatusBuilder extends StatefulWidget { /// Creates a new ConnectionStatusBuilder const ConnectionStatusBuilder({ Key? key, @@ -36,20 +36,32 @@ class ConnectionStatusBuilder extends StatelessWidget { statusBuilder; @override - Widget build(BuildContext context) { - final client = StreamChat.of(context).client; - final stream = connectionStatusStream ?? client.wsConnectionStatusStream; - return BetterStreamBuilder( - initialData: initialStatus ?? client.wsConnectionStatus, - stream: stream, - loadingBuilder: loadingBuilder, - errorBuilder: (context, error) { - if (errorBuilder != null) { - return errorBuilder!(context, error); - } - return const Offstage(); - }, - builder: statusBuilder, - ); + _ConnectionStatusBuilderState createState() => + _ConnectionStatusBuilderState(); +} + +class _ConnectionStatusBuilderState extends State { + late StreamChatClient client; + late Stream stream; + + @override + Widget build(BuildContext context) => BetterStreamBuilder( + initialData: widget.initialStatus ?? client.wsConnectionStatus, + stream: stream, + loadingBuilder: widget.loadingBuilder, + errorBuilder: (context, error) { + if (widget.errorBuilder != null) { + return widget.errorBuilder!(context, error); + } + return const Offstage(); + }, + builder: widget.statusBuilder, + ); + + @override + void didChangeDependencies() { + client = StreamChat.of(context).client; + stream = widget.connectionStatusStream ?? client.wsConnectionStatusStream; + super.didChangeDependencies(); } } diff --git a/packages/stream_chat_flutter/lib/src/emoji/emoji.dart b/packages/stream_chat_flutter/lib/src/emoji/emoji.dart index 617e372a..38af85af 100644 --- a/packages/stream_chat_flutter/lib/src/emoji/emoji.dart +++ b/packages/stream_chat_flutter/lib/src/emoji/emoji.dart @@ -114325,6 +114325,9 @@ class Emoji { /// Get all Emojis static List all() => List.unmodifiable(_emojis); + static Iterable chars() => + _emojis.map((e) => e.char).whereType(); + /// Returns Emoji by [char] and character static Emoji? byChar(String char) { return _emojis.firstWhereOrNull((Emoji emoji) => emoji.char == char); diff --git a/packages/stream_chat_flutter/lib/src/extension.dart b/packages/stream_chat_flutter/lib/src/extension.dart index 3b6ba3e2..5161c8e8 100644 --- a/packages/stream_chat_flutter/lib/src/extension.dart +++ b/packages/stream_chat_flutter/lib/src/extension.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/emoji/emoji.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -final _emojis = Emoji.all(); +final _emojiChars = Emoji.chars(); /// String extension extension StringExtension on String { @@ -17,10 +17,10 @@ extension StringExtension on String { /// 1 to 3 emojis: big size with no text bubble. /// 4+ emojis or emojis+text: standard size with text bubble. bool get isOnlyEmoji { + if (isEmpty) return false; + if (length > 3) return false; final characters = trim().characters; - if (characters.isEmpty) return false; - if (characters.length > 3) return false; - return characters.every((c) => _emojis.map((e) => e.char).contains(c)); + return characters.every(_emojiChars.contains); } } diff --git a/packages/stream_chat_flutter/lib/src/info_tile.dart b/packages/stream_chat_flutter/lib/src/info_tile.dart index 1e935942..b420cadc 100644 --- a/packages/stream_chat_flutter/lib/src/info_tile.dart +++ b/packages/stream_chat_flutter/lib/src/info_tile.dart @@ -40,6 +40,9 @@ class InfoTile extends StatelessWidget { @override Widget build(BuildContext context) { final chatThemeData = StreamChatTheme.of(context); + if (!showMessage) { + return child; + } return PortalEntry( visible: showMessage, portalAnchor: tileAnchor ?? Alignment.topCenter, diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart index 1da4fcea..bdee86a0 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -147,6 +147,119 @@ class _MessageActionsModalState extends State { widget.message.attachments.any((it) => it.type == 'file') == true; final streamChatThemeData = StreamChatTheme.of(context); + + final child = Center( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8), + child: Column( + crossAxisAlignment: widget.reverse + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + if (widget.showReactions && + (widget.message.status == MessageSendingStatus.sent)) + Align( + alignment: Alignment( + user?.id == widget.message.user?.id + ? (divFactor >= 1.0 ? -0.2 : (1.2 - divFactor)) + : (divFactor >= 1.0 ? 0.2 : -(1.2 - divFactor)), + 0), + child: ReactionPicker( + message: widget.message, + ), + ), + const SizedBox(height: 8), + IgnorePointer( + child: MessageWidget( + key: const Key('MessageWidget'), + reverse: widget.reverse, + attachmentBorderRadiusGeometry: widget + .attachmentBorderRadiusGeometry + ?.mirrorBorderIfReversed(reverse: !widget.reverse), + message: widget.message.copyWith( + text: widget.message.text!.length > 200 + // ignore: lines_longer_than_80_chars + ? '${widget.message.text!.substring(0, 200)}...' + : widget.message.text, + ), + messageTheme: widget.messageTheme, + showReactions: false, + showUsername: false, + showReplyMessage: false, + showUserAvatar: widget.showUserAvatar, + attachmentPadding: EdgeInsets.all( + hasFileAttachment ? 4 : 2, + ), + showTimestamp: false, + translateUserAvatar: false, + padding: const EdgeInsets.all(0), + textPadding: EdgeInsets.symmetric( + vertical: 8, + horizontal: widget.message.text!.isOnlyEmoji ? 0 : 16.0, + ), + showReactionPickerIndicator: widget.showReactions && + (widget.message.status == MessageSendingStatus.sent), + showSendingIndicator: false, + shape: widget.messageShape, + attachmentShape: widget.attachmentShape, + showPinHighlight: false, + textBuilder: widget.textBuilder, + ), + ), + const SizedBox(height: 8), + Padding( + padding: EdgeInsets.only( + left: widget.reverse ? 0 : 40, + ), + child: SizedBox( + width: mediaQueryData.size.width * 0.75, + child: Material( + color: streamChatThemeData.colorTheme.whiteSnow, + clipBehavior: Clip.hardEdge, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (widget.showReplyMessage && + widget.message.status == MessageSendingStatus.sent) + _buildReplyButton(context), + if (widget.showThreadReplyMessage && + (widget.message.status == + MessageSendingStatus.sent) && + widget.message.parentId == null) + _buildThreadReplyButton(context), + if (widget.showResendMessage) + _buildResendMessage(context), + if (widget.showEditMessage) _buildEditMessage(context), + if (widget.showCopyMessage) _buildCopyButton(context), + if (widget.showFlagButton) _buildFlagButton(context), + if (widget.showPinButton) _buildPinButton(context), + if (widget.showDeleteMessage) + _buildDeleteButton(context), + ...widget.customActions + .map((action) => _buildCustomAction( + context, + action, + )) + ].insertBetween( + Container( + height: 1, + color: streamChatThemeData.colorTheme.greyWhisper, + ), + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + return GestureDetector( behavior: HitTestBehavior.translucent, onTap: () => Navigator.maybePop(context), @@ -168,136 +281,11 @@ class _MessageActionsModalState extends State { tween: Tween(begin: 0, end: 1), duration: const Duration(milliseconds: 300), curve: Curves.easeInOutBack, - builder: (context, val, snapshot) => Transform.scale( + builder: (context, val, child) => Transform.scale( scale: val, - child: Center( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(8), - child: Column( - crossAxisAlignment: widget.reverse - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, - children: [ - if (widget.showReactions && - (widget.message.status == - MessageSendingStatus.sent)) - Align( - alignment: Alignment( - user?.id == widget.message.user?.id - ? (divFactor >= 1.0 - ? -0.2 - : (1.2 - divFactor)) - : (divFactor >= 1.0 - ? 0.2 - : -(1.2 - divFactor)), - 0), - child: ReactionPicker( - message: widget.message, - ), - ), - const SizedBox(height: 8), - IgnorePointer( - child: MessageWidget( - key: const Key('MessageWidget'), - reverse: widget.reverse, - attachmentBorderRadiusGeometry: widget - .attachmentBorderRadiusGeometry - ?.mirrorBorderIfReversed( - reverse: !widget.reverse), - message: widget.message.copyWith( - text: widget.message.text!.length > 200 - // ignore: lines_longer_than_80_chars - ? '${widget.message.text!.substring(0, 200)}...' - : widget.message.text, - ), - messageTheme: widget.messageTheme, - showReactions: false, - showUsername: false, - showReplyMessage: false, - showUserAvatar: widget.showUserAvatar, - attachmentPadding: EdgeInsets.all( - hasFileAttachment ? 4 : 2, - ), - showTimestamp: false, - translateUserAvatar: false, - padding: const EdgeInsets.all(0), - textPadding: EdgeInsets.symmetric( - vertical: 8, - horizontal: - widget.message.text!.isOnlyEmoji ? 0 : 16.0, - ), - showReactionPickerIndicator: - widget.showReactions && - (widget.message.status == - MessageSendingStatus.sent), - showSendingIndicator: false, - shape: widget.messageShape, - attachmentShape: widget.attachmentShape, - showPinHighlight: false, - textBuilder: widget.textBuilder, - ), - ), - const SizedBox(height: 8), - Padding( - padding: EdgeInsets.only( - left: widget.reverse ? 0 : 40, - ), - child: SizedBox( - width: mediaQueryData.size.width * 0.75, - child: Material( - color: streamChatThemeData.colorTheme.whiteSnow, - clipBehavior: Clip.hardEdge, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: [ - if (widget.showReplyMessage && - widget.message.status == - MessageSendingStatus.sent) - _buildReplyButton(context), - if (widget.showThreadReplyMessage && - (widget.message.status == - MessageSendingStatus.sent) && - widget.message.parentId == null) - _buildThreadReplyButton(context), - if (widget.showResendMessage) - _buildResendMessage(context), - if (widget.showEditMessage) - _buildEditMessage(context), - if (widget.showCopyMessage) - _buildCopyButton(context), - if (widget.showFlagButton) - _buildFlagButton(context), - if (widget.showPinButton) - _buildPinButton(context), - if (widget.showDeleteMessage) - _buildDeleteButton(context), - ...widget.customActions - .map((action) => _buildCustomAction( - context, - action, - )) - ].insertBetween( - Container( - height: 1, - color: streamChatThemeData - .colorTheme.greyWhisper, - ), - ), - ), - ), - ), - ), - ], - ), - ), - ), - ), + child: child, ), + child: child, ), ], ), diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 0d2070e7..7e62f1c3 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -268,6 +268,8 @@ class MessageInputState extends State { /// The editing controller passed to the input TextField late final TextEditingController textEditingController; + late StreamChatThemeData _streamChatTheme; + bool get _hasQuotedMessage => widget.quotedMessage != null; @override @@ -305,9 +307,10 @@ class MessageInputState extends State { @override Widget build(BuildContext context) { - final streamChatThemeData = StreamChatTheme.of(context); - Widget child = Container( - color: streamChatThemeData.messageInputTheme.inputBackground, + Widget child = DecoratedBox( + decoration: BoxDecoration( + color: _streamChatTheme.messageInputTheme.inputBackground, + ), child: SafeArea( child: GestureDetector( onPanUpdate: (details) { @@ -332,7 +335,7 @@ class MessageInputState extends State { Padding( padding: const EdgeInsets.all(8), child: StreamSvgIcon.reply( - color: streamChatThemeData.colorTheme.greyGainsboro, + color: _streamChatTheme.colorTheme.greyGainsboro, ), ), const Text( @@ -390,65 +393,62 @@ class MessageInputState extends State { ], ); - Widget _buildDmCheckbox() { - final streamChatThemeData = StreamChatTheme.of(context); - return Row( - children: [ - Container( - height: 16, - width: 16, - foregroundDecoration: BoxDecoration( - border: _sendAsDm - ? null - : Border.all( - color: streamChatThemeData.colorTheme.black.withOpacity(.5), - width: 2, - ), - borderRadius: BorderRadius.circular(3), - ), - child: Center( - child: Material( + Widget _buildDmCheckbox() => Row( + children: [ + Container( + height: 16, + width: 16, + foregroundDecoration: BoxDecoration( + border: _sendAsDm + ? null + : Border.all( + color: _streamChatTheme.colorTheme.black.withOpacity(.5), + width: 2, + ), borderRadius: BorderRadius.circular(3), - color: _sendAsDm - ? streamChatThemeData.colorTheme.accentBlue - : streamChatThemeData.colorTheme.white, - child: InkWell( - onTap: () { - setState(() { - _sendAsDm = !_sendAsDm; - }); - }, - child: AnimatedCrossFade( - duration: const Duration(milliseconds: 300), - reverseDuration: const Duration(milliseconds: 300), - crossFadeState: _sendAsDm - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, - firstChild: StreamSvgIcon.check( - size: 16, - color: streamChatThemeData.colorTheme.white, - ), - secondChild: const SizedBox( - height: 16, - width: 16, + ), + child: Center( + child: Material( + borderRadius: BorderRadius.circular(3), + color: _sendAsDm + ? _streamChatTheme.colorTheme.accentBlue + : _streamChatTheme.colorTheme.white, + child: InkWell( + onTap: () { + setState(() { + _sendAsDm = !_sendAsDm; + }); + }, + child: AnimatedCrossFade( + duration: const Duration(milliseconds: 300), + reverseDuration: const Duration(milliseconds: 300), + crossFadeState: _sendAsDm + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + firstChild: StreamSvgIcon.check( + size: 16, + color: _streamChatTheme.colorTheme.white, + ), + secondChild: const SizedBox( + height: 16, + width: 16, + ), ), ), ), ), ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Text( - 'Also send as direct message', - style: streamChatThemeData.textTheme.footnote.copyWith( - color: streamChatThemeData.colorTheme.black.withOpacity(0.5), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Text( + 'Also send as direct message', + style: _streamChatTheme.textTheme.footnote.copyWith( + color: _streamChatTheme.colorTheme.black.withOpacity(0.5), + ), ), ), - ), - ], - ); - } + ], + ); Widget _animateSendButton(BuildContext context) { final sendButton = widget.activeSendButton != null @@ -463,8 +463,7 @@ class MessageInputState extends State { : CrossFadeState.showSecond, firstChild: sendButton, secondChild: widget.idleSendButton ?? _buildIdleSendButton(context), - duration: - StreamChatTheme.of(context).messageInputTheme.sendAnimationDuration!, + duration: _streamChatTheme.messageInputTheme.sendAnimationDuration!, alignment: Alignment.center, ); } @@ -478,16 +477,18 @@ class MessageInputState extends State { ? CrossFadeState.showFirst : CrossFadeState.showSecond, firstChild: IconButton( - onPressed: () => setState(() => _actionsShrunk = false), + onPressed: () { + if (_actionsShrunk) { + setState(() => _actionsShrunk = false); + } + }, icon: Transform.rotate( angle: (widget.actionsLocation == ActionsLocation.right || widget.actionsLocation == ActionsLocation.rightInside) ? pi : 0, child: StreamSvgIcon.emptyCircleLeft( - color: StreamChatTheme.of(context) - .messageInputTheme - .expandButtonColor, + color: _streamChatTheme.messageInputTheme.expandButtonColor, ), ), padding: const EdgeInsets.all(0), @@ -522,7 +523,6 @@ class MessageInputState extends State { } Expanded _buildTextInput(BuildContext context) { - final theme = StreamChatTheme.of(context); final margin = (widget.sendButtonLocation == SendButtonLocation.inside ? const EdgeInsets.only(right: 8) : EdgeInsets.zero) + @@ -530,49 +530,46 @@ class MessageInputState extends State { ? const EdgeInsets.only(left: 8) : EdgeInsets.zero); return Expanded( - child: Center( - child: Container( - clipBehavior: Clip.antiAlias, - margin: margin, - decoration: BoxDecoration( - borderRadius: theme.messageInputTheme.borderRadius, - gradient: _focusNode.hasFocus - ? theme.messageInputTheme.activeBorderGradient - : theme.messageInputTheme.idleBorderGradient, - ), - child: Padding( - padding: const EdgeInsets.all(1.5), - child: Container( - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - borderRadius: theme.messageInputTheme.borderRadius, - color: theme.messageInputTheme.inputBackground, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildReplyToMessage(), - _buildAttachments(), - LimitedBox( - maxHeight: widget.maxHeight, - child: TextField( - key: const Key('messageInputText'), - enabled: _inputEnabled, - maxLines: null, - onSubmitted: (_) => sendMessage(), - keyboardType: widget.keyboardType, - controller: textEditingController, - focusNode: _focusNode, - style: theme.messageInputTheme.inputTextStyle, - autofocus: widget.autofocus, - textAlignVertical: TextAlignVertical.center, - decoration: _getInputDecoration(), - textCapitalization: TextCapitalization.sentences, - ), - ) - ], - ), + child: Container( + clipBehavior: Clip.hardEdge, + margin: margin, + decoration: BoxDecoration( + borderRadius: _streamChatTheme.messageInputTheme.borderRadius, + gradient: _focusNode.hasFocus + ? _streamChatTheme.messageInputTheme.activeBorderGradient + : _streamChatTheme.messageInputTheme.idleBorderGradient, + ), + child: Padding( + padding: const EdgeInsets.all(1.5), + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: _streamChatTheme.messageInputTheme.borderRadius, + color: _streamChatTheme.messageInputTheme.inputBackground, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildReplyToMessage(), + _buildAttachments(), + LimitedBox( + maxHeight: widget.maxHeight, + child: TextField( + key: const Key('messageInputText'), + enabled: _inputEnabled, + maxLines: null, + onSubmitted: (_) => sendMessage(), + keyboardType: widget.keyboardType, + controller: textEditingController, + focusNode: _focusNode, + style: _streamChatTheme.messageInputTheme.inputTextStyle, + autofocus: widget.autofocus, + textAlignVertical: TextAlignVertical.center, + decoration: _getInputDecoration(), + textCapitalization: TextCapitalization.sentences, + ), + ) + ], ), ), ), @@ -581,13 +578,12 @@ class MessageInputState extends State { } InputDecoration _getInputDecoration() { - final theme = StreamChatTheme.of(context); - final passedDecoration = theme.messageInputTheme.inputDecoration; + final passedDecoration = _streamChatTheme.messageInputTheme.inputDecoration; return InputDecoration( isDense: true, hintText: _getHint(), - hintStyle: theme.messageInputTheme.inputTextStyle!.copyWith( - color: theme.colorTheme.grey, + hintStyle: _streamChatTheme.messageInputTheme.inputTextStyle!.copyWith( + color: _streamChatTheme.colorTheme.grey, ), border: const OutlineInputBorder( borderSide: BorderSide( @@ -625,7 +621,7 @@ class MessageInputState extends State { constraints: BoxConstraints.tight(const Size(64, 24)), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), - color: theme.colorTheme.accentBlue, + color: _streamChatTheme.colorTheme.accentBlue, ), alignment: Alignment.center, child: Row( @@ -637,7 +633,8 @@ class MessageInputState extends State { ), Text( _chosenCommand?.name.toUpperCase() ?? '', - style: theme.textTheme.footnoteBold.copyWith( + style: + _streamChatTheme.textTheme.footnoteBold.copyWith( color: Colors.white, ), ), @@ -828,129 +825,126 @@ class MessageInputState extends State { final renderBox = context.findRenderObject() as RenderBox; final size = renderBox.size; + final child = Padding( + padding: const EdgeInsets.all(8), + child: Card( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + color: _streamChatTheme.colorTheme.white, + clipBehavior: Clip.hardEdge, + child: Container( + constraints: BoxConstraints.loose(const Size.fromHeight(400)), + decoration: BoxDecoration( + color: _streamChatTheme.colorTheme.white, + borderRadius: BorderRadius.circular(8)), + child: ListView( + padding: const EdgeInsets.all(0), + shrinkWrap: true, + children: [ + if (commands.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8, + ), + child: StreamSvgIcon.lightning( + color: _streamChatTheme.colorTheme.accentBlue, + ), + ), + Text( + 'Instant Commands', + style: TextStyle( + color: + _streamChatTheme.colorTheme.black.withOpacity(.5), + ), + ) + ], + ), + ), + const SizedBox( + height: 10, + ), + ...commands + .map( + (c) => InkWell( + onTap: () { + _setCommand(c); + }, + child: SizedBox( + height: 40, + child: Row( + children: [ + const SizedBox( + width: 16, + ), + _buildCommandIcon(c.name), + const SizedBox( + width: 8, + ), + Text.rich( + TextSpan( + text: c.name.capitalize(), + style: const TextStyle( + fontWeight: FontWeight.bold), + children: [ + TextSpan( + text: ' /${c.name} ${c.args}', + style: _streamChatTheme.textTheme.body + .copyWith( + // ignore: lines_longer_than_80_chars + color: _streamChatTheme + // ignore: lines_longer_than_80_chars + .colorTheme + .grey, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ) + .toList(), + ], + ), + ), + ), + ); return OverlayEntry( builder: (context) => Positioned( bottom: size.height + MediaQuery.of(context).viewInsets.bottom, left: 0, right: 0, child: TweenAnimationBuilder( - tween: Tween(begin: 0, end: 1), - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOutExpo, - builder: (context, val, wid) { - final streamChatThemeData = StreamChatTheme.of(context); - return Transform.scale( - scale: val, - child: Padding( - padding: const EdgeInsets.all(8), - child: Card( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - color: streamChatThemeData.colorTheme.white, - clipBehavior: Clip.antiAlias, - child: Container( - constraints: BoxConstraints.loose( - const Size.fromHeight(400)), - decoration: BoxDecoration( - color: streamChatThemeData.colorTheme.white, - borderRadius: BorderRadius.circular(8)), - child: ListView( - padding: const EdgeInsets.all(0), - shrinkWrap: true, - children: [ - if (commands.isNotEmpty) - Padding( - padding: const EdgeInsets.only(top: 8), - child: Row( - children: [ - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8, - ), - child: StreamSvgIcon.lightning( - color: streamChatThemeData - .colorTheme.accentBlue, - ), - ), - Text( - 'Instant Commands', - style: TextStyle( - color: streamChatThemeData - .colorTheme.black - .withOpacity(.5), - ), - ) - ], - ), - ), - const SizedBox( - height: 10, - ), - ...commands - .map( - (c) => InkWell( - onTap: () { - _setCommand(c); - }, - child: SizedBox( - height: 40, - child: Row( - children: [ - const SizedBox( - width: 16, - ), - _buildCommandIcon(c.name), - const SizedBox( - width: 8, - ), - Text.rich( - TextSpan( - text: c.name.capitalize(), - style: const TextStyle( - fontWeight: - FontWeight.bold), - children: [ - TextSpan( - text: - ' /${c.name} ${c.args}', - style: streamChatThemeData - .textTheme.body - .copyWith( - // ignore: lines_longer_than_80_chars - color: streamChatThemeData - // ignore: lines_longer_than_80_chars - .colorTheme - .grey, - ), - ), - ], - ), - ), - ], - ), - ), - ), - ) - .toList(), - ], - ), - ), - ), - ), - ); - }), + tween: Tween(begin: 0, end: 1), + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOutExpo, + builder: (context, val, child) => Transform.scale( + scale: val, + child: child, + ), + child: child, + ), )); } Widget _buildFilePickerSection() { + if (!_openFilePickerSection) { + return const Offstage(); + } + final _attachmentContainsFile = _attachments.values.any((it) => it.type == 'file'); - final chatThemeData = StreamChatTheme.of(context); Color _getIconColor(int index) { - final streamChatThemeData = chatThemeData; + final streamChatThemeData = _streamChatTheme; switch (index) { case 0: return _attachments.isEmpty @@ -982,7 +976,7 @@ class MessageInputState extends State { _animateContainer ? const Duration(milliseconds: 300) : Duration.zero, height: _openFilePickerSection ? _filePickerSize : 0, child: Material( - color: chatThemeData.colorTheme.whiteSmoke, + color: _streamChatTheme.colorTheme.whiteSmoke, child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -1044,9 +1038,9 @@ class MessageInputState extends State { ); }); }, - child: Container( + child: DecoratedBox( decoration: BoxDecoration( - color: chatThemeData.colorTheme.white, + color: _streamChatTheme.colorTheme.white, borderRadius: const BorderRadius.only( topLeft: Radius.circular(16), topRight: Radius.circular(16), @@ -1057,12 +1051,14 @@ class MessageInputState extends State { child: Center( child: Padding( padding: const EdgeInsets.all(8), - child: Container( + child: SizedBox( width: 40, height: 4, - decoration: BoxDecoration( - color: chatThemeData.colorTheme.whiteSmoke, - borderRadius: BorderRadius.circular(4), + child: DecoratedBox( + decoration: BoxDecoration( + color: _streamChatTheme.colorTheme.whiteSmoke, + borderRadius: BorderRadius.circular(4), + ), ), ), ), @@ -1072,13 +1068,14 @@ class MessageInputState extends State { ), if (_openFilePickerSection) Expanded( - child: Container( + child: DecoratedBox( decoration: BoxDecoration( - color: chatThemeData.colorTheme.white, + color: _streamChatTheme.colorTheme.white, borderRadius: BorderRadius.circular(8), ), child: _PickerWidget( filePickerIndex: _filePickerIndex, + streamChatTheme: _streamChatTheme, containsFile: _attachmentContainsFile, selectedMedias: _attachments.keys.toList(), onAddMoreFilesClick: pickFile, @@ -1150,7 +1147,6 @@ class MessageInputState extends State { } Widget _buildCommandIcon(String iconType) { - final chatThemeData = StreamChatTheme.of(context); switch (iconType) { case 'giphy': return CircleAvatar( @@ -1161,7 +1157,7 @@ class MessageInputState extends State { ); case 'ban': return CircleAvatar( - backgroundColor: chatThemeData.colorTheme.accentBlue, + backgroundColor: _streamChatTheme.colorTheme.accentBlue, radius: 12, child: StreamSvgIcon.iconUserDelete( size: 16, @@ -1170,7 +1166,7 @@ class MessageInputState extends State { ); case 'flag': return CircleAvatar( - backgroundColor: chatThemeData.colorTheme.accentBlue, + backgroundColor: _streamChatTheme.colorTheme.accentBlue, radius: 12, child: StreamSvgIcon.flag( size: 14, @@ -1179,7 +1175,7 @@ class MessageInputState extends State { ); case 'imgur': return CircleAvatar( - backgroundColor: chatThemeData.colorTheme.accentBlue, + backgroundColor: _streamChatTheme.colorTheme.accentBlue, radius: 12, child: ClipOval( child: StreamSvgIcon.imgur( @@ -1189,7 +1185,7 @@ class MessageInputState extends State { ); case 'mute': return CircleAvatar( - backgroundColor: chatThemeData.colorTheme.accentBlue, + backgroundColor: _streamChatTheme.colorTheme.accentBlue, radius: 12, child: StreamSvgIcon.mute( size: 16, @@ -1198,7 +1194,7 @@ class MessageInputState extends State { ); case 'unban': return CircleAvatar( - backgroundColor: chatThemeData.colorTheme.accentBlue, + backgroundColor: _streamChatTheme.colorTheme.accentBlue, radius: 12, child: StreamSvgIcon.userAdd( size: 16, @@ -1207,7 +1203,7 @@ class MessageInputState extends State { ); case 'unmute': return CircleAvatar( - backgroundColor: chatThemeData.colorTheme.accentBlue, + backgroundColor: _streamChatTheme.colorTheme.accentBlue, radius: 12, child: StreamSvgIcon.volumeUp( size: 16, @@ -1216,7 +1212,7 @@ class MessageInputState extends State { ); default: return CircleAvatar( - backgroundColor: chatThemeData.colorTheme.accentBlue, + backgroundColor: _streamChatTheme.colorTheme.accentBlue, radius: 12, child: StreamSvgIcon.lightning( size: 16, @@ -1253,7 +1249,70 @@ class MessageInputState extends State { // ignore: cast_nullable_to_non_nullable final renderBox = context.findRenderObject() as RenderBox; final size = renderBox.size; + final child = Card( + margin: const EdgeInsets.all(8), + elevation: 2, + color: _streamChatTheme.colorTheme.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + clipBehavior: Clip.hardEdge, + child: Container( + constraints: BoxConstraints.loose(const Size.fromHeight(240)), + decoration: BoxDecoration( + color: _streamChatTheme.colorTheme.white, + ), + child: FutureBuilder>( + future: queryMembers ?? Future.value(members), + initialData: members, + builder: (context, snapshot) => ListView( + padding: const EdgeInsets.all(0), + shrinkWrap: true, + children: [ + const SizedBox( + height: 8, + ), + ...snapshot.data! + .where((it) => it.user != null) + .map( + (m) => Material( + color: _streamChatTheme.colorTheme.white, + child: InkWell( + onTap: () { + if (m.user != null) { + _mentionedUsers.add(m.user!); + } + splits[splits.length - 1] = m.user!.name; + final rejoin = splits.join('@'); + + textEditingController.value = TextEditingValue( + text: rejoin + + textEditingController.text.substring( + textEditingController.selection.start), + selection: TextSelection.collapsed( + offset: rejoin.length, + ), + ); + _debounce!.cancel(); + _mentionsOverlay?.remove(); + _mentionsOverlay = null; + }, + child: widget.mentionsTileBuilder != null + ? widget.mentionsTileBuilder!(context, m) + : MentionTile(m), + ), + ), + ) + .toList(), + const SizedBox( + height: 8, + ), + ], + ), + ), + ), + ); return OverlayEntry( builder: (context) => Positioned( bottom: size.height + MediaQuery.of(context).viewInsets.bottom, @@ -1263,78 +1322,11 @@ class MessageInputState extends State { tween: Tween(begin: 0, end: 1), duration: const Duration(milliseconds: 300), curve: Curves.easeInOutExpo, - builder: (context, val, wid) { - final chatThemeData = StreamChatTheme.of(context); - return Transform.scale( - scale: val, - child: Card( - margin: const EdgeInsets.all(8), - elevation: 2, - color: chatThemeData.colorTheme.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - clipBehavior: Clip.antiAlias, - child: Container( - constraints: BoxConstraints.loose(const Size.fromHeight(240)), - decoration: BoxDecoration( - color: chatThemeData.colorTheme.white, - ), - child: FutureBuilder>( - future: queryMembers ?? Future.value(members), - initialData: members, - builder: (context, snapshot) => ListView( - padding: const EdgeInsets.all(0), - shrinkWrap: true, - children: [ - const SizedBox( - height: 8, - ), - ...snapshot.data! - .where((it) => it.user != null) - .map( - (m) => Material( - color: chatThemeData.colorTheme.white, - child: InkWell( - onTap: () { - if (m.user != null) { - _mentionedUsers.add(m.user!); - } - - splits[splits.length - 1] = m.user!.name; - final rejoin = splits.join('@'); - - textEditingController.value = - TextEditingValue( - text: rejoin + - textEditingController.text.substring( - textEditingController - .selection.start), - selection: TextSelection.collapsed( - offset: rejoin.length, - ), - ); - _debounce!.cancel(); - _mentionsOverlay?.remove(); - _mentionsOverlay = null; - }, - child: widget.mentionsTileBuilder != null - ? widget.mentionsTileBuilder!(context, m) - : MentionTile(m), - ), - ), - ) - .toList(), - const SizedBox( - height: 8, - ), - ], - ), - ), - ), - ), - ); - }, + builder: (context, val, child) => Transform.scale( + scale: val, + child: child, + ), + child: child, ), ), ); @@ -1363,88 +1355,88 @@ class MessageInputState extends State { final renderBox = context.findRenderObject() as RenderBox; final size = renderBox.size; - return OverlayEntry(builder: (context) { - final chatThemeData = StreamChatTheme.of(context); - return Positioned( - bottom: size.height + MediaQuery.of(context).viewInsets.bottom, - left: 0, - right: 0, - child: Card( - margin: const EdgeInsets.all(8), - elevation: 2, - color: chatThemeData.colorTheme.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - clipBehavior: Clip.antiAlias, - child: Container( - constraints: BoxConstraints.loose(const Size.fromHeight(200)), - decoration: BoxDecoration( - boxShadow: const [ - BoxShadow( - spreadRadius: -8, - blurRadius: 5, - offset: Offset(0, -4), + return OverlayEntry( + builder: (context) => Positioned( + bottom: size.height + MediaQuery.of(context).viewInsets.bottom, + left: 0, + right: 0, + child: Card( + margin: const EdgeInsets.all(8), + elevation: 2, + color: _streamChatTheme.colorTheme.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), ), - ], - color: chatThemeData.colorTheme.white, - ), - child: ListView.builder( - padding: const EdgeInsets.all(0), - shrinkWrap: true, - itemCount: emojis.length + 1, - itemBuilder: (context, i) { - if (i == 0) { - return Padding( - padding: const EdgeInsets.only(left: 8, top: 8), - child: Row( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: StreamSvgIcon.smile( - color: chatThemeData.colorTheme.accentBlue, + clipBehavior: Clip.hardEdge, + child: Container( + constraints: BoxConstraints.loose(const Size.fromHeight(200)), + decoration: BoxDecoration( + boxShadow: const [ + BoxShadow( + spreadRadius: -8, + blurRadius: 5, + offset: Offset(0, -4), + ), + ], + color: _streamChatTheme.colorTheme.white, + ), + child: ListView.builder( + padding: const EdgeInsets.all(0), + shrinkWrap: true, + itemCount: emojis.length + 1, + itemBuilder: (context, i) { + if (i == 0) { + return Padding( + padding: const EdgeInsets.only(left: 8, top: 8), + child: Row( + children: [ + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 8), + child: StreamSvgIcon.smile( + color: + _streamChatTheme.colorTheme.accentBlue, + ), + ), + Flexible( + child: Text( + 'Emoji matching "$query"', + style: TextStyle( + color: _streamChatTheme.colorTheme.black + .withOpacity(.5), + ), + ), + ) + ], + ), + ); + } + + final emoji = emojis.elementAt(i - 1)!; + final themeData = Theme.of(context); + return ListTile( + title: SubstringHighlight( + text: + // ignore: lines_longer_than_80_chars + "${emoji.char} ${emoji.name!.replaceAll('_', ' ')}", + term: query, + textStyleHighlight: + themeData.textTheme.headline6!.copyWith( + fontSize: 14.5, + fontWeight: FontWeight.bold, + ), + textStyle: themeData.textTheme.headline6!.copyWith( + fontSize: 14.5, ), ), - Flexible( - child: Text( - 'Emoji matching "$query"', - style: TextStyle( - color: chatThemeData.colorTheme.black - .withOpacity(.5), - ), - ), - ) - ], - ), - ); - } - - final emoji = emojis.elementAt(i - 1)!; - final themeData = Theme.of(context); - return ListTile( - title: SubstringHighlight( - text: - // ignore: lines_longer_than_80_chars - "${emoji.char} ${emoji.name!.replaceAll('_', ' ')}", - term: query, - textStyleHighlight: - themeData.textTheme.headline6!.copyWith( - fontSize: 14.5, - fontWeight: FontWeight.bold, - ), - textStyle: themeData.textTheme.headline6!.copyWith( - fontSize: 14.5, - ), - ), - onTap: () { - _chooseEmoji(splits, emoji); - }, - ); - }), - ), - ), - ); - }); + onTap: () { + _chooseEmoji(splits, emoji); + }, + ); + }), + ), + ), + )); } void _chooseEmoji(List splits, Emoji emoji) { @@ -1483,7 +1475,7 @@ class MessageInputState extends State { reverse: true, showBorder: !containsUrl, message: widget.quotedMessage!, - messageTheme: StreamChatTheme.of(context).otherMessageTheme, + messageTheme: _streamChatTheme.otherMessageTheme, padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), ); } @@ -1568,32 +1560,29 @@ class MessageInputState extends State { ); } - Widget _buildRemoveButton(Attachment attachment) { - final chatThemeData = StreamChatTheme.of(context); - return SizedBox( - height: 24, - width: 24, - child: RawMaterialButton( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - elevation: 0, - highlightElevation: 0, - focusElevation: 0, - hoverElevation: 0, - onPressed: () { - setState(() => _attachments.remove(attachment.id)); - }, - fillColor: chatThemeData.colorTheme.black.withOpacity(.5), - child: Center( - child: StreamSvgIcon.close( - size: 24, - color: chatThemeData.colorTheme.white, + Widget _buildRemoveButton(Attachment attachment) => SizedBox( + height: 24, + width: 24, + child: RawMaterialButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + elevation: 0, + highlightElevation: 0, + focusElevation: 0, + hoverElevation: 0, + onPressed: () { + setState(() => _attachments.remove(attachment.id)); + }, + fillColor: _streamChatTheme.colorTheme.black.withOpacity(.5), + child: Center( + child: StreamSvgIcon.close( + size: 24, + color: _streamChatTheme.colorTheme.white, + ), ), ), - ), - ); - } + ); Widget _buildAttachment(Attachment attachment) { if (widget.attachmentThumbnailBuilders?.containsKey(attachment.type) == @@ -1623,18 +1612,15 @@ class MessageInputState extends State { fit: BoxFit.cover, errorWidget: (_, obj, trace) => getFileTypeImage(attachment.extraData['other'] as String?), - progressIndicatorBuilder: (context, _, progress) { - final chatThemeData = StreamChatTheme.of(context); - return Shimmer.fromColors( - baseColor: chatThemeData.colorTheme.greyGainsboro, - highlightColor: chatThemeData.colorTheme.whiteSmoke, - child: Image.asset( - 'images/placeholder.png', - fit: BoxFit.cover, - package: 'stream_chat_flutter', - ), - ); - }, + placeholder: (context, _) => Shimmer.fromColors( + baseColor: _streamChatTheme.colorTheme.greyGainsboro, + highlightColor: _streamChatTheme.colorTheme.whiteSmoke, + child: Image.asset( + 'images/placeholder.png', + fit: BoxFit.cover, + package: 'stream_chat_flutter', + ), + ), ); case 'video': return Stack( @@ -1666,14 +1652,13 @@ class MessageInputState extends State { Widget _buildCommandButton() { final s = textEditingController.text.trim(); - final chatThemeData = StreamChatTheme.of(context); return IconButton( icon: StreamSvgIcon.lightning( color: s.isNotEmpty - ? chatThemeData.colorTheme.greyGainsboro + ? _streamChatTheme.colorTheme.greyGainsboro : (_commandsOverlay != null - ? chatThemeData.messageInputTheme.actionButtonColor - : chatThemeData.messageInputTheme.actionButtonIdleColor), + ? _streamChatTheme.messageInputTheme.actionButtonColor + : _streamChatTheme.messageInputTheme.actionButtonIdleColor), ), padding: const EdgeInsets.all(0), constraints: const BoxConstraints.tightFor( @@ -1708,40 +1693,37 @@ class MessageInputState extends State { ); } - Widget _buildAttachmentButton() { - final chatThemeData = StreamChatTheme.of(context); - return IconButton( - icon: StreamSvgIcon.attach( - color: _openFilePickerSection - ? chatThemeData.messageInputTheme.actionButtonColor - : chatThemeData.messageInputTheme.actionButtonIdleColor, - ), - padding: const EdgeInsets.all(0), - constraints: const BoxConstraints.tightFor( - height: 24, - width: 24, - ), - splashRadius: 24, - onPressed: () async { - _emojiOverlay?.remove(); - _emojiOverlay = null; - _commandsOverlay?.remove(); - _commandsOverlay = null; - _mentionsOverlay?.remove(); - _mentionsOverlay = null; + Widget _buildAttachmentButton() => IconButton( + icon: StreamSvgIcon.attach( + color: _openFilePickerSection + ? _streamChatTheme.messageInputTheme.actionButtonColor + : _streamChatTheme.messageInputTheme.actionButtonIdleColor, + ), + padding: const EdgeInsets.all(0), + constraints: const BoxConstraints.tightFor( + height: 24, + width: 24, + ), + splashRadius: 24, + onPressed: () async { + _emojiOverlay?.remove(); + _emojiOverlay = null; + _commandsOverlay?.remove(); + _commandsOverlay = null; + _mentionsOverlay?.remove(); + _mentionsOverlay = null; - if (_openFilePickerSection) { - setState(() { - _animateContainer = true; - _openFilePickerSection = false; - _filePickerSize = _kMinMediaPickerSize; - }); - } else { - showAttachmentModal(); - } - }, - ); - } + if (_openFilePickerSection) { + setState(() { + _animateContainer = true; + _openFilePickerSection = false; + _filePickerSize = _kMinMediaPickerSize; + }); + } else { + showAttachmentModal(); + } + }, + ); /// Show the attachment modal, making the user choose where to /// pick a media from @@ -1948,8 +1930,7 @@ class MessageInputState extends State { padding: const EdgeInsets.all(8), child: StreamSvgIcon( assetName: _getIdleSendIcon(), - color: - StreamChatTheme.of(context).messageInputTheme.sendButtonIdleColor, + color: _streamChatTheme.messageInputTheme.sendButtonIdleColor, ), ); @@ -1965,8 +1946,7 @@ class MessageInputState extends State { ), icon: StreamSvgIcon( assetName: _getSendIcon(), - color: - StreamChatTheme.of(context).messageInputTheme.sendButtonColor, + color: _streamChatTheme.messageInputTheme.sendButtonColor, ), ), ); @@ -2083,9 +2063,8 @@ class MessageInputState extends State { StreamSubscription? _keyboardListener; void _showErrorAlert(String description) { - final chatThemeData = StreamChatTheme.of(context); showModalBottomSheet( - backgroundColor: chatThemeData.colorTheme.white, + backgroundColor: _streamChatTheme.colorTheme.white, context: context, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( @@ -2099,7 +2078,7 @@ class MessageInputState extends State { height: 26, ), StreamSvgIcon.error( - color: chatThemeData.colorTheme.accentRed, + color: _streamChatTheme.colorTheme.accentRed, size: 24, ), const SizedBox( @@ -2107,7 +2086,7 @@ class MessageInputState extends State { ), Text( 'Something went wrong', - style: chatThemeData.textTheme.headlineBold, + style: _streamChatTheme.textTheme.headlineBold, ), const SizedBox( height: 7, @@ -2123,7 +2102,7 @@ class MessageInputState extends State { height: 36, ), Container( - color: chatThemeData.colorTheme.black.withOpacity(.08), + color: _streamChatTheme.colorTheme.black.withOpacity(.08), height: 1, ), Row( @@ -2135,8 +2114,8 @@ class MessageInputState extends State { }, child: Text( 'OK', - style: chatThemeData.textTheme.bodyBold - .copyWith(color: chatThemeData.colorTheme.accentBlue), + style: _streamChatTheme.textTheme.bodyBold + .copyWith(color: _streamChatTheme.colorTheme.accentBlue), ), ), ], @@ -2169,6 +2148,7 @@ class MessageInputState extends State { @override void didChangeDependencies() { + _streamChatTheme = StreamChatTheme.of(context); if (widget.editMessage != null && !_initialized) { FocusScope.of(context).requestFocus(_focusNode); _initialized = true; @@ -2233,6 +2213,7 @@ class _PickerWidget extends StatefulWidget { required this.selectedMedias, required this.onAddMoreFilesClick, required this.onMediaSelected, + required this.streamChatTheme, }) : super(key: key); final int filePickerIndex; @@ -2240,6 +2221,7 @@ class _PickerWidget extends StatefulWidget { final List selectedMedias; final void Function(DefaultAttachmentTypes) onAddMoreFilesClick; final void Function(AssetEntity) onMediaSelected; + final StreamChatThemeData streamChatTheme; @override __PickerWidgetState createState() => __PickerWidgetState(); @@ -2266,7 +2248,6 @@ class __PickerWidgetState extends State<_PickerWidget> { return const Center(child: CircularProgressIndicator()); } - final chatThemeData = StreamChatTheme.of(context); if (snapshot.data!) { if (widget.containsFile) { return GestureDetector( @@ -2275,12 +2256,12 @@ class __PickerWidgetState extends State<_PickerWidget> { }, child: Container( constraints: const BoxConstraints.expand(), - color: chatThemeData.colorTheme.whiteSmoke, + color: widget.streamChatTheme.colorTheme.whiteSmoke, alignment: Alignment.center, child: Text( 'Add more files', style: TextStyle( - color: chatThemeData.colorTheme.accentBlue, + color: widget.streamChatTheme.colorTheme.accentBlue, fontWeight: FontWeight.bold, ), ), @@ -2298,7 +2279,7 @@ class __PickerWidgetState extends State<_PickerWidget> { PhotoManager.openSetting(); }, child: Container( - color: chatThemeData.colorTheme.whiteSmoke, + color: widget.streamChatTheme.colorTheme.whiteSmoke, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, @@ -2307,21 +2288,21 @@ class __PickerWidgetState extends State<_PickerWidget> { 'svgs/icon_picture_empty_state.svg', package: 'stream_chat_flutter', height: 140, - color: chatThemeData.colorTheme.greyGainsboro, + color: widget.streamChatTheme.colorTheme.greyGainsboro, ), Text( // ignore: lines_longer_than_80_chars 'Please enable access to your photos \nand videos so you can share them with friends.', - style: chatThemeData.textTheme.body - .copyWith(color: chatThemeData.colorTheme.grey), + style: widget.streamChatTheme.textTheme.body.copyWith( + color: widget.streamChatTheme.colorTheme.grey), textAlign: TextAlign.center, ), const SizedBox(height: 6), Center( child: Text( 'Allow access to your gallery', - style: chatThemeData.textTheme.bodyBold.copyWith( - color: chatThemeData.colorTheme.accentBlue, + style: widget.streamChatTheme.textTheme.bodyBold.copyWith( + color: widget.streamChatTheme.colorTheme.accentBlue, ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index cb1f66b7..468f2d9f 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:math'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; import 'package:rxdart/rxdart.dart'; @@ -281,8 +282,10 @@ class _MessageListViewState extends State { void Function(Message)? _onThreadTap; bool _showScrollToBottom = false; late final ItemPositionsListener _itemPositionListener; + late final Stream> _itemPositionStream; int? _messageListLength; StreamChannelState? streamChannel; + late StreamChatThemeData _streamTheme; int? get _initialIndex { if (widget.initialScrollIndex != null) return widget.initialScrollIndex; @@ -324,37 +327,34 @@ class _MessageListViewState extends State { final MessageListController _messageListController = MessageListController(); @override - Widget build(BuildContext context) { - final chatThemeData = StreamChatTheme.of(context); - return MessageListCore( - messageFilter: widget.messageFilter, - loadingBuilder: widget.loadingBuilder ?? - (context) => const Center( - child: CircularProgressIndicator(), - ), - emptyBuilder: widget.emptyBuilder ?? - (context) => Center( - child: Text( - 'No chats here yet...', - style: chatThemeData.textTheme.footnote.copyWith( - color: chatThemeData.colorTheme.black.withOpacity(.5)), + Widget build(BuildContext context) => MessageListCore( + messageFilter: widget.messageFilter, + loadingBuilder: widget.loadingBuilder ?? + (context) => const Center( + child: CircularProgressIndicator(), ), - ), - messageListBuilder: - widget.messageListBuilder ?? (context, list) => _buildListView(list), - messageListController: _messageListController, - parentMessage: widget.parentMessage, - showScrollToBottom: widget.showScrollToBottom, - errorWidgetBuilder: widget.errorWidgetBuilder ?? - (BuildContext context, Object error) => Center( - child: Text( - 'Something went wrong', - style: chatThemeData.textTheme.footnote.copyWith( - color: chatThemeData.colorTheme.black.withOpacity(.5)), + emptyBuilder: widget.emptyBuilder ?? + (context) => Center( + child: Text( + 'No chats here yet...', + style: _streamTheme.textTheme.footnote.copyWith( + color: _streamTheme.colorTheme.black.withOpacity(.5)), + ), ), - ), - ); - } + messageListBuilder: widget.messageListBuilder ?? + (context, list) => _buildListView(list), + messageListController: _messageListController, + parentMessage: widget.parentMessage, + showScrollToBottom: widget.showScrollToBottom, + errorWidgetBuilder: widget.errorWidgetBuilder ?? + (BuildContext context, Object error) => Center( + child: Text( + 'Something went wrong', + style: _streamTheme.textTheme.footnote.copyWith( + color: _streamTheme.colorTheme.black.withOpacity(.5)), + ), + ), + ); Widget _buildListView(List data) { messages = data; @@ -400,8 +400,7 @@ class _MessageListViewState extends State { } return InfoTile( - // ignore: avoid_bool_literals_in_conditional_expressions - showMessage: widget.showConnectionStateTile ? showStatus : false, + showMessage: widget.showConnectionStateTile && showStatus, tileAnchor: Alignment.topCenter, childAnchor: Alignment.topCenter, message: statusString, @@ -448,10 +447,9 @@ class _MessageListViewState extends State { if (i == 0) return const SizedBox(height: 30); if (i == messages.length + 1) { final replyCount = widget.parentMessage!.replyCount; - final chatThemeData = StreamChatTheme.of(context); return Container( decoration: BoxDecoration( - gradient: chatThemeData.colorTheme.bgGradient, + gradient: _streamTheme.colorTheme.bgGradient, ), child: Padding( padding: const EdgeInsets.all(8), @@ -459,7 +457,7 @@ class _MessageListViewState extends State { // ignore: lines_longer_than_80_chars '$replyCount ${replyCount == 1 ? 'Reply' : 'Replies'}', textAlign: TextAlign.center, - style: chatThemeData + style: _streamTheme .channelTheme.channelHeaderTheme.subtitle, ), ), @@ -571,9 +569,18 @@ class _MessageListViewState extends State { if (widget.showScrollToBottom) _buildScrollToBottom(), Positioned( top: 20, - child: ValueListenableBuilder>( - valueListenable: _itemPositionListener.itemPositions, - builder: (context, values, _) { + child: BetterStreamBuilder>( + initialData: _itemPositionListener.itemPositions.value, + stream: _itemPositionStream, + comparator: (a, b) { + if (a == null) { + return false; + } + final aTop = _getTopElement(a)?.index; + final bTop = _getTopElement(b)?.index; + return aTop == bTop; + }, + builder: (context, values) { final items = _itemPositionListener.itemPositions.value; if (items.isEmpty || messages.isEmpty) { return const SizedBox(); @@ -640,7 +647,6 @@ class _MessageListViewState extends State { final showUnreadCount = unreadCount > 0 && streamChannel!.channel.state!.members.any((e) => e.userId == streamChannel!.channel.client.state.user!.id); - final chatThemeData = StreamChatTheme.of(context); return Positioned( bottom: 8, right: 8, @@ -650,7 +656,7 @@ class _MessageListViewState extends State { clipBehavior: Clip.none, children: [ FloatingActionButton( - backgroundColor: chatThemeData.colorTheme.white, + backgroundColor: _streamTheme.colorTheme.white, onPressed: () { if (unreadCount > 0) { streamChannel!.channel.markRead(); @@ -669,7 +675,7 @@ class _MessageListViewState extends State { } }, child: StreamSvgIcon.down( - color: chatThemeData.colorTheme.black, + color: _streamTheme.colorTheme.black, ), ), if (showUnreadCount) @@ -700,39 +706,13 @@ class _MessageListViewState extends State { Widget _buildLoadingIndicator( StreamChannelState streamChannel, QueryDirection direction, - ) { - final stream = direction == QueryDirection.top - ? streamChannel.queryTopMessages - : streamChannel.queryBottomMessages; - return BetterStreamBuilder( - key: Key('LOADING-INDICATOR $direction'), - stream: stream, - initialData: false, - errorBuilder: (context, error) => Container( - color: StreamChatTheme.of(context).colorTheme.accentRed.withOpacity(.2), - child: const Center( - child: Text('Error loading messages'), - ), - ), - builder: (context, snapshot) { - if (!snapshot) { - if (!_isThreadConversation && direction == QueryDirection.top) { - return const SizedBox( - height: 52, - width: double.infinity, - ); - } - return const Offstage(); - } - return const Center( - child: Padding( - padding: EdgeInsets.all(8), - child: CircularProgressIndicator(), - ), - ); - }, - ); - } + ) => + _LoadingIndicator( + direction: direction, + streamTheme: _streamTheme, + streamChannel: streamChannel, + isThreadConversation: _isThreadConversation, + ); Widget _buildTopMessage( BuildContext context, @@ -818,7 +798,6 @@ class _MessageListViewState extends State { final currentUserMember = members.firstWhere((e) => e.user!.id == currentUser!.id); - final chatThemeData = StreamChatTheme.of(context); return MessageWidget( showReplyMessage: false, showResendMessage: false, @@ -847,8 +826,8 @@ class _MessageListViewState extends State { borderSide: isMyMessage || isOnlyEmoji ? BorderSide.none : null, showUserAvatar: isMyMessage ? DisplayWidget.gone : DisplayWidget.show, messageTheme: isMyMessage - ? chatThemeData.ownMessageTheme - : chatThemeData.otherMessageTheme, + ? _streamTheme.ownMessageTheme + : _streamTheme.otherMessageTheme, onShowMessage: widget.onShowMessage, onReturnAction: (action) { switch (action) { @@ -962,7 +941,6 @@ class _MessageListViewState extends State { final currentUserMember = members.firstWhere((e) => e.user!.id == currentUser!.id); - final chatThemeData = StreamChatTheme.of(context); Widget child = MessageWidget( key: ValueKey('MESSAGE-${message.id}'), message: message, @@ -1049,8 +1027,8 @@ class _MessageListViewState extends State { horizontal: isOnlyEmoji ? 0 : 16.0, ), messageTheme: isMyMessage - ? chatThemeData.ownMessageTheme - : chatThemeData.otherMessageTheme, + ? _streamTheme.ownMessageTheme + : _streamTheme.otherMessageTheme, readList: readList, allRead: allRead, onShowMessage: widget.onShowMessage, @@ -1091,7 +1069,7 @@ class _MessageListViewState extends State { widget.onMessageSwiped?.call(message); }, backgroundIcon: StreamSvgIcon.reply( - color: chatThemeData.colorTheme.accentBlue, + color: _streamTheme.colorTheme.accentBlue, ), child: child, ), @@ -1101,7 +1079,7 @@ class _MessageListViewState extends State { if (!initialMessageHighlightComplete && widget.highlightInitialMessage && _isInitialMessage(message.id)) { - final colorTheme = chatThemeData.colorTheme; + final colorTheme = _streamTheme.colorTheme; final highlightColor = widget.messageHighlightColor ?? colorTheme.highlight; child = TweenAnimationBuilder( @@ -1131,6 +1109,8 @@ class _MessageListViewState extends State { _scrollController = widget.scrollController ?? ItemScrollController(); _itemPositionListener = widget.itemPositionListener ?? ItemPositionsListener.create(); + _itemPositionStream = + valueListenableToStreamAdapter(_itemPositionListener.itemPositions); _getOnThreadTap(); super.initState(); @@ -1139,6 +1119,7 @@ class _MessageListViewState extends State { @override void didChangeDependencies() { final newStreamChannel = StreamChannel.of(context); + _streamTheme = StreamChatTheme.of(context); if (newStreamChannel != streamChannel) { streamChannel = newStreamChannel; @@ -1209,3 +1190,78 @@ class _MessageListViewState extends State { super.dispose(); } } + +class _LoadingIndicator extends StatelessWidget { + const _LoadingIndicator({ + Key? key, + required this.streamTheme, + required this.isThreadConversation, + required this.direction, + required this.streamChannel, + }) : super(key: key); + + final StreamChatThemeData streamTheme; + final bool isThreadConversation; + final QueryDirection direction; + final StreamChannelState streamChannel; + + @override + Widget build(BuildContext context) { + final stream = direction == QueryDirection.top + ? streamChannel.queryTopMessages + : streamChannel.queryBottomMessages; + return BetterStreamBuilder( + key: Key('LOADING-INDICATOR $direction'), + stream: stream, + initialData: false, + errorBuilder: (context, error) => Container( + color: streamTheme.colorTheme.accentRed.withOpacity(.2), + child: const Center( + child: Text('Error loading messages'), + ), + ), + builder: (context, snapshot) { + if (!snapshot) { + if (!isThreadConversation && direction == QueryDirection.top) { + return const SizedBox( + height: 52, + width: double.infinity, + ); + } + return const Offstage(); + } + return const Center( + child: Padding( + padding: EdgeInsets.all(8), + child: CircularProgressIndicator(), + ), + ); + }, + ); + } +} + +Stream valueListenableToStreamAdapter(ValueListenable listenable) { + late StreamController controller; + + void listener() { + controller.add(listenable.value); + } + + void start() { + listenable.addListener(listener); + } + + void end() { + listenable.removeListener(listener); + } + + controller = StreamController( + onListen: start, + onPause: end, + onResume: start, + onCancel: end, + ); + + return controller.stream; +} diff --git a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart index 9d78e3d9..e9369e99 100644 --- a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart @@ -77,113 +77,111 @@ class MessageReactionsModal extends StatelessWidget { final divFactor = message.attachments.isNotEmpty == true ? 1 : (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize)); + final hasFileAttachment = + message.attachments.any((it) => it.type == 'file') == true; - return TweenAnimationBuilder( - tween: Tween(begin: 0, end: 1), - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOutBack, - builder: (context, val, snapshot) { - final hasFileAttachment = - message.attachments.any((it) => it.type == 'file') == true; - return GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () => Navigator.maybePop(context), - child: Stack( - children: [ - Positioned.fill( - child: BackdropFilter( - filter: ImageFilter.blur( - sigmaX: 10, - sigmaY: 10, - ), - child: Container( - color: StreamChatTheme.of(context).colorTheme.overlay, + final child = Center( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (showReactions && + (message.status == MessageSendingStatus.sent)) + Align( + alignment: Alignment( + user!.id == message.user!.id + ? (divFactor >= 1.0 ? -0.2 : (1.2 - divFactor)) + : (divFactor >= 1.0 ? 0.2 : -(1.2 - divFactor)), + 0), + child: ReactionPicker( + message: message, ), ), - ), - Transform.scale( - scale: val, - child: Center( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(8), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (showReactions && - (message.status == MessageSendingStatus.sent)) - Align( - alignment: Alignment( - user!.id == message.user!.id - ? (divFactor >= 1.0 - ? -0.2 - : (1.2 - divFactor)) - : (divFactor >= 1.0 - ? 0.2 - : -(1.2 - divFactor)), - 0), - child: ReactionPicker( - message: message, - ), - ), - const SizedBox(height: 8), - IgnorePointer( - child: MessageWidget( - key: const Key('MessageWidget'), - reverse: reverse, - message: message.copyWith( - text: message.text!.length > 200 - ? '${message.text!.substring(0, 200)}...' - : message.text, - ), - messageTheme: messageTheme, - showReactions: false, - showUsername: false, - showUserAvatar: showUserAvatar, - showTimestamp: false, - translateUserAvatar: false, - showSendingIndicator: false, - shape: messageShape, - attachmentShape: attachmentShape, - padding: const EdgeInsets.all(0), - attachmentBorderRadiusGeometry: - attachmentBorderRadiusGeometry - ?.mirrorBorderIfReversed( - reverse: !reverse), - attachmentPadding: EdgeInsets.all( - hasFileAttachment ? 4 : 2, - ), - textPadding: EdgeInsets.symmetric( - vertical: 8, - horizontal: - message.text!.isOnlyEmoji ? 0 : 16.0, - ), - showReactionPickerIndicator: showReactions && - (message.status == MessageSendingStatus.sent), - textBuilder: textBuilder, - showPinHighlight: false, - ), - ), - if (message.latestReactions?.isNotEmpty == true) ...[ - const SizedBox(height: 8), - _buildReactionCard(context), - ] - ], - ), - ), + const SizedBox(height: 8), + IgnorePointer( + child: MessageWidget( + key: const Key('MessageWidget'), + reverse: reverse, + message: message.copyWith( + text: message.text!.length > 200 + ? '${message.text!.substring(0, 200)}...' + : message.text, ), + messageTheme: messageTheme, + showReactions: false, + showUsername: false, + showUserAvatar: showUserAvatar, + showTimestamp: false, + translateUserAvatar: false, + showSendingIndicator: false, + shape: messageShape, + attachmentShape: attachmentShape, + padding: const EdgeInsets.all(0), + attachmentBorderRadiusGeometry: attachmentBorderRadiusGeometry + ?.mirrorBorderIfReversed(reverse: !reverse), + attachmentPadding: EdgeInsets.all( + hasFileAttachment ? 4 : 2, + ), + textPadding: EdgeInsets.symmetric( + vertical: 8, + horizontal: message.text!.isOnlyEmoji ? 0 : 16.0, + ), + showReactionPickerIndicator: showReactions && + (message.status == MessageSendingStatus.sent), + textBuilder: textBuilder, + showPinHighlight: false, ), ), + if (message.latestReactions?.isNotEmpty == true) ...[ + const SizedBox(height: 8), + _buildReactionCard( + context, + user, + ), + ] ], ), - ); - }, + ), + ), + ); + + return GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () => Navigator.maybePop(context), + child: Stack( + children: [ + Positioned.fill( + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 10, + sigmaY: 10, + ), + child: DecoratedBox( + decoration: BoxDecoration( + color: StreamChatTheme.of(context).colorTheme.overlay, + ), + ), + ), + ), + TweenAnimationBuilder( + tween: Tween(begin: 0, end: 1), + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOutBack, + builder: (context, val, widget) => Transform.scale( + scale: val, + child: widget, + ), + child: child, + ), + ], + ), ); } - Widget _buildReactionCard(BuildContext context) { - final currentUser = StreamChat.of(context).user; + Widget _buildReactionCard(BuildContext context, User? user) { final chatThemeData = StreamChatTheme.of(context); return Card( color: chatThemeData.colorTheme.white, @@ -210,7 +208,7 @@ class MessageReactionsModal extends StatelessWidget { children: message.latestReactions! .map((e) => _buildReaction( e, - currentUser!, + user!, context, )) .toList(), diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index f1b5428d..7fb092d6 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -413,37 +413,39 @@ class _MessageWidgetState extends State bool get showTimeStamp => widget.showTimestamp; - bool get isMessageRead => widget.readList?.isNotEmpty == true; + late final bool isMessageRead = widget.readList?.isNotEmpty == true; bool get showInChannel => widget.showInChannelIndicator; bool get hasQuotedMessage => widget.message.quotedMessage != null; - bool get isSendFailed => widget.message.status == MessageSendingStatus.failed; + late final bool isSendFailed = + widget.message.status == MessageSendingStatus.failed; - bool get isUpdateFailed => + late final bool isUpdateFailed = widget.message.status == MessageSendingStatus.failed_update; - bool get isDeleteFailed => + late final bool isDeleteFailed = widget.message.status == MessageSendingStatus.failed_delete; - bool get isFailedState => isSendFailed || isUpdateFailed || isDeleteFailed; + late final bool isFailedState = + isSendFailed || isUpdateFailed || isDeleteFailed; - bool get isGiphy => + late final bool isGiphy = widget.message.attachments.any((element) => element.type == 'giphy') == - true; + true; - bool get hasNonUrlAttachments => - widget.message.attachments + late final bool isOnlyEmoji = widget.message.text?.isOnlyEmoji == true; + + late final bool hasNonUrlAttachments = widget.message.attachments .where((it) => it.ogScrapeUrl == null) .isNotEmpty == true; - bool get hasUrlAttachments => + late final bool hasUrlAttachments = widget.message.attachments.any((it) => it.ogScrapeUrl != null) == true; - bool get showBottomRow => - showThreadReplyIndicator || + late final bool showBottomRow = showThreadReplyIndicator || showUsername || showTimeStamp || showInChannel || @@ -453,6 +455,9 @@ class _MessageWidgetState extends State @override bool get wantKeepAlive => widget.message.attachments.isNotEmpty == true; + late StreamChatThemeData _streamChatTheme; + late StreamChatState _streamChat; + @override Widget build(BuildContext context) { super.build(context); @@ -466,7 +471,7 @@ class _MessageWidgetState extends State ? MaterialType.card : MaterialType.transparency, color: widget.message.pinned && widget.showPinHighlight - ? StreamChatTheme.of(context).colorTheme.highlight + ? _streamChatTheme.colorTheme.highlight : null, child: Portal( child: InkWell( @@ -527,7 +532,8 @@ class _MessageWidgetState extends State transform: Matrix4.translationValues( widget.reverse ? 12 : -12, 0, 0), constraints: const BoxConstraints( - maxWidth: 22 * 6.0), + maxWidth: 22 * 6.0, + ), child: _buildReactionIndicator(context), ), portalAnchor: @@ -572,7 +578,7 @@ class _MessageWidgetState extends State ), ) : Card( - clipBehavior: Clip.antiAlias, + clipBehavior: Clip.hardEdge, elevation: 0, margin: EdgeInsets.symmetric( horizontal: (isFailedState @@ -626,9 +632,8 @@ class _MessageWidgetState extends State top: -8, child: CustomPaint( painter: ReactionBubblePainter( - StreamChatTheme.of(context) - .colorTheme - .white, + _streamChatTheme + .colorTheme.white, Colors.transparent, Colors.transparent, tailCirclesSpace: 1, @@ -673,14 +678,20 @@ class _MessageWidgetState extends State ); } + @override + void didChangeDependencies() { + _streamChatTheme = StreamChatTheme.of(context); + _streamChat = StreamChat.of(context); + super.didChangeDependencies(); + } + Widget _buildQuotedMessage() { - final isMyMessage = - widget.message.user?.id == StreamChat.of(context).user?.id; + final isMyMessage = widget.message.user?.id == _streamChat.user?.id; final onTap = widget.message.quotedMessage?.isDeleted != true && widget.onQuotedMessageTap != null ? () => widget.onQuotedMessageTap!(widget.message.quotedMessageId) : null; - final chatThemeData = StreamChatTheme.of(context); + final chatThemeData = _streamChatTheme; return QuotedMessageWidget( onTap: onTap, message: widget.message.quotedMessage!, @@ -695,7 +706,7 @@ class _MessageWidgetState extends State Widget get _bottomRow { if (isDeleted) { - final chatThemeData = StreamChatTheme.of(context); + final chatThemeData = _streamChatTheme; return Row( mainAxisSize: MainAxisSize.min, children: [ @@ -849,36 +860,16 @@ class _MessageWidgetState extends State ); } - Widget _buildThreadParticipantsIndicator(Iterable threadParticipants) { - var padding = 0.0; - return Stack( - children: threadParticipants.map((user) { - padding += 8.0; - return Positioned( - right: padding - 8, - bottom: 0, - top: 0, - child: Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - color: StreamChatTheme.of(context).colorTheme.white, - ), - padding: const EdgeInsets.all(1), - child: UserAvatar( - user: user, - constraints: BoxConstraints.loose(const Size.fromRadius(7)), - showOnlineStatus: false, - ), - ), - ); - }).toList(), - ); - } + Widget _buildThreadParticipantsIndicator(Iterable threadParticipants) => + _ThreadParticipants( + streamChatTheme: _streamChatTheme, + threadParticipants: threadParticipants, + ); Widget _buildReactionIndicator( BuildContext context, ) { - final ownId = StreamChat.of(context).user!.id; + final ownId = _streamChat.user!.id; final reactionsMap = {}; widget.message.latestReactions?.forEach((element) { if (!reactionsMap.containsKey(element.type) || @@ -918,7 +909,7 @@ class _MessageWidgetState extends State showDialog( context: context, - barrierColor: StreamChatTheme.of(context).colorTheme.overlay, + barrierColor: _streamChatTheme.colorTheme.overlay, builder: (context) => StreamChannel( channel: channel, child: MessageActionsModal( @@ -969,7 +960,7 @@ class _MessageWidgetState extends State final channel = StreamChannel.of(context).channel; showDialog( context: context, - barrierColor: StreamChatTheme.of(context).colorTheme.overlay, + barrierColor: _streamChatTheme.colorTheme.overlay, builder: (context) => StreamChannel( channel: channel, child: MessageReactionsModal( @@ -1000,7 +991,7 @@ class _MessageWidgetState extends State side: hasFiles ? widget.attachmentBorderSide ?? BorderSide( - color: StreamChatTheme.of(context).colorTheme.greyWhisper, + color: _streamChatTheme.colorTheme.greyWhisper, ) : BorderSide.none, borderRadius: widget.attachmentBorderRadiusGeometry ?? BorderRadius.zero, @@ -1010,7 +1001,7 @@ class _MessageWidgetState extends State ShapeBorder _getDefaultShape(BuildContext context) => RoundedRectangleBorder( side: widget.borderSide ?? BorderSide( - color: StreamChatTheme.of(context).colorTheme.greyWhisper, + color: _streamChatTheme.colorTheme.greyWhisper, ), borderRadius: widget.borderRadiusGeometry ?? BorderRadius.zero, ); @@ -1101,7 +1092,7 @@ class _MessageWidgetState extends State Text( widget.readList!.length.toString(), style: style.copyWith( - color: StreamChatTheme.of(context).colorTheme.accentBlue, + color: _streamChatTheme.colorTheme.accentBlue, ), ), const SizedBox(width: 2), @@ -1158,7 +1149,7 @@ class _MessageWidgetState extends State Widget _buildPinnedMessage(Message message) { final pinnedBy = message.pinnedBy; - final pinnedByMe = StreamChat.of(context).user!.id == pinnedBy!.id; + final pinnedByMe = _streamChat.user!.id == pinnedBy!.id; return Padding( padding: const EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 8), @@ -1174,7 +1165,7 @@ class _MessageWidgetState extends State Text( 'Pinned by ${pinnedByMe ? 'You' : pinnedBy.name}', style: TextStyle( - color: StreamChatTheme.of(context).colorTheme.grey, + color: _streamChatTheme.colorTheme.grey, fontSize: 13, fontWeight: FontWeight.w400, ), @@ -1184,9 +1175,7 @@ class _MessageWidgetState extends State ); } - bool get isOnlyEmoji => widget.message.text!.isOnlyEmoji; - - bool get isPinned => widget.message.pinned; + late final bool isPinned = widget.message.pinned; Color? _getBackgroundColor() { if (hasQuotedMessage) { @@ -1194,7 +1183,7 @@ class _MessageWidgetState extends State } if (hasUrlAttachments) { - return StreamChatTheme.of(context).colorTheme.blueAlice; + return _streamChatTheme.colorTheme.blueAlice; } if (isOnlyEmoji) { @@ -1226,6 +1215,45 @@ class _MessageWidgetState extends State } } +class _ThreadParticipants extends StatelessWidget { + const _ThreadParticipants({ + Key? key, + required StreamChatThemeData streamChatTheme, + required this.threadParticipants, + }) : _streamChatTheme = streamChatTheme, + super(key: key); + + final StreamChatThemeData _streamChatTheme; + final Iterable threadParticipants; + + @override + Widget build(BuildContext context) { + var padding = 0.0; + return Stack( + children: threadParticipants.map((user) { + padding += 8.0; + return Positioned( + right: padding - 8, + bottom: 0, + top: 0, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: _streamChatTheme.colorTheme.white, + ), + padding: const EdgeInsets.all(1), + child: UserAvatar( + user: user, + constraints: BoxConstraints.loose(const Size.fromRadius(7)), + showOnlineStatus: false, + ), + ), + ); + }).toList(), + ); + } +} + class _ThreadReplyPainter extends CustomPainter { const _ThreadReplyPainter({ this.context, diff --git a/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart b/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart index 07a820b9..43620640 100644 --- a/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart @@ -1,9 +1,9 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:video_player/video_player.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; /// Widget builder for quoted message attachment thumnail typedef QuotedMessageAttachmentThumbnailBuilder = Widget Function( @@ -217,7 +217,7 @@ class QuotedMessageWidget extends StatelessWidget { } child = AbsorbPointer(child: child); return Material( - clipBehavior: Clip.antiAlias, + clipBehavior: Clip.hardEdge, type: MaterialType.transparency, shape: attachment.type == 'file' ? null : _getDefaultShape(context), child: child, diff --git a/packages/stream_chat_flutter/lib/src/reaction_picker.dart b/packages/stream_chat_flutter/lib/src/reaction_picker.dart index 8a89b8c3..2e47b933 100644 --- a/packages/stream_chat_flutter/lib/src/reaction_picker.dart +++ b/packages/stream_chat_flutter/lib/src/reaction_picker.dart @@ -1,5 +1,3 @@ -import 'dart:math'; - import 'package:ezanimation/ezanimation.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; @@ -50,95 +48,90 @@ class _ReactionPickerState extends State triggerAnimations(); } - return TweenAnimationBuilder( - tween: Tween(begin: 0, end: 1), - curve: Curves.easeInOutBack, - duration: const Duration(milliseconds: 500), - builder: (context, val, wid) => Transform.scale( - scale: val, - child: Material( - borderRadius: BorderRadius.circular(24), - color: chatThemeData.colorTheme.white, - clipBehavior: Clip.hardEdge, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: reactionIcons - .map((reactionIcon) { - final ownReactionIndex = widget.message.ownReactions - ?.indexWhere((reaction) => - reaction.type == reactionIcon.type) ?? - -1; - final index = reactionIcons.indexOf(reactionIcon); + final child = Material( + borderRadius: BorderRadius.circular(24), + color: chatThemeData.colorTheme.white, + clipBehavior: Clip.hardEdge, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: reactionIcons + .map((reactionIcon) { + final ownReactionIndex = widget.message.ownReactions + ?.indexWhere( + (reaction) => reaction.type == reactionIcon.type) ?? + -1; + final index = reactionIcons.indexOf(reactionIcon); - return ConstrainedBox( - constraints: const BoxConstraints.tightFor( - height: 24, - width: 24, - ), - child: RawMaterialButton( - elevation: 0, - shape: ContinuousRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - constraints: const BoxConstraints.tightFor( - height: 24, - width: 24, - ), - onPressed: () { - if (ownReactionIndex != -1) { - removeReaction( - context, - widget.message - .ownReactions![ownReactionIndex], - ); - } else { - sendReaction( - context, - reactionIcon.type, - ); - } - }, - child: AnimatedBuilder( - animation: animations[index], - builder: (context, val) => Transform.scale( - scale: animations[index].value, - child: StreamSvgIcon( - assetName: reactionIcon.assetName, - height: max( - 0, - animations[index].value * 24.0, - ), - width: max( - 0, - animations[index].value * 24.0, - ), - color: ownReactionIndex != -1 - ? chatThemeData - .colorTheme.accentBlue - : Theme.of(context) - .iconTheme - .color! - .withOpacity(.5), - ), - )), - ), - ); - }) - .insertBetween(const SizedBox( - width: 16, - )) - .toList(), + final child = StreamSvgIcon( + assetName: reactionIcon.assetName, + color: ownReactionIndex != -1 + ? chatThemeData.colorTheme.accentBlue + : Theme.of(context).iconTheme.color!.withOpacity(.5), + ); + + return ConstrainedBox( + constraints: const BoxConstraints.tightFor( + height: 24, + width: 24, ), - ), - ), - )); + child: RawMaterialButton( + elevation: 0, + shape: ContinuousRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + constraints: const BoxConstraints.tightFor( + height: 24, + width: 24, + ), + onPressed: () { + if (ownReactionIndex != -1) { + removeReaction( + context, + widget.message.ownReactions![ownReactionIndex], + ); + } else { + sendReaction( + context, + reactionIcon.type, + ); + } + }, + child: AnimatedBuilder( + animation: animations[index], + builder: (context, child) => Transform.scale( + scale: animations[index].value, + child: child, + ), + child: child, + ), + ), + ); + }) + .insertBetween(const SizedBox( + width: 16, + )) + .toList(), + ), + ), + ); + + return TweenAnimationBuilder( + tween: Tween(begin: 0, end: 1), + curve: Curves.easeInOutBack, + duration: const Duration(milliseconds: 500), + builder: (context, val, widget) => Transform.scale( + scale: val, + child: widget, + ), + child: child, + ); } void triggerAnimations() async { diff --git a/packages/stream_chat_flutter/lib/src/typing_indicator.dart b/packages/stream_chat_flutter/lib/src/typing_indicator.dart index 8f974d6e..5609c18b 100644 --- a/packages/stream_chat_flutter/lib/src/typing_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/typing_indicator.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; import 'package:lottie/lottie.dart'; -import 'package:stream_chat_flutter/src/utils.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Widget to show the current list of typing users -class TypingIndicator extends StatelessWidget { +class TypingIndicator extends StatefulWidget { /// Instantiate a new TypingIndicator const TypingIndicator({ Key? key, @@ -30,16 +29,21 @@ class TypingIndicator extends StatelessWidget { /// Alignment of the typing indicator final Alignment alignment; + @override + _TypingIndicatorState createState() => _TypingIndicatorState(); +} + +class _TypingIndicatorState extends State { @override Widget build(BuildContext context) { final channelState = - channel?.state ?? StreamChannel.of(context).channel.state!; + widget.channel?.state ?? StreamChannel.of(context).channel.state!; final altWidget = Align( key: const Key('alternative'), - alignment: alignment, + alignment: widget.alignment, child: Container( - child: alternativeWidget ?? const Offstage(), + child: widget.alternativeWidget ?? const Offstage(), ), ); return BetterStreamBuilder>( @@ -49,10 +53,11 @@ class TypingIndicator extends StatelessWidget { duration: const Duration(milliseconds: 300), child: snapshot.isNotEmpty == true ? Padding( - padding: padding, + key: const Key('main'), + padding: widget.padding, child: Align( key: const Key('typings'), - alignment: alignment, + alignment: widget.alignment, child: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -65,7 +70,7 @@ class TypingIndicator extends StatelessWidget { // ignore: lines_longer_than_80_chars ' ${snapshot[0].name}${snapshot.length == 1 ? '' : ' and ${snapshot.length - 1} more'} ${snapshot.length == 1 ? 'is' : 'are'} typing', maxLines: 1, - style: style, + style: widget.style, ), ], ), diff --git a/packages/stream_chat_flutter/lib/src/url_attachment.dart b/packages/stream_chat_flutter/lib/src/url_attachment.dart index 995475a8..4944c73c 100644 --- a/packages/stream_chat_flutter/lib/src/url_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/url_attachment.dart @@ -40,7 +40,7 @@ class UrlAttachment extends StatelessWidget { children: [ if (urlAttachment.imageUrl != null) Container( - clipBehavior: Clip.antiAliasWithSaveLayer, + clipBehavior: Clip.hardEdge, margin: const EdgeInsets.symmetric(horizontal: 8), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), diff --git a/packages/stream_chat_flutter/lib/src/utils.dart b/packages/stream_chat_flutter/lib/src/utils.dart index 644ea99f..e98112fc 100644 --- a/packages/stream_chat_flutter/lib/src/utils.dart +++ b/packages/stream_chat_flutter/lib/src/utils.dart @@ -334,7 +334,7 @@ Widget wrapAttachmentWidget( bool reverse, ) => Material( - clipBehavior: Clip.antiAlias, + clipBehavior: Clip.hardEdge, shape: attachmentShape, type: MaterialType.transparency, child: attachmentWidget, diff --git a/packages/stream_chat_flutter_core/lib/src/better_stream_builder.dart b/packages/stream_chat_flutter_core/lib/src/better_stream_builder.dart index bf50966b..9691618a 100644 --- a/packages/stream_chat_flutter_core/lib/src/better_stream_builder.dart +++ b/packages/stream_chat_flutter_core/lib/src/better_stream_builder.dart @@ -25,29 +25,31 @@ class BetterStreamBuilder extends StatefulWidget { } class _BetterStreamBuilderState extends State> { - Widget? _child; T? _lastEvent; StreamSubscription? _subscription; Object? _lastError; @override - Widget build(BuildContext context) => _child ?? const Offstage(); + Widget build(BuildContext context) { + if (_lastError != null) { + return widget.errorBuilder!(context, _lastError!); + } + + if (_lastEvent == null) { + return widget.loadingBuilder?.call(context) ?? const Offstage(); + } + return widget.builder(context, _lastEvent ?? widget.initialData); + } bool _firstTime = true; @override void didChangeDependencies() { if (_firstTime) { - if (widget.initialData == null && widget.loadingBuilder != null) { - _child = widget.loadingBuilder!(context); - } else { - _onEvent(widget.initialData); - } _lastEvent = widget.initialData; _subscription = widget.stream?.listen( _onEvent, onError: _onError, ); - _firstTime = false; } super.didChangeDependencies(); @@ -55,12 +57,6 @@ class _BetterStreamBuilderState extends State> { @override void didUpdateWidget(covariant BetterStreamBuilder oldWidget) { - if (_lastError != null && oldWidget.errorBuilder != widget.errorBuilder) { - _onError(_lastError); - } else if (oldWidget.builder != widget.builder) { - _onEvent(_lastEvent); - } - if (oldWidget.stream != widget.stream) { _subscription?.cancel(); _subscription = widget.stream?.listen( @@ -79,21 +75,22 @@ class _BetterStreamBuilderState extends State> { void _onError(error) { if (widget.errorBuilder != null && error != _lastError) { - setState(() { - _child = widget.errorBuilder!(context, error); - }); + if (mounted) { + setState(() {}); + } _lastError = error; } } - void _onEvent(event) { + void _onEvent(T event) { _lastError = null; - if (widget.comparator != null + final isEqual = widget.comparator != null ? widget.comparator!(_lastEvent, event) - : event != _lastEvent) { - setState(() { - _child = widget.builder(context, event); - }); + : event == _lastEvent; + if (!isEqual) { + if (mounted) { + setState(() {}); + } _lastEvent = event; } } diff --git a/packages/stream_chat_flutter_core/lib/src/stream_channel.dart b/packages/stream_chat_flutter_core/lib/src/stream_channel.dart index 20304cdd..ad1f11ae 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_channel.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel.dart @@ -346,36 +346,40 @@ class StreamChannelState extends State { @override Widget build(BuildContext context) { - Widget child = FutureBuilder>( - future: Future.wait(_futures), - initialData: [ - channel.state != null, - if (initialMessageId != null) false, - ], - builder: (context, snapshot) { - if (snapshot.hasError) { - var message = snapshot.error.toString(); - if (snapshot.error is DioError) { - final dioError = snapshot.error as DioError?; - if (dioError?.type == DioErrorType.response) { - message = dioError!.message; - } else { - message = 'Check your connection and retry'; + var child = widget.child; + if (widget.showLoading && + (initialMessageId != null || channel.state == null)) { + child = FutureBuilder>( + future: Future.wait(_futures), + initialData: [ + channel.state != null, + if (initialMessageId != null) false, + ], + builder: (context, snapshot) { + if (snapshot.hasError) { + var message = snapshot.error.toString(); + if (snapshot.error is DioError) { + final dioError = snapshot.error as DioError?; + if (dioError?.type == DioErrorType.response) { + message = dioError!.message; + } else { + 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 const Center( - child: CircularProgressIndicator(), - ); - } - return widget.child; - }, - ); + final initialized = snapshot.data![0]; + final dataLoaded = initialMessageId == null || snapshot.data![1]; + if (!initialized || !dataLoaded) { + return const Center( + child: CircularProgressIndicator(), + ); + } + return widget.child; + }, + ); + } + if (initialMessageId != null) { child = Material(child: child); } From 23fed8d35ffca8a97e60f99baeb74027c2cda7d8 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 9 Jun 2021 16:28:05 +0200 Subject: [PATCH 022/150] fix analyze --- .../lib/src/message_list_view.dart | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 468f2d9f..195ae1f2 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -1110,7 +1110,7 @@ class _MessageListViewState extends State { _itemPositionListener = widget.itemPositionListener ?? ItemPositionsListener.create(); _itemPositionStream = - valueListenableToStreamAdapter(_itemPositionListener.itemPositions); + _valueListenableToStreamAdapter(_itemPositionListener.itemPositions); _getOnThreadTap(); super.initState(); @@ -1241,11 +1241,12 @@ class _LoadingIndicator extends StatelessWidget { } } -Stream valueListenableToStreamAdapter(ValueListenable listenable) { - late StreamController controller; +Stream _valueListenableToStreamAdapter(ValueListenable listenable) { + // ignore: close_sinks + late StreamController _controller; void listener() { - controller.add(listenable.value); + _controller.add(listenable.value); } void start() { @@ -1256,12 +1257,12 @@ Stream valueListenableToStreamAdapter(ValueListenable listenable) { listenable.removeListener(listener); } - controller = StreamController( + _controller = StreamController( onListen: start, onPause: end, onResume: start, onCancel: end, ); - return controller.stream; + return _controller.stream; } From e2529231fdd1fb8a12928a8e6c86240784015911 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 9 Jun 2021 21:01:35 +0530 Subject: [PATCH 023/150] add initialized channel tests Signed-off-by: Sahil Kumar --- .../stream_chat/lib/src/client/channel.dart | 51 +- .../stream_chat/lib/src/client/client.dart | 2 +- .../lib/src/core/api/channel_api.dart | 2 +- .../test/src/api/channel_test.dart | 1872 +++++++++++++++++ .../src/{ => core}/api/requests_test.dart | 0 .../src/{ => core}/api/responses_test.dart | 0 packages/stream_chat/test/src/fakes.dart | 21 + packages/stream_chat/test/src/matchers.dart | 72 + packages/stream_chat/test/src/mocks.dart | 27 + 9 files changed, 2019 insertions(+), 28 deletions(-) create mode 100644 packages/stream_chat/test/src/api/channel_test.dart rename packages/stream_chat/test/src/{ => core}/api/requests_test.dart (100%) rename packages/stream_chat/test/src/{ => core}/api/responses_test.dart (100%) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 5bc59ae8..fc37cd5c 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -51,9 +51,9 @@ class Channel { String? _id; String? _cid; - final Map _extraData; + final Map _extraData; - set extraData(Map extraData) { + set extraData(Map extraData) { if (_initializedCompleter.isCompleted) { throw StateError( 'Once the channel is initialized you should use channel.update ' @@ -734,7 +734,7 @@ class Channel { /// Edit the channel custom data Future update( - Map channelData, [ + Map channelData, [ Message? updateMessage, ]) async { _checkInitialized(); @@ -814,19 +814,24 @@ class Channel { final messageId = message.id; final res = await _client.sendAction(id!, type, messageId, formData); + // update the passed message with response message if (res.message != null) { state!.addMessage(res.message!); } else { + // remove the passed message if response does + // not contain message final oldIndex = state!.messages.indexWhere((m) => m.id == messageId); - Message? oldMessage; + // remove regular message if present if (oldIndex != -1) { - oldMessage = state!.messages[oldIndex]; + final oldMessage = state!.messages[oldIndex]; state!.updateChannelState(state!._channelState.copyWith( messages: state?.messages?..remove(oldMessage), )); } else { - oldMessage = state!.threads.values + // remove thread message if present + // also reduces total reply count + final oldMessage = state!.threads.values .expand((messages) => messages) .firstWhereOrNull((m) => m.id == messageId); if (oldMessage?.parentId != null) { @@ -858,18 +863,11 @@ class Channel { } /// Loads the initial channel state and watches for changes - Future watch([Map options = const {}]) async { - final watchOptions = Map.from({ - 'state': true, - 'watch': true, - 'presence': false, - }) - ..addAll(options); - + Future watch() async { ChannelState response; try { - response = await query(options: watchOptions); + response = await query(watch: true); } catch (error, stackTrace) { if (!_initializedCompleter.isCompleted) { _initializedCompleter.completeError(error, stackTrace); @@ -956,17 +954,15 @@ class Channel { ); /// Creates a new channel - Future create() async => query(options: { - 'watch': false, - 'state': false, - 'presence': false, - }); + Future create() async => query(state: false); /// Query the API, get messages, members or other channel fields /// Set [preferOffline] to true to avoid the api call if the data is already /// in the offline storage Future query({ - Map options = const {}, + bool state = true, + bool watch = false, + bool presence = false, PaginationParams? messagesPagination, PaginationParams? membersPagination, PaginationParams? watchersPagination, @@ -976,10 +972,10 @@ class Channel { final updatedState = await _client.chatPersistenceClient ?.getChannelStateByCid(cid!, messagePagination: messagesPagination); if (updatedState != null && updatedState.messages.isNotEmpty) { - if (state == null) { + if (this.state == null) { _initState(updatedState); } else { - state?.updateChannelState(updatedState); + this.state?.updateChannelState(updatedState); } return updatedState; } @@ -990,6 +986,9 @@ class Channel { type, channelId: id, channelData: _extraData, + state: state, + watch: watch, + presence: presence, messagesPagination: messagesPagination, membersPagination: membersPagination, watchersPagination: watchersPagination, @@ -1000,7 +999,7 @@ class Channel { _cid = updatedState.channel!.cid; } - state?.updateChannelState(updatedState); + this.state?.updateChannelState(updatedState); return updatedState; } catch (e) { if (!_client.persistenceEnabled) { @@ -1775,7 +1774,7 @@ class ChannelClientState { ); } - late Timer _cleaningTimer; + Timer? _cleaningTimer; void _startCleaning() { if (_channelState.channel?.config.typingEvents == false) { @@ -1842,7 +1841,7 @@ class ChannelClientState { _channelStateController.close(); _isUpToDateController.close(); _threadsController.close(); - _cleaningTimer.cancel(); + _cleaningTimer?.cancel(); _pinnedMessagesTimer.cancel(); _typingEventsController.close(); } diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 9ecc780b..87e471e6 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -757,7 +757,7 @@ class StreamChatClient { Future updateChannel( String channelId, String channelType, - Map data, { + Map data, { Message? message, }) => _chatApi.channel.updateChannel( diff --git a/packages/stream_chat/lib/src/core/api/channel_api.dart b/packages/stream_chat/lib/src/core/api/channel_api.dart index 761f10da..bdf2a401 100644 --- a/packages/stream_chat/lib/src/core/api/channel_api.dart +++ b/packages/stream_chat/lib/src/core/api/channel_api.dart @@ -92,7 +92,7 @@ class ChannelApi { Future updateChannel( String channelId, String channelType, - Map data, { + Map data, { Message? message, }) async { final response = await _client.post( diff --git a/packages/stream_chat/test/src/api/channel_test.dart b/packages/stream_chat/test/src/api/channel_test.dart new file mode 100644 index 00000000..85f83db5 --- /dev/null +++ b/packages/stream_chat/test/src/api/channel_test.dart @@ -0,0 +1,1872 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/client/channel.dart'; +import 'package:stream_chat/src/client/retry_policy.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:test/test.dart'; + +import '../fakes.dart'; +import '../matchers.dart'; +import '../mocks.dart'; + +void main() { + ChannelState _generateChannelState( + String channelId, + String channelType, { + bool mockChannelConfig = false, + }) { + ChannelConfig? config; + if (mockChannelConfig) { + config = MockChannelConfig(); + when(() => config!.readEvents).thenReturn(true); + when(() => config!.typingEvents).thenReturn(true); + } + final channel = ChannelModel( + id: channelId, + type: channelType, + config: config, + ); + final state = ChannelState(channel: channel); + return state; + } + + Logger _createLogger(String name) { + final logger = Logger.detached(name)..level = Level.ALL; + logger.onRecord.listen(print); + return logger; + } + + // TODO : test all persistence related logic in this group + group('Initialized Channel with Persistence', () { + late final client = MockStreamChatClientWithPersistence(); + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + const channelCid = '$channelType:$channelId'; + late Channel channel; + + setUpAll(() { + // Fallback values + registerFallbackValue(FakeMessage()); + registerFallbackValue>([]); + registerFallbackValue(FakeAttachmentFile()); + + // detached loggers + when(() => client.detachedLogger(any())).thenAnswer((invocation) { + final name = invocation.positionalArguments.first; + return _createLogger(name); + }); + + final retryPolicy = RetryPolicy( + shouldRetry: (_, __, ___) => false, + retryTimeout: (_, __, ___) => Duration.zero, + ); + when(() => client.retryPolicy).thenReturn(retryPolicy); + + final event = Event(type: 'event.local'); + when(() => client.on(any(), any(), any(), any())) + .thenAnswer((_) => Stream.value(event)); + + // fake clientState + final clientState = FakeClientState(); + when(() => client.state).thenReturn(clientState); + + // mock persistence client + final channelThreads = >{}; + when(() => client.chatPersistenceClient.getChannelThreads(channelCid)) + .thenAnswer((_) async => channelThreads); + final channelState = _generateChannelState(channelId, channelType); + when(() => client.chatPersistenceClient.getChannelStateByCid(channelCid)) + .thenAnswer((_) async => channelState); + when(() => client.chatPersistenceClient.updateMessages(channelCid, any())) + .thenAnswer((_) => Future.value()); + + // client logger + when(() => client.logger).thenReturn(_createLogger('mock-client-logger')); + }); + + // Setting up a initialized channel + setUp(() { + final channelState = _generateChannelState(channelId, channelType); + channel = Channel.fromState(client, channelState); + }); + + tearDown(() { + channel.dispose(); + }); + }); + + group('Initialized Channel', () { + late final client = MockStreamChatClient(); + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + const channelCid = '$channelType:$channelId'; + late Channel channel; + + setUpAll(() { + // Fallback values + registerFallbackValue(FakeMessage()); + registerFallbackValue(FakeAttachmentFile()); + registerFallbackValue(FakeEvent()); + + // detached loggers + when(() => client.detachedLogger(any())).thenAnswer((invocation) { + final name = invocation.positionalArguments.first; + return _createLogger(name); + }); + + final retryPolicy = RetryPolicy( + shouldRetry: (_, __, ___) => false, + retryTimeout: (_, __, ___) => Duration.zero, + ); + when(() => client.retryPolicy).thenReturn(retryPolicy); + + final event = Event(type: 'event.local'); + when(() => client.on(any(), any(), any(), any())) + .thenAnswer((_) => Stream.value(event)); + + // fake clientState + final clientState = FakeClientState(); + when(() => client.state).thenReturn(clientState); + + // client logger + when(() => client.logger).thenReturn(_createLogger('mock-client-logger')); + }); + + // Setting up a initialized channel + setUp(() { + final channelState = _generateChannelState( + channelId, + channelType, + mockChannelConfig: true, + ); + channel = Channel.fromState(client, channelState); + }); + + tearDown(() { + channel.dispose(); + }); + + group('`.sendMessage`', () { + test('should work fine', () async { + final message = Message(id: 'test-message-id'); + + final sendMessageResponse = SendMessageResponse()..message = message; + + when(() => client.sendMessage( + any(that: isSameMessageAs(message)), + channelId, + channelType, + )).thenAnswer((_) async => sendMessageResponse); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith(status: MessageSendingStatus.sending), + matchSendingStatus: true, + ), + ], + [ + isSameMessageAs( + message.copyWith(status: MessageSendingStatus.sent), + matchSendingStatus: true, + ), + ], + ]), + ); + + final res = await channel.sendMessage(message); + + expect(res, isNotNull); + expect(res.message.id, message.id); + + verify(() => client.sendMessage( + any(that: isSameMessageAs(message)), + channelId, + channelType, + )).called(1); + }); + + test('with attachments should work just fine', () async { + final attachments = List.generate( + 3, + (index) => Attachment( + id: 'test-attachment-id-$index', + type: index.isEven ? 'image' : 'file', + file: AttachmentFile(size: 33 * index, path: 'test-file-path'), + ), + ); + + final message = Message( + id: 'test-message-id', + attachments: attachments, + ); + + final sendImageResponse = SendImageResponse()..file = 'test-image-url'; + final sendFileResponse = SendFileResponse()..file = 'test-file-url'; + + when(() => client.sendImage( + any(), + channelId, + channelType, + onSendProgress: any(named: 'onSendProgress'), + cancelToken: any(named: 'cancelToken'), + )).thenAnswer((_) async => sendImageResponse); + + when(() => client.sendFile( + any(), + channelId, + channelType, + onSendProgress: any(named: 'onSendProgress'), + cancelToken: any(named: 'cancelToken'), + )).thenAnswer((_) async => sendFileResponse); + + when(() => client.sendMessage( + any(that: isSameMessageAs(message)), + channelId, + channelType, + )).thenAnswer((_) async => SendMessageResponse() + ..message = message.copyWith( + attachments: attachments + .map((it) => + it.copyWith(uploadState: const UploadState.success())) + .toList(growable: false), + )); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith(status: MessageSendingStatus.sending), + matchSendingStatus: true, + ), + ], + [ + isSameMessageAs( + message.copyWith(status: MessageSendingStatus.sent), + matchSendingStatus: true, + ), + ], + ]), + ); + + final res = await channel.sendMessage(message); + + expect(res, isNotNull); + expect(res.message.id, message.id); + expect(res.message.attachments.length, message.attachments.length); + expect( + res.message.attachments.every( + (it) => it.uploadState == const UploadState.success(), + ), + isTrue, + ); + + verify(() => client.sendImage( + any(), + channelId, + channelType, + onSendProgress: any(named: 'onSendProgress'), + cancelToken: any(named: 'cancelToken'), + )).called(2); + + verify(() => client.sendFile( + any(), + channelId, + channelType, + onSendProgress: any(named: 'onSendProgress'), + cancelToken: any(named: 'cancelToken'), + )).called(1); + + verify(() => client.sendMessage( + any(that: isSameMessageAs(message)), + channelId, + channelType, + )).called(1); + }); + }); + + group('`.updateMessage`', () { + test('should work fine', () async { + final message = Message(id: 'test-message-id'); + + final updateMessageResponse = UpdateMessageResponse() + ..message = message; + + when(() => client.updateMessage(any(that: isSameMessageAs(message)))) + .thenAnswer((_) async => updateMessageResponse); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith(status: MessageSendingStatus.updating), + matchSendingStatus: true, + ), + ], + [ + isSameMessageAs( + message.copyWith(status: MessageSendingStatus.sent), + matchSendingStatus: true, + ), + ], + ]), + ); + + final res = await channel.updateMessage(message); + + expect(res, isNotNull); + expect(res.message.id, message.id); + + verify(() => client.updateMessage( + any(that: isSameMessageAs(message)), + )).called(1); + }); + + test('with attachments should work just fine', () async { + final attachments = List.generate( + 3, + (index) => Attachment( + id: 'test-attachment-id-$index', + type: index.isEven ? 'image' : 'file', + file: AttachmentFile(size: 33 * index, path: 'test-file-path'), + ), + ); + + final message = Message( + id: 'test-message-id', + attachments: attachments, + ); + + final sendImageResponse = SendImageResponse()..file = 'test-image-url'; + final sendFileResponse = SendFileResponse()..file = 'test-file-url'; + + when(() => client.sendImage( + any(), + channelId, + channelType, + onSendProgress: any(named: 'onSendProgress'), + cancelToken: any(named: 'cancelToken'), + )).thenAnswer((_) async => sendImageResponse); + + when(() => client.sendFile( + any(), + channelId, + channelType, + onSendProgress: any(named: 'onSendProgress'), + cancelToken: any(named: 'cancelToken'), + )).thenAnswer((_) async => sendFileResponse); + + when(() => client.updateMessage( + any(that: isSameMessageAs(message)), + )).thenAnswer((_) async => UpdateMessageResponse() + ..message = message.copyWith( + attachments: attachments + .map((it) => + it.copyWith(uploadState: const UploadState.success())) + .toList(growable: false), + )); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith(status: MessageSendingStatus.updating), + matchSendingStatus: true, + ), + ], + [ + isSameMessageAs( + message.copyWith(status: MessageSendingStatus.sent), + matchSendingStatus: true, + ), + ], + ]), + ); + + final res = await channel.updateMessage(message); + + expect(res, isNotNull); + expect(res.message.id, message.id); + expect(res.message.attachments.length, message.attachments.length); + expect( + res.message.attachments.every( + (it) => it.uploadState == const UploadState.success(), + ), + isTrue, + ); + + verify(() => client.sendImage( + any(), + channelId, + channelType, + onSendProgress: any(named: 'onSendProgress'), + cancelToken: any(named: 'cancelToken'), + )).called(2); + + verify(() => client.sendFile( + any(), + channelId, + channelType, + onSendProgress: any(named: 'onSendProgress'), + cancelToken: any(named: 'cancelToken'), + )).called(1); + + verify(() => client.updateMessage( + any(that: isSameMessageAs(message)), + )).called(1); + }); + }); + + group('`.deleteMessage`', () { + test('should work fine', () async { + const messageId = 'test-message-id'; + final message = Message(id: messageId); + + when(() => client.deleteMessage(messageId)) + .thenAnswer((_) async => EmptyResponse()); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith(status: MessageSendingStatus.deleting), + matchSendingStatus: true, + ), + ], + [ + isSameMessageAs( + message.copyWith(status: MessageSendingStatus.sent), + matchSendingStatus: true, + ), + ], + ]), + ); + + final res = await channel.deleteMessage(message); + + expect(res, isNotNull); + + verify(() => client.deleteMessage(messageId)).called(1); + }); + + test( + 'should directly update the state with message as deleted if the state is sending or failed', + () async { + const messageId = 'test-message-id'; + final message = Message( + id: messageId, + status: MessageSendingStatus.sending, + ); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith(status: MessageSendingStatus.sent), + matchSendingStatus: true, + ), + ], + ]), + ); + + final res = await channel.deleteMessage(message); + + expect(res, isNotNull); + }, + ); + }); + + group('`.pinMessage`', () { + test('should work fine without passing timeoutOrExpirationDate', + () async { + final message = Message(id: 'test-message-id'); + + when(() => client.updateMessage(any(that: isSameMessageAs(message)))) + .thenAnswer((invocation) async => UpdateMessageResponse() + ..message = (invocation.positionalArguments.first as Message) + .copyWith(status: MessageSendingStatus.sent)); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith(status: MessageSendingStatus.updating), + matchSendingStatus: true, + ), + ], + [ + isSameMessageAs( + message.copyWith(status: MessageSendingStatus.sent), + matchSendingStatus: true, + ), + ], + ]), + ); + + final res = await channel.pinMessage(message); + + expect(res, isNotNull); + expect(res.message.pinned, isTrue); + expect(res.message.pinExpires, isNull); + + verify(() => client.updateMessage(any(that: isSameMessageAs(message)))) + .called(1); + }); + + test( + 'should work fine if passed timeoutOrExpirationDate as num(seconds)', + () async { + final message = Message(id: 'test-message-id'); + const timeoutOrExpirationDate = 300; // 300 seconds + + when(() => client.updateMessage(any(that: isSameMessageAs(message)))) + .thenAnswer((invocation) async => UpdateMessageResponse() + ..message = (invocation.positionalArguments.first as Message) + .copyWith(status: MessageSendingStatus.sent)); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith(status: MessageSendingStatus.updating), + matchSendingStatus: true, + ), + ], + [ + isSameMessageAs( + message.copyWith(status: MessageSendingStatus.sent), + matchSendingStatus: true, + ), + ], + ]), + ); + + final res = await channel.pinMessage( + message, + timeoutOrExpirationDate: timeoutOrExpirationDate, + ); + + expect(res, isNotNull); + expect(res.message.pinned, isTrue); + expect(res.message.pinExpires, isNotNull); + + verify(() => + client.updateMessage(any(that: isSameMessageAs(message)))) + .called(1); + }, + ); + + test( + 'should work fine if passed timeoutOrExpirationDate as DateTime', + () async { + final message = Message(id: 'test-message-id'); + final timeoutOrExpirationDate = + DateTime.now().add(const Duration(days: 3)); // 3 days + + when(() => client.updateMessage(any(that: isSameMessageAs(message)))) + .thenAnswer((invocation) async => UpdateMessageResponse() + ..message = (invocation.positionalArguments.first as Message) + .copyWith(status: MessageSendingStatus.sent)); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith(status: MessageSendingStatus.updating), + matchSendingStatus: true, + ), + ], + [ + isSameMessageAs( + message.copyWith(status: MessageSendingStatus.sent), + matchSendingStatus: true, + ), + ], + ]), + ); + + final res = await channel.pinMessage( + message, + timeoutOrExpirationDate: timeoutOrExpirationDate, + ); + + expect(res, isNotNull); + expect(res.message.pinned, isTrue); + expect(res.message.pinExpires, isNotNull); + expect(res.message.pinExpires, timeoutOrExpirationDate.toUtc()); + + verify( + () => client.updateMessage(any(that: isSameMessageAs(message))), + ).called(1); + }, + ); + + test( + 'should throw if invalid timeoutOrExpirationDate is passed', + () async { + final message = Message(id: 'test-message-id'); + const timeoutOrExpirationDate = 'invalid-value'; + + try { + await channel.pinMessage( + message, + timeoutOrExpirationDate: timeoutOrExpirationDate, + ); + } catch (e) { + expect(e, isA()); + } + }, + ); + }); + + test('`.unpinMessage`', () async { + final message = Message(id: 'test-message-id', pinned: true); + + when(() => client.updateMessage(any(that: isSameMessageAs(message)))) + .thenAnswer((invocation) async => UpdateMessageResponse() + ..message = (invocation.positionalArguments.first as Message) + .copyWith(status: MessageSendingStatus.sent)); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith(status: MessageSendingStatus.updating), + matchSendingStatus: true, + ), + ], + [ + isSameMessageAs( + message.copyWith(status: MessageSendingStatus.sent), + matchSendingStatus: true, + ), + ], + ]), + ); + + final res = await channel.unpinMessage(message); + + expect(res, isNotNull); + expect(res.message.pinned, isFalse); + + verify( + () => client.updateMessage(any(that: isSameMessageAs(message))), + ).called(1); + }); + + // + // /// Send a file to this channel + // Future sendFile( + // AttachmentFile file, { + // ProgressCallback? onSendProgress, + // CancelToken? cancelToken, + // }) { + // _checkInitialized(); + // return _client.sendFile( + // file, + // id!, + // type, + // onSendProgress: onSendProgress, + // cancelToken: cancelToken, + // ); + // } + // + // /// Send an image to this channel + // Future sendImage( + // AttachmentFile file, { + // ProgressCallback? onSendProgress, + // CancelToken? cancelToken, + // }) { + // _checkInitialized(); + // return _client.sendImage( + // file, + // id!, + // type, + // onSendProgress: onSendProgress, + // cancelToken: cancelToken, + // ); + // } + + group('`.search`', () { + final filter = Filter.in_('cid', const [channelCid]); + + test('should work fine with `query`', () async { + const query = 'test-search-query'; + const sort = [SortOption('test-sort-field')]; + const pagination = PaginationParams(); + + final results = List.generate(3, (index) => GetMessageResponse()); + + when(() => client.search( + filter, + query: query, + sort: any(named: 'sort'), + paginationParams: any(named: 'paginationParams'), + )).thenAnswer( + (_) async => SearchMessagesResponse()..results = results, + ); + + final res = await channel.search( + query: query, + sort: sort, + paginationParams: pagination, + ); + + expect(res, isNotNull); + expect(res.results.length, results.length); + + verify(() => client.search( + filter, + query: query, + sort: any(named: 'sort'), + paginationParams: any(named: 'paginationParams'), + )).called(1); + }); + + test('should work fine with `messageFilters`', () async { + final messageFilters = Filter.query('key', 'text'); + const sort = [SortOption('test-sort-field')]; + const pagination = PaginationParams(); + + final results = List.generate(3, (index) => GetMessageResponse()); + + when(() => client.search( + filter, + messageFilters: messageFilters, + sort: any(named: 'sort'), + paginationParams: any(named: 'paginationParams'), + )).thenAnswer( + (_) async => SearchMessagesResponse()..results = results, + ); + + final res = await channel.search( + sort: sort, + paginationParams: pagination, + messageFilters: messageFilters, + ); + + expect(res, isNotNull); + expect(res.results.length, results.length); + + verify(() => client.search( + filter, + messageFilters: messageFilters, + sort: any(named: 'sort'), + paginationParams: any(named: 'paginationParams'), + )).called(1); + }); + }); + + test('`.deleteFile`', () async { + const url = 'test-file-url'; + + when(() => client.deleteFile(url, channelId, channelType, + cancelToken: any(named: 'cancelToken'))) + .thenAnswer((_) async => EmptyResponse()); + + final res = await channel.deleteFile(url); + + expect(res, isNotNull); + + verify(() => client.deleteFile(url, channelId, channelType, + cancelToken: any(named: 'cancelToken'))).called(1); + }); + + test('`.deleteImage`', () async { + const url = 'test-image-url'; + + when(() => client.deleteImage(url, channelId, channelType, + cancelToken: any(named: 'cancelToken'))) + .thenAnswer((_) async => EmptyResponse()); + + final res = await channel.deleteImage(url); + + expect(res, isNotNull); + + verify(() => client.deleteImage(url, channelId, channelType, + cancelToken: any(named: 'cancelToken'))).called(1); + }); + + test('`.sendEvent`', () async { + final event = Event(type: 'event.local'); + + when(() => client.sendEvent(channelId, channelType, event)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await channel.sendEvent(event); + + expect(res, isNotNull); + + verify(() => client.sendEvent(channelId, channelType, event)).called(1); + }); + + group('`.sendReaction`', () { + test('should work fine', () async { + const type = 'test-reaction-type'; + final message = Message(id: 'test-message-id'); + + final reaction = Reaction(type: type, messageId: message.id); + + when(() => client.sendReaction(message.id, type)).thenAnswer( + (_) async => SendReactionResponse() + ..message = message + ..reaction = reaction, + ); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith( + status: MessageSendingStatus.sent, + reactionCounts: {type: 1}, + reactionScores: {type: 1}, + latestReactions: [reaction], + ownReactions: [reaction], + ), + matchReactions: true, + matchSendingStatus: true, + ), + ], + ]), + ); + + final res = await channel.sendReaction(message, type); + + expect(res, isNotNull); + expect(res.reaction.type, type); + expect(res.reaction.messageId, message.id); + + verify(() => client.sendReaction(message.id, type)).called(1); + }); + + test( + 'should restore previous message if `client.sendReaction` throws', + () async { + const type = 'test-reaction-type'; + final message = Message(id: 'test-message-id'); + + final reaction = Reaction(type: type, messageId: message.id); + + when(() => client.sendReaction(message.id, type)) + .thenThrow(StreamChatNetworkError(ChatErrorCode.inputError)); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith( + status: MessageSendingStatus.sent, + reactionCounts: {type: 1}, + reactionScores: {type: 1}, + latestReactions: [reaction], + ownReactions: [reaction], + ), + matchReactions: true, + matchSendingStatus: true, + ), + ], + [ + isSameMessageAs( + message, + matchReactions: true, + matchSendingStatus: true, + ), + ], + ]), + ); + + try { + await channel.sendReaction(message, type); + } catch (e) { + expect(e, isA()); + } + + verify(() => client.sendReaction(message.id, type)).called(1); + }, + ); + + test( + 'should override previous reaction if present and `enforceUnique` is true', + () async { + const userId = 'test-user-id'; + const messageId = 'test-message-id'; + const prevType = 'test-reaction-type'; + final prevReaction = Reaction( + type: prevType, + messageId: messageId, + userId: userId, + ); + final message = Message( + id: messageId, + ownReactions: [prevReaction], + latestReactions: [prevReaction], + reactionScores: const {prevType: 1}, + reactionCounts: const {prevType: 1}, + ); + + const type = 'test-reaction-type-2'; + final newReaction = Reaction( + type: type, + messageId: messageId, + userId: userId, + ); + final newMessage = message.copyWith( + ownReactions: [newReaction], + latestReactions: [newReaction], + ); + + const enforceUnique = true; + + when(() => client.sendReaction( + messageId, + type, + enforceUnique: enforceUnique, + )).thenAnswer( + (_) async => SendReactionResponse() + ..message = newMessage + ..reaction = newReaction, + ); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + newMessage.copyWith(status: MessageSendingStatus.sent), + matchReactions: true, + matchSendingStatus: true, + ), + ], + ]), + ); + + final res = await channel.sendReaction( + message, + type, + enforceUnique: enforceUnique, + ); + + expect(res, isNotNull); + expect(res.reaction.type, type); + expect(res.reaction.messageId, messageId); + + verify(() => client.sendReaction( + messageId, + type, + enforceUnique: enforceUnique, + )).called(1); + }, + ); + }); + + group('`.deleteReaction`', () { + test('should work fine', () async { + const userId = 'test-user-id'; + const messageId = 'test-message-id'; + const type = 'test-reaction-type'; + final reaction = Reaction( + type: type, + messageId: messageId, + userId: userId, + ); + final message = Message( + id: messageId, + ownReactions: [reaction], + latestReactions: [reaction], + reactionScores: const {type: 1}, + reactionCounts: const {type: 1}, + ); + + when(() => client.deleteReaction(messageId, type)) + .thenAnswer((_) async => EmptyResponse()); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith( + status: MessageSendingStatus.sent, + latestReactions: [], + ownReactions: [], + ), + matchReactions: true, + matchSendingStatus: true, + ), + ], + ]), + ); + + final res = await channel.deleteReaction(message, reaction); + + expect(res, isNotNull); + + verify(() => client.deleteReaction(messageId, type)).called(1); + }); + + test( + 'should restore prev message state if `client.deleteReaction` throws', + () async { + const userId = 'test-user-id'; + const messageId = 'test-message-id'; + const type = 'test-reaction-type'; + final reaction = Reaction( + type: type, + messageId: messageId, + userId: userId, + ); + final message = Message( + id: messageId, + ownReactions: [reaction], + latestReactions: [reaction], + reactionScores: const {type: 1}, + reactionCounts: const {type: 1}, + ); + + when(() => client.deleteReaction(messageId, type)) + .thenThrow(StreamChatNetworkError(ChatErrorCode.inputError)); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith( + status: MessageSendingStatus.sent, + latestReactions: [], + ownReactions: [], + ), + matchReactions: true, + matchSendingStatus: true, + ), + ], + [ + isSameMessageAs( + message, + matchReactions: true, + matchSendingStatus: true, + ), + ], + ]), + ); + + try { + await channel.deleteReaction(message, reaction); + } catch (e) { + expect(e, isA()); + } + + verify(() => client.deleteReaction(messageId, type)).called(1); + }, + ); + }); + + test('`.update`', () async { + const channelData = { + 'name': 'Stream Team', + 'profile_image': 'test-profile-image', + }; + final updateMessage = Message( + id: 'test-message-id', + text: 'updated channel', + ); + + final channelModel = ChannelModel( + cid: channelCid, + extraData: channelData, + ); + + when(() => client.updateChannel(channelId, channelType, channelData, + message: any(named: 'message'))).thenAnswer( + (_) async => UpdateChannelResponse() + ..channel = channelModel + ..message = updateMessage, + ); + + final res = await channel.update(channelData, updateMessage); + + expect(res, isNotNull); + expect(res.channel.cid, channelModel.cid); + expect(res.channel.extraData, channelData); + expect(res.message?.id, updateMessage.id); + + verify(() => client.updateChannel(channelId, channelType, channelData, + message: any(named: 'message'))).called(1); + }); + + test('`.updatePartial`', () async { + const channelData = { + 'name': 'Stream Team', + 'profile_image': 'test-profile-image', + }; + + final channelModel = ChannelModel( + cid: channelCid, + extraData: { + 'coolness': 999, + ...channelData, + }, + ); + + when(() => client.updateChannelPartial( + channelId, + channelType, + channelData, + )).thenAnswer( + (_) async => PartialUpdateChannelResponse()..channel = channelModel, + ); + + final res = await channel.updatePartial(channelData); + + expect(res, isNotNull); + expect(res.channel.cid, channelModel.cid); + expect( + res.channel.extraData, + {'coolness': 999, ...channelData}, + ); + + verify(() => client.updateChannelPartial( + channelId, + channelType, + channelData, + )).called(1); + }); + + test('`.delete`', () async { + when(() => client.deleteChannel(channelId, channelType)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await channel.delete(); + + expect(res, isNotNull); + + verify(() => client.deleteChannel(channelId, channelType)).called(1); + }); + + test('`.truncate`', () async { + when(() => client.truncateChannel(channelId, channelType)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await channel.truncate(); + + expect(res, isNotNull); + + verify(() => client.truncateChannel(channelId, channelType)).called(1); + }); + + test('`.acceptInvite`', () async { + final message = Message(id: 'test-message-id', text: 'Invite Accepted'); + + final channelModel = ChannelModel(cid: channelCid); + + when(() => client.acceptChannelInvite(channelId, channelType, + message: any(named: 'message'))).thenAnswer( + (_) async => AcceptInviteResponse() + ..channel = channelModel + ..message = message, + ); + + final res = await channel.acceptInvite(message); + + expect(res, isNotNull); + expect(res.channel.cid, channelModel.cid); + expect(res.message?.id, message.id); + + verify(() => client.acceptChannelInvite(channelId, channelType, + message: any(named: 'message'))).called(1); + }); + + test('`.rejectInvite`', () async { + final message = Message(id: 'test-message-id', text: 'Invite Rejected'); + + final channelModel = ChannelModel(cid: channelCid); + + when(() => client.rejectChannelInvite(channelId, channelType, + message: any(named: 'message'))).thenAnswer( + (_) async => RejectInviteResponse() + ..channel = channelModel + ..message = message, + ); + + final res = await channel.rejectInvite(message); + + expect(res, isNotNull); + expect(res.channel.cid, channelModel.cid); + expect(res.message?.id, message.id); + + verify(() => client.rejectChannelInvite(channelId, channelType, + message: any(named: 'message'))).called(1); + }); + + test('`.addMembers`', () async { + final members = List.generate( + 3, + (index) => Member(userId: 'test-member-id-$index'), + ); + final memberIds = members + .map((it) => it.userId) + .whereType() + .toList(growable: false); + final message = Message(id: 'test-message-id', text: 'Members Added'); + + final channelModel = ChannelModel(cid: channelCid); + + when(() => client.addChannelMembers(channelId, channelType, memberIds, + message: any(named: 'message'))).thenAnswer( + (_) async => AddMembersResponse() + ..channel = channelModel + ..members = members + ..message = message, + ); + + final res = await channel.addMembers(memberIds, message); + + expect(res, isNotNull); + expect(res.channel.cid, channelModel.cid); + expect(res.members.length, members.length); + expect(res.message?.id, message.id); + + verify(() => client.addChannelMembers(channelId, channelType, memberIds, + message: any(named: 'message'))).called(1); + }); + + test('`.inviteMembers`', () async { + final members = List.generate( + 3, + (index) => Member(userId: 'test-member-id-$index'), + ); + final memberIds = members + .map((it) => it.userId) + .whereType() + .toList(growable: false); + final message = Message(id: 'test-message-id', text: 'Members Invited'); + + final channelModel = ChannelModel(cid: channelCid); + + when(() => client.inviteChannelMembers(channelId, channelType, memberIds, + message: any(named: 'message'))).thenAnswer( + (_) async => InviteMembersResponse() + ..channel = channelModel + ..members = members + ..message = message, + ); + + final res = await channel.inviteMembers(memberIds, message); + + expect(res, isNotNull); + expect(res.channel.cid, channelModel.cid); + expect(res.members.length, members.length); + expect(res.message?.id, message.id); + + verify(() => client.inviteChannelMembers( + channelId, channelType, memberIds, + message: any(named: 'message'))).called(1); + }); + + test('`.removeMembers`', () async { + final members = List.generate( + 3, + (index) => Member(userId: 'test-member-id-$index'), + ); + final memberIds = members + .map((it) => it.userId) + .whereType() + .toList(growable: false); + final message = Message(id: 'test-message-id', text: 'Members Removed'); + + final channelModel = ChannelModel(cid: channelCid); + + when(() => client.removeChannelMembers(channelId, channelType, memberIds, + message: any(named: 'message'))).thenAnswer( + (_) async => RemoveMembersResponse() + ..channel = channelModel + ..members = members + ..message = message, + ); + + final res = await channel.removeMembers(memberIds, message); + + expect(res, isNotNull); + expect(res.channel.cid, channelModel.cid); + expect(res.members.length, members.length); + expect(res.message?.id, message.id); + + verify(() => client.removeChannelMembers( + channelId, channelType, memberIds, + message: any(named: 'message'))).called(1); + }); + + group('`.sendAction`', () { + test('should work fine', () async { + final message = Message(id: 'test-message-id', text: 'Action Sent'); + const formData = {'key': 'value'}; + + when( + () => client.sendAction(channelId, channelType, message.id, formData), + ).thenAnswer((_) async => SendActionResponse()); + + final res = await channel.sendAction(message, formData); + + expect(res, isNotNull); + + verify( + () => client.sendAction(channelId, channelType, message.id, formData), + ).called(1); + }); + + test('should emit received message if not null', () async { + final message = Message(id: 'test-message-id', text: 'Action Sent'); + const formData = {'key': 'value'}; + + when( + () => client.sendAction(channelId, channelType, message.id, formData), + ).thenAnswer((_) async => SendActionResponse()..message = message); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message, + matchSendingStatus: true, + ), + ], + ]), + ); + + final res = await channel.sendAction(message, formData); + + expect(res, isNotNull); + expect(res.message?.id, message.id); + + verify( + () => client.sendAction(channelId, channelType, message.id, formData), + ).called(1); + }); + }); + + test('`.markRead`', () async { + const messageId = 'test-message-id'; + + when(() => client.markChannelRead(channelId, channelType, + messageId: messageId)).thenAnswer((_) async => EmptyResponse()); + + expectLater( + // skipping first seed unread count -> 0 unread count + channel.state?.unreadCountStream.skip(1), + emitsInOrder([0]), + ); + + final res = await channel.markRead(messageId: messageId); + + expect(res, isNotNull); + expect(client.state.totalUnreadCount, 0); + + verify(() => client.markChannelRead(channelId, channelType, + messageId: messageId)).called(1); + }); + + group('`.watch`', () { + test('should work fine', () async { + when(() => client.queryChannel( + channelType, + channelId: channelId, + watch: true, + channelData: any(named: 'channelData'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + )).thenAnswer( + (_) async => _generateChannelState(channelId, channelType), + ); + + final res = await channel.watch(); + + expect(res, isNotNull); + expect(res.channel, isNotNull); + expect(res.channel?.cid, channelCid); + + verify(() => client.queryChannel( + channelType, + channelId: channelId, + watch: true, + channelData: any(named: 'channelData'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + )).called(1); + }); + + test('should rethrow if `.query` throws', () async { + when(() => client.queryChannel( + channelType, + channelId: channelId, + watch: true, + channelData: any(named: 'channelData'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + )).thenThrow(StreamChatNetworkError(ChatErrorCode.inputError)); + + try { + await channel.watch(); + } catch (e) { + expect(e, isA()); + } + + verify(() => client.queryChannel( + channelType, + channelId: channelId, + watch: true, + channelData: any(named: 'channelData'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + )).called(1); + }); + }); + + test('`.stopWatching`', () async { + when(() => client.stopChannelWatching(channelId, channelType)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await channel.stopWatching(); + + expect(res, isNotNull); + + verify(() => client.stopChannelWatching(channelId, channelType)) + .called(1); + }); + + test('`.getReplies`', () async { + const parentId = 'test-parent-id'; + const options = PaginationParams(); + + final messages = List.generate( + 3, + (index) => Message( + id: 'test-message-id-$index', + parentId: parentId, + ), + ); + + when(() => client.getReplies(parentId, options)).thenAnswer( + (_) async => QueryRepliesResponse()..messages = messages, + ); + + final res = await channel.getReplies(parentId, options); + + expect(res, isNotNull); + expect(res.messages.length, messages.length); + expect(res.messages.every((it) => it.parentId == parentId), isTrue); + + verify(() => client.getReplies(parentId, options)).called(1); + }); + + test('`.getReactions`', () async { + const messageId = 'test-message-id'; + const options = PaginationParams(); + + final reactions = List.generate( + 3, + (index) => Reaction( + type: 'test-reaction-type-$index', + messageId: messageId, + ), + ); + + when(() => client.getReactions(messageId, options)).thenAnswer( + (_) async => QueryReactionsResponse()..reactions = reactions, + ); + + final res = await channel.getReactions(messageId, options); + + expect(res, isNotNull); + expect(res.reactions.length, reactions.length); + expect(res.reactions.every((it) => it.messageId == messageId), isTrue); + + verify(() => client.getReactions(messageId, options)).called(1); + }); + + test('`.getMessagesById`', () async { + final messages = List.generate( + 3, + (index) => Message(id: 'test-message-id-$index'), + ); + + final messageIds = messages.map((it) => it.id).toList(growable: false); + + when(() => client.getMessagesById(channelId, channelType, messageIds)) + .thenAnswer( + (_) async => GetMessagesByIdResponse()..messages = messages, + ); + + final res = await channel.getMessagesById(messageIds); + + expect(res, isNotNull); + expect(res.messages.length, messageIds.length); + + verify( + () => client.getMessagesById(channelId, channelType, messageIds), + ).called(1); + }); + + test('`.translateMessage`', () async { + const messageId = 'test-message-id'; + const language = 'hi'; // Hindi + const translatedMessageText = 'नमसà¥à¤¤à¥‡'; + final translatedMessage = TranslatedMessage(const { + language: translatedMessageText, + }); + + when(() => client.translateMessage(messageId, language)).thenAnswer( + (_) async => TranslateMessageResponse()..message = translatedMessage, + ); + + final res = await channel.translateMessage(messageId, language); + + expect(res, isNotNull); + expect(res.message.i18n, translatedMessage.i18n); + + verify(() => client.translateMessage(messageId, language)).called(1); + }); + + group('`.query`', () { + test('should work fine', () async { + final channelState = _generateChannelState(channelId, channelType); + + when( + () => client.queryChannel( + channelType, + channelId: channelId, + channelData: any(named: 'channelData'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + ), + ).thenAnswer((_) async => channelState); + + final res = await channel.query(); + + expect(res, isNotNull); + + verify( + () => client.queryChannel( + channelType, + channelId: channelId, + channelData: any(named: 'channelData'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + ), + ).called(1); + }); + + test('should rethrow if `client.queryChannel` throws', () async { + when( + () => client.queryChannel( + channelType, + channelId: channelId, + channelData: any(named: 'channelData'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + ), + ).thenThrow(StreamChatNetworkError(ChatErrorCode.inputError)); + + try { + await channel.query(); + } catch (e) { + expect(e, isA()); + } + + verify( + () => client.queryChannel( + channelType, + channelId: channelId, + channelData: any(named: 'channelData'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + ), + ).called(1); + }); + }); + + test('`.queryMembers`', () async { + final filter = Filter.in_('cid', const [channelCid]); + + final members = List.generate( + 3, + (index) => Member(userId: 'test-user-id-$index'), + ); + + when(() => client.queryMembers( + channelType, + channelId: channelId, + filter: filter, + members: any(named: 'members'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + )).thenAnswer((_) async => QueryMembersResponse()..members = members); + + final res = await channel.queryMembers(filter: filter); + + expect(res, isNotNull); + expect(res.members.length, members.length); + + verify(() => client.queryMembers( + channelType, + channelId: channelId, + filter: filter, + members: any(named: 'members'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + )).called(1); + }); + + test('`.mute`', () async { + when(() => client.muteChannel( + channelCid, + expiration: any(named: 'expiration'), + )).thenAnswer((_) async => EmptyResponse()); + + final res = await channel.mute(); + + expect(res, isNotNull); + + verify(() => client.muteChannel( + channelCid, + expiration: any(named: 'expiration'), + )).called(1); + }); + + test('`.unmute`', () async { + when( + () => client.unmuteChannel(channelCid), + ).thenAnswer((_) async => EmptyResponse()); + + final res = await channel.unmute(); + + expect(res, isNotNull); + + verify( + () => client.unmuteChannel(channelCid), + ).called(1); + }); + + test('`.banUser`', () async { + const userId = 'test-user-id'; + const options = {'key': 'value'}; + + when(() => client.banUser( + userId, + {'type': channelType, 'id': channelId, ...options}, + )).thenAnswer((_) async => EmptyResponse()); + + final res = await channel.banUser(userId, options); + + expect(res, isNotNull); + + verify(() => client.banUser( + userId, + {'type': channelType, 'id': channelId, ...options}, + )).called(1); + }); + + test('`.unbanUser`', () async { + const userId = 'test-user-id'; + + when(() => client.unbanUser(userId, any())) + .thenAnswer((_) async => EmptyResponse()); + + final res = await channel.unbanUser(userId); + + expect(res, isNotNull); + + verify(() => client.unbanUser(userId, any())).called(1); + }); + + test('`.shadowBan`', () async { + const userId = 'test-user-id'; + const options = {'key': 'value'}; + + when(() => client.shadowBan( + userId, + {'type': channelType, 'id': channelId, ...options}, + )).thenAnswer((_) async => EmptyResponse()); + + final res = await channel.shadowBan(userId, options); + + expect(res, isNotNull); + + verify(() => client.shadowBan( + userId, + {'type': channelType, 'id': channelId, ...options}, + )).called(1); + }); + + test('`.removeShadowBan`', () async { + const userId = 'test-user-id'; + + when(() => client.removeShadowBan(userId, any())) + .thenAnswer((_) async => EmptyResponse()); + + final res = await channel.removeShadowBan(userId); + + expect(res, isNotNull); + + verify(() => client.removeShadowBan(userId, any())).called(1); + }); + + test('`.hide`', () async { + const clearHistory = true; + + when(() => client.hideChannel( + channelId, + channelType, + clearHistory: clearHistory, + )).thenAnswer((_) async => EmptyResponse()); + + final res = await channel.hide(clearHistory: clearHistory); + + expect(res, isNotNull); + + verify(() => client.hideChannel( + channelId, + channelType, + clearHistory: clearHistory, + )).called(1); + }); + + test('`.show`', () async { + when(() => client.showChannel(channelId, channelType)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await channel.show(); + + expect(res, isNotNull); + + verify(() => client.showChannel(channelId, channelType)).called(1); + }); + + test('`.on`', () async { + const eventType = 'test.event'; + final event = Event(type: eventType, cid: channelCid); + + when(() => client.on(eventType, any(), any(), any())) + .thenAnswer((_) => Stream.value(event)); + + expectLater(channel.on(eventType), emitsInOrder([event])); + + verify(() => client.on(eventType, any(), any(), any())).called(1); + }); + + group( + '`.keyStroke`', + () { + test('should return if `config.typingEvents` is false', () async { + when(() => channel.config?.typingEvents).thenReturn(false); + + final typingEvent = Event(type: EventType.typingStart); + + await channel.keyStroke(); + + verifyNever(() => client.sendEvent( + channelId, + channelType, + any(that: isSameEventAs(typingEvent)), + )); + }); + + test( + 'should send `typingStart` event if there is not already a typingEvent or the difference between the two is >= 2 seconds', + () async { + final typingEvent = Event(type: EventType.typingStart); + + when(() => channel.config?.typingEvents).thenReturn(true); + + when(() => client.sendEvent( + channelId, + channelType, + any(that: isSameEventAs(typingEvent)), + )).thenAnswer((_) async => EmptyResponse()); + + await channel.keyStroke(); + + verify(() => client.sendEvent( + channelId, + channelType, + any(that: isSameEventAs(typingEvent)), + )).called(1); + }, + ); + }, + ); + + group('`.stopTyping`', () { + test('should return if `config.typingEvents` is false', () async { + when(() => channel.config?.typingEvents).thenReturn(false); + + final typingStopEvent = Event(type: EventType.typingStop); + + await channel.keyStroke(); + + verifyNever(() => client.sendEvent( + channelId, + channelType, + any(that: isSameEventAs(typingStopEvent)), + )); + }); + + test('should send `typingStop` successfully', () async { + final typingStopEvent = Event(type: EventType.typingStop); + + when(() => channel.config?.typingEvents).thenReturn(true); + + when(() => client.sendEvent( + channelId, + channelType, + any(that: isSameEventAs(typingStopEvent)), + )).thenAnswer((_) async => EmptyResponse()); + + await channel.stopTyping(); + + verify(() => client.sendEvent( + channelId, + channelType, + any(that: isSameEventAs(typingStopEvent)), + )).called(1); + }); + }); + }); +} diff --git a/packages/stream_chat/test/src/api/requests_test.dart b/packages/stream_chat/test/src/core/api/requests_test.dart similarity index 100% rename from packages/stream_chat/test/src/api/requests_test.dart rename to packages/stream_chat/test/src/core/api/requests_test.dart diff --git a/packages/stream_chat/test/src/api/responses_test.dart b/packages/stream_chat/test/src/core/api/responses_test.dart similarity index 100% rename from packages/stream_chat/test/src/api/responses_test.dart rename to packages/stream_chat/test/src/core/api/responses_test.dart diff --git a/packages/stream_chat/test/src/fakes.dart b/packages/stream_chat/test/src/fakes.dart index b7e431b1..f1468271 100644 --- a/packages/stream_chat/test/src/fakes.dart +++ b/packages/stream_chat/test/src/fakes.dart @@ -82,3 +82,24 @@ class FakeChatApi extends Fake implements StreamChatApi { AttachmentFileUploader get fileUploader => _fileUploader ??= MockAttachmentFileUploader(); } + +class FakeClientState extends Fake implements ClientState { + @override + OwnUser? get user => OwnUser(id: 'test-user-id'); + + var _totalUnreadCount = 0; + + @override + int? get totalUnreadCount => _totalUnreadCount; + + @override + set totalUnreadCount(int? unreadCount) { + _totalUnreadCount += unreadCount ?? 0; + } +} + +class FakeMessage extends Fake implements Message {} + +class FakeAttachmentFile extends Fake implements AttachmentFile {} + +class FakeEvent extends Fake implements Event {} diff --git a/packages/stream_chat/test/src/matchers.dart b/packages/stream_chat/test/src/matchers.dart index a2be680f..c958ff5f 100644 --- a/packages/stream_chat/test/src/matchers.dart +++ b/packages/stream_chat/test/src/matchers.dart @@ -1,4 +1,7 @@ +import 'package:collection/collection.dart'; import 'package:dio/dio.dart' show MultipartFile; +import 'package:stream_chat/src/core/models/event.dart'; +import 'package:stream_chat/src/core/models/message.dart'; import 'package:test/test.dart'; Matcher isSameMultipartFileAs(MultipartFile targetFile) => @@ -17,3 +20,72 @@ class _IsSameMultipartFileAs extends Matcher { bool matches(covariant MultipartFile file, Map matchState) => file.length == targetFile.length; } + +Matcher isSameEventAs(Event targetEvent) => + _IsSameEventAs(targetEvent: targetEvent); + +class _IsSameEventAs extends Matcher { + const _IsSameEventAs({required this.targetEvent}); + + final Event targetEvent; + + @override + Description describe(Description description) => + description.add('is same event as $targetEvent'); + + @override + bool matches(covariant Event event, Map matchState) => + event.type == targetEvent.type; +} + +Matcher isSameMessageAs( + Message targetMessage, { + bool matchReactions = false, + bool matchSendingStatus = false, +}) => + _IsSameMessageAs( + targetMessage: targetMessage, + matchReactions: matchReactions, + matchSendingStatus: matchSendingStatus, + ); + +class _IsSameMessageAs extends Matcher { + const _IsSameMessageAs({ + required this.targetMessage, + this.matchReactions = false, + this.matchSendingStatus = false, + }); + + final Message targetMessage; + final bool matchReactions; + final bool matchSendingStatus; + + @override + Description describe(Description description) => + description.add('is same message as $targetMessage'); + + @override + bool matches(covariant Message message, Map matchState) { + var matches = message.id == targetMessage.id; + if (matchSendingStatus) { + matches &= message.status == targetMessage.status; + } + if (matchReactions) { + matches &= const ListEquality().equals( + message.ownReactions + ?.map((it) => '${it.type}-${it.messageId}') + .toList(), + targetMessage.ownReactions + ?.map((it) => '${it.type}-${it.messageId}') + .toList()); + matches &= const ListEquality().equals( + message.latestReactions + ?.map((it) => '${it.type}-${it.messageId}') + .toList(), + targetMessage.latestReactions + ?.map((it) => '${it.type}-${it.messageId}') + .toList()); + } + return matches; + } +} diff --git a/packages/stream_chat/test/src/mocks.dart b/packages/stream_chat/test/src/mocks.dart index 411f06a3..9c20be7c 100644 --- a/packages/stream_chat/test/src/mocks.dart +++ b/packages/stream_chat/test/src/mocks.dart @@ -1,6 +1,7 @@ import 'package:dio/dio.dart'; import 'package:logging/logging.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/client/client.dart'; import 'package:stream_chat/src/core/api/attachment_file_uploader.dart'; import 'package:stream_chat/src/core/api/channel_api.dart'; import 'package:stream_chat/src/core/api/device_api.dart'; @@ -12,8 +13,13 @@ import 'package:stream_chat/src/core/api/user_api.dart'; import 'package:stream_chat/src/core/http/connection_id_manager.dart'; import 'package:stream_chat/src/core/http/stream_http_client.dart'; import 'package:stream_chat/src/core/http/token_manager.dart'; +import 'package:stream_chat/src/core/models/channel_config.dart'; +import 'package:stream_chat/src/core/models/channel_model.dart'; +import 'package:stream_chat/src/db/chat_persistence_client.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; +import 'db/chat_persistence_client_test.dart'; + class MockWebSocketChannel extends Mock implements WebSocketChannel {} class MockWebSocketSink extends Mock implements WebSocketSink {} @@ -57,3 +63,24 @@ class MockGeneralApi extends Mock implements GeneralApi {} class MockAttachmentFileUploader extends Mock implements AttachmentFileUploader {} + +class MockPersistenceClient extends Mock implements ChatPersistenceClient {} + +class MockStreamChatClient extends Mock implements StreamChatClient { + @override + bool get persistenceEnabled => false; +} + +class MockStreamChatClientWithPersistence extends Mock + implements StreamChatClient { + ChatPersistenceClient? _persistenceClient; + + @override + ChatPersistenceClient get chatPersistenceClient => + _persistenceClient ??= MockPersistenceClient(); + + @override + bool get persistenceEnabled => true; +} + +class MockChannelConfig extends Mock implements ChannelConfig {} From 33f19ebd5d89a458aa29b3c137f880b86c06d3ab Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 10 Jun 2021 11:06:15 +0200 Subject: [PATCH 024/150] add comments --- .../lib/src/better_stream_builder.dart | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/stream_chat_flutter_core/lib/src/better_stream_builder.dart b/packages/stream_chat_flutter_core/lib/src/better_stream_builder.dart index 9691618a..a51c0762 100644 --- a/packages/stream_chat_flutter_core/lib/src/better_stream_builder.dart +++ b/packages/stream_chat_flutter_core/lib/src/better_stream_builder.dart @@ -2,7 +2,12 @@ import 'dart:async'; import 'package:flutter/widgets.dart'; +/// A more efficient [StreamBuilder] +/// It requires [initialData] and will rebuild +/// only when the new data is different than the current data +/// The [comparator] is used to check if the new data is different class BetterStreamBuilder extends StatefulWidget { + /// Creates a new BetterStreamBuilder const BetterStreamBuilder({ required this.stream, required this.initialData, @@ -13,11 +18,22 @@ class BetterStreamBuilder extends StatefulWidget { Key? key, }) : super(key: key); + /// The stream to listen to final Stream? stream; + + /// The initial data available final T initialData; + + /// Comparator used to check if the new data is different than the last one final bool Function(T?, T)? comparator; + + /// Builder that builds based on the new snapshot final Widget Function(BuildContext context, T data) builder; + + /// Builder that builds when the data is null final Widget Function(BuildContext context)? loadingBuilder; + + /// Builder used when there is an error final Widget Function(BuildContext context, Object error)? errorBuilder; @override From a6ff8e70393206532478bc419188c32e5aba3a0a Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 10 Jun 2021 12:33:20 +0200 Subject: [PATCH 025/150] fix tests --- .../test/src/channel_header_test.dart | 2 + .../test/src/message_input_test.dart | 10 ---- .../stream_chat_flutter/test/src/mocks.dart | 13 +++- .../test/src/unread_indicator_test.dart | 2 +- .../lib/src/stream_channel.dart | 60 +++++++++---------- .../test/message_list_core_test.dart | 7 +++ .../stream_chat_flutter_core/test/mocks.dart | 4 ++ .../test/stream_chat_core_test.dart | 45 +++++++------- 8 files changed, 77 insertions(+), 66 deletions(-) diff --git a/packages/stream_chat_flutter/test/src/channel_header_test.dart b/packages/stream_chat_flutter/test/src/channel_header_test.dart index 6da3e20f..9f6e981d 100644 --- a/packages/stream_chat_flutter/test/src/channel_header_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_header_test.dart @@ -107,6 +107,8 @@ void main() { ]); when(() => client.wsConnectionStatusStream) .thenAnswer((_) => Stream.value(ConnectionStatus.disconnected)); + when(() => client.wsConnectionStatus) + .thenReturn(ConnectionStatus.disconnected); when(() => clientState.totalUnreadCountStream) .thenAnswer((i) => Stream.value(1)); diff --git a/packages/stream_chat_flutter/test/src/message_input_test.dart b/packages/stream_chat_flutter/test/src/message_input_test.dart index 63ddb10f..f07f518d 100644 --- a/packages/stream_chat_flutter/test/src/message_input_test.dart +++ b/packages/stream_chat_flutter/test/src/message_input_test.dart @@ -53,15 +53,6 @@ void main() { ) ])); - when(() => channelState.typingEvents).thenAnswer((i) => [ - User(id: 'other-user', extraData: {'name': 'demo'}) - ]); - when(() => channelState.typingEventsStream) - .thenAnswer((i) => Stream.value([ - User(id: 'other-user', extraData: {'name': 'demo'}), - User(id: 'other-user', extraData: {'name': 'demo'}), - ])); - await tester.pumpWidget(MaterialApp( home: StreamChat( client: client, @@ -75,7 +66,6 @@ void main() { )); expect(find.byType(TextField), findsOneWidget); - expect(find.byType(StreamSvgIcon), findsNWidgets(8)); expect(find.byKey(const Key('messageInputText')), findsOneWidget); }, ); diff --git a/packages/stream_chat_flutter/test/src/mocks.dart b/packages/stream_chat_flutter/test/src/mocks.dart index 083c2ff5..d2b04e11 100644 --- a/packages/stream_chat_flutter/test/src/mocks.dart +++ b/packages/stream_chat_flutter/test/src/mocks.dart @@ -2,7 +2,11 @@ import 'package:flutter/material.dart'; import 'package:mocktail/mocktail.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -class MockClient extends Mock implements StreamChatClient {} +class MockClient extends Mock implements StreamChatClient { + MockClient() { + when(() => wsConnectionStatus).thenReturn(ConnectionStatus.connected); + } +} class MockClientState extends Mock implements ClientState {} @@ -17,7 +21,12 @@ class MockChannel extends Mock implements Channel { } } -class MockChannelState extends Mock implements ChannelClientState {} +class MockChannelState extends Mock implements ChannelClientState { + MockChannelState() { + when(() => typingEvents).thenReturn([]); + when(() => typingEventsStream).thenAnswer((_) => Stream.value([])); + } +} class MockNavigatorObserver extends Mock implements NavigatorObserver {} diff --git a/packages/stream_chat_flutter/test/src/unread_indicator_test.dart b/packages/stream_chat_flutter/test/src/unread_indicator_test.dart index 29bc0d79..96a6c678 100644 --- a/packages/stream_chat_flutter/test/src/unread_indicator_test.dart +++ b/packages/stream_chat_flutter/test/src/unread_indicator_test.dart @@ -83,7 +83,7 @@ void main() { ), )); - expect(find.byType(SizedBox), findsOneWidget); + expect(find.text('0'), findsNothing); }, ); diff --git a/packages/stream_chat_flutter_core/lib/src/stream_channel.dart b/packages/stream_chat_flutter_core/lib/src/stream_channel.dart index ad1f11ae..20304cdd 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_channel.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel.dart @@ -346,40 +346,36 @@ class StreamChannelState extends State { @override Widget build(BuildContext context) { - var child = widget.child; - if (widget.showLoading && - (initialMessageId != null || channel.state == null)) { - child = FutureBuilder>( - future: Future.wait(_futures), - initialData: [ - channel.state != null, - if (initialMessageId != null) false, - ], - builder: (context, snapshot) { - if (snapshot.hasError) { - var message = snapshot.error.toString(); - if (snapshot.error is DioError) { - final dioError = snapshot.error as DioError?; - if (dioError?.type == DioErrorType.response) { - message = dioError!.message; - } else { - message = 'Check your connection and retry'; - } + Widget child = FutureBuilder>( + future: Future.wait(_futures), + initialData: [ + channel.state != null, + if (initialMessageId != null) false, + ], + builder: (context, snapshot) { + if (snapshot.hasError) { + var message = snapshot.error.toString(); + if (snapshot.error is DioError) { + final dioError = snapshot.error as DioError?; + if (dioError?.type == DioErrorType.response) { + message = dioError!.message; + } else { + message = 'Check your connection and retry'; } - return Center(child: Text(message)); } - final initialized = snapshot.data![0]; - final dataLoaded = initialMessageId == null || snapshot.data![1]; - if (!initialized || !dataLoaded) { - return const Center( - child: CircularProgressIndicator(), - ); - } - return widget.child; - }, - ); - } - + 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 const Center( + child: CircularProgressIndicator(), + ); + } + return widget.child; + }, + ); if (initialMessageId != null) { child = Material(child: child); } diff --git a/packages/stream_chat_flutter_core/test/message_list_core_test.dart b/packages/stream_chat_flutter_core/test/message_list_core_test.dart index e53cd0a8..4e34f8f2 100644 --- a/packages/stream_chat_flutter_core/test/message_list_core_test.dart +++ b/packages/stream_chat_flutter_core/test/message_list_core_test.dart @@ -100,6 +100,7 @@ void main() { when(() => mockChannel.state.isUpToDate).thenReturn(true); when(() => mockChannel.state.messagesStream) .thenAnswer((_) => Stream.value([])); + when(() => mockChannel.state.messages).thenReturn([]); await tester.pumpWidget( StreamChannel( @@ -133,6 +134,7 @@ void main() { when(() => mockChannel.state.isUpToDate).thenReturn(true); when(() => mockChannel.state.messagesStream) .thenAnswer((_) => Stream.value([])); + when(() => mockChannel.state.messages).thenReturn([]); when(() => mockChannel.initialized).thenAnswer((_) => Future.value(true)); await tester.pumpWidget( @@ -174,6 +176,7 @@ void main() { when(() => mockChannel.state.messages).thenReturn(messages); when(() => mockChannel.state.messagesStream) .thenAnswer((_) => Stream.value(messages)); + when(() => mockChannel.state.messages).thenReturn(messages); when(() => mockChannel.initialized).thenAnswer((_) => Future.value(true)); await tester.pumpWidget( @@ -220,6 +223,7 @@ void main() { const error = 'Error! Error! Error!'; when(() => mockChannel.state.messagesStream) .thenAnswer((_) => Stream.error(error)); + when(() => mockChannel.state.messages).thenReturn([]); await tester.pumpWidget( Directionality( @@ -259,6 +263,7 @@ void main() { const messages = []; when(() => mockChannel.state.messagesStream) .thenAnswer((_) => Stream.value(messages)); + when(() => mockChannel.state.messages).thenReturn(messages); await tester.pumpWidget( Directionality( @@ -305,6 +310,7 @@ void main() { const messages = []; when(() => mockChannel.state.messagesStream) .thenAnswer((_) => Stream.value(messages)); + when(() => mockChannel.state.messages).thenReturn(messages); await tester.pumpWidget( Directionality( @@ -349,6 +355,7 @@ void main() { final messages = _generateMessages(); when(() => mockChannel.state.messagesStream) .thenAnswer((_) => Stream.value(messages)); + when(() => mockChannel.state.messages).thenReturn(messages); await tester.pumpWidget( Directionality( diff --git a/packages/stream_chat_flutter_core/test/mocks.dart b/packages/stream_chat_flutter_core/test/mocks.dart index 6ca24363..73227e7c 100644 --- a/packages/stream_chat_flutter_core/test/mocks.dart +++ b/packages/stream_chat_flutter_core/test/mocks.dart @@ -4,6 +4,10 @@ import 'package:stream_chat/stream_chat.dart'; class MockLogger extends Mock implements Logger {} class MockClient extends Mock implements StreamChatClient { + MockClient() { + when(() => wsConnectionStatus).thenReturn(ConnectionStatus.connected); + } + @override final Logger logger = MockLogger(); diff --git a/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart b/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart index 936e03f0..f8423e5f 100644 --- a/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart +++ b/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart @@ -235,6 +235,14 @@ void main() { final mockClient = MockClient(); const streamChatCoreKey = Key('streamChatCore'); const childKey = Key('child'); + + final event = Event(); + when(() => mockClient.on()).thenAnswer((_) => Stream.value(event)); + when(() => mockClient.connect()).thenAnswer((_) async => event); + when(() => mockClient.disconnect()).thenAnswer((_) async => null); + when(() => mockClient.wsConnectionStatus) + .thenReturn(ConnectionStatus.disconnected); + final streamChatCore = StreamChatCore( key: streamChatCoreKey, client: mockClient, @@ -247,13 +255,6 @@ void main() { 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.disconnect).thenAnswer((_) async => null); - when(() => mockClient.wsConnectionStatus) - .thenReturn(ConnectionStatus.disconnected); - final streamChatCoreState = tester.state( find.byKey(streamChatCoreKey), ); @@ -323,6 +324,14 @@ void main() { const childKey = Key('child'); final _connectivityController = BehaviorSubject.seeded(ConnectivityResult.none); + + final event = Event(); + when(() => mockClient.on()).thenAnswer((_) => Stream.value(event)); + when(() => mockClient.connect()).thenAnswer((_) async => event); + when(() => mockClient.disconnect()).thenAnswer((_) async => null); + when(() => mockClient.wsConnectionStatus) + .thenReturn(ConnectionStatus.disconnected); + final streamChatCore = StreamChatCore( key: streamChatCoreKey, client: mockClient, @@ -335,13 +344,6 @@ void main() { 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.disconnect).thenAnswer((_) async => null); - when(() => mockClient.wsConnectionStatus) - .thenReturn(ConnectionStatus.disconnected); - _connectivityController.add(ConnectivityResult.mobile); await Future.delayed(const Duration(seconds: 1)); @@ -397,6 +399,14 @@ void main() { const childKey = Key('child'); final _connectivityController = BehaviorSubject.seeded(ConnectivityResult.none); + + final event = Event(); + when(() => mockClient.on()).thenAnswer((_) => Stream.value(event)); + when(() => mockClient.connect()).thenAnswer((_) async => event); + when(() => mockClient.disconnect()).thenAnswer((_) async => null); + when(() => mockClient.wsConnectionStatus) + .thenReturn(ConnectionStatus.disconnected); + final streamChatCore = StreamChatCore( key: streamChatCoreKey, client: mockClient, @@ -409,13 +419,6 @@ void main() { 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.disconnect).thenAnswer((_) async => null); - when(() => mockClient.wsConnectionStatus) - .thenReturn(ConnectionStatus.disconnected); - final streamChatCoreState = tester.state( find.byKey(streamChatCoreKey), ); From b1201003f5ccbb4e5202b2734a1deed6412f65e3 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 11 Jun 2021 10:31:57 +0200 Subject: [PATCH 026/150] fix review --- .../lib/src/channel_image.dart | 12 ++--- .../lib/src/channel_info.dart | 4 +- .../lib/src/channel_list_view.dart | 2 +- .../lib/src/channel_name.dart | 4 +- .../lib/src/channel_preview.dart | 22 ++++---- .../lib/src/connection_status_builder.dart | 50 +++++++------------ .../lib/src/info_tile.dart | 3 -- .../lib/src/message_list_view.dart | 8 +-- .../lib/src/typing_indicator.dart | 6 +-- .../lib/src/unread_indicator.dart | 6 +-- .../lib/src/message_list_core.dart | 4 +- 11 files changed, 51 insertions(+), 70 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/channel_image.dart b/packages/stream_chat_flutter/lib/src/channel_image.dart index dd9bb314..d3e028ee 100644 --- a/packages/stream_chat_flutter/lib/src/channel_image.dart +++ b/packages/stream_chat_flutter/lib/src/channel_image.dart @@ -86,11 +86,11 @@ class ChannelImage extends StatelessWidget { return BetterStreamBuilder>( stream: channel.extraDataStream, initialData: channel.extraData, - builder: (context, snapshot) { + builder: (context, data) { String? image; final chatThemeData = StreamChatTheme.of(context); - if (snapshot.containsKey('image') == true) { - image = snapshot['image']; + if (data.containsKey('image') == true) { + image = data['image']; } else if (channel.state?.members.length == 2) { final otherMember = channel.state?.members .firstWhere((member) => member.user?.id != streamChat.user?.id); @@ -100,11 +100,11 @@ class ChannelImage extends StatelessWidget { users[otherMember?.userId] ?? otherMember!.user!) .distinct(), initialData: otherMember!.user, - builder: (context, snapshot) => UserAvatar( + builder: (context, user) => UserAvatar( borderRadius: borderRadius ?? chatThemeData .channelPreviewTheme.avatarTheme?.borderRadius, - user: snapshot ?? otherMember.user!, + user: user ?? otherMember.user!, constraints: constraints ?? chatThemeData .channelPreviewTheme.avatarTheme?.constraints, @@ -155,7 +155,7 @@ class ChannelImage extends StatelessWidget { imageUrl: image, errorWidget: (_, __, ___) => Center( child: Text( - snapshot.containsKey('name') ? snapshot['name'][0] : '', + data.containsKey('name') ? data['name'][0] : '', style: TextStyle( color: chatThemeData.colorTheme.white, fontWeight: FontWeight.bold, diff --git a/packages/stream_chat_flutter/lib/src/channel_info.dart b/packages/stream_chat_flutter/lib/src/channel_info.dart index 91e7aceb..32dbeb67 100644 --- a/packages/stream_chat_flutter/lib/src/channel_info.dart +++ b/packages/stream_chat_flutter/lib/src/channel_info.dart @@ -28,11 +28,11 @@ class ChannelInfo extends StatelessWidget { return BetterStreamBuilder>( stream: channel.state!.membersStream, initialData: channel.state!.members, - builder: (context, snapshot) => ConnectionStatusBuilder( + builder: (context, data) => ConnectionStatusBuilder( statusBuilder: (context, status) { switch (status) { case ConnectionStatus.connected: - return _buildConnectedTitleState(context, snapshot); + return _buildConnectedTitleState(context, data); case ConnectionStatus.connecting: return _buildConnectingTitleState(context); case ConnectionStatus.disconnected: diff --git a/packages/stream_chat_flutter/lib/src/channel_list_view.dart b/packages/stream_chat_flutter/lib/src/channel_list_view.dart index 12b73f72..d9f25710 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -648,7 +648,7 @@ class _ChannelListViewState extends State { ), ), ), - builder: (context, snapshot) => snapshot + builder: (context, data) => data ? const Center( child: Padding( padding: EdgeInsets.all(16), diff --git a/packages/stream_chat_flutter/lib/src/channel_name.dart b/packages/stream_chat_flutter/lib/src/channel_name.dart index 4d560bed..4819ecc7 100644 --- a/packages/stream_chat_flutter/lib/src/channel_name.dart +++ b/packages/stream_chat_flutter/lib/src/channel_name.dart @@ -29,8 +29,8 @@ class ChannelName extends StatelessWidget { return BetterStreamBuilder>( stream: channel.extraDataStream, initialData: channel.extraData, - builder: (context, snapshot) => _buildName( - snapshot, + builder: (context, data) => _buildName( + data, channel.state?.members, client, ), diff --git a/packages/stream_chat_flutter/lib/src/channel_preview.dart b/packages/stream_chat_flutter/lib/src/channel_preview.dart index bb41f652..d9170eb8 100644 --- a/packages/stream_chat_flutter/lib/src/channel_preview.dart +++ b/packages/stream_chat_flutter/lib/src/channel_preview.dart @@ -73,8 +73,8 @@ class ChannelPreview extends StatelessWidget { return BetterStreamBuilder( stream: channel.isMutedStream, initialData: channel.isMuted, - builder: (context, snapshot) => AnimatedOpacity( - opacity: snapshot ? 0.5 : 1, + builder: (context, data) => AnimatedOpacity( + opacity: data ? 0.5 : 1, duration: const Duration(milliseconds: 300), child: ListTile( visualDensity: VisualDensity.compact, @@ -108,9 +108,9 @@ class ChannelPreview extends StatelessWidget { stream: channel.state?.membersStream, initialData: channel.state?.members, comparator: const ListEquality().equals, - builder: (context, snapshot) { - if (snapshot?.isEmpty == true || - snapshot?.any((Member e) => + builder: (context, members) { + if (members?.isEmpty == true || + members?.any((Member e) => e.user!.id == channel.client.state.user?.id) != true) { @@ -165,11 +165,11 @@ class ChannelPreview extends StatelessWidget { Widget _buildDate(BuildContext context) => BetterStreamBuilder( stream: channel.lastMessageAtStream, initialData: channel.lastMessageAt, - builder: (context, snapshot) { - if (snapshot == null) { + builder: (context, data) { + if (data == null) { return const Offstage(); } - final lastMessageAt = snapshot.toLocal(); + final lastMessageAt = data.toLocal(); String stringDate; final now = DateTime.now(); @@ -225,9 +225,9 @@ class ChannelPreview extends StatelessWidget { BetterStreamBuilder?>( stream: channel.state!.messagesStream, initialData: channel.state!.messages, - builder: (context, snapshot) { - final lastMessage = snapshot - ?.lastWhereOrNull((m) => m.shadowed != true && !m.isDeleted); + builder: (context, data) { + final lastMessage = + data?.lastWhereOrNull((m) => m.shadowed != true && !m.isDeleted); if (lastMessage == null) { return const SizedBox(); } diff --git a/packages/stream_chat_flutter/lib/src/connection_status_builder.dart b/packages/stream_chat_flutter/lib/src/connection_status_builder.dart index 98daa580..fbe7ffae 100644 --- a/packages/stream_chat_flutter/lib/src/connection_status_builder.dart +++ b/packages/stream_chat_flutter/lib/src/connection_status_builder.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; @@ -8,20 +7,16 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// /// The widget will use the closest [StreamChatClient.wsConnectionStatusStream] /// in case no stream is provided. -class ConnectionStatusBuilder extends StatefulWidget { +class ConnectionStatusBuilder extends StatelessWidget { /// Creates a new ConnectionStatusBuilder const ConnectionStatusBuilder({ Key? key, required this.statusBuilder, - this.initialStatus, this.connectionStatusStream, this.errorBuilder, this.loadingBuilder, }) : super(key: key); - /// The connection status that will be used to create the initial snapshot. - final ConnectionStatus? initialStatus; - /// The asynchronous computation to which this builder is currently connected. final Stream? connectionStatusStream; @@ -36,32 +31,21 @@ class ConnectionStatusBuilder extends StatefulWidget { statusBuilder; @override - _ConnectionStatusBuilderState createState() => - _ConnectionStatusBuilderState(); -} - -class _ConnectionStatusBuilderState extends State { - late StreamChatClient client; - late Stream stream; - - @override - Widget build(BuildContext context) => BetterStreamBuilder( - initialData: widget.initialStatus ?? client.wsConnectionStatus, - stream: stream, - loadingBuilder: widget.loadingBuilder, - errorBuilder: (context, error) { - if (widget.errorBuilder != null) { - return widget.errorBuilder!(context, error); - } - return const Offstage(); - }, - builder: widget.statusBuilder, - ); - - @override - void didChangeDependencies() { - client = StreamChat.of(context).client; - stream = widget.connectionStatusStream ?? client.wsConnectionStatusStream; - super.didChangeDependencies(); + Widget build(BuildContext context) { + final stream = connectionStatusStream ?? + StreamChat.of(context).client.wsConnectionStatusStream; + final client = StreamChat.of(context).client; + return BetterStreamBuilder( + initialData: client.wsConnectionStatus, + stream: stream, + loadingBuilder: loadingBuilder, + errorBuilder: (context, error) { + if (errorBuilder != null) { + return errorBuilder!(context, error); + } + return const Offstage(); + }, + builder: statusBuilder, + ); } } diff --git a/packages/stream_chat_flutter/lib/src/info_tile.dart b/packages/stream_chat_flutter/lib/src/info_tile.dart index b420cadc..1e935942 100644 --- a/packages/stream_chat_flutter/lib/src/info_tile.dart +++ b/packages/stream_chat_flutter/lib/src/info_tile.dart @@ -40,9 +40,6 @@ class InfoTile extends StatelessWidget { @override Widget build(BuildContext context) { final chatThemeData = StreamChatTheme.of(context); - if (!showMessage) { - return child; - } return PortalEntry( visible: showMessage, portalAnchor: tileAnchor ?? Alignment.topCenter, diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 195ae1f2..82b64c27 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -1170,9 +1170,9 @@ class _MessageListViewState extends State { (messages) => messages!.firstWhere((m) => m.id == message.id)), initialData: message, - builder: (_, snapshot) => StreamChannel( + builder: (_, data) => StreamChannel( channel: streamChannel!.channel, - child: widget.threadBuilder!(context, snapshot), + child: widget.threadBuilder!(context, data), ), ), ), @@ -1220,8 +1220,8 @@ class _LoadingIndicator extends StatelessWidget { child: Text('Error loading messages'), ), ), - builder: (context, snapshot) { - if (!snapshot) { + builder: (context, data) { + if (!data) { if (!isThreadConversation && direction == QueryDirection.top) { return const SizedBox( height: 52, diff --git a/packages/stream_chat_flutter/lib/src/typing_indicator.dart b/packages/stream_chat_flutter/lib/src/typing_indicator.dart index 5609c18b..cdd2ce68 100644 --- a/packages/stream_chat_flutter/lib/src/typing_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/typing_indicator.dart @@ -49,9 +49,9 @@ class _TypingIndicatorState extends State { return BetterStreamBuilder>( initialData: channelState.typingEvents, stream: channelState.typingEventsStream, - builder: (context, snapshot) => AnimatedSwitcher( + builder: (context, data) => AnimatedSwitcher( duration: const Duration(milliseconds: 300), - child: snapshot.isNotEmpty == true + child: data.isNotEmpty == true ? Padding( key: const Key('main'), padding: widget.padding, @@ -68,7 +68,7 @@ class _TypingIndicatorState extends State { ), Text( // ignore: lines_longer_than_80_chars - ' ${snapshot[0].name}${snapshot.length == 1 ? '' : ' and ${snapshot.length - 1} more'} ${snapshot.length == 1 ? 'is' : 'are'} typing', + ' ${data[0].name}${data.length == 1 ? '' : ' and ${data.length - 1} more'} ${data.length == 1 ? 'is' : 'are'} typing', maxLines: 1, style: widget.style, ), diff --git a/packages/stream_chat_flutter/lib/src/unread_indicator.dart b/packages/stream_chat_flutter/lib/src/unread_indicator.dart index 8b235187..381bad39 100644 --- a/packages/stream_chat_flutter/lib/src/unread_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/unread_indicator.dart @@ -24,8 +24,8 @@ class UnreadIndicator extends StatelessWidget { initialData: cid != null ? client.state.channels[cid]?.state?.unreadCount : client.state.totalUnreadCount, - builder: (context, snapshot) { - if (snapshot == null || snapshot == 0) { + builder: (context, data) { + if (data == null || data == 0) { return const Offstage(); } return Material( @@ -42,7 +42,7 @@ class UnreadIndicator extends StatelessWidget { ), child: Center( child: Text( - '${snapshot > 99 ? '99+' : snapshot}', + '${data > 99 ? '99+' : data}', style: const TextStyle( fontSize: 11, color: Colors.white, diff --git a/packages/stream_chat_flutter_core/lib/src/message_list_core.dart b/packages/stream_chat_flutter_core/lib/src/message_list_core.dart index 0da733bf..2a257fe6 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_list_core.dart @@ -151,8 +151,8 @@ class MessageListCoreState extends State { ), errorBuilder: widget.errorWidgetBuilder, loadingBuilder: widget.loadingBuilder, - builder: (context, snapshot) { - final messageList = snapshot?.reversed.toList(growable: false) ?? []; + builder: (context, data) { + final messageList = data?.reversed.toList(growable: false) ?? []; if (messageList.isEmpty && !_isThreadConversation) { if (_upToDate) { return widget.emptyBuilder(context); From 712e42c240a3dbf320174538d2548bdcc360d966 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 11 Jun 2021 10:43:13 +0200 Subject: [PATCH 027/150] revert typinh indicator to stateless widget --- .../lib/src/typing_indicator.dart | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/typing_indicator.dart b/packages/stream_chat_flutter/lib/src/typing_indicator.dart index cdd2ce68..100cdb53 100644 --- a/packages/stream_chat_flutter/lib/src/typing_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/typing_indicator.dart @@ -3,7 +3,7 @@ import 'package:lottie/lottie.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Widget to show the current list of typing users -class TypingIndicator extends StatefulWidget { +class TypingIndicator extends StatelessWidget { /// Instantiate a new TypingIndicator const TypingIndicator({ Key? key, @@ -29,21 +29,16 @@ class TypingIndicator extends StatefulWidget { /// Alignment of the typing indicator final Alignment alignment; - @override - _TypingIndicatorState createState() => _TypingIndicatorState(); -} - -class _TypingIndicatorState extends State { @override Widget build(BuildContext context) { final channelState = - widget.channel?.state ?? StreamChannel.of(context).channel.state!; + channel?.state ?? StreamChannel.of(context).channel.state!; final altWidget = Align( key: const Key('alternative'), - alignment: widget.alignment, + alignment: alignment, child: Container( - child: widget.alternativeWidget ?? const Offstage(), + child: alternativeWidget ?? const Offstage(), ), ); return BetterStreamBuilder>( @@ -54,10 +49,10 @@ class _TypingIndicatorState extends State { child: data.isNotEmpty == true ? Padding( key: const Key('main'), - padding: widget.padding, + padding: padding, child: Align( key: const Key('typings'), - alignment: widget.alignment, + alignment: alignment, child: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -70,7 +65,7 @@ class _TypingIndicatorState extends State { // ignore: lines_longer_than_80_chars ' ${data[0].name}${data.length == 1 ? '' : ' and ${data.length - 1} more'} ${data.length == 1 ? 'is' : 'are'} typing', maxLines: 1, - style: widget.style, + style: style, ), ], ), From f521e13aca461ca6aa5d17aa7cc3dbad758103c5 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 11 Jun 2021 10:51:25 +0200 Subject: [PATCH 028/150] fix better stream builder --- .../lib/src/better_stream_builder.dart | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/stream_chat_flutter_core/lib/src/better_stream_builder.dart b/packages/stream_chat_flutter_core/lib/src/better_stream_builder.dart index a51c0762..ddf342dc 100644 --- a/packages/stream_chat_flutter_core/lib/src/better_stream_builder.dart +++ b/packages/stream_chat_flutter_core/lib/src/better_stream_builder.dart @@ -57,18 +57,14 @@ class _BetterStreamBuilderState extends State> { return widget.builder(context, _lastEvent ?? widget.initialData); } - bool _firstTime = true; @override - void didChangeDependencies() { - if (_firstTime) { - _lastEvent = widget.initialData; - _subscription = widget.stream?.listen( - _onEvent, - onError: _onError, - ); - _firstTime = false; - } - super.didChangeDependencies(); + void initState() { + _lastEvent = widget.initialData; + _subscription = widget.stream?.listen( + _onEvent, + onError: _onError, + ); + super.initState(); } @override From a5776fcf025d6fb6fa726cc22fc8895f8d908e7b Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 11 Jun 2021 13:07:03 +0200 Subject: [PATCH 029/150] fix pin message flow --- packages/stream_chat_flutter/lib/src/message_actions_modal.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart index bdee86a0..13900c21 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -366,13 +366,13 @@ class _MessageActionsModalState extends State { void _togglePin() async { final channel = StreamChannel.of(context).channel; + Navigator.pop(context); try { if (!widget.message.pinned) { await channel.pinMessage(widget.message); } else { await channel.unpinMessage(widget.message); } - Navigator.pop(context); } catch (e) { _showErrorAlert(); } From eb20b31c825ae430f1cdf367f40fe40e635a5cfc Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 11 Jun 2021 13:12:52 +0200 Subject: [PATCH 030/150] feat: add flag for floating date divider --- .../lib/src/message_list_view.dart | 84 ++++++++++--------- 1 file changed, 45 insertions(+), 39 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 82b64c27..d4885c86 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -161,6 +161,7 @@ class MessageListView extends StatefulWidget { this.pinPermissions = const [], this.textBuilder, this.usernameBuilder, + this.showFloatingDateDivider = true, }) : super(key: key); /// Function used to build a custom message widget @@ -227,6 +228,9 @@ class MessageListView extends StatefulWidget { /// Flag for showing tile on header final bool showConnectionStateTile; + /// Flag for showing the floating date divider + final bool showFloatingDateDivider; + /// Function called when messages are fetched final Widget Function(BuildContext, List)? messageListBuilder; @@ -567,49 +571,51 @@ class _MessageListViewState extends State { }, ), if (widget.showScrollToBottom) _buildScrollToBottom(), - Positioned( - top: 20, - child: BetterStreamBuilder>( - initialData: _itemPositionListener.itemPositions.value, - stream: _itemPositionStream, - comparator: (a, b) { - if (a == null) { - return false; - } - final aTop = _getTopElement(a)?.index; - final bTop = _getTopElement(b)?.index; - return aTop == bTop; - }, - builder: (context, values) { - final items = _itemPositionListener.itemPositions.value; - if (items.isEmpty || messages.isEmpty) { - return const SizedBox(); - } - - var index = _getTopElement(values)?.index; - - if (index == null || index > messages.length) { - return const SizedBox(); - } - - if (index == messages.length) { - index = max(index - 1, 0); - } - - return widget.dateDividerBuilder != null - ? widget.dateDividerBuilder!( - messages[index].createdAt.toLocal(), - ) - : DateDivider( - dateTime: messages[index].createdAt.toLocal(), - ); - }, - ), - ), + if (widget.showFloatingDateDivider) _buildFloatingDateDivider(), ], ); } + Positioned _buildFloatingDateDivider() => Positioned( + top: 20, + child: BetterStreamBuilder>( + initialData: _itemPositionListener.itemPositions.value, + stream: _itemPositionStream, + comparator: (a, b) { + if (a == null) { + return false; + } + final aTop = _getTopElement(a)?.index; + final bTop = _getTopElement(b)?.index; + return aTop == bTop; + }, + builder: (context, values) { + final items = _itemPositionListener.itemPositions.value; + if (items.isEmpty || messages.isEmpty) { + return const SizedBox(); + } + + var index = _getTopElement(values)?.index; + + if (index == null || index > messages.length) { + return const SizedBox(); + } + + if (index == messages.length) { + index = max(index - 1, 0); + } + + return widget.dateDividerBuilder != null + ? widget.dateDividerBuilder!( + messages[index].createdAt.toLocal(), + ) + : DateDivider( + dateTime: messages[index].createdAt.toLocal(), + ); + }, + ), + ); + Future _paginateData( StreamChannelState? channel, QueryDirection direction) => _messageListController.paginateData!(direction: direction); From 850145f79166658dbced451f5ba2e284bec7ee5b Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 11 Jun 2021 16:37:30 +0200 Subject: [PATCH 031/150] add thread separator builder --- .../lib/src/message_list_view.dart | 43 ++++++++++++------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index d4885c86..aae9d193 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -162,6 +162,7 @@ class MessageListView extends StatefulWidget { this.textBuilder, this.usernameBuilder, this.showFloatingDateDivider = true, + this.threadSeparatorBuilder, }) : super(key: key); /// Function used to build a custom message widget @@ -277,6 +278,9 @@ class MessageListView extends StatefulWidget { /// A List of user types that have permission to pin messages final List pinPermissions; + /// Builder used to build the thread separator in case it's a thread view + final WidgetBuilder? threadSeparatorBuilder; + @override _MessageListViewState createState() => _MessageListViewState(); } @@ -450,22 +454,7 @@ class _MessageListViewState extends State { if (i == messages.length) return const Offstage(); if (i == 0) return const SizedBox(height: 30); if (i == messages.length + 1) { - final replyCount = widget.parentMessage!.replyCount; - return Container( - decoration: BoxDecoration( - gradient: _streamTheme.colorTheme.bgGradient, - ), - child: Padding( - padding: const EdgeInsets.all(8), - child: Text( - // ignore: lines_longer_than_80_chars - '$replyCount ${replyCount == 1 ? 'Reply' : 'Replies'}', - textAlign: TextAlign.center, - style: _streamTheme - .channelTheme.channelHeaderTheme.subtitle, - ), - ), - ); + return _buildThreadSeparator(); } final message = messages[i]; @@ -576,6 +565,28 @@ class _MessageListViewState extends State { ); } + Widget _buildThreadSeparator() { + if (widget.threadSeparatorBuilder != null) { + return widget.threadSeparatorBuilder!.call(context); + } + + final replyCount = widget.parentMessage!.replyCount; + return DecoratedBox( + decoration: BoxDecoration( + gradient: _streamTheme.colorTheme.bgGradient, + ), + child: Padding( + padding: const EdgeInsets.all(8), + child: Text( + // ignore: lines_longer_than_80_chars + '$replyCount ${replyCount == 1 ? 'Reply' : 'Replies'}', + textAlign: TextAlign.center, + style: _streamTheme.channelTheme.channelHeaderTheme.subtitle, + ), + ), + ); + } + Positioned _buildFloatingDateDivider() => Positioned( top: 20, child: BetterStreamBuilder>( From cf419f3a37ebe4becb5c896cf3d8984fc67c493b Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 14 Jun 2021 10:35:19 +0200 Subject: [PATCH 032/150] update changelogs and pubspecs --- packages/stream_chat/CHANGELOG.md | 6 ++++++ packages/stream_chat/pubspec.yaml | 2 +- packages/stream_chat_flutter/CHANGELOG.md | 7 +++++++ packages/stream_chat_flutter/pubspec.yaml | 4 ++-- packages/stream_chat_flutter_core/CHANGELOG.md | 7 +++++++ packages/stream_chat_flutter_core/pubspec.yaml | 4 ++-- packages/stream_chat_persistence/CHANGELOG.md | 5 +++++ packages/stream_chat_persistence/pubspec.yaml | 4 ++-- 8 files changed, 32 insertions(+), 7 deletions(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 75fecc34..9a3910bb 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,3 +1,9 @@ +## 2.0.0-nullsafety.5 + +- Minor fixes +- Performance improvements +- Fixed `skip_push` in `client.sendMessage` + ## 2.0.0-nullsafety.2 - Added new `Filter.raw` constructor diff --git a/packages/stream_chat/pubspec.yaml b/packages/stream_chat/pubspec.yaml index d473beea..5f72d253 100644 --- a/packages/stream_chat/pubspec.yaml +++ b/packages/stream_chat/pubspec.yaml @@ -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: 2.0.0-nullsafety.2 +version: 2.0.0-nullsafety.5 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index e0b60fad..509ac26a 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,3 +1,10 @@ +## 2.0.0-nullsafety.5 + +- Minor fixes and improvements +- Updated `stream_chat_core` dependency +- Performance improvements +- Added pinMessage ui support +- Added `MessageListView.threadSeparatorBuilder` property ## 2.0.0-nullsafety.4 - Minor fixes and improvements diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index 2aa5ab4a..b8241bba 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -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: 2.0.0-nullsafety.4 +version: 2.0.0-nullsafety.5 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -36,7 +36,7 @@ dependencies: scrollable_positioned_list: ^0.2.0-nullsafety.0 share_plus: ^2.0.3 shimmer: ^2.0.0 - stream_chat_flutter_core: ^2.0.0-nullsafety.3 + stream_chat_flutter_core: ^2.0.0-nullsafety.5 substring_highlight: ^1.0.26 synchronized: ^3.0.0 url_launcher: ^6.0.3 diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index 1930e36a..26cfa6c3 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,3 +1,10 @@ +## 2.0.0-nullsafety.5 + +* Update llc dependency +* Minor fixes and improvements +* Performance improvements +* Monitor connection using `connectivity_plus` package + ## 2.0.0-nullsafety.3 * Update llc dependency diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml index 4292772b..f9768bdd 100644 --- a/packages/stream_chat_flutter_core/pubspec.yaml +++ b/packages/stream_chat_flutter_core/pubspec.yaml @@ -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: 2.0.0-nullsafety.3 +version: 2.0.0-nullsafety.5 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -16,7 +16,7 @@ dependencies: sdk: flutter meta: ^1.3.0 rxdart: ^0.27.0 - stream_chat: ^2.0.0-nullsafety.2 + stream_chat: ^2.0.0-nullsafety.5 dev_dependencies: fake_async: ^1.2.0 diff --git a/packages/stream_chat_persistence/CHANGELOG.md b/packages/stream_chat_persistence/CHANGELOG.md index c35459ca..aa7c7513 100644 --- a/packages/stream_chat_persistence/CHANGELOG.md +++ b/packages/stream_chat_persistence/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.0.0-nullsafety.5 + +* Update llc dependency +* Minor fixes and improvements + ## 2.0.0-nullsafety.2 * Update llc dependency diff --git a/packages/stream_chat_persistence/pubspec.yaml b/packages/stream_chat_persistence/pubspec.yaml index 661f55bb..749804de 100644 --- a/packages/stream_chat_persistence/pubspec.yaml +++ b/packages/stream_chat_persistence/pubspec.yaml @@ -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: 2.0.0-nullsafety.2 +version: 2.0.0-nullsafety.5 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -18,7 +18,7 @@ dependencies: path: ^1.8.0 path_provider: ^2.0.1 sqlite3_flutter_libs: ^0.4.2 - stream_chat: ^2.0.0-nullsafety.2 + stream_chat: ^2.0.0-nullsafety.5 dev_dependencies: build_runner: ^2.0.1 From 2fda85eb4aeb3fef98fdef11ee9032ce974bf3ad Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 14 Jun 2021 10:44:01 +0200 Subject: [PATCH 033/150] upgrade deps --- packages/stream_chat/pubspec.yaml | 2 +- packages/stream_chat_flutter/pubspec.yaml | 6 +++--- packages/stream_chat_persistence/pubspec.yaml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/stream_chat/pubspec.yaml b/packages/stream_chat/pubspec.yaml index 5f72d253..b13838aa 100644 --- a/packages/stream_chat/pubspec.yaml +++ b/packages/stream_chat/pubspec.yaml @@ -28,4 +28,4 @@ dev_dependencies: freezed: ^0.14.1+3 json_serializable: ^4.1.0 mocktail: ^0.1.1 - test: ^1.16.8 + test: ^1.17.7 diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index b8241bba..61aad8df 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -11,7 +11,7 @@ environment: dependencies: cached_network_image: ^3.0.0 characters: ^1.1.0 - chewie: ^1.0.0 + chewie: ^1.2.0 collection: ^1.15.0 dio: ^4.0.0 ezanimation: ^0.5.0 @@ -25,7 +25,7 @@ dependencies: flutter_svg: ^0.22.0 http_parser: ^4.0.0 image_gallery_saver: ^1.6.9 - image_picker: ^0.7.4 + image_picker: ^0.8.0 jiffy: ^4.1.0 lottie: ^1.0.1 meta: ^1.3.0 @@ -41,7 +41,7 @@ dependencies: synchronized: ^3.0.0 url_launcher: ^6.0.3 video_compress: ^3.0.0 - video_player: ^2.1.1 + video_player: ^2.1.0 video_thumbnail: ^0.3.3 visibility_detector: ^0.2.0 diff --git a/packages/stream_chat_persistence/pubspec.yaml b/packages/stream_chat_persistence/pubspec.yaml index 749804de..9d141198 100644 --- a/packages/stream_chat_persistence/pubspec.yaml +++ b/packages/stream_chat_persistence/pubspec.yaml @@ -17,7 +17,7 @@ dependencies: mutex: ^3.0.0 path: ^1.8.0 path_provider: ^2.0.1 - sqlite3_flutter_libs: ^0.4.2 + sqlite3_flutter_libs: ^0.5.0 stream_chat: ^2.0.0-nullsafety.5 dev_dependencies: @@ -25,4 +25,4 @@ dev_dependencies: mocktail: ^0.1.1 moor_generator: ^4.2.1 pedantic: ^1.11.0 - test: ^1.16.8 + test: ^1.17.7 From 364c43e9a20dd2cf0137819db8c6bbfc1ea4702c Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 14 Jun 2021 10:59:09 +0200 Subject: [PATCH 034/150] update version --- packages/stream_chat/lib/version.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat/lib/version.dart b/packages/stream_chat/lib/version.dart index 6c68818a..da7d8aa3 100644 --- a/packages/stream_chat/lib/version.dart +++ b/packages/stream_chat/lib/version.dart @@ -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 = '2.0.0-nullsafety.2'; +const PACKAGE_VERSION = '2.0.0-nullsafety.5'; From dd537b6734095b37e63ddd8baea0466ed66a4b60 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 14 Jun 2021 11:49:41 +0200 Subject: [PATCH 035/150] update changelog --- packages/stream_chat/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 9a3910bb..b6c5cecf 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -3,6 +3,7 @@ - Minor fixes - Performance improvements - Fixed `skip_push` in `client.sendMessage` +- Added partial message update method ## 2.0.0-nullsafety.2 From 2a58ca535dd7d8d04e26b4fd8edb5cbdde82f9bf Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 14 Jun 2021 16:56:36 +0530 Subject: [PATCH 036/150] Add retry_queue_test.dart Signed-off-by: Sahil Kumar --- .../lib/src/client/retry_queue.dart | 3 + .../test/src/api/retry_queue_test.dart | 74 +++++++++++++++++++ packages/stream_chat/test/src/mocks.dart | 21 ++++++ 3 files changed, 98 insertions(+) create mode 100644 packages/stream_chat/test/src/api/retry_queue_test.dart diff --git a/packages/stream_chat/lib/src/client/retry_queue.dart b/packages/stream_chat/lib/src/client/retry_queue.dart index 008270e5..4c44433f 100644 --- a/packages/stream_chat/lib/src/client/retry_queue.dart +++ b/packages/stream_chat/lib/src/client/retry_queue.dart @@ -177,6 +177,9 @@ class RetryQueue { } } + /// Whether our [_messageQueue] has messages or not + bool get hasMessages => _messageQueue.isNotEmpty; + /// Call this method to dispose this object void dispose() { _messageQueue.clear(); diff --git a/packages/stream_chat/test/src/api/retry_queue_test.dart b/packages/stream_chat/test/src/api/retry_queue_test.dart new file mode 100644 index 00000000..084b82ec --- /dev/null +++ b/packages/stream_chat/test/src/api/retry_queue_test.dart @@ -0,0 +1,74 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/client/retry_policy.dart'; +import 'package:stream_chat/src/client/retry_queue.dart'; +import 'package:stream_chat/src/core/models/event.dart'; +import 'package:stream_chat/src/core/models/message.dart'; +import 'package:stream_chat/src/event_type.dart'; +import 'package:test/scaffolding.dart'; +import 'package:test/test.dart'; + +import '../mocks.dart'; + +void main() { + late final channel = MockRetryQueueChannel(); + late final logger = MockLogger(); + late RetryQueue retryQueue; + + setUpAll(() { + final retryPolicy = RetryPolicy( + shouldRetry: (_, attempt, __) => attempt < 5, + retryTimeout: (_, attempt, __) => Duration(seconds: attempt), + ); + when(() => channel.client.retryPolicy).thenReturn(retryPolicy); + + when(() => channel.client.on(EventType.connectionRecovered)).thenAnswer( + (_) => Stream.value(Event( + type: EventType.connectionRecovered, + online: false, + )), + ); + + when(() => channel.on(any(), any(), any(), any())).thenAnswer( + (_) => Stream.value( + Event(type: EventType.any), + ), + ); + }); + + setUp(() { + retryQueue = RetryQueue(channel: channel, logger: logger); + }); + + tearDown(() { + retryQueue.dispose(); + }); + + group('`.add`', () { + test('should return if message list is empty', () { + expect(() => retryQueue.add([]), returnsNormally); + verifyNever(() => logger.info(any())); + }); + + test('should return if queue already contains the message', () { + final message = Message( + id: 'test-message-id', + text: 'Sample message test', + ); + retryQueue.add([message]); + expect(() => retryQueue.add([message]), returnsNormally); + // Called only for the first message + verify(() => logger.info('Adding 1 messages')).called(1); + }); + + test('`.add` should add failed request to the queue', () async { + final message = Message( + id: 'test-message-id', + text: 'Sample message test', + ); + retryQueue.add([message]); + expect(retryQueue.hasMessages, isTrue); + }); + }); + + // TODO: Add more tests once macbook is fixed :( +} diff --git a/packages/stream_chat/test/src/mocks.dart b/packages/stream_chat/test/src/mocks.dart index 9c20be7c..31e768e8 100644 --- a/packages/stream_chat/test/src/mocks.dart +++ b/packages/stream_chat/test/src/mocks.dart @@ -1,7 +1,9 @@ import 'package:dio/dio.dart'; import 'package:logging/logging.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/client/channel.dart'; import 'package:stream_chat/src/client/client.dart'; +import 'package:stream_chat/src/client/retry_policy.dart'; import 'package:stream_chat/src/core/api/attachment_file_uploader.dart'; import 'package:stream_chat/src/core/api/channel_api.dart'; import 'package:stream_chat/src/core/api/device_api.dart'; @@ -84,3 +86,22 @@ class MockStreamChatClientWithPersistence extends Mock } class MockChannelConfig extends Mock implements ChannelConfig {} + +class MockRetryQueueChannel extends Mock implements Channel { + final channelId = 'test-channel-id'; + final channelType = 'test-channel-type'; + + @override + String? get id => channelId; + + @override + String get type => channelType; + + @override + String? get cid => '$channelType:$channelId'; + + StreamChatClient? _client; + + @override + StreamChatClient get client => _client ??= MockStreamChatClient(); +} From b0cc64ef3e1655bca7a7cabfa17eabe9eb639145 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 14 Jun 2021 17:47:14 +0530 Subject: [PATCH 037/150] remove deprecated methods Signed-off-by: Sahil Kumar --- .../stream_chat/lib/src/client/client.dart | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 87e471e6..9abfa94c 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -229,38 +229,17 @@ class StreamChatClient { logger.info('logger setup'); } - /// Set the current user, this triggers a connection to the API. - /// Set the current user, this triggers a connection to the API. - /// It returns a [Future] that resolves when the connection is setup. - @Deprecated('Use `connectUser` instead. Will be removed in Future releases') - Future setUser(User user, String token) => connectUser(user, token); - /// Connects the current user, this triggers a connection to the API. /// It returns a [Future] that resolves when the connection is setup. Future connectUser(User user, String token) => _connectUser(user, token: Token.fromRawValue(token)); - /// Set the current user using the [tokenProvider] to fetch the token. - /// It returns a [Future] that resolves when the connection is setup. - @Deprecated( - 'Use `connectUserWithProvider` instead. Will be removed in Future releases', - ) - Future setUserWithProvider(User user, TokenProvider tokenProvider) => - connectUserWithProvider(user, tokenProvider); - /// Connects the current user using the [tokenProvider] to fetch the token. /// It returns a [Future] that resolves when the connection is setup. Future connectUserWithProvider( User user, TokenProvider tokenProvider) => _connectUser(user, provider: tokenProvider); - /// Set the current user with an anonymous id, this triggers a connection to - /// the API. It returns a [Future] that resolves when the connection is setup. - @Deprecated( - 'Use `connectAnonymousUser` instead. Will be removed in Future releases', - ) - Future setAnonymousUser() => connectAnonymousUser(); - /// Connects the current user with an anonymous id, this triggers a connection /// to the API. It returns a [Future] that resolves when the connection is /// setup. @@ -270,13 +249,6 @@ class StreamChatClient { return _connectUser(user, token: token); } - /// Set the current user as guest, this triggers a connection to the API. - /// It returns a [Future] that resolves when the connection is setup. - @Deprecated( - 'Use `connectGuestUser` instead. Will be removed in Future releases', - ) - Future setGuestUser(User user) => connectGuestUser(user); - /// Connects the current user as guest, this triggers a connection to the API. /// It returns a [Future] that resolves when the connection is setup. Future connectGuestUser(User user) async { From d47684131231a2ad57b97abe932696e652380272 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 14 Jun 2021 16:45:39 +0200 Subject: [PATCH 038/150] fix thread reply with attachment --- packages/stream_chat/lib/src/api/channel.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat/lib/src/api/channel.dart b/packages/stream_chat/lib/src/api/channel.dart index 1e7f824e..8a099aff 100644 --- a/packages/stream_chat/lib/src/api/channel.dart +++ b/packages/stream_chat/lib/src/api/channel.dart @@ -256,7 +256,10 @@ class Channel { String messageId, Iterable attachmentIds, ) { - final message = state!.messages.firstWhereOrNull( + final message = [ + ...state!.messages, + ...state!.threads.values.expand((element) => element), + ].firstWhereOrNull( (it) => it.id == messageId, ); From 7bc55a018254d32b87a609ce044be9b4c9a94a8c Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 14 Jun 2021 17:03:40 +0200 Subject: [PATCH 039/150] Update packages/stream_chat/lib/src/api/channel.dart Co-authored-by: Sahil Kumar --- packages/stream_chat/lib/src/api/channel.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat/lib/src/api/channel.dart b/packages/stream_chat/lib/src/api/channel.dart index 8a099aff..aeda98e7 100644 --- a/packages/stream_chat/lib/src/api/channel.dart +++ b/packages/stream_chat/lib/src/api/channel.dart @@ -258,7 +258,7 @@ class Channel { ) { final message = [ ...state!.messages, - ...state!.threads.values.expand((element) => element), + ...state!.threads.values.expand((messages) => messages), ].firstWhereOrNull( (it) => it.id == messageId, ); From 3a44117628a93a0f244f2ed69dd91b1a3c51e14f Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 14 Jun 2021 17:20:52 +0200 Subject: [PATCH 040/150] let users use custom reaction builders --- packages/stream_chat_flutter/CHANGELOG.md | 1 + .../lib/src/message_actions_modal.dart | 12 +++- .../lib/src/message_reactions_modal.dart | 12 +++- .../lib/src/reaction_bubble.dart | 15 +++-- .../lib/src/reaction_icon.dart | 10 +++- .../lib/src/reaction_picker.dart | 10 ++-- .../lib/src/stream_chat_theme.dart | 54 +++++++++++++++-- .../lib/src/stream_svg_icon.dart | 60 +++++++++++++++++++ 8 files changed, 148 insertions(+), 26 deletions(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 509ac26a..39247b13 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -5,6 +5,7 @@ - Performance improvements - Added pinMessage ui support - Added `MessageListView.threadSeparatorBuilder` property + ## 2.0.0-nullsafety.4 - Minor fixes and improvements diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart index 13900c21..f763585b 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -148,6 +148,10 @@ class _MessageActionsModalState extends State { final streamChatThemeData = StreamChatTheme.of(context); + final numberOfReactions = streamChatThemeData.reactionIcons.length; + final shiftFactor = + numberOfReactions < 5 ? (5 - numberOfReactions) * 0.1 : 0.0; + final child = Center( child: SingleChildScrollView( child: Padding( @@ -162,8 +166,12 @@ class _MessageActionsModalState extends State { Align( alignment: Alignment( user?.id == widget.message.user?.id - ? (divFactor >= 1.0 ? -0.2 : (1.2 - divFactor)) - : (divFactor >= 1.0 ? 0.2 : -(1.2 - divFactor)), + ? (divFactor >= 1.0 + ? -0.2 - shiftFactor + : (1.2 - divFactor)) + : (divFactor >= 1.0 + ? 0.2 + shiftFactor + : -(1.2 - divFactor)), 0), child: ReactionPicker( message: widget.message, diff --git a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart index e9369e99..842fe79d 100644 --- a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart @@ -80,6 +80,10 @@ class MessageReactionsModal extends StatelessWidget { final hasFileAttachment = message.attachments.any((it) => it.type == 'file') == true; + final numberOfReactions = StreamChatTheme.of(context).reactionIcons.length; + final shiftFactor = + numberOfReactions < 5 ? (5 - numberOfReactions) * 0.1 : 0.0; + final child = Center( child: SingleChildScrollView( child: Padding( @@ -93,8 +97,12 @@ class MessageReactionsModal extends StatelessWidget { Align( alignment: Alignment( user!.id == message.user!.id - ? (divFactor >= 1.0 ? -0.2 : (1.2 - divFactor)) - : (divFactor >= 1.0 ? 0.2 : -(1.2 - divFactor)), + ? (divFactor >= 1.0 + ? -0.2 - shiftFactor + : (1.2 - divFactor)) + : (divFactor >= 1.0 + ? 0.2 + shiftFactor + : -(1.2 - divFactor)), 0), child: ReactionPicker( message: message, diff --git a/packages/stream_chat_flutter/lib/src/reaction_bubble.dart b/packages/stream_chat_flutter/lib/src/reaction_bubble.dart index ce75445b..a3b2052f 100644 --- a/packages/stream_chat_flutter/lib/src/reaction_bubble.dart +++ b/packages/stream_chat_flutter/lib/src/reaction_bubble.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:stream_chat_flutter/src/reaction_icon.dart'; -import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Creates reaction bubble widget for displaying over messages @@ -130,13 +129,13 @@ class ReactionBubble extends StatelessWidget { horizontal: 4, ), child: reactionIcon != null - ? StreamSvgIcon( - assetName: reactionIcon.assetName, - width: 16, - height: 16, - color: (!highlightOwnReactions || reaction.user?.id == userId) - ? chatThemeData.colorTheme.accentBlue - : chatThemeData.colorTheme.black.withOpacity(.5), + ? ConstrainedBox( + constraints: BoxConstraints.tight(const Size.square(16)), + child: reactionIcon.builder( + context, + !highlightOwnReactions || reaction.user?.id == userId, + 16, + ), ) : Icon( Icons.help_outline_rounded, diff --git a/packages/stream_chat_flutter/lib/src/reaction_icon.dart b/packages/stream_chat_flutter/lib/src/reaction_icon.dart index 4466e154..e675128b 100644 --- a/packages/stream_chat_flutter/lib/src/reaction_icon.dart +++ b/packages/stream_chat_flutter/lib/src/reaction_icon.dart @@ -1,14 +1,20 @@ +import 'package:flutter/material.dart'; + /// Reaction icon data class ReactionIcon { /// Constructor for creating [ReactionIcon] ReactionIcon({ required this.type, - required this.assetName, + required this.builder, }); /// Type of reaction final String type; /// Asset to display for reaction - final String assetName; + final Widget Function( + BuildContext, + bool highlighted, + double size, + ) builder; } diff --git a/packages/stream_chat_flutter/lib/src/reaction_picker.dart b/packages/stream_chat_flutter/lib/src/reaction_picker.dart index 2e47b933..5101cbb7 100644 --- a/packages/stream_chat_flutter/lib/src/reaction_picker.dart +++ b/packages/stream_chat_flutter/lib/src/reaction_picker.dart @@ -1,7 +1,6 @@ import 'package:ezanimation/ezanimation.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/reaction_picker.png) @@ -69,11 +68,10 @@ class _ReactionPickerState extends State -1; final index = reactionIcons.indexOf(reactionIcon); - final child = StreamSvgIcon( - assetName: reactionIcon.assetName, - color: ownReactionIndex != -1 - ? chatThemeData.colorTheme.accentBlue - : Theme.of(context).iconTheme.color!.withOpacity(.5), + final child = reactionIcon.builder( + context, + ownReactionIndex != -1, + 24, ); return ConstrainedBox( diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart index 25c2ca32..981d8131 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart @@ -217,10 +217,11 @@ class StreamChatThemeData { TextTheme textTheme, ) { final accentColor = colorTheme.accentBlue; + final iconTheme = IconThemeData(color: colorTheme.black.withOpacity(.5)); return StreamChatThemeData.raw( textTheme: textTheme, colorTheme: colorTheme, - primaryIconTheme: IconThemeData(color: colorTheme.black.withOpacity(.5)), + primaryIconTheme: iconTheme, defaultChannelImage: (context, channel) => const SizedBox(), defaultUserImage: (context, user) => Center( child: CachedNetworkImage( @@ -342,29 +343,70 @@ class StreamChatThemeData { reactionIcons: [ ReactionIcon( type: 'love', - assetName: 'Icon_love_reaction.svg', + builder: (context, highlighted, size) => StreamSvgIcon.loveReaction( + color: highlighted + ? colorTheme.accentBlue + : iconTheme.color!.withOpacity(.5), + size: size, + ), ), ReactionIcon( type: 'like', - assetName: 'Icon_thumbs_up_reaction.svg', + builder: (context, highlighted, size) => + StreamSvgIcon.thumbsUpReaction( + color: highlighted + ? colorTheme.accentBlue + : iconTheme.color!.withOpacity(.5), + size: size, + ), ), ReactionIcon( type: 'sad', - assetName: 'Icon_thumbs_down_reaction.svg', + builder: (context, highlighted, size) => + StreamSvgIcon.thumbsDownReaction( + color: highlighted + ? colorTheme.accentBlue + : iconTheme.color!.withOpacity(.5), + size: size, + ), ), ReactionIcon( type: 'haha', - assetName: 'Icon_LOL_reaction.svg', + builder: (context, highlighted, size) => StreamSvgIcon.lolReaction( + color: highlighted + ? colorTheme.accentBlue + : iconTheme.color!.withOpacity(.5), + size: size, + ), ), ReactionIcon( type: 'wow', - assetName: 'Icon_wut_reaction.svg', + builder: (context, highlighted, size) => StreamSvgIcon.wutReaction( + color: highlighted + ? colorTheme.accentBlue + : iconTheme.color!.withOpacity(.5), + size: size, + ), ), ], ); } } +class ReactionTheme { + final double singleSize; + final double size; + final Color ownReactionColor; + final Color otherReactionColor; + + const ReactionTheme({ + required this.singleSize, + required this.size, + required this.ownReactionColor, + required this.otherReactionColor, + }); +} + /// Class for holding text theme class TextTheme { /// Initialise light text theme diff --git a/packages/stream_chat_flutter/lib/src/stream_svg_icon.dart b/packages/stream_chat_flutter/lib/src/stream_svg_icon.dart index ae344a89..fb5017e1 100644 --- a/packages/stream_chat_flutter/lib/src/stream_svg_icon.dart +++ b/packages/stream_chat_flutter/lib/src/stream_svg_icon.dart @@ -49,6 +49,66 @@ class StreamSvgIcon extends StatelessWidget { height: size, ); + /// [StreamSvgIcon] type + factory StreamSvgIcon.loveReaction({ + double? size, + Color? color, + }) => + StreamSvgIcon( + assetName: 'Icon_love_reaction.svg', + color: color, + width: size, + height: size, + ); + + /// [StreamSvgIcon] type + factory StreamSvgIcon.thumbsUpReaction({ + double? size, + Color? color, + }) => + StreamSvgIcon( + assetName: 'Icon_thumbs_up_reaction.svg', + color: color, + width: size, + height: size, + ); + + /// [StreamSvgIcon] type + factory StreamSvgIcon.thumbsDownReaction({ + double? size, + Color? color, + }) => + StreamSvgIcon( + assetName: 'Icon_thumbs_down_reaction.svg', + color: color, + width: size, + height: size, + ); + + /// [StreamSvgIcon] type + factory StreamSvgIcon.lolReaction({ + double? size, + Color? color, + }) => + StreamSvgIcon( + assetName: 'Icon_LOL_reaction.svg', + color: color, + width: size, + height: size, + ); + + /// [StreamSvgIcon] type + factory StreamSvgIcon.wutReaction({ + double? size, + Color? color, + }) => + StreamSvgIcon( + assetName: 'Icon_wut_reaction.svg', + color: color, + width: size, + height: size, + ); + /// [StreamSvgIcon] type factory StreamSvgIcon.smile({ double? size, From b31754d8d10034c8b6fd3030464954669ce3a12b Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 14 Jun 2021 17:23:39 +0200 Subject: [PATCH 041/150] little fix --- .../lib/src/stream_chat_theme.dart | 91 +++++++++---------- 1 file changed, 45 insertions(+), 46 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart index 981d8131..91ed2537 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart @@ -343,70 +343,69 @@ class StreamChatThemeData { reactionIcons: [ ReactionIcon( type: 'love', - builder: (context, highlighted, size) => StreamSvgIcon.loveReaction( - color: highlighted - ? colorTheme.accentBlue - : iconTheme.color!.withOpacity(.5), - size: size, - ), + builder: (context, highlighted, size) { + final theme = StreamChatTheme.of(context); + return StreamSvgIcon.loveReaction( + color: highlighted + ? theme.colorTheme.accentBlue + : theme.primaryIconTheme.color!.withOpacity(.5), + size: size, + ); + }, ), ReactionIcon( type: 'like', - builder: (context, highlighted, size) => - StreamSvgIcon.thumbsUpReaction( - color: highlighted - ? colorTheme.accentBlue - : iconTheme.color!.withOpacity(.5), - size: size, - ), + builder: (context, highlighted, size) { + final theme = StreamChatTheme.of(context); + return StreamSvgIcon.thumbsUpReaction( + color: highlighted + ? theme.colorTheme.accentBlue + : theme.primaryIconTheme.color!.withOpacity(.5), + size: size, + ); + }, ), ReactionIcon( type: 'sad', - builder: (context, highlighted, size) => - StreamSvgIcon.thumbsDownReaction( - color: highlighted - ? colorTheme.accentBlue - : iconTheme.color!.withOpacity(.5), - size: size, - ), + builder: (context, highlighted, size) { + final theme = StreamChatTheme.of(context); + return StreamSvgIcon.thumbsDownReaction( + color: highlighted + ? theme.colorTheme.accentBlue + : theme.primaryIconTheme.color!.withOpacity(.5), + size: size, + ); + }, ), ReactionIcon( type: 'haha', - builder: (context, highlighted, size) => StreamSvgIcon.lolReaction( - color: highlighted - ? colorTheme.accentBlue - : iconTheme.color!.withOpacity(.5), - size: size, - ), + builder: (context, highlighted, size) { + final theme = StreamChatTheme.of(context); + return StreamSvgIcon.lolReaction( + color: highlighted + ? theme.colorTheme.accentBlue + : theme.primaryIconTheme.color!.withOpacity(.5), + size: size, + ); + }, ), ReactionIcon( type: 'wow', - builder: (context, highlighted, size) => StreamSvgIcon.wutReaction( - color: highlighted - ? colorTheme.accentBlue - : iconTheme.color!.withOpacity(.5), - size: size, - ), + builder: (context, highlighted, size) { + final theme = StreamChatTheme.of(context); + return StreamSvgIcon.wutReaction( + color: highlighted + ? theme.colorTheme.accentBlue + : theme.primaryIconTheme.color!.withOpacity(.5), + size: size, + ); + }, ), ], ); } } -class ReactionTheme { - final double singleSize; - final double size; - final Color ownReactionColor; - final Color otherReactionColor; - - const ReactionTheme({ - required this.singleSize, - required this.size, - required this.ownReactionColor, - required this.otherReactionColor, - }); -} - /// Class for holding text theme class TextTheme { /// Initialise light text theme From a0f1c761005a108fb6bd990faf1e19b93e4b274c Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 15 Jun 2021 13:57:03 +0530 Subject: [PATCH 042/150] minor fixes Signed-off-by: Sahil Kumar --- packages/stream_chat/lib/src/client/client.dart | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 9abfa94c..7a3734da 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -320,7 +320,7 @@ class StreamChatClient { } if (wsConnectionStatus == ConnectionStatus.connected) { - throw StreamChatError('Connection already connected for ${user.id}'); + throw StreamChatError('Connection already available for ${user.id}'); } _wsConnectionStatus = ConnectionStatus.connecting; @@ -343,6 +343,9 @@ class StreamChatClient { /// This will not trigger default auto-retry mechanism for reconnection. /// You need to call [openConnection] to reconnect to [_ws]. void closeConnection() { + if (wsConnectionStatus == ConnectionStatus.disconnected) return; + + logger.info('Closing web-socket connection for ${state.user?.id}'); _wsConnectionStatus = ConnectionStatus.disconnected; _connectionStatusSubscription?.cancel(); @@ -1225,6 +1228,8 @@ class StreamChatClient { /// If [flushChatPersistence] is true the client deletes all offline /// user's data. Future disconnectUser({bool flushChatPersistence = false}) async { + logger.info('Disconnecting user : ${state.user?.id}'); + // resetting state state.dispose(); state = ClientState(this); @@ -1243,15 +1248,19 @@ class StreamChatClient { /// Call this function to dispose the client Future dispose() async { + logger.info('Disposing new StreamChatClient'); + + // disposing state state.dispose(); - await _eventController.close(); - await _wsConnectionStatusController.close(); // disconnecting persistence client await _chatPersistenceClient?.disconnect(); // closing web-socket connection closeConnection(); + + await _eventController.close(); + await _wsConnectionStatusController.close(); } } From 94e2c97468819deed4c3cd2227a23abb54a1bdd4 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 15 Jun 2021 12:38:58 +0200 Subject: [PATCH 043/150] update changelogs and pubspecs --- packages/stream_chat/CHANGELOG.md | 5 +++++ packages/stream_chat/lib/version.dart | 2 +- packages/stream_chat/pubspec.yaml | 2 +- packages/stream_chat_flutter/CHANGELOG.md | 6 ++++++ packages/stream_chat_flutter/pubspec.yaml | 4 ++-- packages/stream_chat_flutter_core/CHANGELOG.md | 5 +++++ packages/stream_chat_flutter_core/pubspec.yaml | 4 ++-- 7 files changed, 22 insertions(+), 6 deletions(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index b6c5cecf..a3bdc6ce 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.0.0-nullsafety.6 + +- Fix thread reply not working with attachments +- Minor fixes + ## 2.0.0-nullsafety.5 - Minor fixes diff --git a/packages/stream_chat/lib/version.dart b/packages/stream_chat/lib/version.dart index da7d8aa3..4049805a 100644 --- a/packages/stream_chat/lib/version.dart +++ b/packages/stream_chat/lib/version.dart @@ -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 = '2.0.0-nullsafety.5'; +const PACKAGE_VERSION = '2.0.0-nullsafety.6'; diff --git a/packages/stream_chat/pubspec.yaml b/packages/stream_chat/pubspec.yaml index b13838aa..ec1d5bd1 100644 --- a/packages/stream_chat/pubspec.yaml +++ b/packages/stream_chat/pubspec.yaml @@ -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: 2.0.0-nullsafety.5 +version: 2.0.0-nullsafety.6 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 39247b13..170bcf6b 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,3 +1,9 @@ +## 2.0.0-nullsafety.6 + +- Minor fixes and improvements +- Updated `stream_chat_core` dependency +- 🛑 **BREAKING** Updated StreamChatThemeData.reactionIcons to accept custom builder + ## 2.0.0-nullsafety.5 - Minor fixes and improvements diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index 61aad8df..e165b136 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -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: 2.0.0-nullsafety.5 +version: 2.0.0-nullsafety.6 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -36,7 +36,7 @@ dependencies: scrollable_positioned_list: ^0.2.0-nullsafety.0 share_plus: ^2.0.3 shimmer: ^2.0.0 - stream_chat_flutter_core: ^2.0.0-nullsafety.5 + stream_chat_flutter_core: ^2.0.0-nullsafety.6 substring_highlight: ^1.0.26 synchronized: ^3.0.0 url_launcher: ^6.0.3 diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index 26cfa6c3..71ff8f51 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.0.0-nullsafety.6 + +* Update llc dependency +* Minor fixes and improvements + ## 2.0.0-nullsafety.5 * Update llc dependency diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml index f9768bdd..40a40c39 100644 --- a/packages/stream_chat_flutter_core/pubspec.yaml +++ b/packages/stream_chat_flutter_core/pubspec.yaml @@ -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: 2.0.0-nullsafety.5 +version: 2.0.0-nullsafety.6 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -16,7 +16,7 @@ dependencies: sdk: flutter meta: ^1.3.0 rxdart: ^0.27.0 - stream_chat: ^2.0.0-nullsafety.5 + stream_chat: ^2.0.0-nullsafety.6 dev_dependencies: fake_async: ^1.2.0 From 2af7c78bd102f751057fc2f913561eb93db097e1 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 16 Jun 2021 10:48:19 +0200 Subject: [PATCH 044/150] fix reconnection logic --- .../stream_chat_flutter_core/lib/src/stream_chat_core.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart b/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart index d3ed4a0b..8de25615 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart @@ -125,7 +125,8 @@ class StreamChatCoreState extends State _isConnectionAvailable = result != ConnectivityResult.none; if (!_isInForeground) return; if (_isConnectionAvailable) { - if (client.wsConnectionStatus == ConnectionStatus.disconnected) { + if (client.wsConnectionStatus == ConnectionStatus.disconnected && + user != null) { client.connect(); } } else { From e8e1d53aa34dad49a6a75f1b1008e843c3a00fce Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 16 Jun 2021 22:19:00 +0530 Subject: [PATCH 045/150] add client_test.dart Signed-off-by: Sahil Kumar --- .../stream_chat/lib/src/client/channel.dart | 32 +- .../stream_chat/lib/src/client/client.dart | 81 +- .../lib/src/core/api/message_api.dart | 16 +- .../test/src/api/channel_test.dart | 14 +- .../stream_chat/test/src/api/client_test.dart | 2135 +++++++++++++++++ .../test/src/core/api/message_api_test.dart | 4 +- packages/stream_chat/test/src/fakes.dart | 89 + packages/stream_chat/test/src/matchers.dart | 36 + packages/stream_chat/test/src/mocks.dart | 11 +- packages/stream_chat/test/src/utils.dart | 9 + .../lib/src/message_list_view.dart | 2 +- .../lib/src/stream_channel.dart | 2 +- 12 files changed, 2356 insertions(+), 75 deletions(-) create mode 100644 packages/stream_chat/test/src/api/client_test.dart diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index fe56aed4..9d0dc96c 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -22,9 +22,9 @@ class Channel { this._client, this._type, this._id, { - Map extraData = const {}, + Map? extraData, }) : _cid = _id != null ? '$_type:$_id' : null, - _extraData = extraData { + _extraData = extraData ?? {} { _client.logger.info('New Channel instance not initialized created'); } @@ -202,8 +202,13 @@ class Channel { } /// Channel extra data - Map get extraData => - state?._channelState.channel?.extraData ?? _extraData; + Map get extraData { + var data = state?._channelState.channel?.extraData; + if (data == null || data.isEmpty) { + data = _extraData; + } + return data; + } /// Channel extra data as a stream Stream> get extraDataStream { @@ -942,7 +947,7 @@ class Channel { state = ChannelClientState(this, channelState); if (cid != null) { - client.state.channels[cid!] = this; + client.state.channels = {cid!: this}; } if (!_initializedCompleter.isCompleted) { _initializedCompleter.complete(true); @@ -959,8 +964,8 @@ class Channel { /// Set [preferOffline] to true to avoid the api call if the data is already /// in the offline storage Future getReplies( - String parentId, - PaginationParams options, { + String parentId, { + PaginationParams? options, bool preferOffline = false, }) async { final cachedReplies = await _client.chatPersistenceClient?.getReplies( @@ -973,19 +978,22 @@ class Channel { return QueryRepliesResponse()..messages = cachedReplies; } } - final repliesResponse = await _client.getReplies(parentId, options); + final repliesResponse = await _client.getReplies( + parentId, + options: options, + ); state?.updateThreadInfo(parentId, repliesResponse.messages); return repliesResponse; } /// List the reactions for a message in the channel Future getReactions( - String messageId, - PaginationParams options, - ) => + String messageId, { + PaginationParams? options, + }) => _client.getReactions( messageId, - options, + options: options, ); /// Retrieves a list of messages by ID diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 7a22ac50..ecadfe3b 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -63,7 +63,7 @@ class StreamChatClient { /// application. StreamChatClient( String apiKey, { - this.logLevel = Level.ALL, + this.logLevel = Level.WARNING, LogHandlerFunction? logHandlerFunction, RetryPolicy? retryPolicy, Location? location, @@ -71,9 +71,11 @@ class StreamChatClient { Duration connectTimeout = const Duration(seconds: 6), Duration receiveTimeout = const Duration(seconds: 6), StreamChatApi? chatApi, + WebSocket? ws, AttachmentFileUploader? attachmentFileUploader, }) { - _setupLogger(logHandlerFunction); + this.logHandlerFunction = logHandlerFunction ?? _defaultLogHandler; + logger.info('Initiating new StreamChatClient'); final options = StreamHttpClientOptions( baseUrl: baseURL, @@ -92,13 +94,14 @@ class StreamChatClient { logger: detachedLogger('🕸ï¸'), ); - _ws = WebSocket( - apiKey: apiKey, - baseUrl: options.baseUrl, - tokenManager: _tokenManager, - handler: handleEvent, - logger: detachedLogger('🔌'), - ); + _ws = ws ?? + WebSocket( + apiKey: apiKey, + baseUrl: options.baseUrl, + tokenManager: _tokenManager, + handler: handleEvent, + logger: detachedLogger('🔌'), + ); _retryPolicy = retryPolicy ?? RetryPolicy( @@ -107,8 +110,6 @@ class StreamChatClient { ); state = ClientState(this); - - logger.info('instantiating new client'); } late final StreamChatApi _chatApi; @@ -160,7 +161,7 @@ class StreamChatClient { /// Client specific logger instance. /// Refer to the class [Logger] to learn more about the specific /// implementation. - final Logger logger = Logger.detached('📡'); + late final Logger logger = detachedLogger('📡'); /// A function that has a parameter of type [LogRecord]. /// This is called on every new log record. @@ -185,7 +186,7 @@ class StreamChatClient { final _eventController = BehaviorSubject(); - /// Stream of [Event] coming from websocket connection + /// Stream of [Event] coming from [_ws] connection /// Listen to this or use the [on] method to filter specific event types Stream get eventStream => _eventController.stream; @@ -195,12 +196,12 @@ class StreamChatClient { set _wsConnectionStatus(ConnectionStatus status) => _wsConnectionStatusController.add(status); - /// The current status value of the websocket connection + /// The current status value of the [_ws] connection ConnectionStatus get wsConnectionStatus => _wsConnectionStatusController.value; - /// This notifies the connection status of the websocket connection. - /// Listen to this to get notified when the websocket tries to reconnect. + /// This notifies the connection status of the [_ws] connection. + /// Listen to this to get notified when the [_ws] tries to reconnect. Stream get wsConnectionStatusStream => _wsConnectionStatusController.stream.distinct(); @@ -215,19 +216,9 @@ class StreamChatClient { }; /// - Logger detachedLogger( - String name, - ) => - Logger.detached(name) - ..level = logLevel - ..onRecord.listen(logHandlerFunction); - - void _setupLogger(LogHandlerFunction? logHandlerFunction) { - logger.level = logLevel; - this.logHandlerFunction = logHandlerFunction ?? _defaultLogHandler; - logger.onRecord.listen(this.logHandlerFunction); - logger.info('logger setup'); - } + Logger detachedLogger(String name) => Logger.detached(name) + ..level = logLevel + ..onRecord.listen(logHandlerFunction); /// Connects the current user, this triggers a connection to the API. /// It returns a [Future] that resolves when the connection is setup. @@ -325,9 +316,13 @@ class StreamChatClient { _wsConnectionStatus = ConnectionStatus.connecting; - _connectionStatusSubscription = _ws.connectionStatusStream.listen( - _connectionStatusHandler, - ); + // skipping `ws` seed connection status -> ConnectionStatus.disconnected + // otherwise `client.wsConnectionStatusStream` will emit in order + // 1. ConnectionStatus.disconnected -> client seed status + // 2. ConnectionStatus.connecting -> client connecting status + // 3. ConnectionStatus.disconnected -> ws seed status + _connectionStatusSubscription = + _ws.connectionStatusStream.skip(1).listen(_connectionStatusHandler); try { return await _ws.connect(user); @@ -768,13 +763,13 @@ class StreamChatClient { _chatApi.device.removeDevice(id); /// Get a development token - String devToken(String userId) => Token.development(userId).rawValue; + Token devToken(String userId) => Token.development(userId); /// Returns a channel client with the given type, id and custom data. Channel channel( String type, { String? id, - Map extraData = const {}, + Map? extraData, }) { if (id != null && state.channels.containsKey('$type:$id')) { return state.channels['$type:$id']!; @@ -796,6 +791,7 @@ class StreamChatClient { ); /// watches the provided channel + /// Creates first if not yet created Future watchChannel( String channelType, { String? channelId, @@ -809,6 +805,7 @@ class StreamChatClient { ); /// Query the API, get messages, members or other channel fields + /// Creates the channel first if not yet created Future queryChannel( String channelType, { bool state = true, @@ -1139,22 +1136,22 @@ class StreamChatClient { /// Lists all the message replies for the [parentId] Future getReplies( - String parentId, - PaginationParams options, - ) => + String parentId, { + PaginationParams? options, + }) => _chatApi.message.getReplies( parentId, - options, + options: options, ); /// Get all the reactions for a [messageId] Future getReactions( - String messageId, - PaginationParams options, - ) => + String messageId, { + PaginationParams? options, + }) => _chatApi.message.getReactions( messageId, - options, + options: options, ); /// Update the given message diff --git a/packages/stream_chat/lib/src/core/api/message_api.dart b/packages/stream_chat/lib/src/core/api/message_api.dart index 6d80f35b..22f7bfef 100644 --- a/packages/stream_chat/lib/src/core/api/message_api.dart +++ b/packages/stream_chat/lib/src/core/api/message_api.dart @@ -142,13 +142,13 @@ class MessageApi { /// Get all the reactions for a [messageId] Future getReactions( - String messageId, - PaginationParams options, - ) async { + String messageId, { + PaginationParams? options, + }) async { final response = await _client.get( '/messages/$messageId/reactions', queryParameters: { - ...options.toJson(), + if (options != null) ...options.toJson(), }, ); return QueryReactionsResponse.fromJson(response.data); @@ -168,13 +168,13 @@ class MessageApi { /// Lists all the message replies for the [parentId] Future getReplies( - String parentId, - PaginationParams options, - ) async { + String parentId, { + PaginationParams? options, + }) async { final response = await _client.get( '/messages/$parentId/replies', queryParameters: { - ...options.toJson(), + if (options != null) ...options.toJson(), }, ); return QueryRepliesResponse.fromJson(response.data); diff --git a/packages/stream_chat/test/src/api/channel_test.dart b/packages/stream_chat/test/src/api/channel_test.dart index 41b50360..22d04a43 100644 --- a/packages/stream_chat/test/src/api/channel_test.dart +++ b/packages/stream_chat/test/src/api/channel_test.dart @@ -1490,7 +1490,6 @@ void main() { test('`.getReplies`', () async { const parentId = 'test-parent-id'; - const options = PaginationParams(); final messages = List.generate( 3, @@ -1500,22 +1499,21 @@ void main() { ), ); - when(() => client.getReplies(parentId, options)).thenAnswer( + when(() => client.getReplies(parentId)).thenAnswer( (_) async => QueryRepliesResponse()..messages = messages, ); - final res = await channel.getReplies(parentId, options); + final res = await channel.getReplies(parentId); expect(res, isNotNull); expect(res.messages.length, messages.length); expect(res.messages.every((it) => it.parentId == parentId), isTrue); - verify(() => client.getReplies(parentId, options)).called(1); + verify(() => client.getReplies(parentId)).called(1); }); test('`.getReactions`', () async { const messageId = 'test-message-id'; - const options = PaginationParams(); final reactions = List.generate( 3, @@ -1525,17 +1523,17 @@ void main() { ), ); - when(() => client.getReactions(messageId, options)).thenAnswer( + when(() => client.getReactions(messageId)).thenAnswer( (_) async => QueryReactionsResponse()..reactions = reactions, ); - final res = await channel.getReactions(messageId, options); + final res = await channel.getReactions(messageId); expect(res, isNotNull); expect(res.reactions.length, reactions.length); expect(res.reactions.every((it) => it.messageId == messageId), isTrue); - verify(() => client.getReactions(messageId, options)).called(1); + verify(() => client.getReactions(messageId)).called(1); }); test('`.getMessagesById`', () async { diff --git a/packages/stream_chat/test/src/api/client_test.dart b/packages/stream_chat/test/src/api/client_test.dart new file mode 100644 index 00000000..b0b30c83 --- /dev/null +++ b/packages/stream_chat/test/src/api/client_test.dart @@ -0,0 +1,2135 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/client/client.dart'; +import 'package:stream_chat/src/core/api/device_api.dart'; +import 'package:stream_chat/src/core/api/requests.dart'; +import 'package:stream_chat/src/core/api/responses.dart'; +import 'package:stream_chat/src/core/error/error.dart'; +import 'package:stream_chat/src/core/http/token.dart'; +import 'package:stream_chat/src/core/models/channel_model.dart'; +import 'package:stream_chat/src/core/models/event.dart'; +import 'package:stream_chat/src/core/models/filter.dart'; +import 'package:stream_chat/src/core/models/message.dart'; +import 'package:stream_chat/src/core/models/own_user.dart'; +import 'package:stream_chat/src/core/models/user.dart'; +import 'package:stream_chat/src/event_type.dart'; +import 'package:stream_chat/src/ws/connection_status.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:test/scaffolding.dart'; +import 'package:test/test.dart'; + +import '../fakes.dart'; +import '../matchers.dart'; +import '../mocks.dart'; +import '../utils.dart'; + +void main() { + group('Fake web-socket connection functions', () { + const apiKey = 'test-api-key'; + late final api = FakeChatApi(); + + late StreamChatClient client; + + setUpAll(() { + // fallback values + registerFallbackValue(FakeUser()); + }); + + setUp(() { + final ws = FakeWebSocket(); + client = StreamChatClient(apiKey, ws: ws, chatApi: api); + }); + + tearDown(() { + client.dispose(); + }); + + test('`.connectUser` should work fine', () async { + final user = User(id: 'test-user-id'); + final token = Token.development(user.id).rawValue; + + final event = Event( + type: EventType.healthCheck, + connectionId: 'fake-connection-id', + me: OwnUser.fromUser(user), + ); + + expectLater( + // skipping first seed status -> ConnectionStatus.disconnected + client.wsConnectionStatusStream.skip(1), + emitsInOrder([ + ConnectionStatus.connecting, + ConnectionStatus.connected, + ]), + ); + + final res = await client.connectUser(user, token); + expect(res, isNotNull); + expect(res.type, event.type); + expect(res.connectionId, event.connectionId); + expect(res.me, isSameUserAs(user)); + }); + + test('`.connectUserWithProvider` should work fine', () async { + final user = User(id: 'test-user-id'); + Future tokenProvider(String userId) async { + expect(userId, user.id); + return Token.development(userId).rawValue; + } + + final event = Event( + type: EventType.healthCheck, + connectionId: 'fake-connection-id', + me: OwnUser.fromUser(user), + ); + + expectLater( + // skipping first seed status -> ConnectionStatus.disconnected + client.wsConnectionStatusStream.skip(1), + emitsInOrder([ + ConnectionStatus.connecting, + ConnectionStatus.connected, + ]), + ); + + final res = await client.connectUserWithProvider(user, tokenProvider); + expect(res, isNotNull); + expect(res.type, event.type); + expect(res.connectionId, event.connectionId); + expect(res.me, isSameUserAs(user)); + }); + + group('`.connectGuestUser`', () { + test('should work fine', () async { + final user = User(id: 'test-user-id'); + final token = Token.development(user.id).rawValue; + + when(() => api.guest.getGuestUser(any(that: isSameUserAs(user)))) + .thenAnswer( + (_) async => ConnectGuestUserResponse() + ..user = user + ..accessToken = token, + ); + + final event = Event( + type: EventType.healthCheck, + connectionId: 'fake-connection-id', + me: OwnUser.fromUser(user), + ); + + expectLater( + // skipping first seed status -> ConnectionStatus.disconnected + client.wsConnectionStatusStream.skip(1), + emitsInOrder([ + ConnectionStatus.connecting, + ConnectionStatus.connected, + ]), + ); + + final res = await client.connectGuestUser(user); + expect(res, isNotNull); + expect(res.type, event.type); + expect(res.connectionId, event.connectionId); + expect(res.me, isSameUserAs(user)); + + verify( + () => api.guest.getGuestUser(any(that: isSameUserAs(user))), + ).called(1); + }); + + test('should throw if `.getGuestUser` fails', () async { + final user = User(id: 'test-user-id'); + + when(() => api.guest.getGuestUser(any(that: isSameUserAs(user)))) + .thenThrow(StreamChatNetworkError(ChatErrorCode.inputError)); + + expectLater( + client.wsConnectionStatusStream, + emitsInOrder([ + // only emits the seed -> disconnected status + // as the call never reaches `ws.connect` + ConnectionStatus.disconnected, + ]), + ); + + try { + await client.connectGuestUser(user); + } catch (e) { + expect(e, isA()); + } + + verify( + () => api.guest.getGuestUser(any(that: isSameUserAs(user))), + ).called(1); + }); + }); + + test('`.connectAnonymousUser` should work fine', () async { + expectLater( + // skipping first seed status -> ConnectionStatus.disconnected + client.wsConnectionStatusStream.skip(1), + emitsInOrder([ + ConnectionStatus.connecting, + ConnectionStatus.connected, + ]), + ); + + final res = await client.connectAnonymousUser(); + expect(res, isNotNull); + expect(res.type, EventType.healthCheck); + expect(res.connectionId, 'fake-connection-id'); + expect(res.me, isNotNull); + }); + + group('`.openConnection`', () { + test('should throw if state does not contain user', () async { + expect(client.state.user, isNull); + try { + await client.openConnection(); + } catch (e) { + expect(e, isA()); + } + }); + + test('should throw if connection is already in progress', () async { + expect(client.state.user, isNull); + try { + await client.connectAnonymousUser(); + await client.openConnection(); + } catch (e) { + expect(e, isA()); + final err = e as StreamChatError; + expect( + err.message.contains('Connection already in progress for'), + isTrue, + ); + } + }); + + test('should throw if connection is already available', () async { + expect(client.state.user, isNull); + try { + await client.connectAnonymousUser(); + // waiting 300ms for `wsConnectionStatusStream` to emit + await delay(300); + + await client.openConnection(); + } catch (e) { + expect(e, isA()); + final err = e as StreamChatError; + expect( + err.message.contains('Connection already available for'), + isTrue, + ); + } + }); + + test('should open connection for closed connection', () async { + expectLater( + client.wsConnectionStatusStream.skip(1), + emitsInOrder([ + // initial connectUser + ConnectionStatus.connecting, + ConnectionStatus.connected, + // close connection + ConnectionStatus.disconnected, + // open connection + ConnectionStatus.connecting, + ConnectionStatus.connected, + ]), + ); + + await client.connectAnonymousUser(); + // waiting 300ms for `wsConnectionStatusStream` to emit + await delay(300); + + client.closeConnection(); + + await client.openConnection(); + }); + }); + }); + + group('Fake web-socket connection functions failure', () { + const apiKey = 'test-api-key'; + late final api = FakeChatApi(); + + late StreamChatClient client; + + setUpAll(() { + // fallback values + registerFallbackValue(FakeUser()); + }); + + setUp(() { + final ws = FakeWebSocketWithConnectionError(); + client = StreamChatClient(apiKey, chatApi: api, ws: ws); + }); + + tearDown(() { + client.dispose(); + }); + + test('`.connectUser` should throw if `ws.connect` fails', () async { + final user = User(id: 'test-user-id'); + final token = Token.development(user.id).rawValue; + + try { + await client.connectUser(user, token); + } catch (e) { + expect(e, isA()); + } + }); + + test( + '`.connectUserWithProvider` should throw if `ws.connect` fails', + () async { + final user = User(id: 'test-user-id'); + Future tokenProvider(String userId) async { + expect(userId, user.id); + return Token.development(userId).rawValue; + } + + try { + await client.connectUserWithProvider(user, tokenProvider); + } catch (e) { + expect(e, isA()); + } + }, + ); + + test('`.connectGuestUser` should throw if `ws.connect` fails', () async { + final user = User(id: 'test-user-id'); + final token = Token.development(user.id).rawValue; + + when(() => api.guest.getGuestUser(any(that: isSameUserAs(user)))) + .thenAnswer( + (_) async => ConnectGuestUserResponse() + ..user = user + ..accessToken = token, + ); + + try { + await client.connectGuestUser(user); + } catch (e) { + expect(e, isA()); + } + verify( + () => api.guest.getGuestUser(any(that: isSameUserAs(user))), + ).called(1); + }); + + test( + '`.connectAnonymousUser` should throw if `ws.connect` fails', + () async { + try { + await client.connectAnonymousUser(); + } catch (e) { + expect(e, isA()); + } + }, + ); + }); + + group('Client with connected user with persistence', () { + const apiKey = 'test-api-key'; + late final api = FakeChatApi(); + late final ws = FakeWebSocket(); + late final persistence = MockPersistenceClient(); + + final user = User(id: 'test-user-id'); + final token = Token.development(user.id).rawValue; + + late StreamChatClient client; + + setUpAll(() { + // fallback values + registerFallbackValue(FakeEvent()); + registerFallbackValue(const PaginationParams()); + registerFallbackValue(FakeChannelState()); + }); + + setUp(() async { + client = StreamChatClient(apiKey, chatApi: api, ws: ws) + ..chatPersistenceClient = persistence; + await client.connectUser(user, token); + await delay(300); + expect(client.persistenceEnabled, isTrue); + expect(client.wsConnectionStatus, ConnectionStatus.connected); + }); + + tearDown(() { + client.dispose(); + }); + + group('`.sync`', () { + test( + 'should update persistence connectionInfo and lastSync when sync succeeds', + () async { + const cids = ['test-cid-1', 'test-cid-2', 'test-cid-3']; + final lastSyncAt = DateTime.now(); + + when(() => api.general.sync(cids, lastSyncAt)) + .thenAnswer((_) async => SyncResponse() + ..events = [ + Event( + isLocal: false, + type: EventType.healthCheck, + connectionId: 'test-connection-id', + me: OwnUser.fromUser(user), + ), + Event( + isLocal: false, + type: EventType.messageDeleted, + message: Message(id: 'test-message-id'), + ), + ]); + + when(() => persistence.updateConnectionInfo(any())) + .thenAnswer((_) => Future.value()); + when(() => persistence.updateLastSyncAt(any())) + .thenAnswer((_) => Future.value()); + + await client.sync(cids: cids, lastSyncAt: lastSyncAt); + + verify(() => persistence.updateConnectionInfo(any())).called(1); + verify(() => persistence.updateLastSyncAt(any())).called(1); + verify(() => api.general.sync(cids, lastSyncAt)).called(1); + }, + ); + + test( + 'should work fine if persistence contains sync params', + () async { + const cids = ['test-cid-1', 'test-cid-2', 'test-cid-3']; + final lastSyncAt = DateTime.now(); + + when(persistence.getChannelCids).thenAnswer((_) async => cids); + when(persistence.getLastSyncAt).thenAnswer((_) async => lastSyncAt); + + when(() => api.general.sync(cids, lastSyncAt)) + .thenAnswer((_) async => SyncResponse() + ..events = [ + Event( + isLocal: false, + type: EventType.healthCheck, + connectionId: 'test-connection-id', + me: OwnUser.fromUser(user), + ), + Event( + isLocal: false, + type: EventType.messageDeleted, + message: Message(id: 'test-message-id', text: 'Hey!'), + ), + ]); + + when(() => persistence.updateConnectionInfo(any())) + .thenAnswer((_) => Future.value()); + when(() => persistence.updateLastSyncAt(any())) + .thenAnswer((_) => Future.value()); + + await client.sync(); + + verify(() => persistence.updateConnectionInfo(any())).called(1); + verify(() => persistence.updateLastSyncAt(any())).called(1); + verify(() => api.general.sync(cids, lastSyncAt)).called(1); + verify(persistence.getChannelCids).called(1); + verify(persistence.getLastSyncAt).called(1); + }, + ); + }); + + group('`.queryChannels`', () { + test( + 'should emit channels twice if persistence contains some channels', + () async { + final persistentChannelStates = List.generate( + 3, + (index) => ChannelState( + channel: ChannelModel(cid: 'p-test-type-$index:p-test-id-$index'), + ), + ); + + when(() => persistence.getChannelStates( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + paginationParams: any(named: 'paginationParams'), + )).thenAnswer((_) async => persistentChannelStates); + + final channelStates = List.generate( + 3, + (index) => ChannelState( + channel: ChannelModel(cid: 'test-type-$index:test-id-$index'), + ), + ); + + when(() => api.channel.queryChannels( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), + paginationParams: any(named: 'paginationParams'), + )).thenAnswer( + (_) async => QueryChannelsResponse()..channels = channelStates, + ); + + when(() => persistence.getChannelThreads(any())) + .thenAnswer((_) async => {}); + when(() => persistence.updateMessages(any(), any())) + .thenAnswer((_) => Future.value()); + when(() => persistence.getChannelStateByCid(any(), + messagePagination: any(named: 'messagePagination'), + pinnedMessagePagination: + any(named: 'pinnedMessagePagination'))).thenAnswer( + (invocation) async => ChannelState( + channel: ChannelModel(cid: invocation.positionalArguments.first), + ), + ); + when(() => persistence.updateChannelQueries(any(), any(), + clearQueryCache: any(named: 'clearQueryCache'))) + .thenAnswer((_) => Future.value()); + + expectLater( + client.queryChannels(), + emitsInOrder([ + // emits persistent channels first + persistentChannelStates.map(isCorrectChannelFor), + // makes api call and emits network fetched channels + channelStates.map(isCorrectChannelFor), + ]), + ); + + // Hack as `teardown` gets called even + // before our stream starts emitting data + await delay(300); + + verify(() => persistence.getChannelStates( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + paginationParams: any(named: 'paginationParams'), + )).called(1); + + verify(() => api.channel.queryChannels( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), + paginationParams: any(named: 'paginationParams'), + )).called(1); + + verify(() => persistence.getChannelThreads(any())) + .called((persistentChannelStates + channelStates).length); + verify(() => persistence.updateMessages(any(), any())) + .called((persistentChannelStates + channelStates).length); + verify( + () => persistence.getChannelStateByCid(any(), + messagePagination: any(named: 'messagePagination'), + pinnedMessagePagination: any(named: 'pinnedMessagePagination')), + ).called((persistentChannelStates + channelStates).length); + verify(() => persistence.updateChannelQueries(any(), any(), + clearQueryCache: any(named: 'clearQueryCache'))).called(1); + }, + ); + + test( + 'should never rethrow network call if persistence already emitted some channels', + () async { + final persistentChannelStates = List.generate( + 3, + (index) => ChannelState( + channel: ChannelModel(cid: 'p-test-type-$index:p-test-id-$index'), + ), + ); + + when(() => persistence.getChannelStates( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + paginationParams: any(named: 'paginationParams'), + )).thenAnswer((_) async => persistentChannelStates); + + when(() => api.channel.queryChannels( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), + paginationParams: any(named: 'paginationParams'), + )).thenThrow(StreamChatNetworkError(ChatErrorCode.inputError)); + + when(() => persistence.getChannelThreads(any())) + .thenAnswer((_) async => {}); + when(() => persistence.updateMessages(any(), any())) + .thenAnswer((_) => Future.value()); + when(() => persistence.getChannelStateByCid(any(), + messagePagination: any(named: 'messagePagination'), + pinnedMessagePagination: + any(named: 'pinnedMessagePagination'))).thenAnswer( + (invocation) async => ChannelState( + channel: ChannelModel(cid: invocation.positionalArguments.first), + ), + ); + + expectLater( + client.queryChannels(), + emitsInOrder([ + // emits persistent channels + persistentChannelStates.map(isCorrectChannelFor), + ]), + ); + + // Hack as `teardown` gets called even + // before our stream starts emitting data + await delay(300); + + verify(() => persistence.getChannelStates( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + paginationParams: any(named: 'paginationParams'), + )).called(1); + + verify(() => api.channel.queryChannels( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), + paginationParams: any(named: 'paginationParams'), + )).called(1); + + verify(() => persistence.getChannelThreads(any())) + .called(persistentChannelStates.length); + verify(() => persistence.updateMessages(any(), any())) + .called(persistentChannelStates.length); + verify( + () => persistence.getChannelStateByCid(any(), + messagePagination: any(named: 'messagePagination'), + pinnedMessagePagination: any(named: 'pinnedMessagePagination')), + ).called(persistentChannelStates.length); + }, + ); + }); + + test('`.disconnectUser` should reset state and user', () async { + expect(client.state.user, isNotNull); + expect(client.wsConnectionStatus, ConnectionStatus.connected); + + expectLater( + // skipping initial connected value + client.wsConnectionStatusStream.skip(1), + emits(ConnectionStatus.disconnected), + ); + + await client.disconnectUser(); + + expect(client.state.user, isNull); + expect(client.wsConnectionStatus, ConnectionStatus.disconnected); + }); + }); + + group('Client with connected user without persistence', () { + const apiKey = 'test-api-key'; + late final api = FakeChatApi(); + late final ws = FakeWebSocket(); + + final user = User(id: 'test-user-id'); + final token = Token.development(user.id).rawValue; + + late StreamChatClient client; + + setUpAll(() { + // fallback values + registerFallbackValue(FakeEvent()); + registerFallbackValue(FakeMessage()); + registerFallbackValue(const PaginationParams()); + }); + + setUp(() async { + client = StreamChatClient(apiKey, chatApi: api, ws: ws); + await client.connectUser(user, token); + await delay(300); + expect(client.persistenceEnabled, isFalse); + expect(client.wsConnectionStatus, ConnectionStatus.connected); + }); + + tearDown(() { + client.dispose(); + }); + + group('`.sync`', () { + test('should work fine', () async { + const cids = ['test-cid-1', 'test-cid-2', 'test-cid-3']; + final lastSyncAt = DateTime.now(); + + when(() => api.general.sync(cids, lastSyncAt)) + .thenAnswer((_) async => SyncResponse() + ..events = [ + Event( + isLocal: false, + type: EventType.healthCheck, + connectionId: 'test-connection-id', + me: OwnUser.fromUser(user), + ), + Event( + isLocal: false, + type: EventType.messageDeleted, + message: Message(id: 'test-message-id'), + ), + ]); + + await client.sync(cids: cids, lastSyncAt: lastSyncAt); + + verify(() => api.general.sync(cids, lastSyncAt)).called(1); + }); + + test('should return if `cids` is not available', () async { + expect(client.sync, returnsNormally); + verifyNever(() => api.general.sync(any(), any())); + }); + + test('should return if `lastSyncAt` is not available', () async { + expect(() => client.sync(cids: ['test-cid-1']), returnsNormally); + verifyNever(() => api.general.sync(any(), any())); + }); + }); + + group('`.queryChannels`', () { + test('should work fine without persistent channels', () async { + final channelStates = List.generate( + 3, + (index) => ChannelState( + channel: ChannelModel(cid: 'test-type-$index:test-id-$index'), + ), + ); + + when(() => api.channel.queryChannels( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), + paginationParams: any(named: 'paginationParams'), + )).thenAnswer( + (_) async => QueryChannelsResponse()..channels = channelStates, + ); + + expectLater( + client.queryChannels(), + emitsInOrder([channelStates.map(isCorrectChannelFor)]), + ); + + // Hack as `teardown` gets called even + // before our stream starts emitting data + await delay(300); + + verify(() => api.channel.queryChannels( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), + paginationParams: any(named: 'paginationParams'), + )).called(1); + }); + + test( + 'should rethrow if `.queryChannelsOnline` throws and persistence channels are empty', + () async { + when(() => api.channel.queryChannels( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), + paginationParams: any(named: 'paginationParams'), + )).thenThrow(StreamChatNetworkError(ChatErrorCode.inputError)); + + expectLater( + client.queryChannels(), + emitsError(isA()), + ); + + // Hack as `teardown` gets called even + // before our stream starts emitting data + await delay(300); + + verify(() => api.channel.queryChannels( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), + paginationParams: any(named: 'paginationParams'), + )).called(1); + }, + ); + }); + + test('`.queryUsers`', () async { + final users = List.generate( + 3, + (index) => User(id: 'test-user-id-$index'), + ); + + when(() => api.user.queryUsers( + presence: any(named: 'presence'), + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + )).thenAnswer((_) async => QueryUsersResponse()..users = users); + + expectLater( + // skipping initial seed event -> {} users + client.state.usersStream.skip(1), + emitsInOrder([ + {for (var user in users) user.id: user}, + ]), + ); + + final res = await client.queryUsers(); + expect(res, isNotNull); + expect(res.users.length, users.length); + + verify(() => api.user.queryUsers( + presence: any(named: 'presence'), + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + )).called(1); + verifyNoMoreInteractions(api.user); + }); + + test('`.search`', () async { + const cid = 'test-type:test-id'; + final filter = Filter.in_('cid', const [cid]); + + final messages = List.generate( + 3, + (index) => GetMessageResponse() + ..channel = ChannelModel(cid: cid) + ..message = Message(id: 'test-message-id-$index'), + ); + + when(() => api.general.searchMessages(filter, + query: any(named: 'query'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + messageFilters: any(named: 'messageFilters'))) + .thenAnswer( + (_) async => SearchMessagesResponse()..results = messages); + + final res = await client.search(filter); + expect(res, isNotNull); + expect(res.results.length, messages.length); + + verify(() => api.general.searchMessages(filter, + query: any(named: 'query'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + messageFilters: any(named: 'messageFilters'))).called(1); + verifyNoMoreInteractions(api.general); + }); + + test('`.sendFile`', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + final file = AttachmentFile(size: 33, path: 'test-file-path'); + + const fileUrl = 'test-file-url'; + + when(() => api.fileUploader.sendFile(file, channelId, channelType)) + .thenAnswer((_) async => SendFileResponse()..file = fileUrl); + + final res = await client.sendFile(file, channelId, channelType); + expect(res, isNotNull); + expect(res.file, fileUrl); + + verify(() => api.fileUploader.sendFile(file, channelId, channelType)) + .called(1); + verifyNoMoreInteractions(api.fileUploader); + }); + + test('`.sendImage`', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + final image = AttachmentFile(size: 33, path: 'test-image-path'); + + const fileUrl = 'test-image-url'; + + when(() => api.fileUploader.sendImage(image, channelId, channelType)) + .thenAnswer((_) async => SendImageResponse()..file = fileUrl); + + final res = await client.sendImage(image, channelId, channelType); + expect(res, isNotNull); + expect(res.file, fileUrl); + + verify(() => api.fileUploader.sendImage(image, channelId, channelType)) + .called(1); + verifyNoMoreInteractions(api.fileUploader); + }); + + test('`.deleteFile`', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + const fileUrl = 'test-file-url'; + + when(() => api.fileUploader.deleteFile(fileUrl, channelId, channelType)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.deleteFile(fileUrl, channelId, channelType); + expect(res, isNotNull); + + verify(() => api.fileUploader.deleteFile(fileUrl, channelId, channelType)) + .called(1); + verifyNoMoreInteractions(api.fileUploader); + }); + + test('`.deleteImage`', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + const imageUrl = 'test-image-url'; + + when(() => api.fileUploader.deleteImage(imageUrl, channelId, channelType)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.deleteImage(imageUrl, channelId, channelType); + expect(res, isNotNull); + + verify( + () => api.fileUploader.deleteImage(imageUrl, channelId, channelType), + ).called(1); + verifyNoMoreInteractions(api.fileUploader); + }); + + test('`.updateChannel`', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + const data = {'name': 'test-channel'}; + + when(() => api.channel.updateChannel(channelId, channelType, data)) + .thenAnswer((invocation) async => UpdateChannelResponse() + ..channel = ChannelModel( + id: channelId, + type: channelType, + extraData: {...data}, + )); + + final res = await client.updateChannel(channelId, channelType, data); + expect(res, isNotNull); + expect(res.channel.cid, '$channelType:$channelId'); + expect(res.channel.extraData['name'], 'test-channel'); + + verify(() => api.channel.updateChannel(channelId, channelType, data)) + .called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.updateChannelPartial`', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + const set = { + 'name': 'Stream Team', + 'profile_image': 'test-profile-image', + }; + const unset = ['tag', 'last_name']; + + when(() => api.channel.updateChannelPartial(channelId, channelType, + set: set, unset: unset)) + .thenAnswer((invocation) async => PartialUpdateChannelResponse() + ..channel = ChannelModel( + id: channelId, + type: channelType, + extraData: {...set}, + )); + + final res = await client.updateChannelPartial( + channelId, + channelType, + set: set, + unset: unset, + ); + expect(res, isNotNull); + expect(res.channel.cid, '$channelType:$channelId'); + expect(res.channel.extraData, set); + + verify(() => api.channel.updateChannelPartial(channelId, channelType, + set: set, unset: unset)).called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.addDevice`', () async { + const id = 'test-device-id'; + const provider = PushProvider.firebase; + + when(() => api.device.addDevice(id, provider)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.addDevice(id, provider); + expect(res, isNotNull); + + verify(() => api.device.addDevice(id, provider)).called(1); + verifyNoMoreInteractions(api.device); + }); + + test('`.getDevices`', () async { + final devices = List.generate( + 3, + (index) => Device( + id: 'test-device-id-$index', + pushProvider: PushProvider.firebase.name, + ), + ); + + when(() => api.device.getDevices()) + .thenAnswer((_) async => ListDevicesResponse()..devices = devices); + + final res = await client.getDevices(); + expect(res, isNotNull); + expect(res.devices.length, devices.length); + + verify(() => api.device.getDevices()).called(1); + verifyNoMoreInteractions(api.device); + }); + + test('`.removeDevice`', () async { + const deviceId = 'test-device-id'; + + when(() => api.device.removeDevice(deviceId)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.removeDevice(deviceId); + expect(res, isNotNull); + + verify(() => api.device.removeDevice(deviceId)).called(1); + verifyNoMoreInteractions(api.device); + }); + + test('`.devToken`', () async { + const userId = 'test-user-id'; + + final token = client.devToken(userId); + + expect(token, isNotNull); + expect(token.userId, userId); + expect(token.authType, AuthType.jwt); + }); + + group('`.channel`', () { + test('should return back a new channel instance', () { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + const channelData = {'name': 'test-channel-name'}; + + final channel = client.channel( + channelType, + id: channelId, + extraData: channelData, + ); + + expect(channel, isNotNull); + expect(channel.type, channelType); + expect(channel.id, channelId); + expect(channel.cid, '$channelType:$channelId'); + expect(channel.extraData, channelData); + }); + + test('should return back in memory channel instance if available', + () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + const channelData = {'name': 'test-channel-name'}; + const channelCid = '$channelType:$channelId'; + + final channel = client.channel( + channelType, + id: channelId, + extraData: channelData, + ); + + final channelState = ChannelState( + channel: ChannelModel(cid: channelCid), + ); + + when(() => api.channel.queryChannel( + channelType, + channelId: channelId, + channelData: channelData, + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + )).thenAnswer((_) async => channelState); + + expectLater( + client.state.channelsStream.skip(1), + emitsInOrder([ + {channelCid: isCorrectChannelFor(channelState)} + ]), + ); + + await channel.watch(); + + final newChannel = client.channel(channelType, id: channelId); + expect(newChannel, channel); + + verify(() => api.channel.queryChannel( + channelType, + channelId: channelId, + channelData: channelData, + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + )).called(1); + }); + }); + + test('`.createChannel`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + const channelData = {'name': 'test-channel-name'}; + const channelCid = '$channelType:$channelId'; + + final channelState = ChannelState( + channel: ChannelModel(cid: channelCid, extraData: channelData), + ); + + when(() => api.channel.queryChannel( + channelType, + channelId: channelId, + channelData: channelData, + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + )).thenAnswer((_) async => channelState); + + final res = await client.createChannel( + channelType, + channelId: channelId, + channelData: channelData, + ); + + expect(res, isNotNull); + expect(res.channel, isNotNull); + final channel = res.channel!; + expect(channel.type, channelType); + expect(channel.id, channelId); + expect(channel.cid, '$channelType:$channelId'); + expect(channel.extraData, channelData); + + verify(() => api.channel.queryChannel( + channelType, + channelId: channelId, + channelData: channelData, + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + )).called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.watchChannel`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + const channelData = {'name': 'test-channel-name'}; + const channelCid = '$channelType:$channelId'; + + final channelState = ChannelState( + channel: ChannelModel(cid: channelCid, extraData: channelData), + ); + + when(() => api.channel.queryChannel( + channelType, + channelId: channelId, + channelData: channelData, + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + )).thenAnswer((_) async => channelState); + + final res = await client.watchChannel( + channelType, + channelId: channelId, + channelData: channelData, + ); + + expect(res, isNotNull); + expect(res.channel, isNotNull); + final channel = res.channel!; + expect(channel.type, channelType); + expect(channel.id, channelId); + expect(channel.cid, '$channelType:$channelId'); + expect(channel.extraData, channelData); + + verify(() => api.channel.queryChannel( + channelType, + channelId: channelId, + channelData: channelData, + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + )).called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.queryChannel`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + const channelData = {'name': 'test-channel-name'}; + const channelCid = '$channelType:$channelId'; + + final channelState = ChannelState( + channel: ChannelModel(cid: channelCid, extraData: channelData), + ); + + when(() => api.channel.queryChannel( + channelType, + channelId: channelId, + channelData: channelData, + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + )).thenAnswer((_) async => channelState); + + final res = await client.queryChannel( + channelType, + channelId: channelId, + channelData: channelData, + ); + + expect(res, isNotNull); + expect(res.channel, isNotNull); + final channel = res.channel!; + expect(channel.type, channelType); + expect(channel.id, channelId); + expect(channel.cid, '$channelType:$channelId'); + expect(channel.extraData, channelData); + + verify(() => api.channel.queryChannel( + channelType, + channelId: channelId, + channelData: channelData, + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + )).called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.queryMembers`', () async { + const channelType = 'test-channel-type'; + + final members = List.generate( + 3, + (index) => Member(userId: 'test-user-id-$index'), + ); + + when(() => api.general.queryMembers(channelType)).thenAnswer( + (_) async => QueryMembersResponse()..members = members, + ); + + final res = await client.queryMembers(channelType); + expect(res, isNotNull); + expect(res.members.length, members.length); + + verify(() => api.general.queryMembers(channelType)).called(1); + verifyNoMoreInteractions(api.general); + }); + + test('`.hideChannel`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + + when(() => api.channel.hideChannel(channelId, channelType)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.hideChannel(channelId, channelType); + + expect(res, isNotNull); + + verify(() => api.channel.hideChannel(channelId, channelType)).called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.showChannel`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + + when(() => api.channel.showChannel(channelId, channelType)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.showChannel(channelId, channelType); + + expect(res, isNotNull); + + verify(() => api.channel.showChannel(channelId, channelType)).called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.deleteChannel`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + + when(() => api.channel.deleteChannel(channelId, channelType)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.deleteChannel(channelId, channelType); + + expect(res, isNotNull); + + verify(() => api.channel.deleteChannel(channelId, channelType)).called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.truncateChannel`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + + when(() => api.channel.truncateChannel(channelId, channelType)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.truncateChannel(channelId, channelType); + + expect(res, isNotNull); + + verify( + () => api.channel.truncateChannel(channelId, channelType), + ).called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.muteChannel`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + const channelCid = '$channelType:$channelId'; + + when(() => api.moderation.muteChannel(channelCid)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.muteChannel(channelCid); + + expect(res, isNotNull); + + verify(() => api.moderation.muteChannel(channelCid)).called(1); + verifyNoMoreInteractions(api.moderation); + }); + + test('`.unmuteChannel`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + const channelCid = '$channelType:$channelId'; + + when(() => api.moderation.unmuteChannel(channelCid)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.unmuteChannel(channelCid); + + expect(res, isNotNull); + + verify(() => api.moderation.unmuteChannel(channelCid)).called(1); + verifyNoMoreInteractions(api.moderation); + }); + + test('`.acceptChannelInvite`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + const channelCid = '$channelType:$channelId'; + + when(() => api.channel.acceptChannelInvite(channelId, channelType)) + .thenAnswer((_) async => + AcceptInviteResponse()..channel = ChannelModel(cid: channelCid)); + + final res = await client.acceptChannelInvite(channelId, channelType); + expect(res, isNotNull); + expect(res.channel.cid, channelCid); + + verify(() => api.channel.acceptChannelInvite(channelId, channelType)) + .called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.rejectChannelInvite`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + const channelCid = '$channelType:$channelId'; + + when(() => api.channel.rejectChannelInvite(channelId, channelType)) + .thenAnswer((_) async => + RejectInviteResponse()..channel = ChannelModel(cid: channelCid)); + + final res = await client.rejectChannelInvite(channelId, channelType); + expect(res, isNotNull); + expect(res.channel.cid, channelCid); + + verify(() => api.channel.rejectChannelInvite(channelId, channelType)) + .called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.addChannelMembers`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + const channelCid = '$channelType:$channelId'; + + final members = List.generate( + 3, + (index) => Member(userId: 'test-user-id-$index'), + ); + + final memberIds = members.map((e) => e.userId!).toList(growable: false); + + when(() => api.channel.addMembers(channelId, channelType, memberIds)) + .thenAnswer((_) async => AddMembersResponse() + ..channel = ChannelModel(cid: channelCid) + ..members = members); + + final res = await client.addChannelMembers( + channelId, + channelType, + memberIds, + ); + + expect(res, isNotNull); + expect(res.channel.cid, channelCid); + expect(res.members.length, memberIds.length); + + verify( + () => api.channel.addMembers(channelId, channelType, memberIds), + ).called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.removeChannelMembers`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + const channelCid = '$channelType:$channelId'; + + final members = List.generate( + 3, + (index) => Member(userId: 'test-user-id-$index'), + ); + + final memberIds = members.map((e) => e.userId!).toList(growable: false); + + when(() => api.channel.removeMembers(channelId, channelType, memberIds)) + .thenAnswer((_) async => RemoveMembersResponse() + ..channel = ChannelModel(cid: channelCid) + ..members = members); + + final res = await client.removeChannelMembers( + channelId, + channelType, + memberIds, + ); + + expect(res, isNotNull); + expect(res.channel.cid, channelCid); + expect(res.members.length, memberIds.length); + + verify( + () => api.channel.removeMembers(channelId, channelType, memberIds), + ).called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.inviteChannelMembers`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + const channelCid = '$channelType:$channelId'; + + final members = List.generate( + 3, + (index) => Member(userId: 'test-user-id-$index'), + ); + + final memberIds = members.map((e) => e.userId!).toList(growable: false); + + when(() => api.channel + .inviteChannelMembers(channelId, channelType, memberIds)) + .thenAnswer((_) async => InviteMembersResponse() + ..channel = ChannelModel(cid: channelCid) + ..members = members); + + final res = await client.inviteChannelMembers( + channelId, + channelType, + memberIds, + ); + + expect(res, isNotNull); + expect(res.channel.cid, channelCid); + expect(res.members.length, memberIds.length); + + verify(() => api.channel + .inviteChannelMembers(channelId, channelType, memberIds)).called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.stopChannelWatching`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + + when(() => api.channel.stopWatching(channelId, channelType)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.stopChannelWatching(channelId, channelType); + expect(res, isNotNull); + + verify(() => api.channel.stopWatching(channelId, channelType)).called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.sendAction`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + const messageId = 'test-message-id'; + const formData = {'key': 'value'}; + + when(() => api.message + .sendAction(channelId, channelType, messageId, formData)) + .thenAnswer((_) async => SendActionResponse()); + + final res = await client.sendAction( + channelId, + channelType, + messageId, + formData, + ); + + expect(res, isNotNull); + + verify(() => api.message + .sendAction(channelId, channelType, messageId, formData)).called(1); + verifyNoMoreInteractions(api.message); + }); + + test('`.markChannelRead`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + + when(() => api.channel.markRead(channelId, channelType)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.markChannelRead(channelId, channelType); + + expect(res, isNotNull); + + verify(() => api.channel.markRead(channelId, channelType)).called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.updateUser`', () async { + final user = User( + id: 'test-user-id', + extraData: const {'name': 'test-user'}, + ); + + when(() => api.user.updateUsers([user])).thenAnswer( + (_) async => UpdateUsersResponse()..users = {user.id: user}); + + final res = await client.updateUser(user); + + expect(res, isNotNull); + expect(res.users, {user.id: user}); + + verify(() => api.user.updateUsers([user])).called(1); + verifyNoMoreInteractions(api.user); + }); + + test('`.banUser`', () async { + const userId = 'test-user-id'; + + when(() => api.moderation.banUser(userId, options: any(named: 'options'))) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.banUser(userId); + + expect(res, isNotNull); + + verify( + () => api.moderation.banUser(userId, options: any(named: 'options')), + ).called(1); + verifyNoMoreInteractions(api.moderation); + }); + + test('`.unbanUser`', () async { + const userId = 'test-user-id'; + + when(() => + api.moderation.unbanUser(userId, options: any(named: 'options'))) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.unbanUser(userId); + + expect(res, isNotNull); + + verify( + () => api.moderation.unbanUser(userId, options: any(named: 'options')), + ).called(1); + verifyNoMoreInteractions(api.moderation); + }); + + test('`.shadowBan`', () async { + const userId = 'test-user-id'; + + when(() => api.moderation.banUser(userId, options: {'shadow': true})) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.shadowBan(userId); + + expect(res, isNotNull); + + verify( + () => api.moderation.banUser(userId, options: {'shadow': true}), + ).called(1); + verifyNoMoreInteractions(api.moderation); + }); + + test('`.removeShadowBan`', () async { + const userId = 'test-user-id'; + + when(() => api.moderation.unbanUser(userId, options: {'shadow': true})) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.removeShadowBan(userId); + + expect(res, isNotNull); + + verify( + () => api.moderation.unbanUser(userId, options: {'shadow': true}), + ).called(1); + verifyNoMoreInteractions(api.moderation); + }); + + test('`.muteUser`', () async { + const userId = 'test-user-id'; + + when(() => api.moderation.muteUser(userId)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.muteUser(userId); + + expect(res, isNotNull); + + verify(() => api.moderation.muteUser(userId)).called(1); + verifyNoMoreInteractions(api.moderation); + }); + + test('`.unmuteUser`', () async { + const userId = 'test-user-id'; + + when(() => api.moderation.unmuteUser(userId)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.unmuteUser(userId); + + expect(res, isNotNull); + + verify(() => api.moderation.unmuteUser(userId)).called(1); + verifyNoMoreInteractions(api.moderation); + }); + + test('`.flagMessage`', () async { + const messageId = 'test-message-id'; + + when(() => api.moderation.flagMessage(messageId)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.flagMessage(messageId); + + expect(res, isNotNull); + + verify(() => api.moderation.flagMessage(messageId)).called(1); + verifyNoMoreInteractions(api.moderation); + }); + + test('`.unflagMessage`', () async { + const messageId = 'test-message-id'; + + when(() => api.moderation.unflagMessage(messageId)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.unflagMessage(messageId); + + expect(res, isNotNull); + + verify(() => api.moderation.unflagMessage(messageId)).called(1); + verifyNoMoreInteractions(api.moderation); + }); + + test('`.flagUser`', () async { + const userId = 'test-message-id'; + + when(() => api.moderation.flagUser(userId)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.flagUser(userId); + + expect(res, isNotNull); + + verify(() => api.moderation.flagUser(userId)).called(1); + verifyNoMoreInteractions(api.moderation); + }); + + test('`.unflagUser`', () async { + const userId = 'test-message-id'; + + when(() => api.moderation.unflagUser(userId)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.unflagUser(userId); + + expect(res, isNotNull); + + verify(() => api.moderation.unflagUser(userId)).called(1); + verifyNoMoreInteractions(api.moderation); + }); + + test('`.markAllRead`', () async { + when(() => api.channel.markAllRead()) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.markAllRead(); + expect(res, isNotNull); + + verify(() => api.channel.markAllRead()).called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.sendEvent`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + final event = Event(type: EventType.any); + + when( + () => api.channel.sendEvent( + channelId, + channelType, + any(that: isSameEventAs(event)), + ), + ).thenAnswer((_) async => EmptyResponse()); + + final res = await client.sendEvent(channelId, channelType, event); + expect(res, isNotNull); + + verify(() => api.channel.sendEvent( + channelId, + channelType, + any(that: isSameEventAs(event)), + )).called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.sendReaction`', () async { + const messageId = 'test-message-id'; + const reactionType = 'like'; + + when(() => api.message.sendReaction(messageId, reactionType)) + .thenAnswer((_) async => SendReactionResponse() + ..message = Message(id: messageId) + ..reaction = Reaction(type: reactionType, messageId: messageId)); + + final res = await client.sendReaction(messageId, reactionType); + expect(res, isNotNull); + expect(res.message.id, messageId); + expect(res.reaction.type, reactionType); + expect(res.reaction.messageId, messageId); + + verify(() => api.message.sendReaction(messageId, reactionType)).called(1); + verifyNoMoreInteractions(api.message); + }); + + test('`.deleteReaction`', () async { + const messageId = 'test-message-id'; + const reactionType = 'like'; + + when(() => api.message.deleteReaction(messageId, reactionType)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.deleteReaction(messageId, reactionType); + expect(res, isNotNull); + + verify( + () => api.message.deleteReaction(messageId, reactionType), + ).called(1); + verifyNoMoreInteractions(api.message); + }); + + test('`.sendMessage`', () async { + final message = Message(id: 'test-message-id'); + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + + when(() => api.message.sendMessage( + channelId, channelType, any(that: isSameMessageAs(message)))) + .thenAnswer((_) async => SendMessageResponse()..message = message); + + final res = await client.sendMessage(message, channelId, channelType); + expect(res, isNotNull); + expect(res.message, isSameMessageAs(message)); + + verify(() => api.message.sendMessage( + channelId, + channelType, + any(that: isSameMessageAs(message)), + )).called(1); + verifyNoMoreInteractions(api.message); + }); + + test('`.getReplies`', () async { + const parentId = 'test-parent-id'; + + final messages = List.generate( + 3, + (index) => Message(id: 'test-message-id-$index'), + ); + + when(() => api.message.getReplies(parentId)) + .thenAnswer((_) async => QueryRepliesResponse()..messages = messages); + + final res = await client.getReplies(parentId); + expect(res, isNotNull); + expect(res.messages.length, messages.length); + + verify(() => api.message.getReplies(parentId)).called(1); + verifyNoMoreInteractions(api.message); + }); + + test('`.getReactions`', () async { + const messageId = 'test-parent-id'; + + final reactions = List.generate( + 3, + (index) => Reaction( + type: 'test-reactions-type-$index', + messageId: messageId, + ), + ); + + when(() => api.message.getReactions(messageId)).thenAnswer( + (_) async => QueryReactionsResponse()..reactions = reactions); + + final res = await client.getReactions(messageId); + expect(res, isNotNull); + expect(res.reactions.length, reactions.length); + expect(res.reactions.every((it) => it.messageId == messageId), isTrue); + + verify(() => api.message.getReactions(messageId)).called(1); + verifyNoMoreInteractions(api.message); + }); + + test('`.updateMessage`', () async { + final message = Message(id: 'test-message-id', text: 'Hello!'); + + when(() => api.message.updateMessage(any(that: isSameMessageAs(message)))) + .thenAnswer((_) async => UpdateMessageResponse()..message = message); + + final res = await client.updateMessage(message); + expect(res, isNotNull); + expect(res.message, isSameMessageAs(message)); + + verify( + () => api.message.updateMessage(any(that: isSameMessageAs(message))), + ).called(1); + verifyNoMoreInteractions(api.message); + }); + + test('`.deleteMessage`', () async { + const messageId = 'test-message-id'; + + when(() => api.message.deleteMessage(messageId)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.deleteMessage(messageId); + expect(res, isNotNull); + + verify(() => api.message.deleteMessage(messageId)).called(1); + verifyNoMoreInteractions(api.message); + }); + + test('`.getMessage`', () async { + const messageId = 'test-message-id'; + final message = Message(id: messageId); + + when(() => api.message.getMessage(messageId)) + .thenAnswer((_) async => GetMessageResponse()..message = message); + + final res = await client.getMessage(messageId); + expect(res, isNotNull); + expect(res.message.id, messageId); + + verify(() => api.message.getMessage(messageId)).called(1); + verifyNoMoreInteractions(api.message); + }); + + test('`.getMessagesById`', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + const messageIds = ['test-message-id']; + + final messages = messageIds.map((id) => Message(id: id)).toList(); + + when( + () => api.message.getMessagesById(channelId, channelType, messageIds), + ).thenAnswer((_) async => GetMessagesByIdResponse()..messages = messages); + + final res = await client.getMessagesById( + channelId, + channelType, + messageIds, + ); + expect(res, isNotNull); + expect(res.messages.length, messageIds.length); + + verify( + () => api.message.getMessagesById(channelId, channelType, messageIds), + ).called(1); + verifyNoMoreInteractions(api.message); + }); + + test('`.translateMessage`', () async { + const messageId = 'test-message-id'; + const language = 'hi'; // Hindi + const translatedMessageText = 'नमसà¥à¤¤à¥‡'; + final translatedMessage = TranslatedMessage(const { + language: translatedMessageText, + }); + + when(() => api.message.translateMessage(messageId, language)).thenAnswer( + (_) async => TranslateMessageResponse()..message = translatedMessage, + ); + + final res = await client.translateMessage(messageId, language); + + expect(res, isNotNull); + expect(res.message.i18n, translatedMessage.i18n); + + verify(() => api.message.translateMessage(messageId, language)).called(1); + verifyNoMoreInteractions(api.message); + }); + + test('`.partialUpdateMessage`', () async { + const messageId = 'test-message-id'; + final message = Message(id: messageId); + + const set = {'text': 'Update Message text'}; + const unset = ['pinExpires']; + + final updateMessageResponse = UpdateMessageResponse() + ..message = message.copyWith(text: set['text'], pinExpires: null); + + when(() => api.message.partialUpdateMessage( + message.id, + set: set, + unset: unset, + )).thenAnswer((_) async => updateMessageResponse); + + final res = await client.partialUpdateMessage( + messageId, + set: set, + unset: unset, + ); + + expect(res, isNotNull); + expect(res.message.id, message.id); + expect(res.message.id, message.id); + expect(res.message.text, set['text']); + expect(res.message.pinExpires, isNull); + + verify(() => api.message.partialUpdateMessage( + message.id, + set: set, + unset: unset, + )).called(1); + verifyNoMoreInteractions(api.message); + }); + + group('`.pinMessage`', () { + test('should work fine without passing timeoutOrExpirationDate', + () async { + const messageId = 'test-message-id'; + final message = Message(id: messageId); + + when(() => api.message.partialUpdateMessage( + messageId, + set: any(named: 'set'), + unset: any(named: 'unset'), + )).thenAnswer((_) async => UpdateMessageResponse() + ..message = message.copyWith( + pinned: true, + pinExpires: null, + status: MessageSendingStatus.sent, + )); + + final res = await client.pinMessage(messageId); + + expect(res, isNotNull); + expect(res.message.pinned, isTrue); + expect(res.message.pinExpires, isNull); + + verify(() => api.message.partialUpdateMessage( + messageId, + set: any(named: 'set'), + unset: any(named: 'unset'), + )).called(1); + verifyNoMoreInteractions(api.message); + }); + + test( + 'should work fine if passed timeoutOrExpirationDate as num(seconds)', + () async { + const messageId = 'test-message-id'; + final message = Message(id: messageId); + const timeoutOrExpirationDate = 300; // 300 seconds + + when(() => api.message.partialUpdateMessage( + message.id, + set: any(named: 'set'), + unset: any(named: 'unset'), + )).thenAnswer((_) async => UpdateMessageResponse() + ..message = message.copyWith( + pinned: true, + pinExpires: DateTime.now().add( + const Duration(seconds: timeoutOrExpirationDate), + ), + status: MessageSendingStatus.sent, + )); + + final res = await client.pinMessage( + messageId, + timeoutOrExpirationDate: timeoutOrExpirationDate, + ); + + expect(res, isNotNull); + expect(res.message.pinned, isTrue); + expect(res.message.pinExpires, isNotNull); + + verify(() => api.message.partialUpdateMessage( + messageId, + set: any(named: 'set'), + unset: any(named: 'unset'), + )).called(1); + verifyNoMoreInteractions(api.message); + }, + ); + + test( + 'should work fine if passed timeoutOrExpirationDate as DateTime', + () async { + const messageId = 'test-message-id'; + final message = Message(id: messageId); + final timeoutOrExpirationDate = + DateTime.now().add(const Duration(days: 3)); // 3 days + + when(() => api.message.partialUpdateMessage( + messageId, + set: any(named: 'set'), + unset: any(named: 'unset'), + )).thenAnswer((_) async => UpdateMessageResponse() + ..message = message.copyWith( + pinned: true, + pinExpires: timeoutOrExpirationDate, + status: MessageSendingStatus.sent, + )); + + final res = await client.pinMessage( + messageId, + timeoutOrExpirationDate: timeoutOrExpirationDate, + ); + + expect(res, isNotNull); + expect(res.message.pinned, isTrue); + expect(res.message.pinExpires, isNotNull); + expect(res.message.pinExpires, timeoutOrExpirationDate.toUtc()); + + verify(() => api.message.partialUpdateMessage( + messageId, + set: any(named: 'set'), + unset: any(named: 'unset'), + )).called(1); + verifyNoMoreInteractions(api.message); + }, + ); + + test( + 'should throw if invalid timeoutOrExpirationDate is passed', + () async { + const messageId = 'test-message-id'; + const timeoutOrExpirationDate = 'invalid-value'; + + try { + await client.pinMessage( + messageId, + timeoutOrExpirationDate: timeoutOrExpirationDate, + ); + } catch (e) { + expect(e, isA()); + } + }, + ); + }); + + test('`.unpinMessage`', () async { + const messageId = 'test-message-id'; + final message = Message(id: messageId, pinned: true); + + when(() => api.message.partialUpdateMessage( + messageId, + set: {'pinned': false}, + )).thenAnswer((_) async => UpdateMessageResponse() + ..message = message.copyWith( + pinned: false, + status: MessageSendingStatus.sent, + )); + + final res = await client.unpinMessage(messageId); + + expect(res, isNotNull); + expect(res.message.pinned, isFalse); + + verify(() => api.message.partialUpdateMessage( + messageId, + set: {'pinned': false}, + )).called(1); + verifyNoMoreInteractions(api.message); + }); + }); +} diff --git a/packages/stream_chat/test/src/core/api/message_api_test.dart b/packages/stream_chat/test/src/core/api/message_api_test.dart index 041c18f5..d7fa93cd 100644 --- a/packages/stream_chat/test/src/core/api/message_api_test.dart +++ b/packages/stream_chat/test/src/core/api/message_api_test.dart @@ -252,7 +252,7 @@ void main() { 'reactions': [...reactions.map((it) => it.toJson())] })); - final res = await messageApi.getReactions(messageId, options); + final res = await messageApi.getReactions(messageId, options: options); expect(res, isNotNull); expect(res.reactions.length, reactions.length); @@ -311,7 +311,7 @@ void main() { 'messages': [...messages.map((it) => it.toJson())] })); - final res = await messageApi.getReplies(parentId, options); + final res = await messageApi.getReplies(parentId, options: options); expect(res, isNotNull); expect(res.messages.length, messages.length); diff --git a/packages/stream_chat/test/src/fakes.dart b/packages/stream_chat/test/src/fakes.dart index 74280a0b..15d5991c 100644 --- a/packages/stream_chat/test/src/fakes.dart +++ b/packages/stream_chat/test/src/fakes.dart @@ -1,5 +1,8 @@ +import 'dart:async'; + import 'package:dio/dio.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/src/core/api/channel_api.dart'; import 'package:stream_chat/src/core/api/device_api.dart'; import 'package:stream_chat/src/core/api/general_api.dart'; @@ -10,6 +13,7 @@ import 'package:stream_chat/src/core/api/user_api.dart'; import 'package:stream_chat/src/core/api/guest_api.dart'; import 'package:stream_chat/src/core/http/token.dart'; import 'package:stream_chat/src/core/http/token_manager.dart'; +import 'package:stream_chat/src/ws/websocket.dart'; import 'package:stream_chat/stream_chat.dart'; import 'mocks.dart'; @@ -96,3 +100,88 @@ class FakeMessage extends Fake implements Message {} class FakeAttachmentFile extends Fake implements AttachmentFile {} class FakeEvent extends Fake implements Event {} + +class FakeUser extends Fake implements User {} + +class FakeWebSocket extends Fake implements WebSocket { + BehaviorSubject? _connectionStatusController; + + BehaviorSubject get connectionStatusController => + _connectionStatusController ??= + BehaviorSubject.seeded(ConnectionStatus.disconnected); + + set connectionStatus(ConnectionStatus value) { + connectionStatusController.add(value); + } + + @override + ConnectionStatus get connectionStatus => connectionStatusController.value; + + @override + Stream get connectionStatusStream => + connectionStatusController.stream; + + @override + Completer? connectionCompleter; + + @override + Future connect(User user) async { + connectionStatus = ConnectionStatus.connecting; + final event = Event( + type: EventType.healthCheck, + connectionId: 'fake-connection-id', + me: OwnUser.fromUser(user), + ); + connectionCompleter = Completer()..complete(event); + connectionStatus = ConnectionStatus.connected; + return connectionCompleter!.future; + } + + @override + void disconnect() { + connectionStatus = ConnectionStatus.disconnected; + connectionCompleter = null; + _connectionStatusController?.close(); + _connectionStatusController = null; + } +} + +class FakeWebSocketWithConnectionError extends Fake implements WebSocket { + BehaviorSubject? _connectionStatusController; + + BehaviorSubject get connectionStatusController => + _connectionStatusController ??= + BehaviorSubject.seeded(ConnectionStatus.disconnected); + + set connectionStatus(ConnectionStatus value) { + connectionStatusController.add(value); + } + + @override + ConnectionStatus get connectionStatus => connectionStatusController.value; + + @override + Stream get connectionStatusStream => + connectionStatusController.stream; + + @override + Completer? connectionCompleter; + + @override + Future connect(User user) async { + connectionStatus = ConnectionStatus.connecting; + const error = StreamWebSocketError('Error Connecting'); + connectionCompleter = Completer()..completeError(error); + return connectionCompleter!.future; + } + + @override + void disconnect() { + connectionStatus = ConnectionStatus.disconnected; + connectionCompleter = null; + _connectionStatusController?.close(); + _connectionStatusController = null; + } +} + +class FakeChannelState extends Fake implements ChannelState {} diff --git a/packages/stream_chat/test/src/matchers.dart b/packages/stream_chat/test/src/matchers.dart index 967f3c21..c039b416 100644 --- a/packages/stream_chat/test/src/matchers.dart +++ b/packages/stream_chat/test/src/matchers.dart @@ -1,7 +1,11 @@ import 'package:collection/collection.dart'; import 'package:dio/dio.dart' show MultipartFile; +import 'package:stream_chat/src/client/channel.dart'; +import 'package:stream_chat/src/core/models/channel_model.dart'; +import 'package:stream_chat/src/core/models/channel_state.dart'; import 'package:stream_chat/src/core/models/event.dart'; import 'package:stream_chat/src/core/models/message.dart'; +import 'package:stream_chat/src/core/models/user.dart'; import 'package:test/test.dart'; Matcher isSameMultipartFileAs(MultipartFile targetFile) => @@ -96,3 +100,35 @@ class _IsSameMessageAs extends Matcher { return matches; } } + +Matcher isSameUserAs(User targetUser) => _IsSameUserAs(targetUser: targetUser); + +class _IsSameUserAs extends Matcher { + const _IsSameUserAs({required this.targetUser}); + + final User targetUser; + + @override + Description describe(Description description) => + description.add('is same user as $targetUser'); + + @override + bool matches(covariant User user, Map matchState) => user.id == targetUser.id; +} + +Matcher isCorrectChannelFor(ChannelState channelState) => + _IsCorrectChannelFor(channelState: channelState); + +class _IsCorrectChannelFor extends Matcher { + const _IsCorrectChannelFor({required this.channelState}); + + final ChannelState channelState; + + @override + Description describe(Description description) => + description.add('is correct channel for $channelState'); + + @override + bool matches(covariant Channel channel, Map matchState) => + channel.cid == channelState.channel?.cid; +} diff --git a/packages/stream_chat/test/src/mocks.dart b/packages/stream_chat/test/src/mocks.dart index 31e768e8..528c8a91 100644 --- a/packages/stream_chat/test/src/mocks.dart +++ b/packages/stream_chat/test/src/mocks.dart @@ -18,6 +18,7 @@ import 'package:stream_chat/src/core/http/token_manager.dart'; import 'package:stream_chat/src/core/models/channel_config.dart'; import 'package:stream_chat/src/core/models/channel_model.dart'; import 'package:stream_chat/src/db/chat_persistence_client.dart'; +import 'package:stream_chat/src/ws/websocket.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import 'db/chat_persistence_client_test.dart'; @@ -66,7 +67,13 @@ class MockGeneralApi extends Mock implements GeneralApi {} class MockAttachmentFileUploader extends Mock implements AttachmentFileUploader {} -class MockPersistenceClient extends Mock implements ChatPersistenceClient {} +class MockPersistenceClient extends Mock implements ChatPersistenceClient { + @override + Future connect(String userId) => Future.value(); + + @override + Future disconnect({bool flush = false}) => Future.value(); +} class MockStreamChatClient extends Mock implements StreamChatClient { @override @@ -105,3 +112,5 @@ class MockRetryQueueChannel extends Mock implements Channel { @override StreamChatClient get client => _client ??= MockStreamChatClient(); } + +class MockWebSocket extends Mock implements WebSocket {} diff --git a/packages/stream_chat/test/src/utils.dart b/packages/stream_chat/test/src/utils.dart index b17e35a9..91a61204 100644 --- a/packages/stream_chat/test/src/utils.dart +++ b/packages/stream_chat/test/src/utils.dart @@ -13,3 +13,12 @@ Directory get currentDirectory { } return directory; } + +// Extension function to convert int into durations +extension IntX on num { + Duration toDuration() => Duration(milliseconds: toInt()); +} + +// Top level util function to delay the code execution +Future delay(num milliseconds) => + Future.delayed(Duration(milliseconds: milliseconds.toInt())); diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index aae9d193..976aacb0 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -791,7 +791,7 @@ class _MessageListViewState extends State { final channel = streamChannel.channel; if (_upToDate && channel.config?.readEvents == true && - channel.state!.unreadCount! > 0) { + channel.state!.unreadCount > 0) { streamChannel.channel.markRead(); } } diff --git a/packages/stream_chat_flutter_core/lib/src/stream_channel.dart b/packages/stream_chat_flutter_core/lib/src/stream_channel.dart index 20304cdd..1dcf9c3c 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_channel.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel.dart @@ -174,7 +174,7 @@ class StreamChannelState extends State { try { final response = await channel.getReplies( parentId, - PaginationParams( + options: PaginationParams( lessThan: message?.id, limit: limit, ), From 2b80611cd3f6fe70ad06fed4b4916445b90413aa Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 17 Jun 2021 12:52:48 +0530 Subject: [PATCH 046/150] fix tests Signed-off-by: Sahil Kumar --- .../test/src/back_button_test.dart | 3 +- .../test/src/channel_header_test.dart | 4 ++ .../src/goldens/deleted_message_custom.png | Bin 2936 -> 2334 bytes .../test/src/goldens/deleted_message_dark.png | Bin 2785 -> 2262 bytes .../src/goldens/deleted_message_light.png | Bin 2750 -> 2161 bytes .../test/src/goldens/reaction_bubble_2.png | Bin 2920 -> 2937 bytes .../test/src/goldens/system_message_dark.png | Bin 1812 -> 1679 bytes .../test/src/goldens/system_message_light.png | Bin 1866 -> 1746 bytes .../test/src/message_action_modal_test.dart | 18 +++----- .../test/src/thread_header_test.dart | 1 + .../test/message_list_core_test.dart | 4 +- .../test/stream_channel_test.dart | 40 +++++++++++++----- .../test/stream_chat_core_test.dart | 26 ++++++------ 13 files changed, 59 insertions(+), 37 deletions(-) diff --git a/packages/stream_chat_flutter/test/src/back_button_test.dart b/packages/stream_chat_flutter/test/src/back_button_test.dart index 3fae1ace..efb889ac 100644 --- a/packages/stream_chat_flutter/test/src/back_button_test.dart +++ b/packages/stream_chat_flutter/test/src/back_button_test.dart @@ -118,8 +118,9 @@ void main() { final clientState = MockClientState(); when(() => client.state).thenReturn(clientState); + when(() => clientState.totalUnreadCount).thenAnswer((_) => 0); when(() => clientState.totalUnreadCountStream) - .thenAnswer((i) => Stream.value(0)); + .thenAnswer((_) => Stream.value(0)); await tester.pumpWidget( MaterialApp( diff --git a/packages/stream_chat_flutter/test/src/channel_header_test.dart b/packages/stream_chat_flutter/test/src/channel_header_test.dart index 9f6e981d..87f3d028 100644 --- a/packages/stream_chat_flutter/test/src/channel_header_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_header_test.dart @@ -34,6 +34,7 @@ void main() { .thenAnswer((_) => Stream.value(ConnectionStatus.connected)); when(() => channelState.unreadCountStream) .thenAnswer((i) => Stream.value(1)); + when(() => clientState.totalUnreadCount).thenAnswer((i) => 1); when(() => clientState.totalUnreadCountStream) .thenAnswer((i) => Stream.value(1)); when(() => channelState.membersStream).thenAnswer((i) => Stream.value([ @@ -109,6 +110,7 @@ void main() { .thenAnswer((_) => Stream.value(ConnectionStatus.disconnected)); when(() => client.wsConnectionStatus) .thenReturn(ConnectionStatus.disconnected); + when(() => clientState.totalUnreadCount).thenAnswer((i) => 1); when(() => clientState.totalUnreadCountStream) .thenAnswer((i) => Stream.value(1)); @@ -171,6 +173,7 @@ void main() { ]); when(() => client.wsConnectionStatusStream) .thenAnswer((_) => Stream.value(ConnectionStatus.connecting)); + when(() => clientState.totalUnreadCount).thenAnswer((i) => 1); when(() => clientState.totalUnreadCountStream) .thenAnswer((i) => Stream.value(1)); @@ -375,6 +378,7 @@ void main() { ]); when(() => client.wsConnectionStatusStream) .thenAnswer((_) => Stream.value(ConnectionStatus.connecting)); + when(() => clientState.totalUnreadCount).thenAnswer((i) => 1); when(() => clientState.totalUnreadCountStream) .thenAnswer((i) => Stream.value(1)); diff --git a/packages/stream_chat_flutter/test/src/goldens/deleted_message_custom.png b/packages/stream_chat_flutter/test/src/goldens/deleted_message_custom.png index c464bc01de48fcef06c538bd284ed284370f55aa..693db3c78ee724f41d6376bd5c7ba1eb1385750d 100644 GIT binary patch delta 1816 zcmV+z2j}?s7M>E2Kz|6xNkl`t918 zW=r}^wyN@nb8~`9;(r3==Ah7?x7$?0|NsZ9Uax+;9%TC zktE#9kTCG-`LqHE}wSQVVIy#DV46z2%Xf)zHJo>=y?ryBdk1ddslM`;p(behc zsZLH#Vmp3pfdoMi*P+p8;+!i4f}VFL0|EL0fdB>qxkZ+jmsPD+yY3eRffg1P)ND3y zU{u#1S1-CXfh;U6XmN4z%56aqXmN4zx7%-vot+)!a=EVi`MxjboPW~k^bL&a`s3$&(ntwA8uq^z3A2i629OU zmkqw}YinyunM|f{U!?2!lS%LU{v#D~qEa_~C)NZ#mJw4s^+;tmQzkj(ESCE~Z9c8mwWipwL zKs?V=GMSWfu4BzzsZ`W#HudJs8_mtlMf*zs1u#85t@-(R<@0&1uC8|7)^4}8w6vsb zHmmvhdCktwYI}P-x}w$Vb(PCyjgOCOZf;H!6BAlrUylN))oSuQPZJXpnxCInE|*iK zQn~W^R;yLj>wk5XN+r$B%|&kx;bU3P@8bQ(@Al2*a+;l;l_bs0%|&rokKJut{iyE) z3D>ykbXw_jI+~EpW>Z^RTPl@Gs@Ln$WxiIc$@l%}UC%kEY&IK(75@3~*M&ksGcz+? zw@K2>%!~?!f+R)nsG(}Ls$#LILZKk%oN~Eb$FajJTz@#3a=F}<|B$oYZfkvgJ=!;X z4C!p0-|hLGU-DdM-%KVGT>-8AyN#<~_5Bj-{EO(Ei(cGHrJ`c7sAMuJ-}j@5Td&vE zYPBwXqE##wC7n(BaH&)-mzB@wH8C;Kb(?fHfy(7_#|H)3Y*x)?Q>|7j8iRBeTMz`% zq?V+mrGKT4F0Zu_2}+}8fx#?`NGMIh(jsn*uk z2z94OG_#ii<+32 zkaJGeYBjpzgu#UG#Nn~RuzlZGKA-QnFZi8yyMNvBI2X@>_53d0e^~pT-+uAeSM6V| zR?TtstG-`ih4&xQf6JQiQZHRzbB~RUX=7s}y3*8YwdgOgn$4y>&+AxXIOmj1CcAD6 zf*?9Ztya_4)>bsGaN=xiY)CqLCth1y>j-;$dplY#`o6D9rE=v**3LOK8ja|fp67L( z1ApuJ{pq;~fXdKr5-Nx0g?!SNk{(o^C_~__JKY#xGtH1`ZGUY|o12>&85z;nuV0nV=ao*UH8eD&4}bbNfQ7cX8Yl}cTD&gpbo-@bj*r%#{s z`SWKv=hSRAqj!4i`E71)>fytO*E$!!e*MyyFJH92zOLubpKEAn$lSl%xciP*n254~?-U?A4->uQa4hK`Po#(8-3fw<>Ltbq&;4r+LKIL;xUhYb%8 z$NnYOv9EDPMn;GPmkbXN$M(t9d9i=Hd+6xsNV~hceB0t3mq;X($z)=`a)f#&kWly^ z=s7XA-_lTcw*IrhN-0000Z9#;F@OiS%YeXV+$nc91r*^VQS@vv_F`3$EE6}V1C`qut@p?JNJ z$0L!g9{h?k#YU60Y;A1=f`bj!U8(CSbpZhZXn)D(!r;oem;FV3B9Wl1OgwEC{W`%; z*M4L9-J)tk{-CkLaCC4Gb?V@W{olm+u6~K#mC^LmK!1NsteRCre0=5@p$guVSMbUYs$q-f%ZG-rIE}w&TMxC}W2)3i-8&lD zd*O7qlXN$oDmz-M5#5dL9lcRI^L6ZolGyR)O*W1|CI^0q_6rOQ+r=_J)lB`Dt2D%x+DeQ)Z^Rse!C1m#6@XQ~Vu17_A!eFrPM0gwy4L{%$85!A1 zEGsKR*xIVfG*3ZS|1I?M)vHYkYh8+7z-1%t;BSGUtiA7)Z0s{j0|XRB!S>gwXv~z z?CAB}S2;m()XsE)seRrvIy(2!tODoh?(QA{MCoh2T@pIZRi}0a7$7w?G{7XfJ3AlT zB9nXj`z0T7;M0#DKSo;|#Y9GOD=RB4936QYhfG^07L4+R5um;H_V&W!;ywJ%; z6Trdlbd5vH-R@`D-bOP6f(Y zm><|^X$$U3LV{mPT`B>WAg3w-GlpF>FVP^J3 zsZ>%ak(nGEJz2j=anbBBIwYiEX(X4;KH2os_jsr_$=`8(KFxgVFc|d6NS>W8C1qm; zi&U|$A`l4bGpY1CQnfXIrh5KP=VR-Fyu3VWI{-mvz9WxLhk1B-oTRn7QXesyf~qPG z(~?lgVqNU$=%8*^D|)-S{_O3=L`Fpw3IrHS!bmn8xbI3eFMjHG>P1tN^OyVfK~@)6 z#(S$)SK~cAJs%B{q5)e7b#&a?nAp6i*%VQH6SJD?bo{HtnxNe~cYvGA<(>rl3|tU* z=Ax35Ykn$u2zrDg`F#GOBB|{fzq89;OU+1I9Z6NHRA8Z<>!u`?yes*6UbU9S4mVO}skH>(4nDDA^h7EOZcADGR7>7I7k2~LOYjY!#NbM8W zNjg`rU31Z;H1Fkq@@raJ_uUrjbA1@NAQt<|ws*n?mikH{Kq{BQboSrFpil*yn<>r( zK1j&(Ex*6$PQ#NC@|?Z+d-e1?V4buxoU$(&K)oUntG3qR^y$;v`R8yrAm`1d?Nc`^ zVD;}4NvB}M8o=BX0sGD=i0ORBH;gpHCdL?Nt zFE4qupE|fUzup*N2_Tv4saN^mD_lnli=Q8W7!bE(u%K6xuyQ>%76s`gFK2<7$38Q4 zL1VEjfdJv*<;8ja+!=*J0q%>QyEC>?B9VkEHc}847Gc1IARhKlZ h$U5GC0pAKv z4-*p;<;L15EYl^oH9Thj_h-*O6$Oz!wmIeJ=S?D&!PzAxK481cqK19J`T+_?jn;C# z93aJH%bvSZsq__yPgd4f#JISyxHvR8jjS_EO6SJb3wkQo=0-_*=8X1M@zNjv`fMya z_baXO-Y$tuhH`iR?b(KGe$O0Kt(a!2-)mG#Sov@y@bi{9mjp*{rv!c=M$UXyS}Fw+ zw~16y^x}m}!;juH8V%ZpCKk(pTeA-8=;#2#1Ox^3EiS%{m6Y=N?-EBqfB`-tl9F&> z^KYqU%k&iCmHbelR6bm_&BpidADXjH%J28B>Lg)I1W`ZykX9YUOW9O}_V)Dr#o@H$ z>UzrC=n;Aq!vh01e(ZTjm;3knE&8fxlAsX8|Mr4_V(WT=sTgb1zI$R z2bGre6_XanE6IHo39#v{l2gnMskE0Potg0t4Q1uzWa2~5=4|; zfu{IF&0O+OYW;R8V@uXJ*TC?P(K%6l{cT@%Hh|jlSi`OwmAtCc?lVJX1j}zBVQ58o ztia9!fzVOqFbz8dSS=ZPw^d6=a+BcP&}Qw4Y)#8G7*?!LBod<>?AvY+s_t`A!RxsL z!qBmN&@CE;qH#i^FCeri%>_VMbLC~qRu1hYp?q_=x_i@d2GFV2M1%TiQ5w=T5d)5n zO;y^~lW{278;_q=R!=?3GD!%aJ Goqqs9Ee-zw diff --git a/packages/stream_chat_flutter/test/src/goldens/deleted_message_dark.png b/packages/stream_chat_flutter/test/src/goldens/deleted_message_dark.png index 7db0a6ef17a3f5586f9a0f7d67b1f49dd6fd5b60..d9bb3138bf3a71dbde33bda6e4605afbe76b5146 100644 GIT binary patch delta 1719 zcmV;o21xng71j}uKyCE<|^YbkM_@IGa4*)pNlTiU$lVAZ$lVAZ$lVAZ2F(iAn;loy|SDQv38b~C1 zvfZ8q(rUGI*lH=!qa8h$9}k1j+Y!SJvo$#EPx$@hPICYR01aUAW`YG*yPGjn_lYH!}0YTeTwi(EFV zzkUDxnT+c^x)^_f1VQ`_e|Opt3=eC3d|dTV1UXH&em(6NClac3n+NssFR4S?2Y)X>S zsgw${v+_JoVHj$qT-MgsR@Y^&>#8t2t1r@NNz$wBZIypYC52HGe{C+C)!^Wu_V@R- zy}hj<2=vcifBoR{rBdmmSKfEW?oF zKJ0qzx{a&fTuP_YfA7VHTkXFG`4qp+`)#qPN~NMJSFR`u!}z3{$>%k2>5|^mY8o9G z(UWpnl2m`3o0I2x`t8vpRVo#Yjg86oeO0T~IDq>PAL{YSiY|ZlnX=QV;9HTCFC}^Wv|QB+qpvNoyM$y8qxo*JYA) z|G|HQ_=Ilm?{|!|FgvTKm5SCkHWWsYwyRZz?U-HHjqh;Bac@_v@t96>T~||6Q}MdT z$iD48eXrH|Eb8~vyzH(g+0RjVnV%PE)5DvTnnZ*0hQU3s1tU&XElfgY7g@pD7RYYl%7 z%k?~!+VgYY_jP-5@qf=4A6eFDG<17$QBNxsmCI#?QKZ7mjQqhtNs8ZDJH8ShZ|gYh zAPDq)b2ENzi|4u>uk-FPu=elW=MQV$U!Z6f+v)Of-<2dm)GMS9V z$H(K7YxAAJUKC!hQVsu?`Op6-ANqxt#v}`|o*_|KpnNW;q~qxM^XJ;#+k1a+pBs&af*{cB*I#RFY)oMocH9-L{hRpmONCK%s$(%Q zFraiQrKzbYy?FIX2L}h{`rXF$XY}$RJuyE&-;@7d!K>sH6~X}(z0(@KRn`aoFUKcx4AwT%tU z7mM;dPxX3TOQlj@|M2XugeexzHKtg=IYuCWfj|HQfdB>q0Sp8J7<&41kQSd2f-YJ; z{)mj81(HZ4xP76kUVlVJ&jRV?hoS(d#YG&GApwe$U;#^$U;!x>_%Fneu2z=3CMo~` N002ovPDHLkV1k_0l*0f3 delta 2230 zcmV;n2ub(W5#be(Kx_(&Nkl?)dLhd1;NlrLHLns#kaa*+u)$L5{ zT6a79U+sU@+8L+Sb~qt{14Qpb`j}kRVA&_6I@+Z{OI<7-HYApLCwbljp#B zpGyLIPM$p35P+BRc6tH8K}M0$+LK@bB`$-nQ!l(zuWP5?=+w)ji}ZLs>hX5wT;8!j zB+2Xb$m8*JatS!#8ja4LM7hNRlRyC$e}=avkZ?GxP$=}u@nA5ha5(%{e=vFa)G1{$ znYQzKqfvb@JFDK_-Z!wS*I(B`^i~8C3I=uI{CWN5lTYM*$&Vz&Vln;o!UbJ8fBvQ8 zZ-YoUtRsViul32Z>$*xgj-Eb!+IDVlG^)(eqrdpX@^jX85WN*gOEQ^k{W%(qe?Bwj z@p$yd)2Ag##bQxAJ3DW5ZEs4_aU@BrtE+A2I?1F;rIKp3THCqfN>t8h51)wMNc1_qQ)r}g>8i!bbNI2_jbbLZso zc$8aR)!68$d_JF+mX_M?UteEe>)%;(?<_1R6b{Sh^J#s3UDax}?Y7&ks~dHFAfaGT z!9YN{wKc^P3B_Wu*4X=}PpecasZb~=?Ydga=jHeN_2JpGdidy(f397>E=f8wJF9p+ zE=ii4m{2Gf)Mpnjs#dG%$l#zRjvbTN>(#OG@mG%ZMx*k0Jo@(PRps+}`FuY8?bA;c z3r8&@_H9+GRSjh_dhgwL^@kHD z^ws6dDwWIT*xng`C7092#)iVpx13kk)|5yj(vLdvwHgUss4NGmJ-d&|K9UQAP`VKpV!szzH4p6V%i(@(qWI(xQtrjAA;l6HTK za?e02f2B&LqOGkhg+d`U8jY8(>3NYzM2$wHHRkbnluRbI^7wJv{d@lT!=&CH*vH1?p0qbrYqlk(>mxSwIV8)%ZkV28X6kXlZ_1x z4-f0mp+mZS<%$MfS3^TXs#Gc}mCG8+WK^%$e|OLFV`Hxz+q+ssA`vB%NxhTJDjtvP zLG$5st9b^Unw-?_J9m_JUAeBS<%bU?Nu^RrDaX;&jnj%YBQ zRyLcJB>nT+HC3xs6^ca-rqdc48q#n!t6Ht5>vMA|7K`TCcBLXGnbcr9t<2G*+ANjS z+uN&^Tu!U2tJ>Pyl9Nm-nMi0TlWEVO_d(N%eiVQ9j@91^smns9-RtN~QA35Adx0d)>KSuUCGbPutI3 z#jI_&Ti4IkXf))JexLtoLnl4|P2X?tU6|iuED#9j&*#o*C709Vl@+-I14=oLzP)<& znK-1^3uDjTJ58?~H}>y!=ho}>X_t;!+ith6Zu3_BsL4TAsZ{jC+?>LpkftUl<@fvb zkMF;4JqO@m``una-lV1F<<{pV=q_AA00V&l1_A*L1OgZc1TYW?VCd-Ey!CoRQg{BY zGTxjXZ|DA_jgAG<lY3<4pEcg2j7wZA{WE|0l%51RqPmgaR2}S07*qoM6N<$ Ef*kF!`v3p{ diff --git a/packages/stream_chat_flutter/test/src/goldens/deleted_message_light.png b/packages/stream_chat_flutter/test/src/goldens/deleted_message_light.png index d5a453ffc0cf157efe7d6a8e84648ab9a4a90530..1f6330eb9bd92be6130bae15872bfe6d51878d88 100644 GIT binary patch delta 1615 zcmV-V2C(_Q74Z;|Kz|4uNklLx2g!BEI-42W6ODdH5(ESyy6gwnH+%rXA?#h&NE7<+wo zGSkdGleuQ@iLnpjIQ}I7SNhMm2LRk<1OgZc1TYW?U?32{K$E@+>61_a2Rx?6wc(SK z6ZLw%acv$GB$Y~Oetv!|+a6mWQ55O;_*l-laSZ_zAdX}G`t|G23q+EfbDEu<9p?}L z6VH=j0UCez$j6T#)oeCLt{2C#Ha0dChT$!Y>gMCRi|$Pz8yg$i+}yl!SscgO+}yl& z`F(MCc&L0nKXSbw2;`hoCX=~^QQdr8chS8FBnSdYXCcIKd}+Dod5Ys$3kwUkJ+xah zI0m^~?ndk0^jLVFrw<=K+{(D_qI(m_;0?bxZ3usYKzn<8%4V|@`zGDQ@0|2O5Zw4! z{LZ@n>$vWsv3^UO&$k3|@Us@Ln1&ZbDc zUQa74DDv+>J&VUftI4|3 zKLM<)tZ03GU4=qH+uPeCm-Tu*)oL~6ayfskudi!uZB6_8`^g>6_kGptbuBI~s#>k8 zTrO*8XD11u)9J|bJeA93t*@^upUjsmS;JdWXk9BilXS!{`Pu3?dkeK+I-BQ@+Q0Kto|~+j&1RE3ptb&}aZRd;pJJVV z6Ph=222L-uYPGJ}-ilSr;(phYA949BWB-LuQp)p-GclUi?*=%;`6R7hi{iyv5f?#O6 zIBMPVC%3i!sBulIdlAU_D^;u2lIMAH≪w6h+B8hlht+US3X4l8dj^>-B$<`FR+I zs?};k&lp!N%VaXD)oLo0N-CGja?YvQY$kV{K``h4dh&eOK@g}=C=5LpT&LITUHU7` z^T)v2zl+Zw*1G4HU;O#1^_$J6Ij%`H@l&k9^M~}$v}SOtmoCq_7Zw)OZnu*=O{ddI zeu))^p*+tUnqoNTluD&WE{lKTIN3(0)6w4EUNWx1iPLVkC7oS~Tdmem*!%nY$#gLY z0yP?qD?hS!&Z*n&CfoEpZ|E3U`}e!YB93D@=aO+)>yH}OqBo;B zdi3bgzkZD%2-N9x^!oK{J$(33&N+FWr&q6D>G9*olFm-b+1Xh=efob?l9W!T6$F8{ zx3|@9w>332r57(=$T_EUI<2FlBYpbxNuK9vYHCW$%gaiq)8;bgoQ{u=)oQi0ySuBu z{`yM?2L~z?3d&?MnwgnVzu#A@)l$3N)`JHRbaZs2moHx?ul@b|cWrNP%kw<-`+Yrs z{#@yF`qDmUG8ujQ_Dz4EKY!MjFJI)GQy7NHmEPLF-Q8VHPfy?ISp59?Q(wP+)y~e2 zo;`b}nVA`L{ZZrkxBC6Qrl+UJ_BZJd;yC^_jtw6i9O&fa^$8j7Mw z*=$zJ%gYn}%dqsDZ<^x6bmpA2w)%(z(62?fj|HQfdGcFe$3nN_jP)D zI?f^Bj_ddPW8MDP0-2ke8`q{WVa9o`jP1Kvz$D-qlW_q+kwQ?A6Hw> zC#RnS=Xp4v(?Ua`P$&z)LwSyR0>E)bAb^2D00V&l1_A*L1d;x+k^w<5L*r4su;Vyd zU0pq@mq#6mL?Y_x={b_uKC(b0DG&(gsi&Sg$|>NOb6xjHU!PknlOX{Ye|Ta7$!4?4 zX0wNG&*gH;X0s}n%a3tZkJpteS2RCAUvu2{_O`~x#s#Gc+!3_U>J3BjCTwGK`LqpB6%gf7h97lmbpyt@^ z?QJbAEd1jO%io#Tar8t4e{%n4y3*Jv4og^s~3aPR2NIzZkuep8uwj`;$ zySwIC$8i)42G!KmRCDajn>RHvG4YR2@V_&!&Gc(%S+EO?i*82Lo{C;_-MjkhQfn zee=yX^85X2ZEe-e%#1RbjLw}qr(&_F%a<=plDfLOG(A17Xf&$NKKo3P4&Bz&)THt8 zaXF5ozP>)C(`k*5kL&E&vwG>JmsBp7b@}pTrPFDJ!(nY~e{5)Wc2>>J&3faFH5|IjvLca)rl+S>EEY95I9T)eHa0f2 zySrQSYjNt-DY>qzVzH=jI4qyfS99OB=2egCJ`mS+mCxtZ-QBHJDzz_=@4x?E!C+8N zKmD}g@wmFWf4X${?p=NT_1Ajtx##rOTW?8{#>U2!OeQ5slarHjU00uf{<)f(nsn>d zElo{LsZ=WI#*G_?ZgX8%<#JgceDHy~y1G;>7WKs!Unrl?OOod1=9EsS_3_6atG&Iw zy1jDi@$I+Y>fXJ3x_I%Tyk4(jv6!x2y{c?Bdtj|Fe=#v3zu&L(=g-UM^T}}>jg5_| zySuyQ@l}4>KJ&~o=Gb%R&S_v^K#4>`BO@bfYip~yZ);xls_p}+tUtoxuuhyfq0P-r zNz&}>tk&1pb>YGV{qVyN)%8a-8rAOZt~?%(e*N`VNqe_tEiEmQq~_*kNqgV;7hZTl zCr_T#f55;%&24_aUuVvo(dz1|W@l%W%jG0V?d|Obxq}*VnbYysVLtk?K{w^7O8|yIZkX%sjsP*B`(A z_FK)dKA*383u*1U*1YOf-3Q`0j=Wy4LZOgCf1!}p*4E^@t|lfXbo%sZwY9ZreSKX+ zLqn3Jg@pzAd_Lv#`Rdg@6bdO4iAa)$hll0!`7}2-ryqa(QNR51i!NNaptiO)b6e$# z-`m@(&d$#2$q|V}luRa7EEe_3E3fREx3qWiOifKm+6%DqkfE`$QT_e>@_6#e?KWg#921mwD|JRZ+O=k%bRogFna zG*q{h%VpiZeOu2y`)tkQdvN_>jm_n9a$R@dgAHr^`1rUvuXDA`u zrat=UBY8ZY>Xo_@QEO|f*4EZEH#et_jtO=M|5~H8(dWzu&K5e=w-| z`FVN0UPU4i-Mo48&~269ddp-oT3ub$@4x@9R4P?noL1HllarHr?X}mmw6vt9r6u+E z_e+vaoH!xJadiFqbqx*<%5fasxN$?DeDaC>e*eB>Zf$K^phm1qoYIbyz`DE>GbK-avZ1nzDZ|ir`~`6eYLi>n%i1hS`-e46^q3b zi^bH}*C$EUTZ^8a9)-hUB@ziG5{c@g+uq*Z>gy(xNli~rYkGQGfj~fSf4}|qfjA`T z)TvXtb?cU*(Ws))Xmw3fIT5YnyZ@(Jt+ByiP+qTBKmYu*T3TAv*Vku`uQjiF)leuD zj^wi!j^ikuP9NF*X+EEq&*wXEk1y?AF}+^zzFVS4-PYLHcwnEIOs4u2sIjq8SFT*q zojZ5*>8GFW8(S#)RxWP1EP?e> z)~H;)Anf4S)%;A*m~y0*d%SMG_p>&TkB6u7gEv;CrrM|`TI6Ubd=duvUWvKh^Q@z$u zb2g09z*x;X;MDZiPI*HoRTes$<_WKiR!^}4f1V})b%m>mu*}xIV0KMAqx@ucC${BX*@ps}n z;<`>x%zji|j{J0wHlh^7y7lbt=XFiILw{Y?r-&<}r4@^{s`CDHk2WBj<{f~#?><#+ z%SK7kjwGqJWutZm>eD>gzz|4^La~ru zYp9Wwd|TC6I8{&1ek{#XjeJkeeoUuwOSJNX5=qjFd)H;u***eE(V5e@6a8MzY-`hV zGuNbhq$?&7?NO|~S-pqF}wP*MDO6qng>)rip%U^0fH(d*Mt#e;_HU z^(X&RSM2K2y2_P8@i+#CjENZP2N@ zm(*SV&y+&xa^!1WvnfvsJM`llp?~iug0* zJ$RxortnuED%`kP?&)_afBt=Pe^uQsSJmyxpL3527Tm8yZ?{g>y`;Xb{y*<|cfuX= zb_Z1&7^_e$lrhI51d@XNC-zH{W;HuiSGHhK+laM)BT343T{C2J4X55iU#R!US1OqI zOXXdElVX8y)V1wZb-w>ch4*h#PC>Eq=ijH?YrG0>c_pQ7)|D;Ll*2Abf9GD)k&Zx8 zaORq-8Eu`aD_c0=^%Jp(qJd^fQf}e6A)ReFB%(bEH*S_|@6I@z<;Oe>;?wf8pi}PB!%QbSm57I=e@?l&L2VsJZAmNs_N{!VphAJP0HOCGKP( zf&3zk54ESfUSF{PpLKYr40^8Nk~@05Bq`R`c)|Bal}=UR&A+)Ij`+z|#X1fsx45F; z?H0sB>K^5kq>Q;wgg{bIQRtH-?JXUx>eg*3ub1U;n#=Rbf2S*F1fJE(8s(O2+9HLU zw<&(?+_6IM!CeY&c}0n6@&G&07gKoOhjNrmlB0NHzuT>D-KNidVO-DKcNkP?^ zDoN6w$q`kzewOlj{p}s{D(0HY(sqzq z=P&k((1Z3>WTMg<;D`_MLc?B{?q9mDO4o-EYVEQB-LLx8>kR4bPgZOBp^wu&+K93If6&Kz`;*n`b%ylZJt0M05#3gPOUB)g zE^!PpXlEYYASECBbN8+;jqC2x=CTE*6nJ~FVI`(J@zM) z4$6OsKrX}1K)qi0@+I}elOLRw2F7Yuvr{wLI+Y)dX!6PaFW|cxG#6c`?om$dEgh{r zR+G;?IL@8x)y3SV7ld&}ecS1N*XI}nSGAf;#34yHoM=+Qf6!c?KyX!R3HtT#J?pjM zy}K{EJSWkoS2nHDubX)Opg-;Y^Ch=KAh;^E1pQinWP{ev8>8RuU36vSP7VLJ0uQDLdT*0K*xQ0nU>j z0X2Ve(>=iDWJ||Z?L4|$flxq>>!>vpNcTvWL{)L6d>)@Fiz_wt>T1m{ot5s922EBh z7E7dih)m|Vjv51N)Y!U4j_ag*oMFM|@o7<2ofcKr$>;HY&)hu< zfn1f9^?gawgn(anZ0V9e98y#HTs4*7tS*mF-;BI2%~M_)laKFHk?W|o?K9Q3 zZB($ZSgU3iX@6xHBq(r1}rWs;0eNE2``D?)ZNN zX`XC&SaRSa-M_m*+sn$-aNA)?Qf>a-Qsjp9Z1eNFDt=i1T->8*K~#&X>U2-_-RT}}K)B3108QVoQ)BBI zNz&u*#I>h*vL3qWY2rACE>EEzy6JyueOFMWhgRoEk{VmrXmfCDnkO3=0!fkMI(lt) zgQVnP)t=(XdUEEYX`X8MdvfNZI*~h8^&gCtB)zy}MMhoiBajsJUB;ad@T;c1UC-4l zOZP~ZMj|1%qeb1{tW$XNTMB>iZ$-ZTP;SRwDUsUI`4clAQB8ZhCItM+%i@1O?u9b} zfuyK0ko-fvV@sD-RM%h7VO40uztp|!(%heH>1waN|dRZplx z(LiP#gZDLSksWK4h{ses z?_TANn=WVj`#Qe%A3E{r3v#}CSDtI8s(9YL$w-=4rgXT~R6bXe4isw;xke`OO1fKjB6d3${p-a*Im`b+(}p?>s2?hi~PbvOqb7qjmDYSBf3_TKN;^ zD8FisV(ohr`)-?Z3P;O3Wr5KxZ0 z-@yNZN47UA5lcU226eY=IQ8?0ifQuJEFPRe2TA-yP&vL)%F4}?Bzb?vOgQzs*rA>3jXBEw z@w2SlJmvUCsW)=G-|c=g@;XgAc2H6|dk`c~%+HMBT(S#BUZQCRBB4Z&`?F8ojutuF z-qVO{Z`6o!Gfpqv<0{LOe<=sNWC$b$V+zJdlG?pvR2~VYyk6g7NT$j0jZ$y4ONrRo zhe5GJJC&VZB=3LJ1yWX)BqLfGJ_HCP1!KL* zK!SxMb&b=J@_Id?{+IRmCtdJdL&f92UXm33cGKCPmrhi{uO^m9x z?emn^>u>KcqH6X<%{7#~({7Vz>_ml|S1Ph=t$LkK70$X-g|qHduhXf>uC)p`uT+k2 zRC2G9g4%z!&$aj3Zb_%V)8US*SSgs}pCxauSMB~I>L@JIk^>*5^uOtO71JdpPuAn1 z4)u1Qkd*uvx?tk3hvnYml&oy|ZoFNQuRqkuK(kH;nt!%Pe$^c1U;i6r<#_tt+>!$y zsiUw+?fxV3=6W;cDMOwINzvPTSL*GSm6{On>w$l!Hr?^tm#>)H+`Cdb#!`PSP(oK95iLOi4Zs*HCv@<01$3 zO!I&9X`X8Md#3q$mBbILp)UF8?SnPTGA0g*KnA3?;ubBcs*@x=`v;GzPwdw6O;4%F z4X1gs;it$AYx$<9bZz&3EnnOtNm^7@moeuaeF$WL?y0_8w^qzkv>>WK|FK6W^Bt}F z@7W?9A^R-H$Q zKrT|>GuEqH-#-<~mCl>%)#9p!T2#fWM{$PD!L8cWxl64-zPvgR%FLS;2dc7oynG&? zs){Q$Wz>|6-edd}fdB@sVgZw}0UeVc0k4xE0U>{`IH8!d=V&6OWpjsk7GT&2#iTcO z_NZZJT$1$0!aUt@b(XnKRxB1vEhQjBof$?bcKY-Adz3^pRZ|v+j zD~vu{+k5+6pK}mg)*8+eha_F!xmSt6esg^S!DXp66wrUSy{XmjEj{P&a}qsz<>O`g z)82T$>Tk_|Kks%31ec}OP(W`UTCF$djM8H}mY?-IUp%7M*FUVkEWAOE>u9!r)_J$X zIS4LGwH3E$OK_{c&{owS-l+eqZ`K#3Gc-H6N7-31HO};C-}soOTwSf&id)XRo&N(J WMtx2FJ+qtu0000VJUX<4B-HR z8jh3>1_m}uPZ!6KiaBp@`1{|EVru*Nov%#QeL|S0a{x!sqHCfljuRLC3q0DEYk#$7 zeI4Vb1V{1Ihpa2SLN;*(@#pF!uD;`Xuy|)nd6hZyEW7=(?5Cew{CIfdXL5S_^B@MX zU)O8(87fY2uo#RY>>-fS`1Nb)-pbF<_DyeOZ*UM>aWyN;Z1(rPJT?p$YxN%}zWiF% zwa7zTTl;E+i0iAbRg;#SOj%@ee(Sz{>n!?2T_>K}bfv;3Z2k4%$Z50umY@0@6DYFs z_FJtkrMdIxXIss^H$&SuSkyJ}uig3Lwd>YtoiFxX9y~R5XTdwy=XHt-Ag?DmHd+q0W+}TeohlzyDoHuF1sr>zkX#u`w|~*JWmAdLBQ1 z{MPN;moHtKl(lu%C6lYq&(GhwbLUFG<%{L*>sH);yXyY?*N+}K&GmB!I{eqCr`>UT zt3vM<%$_})OYz0;@9(eHOEXuQ_+HK1E_!_9CY{M&J}+B${q>|JKnbAq z+O@i-vwds-{0QuITWq!LW=>XpeSCU)x>lFd`T6$yw|2|O$-R2>#%K5L-LuZ7Sy@@F zdZycz^ze@7qzpSy0Bf}@SfnA==NU9Db=LXkSF0we?A*9c#dB5m|6gBMAA7vfi9?Eq z?f>fjzXz2kXnRi5Xemfu6SjJ#2G`O|El;3{U5|epGfa&PGk>yGqzJrC;y*$MneRn?RGE zzJ6&z!Gh*yW}rcjb{yg>Qax=1bkx`1f3tF|=Dzy!B?J^oz)z&!}1e)3oFMRfSCot96)&5$d(UsSGv~bPCf)(H2-w(HriJWF-XSc3Q z>gDsiiK#QMUAwkr`}Xj&XRS~8*S~r5CgvV6DRu#a*1a1`*N9;TnXy0>ySNW53ruPC>$PWFBhBU>ZG eC_)Z_WAl3py=T6&7ZU_lDh!^kelF{r5}E+l;D=KH literal 1812 zcmeHI`%@A~9H*Ny#?iE!9v-QAWuBR+nP~QagM0vYz(s_P7M=M#A+1y_mAKQiw9(Rz zObziZNZ@s8HaTeaz(Ye#G!8k`Ni75sD-4CK+kU;dKcSt^?ss=~_xqji=lj{&!k>uY z_O>uv2n1q}!-kN7{p)jw+JI3MDXs=)RT?=Q0}&56&w|D3Gz^Xc1uX{}cNqe)>B5De zDd%n({*ERcQM(qrQMziUhse1zj^5X#ZiQimP6RA$gme#88S#%J;c|gnDU4W}8##Pq zmXRp1Vj{LzS;RB)0&m`gOJ2q9kMIK(cO~s@Z6*T5f!zda@v06FX^xJ#K~s$(EMxlLfzrg@t9&3qvsZ7<8FwrhoI(Gq-b) zt#SSWidQ4CxjstNgsH6olPrmNZwUwM9uOGV1h)L_Mw0@3xQO6j0kK(OWjXi4TwIO{ zN68XeM2Fgt&Fp5eGhPf0UHenTNR@ul?6Etv<2sJZbsJ`Lsac=`eE{=PW>F_r3hUtdud%i7e_gNA#1YbPhaPfJUys6wMkp-@QPRU=s~_4V5|AXZW)*Z)h$$EQ5|+_YHCcQ8I{Qjt5Iz6i$%fTFFeu(-Ew@l_0+>TcrL*qDVaSqy^l&=+pryg8(7J>e^g_x}a~ zvzHGHBr};*4n9;C_jVY;CB7c!qAedIJy`6Er41mu_a^pvW~YYQtXK|DG#U&$OJYH$ z`kuVCI%X$JSte_^QC^NfAR0?c*Uy{{ZdA;UA3$w!cBTM6=jLwP=I45G@WwHeOXjt$ z>(;INtRO!-4xYu1e&k09tQxgg9@Ke~kv!${^76#|q7pR;qzRJe48G1deR?7K-3Q%V ze*PL4SJ&jk#BhhL$PulUFO^Egx7VO+@QrmM5nTT4S<=aqbwNr%8?3nCbJdQ#i6u`4Q*dZiKOW<8L1p-b{sa`r{8~nfM(qk^)R0SF$BJ%jDkP-sBE-mU&(? zF+XZxwgZkioQCH)61LASD3iRo=f3iG19dR z8^QmO7ZtJS)1|9-3J{3lTg91dr7&VQRyl)g^{ z0A3ccIr}OW?dVyGJ!V;c`LZtaxPJv@Lnz+;YuntKOw9>v=;8F4=vvUmEv@hMnePYb zD5E-6G@+Lz+?5P^W*WzjwnqUN!YRUdNlF(;m7xe!+hkSn}Py<VJUX<4B-HR z8jh3>1_rhoPZ!6KiaBp@Z1j%Kl|T0J^!Lgx^$U+UIn;t@X>zruXl(Jjcrx$k$-v+7 zC7NI23$hm`cxy^_Po5U!^Fm=}(uoT!(UW+Va&D=ZQzUY*d}aGDiS^U<)_?l)`{o|o zIlslvPA^s$K7Rc8awdao_3Nq_9#|<098efVSVLff;jFXE;`UaRUQK==r7*#8kw(z= z+k0Ob7V!S5y~-S;{FCa$pPoBY~nW)D~Ep`ClPw$5r&@TjvlHC^hre6ayfsD{X-`+tky7k=NH zv%YTs`ndJe&-^%Fon|C?v~W$*#uWz>7WgeMei!9YZ(I54$;(jwLfI=>rnP^51a8l} zd+Fc5eSUs^CIA1`u3f+W^^+$qfg&rzR=<3AclWOk51XUbPRqNqW1-vPg;jgk%{I?p zbu}x~Lgv-Exz<&mpY?wG{(Y*~(l7u1R;O-`^!4{w|Gl^7XVCrq_1BLaX(=l!`||U1 z`qCiHx9{F*P4&vGs;YW(Z)=oqubcDBlBnqD=u>H%v&?3TU#t7`W8wAn@!GS`uDY1< z;`R0QCu8pBna_Ov;)MrLp?=(+0IRuStFK;~t{)$wHFe#G{e`t3Sbna&n2}Lf82J6& z-C&8fTQ_e8F28&+!{pMTLrp4~$;r-#9|lY^vA17ev~x}Q`+F;|zkd1faQmz8zi-{R zaUsDVVEgT>i{1ODoPH|8(zxpCtJdWwK7HC$v~$ip`}%dax94YPW_l({th$@0&C#^6 z+C6YdiPhShHzQMxBv~B|%6un@iHnQ2Eo5tMtlAq_RAMt*yGzOQ(!G20Hr-s~wR93N zrnYU{_AkEvTXz3>1$)=PB|d7!iErPWja?fy_55?u;~#fsH!18Wkg>F~nxyiy$kH=N zqV3nWx3`y8_wPQQTCK0EtI5F>swJA6pPyY+RP-iF<~Y!Y7l9V=@$s2R^_EmrXn4+= zIdkWR>lr3NTcbjE-wm^y@BirMWx*5vH*>(%FIh$6WJ@53ldw1@HtPHvI_;^3iFIIEU-PpR>{MV~jtDZf3cJA!e_uuvR zoa^l5T)SpXMrx|-(jZ`f{JXa2f0)e1x~PbVfTckf-@VJ*6s93!Vs5@X{ro)5xqj;M zhyG7j;MlTf&zkM|_g6)&ef8x_NOpGie$(@*)#*l(Krb!5oGB|K^WtDLd)MP1m$okV zpReY>T!8}=RltyYGx_}UP`&A2UtU%RCP(i{Y3b>qI%4(N_v%-7K7Rc8@@lz9yE+zm zw70jSpmWKVgVMe!vTRJs`ur9>HcBCjF|GW{WE9I0IjK) zo;+D{?b{8#i9IRkrlBnrsl>kzs2ss4)v;L{@(8w?N#{;aF7(8A5T-G@y GGywo&aKg+0 literal 1866 zcmeHI`%}_c6vr~hn9R~@OEhTKO4-aElX6{=LPOD1M8#G`+)9{)wuea;XR|jx#XuiI^SobqAZlN2l4Q$g7syqt+CTM%jqkq8%|yZ5$fSJ z)U$HwMKfs`hcmm+TicNWHJM))X^;#_33RC*V^JgySgAU z^j=*RBCAV~RqJ%W5XnrY7%#jtJO2dD%K~;)DOhjsAplcrt{s{hR?vhbe{Dhp()%*$ z0XcNfmN2*cSME$YouG`XH7=d;c!(DY{2j~2$LVWUrOWH|LncOD9-l9(ttAZ_6f@gy zK7A~e3iI-|Jd#MNNlq;Jmfma?-*tr}=J&6s)9Ihy_k2G!*bw4MYt<|)$b*YKggrf> zQ~gq(2V|nHt?h@&$)3^Cie0;RZ|3@b`l#F2l5D_peer6wI*LY%1n9TpQXjJRivfti zhSOrPI55Y*V*kv-XFZy)f}5|Bb4-%Fyu3|d_h_dr27__7yY;eoX}k*&@3w8H&DtS( zQXrK|y|l8tWX;g)_42bL8J?b=_c~96qj?FOv2#rWv48{V(Ib&Hu@T_D%Bm{xI)Tw> zbXh#bLM|45*_GS*UIT?CFqvWQcG~{_`&m!RfbETGL=gf1XkNhGiu81|BsM$YMG5wj zX}UPu;pa-Dkxgx82Vt>Tutt7<^6ooBBYXN4H1VQT|4~<0NH6hDi83U|s8lM?- z=H|_}2t3u;rgawj?&Zb#x`KiNO>b}6+qb`B+}$1l(IePr2Xrh7$22{3zP`S`@`M`%LC!%z zLC(B@lgE!AS7VfMq_=01)jAUspmTJ#;X37ZE}mz5*i(Mox>!m#eg1s1PePR^PGoQQ zN)}{fWXKr^1i}u1P=jE|8XHB&q@r_8dx!}v7LZMR{9zzTfSbIRABN^tQ@=KkY#S|n z{W^AR?&8ytvk~ac#FdM;ym7c1p>W6H@=!3m(}t^P=`-Qlnwox+$z%}8Nz~9_;v466 zAGzGMNf$1N6$+YZW;jwv`m5tM@`^_^l^TKO^}cx{0FmN#(U}|<7Z>OJBOie5;F;|K zyKZ7mc(A>_-Oj-wA~CV9HFc?4C=}8LCKz?OofQNELDSPC1X5gHP6Y^%-N&T& z|LEx81y?ao53Ej4>n_FB;fd3FiJRIyR!N* client.state).thenReturn(clientState); when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); - when(() => client.flagMessage(any())).thenThrow(ApiError( - '{}', - 500, - )); + when(() => client.flagMessage(any())) + .thenThrow(StreamChatNetworkError(ChatErrorCode.internalSystemError)); final themeData = ThemeData(); final streamTheme = StreamChatThemeData.fromTheme(themeData); @@ -609,10 +607,8 @@ void main() { when(() => client.state).thenReturn(clientState); when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); - when(() => client.flagMessage(any())).thenThrow(ApiError( - '{"code":4}', - 400, - )); + when(() => client.flagMessage(any())) + .thenThrow(StreamChatNetworkError(ChatErrorCode.inputError)); final themeData = ThemeData(); final streamTheme = StreamChatThemeData.fromTheme(themeData); @@ -719,10 +715,8 @@ void main() { when(() => client.state).thenReturn(clientState); when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); - when(() => channel.deleteMessage(any())).thenThrow(ApiError( - '{}', - 500, - )); + when(() => channel.deleteMessage(any())) + .thenThrow(StreamChatNetworkError(ChatErrorCode.internalSystemError)); final themeData = ThemeData(); final streamTheme = StreamChatThemeData.fromTheme(themeData); diff --git a/packages/stream_chat_flutter/test/src/thread_header_test.dart b/packages/stream_chat_flutter/test/src/thread_header_test.dart index 578b2ad6..2b649be3 100644 --- a/packages/stream_chat_flutter/test/src/thread_header_test.dart +++ b/packages/stream_chat_flutter/test/src/thread_header_test.dart @@ -45,6 +45,7 @@ void main() { ]); when(() => client.wsConnectionStatusStream) .thenAnswer((_) => Stream.value(ConnectionStatus.connecting)); + when(() => clientState.totalUnreadCount).thenAnswer((i) => 1); when(() => clientState.totalUnreadCountStream) .thenAnswer((i) => Stream.value(1)); diff --git a/packages/stream_chat_flutter_core/test/message_list_core_test.dart b/packages/stream_chat_flutter_core/test/message_list_core_test.dart index 4e34f8f2..98e83f9f 100644 --- a/packages/stream_chat_flutter_core/test/message_list_core_test.dart +++ b/packages/stream_chat_flutter_core/test/message_list_core_test.dart @@ -300,7 +300,9 @@ void main() { when(() => mockChannel.state.isUpToDate).thenReturn(false); when(() => mockChannel.initialized).thenAnswer((_) async => true); when(() => mockChannel.query( - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), membersPagination: any(named: 'membersPagination'), messagesPagination: any(named: 'messagesPagination'), preferOffline: any(named: 'preferOffline'), diff --git a/packages/stream_chat_flutter_core/test/stream_channel_test.dart b/packages/stream_chat_flutter_core/test/stream_channel_test.dart index b48c02b1..2b32cf2d 100644 --- a/packages/stream_chat_flutter_core/test/stream_channel_test.dart +++ b/packages/stream_chat_flutter_core/test/stream_channel_test.dart @@ -165,7 +165,9 @@ void main() { when(() => mockChannel.initialized).thenAnswer((_) async => true); final messages = _generateMessages(); when(() => mockChannel.query( - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), messagesPagination: any(named: 'messagesPagination'), membersPagination: any(named: 'membersPagination'), watchersPagination: any(named: 'watchersPagination'), @@ -183,7 +185,9 @@ void main() { verify(() => mockChannel.initialized).called(1); verify(() => mockChannel.query( - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), messagesPagination: any(named: 'messagesPagination'), membersPagination: any(named: 'membersPagination'), watchersPagination: any(named: 'watchersPagination'), @@ -228,7 +232,9 @@ void main() { final messages = _generateMessages(); when(() => mockChannel.query( - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), messagesPagination: beforePagination, membersPagination: any(named: 'membersPagination'), watchersPagination: any(named: 'watchersPagination'), @@ -236,7 +242,9 @@ void main() { )).thenAnswer((_) async => ChannelState(messages: messages)); when(() => mockChannel.query( - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), messagesPagination: afterPagination, membersPagination: any(named: 'membersPagination'), watchersPagination: any(named: 'watchersPagination'), @@ -259,7 +267,9 @@ void main() { await tester.pumpAndSettle(); verify(() => mockChannel.query( - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), messagesPagination: beforePagination, membersPagination: any(named: 'membersPagination'), watchersPagination: any(named: 'watchersPagination'), @@ -267,7 +277,9 @@ void main() { )).called(1); verify(() => mockChannel.query( - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), messagesPagination: afterPagination, membersPagination: any(named: 'membersPagination'), watchersPagination: any(named: 'watchersPagination'), @@ -285,7 +297,9 @@ void main() { ); when(() => mockChannel.query( - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), messagesPagination: updatedBeforePagination, membersPagination: any(named: 'membersPagination'), watchersPagination: any(named: 'watchersPagination'), @@ -293,7 +307,9 @@ void main() { )).thenAnswer((_) async => ChannelState(messages: messages)); when(() => mockChannel.query( - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), messagesPagination: updatedAfterPagination, membersPagination: any(named: 'membersPagination'), watchersPagination: any(named: 'watchersPagination'), @@ -303,7 +319,9 @@ void main() { await tester.pumpAndSettle(); verify(() => mockChannel.query( - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), messagesPagination: updatedBeforePagination, membersPagination: any(named: 'membersPagination'), watchersPagination: any(named: 'watchersPagination'), @@ -311,7 +329,9 @@ void main() { )).called(1); verify(() => mockChannel.query( - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), messagesPagination: updatedAfterPagination, membersPagination: any(named: 'membersPagination'), watchersPagination: any(named: 'watchersPagination'), diff --git a/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart b/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart index 168454c8..f2b1b514 100644 --- a/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart +++ b/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart @@ -227,7 +227,7 @@ void main() { ); testWidgets( - 'didChangeAppLifecycleState should not call client.connect() ' + 'didChangeAppLifecycleState should not call client.openConnection() ' 'if connection is not available in case the ' 'widget lifestyle changes to AppLifecycleState.resume', (tester) async { @@ -238,8 +238,8 @@ void main() { final event = Event(); when(() => mockClient.on()).thenAnswer((_) => Stream.value(event)); - when(() => mockClient.connect()).thenAnswer((_) async => event); - when(() => mockClient.disconnect()).thenAnswer((_) async => null); + when(() => mockClient.openConnection()).thenAnswer((_) async => event); + when(() => mockClient.closeConnection()).thenAnswer((_) async => null); when(() => mockClient.wsConnectionStatus) .thenReturn(ConnectionStatus.disconnected); @@ -267,7 +267,7 @@ void main() { streamChatCoreState .didChangeAppLifecycleState(AppLifecycleState.resumed); - verifyNever(() => mockClient.connect()); + verifyNever(() => mockClient.openConnection()); }); }, ); @@ -327,8 +327,8 @@ void main() { final event = Event(); when(() => mockClient.on()).thenAnswer((_) => Stream.value(event)); - when(() => mockClient.connect()).thenAnswer((_) async => event); - when(() => mockClient.disconnect()).thenAnswer((_) async => null); + when(() => mockClient.openConnection()).thenAnswer((_) async => event); + when(() => mockClient.closeConnection()).thenAnswer((_) async => null); when(() => mockClient.wsConnectionStatus) .thenReturn(ConnectionStatus.disconnected); @@ -348,7 +348,7 @@ void main() { await Future.delayed(const Duration(seconds: 1)); - verify(() => mockClient.connect()).called(1); + verify(() => mockClient.openConnection()).called(1); }); }, ); @@ -376,8 +376,8 @@ void main() { final event = Event(); when(() => mockClient.on()).thenAnswer((_) => Stream.value(event)); - when(() => mockClient.connect()).thenAnswer((_) async => event); - when(mockClient.disconnect).thenAnswer((_) async => null); + when(() => mockClient.openConnection()).thenAnswer((_) async => event); + when(() => mockClient.closeConnection()).thenAnswer((_) async => null); when(() => mockClient.wsConnectionStatus) .thenReturn(ConnectionStatus.connected); @@ -385,7 +385,7 @@ void main() { await Future.delayed(const Duration(seconds: 1)); - verify(() => mockClient.disconnect()).called(1); + verify(() => mockClient.closeConnection()).called(1); }); }, ); @@ -402,8 +402,8 @@ void main() { final event = Event(); when(() => mockClient.on()).thenAnswer((_) => Stream.value(event)); - when(() => mockClient.connect()).thenAnswer((_) async => event); - when(() => mockClient.disconnect()).thenAnswer((_) async => null); + when(() => mockClient.openConnection()).thenAnswer((_) async => event); + when(() => mockClient.closeConnection()).thenAnswer((_) async => null); when(() => mockClient.wsConnectionStatus) .thenReturn(ConnectionStatus.disconnected); @@ -432,7 +432,7 @@ void main() { await Future.delayed(const Duration(seconds: 1)); - verifyNever(() => mockClient.disconnect()); + verifyNever(() => mockClient.closeConnection()); }); }, ); From 2d6cc796e86e0b6ee2b19ce05ff03edf8a6ecc53 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 16 Jun 2021 12:27:21 +0200 Subject: [PATCH 047/150] move jsons to fixtures --- .../stream_chat/lib/src/core/error/error.dart | 2 +- .../lib/src/core/models/channel_model.dart | 3 +- .../lib/src/core/models/own_user.dart | 11 +- .../stream_chat/lib/src/core/models/user.dart | 4 +- .../stream_chat/test/fixtures/action.json | 7 + .../stream_chat/test/fixtures/attachment.json | 29 + .../stream_chat/test/fixtures/channel.json | 7 + .../test/fixtures/channel_state.json | 832 +++++++++++ .../test/fixtures/channel_state_to_json.json | 418 ++++++ .../stream_chat/test/fixtures/command.json | 5 + .../stream_chat/test/fixtures/device.json | 4 + packages/stream_chat/test/fixtures/event.json | 29 + .../stream_chat/test/fixtures/member.json | 15 + .../stream_chat/test/fixtures/message.json | 65 + .../test/fixtures/message_to_json.json | 29 + .../stream_chat/test/fixtures/reaction.json | 18 + packages/stream_chat/test/fixtures/read.json | 7 + packages/stream_chat/test/fixtures/user.json | 4 + .../test/src/core/models/action_test.dart | 17 +- .../test/src/core/models/attachment_test.dart | 37 +- .../src/core/models/channel_state_test.dart | 1263 +---------------- .../test/src/core/models/channel_test.dart | 16 +- .../test/src/core/models/command_test.dart | 14 +- .../test/src/core/models/device_test.dart | 14 +- .../test/src/core/models/event_test.dart | 38 +- .../test/src/core/models/member_test.dart | 26 +- .../test/src/core/models/message_test.dart | 105 +- .../test/src/core/models/reaction_test.dart | 27 +- .../test/src/core/models/read_test.dart | 18 +- .../test/src/core/models/user_test.dart | 13 +- packages/stream_chat/test/src/utils.dart | 8 + .../lib/src/db/moor_chat_database.g.dart | 4 +- 32 files changed, 1530 insertions(+), 1559 deletions(-) create mode 100644 packages/stream_chat/test/fixtures/action.json create mode 100644 packages/stream_chat/test/fixtures/attachment.json create mode 100644 packages/stream_chat/test/fixtures/channel.json create mode 100644 packages/stream_chat/test/fixtures/channel_state.json create mode 100644 packages/stream_chat/test/fixtures/channel_state_to_json.json create mode 100644 packages/stream_chat/test/fixtures/command.json create mode 100644 packages/stream_chat/test/fixtures/device.json create mode 100644 packages/stream_chat/test/fixtures/event.json create mode 100644 packages/stream_chat/test/fixtures/member.json create mode 100644 packages/stream_chat/test/fixtures/message.json create mode 100644 packages/stream_chat/test/fixtures/message_to_json.json create mode 100644 packages/stream_chat/test/fixtures/reaction.json create mode 100644 packages/stream_chat/test/fixtures/read.json create mode 100644 packages/stream_chat/test/fixtures/user.json diff --git a/packages/stream_chat/lib/src/core/error/error.dart b/packages/stream_chat/lib/src/core/error/error.dart index 6f2c39a3..ac4a1e0d 100644 --- a/packages/stream_chat/lib/src/core/error/error.dart +++ b/packages/stream_chat/lib/src/core/error/error.dart @@ -1,2 +1,2 @@ export 'chat_error_code.dart'; -export 'stream_chat_error.dart'; \ No newline at end of file +export 'stream_chat_error.dart'; diff --git a/packages/stream_chat/lib/src/core/models/channel_model.dart b/packages/stream_chat/lib/src/core/models/channel_model.dart index 74195c71..37e587ff 100644 --- a/packages/stream_chat/lib/src/core/models/channel_model.dart +++ b/packages/stream_chat/lib/src/core/models/channel_model.dart @@ -78,8 +78,7 @@ class ChannelModel { final DateTime? deletedAt; /// The count of this channel members - @JsonKey( - includeIfNull: false, toJson: Serializer.readOnly, defaultValue: 0) + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly, defaultValue: 0) final int memberCount; /// Map of custom channel extraData diff --git a/packages/stream_chat/lib/src/core/models/own_user.dart b/packages/stream_chat/lib/src/core/models/own_user.dart index 70529687..22705b05 100644 --- a/packages/stream_chat/lib/src/core/models/own_user.dart +++ b/packages/stream_chat/lib/src/core/models/own_user.dart @@ -61,21 +61,16 @@ class OwnUser extends User { /// List of users muted by the user @JsonKey( - includeIfNull: false, - toJson: Serializer.readOnly, - defaultValue: []) + includeIfNull: false, toJson: Serializer.readOnly, defaultValue: []) final List mutes; /// List of users muted by the user @JsonKey( - includeIfNull: false, - toJson: Serializer.readOnly, - defaultValue: []) + includeIfNull: false, toJson: Serializer.readOnly, defaultValue: []) final List channelMutes; /// Total unread messages by the user - @JsonKey( - includeIfNull: false, toJson: Serializer.readOnly, defaultValue: 0) + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly, defaultValue: 0) final int totalUnreadCount; /// Total unread channels by the user diff --git a/packages/stream_chat/lib/src/core/models/user.dart b/packages/stream_chat/lib/src/core/models/user.dart index de58055d..c2960af9 100644 --- a/packages/stream_chat/lib/src/core/models/user.dart +++ b/packages/stream_chat/lib/src/core/models/user.dart @@ -22,8 +22,8 @@ class User extends Equatable { updatedAt = updatedAt ?? DateTime.now(); /// Create a new instance from a json - factory User.fromJson(Map json) => _$UserFromJson( - Serializer.moveToExtraDataFromRoot(json, topLevelFields)); + factory User.fromJson(Map json) => + _$UserFromJson(Serializer.moveToExtraDataFromRoot(json, topLevelFields)); /// Known top level fields. /// Useful for [Serializer] methods. diff --git a/packages/stream_chat/test/fixtures/action.json b/packages/stream_chat/test/fixtures/action.json new file mode 100644 index 00000000..59da5684 --- /dev/null +++ b/packages/stream_chat/test/fixtures/action.json @@ -0,0 +1,7 @@ +{ + "name": "name", + "style": "style", + "text": "text", + "type": "type", + "value": "value" +} \ No newline at end of file diff --git a/packages/stream_chat/test/fixtures/attachment.json b/packages/stream_chat/test/fixtures/attachment.json new file mode 100644 index 00000000..faf38b90 --- /dev/null +++ b/packages/stream_chat/test/fixtures/attachment.json @@ -0,0 +1,29 @@ +{ + "type": "giphy", + "title": "awesome", + "title_link": "https://giphy.com/gifs/nrkp3-dance-happy-3o7TKnCdBx5cMg0qti", + "thumb_url": "https://media0.giphy.com/media/3o7TKnCdBx5cMg0qti/giphy.gif", + "actions": [ + { + "name": "image_action", + "text": "Send", + "style": "primary", + "type": "button", + "value": "send" + }, + { + "name": "image_action", + "text": "Shuffle", + "style": "default", + "type": "button", + "value": "shuffle" + }, + { + "name": "image_action", + "text": "Cancel", + "style": "default", + "type": "button", + "value": "cancel" + } + ] +} \ No newline at end of file diff --git a/packages/stream_chat/test/fixtures/channel.json b/packages/stream_chat/test/fixtures/channel.json new file mode 100644 index 00000000..2184cd6e --- /dev/null +++ b/packages/stream_chat/test/fixtures/channel.json @@ -0,0 +1,7 @@ +{ + "id": "test", + "type": "livestream", + "cid": "livestream:test", + "cats": true, + "fruit": ["bananas", "apples"] +} \ No newline at end of file diff --git a/packages/stream_chat/test/fixtures/channel_state.json b/packages/stream_chat/test/fixtures/channel_state.json new file mode 100644 index 00000000..1e6af605 --- /dev/null +++ b/packages/stream_chat/test/fixtures/channel_state.json @@ -0,0 +1,832 @@ + +{ + "channel": { + "id": "dev", + "type": "team", + "cid": "team:dev", + "last_message_at": "2020-01-30T13:43:41.062362Z", + "created_at": "2019-04-03T18:43:33.213373Z", + "updated_at": "2019-04-03T18:43:33.213374Z", + "team": "test", + "created_by": { + "id": "guido", + "role": "user", + "created_at": "2019-04-03T18:43:33.201036Z", + "updated_at": "2019-04-03T18:43:33.204713Z", + "banned": false, + "online": false, + "name": "Guido" + }, + "frozen": true, + "config": { + "created_at": "2019-11-07T22:29:26.776526Z", + "updated_at": "2019-11-07T22:29:48.286746Z", + "name": "team", + "typing_events": true, + "read_events": true, + "connect_events": true, + "search": true, + "reactions": true, + "replies": true, + "mutes": true, + "uploads": true, + "url_enrichment": true, + "message_retention": "infinite", + "max_message_length": 5000, + "automod": "disabled", + "automod_behavior": "flag", + "commands": [ + { + "name": "giphy", + "description": "Post a random gif to the channel", + "args": "[text]", + "set": "fun_set" + } + ] + }, + "name": "#dev", + "image": "https://cdn.chrisshort.net/testing-certificate-chains-in-go/GOPHER_MIC_DROP.png", + "example": 1 + }, + "messages": [ + { + "id": "dry-meadow-0-2b73cc8b-cd86-4a01-8d40-bd82ad07a030", + "text": "fasdfa", + "type": "regular", + "status": "SENT", + "silent": false, + "user": { + "id": "dry-meadow-0", + "role": "user", + "created_at": "2019-03-27T17:40:17.155892Z", + "updated_at": "2020-01-29T03:22:47.641589Z", + "last_active": "2020-01-29T03:22:47.63613Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Dry+meadow", + "name": "Dry meadow" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T03:23:02.843948Z", + "updated_at": "2020-01-29T03:23:02.843949Z", + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "dry-meadow-0-e8e74482-b4cd-48db-9d1e-30e6c191786f", + "text": "test message", + "type": "regular", + "user": { + "id": "dry-meadow-0", + "role": "user", + "created_at": "2019-03-27T17:40:17.155892Z", + "updated_at": "2020-01-29T03:22:47.641589Z", + "last_active": "2020-01-29T03:22:47.63613Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Dry+meadow", + "name": "Dry meadow" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T03:23:07.981091Z", + "updated_at": "2020-01-29T03:23:07.981091Z", + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "dry-meadow-0-53e6299f-9b97-4a9c-a27e-7e2dde49b7e0", + "text": "test message", + "type": "regular", + "user": { + "id": "dry-meadow-0", + "role": "user", + "created_at": "2019-03-27T17:40:17.155892Z", + "updated_at": "2020-01-29T03:22:47.641589Z", + "last_active": "2020-01-29T03:22:47.63613Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Dry+meadow", + "name": "Dry meadow" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T03:23:11.568022Z", + "updated_at": "2020-01-29T03:23:11.568022Z", + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "dry-meadow-0-80925be0-786e-40a5-b225-486518dafd35", + "text": "asdfadf", + "type": "regular", + "user": { + "id": "dry-meadow-0", + "role": "user", + "created_at": "2019-03-27T17:40:17.155892Z", + "updated_at": "2020-01-29T03:22:47.641589Z", + "last_active": "2020-01-29T03:22:47.63613Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Dry+meadow", + "name": "Dry meadow" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T03:32:57.403566Z", + "updated_at": "2020-01-29T03:32:57.403566Z", + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "dry-meadow-0-64d7970f-ede8-4b31-9738-1bc1756d2bfe", + "text": "test", + "type": "regular", + "user": { + "id": "dry-meadow-0", + "role": "user", + "created_at": "2019-03-27T17:40:17.155892Z", + "updated_at": "2020-01-29T03:22:47.641589Z", + "last_active": "2020-01-29T03:22:47.63613Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Dry+meadow", + "name": "Dry meadow" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T03:33:35.294802Z", + "updated_at": "2020-01-29T03:33:35.294802Z", + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "withered-cell-0-84cbd760-cf55-4f7e-9207-c5f66cccc6dc", + "text": "hi", + "type": "regular", + "user": { + "id": "withered-cell-0", + "role": "user", + "created_at": "2020-01-29T03:34:01.698106Z", + "updated_at": "2020-01-29T03:34:01.708808Z", + "last_active": "2020-01-29T03:34:01.70353Z", + "banned": false, + "online": false, + "name": "Withered cell", + "image": "https://getstream.io/random_svg/?name=Withered+cell" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T03:34:27.393296Z", + "updated_at": "2020-01-29T03:34:27.393296Z", + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "dry-meadow-0-e9203588-43c3-40b1-91f7-f217fc42aa53", + "text": "fantastic", + "type": "regular", + "user": { + "id": "dry-meadow-0", + "role": "user", + "created_at": "2019-03-27T17:40:17.155892Z", + "updated_at": "2020-01-29T03:22:47.641589Z", + "last_active": "2020-01-29T03:22:47.63613Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Dry+meadow", + "name": "Dry meadow" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T03:34:37.638376Z", + "updated_at": "2020-01-29T03:34:37.638376Z", + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "withered-cell-0-7e3552d7-7a0d-45f2-a856-e91b23a7e240", + "text": "nice to meet you", + "type": "regular", + "user": { + "id": "withered-cell-0", + "role": "user", + "created_at": "2020-01-29T03:34:01.698106Z", + "updated_at": "2020-01-29T03:34:01.708808Z", + "last_active": "2020-01-29T03:34:01.70353Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Withered+cell", + "name": "Withered cell" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T03:35:04.301566Z", + "updated_at": "2020-01-29T03:35:04.301566Z", + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "dry-meadow-0-1ffeafd4-e4fc-4c84-9394-9d7cb10fff42", + "text": "hey", + "type": "regular", + "user": { + "id": "dry-meadow-0", + "role": "user", + "created_at": "2019-03-27T17:40:17.155892Z", + "updated_at": "2020-01-29T03:22:47.641589Z", + "last_active": "2020-01-29T03:22:47.63613Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Dry+meadow", + "name": "Dry meadow" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T03:35:24.939084Z", + "updated_at": "2020-01-29T03:35:24.939085Z", + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "dry-meadow-0-3f147324-12c8-4b41-9fb5-2db88d065efa", + "text": "hello, everyone", + "type": "regular", + "user": { + "id": "dry-meadow-0", + "role": "user", + "created_at": "2019-03-27T17:40:17.155892Z", + "updated_at": "2020-01-29T03:22:47.641589Z", + "last_active": "2020-01-29T03:22:47.63613Z", + "banned": false, + "online": false, + "name": "Dry meadow", + "image": "https://getstream.io/random_svg/?name=Dry+meadow" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T03:35:33.101566Z", + "updated_at": "2020-01-29T03:35:33.101566Z", + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "dry-meadow-0-51a348ae-0c0a-44de-a556-eac7891c0cf0", + "text": "who is there?", + "type": "regular", + "user": { + "id": "dry-meadow-0", + "role": "user", + "created_at": "2019-03-27T17:40:17.155892Z", + "updated_at": "2020-01-29T03:22:47.641589Z", + "last_active": "2020-01-29T03:22:47.63613Z", + "banned": false, + "online": false, + "name": "Dry meadow", + "image": "https://getstream.io/random_svg/?name=Dry+meadow" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T03:35:45.458685Z", + "updated_at": "2020-01-29T03:35:45.458685Z", + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "icy-recipe-7-a29e237b-8d81-4a97-9bc8-d42bca3f1356", + "text": "하ì´", + "type": "regular", + "user": { + "id": "icy-recipe-7", + "role": "user", + "created_at": "2020-01-21T11:36:22.284503Z", + "updated_at": "2020-01-29T07:01:59.69882Z", + "last_active": "2020-01-29T07:01:59.693378Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Icy+recipe", + "name": "Icy recipe" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T07:02:11.535395Z", + "updated_at": "2020-01-29T07:02:11.535395Z", + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "icy-recipe-7-935c396e-ddf8-4a9a-951c-0a12fa5bf055", + "text": "what are you doing?", + "type": "regular", + "user": { + "id": "icy-recipe-7", + "role": "user", + "created_at": "2020-01-21T11:36:22.284503Z", + "updated_at": "2020-01-29T07:01:59.69882Z", + "last_active": "2020-01-29T07:01:59.693378Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Icy+recipe", + "name": "Icy recipe" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T07:02:22.485136Z", + "updated_at": "2020-01-29T07:02:22.485136Z", + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "throbbing-boat-5-1e4d5730-5ff0-4d25-9948-9f34ffda43e4", + "text": "ðŸ‘", + "type": "regular", + "user": { + "id": "throbbing-boat-5", + "role": "user", + "created_at": "2019-07-30T06:29:53.060413Z", + "updated_at": "2020-01-29T14:11:27.80176Z", + "last_active": "2020-01-29T14:11:27.7963Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Throbbing+boat", + "name": "Throbbing boat" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T14:12:04.688552Z", + "updated_at": "2020-01-29T14:12:04.688552Z", + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "snowy-credit-3-3e0c1a0d-d22f-42ee-b2a1-f9f49477bf21", + "text": "sdasas", + "type": "regular", + "user": { + "id": "snowy-credit-3", + "role": "user", + "created_at": "2020-01-29T15:29:03.693312Z", + "updated_at": "2020-01-29T15:29:03.702648Z", + "last_active": "2020-01-29T15:29:03.696144Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Snowy+credit", + "name": "Snowy credit" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T15:29:36.011315Z", + "updated_at": "2020-01-29T15:29:36.011316Z", + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "snowy-credit-3-3319537e-2d0e-4876-8170-a54f046e4b7d", + "text": "cjshsa", + "type": "regular", + "user": { + "id": "snowy-credit-3", + "role": "user", + "created_at": "2020-01-29T15:29:03.693312Z", + "updated_at": "2020-01-29T15:29:03.702648Z", + "last_active": "2020-01-29T15:29:03.696144Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Snowy+credit", + "name": "Snowy credit" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T15:29:41.677819Z", + "updated_at": "2020-01-29T15:29:41.677819Z", + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "snowy-credit-3-cfaf0b46-1daa-49c5-947c-b16d6697487d", + "text": "nhisagdhsadz", + "type": "regular", + "user": { + "id": "snowy-credit-3", + "role": "user", + "created_at": "2020-01-29T15:29:03.693312Z", + "updated_at": "2020-01-29T15:29:03.702648Z", + "last_active": "2020-01-29T15:29:03.696144Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Snowy+credit", + "name": "Snowy credit" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T15:29:43.354177Z", + "updated_at": "2020-01-29T15:29:43.354177Z", + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "snowy-credit-3-cebe25a7-a3a3-49fc-9919-91c6725e81f3", + "text": "hvadhsahzd", + "type": "regular", + "user": { + "id": "snowy-credit-3", + "role": "user", + "created_at": "2020-01-29T15:29:03.693312Z", + "updated_at": "2020-01-29T15:29:03.702648Z", + "last_active": "2020-01-29T15:29:03.696144Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Snowy+credit", + "name": "Snowy credit" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T15:29:44.754713Z", + "updated_at": "2020-01-29T15:29:44.754713Z", + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "divine-glade-9-0cea9262-5766-48e9-8b22-311870aed3bf", + "text": "hello", + "type": "regular", + "user": { + "id": "divine-glade-9", + "role": "user", + "created_at": "2020-01-29T17:02:18.312524Z", + "updated_at": "2020-01-29T17:02:18.320187Z", + "last_active": "2020-01-29T17:02:18.315074Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Divine+glade", + "name": "Divine glade" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T17:02:36.933852Z", + "updated_at": "2020-01-29T17:02:36.933852Z", + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "red-firefly-9-c4e9007b-bb7d-4238-ae08-5f8e3cd03d73", + "text": "hello", + "type": "regular", + "user": { + "id": "red-firefly-9", + "role": "user", + "created_at": "2019-08-02T18:56:39.366516Z", + "updated_at": "2020-01-29T22:13:50.491769Z", + "last_active": "2020-01-29T22:13:50.450215Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Red+firefly", + "name": "Red firefly" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T22:14:08.54062Z", + "updated_at": "2020-01-29T22:14:08.54062Z", + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "bitter-glade-2-02aee4eb-4093-4736-808b-2de75820e854", + "text": "hello", + "type": "regular", + "user": { + "id": "bitter-glade-2", + "role": "user", + "created_at": "2020-01-30T13:08:56.190678Z", + "updated_at": "2020-01-30T13:08:56.200333Z", + "last_active": "2020-01-30T13:08:56.193882Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Bitter+glade", + "name": "Bitter glade" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-30T13:11:37.191293Z", + "updated_at": "2020-01-30T13:11:37.191293Z", + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "morning-sea-1-0c700bcb-46dd-4224-b590-e77bdbccc480", + "text": "http://jaeger.ui.gtstrm.com/", + "type": "regular", + "user": { + "id": "morning-sea-1", + "role": "user", + "created_at": "2019-07-22T09:19:07.505207Z", + "updated_at": "2020-01-30T13:33:05.831856Z", + "last_active": "2020-01-30T13:33:05.825369Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Morning+sea", + "name": "Morning sea" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-30T13:33:16.853116Z", + "updated_at": "2020-01-30T13:33:16.853116Z", + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "ancient-salad-0-53e8b4e6-5b7b-43ad-aeee-8bfb6a9ed0be", + "text": "hi", + "type": "regular", + "user": { + "id": "ancient-salad-0", + "role": "user", + "created_at": "2020-01-30T13:34:29.286813Z", + "updated_at": "2020-01-30T13:34:29.296196Z", + "last_active": "2020-01-30T13:34:29.289964Z", + "banned": false, + "online": true, + "image": "https://getstream.io/random_svg/?name=Ancient+salad", + "name": "Ancient salad" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-30T13:36:52.749731Z", + "updated_at": "2020-01-30T13:36:52.749732Z", + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "ancient-salad-0-8c225075-bd4c-42e2-8024-530aae13cd40", + "text": "hi", + "type": "regular", + "user": { + "id": "ancient-salad-0", + "role": "user", + "created_at": "2020-01-30T13:34:29.286813Z", + "updated_at": "2020-01-30T13:34:29.296196Z", + "last_active": "2020-01-30T13:34:29.289964Z", + "banned": false, + "online": true, + "image": "https://getstream.io/random_svg/?name=Ancient+salad", + "name": "Ancient salad" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-30T13:37:41.631056Z", + "updated_at": "2020-01-30T13:37:41.631056Z", + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "proud-sea-7-17802096-cbf8-4e3c-addd-4ee31f4c8b5c", + "text": "😃", + "type": "regular", + "user": { + "id": "proud-sea-7", + "role": "user", + "created_at": "2020-01-30T13:43:03.903006Z", + "updated_at": "2020-01-30T13:43:03.912307Z", + "last_active": "2020-01-30T13:43:03.906236Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Proud+sea", + "name": "Proud sea" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-30T13:43:41.062362Z", + "updated_at": "2020-01-30T13:43:41.062362Z", + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + } + ], + "watcher_count": 5, + "members": [] +} \ No newline at end of file diff --git a/packages/stream_chat/test/fixtures/channel_state_to_json.json b/packages/stream_chat/test/fixtures/channel_state_to_json.json new file mode 100644 index 00000000..4b8a0369 --- /dev/null +++ b/packages/stream_chat/test/fixtures/channel_state_to_json.json @@ -0,0 +1,418 @@ + +{ + "channel": { + "id": "dev", + "type": "team", + "frozen": true, + "name": "#dev", + "image": "https://cdn.chrisshort.net/testing-certificate-chains-in-go/GOPHER_MIC_DROP.png", + "example": 1 + }, + "watchers": [], + "read": [], + "messages": [ + { + "id": "dry-meadow-0-2b73cc8b-cd86-4a01-8d40-bd82ad07a030", + "text": "fasdfa", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "dry-meadow-0-e8e74482-b4cd-48db-9d1e-30e6c191786f", + "text": "test message", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "dry-meadow-0-53e6299f-9b97-4a9c-a27e-7e2dde49b7e0", + "text": "test message", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "dry-meadow-0-80925be0-786e-40a5-b225-486518dafd35", + "text": "asdfadf", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "dry-meadow-0-64d7970f-ede8-4b31-9738-1bc1756d2bfe", + "text": "test", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "withered-cell-0-84cbd760-cf55-4f7e-9207-c5f66cccc6dc", + "text": "hi", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "dry-meadow-0-e9203588-43c3-40b1-91f7-f217fc42aa53", + "text": "fantastic", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "withered-cell-0-7e3552d7-7a0d-45f2-a856-e91b23a7e240", + "text": "nice to meet you", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "dry-meadow-0-1ffeafd4-e4fc-4c84-9394-9d7cb10fff42", + "text": "hey", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "dry-meadow-0-3f147324-12c8-4b41-9fb5-2db88d065efa", + "text": "hello, everyone", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "dry-meadow-0-51a348ae-0c0a-44de-a556-eac7891c0cf0", + "text": "who is there?", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "icy-recipe-7-a29e237b-8d81-4a97-9bc8-d42bca3f1356", + "text": "하ì´", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "icy-recipe-7-935c396e-ddf8-4a9a-951c-0a12fa5bf055", + "text": "what are you doing?", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "throbbing-boat-5-1e4d5730-5ff0-4d25-9948-9f34ffda43e4", + "text": "ðŸ‘", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "snowy-credit-3-3e0c1a0d-d22f-42ee-b2a1-f9f49477bf21", + "text": "sdasas", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "snowy-credit-3-3319537e-2d0e-4876-8170-a54f046e4b7d", + "text": "cjshsa", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "snowy-credit-3-cfaf0b46-1daa-49c5-947c-b16d6697487d", + "text": "nhisagdhsadz", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "snowy-credit-3-cebe25a7-a3a3-49fc-9919-91c6725e81f3", + "text": "hvadhsahzd", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "divine-glade-9-0cea9262-5766-48e9-8b22-311870aed3bf", + "text": "hello", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "red-firefly-9-c4e9007b-bb7d-4238-ae08-5f8e3cd03d73", + "text": "hello", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "bitter-glade-2-02aee4eb-4093-4736-808b-2de75820e854", + "text": "hello", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "morning-sea-1-0c700bcb-46dd-4224-b590-e77bdbccc480", + "text": "http://jaeger.ui.gtstrm.com/", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "ancient-salad-0-53e8b4e6-5b7b-43ad-aeee-8bfb6a9ed0be", + "text": "hi", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "ancient-salad-0-8c225075-bd4c-42e2-8024-530aae13cd40", + "text": "hi", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + }, + { + "id": "proud-sea-7-17802096-cbf8-4e3c-addd-4ee31f4c8b5c", + "text": "😃", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null + } + ], + "pinned_messages": [], + "members": [], + "watcher_count": 5 +} diff --git a/packages/stream_chat/test/fixtures/command.json b/packages/stream_chat/test/fixtures/command.json new file mode 100644 index 00000000..38a169a5 --- /dev/null +++ b/packages/stream_chat/test/fixtures/command.json @@ -0,0 +1,5 @@ +{ + "name": "giphy", + "description": "Post a random gif to the channel", + "args": "[text]" +} \ No newline at end of file diff --git a/packages/stream_chat/test/fixtures/device.json b/packages/stream_chat/test/fixtures/device.json new file mode 100644 index 00000000..4555dc1e --- /dev/null +++ b/packages/stream_chat/test/fixtures/device.json @@ -0,0 +1,4 @@ +{ + "id": "device-id", + "push_provider": "push-provider" +} \ No newline at end of file diff --git a/packages/stream_chat/test/fixtures/event.json b/packages/stream_chat/test/fixtures/event.json new file mode 100644 index 00000000..2568af02 --- /dev/null +++ b/packages/stream_chat/test/fixtures/event.json @@ -0,0 +1,29 @@ +{ + "type": "type", + "cid": "cid", + "connection_id": "connectionId", + "created_at": "2019-04-03T18:43:33.213374Z", + "me": { + "id": "dry-meadow-0", + "role": "user", + "created_at": "2019-03-27T17:40:17.155892Z", + "updated_at": "2020-01-29T03:22:47.641589Z", + "last_active": "2020-01-29T03:22:47.63613Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Dry+meadow", + "name": "Dry meadow" + }, + "parent_id": null, + "user": { + "id": "dry-meadow-0", + "role": "user", + "created_at": "2019-03-27T17:40:17.155892Z", + "updated_at": "2020-01-29T03:22:47.641589Z", + "last_active": "2020-01-29T03:22:47.63613Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Dry+meadow", + "name": "Dry meadow" + } +} \ No newline at end of file diff --git a/packages/stream_chat/test/fixtures/member.json b/packages/stream_chat/test/fixtures/member.json new file mode 100644 index 00000000..a33acf13 --- /dev/null +++ b/packages/stream_chat/test/fixtures/member.json @@ -0,0 +1,15 @@ +{ + "user": { + "id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e", + "role": "user", + "created_at": "2020-01-28T22:17:30.826259Z", + "updated_at": "2020-01-28T22:17:31.101222Z", + "banned": false, + "online": false, + "name": "Robin Papa", + "image": "https://pbs.twimg.com/profile_images/669512187778498560/L7wQctBt.jpg" + }, + "role": "member", + "created_at": "2020-01-28T22:17:30.95443Z", + "updated_at": "2020-01-28T22:17:30.95443Z" +} \ No newline at end of file diff --git a/packages/stream_chat/test/fixtures/message.json b/packages/stream_chat/test/fixtures/message.json new file mode 100644 index 00000000..d469639b --- /dev/null +++ b/packages/stream_chat/test/fixtures/message.json @@ -0,0 +1,65 @@ +{ + "id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f", + "text": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA", + "type": "regular", + "silent": false, + "status": "SENT", + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "attachments": [ + { + "type": "video", + "author_name": "GIPHY", + "title": "The Lion King Disney GIF - Find \u0026 Share on GIPHY", + "title_link": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "text": "Discover \u0026 share this Lion King Live Action GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.", + "image_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "thumb_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "asset_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.mp4", + "og_scrape_url": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA" + } + ], + "latest_reactions": [ + { + "message_id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f", + "user_id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "type": "love", + "score": 1, + "created_at": "2020-01-28T22:17:31.128376Z", + "updated_at": "2020-01-28T22:17:31.128376Z" + } + ], + "own_reactions": [], + "reaction_counts": { + "love": 1 + }, + "reaction_scores": { + "love": 1 + }, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null, + "reply_count": 0, + "created_at": "2020-01-28T22:17:31.107978Z", + "updated_at": "2020-01-28T22:17:31.130506Z", + "mentioned_users": [] +} \ No newline at end of file diff --git a/packages/stream_chat/test/fixtures/message_to_json.json b/packages/stream_chat/test/fixtures/message_to_json.json new file mode 100644 index 00000000..b3b1dd6b --- /dev/null +++ b/packages/stream_chat/test/fixtures/message_to_json.json @@ -0,0 +1,29 @@ +{ + "id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f", + "text": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA", + "silent": false, + "attachments": [ + { + "type": "video", + "title_link": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "title": "The Lion King Disney GIF - Find & Share on GIPHY", + "thumb_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "text": "Discover & share this Lion King Live Action GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.", + "og_scrape_url": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA", + "image_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "author_name": "GIPHY", + "asset_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.mp4", + "actions": [] + } + ], + "mentioned_users": [], + "parent_id": "parentId", + "quoted_message": null, + "quoted_message_id": null, + "pinned": false, + "pinned_at": null, + "pin_expires": null, + "pinned_by": null, + "show_in_channel": true, + "hey": "test" +} \ No newline at end of file diff --git a/packages/stream_chat/test/fixtures/reaction.json b/packages/stream_chat/test/fixtures/reaction.json new file mode 100644 index 00000000..ce87ca1d --- /dev/null +++ b/packages/stream_chat/test/fixtures/reaction.json @@ -0,0 +1,18 @@ +{ + "message_id": "76cd8c82-b557-4e48-9d12-87995d3a0e04", + "user_id": "2de0297c-f3f2-489d-b930-ef77342edccf", + "user": { + "id": "2de0297c-f3f2-489d-b930-ef77342edccf", + "role": "user", + "created_at": "2020-01-28T22:17:30.810011Z", + "updated_at": "2020-01-28T22:17:31.077195Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/45.jpg", + "name": "Daisy Morgan" + }, + "type": "wow", + "score": 1, + "created_at": "2020-01-28T22:17:31.108742Z", + "updated_at": "2020-01-28T22:17:31.108742Z" +} \ No newline at end of file diff --git a/packages/stream_chat/test/fixtures/read.json b/packages/stream_chat/test/fixtures/read.json new file mode 100644 index 00000000..7dc596d3 --- /dev/null +++ b/packages/stream_chat/test/fixtures/read.json @@ -0,0 +1,7 @@ +{ + "user": { + "id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e" + }, + "last_read": "2020-01-28T22:17:30.966485504Z", + "unread_messages": 10 +} \ No newline at end of file diff --git a/packages/stream_chat/test/fixtures/user.json b/packages/stream_chat/test/fixtures/user.json new file mode 100644 index 00000000..1fad8ea0 --- /dev/null +++ b/packages/stream_chat/test/fixtures/user.json @@ -0,0 +1,4 @@ +{ + "id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e", + "role": "test-role" +} \ No newline at end of file diff --git a/packages/stream_chat/test/src/core/models/action_test.dart b/packages/stream_chat/test/src/core/models/action_test.dart index d0108948..0cf1c996 100644 --- a/packages/stream_chat/test/src/core/models/action_test.dart +++ b/packages/stream_chat/test/src/core/models/action_test.dart @@ -1,21 +1,12 @@ -import 'dart:convert'; - -import 'package:test/test.dart'; import 'package:stream_chat/src/core/models/action.dart'; +import 'package:test/test.dart'; + +import '../../utils.dart'; void main() { group('src/models/action', () { - const jsonExample = ''' - { - "name": "name", - "style": "style", - "text": "text", - "type": "type", - "value": "value" - }'''; - test('should parse json correctly', () { - final action = Action.fromJson(json.decode(jsonExample)); + final action = Action.fromJson(jsonFixture('action.json')); expect(action.name, 'name'); expect(action.style, 'style'); expect(action.text, 'text'); diff --git a/packages/stream_chat/test/src/core/models/attachment_test.dart b/packages/stream_chat/test/src/core/models/attachment_test.dart index a433bedd..37716656 100644 --- a/packages/stream_chat/test/src/core/models/attachment_test.dart +++ b/packages/stream_chat/test/src/core/models/attachment_test.dart @@ -1,44 +1,13 @@ -import 'dart:convert'; - import 'package:stream_chat/src/core/models/action.dart'; import 'package:stream_chat/src/core/models/attachment.dart'; import 'package:test/test.dart'; +import '../../utils.dart'; + void main() { group('src/models/attachment', () { - const jsonExample = ''' - { - "type": "giphy", - "title": "awesome", - "title_link": "https://giphy.com/gifs/nrkp3-dance-happy-3o7TKnCdBx5cMg0qti", - "thumb_url": "https://media0.giphy.com/media/3o7TKnCdBx5cMg0qti/giphy.gif", - "actions": [ - { - "name": "image_action", - "text": "Send", - "style": "primary", - "type": "button", - "value": "send" - }, - { - "name": "image_action", - "text": "Shuffle", - "style": "default", - "type": "button", - "value": "shuffle" - }, - { - "name": "image_action", - "text": "Cancel", - "style": "default", - "type": "button", - "value": "cancel" - } - ] -}'''; - test('should parse json correctly', () { - final attachment = Attachment.fromJson(json.decode(jsonExample)); + final attachment = Attachment.fromJson(jsonFixture('attachment.json')); expect(attachment.type, 'giphy'); expect(attachment.title, 'awesome'); expect( diff --git a/packages/stream_chat/test/src/core/models/channel_state_test.dart b/packages/stream_chat/test/src/core/models/channel_state_test.dart index ad5245a2..8bd17d46 100644 --- a/packages/stream_chat/test/src/core/models/channel_state_test.dart +++ b/packages/stream_chat/test/src/core/models/channel_state_test.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:stream_chat/src/core/models/channel_config.dart'; import 'package:stream_chat/src/core/models/channel_state.dart'; import 'package:stream_chat/src/core/models/command.dart'; @@ -8,843 +6,13 @@ import 'package:stream_chat/src/core/models/user.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:test/test.dart'; +import '../../utils.dart'; + void main() { group('src/models/channel_state', () { - const jsonExample = ''' - { - "channel": { - "id": "dev", - "type": "team", - "cid": "team:dev", - "last_message_at": "2020-01-30T13:43:41.062362Z", - "created_at": "2019-04-03T18:43:33.213373Z", - "updated_at": "2019-04-03T18:43:33.213374Z", - "team": "test", - "created_by": { - "id": "guido", - "role": "user", - "created_at": "2019-04-03T18:43:33.201036Z", - "updated_at": "2019-04-03T18:43:33.204713Z", - "banned": false, - "online": false, - "name": "Guido" - }, - "frozen": true, - "config": { - "created_at": "2019-11-07T22:29:26.776526Z", - "updated_at": "2019-11-07T22:29:48.286746Z", - "name": "team", - "typing_events": true, - "read_events": true, - "connect_events": true, - "search": true, - "reactions": true, - "replies": true, - "mutes": true, - "uploads": true, - "url_enrichment": true, - "message_retention": "infinite", - "max_message_length": 5000, - "automod": "disabled", - "automod_behavior": "flag", - "commands": [ - { - "name": "giphy", - "description": "Post a random gif to the channel", - "args": "[text]", - "set": "fun_set" - } - ] - }, - "name": "#dev", - "image": "https://cdn.chrisshort.net/testing-certificate-chains-in-go/GOPHER_MIC_DROP.png", - "example": 1 - }, - "messages": [ - { - "id": "dry-meadow-0-2b73cc8b-cd86-4a01-8d40-bd82ad07a030", - "text": "fasdfa", - "type": "regular", - "status": "SENT", - "silent": false, - "user": { - "id": "dry-meadow-0", - "role": "user", - "created_at": "2019-03-27T17:40:17.155892Z", - "updated_at": "2020-01-29T03:22:47.641589Z", - "last_active": "2020-01-29T03:22:47.63613Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?name=Dry+meadow", - "name": "Dry meadow" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 0, - "created_at": "2020-01-29T03:23:02.843948Z", - "updated_at": "2020-01-29T03:23:02.843949Z", - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "dry-meadow-0-e8e74482-b4cd-48db-9d1e-30e6c191786f", - "text": "test message", - "type": "regular", - "user": { - "id": "dry-meadow-0", - "role": "user", - "created_at": "2019-03-27T17:40:17.155892Z", - "updated_at": "2020-01-29T03:22:47.641589Z", - "last_active": "2020-01-29T03:22:47.63613Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?name=Dry+meadow", - "name": "Dry meadow" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 0, - "created_at": "2020-01-29T03:23:07.981091Z", - "updated_at": "2020-01-29T03:23:07.981091Z", - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "dry-meadow-0-53e6299f-9b97-4a9c-a27e-7e2dde49b7e0", - "text": "test message", - "type": "regular", - "user": { - "id": "dry-meadow-0", - "role": "user", - "created_at": "2019-03-27T17:40:17.155892Z", - "updated_at": "2020-01-29T03:22:47.641589Z", - "last_active": "2020-01-29T03:22:47.63613Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?name=Dry+meadow", - "name": "Dry meadow" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 0, - "created_at": "2020-01-29T03:23:11.568022Z", - "updated_at": "2020-01-29T03:23:11.568022Z", - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "dry-meadow-0-80925be0-786e-40a5-b225-486518dafd35", - "text": "asdfadf", - "type": "regular", - "user": { - "id": "dry-meadow-0", - "role": "user", - "created_at": "2019-03-27T17:40:17.155892Z", - "updated_at": "2020-01-29T03:22:47.641589Z", - "last_active": "2020-01-29T03:22:47.63613Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?name=Dry+meadow", - "name": "Dry meadow" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 0, - "created_at": "2020-01-29T03:32:57.403566Z", - "updated_at": "2020-01-29T03:32:57.403566Z", - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "dry-meadow-0-64d7970f-ede8-4b31-9738-1bc1756d2bfe", - "text": "test", - "type": "regular", - "user": { - "id": "dry-meadow-0", - "role": "user", - "created_at": "2019-03-27T17:40:17.155892Z", - "updated_at": "2020-01-29T03:22:47.641589Z", - "last_active": "2020-01-29T03:22:47.63613Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?name=Dry+meadow", - "name": "Dry meadow" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 0, - "created_at": "2020-01-29T03:33:35.294802Z", - "updated_at": "2020-01-29T03:33:35.294802Z", - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "withered-cell-0-84cbd760-cf55-4f7e-9207-c5f66cccc6dc", - "text": "hi", - "type": "regular", - "user": { - "id": "withered-cell-0", - "role": "user", - "created_at": "2020-01-29T03:34:01.698106Z", - "updated_at": "2020-01-29T03:34:01.708808Z", - "last_active": "2020-01-29T03:34:01.70353Z", - "banned": false, - "online": false, - "name": "Withered cell", - "image": "https://getstream.io/random_svg/?name=Withered+cell" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 0, - "created_at": "2020-01-29T03:34:27.393296Z", - "updated_at": "2020-01-29T03:34:27.393296Z", - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "dry-meadow-0-e9203588-43c3-40b1-91f7-f217fc42aa53", - "text": "fantastic", - "type": "regular", - "user": { - "id": "dry-meadow-0", - "role": "user", - "created_at": "2019-03-27T17:40:17.155892Z", - "updated_at": "2020-01-29T03:22:47.641589Z", - "last_active": "2020-01-29T03:22:47.63613Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?name=Dry+meadow", - "name": "Dry meadow" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 0, - "created_at": "2020-01-29T03:34:37.638376Z", - "updated_at": "2020-01-29T03:34:37.638376Z", - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "withered-cell-0-7e3552d7-7a0d-45f2-a856-e91b23a7e240", - "text": "nice to meet you", - "type": "regular", - "user": { - "id": "withered-cell-0", - "role": "user", - "created_at": "2020-01-29T03:34:01.698106Z", - "updated_at": "2020-01-29T03:34:01.708808Z", - "last_active": "2020-01-29T03:34:01.70353Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?name=Withered+cell", - "name": "Withered cell" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 0, - "created_at": "2020-01-29T03:35:04.301566Z", - "updated_at": "2020-01-29T03:35:04.301566Z", - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "dry-meadow-0-1ffeafd4-e4fc-4c84-9394-9d7cb10fff42", - "text": "hey", - "type": "regular", - "user": { - "id": "dry-meadow-0", - "role": "user", - "created_at": "2019-03-27T17:40:17.155892Z", - "updated_at": "2020-01-29T03:22:47.641589Z", - "last_active": "2020-01-29T03:22:47.63613Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?name=Dry+meadow", - "name": "Dry meadow" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 0, - "created_at": "2020-01-29T03:35:24.939084Z", - "updated_at": "2020-01-29T03:35:24.939085Z", - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "dry-meadow-0-3f147324-12c8-4b41-9fb5-2db88d065efa", - "text": "hello, everyone", - "type": "regular", - "user": { - "id": "dry-meadow-0", - "role": "user", - "created_at": "2019-03-27T17:40:17.155892Z", - "updated_at": "2020-01-29T03:22:47.641589Z", - "last_active": "2020-01-29T03:22:47.63613Z", - "banned": false, - "online": false, - "name": "Dry meadow", - "image": "https://getstream.io/random_svg/?name=Dry+meadow" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 0, - "created_at": "2020-01-29T03:35:33.101566Z", - "updated_at": "2020-01-29T03:35:33.101566Z", - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "dry-meadow-0-51a348ae-0c0a-44de-a556-eac7891c0cf0", - "text": "who is there?", - "type": "regular", - "user": { - "id": "dry-meadow-0", - "role": "user", - "created_at": "2019-03-27T17:40:17.155892Z", - "updated_at": "2020-01-29T03:22:47.641589Z", - "last_active": "2020-01-29T03:22:47.63613Z", - "banned": false, - "online": false, - "name": "Dry meadow", - "image": "https://getstream.io/random_svg/?name=Dry+meadow" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 0, - "created_at": "2020-01-29T03:35:45.458685Z", - "updated_at": "2020-01-29T03:35:45.458685Z", - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "icy-recipe-7-a29e237b-8d81-4a97-9bc8-d42bca3f1356", - "text": "하ì´", - "type": "regular", - "user": { - "id": "icy-recipe-7", - "role": "user", - "created_at": "2020-01-21T11:36:22.284503Z", - "updated_at": "2020-01-29T07:01:59.69882Z", - "last_active": "2020-01-29T07:01:59.693378Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?name=Icy+recipe", - "name": "Icy recipe" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 0, - "created_at": "2020-01-29T07:02:11.535395Z", - "updated_at": "2020-01-29T07:02:11.535395Z", - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "icy-recipe-7-935c396e-ddf8-4a9a-951c-0a12fa5bf055", - "text": "what are you doing?", - "type": "regular", - "user": { - "id": "icy-recipe-7", - "role": "user", - "created_at": "2020-01-21T11:36:22.284503Z", - "updated_at": "2020-01-29T07:01:59.69882Z", - "last_active": "2020-01-29T07:01:59.693378Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?name=Icy+recipe", - "name": "Icy recipe" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 0, - "created_at": "2020-01-29T07:02:22.485136Z", - "updated_at": "2020-01-29T07:02:22.485136Z", - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "throbbing-boat-5-1e4d5730-5ff0-4d25-9948-9f34ffda43e4", - "text": "ðŸ‘", - "type": "regular", - "user": { - "id": "throbbing-boat-5", - "role": "user", - "created_at": "2019-07-30T06:29:53.060413Z", - "updated_at": "2020-01-29T14:11:27.80176Z", - "last_active": "2020-01-29T14:11:27.7963Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?name=Throbbing+boat", - "name": "Throbbing boat" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 0, - "created_at": "2020-01-29T14:12:04.688552Z", - "updated_at": "2020-01-29T14:12:04.688552Z", - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "snowy-credit-3-3e0c1a0d-d22f-42ee-b2a1-f9f49477bf21", - "text": "sdasas", - "type": "regular", - "user": { - "id": "snowy-credit-3", - "role": "user", - "created_at": "2020-01-29T15:29:03.693312Z", - "updated_at": "2020-01-29T15:29:03.702648Z", - "last_active": "2020-01-29T15:29:03.696144Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?name=Snowy+credit", - "name": "Snowy credit" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 0, - "created_at": "2020-01-29T15:29:36.011315Z", - "updated_at": "2020-01-29T15:29:36.011316Z", - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "snowy-credit-3-3319537e-2d0e-4876-8170-a54f046e4b7d", - "text": "cjshsa", - "type": "regular", - "user": { - "id": "snowy-credit-3", - "role": "user", - "created_at": "2020-01-29T15:29:03.693312Z", - "updated_at": "2020-01-29T15:29:03.702648Z", - "last_active": "2020-01-29T15:29:03.696144Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?name=Snowy+credit", - "name": "Snowy credit" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 0, - "created_at": "2020-01-29T15:29:41.677819Z", - "updated_at": "2020-01-29T15:29:41.677819Z", - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "snowy-credit-3-cfaf0b46-1daa-49c5-947c-b16d6697487d", - "text": "nhisagdhsadz", - "type": "regular", - "user": { - "id": "snowy-credit-3", - "role": "user", - "created_at": "2020-01-29T15:29:03.693312Z", - "updated_at": "2020-01-29T15:29:03.702648Z", - "last_active": "2020-01-29T15:29:03.696144Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?name=Snowy+credit", - "name": "Snowy credit" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 0, - "created_at": "2020-01-29T15:29:43.354177Z", - "updated_at": "2020-01-29T15:29:43.354177Z", - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "snowy-credit-3-cebe25a7-a3a3-49fc-9919-91c6725e81f3", - "text": "hvadhsahzd", - "type": "regular", - "user": { - "id": "snowy-credit-3", - "role": "user", - "created_at": "2020-01-29T15:29:03.693312Z", - "updated_at": "2020-01-29T15:29:03.702648Z", - "last_active": "2020-01-29T15:29:03.696144Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?name=Snowy+credit", - "name": "Snowy credit" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 0, - "created_at": "2020-01-29T15:29:44.754713Z", - "updated_at": "2020-01-29T15:29:44.754713Z", - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "divine-glade-9-0cea9262-5766-48e9-8b22-311870aed3bf", - "text": "hello", - "type": "regular", - "user": { - "id": "divine-glade-9", - "role": "user", - "created_at": "2020-01-29T17:02:18.312524Z", - "updated_at": "2020-01-29T17:02:18.320187Z", - "last_active": "2020-01-29T17:02:18.315074Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?name=Divine+glade", - "name": "Divine glade" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 0, - "created_at": "2020-01-29T17:02:36.933852Z", - "updated_at": "2020-01-29T17:02:36.933852Z", - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "red-firefly-9-c4e9007b-bb7d-4238-ae08-5f8e3cd03d73", - "text": "hello", - "type": "regular", - "user": { - "id": "red-firefly-9", - "role": "user", - "created_at": "2019-08-02T18:56:39.366516Z", - "updated_at": "2020-01-29T22:13:50.491769Z", - "last_active": "2020-01-29T22:13:50.450215Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?name=Red+firefly", - "name": "Red firefly" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 0, - "created_at": "2020-01-29T22:14:08.54062Z", - "updated_at": "2020-01-29T22:14:08.54062Z", - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "bitter-glade-2-02aee4eb-4093-4736-808b-2de75820e854", - "text": "hello", - "type": "regular", - "user": { - "id": "bitter-glade-2", - "role": "user", - "created_at": "2020-01-30T13:08:56.190678Z", - "updated_at": "2020-01-30T13:08:56.200333Z", - "last_active": "2020-01-30T13:08:56.193882Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?name=Bitter+glade", - "name": "Bitter glade" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 0, - "created_at": "2020-01-30T13:11:37.191293Z", - "updated_at": "2020-01-30T13:11:37.191293Z", - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "morning-sea-1-0c700bcb-46dd-4224-b590-e77bdbccc480", - "text": "http://jaeger.ui.gtstrm.com/", - "type": "regular", - "user": { - "id": "morning-sea-1", - "role": "user", - "created_at": "2019-07-22T09:19:07.505207Z", - "updated_at": "2020-01-30T13:33:05.831856Z", - "last_active": "2020-01-30T13:33:05.825369Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?name=Morning+sea", - "name": "Morning sea" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 0, - "created_at": "2020-01-30T13:33:16.853116Z", - "updated_at": "2020-01-30T13:33:16.853116Z", - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "ancient-salad-0-53e8b4e6-5b7b-43ad-aeee-8bfb6a9ed0be", - "text": "hi", - "type": "regular", - "user": { - "id": "ancient-salad-0", - "role": "user", - "created_at": "2020-01-30T13:34:29.286813Z", - "updated_at": "2020-01-30T13:34:29.296196Z", - "last_active": "2020-01-30T13:34:29.289964Z", - "banned": false, - "online": true, - "image": "https://getstream.io/random_svg/?name=Ancient+salad", - "name": "Ancient salad" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 0, - "created_at": "2020-01-30T13:36:52.749731Z", - "updated_at": "2020-01-30T13:36:52.749732Z", - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "ancient-salad-0-8c225075-bd4c-42e2-8024-530aae13cd40", - "text": "hi", - "type": "regular", - "user": { - "id": "ancient-salad-0", - "role": "user", - "created_at": "2020-01-30T13:34:29.286813Z", - "updated_at": "2020-01-30T13:34:29.296196Z", - "last_active": "2020-01-30T13:34:29.289964Z", - "banned": false, - "online": true, - "image": "https://getstream.io/random_svg/?name=Ancient+salad", - "name": "Ancient salad" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 0, - "created_at": "2020-01-30T13:37:41.631056Z", - "updated_at": "2020-01-30T13:37:41.631056Z", - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "proud-sea-7-17802096-cbf8-4e3c-addd-4ee31f4c8b5c", - "text": "😃", - "type": "regular", - "user": { - "id": "proud-sea-7", - "role": "user", - "created_at": "2020-01-30T13:43:03.903006Z", - "updated_at": "2020-01-30T13:43:03.912307Z", - "last_active": "2020-01-30T13:43:03.906236Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?name=Proud+sea", - "name": "Proud sea" - }, - "attachments": [], - "latest_reactions": [], - "own_reactions": [], - "reaction_counts": {}, - "reaction_scores": {}, - "reply_count": 0, - "created_at": "2020-01-30T13:43:41.062362Z", - "updated_at": "2020-01-30T13:43:41.062362Z", - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - } - ], - "watcher_count": 5, - "members": [] - }'''; - test('should parse json correctly', () { - final channelState = ChannelState.fromJson(json.decode(jsonExample)); + final channelState = + ChannelState.fromJson(jsonFixture('channel_state.json')); expect(channelState.channel?.cid, 'team:dev'); expect(channelState.channel?.id, 'dev'); expect(channelState.channel?.team, 'test'); @@ -879,426 +47,7 @@ void main() { }); test('should serialize to json correctly', () { - const toJsonExample = ''' - { - "channel": { - "id": "dev", - "type": "team", - "frozen": true, - "name": "#dev", - "image": "https://cdn.chrisshort.net/testing-certificate-chains-in-go/GOPHER_MIC_DROP.png", - "example": 1 - }, - "watchers": [], - "read": [], - "messages": [ - { - "id": "dry-meadow-0-2b73cc8b-cd86-4a01-8d40-bd82ad07a030", - "text": "fasdfa", - "attachments": [], - "parent_id": null, - "quoted_message": null, - "quoted_message_id": null, - "show_in_channel": null, - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "dry-meadow-0-e8e74482-b4cd-48db-9d1e-30e6c191786f", - "text": "test message", - "attachments": [], - "parent_id": null, - "quoted_message": null, - "quoted_message_id": null, - "show_in_channel": null, - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "dry-meadow-0-53e6299f-9b97-4a9c-a27e-7e2dde49b7e0", - "text": "test message", - "attachments": [], - "parent_id": null, - "quoted_message": null, - "quoted_message_id": null, - "show_in_channel": null, - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "dry-meadow-0-80925be0-786e-40a5-b225-486518dafd35", - "text": "asdfadf", - "attachments": [], - "parent_id": null, - "quoted_message": null, - "quoted_message_id": null, - "show_in_channel": null, - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "dry-meadow-0-64d7970f-ede8-4b31-9738-1bc1756d2bfe", - "text": "test", - "attachments": [], - "parent_id": null, - "quoted_message": null, - "quoted_message_id": null, - "show_in_channel": null, - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "withered-cell-0-84cbd760-cf55-4f7e-9207-c5f66cccc6dc", - "text": "hi", - "attachments": [], - "parent_id": null, - "quoted_message": null, - "quoted_message_id": null, - "show_in_channel": null, - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "dry-meadow-0-e9203588-43c3-40b1-91f7-f217fc42aa53", - "text": "fantastic", - "attachments": [], - "parent_id": null, - "quoted_message": null, - "quoted_message_id": null, - "show_in_channel": null, - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "withered-cell-0-7e3552d7-7a0d-45f2-a856-e91b23a7e240", - "text": "nice to meet you", - "attachments": [], - "parent_id": null, - "quoted_message": null, - "quoted_message_id": null, - "show_in_channel": null, - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "dry-meadow-0-1ffeafd4-e4fc-4c84-9394-9d7cb10fff42", - "text": "hey", - "attachments": [], - "parent_id": null, - "quoted_message": null, - "quoted_message_id": null, - "show_in_channel": null, - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "dry-meadow-0-3f147324-12c8-4b41-9fb5-2db88d065efa", - "text": "hello, everyone", - "attachments": [], - "parent_id": null, - "quoted_message": null, - "quoted_message_id": null, - "show_in_channel": null, - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "dry-meadow-0-51a348ae-0c0a-44de-a556-eac7891c0cf0", - "text": "who is there?", - "attachments": [], - "parent_id": null, - "quoted_message": null, - "quoted_message_id": null, - "show_in_channel": null, - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "icy-recipe-7-a29e237b-8d81-4a97-9bc8-d42bca3f1356", - "text": "하ì´", - "attachments": [], - "parent_id": null, - "quoted_message": null, - "quoted_message_id": null, - "show_in_channel": null, - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "icy-recipe-7-935c396e-ddf8-4a9a-951c-0a12fa5bf055", - "text": "what are you doing?", - "attachments": [], - "parent_id": null, - "quoted_message": null, - "quoted_message_id": null, - "show_in_channel": null, - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "throbbing-boat-5-1e4d5730-5ff0-4d25-9948-9f34ffda43e4", - "text": "ðŸ‘", - "attachments": [], - "parent_id": null, - "quoted_message": null, - "quoted_message_id": null, - "show_in_channel": null, - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "snowy-credit-3-3e0c1a0d-d22f-42ee-b2a1-f9f49477bf21", - "text": "sdasas", - "attachments": [], - "parent_id": null, - "quoted_message": null, - "quoted_message_id": null, - "show_in_channel": null, - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "snowy-credit-3-3319537e-2d0e-4876-8170-a54f046e4b7d", - "text": "cjshsa", - "attachments": [], - "parent_id": null, - "quoted_message": null, - "quoted_message_id": null, - "show_in_channel": null, - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "snowy-credit-3-cfaf0b46-1daa-49c5-947c-b16d6697487d", - "text": "nhisagdhsadz", - "attachments": [], - "parent_id": null, - "quoted_message": null, - "quoted_message_id": null, - "show_in_channel": null, - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "snowy-credit-3-cebe25a7-a3a3-49fc-9919-91c6725e81f3", - "text": "hvadhsahzd", - "attachments": [], - "parent_id": null, - "quoted_message": null, - "quoted_message_id": null, - "show_in_channel": null, - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "divine-glade-9-0cea9262-5766-48e9-8b22-311870aed3bf", - "text": "hello", - "attachments": [], - "parent_id": null, - "quoted_message": null, - "quoted_message_id": null, - "show_in_channel": null, - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "red-firefly-9-c4e9007b-bb7d-4238-ae08-5f8e3cd03d73", - "text": "hello", - "attachments": [], - "parent_id": null, - "quoted_message": null, - "quoted_message_id": null, - "show_in_channel": null, - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "bitter-glade-2-02aee4eb-4093-4736-808b-2de75820e854", - "text": "hello", - "attachments": [], - "parent_id": null, - "quoted_message": null, - "quoted_message_id": null, - "show_in_channel": null, - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "morning-sea-1-0c700bcb-46dd-4224-b590-e77bdbccc480", - "text": "http://jaeger.ui.gtstrm.com/", - "attachments": [], - "parent_id": null, - "quoted_message": null, - "quoted_message_id": null, - "show_in_channel": null, - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "ancient-salad-0-53e8b4e6-5b7b-43ad-aeee-8bfb6a9ed0be", - "text": "hi", - "attachments": [], - "parent_id": null, - "quoted_message": null, - "quoted_message_id": null, - "show_in_channel": null, - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "ancient-salad-0-8c225075-bd4c-42e2-8024-530aae13cd40", - "text": "hi", - "attachments": [], - "parent_id": null, - "quoted_message": null, - "quoted_message_id": null, - "show_in_channel": null, - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - }, - { - "id": "proud-sea-7-17802096-cbf8-4e3c-addd-4ee31f4c8b5c", - "text": "😃", - "attachments": [], - "parent_id": null, - "quoted_message": null, - "quoted_message_id": null, - "show_in_channel": null, - "mentioned_users": [], - "status": "SENT", - "silent": false, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null - } - ], - "pinned_messages": [], - "members": [], - "watcher_count": 5 - } - '''; - final j = jsonDecode(jsonExample); + final j = jsonFixture('channel_state.json'); final channelState = ChannelState( channel: ChannelModel.fromJson(j['channel']), members: [], @@ -1312,7 +61,7 @@ void main() { expect( channelState.toJson(), - jsonDecode(toJsonExample), + jsonFixture('channel_state_to_json.json'), ); }); }); diff --git a/packages/stream_chat/test/src/core/models/channel_test.dart b/packages/stream_chat/test/src/core/models/channel_test.dart index 1bb400a6..ca734c44 100644 --- a/packages/stream_chat/test/src/core/models/channel_test.dart +++ b/packages/stream_chat/test/src/core/models/channel_test.dart @@ -1,22 +1,12 @@ -import 'dart:convert'; - import 'package:stream_chat/src/core/models/channel_model.dart'; import 'package:test/test.dart'; +import '../../utils.dart'; + void main() { group('src/models/channel', () { - const jsonExample = ''' - { - "id": "test", - "type": "livestream", - "cid": "livestream:test", - "cats": true, - "fruit": ["bananas", "apples"] - } - '''; - test('should parse json correctly', () { - final channel = ChannelModel.fromJson(json.decode(jsonExample)); + final channel = ChannelModel.fromJson(jsonFixture('channel.json')); expect(channel.id, equals('test')); expect(channel.type, equals('livestream')); expect(channel.cid, equals('livestream:test')); diff --git a/packages/stream_chat/test/src/core/models/command_test.dart b/packages/stream_chat/test/src/core/models/command_test.dart index 3306fd0f..e8ffb81e 100644 --- a/packages/stream_chat/test/src/core/models/command_test.dart +++ b/packages/stream_chat/test/src/core/models/command_test.dart @@ -1,20 +1,12 @@ -import 'dart:convert'; - import 'package:stream_chat/src/core/models/command.dart'; import 'package:test/test.dart'; +import '../../utils.dart'; + void main() { group('src/models/command', () { - const jsonExample = ''' - { - "name": "giphy", - "description": "Post a random gif to the channel", - "args": "[text]" - } - '''; - test('should parse json correctly', () { - final command = Command.fromJson(json.decode(jsonExample)); + final command = Command.fromJson(jsonFixture('command.json')); expect(command.name, 'giphy'); expect(command.description, 'Post a random gif to the channel'); expect(command.args, '[text]'); diff --git a/packages/stream_chat/test/src/core/models/device_test.dart b/packages/stream_chat/test/src/core/models/device_test.dart index f4700b7e..b3ac707b 100644 --- a/packages/stream_chat/test/src/core/models/device_test.dart +++ b/packages/stream_chat/test/src/core/models/device_test.dart @@ -1,18 +1,12 @@ -import 'dart:convert'; - -import 'package:test/test.dart'; import 'package:stream_chat/src/core/models/device.dart'; +import 'package:test/test.dart'; + +import '../../utils.dart'; void main() { group('src/models/device', () { - const jsonExample = ''' - { - "id": "device-id", - "push_provider": "push-provider" - }'''; - test('should parse json correctly', () { - final device = Device.fromJson(json.decode(jsonExample)); + final device = Device.fromJson(jsonFixture('device.json')); expect(device.id, 'device-id'); expect(device.pushProvider, 'push-provider'); }); diff --git a/packages/stream_chat/test/src/core/models/event_test.dart b/packages/stream_chat/test/src/core/models/event_test.dart index b05da962..7ebd82fa 100644 --- a/packages/stream_chat/test/src/core/models/event_test.dart +++ b/packages/stream_chat/test/src/core/models/event_test.dart @@ -1,46 +1,14 @@ -import 'dart:convert'; - import 'package:stream_chat/src/core/models/event.dart'; import 'package:stream_chat/src/core/models/own_user.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:test/test.dart'; +import '../../utils.dart'; + void main() { group('src/models/event', () { - const jsonExample = ''' - { - "type": "type", - "cid": "cid", - "connection_id": "connectionId", - "created_at": "2019-04-03T18:43:33.213374Z", - "me": { - "id": "dry-meadow-0", - "role": "user", - "created_at": "2019-03-27T17:40:17.155892Z", - "updated_at": "2020-01-29T03:22:47.641589Z", - "last_active": "2020-01-29T03:22:47.63613Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?name=Dry+meadow", - "name": "Dry meadow" - }, - "parent_id": null, - "user": { - "id": "dry-meadow-0", - "role": "user", - "created_at": "2019-03-27T17:40:17.155892Z", - "updated_at": "2020-01-29T03:22:47.641589Z", - "last_active": "2020-01-29T03:22:47.63613Z", - "banned": false, - "online": false, - "image": "https://getstream.io/random_svg/?name=Dry+meadow", - "name": "Dry meadow" - } - } - '''; - test('should parse json correctly', () { - final event = Event.fromJson(json.decode(jsonExample)); + final event = Event.fromJson(jsonFixture('event.json')); expect(event.type, 'type'); expect(event.cid, 'cid'); expect(event.connectionId, 'connectionId'); diff --git a/packages/stream_chat/test/src/core/models/member_test.dart b/packages/stream_chat/test/src/core/models/member_test.dart index 54c258e1..4cd8efda 100644 --- a/packages/stream_chat/test/src/core/models/member_test.dart +++ b/packages/stream_chat/test/src/core/models/member_test.dart @@ -1,31 +1,13 @@ -import 'dart:convert'; - -import 'package:test/test.dart'; import 'package:stream_chat/src/core/models/member.dart'; import 'package:stream_chat/src/core/models/user.dart'; +import 'package:test/test.dart'; + +import '../../utils.dart'; void main() { group('src/models/member', () { - const jsonExample = ''' - { - "user": { - "id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e", - "role": "user", - "created_at": "2020-01-28T22:17:30.826259Z", - "updated_at": "2020-01-28T22:17:31.101222Z", - "banned": false, - "online": false, - "name": "Robin Papa", - "image": "https://pbs.twimg.com/profile_images/669512187778498560/L7wQctBt.jpg" - }, - "role": "member", - "created_at": "2020-01-28T22:17:30.95443Z", - "updated_at": "2020-01-28T22:17:30.95443Z" - } - '''; - test('should parse json correctly', () { - final member = Member.fromJson(json.decode(jsonExample)); + final member = Member.fromJson(jsonFixture('member.json')); expect(member.user, isA()); expect(member.role, 'member'); expect(member.createdAt, DateTime.parse('2020-01-28T22:17:30.95443Z')); diff --git a/packages/stream_chat/test/src/core/models/message_test.dart b/packages/stream_chat/test/src/core/models/message_test.dart index f7b8f003..a5571430 100644 --- a/packages/stream_chat/test/src/core/models/message_test.dart +++ b/packages/stream_chat/test/src/core/models/message_test.dart @@ -1,82 +1,15 @@ -import 'dart:convert'; - import 'package:stream_chat/src/core/models/attachment.dart'; import 'package:stream_chat/src/core/models/message.dart'; import 'package:stream_chat/src/core/models/reaction.dart'; import 'package:stream_chat/src/core/models/user.dart'; import 'package:test/test.dart'; +import '../../utils.dart'; + void main() { group('src/models/message', () { - const jsonExample = r''' - { - "id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f", - "text": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA", - "type": "regular", - "silent": false, - "status": "SENT", - "user": { - "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", - "role": "user", - "created_at": "2020-01-28T22:17:30.83015Z", - "updated_at": "2020-01-28T22:17:31.19435Z", - "banned": false, - "online": false, - "image": "https://randomuser.me/api/portraits/women/2.jpg", - "name": "Mia Denys" - }, - "attachments": [ - { - "type": "video", - "author_name": "GIPHY", - "title": "The Lion King Disney GIF - Find \u0026 Share on GIPHY", - "title_link": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", - "text": "Discover \u0026 share this Lion King Live Action GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.", - "image_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", - "thumb_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", - "asset_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.mp4", - "og_scrape_url": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA" - } - ], - "latest_reactions": [ - { - "message_id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f", - "user_id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", - "user": { - "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", - "role": "user", - "created_at": "2020-01-28T22:17:30.83015Z", - "updated_at": "2020-01-28T22:17:31.19435Z", - "banned": false, - "online": false, - "image": "https://randomuser.me/api/portraits/women/2.jpg", - "name": "Mia Denys" - }, - "type": "love", - "score": 1, - "created_at": "2020-01-28T22:17:31.128376Z", - "updated_at": "2020-01-28T22:17:31.128376Z" - } - ], - "own_reactions": [], - "reaction_counts": { - "love": 1 - }, - "reaction_scores": { - "love": 1 - }, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null, - "reply_count": 0, - "created_at": "2020-01-28T22:17:31.107978Z", - "updated_at": "2020-01-28T22:17:31.130506Z", - "mentioned_users": [] - }'''; - test('should parse json correctly', () { - final message = Message.fromJson(json.decode(jsonExample)); + final message = Message.fromJson(jsonFixture('message.json')); expect(message.id, '4637f7e4-a06b-42db-ba5a-8d8270dd926f'); expect(message.text, 'https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA'); @@ -128,37 +61,7 @@ void main() { expect( message.toJson(), - json.decode(''' - { - "id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f", - "text": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA", - "silent": false, - "attachments": [ - { - "type": "video", - "title_link": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", - "title": "The Lion King Disney GIF - Find & Share on GIPHY", - "thumb_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", - "text": "Discover & share this Lion King Live Action GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.", - "og_scrape_url": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA", - "image_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", - "author_name": "GIPHY", - "asset_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.mp4", - "actions": [] - } - ], - "mentioned_users": [], - "parent_id": "parentId", - "quoted_message": null, - "quoted_message_id": null, - "pinned": false, - "pinned_at": null, - "pin_expires": null, - "pinned_by": null, - "show_in_channel": true, - "hey": "test" - } - '''), + jsonFixture('message_to_json.json'), ); }); }); diff --git a/packages/stream_chat/test/src/core/models/reaction_test.dart b/packages/stream_chat/test/src/core/models/reaction_test.dart index b69bd986..facb8d07 100644 --- a/packages/stream_chat/test/src/core/models/reaction_test.dart +++ b/packages/stream_chat/test/src/core/models/reaction_test.dart @@ -1,34 +1,13 @@ -import 'dart:convert'; - import 'package:stream_chat/src/core/models/reaction.dart'; import 'package:stream_chat/src/core/models/user.dart'; import 'package:test/test.dart'; +import '../../utils.dart'; + void main() { group('src/models/reaction', () { - const jsonExample = ''' - { - "message_id": "76cd8c82-b557-4e48-9d12-87995d3a0e04", - "user_id": "2de0297c-f3f2-489d-b930-ef77342edccf", - "user": { - "id": "2de0297c-f3f2-489d-b930-ef77342edccf", - "role": "user", - "created_at": "2020-01-28T22:17:30.810011Z", - "updated_at": "2020-01-28T22:17:31.077195Z", - "banned": false, - "online": false, - "image": "https://randomuser.me/api/portraits/women/45.jpg", - "name": "Daisy Morgan" - }, - "type": "wow", - "score": 1, - "created_at": "2020-01-28T22:17:31.108742Z", - "updated_at": "2020-01-28T22:17:31.108742Z" - } - '''; - test('should parse json correctly', () { - final reaction = Reaction.fromJson(json.decode(jsonExample)); + final reaction = Reaction.fromJson(jsonFixture('reaction.json')); expect(reaction.messageId, '76cd8c82-b557-4e48-9d12-87995d3a0e04'); expect(reaction.createdAt, DateTime.parse('2020-01-28T22:17:31.108742Z')); expect(reaction.type, 'wow'); diff --git a/packages/stream_chat/test/src/core/models/read_test.dart b/packages/stream_chat/test/src/core/models/read_test.dart index 47c252ac..eabfc18c 100644 --- a/packages/stream_chat/test/src/core/models/read_test.dart +++ b/packages/stream_chat/test/src/core/models/read_test.dart @@ -1,23 +1,13 @@ -import 'dart:convert'; - -import 'package:test/test.dart'; import 'package:stream_chat/src/core/models/read.dart'; import 'package:stream_chat/src/core/models/user.dart'; +import 'package:test/test.dart'; + +import '../../utils.dart'; void main() { group('src/models/read', () { - const jsonExample = ''' - { - "user": { - "id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e" - }, - "last_read": "2020-01-28T22:17:30.966485504Z", - "unread_messages": 10 - } - '''; - test('should parse json correctly', () { - final read = Read.fromJson(json.decode(jsonExample)); + final read = Read.fromJson(jsonFixture('read.json')); expect(read.lastRead, DateTime.parse('2020-01-28T22:17:30.966485504Z')); expect(read.user.id, 'bbb19d9a-ee50-45bc-84e5-0584e79d0c9e'); expect(read.unreadMessages, 10); diff --git a/packages/stream_chat/test/src/core/models/user_test.dart b/packages/stream_chat/test/src/core/models/user_test.dart index f4ff5ee0..89d22b30 100644 --- a/packages/stream_chat/test/src/core/models/user_test.dart +++ b/packages/stream_chat/test/src/core/models/user_test.dart @@ -1,19 +1,12 @@ -import 'dart:convert'; - import 'package:stream_chat/src/core/models/user.dart'; import 'package:test/test.dart'; +import '../../utils.dart'; + void main() { group('src/models/user', () { - const jsonExample = ''' - { - "id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e", - "role": "test-role" - } - '''; - test('should parse json correctly', () { - final user = User.fromJson(json.decode(jsonExample)); + final user = User.fromJson(jsonFixture('user.json')); expect(user.id, 'bbb19d9a-ee50-45bc-84e5-0584e79d0c9e'); }); diff --git a/packages/stream_chat/test/src/utils.dart b/packages/stream_chat/test/src/utils.dart index 91a61204..768d2fe2 100644 --- a/packages/stream_chat/test/src/utils.dart +++ b/packages/stream_chat/test/src/utils.dart @@ -1,10 +1,18 @@ +import 'dart:convert'; import 'dart:io'; +String fixture(String name) { + final dir = currentDirectory.path; + return File('$dir/test/fixtures/$name').readAsStringSync(); +} + File assetFile(String name) { final dir = currentDirectory.path; return File('$dir/test/assets/$name'); } +Map jsonFixture(String name) => json.decode(fixture(name)); + // https://github.com/flutter/flutter/issues/20907 Directory get currentDirectory { var directory = Directory.current; diff --git a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.g.dart b/packages/stream_chat_persistence/lib/src/db/moor_chat_database.g.dart index 09156642..fe921e70 100644 --- a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.g.dart +++ b/packages/stream_chat_persistence/lib/src/db/moor_chat_database.g.dart @@ -3550,7 +3550,7 @@ class UsersCompanion extends UpdateCompanion { this.online = const Value.absent(), this.banned = const Value.absent(), required Map extraData, - }) : id = Value(id), + }) : id = Value(id), extraData = Value(extraData); static Insertable custom({ Expression? id, @@ -4723,7 +4723,7 @@ class ChannelQueriesCompanion extends UpdateCompanion { ChannelQueriesCompanion.insert({ required String queryHash, required String channelCid, - }) : queryHash = Value(queryHash), + }) : queryHash = Value(queryHash), channelCid = Value(channelCid); static Insertable custom({ Expression? queryHash, From 51f8e030f51dc2bf3d5a9d20c9b3e6d2b777f477 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 16 Jun 2021 13:58:24 +0200 Subject: [PATCH 048/150] add mute test --- .../stream_chat/lib/src/core/models/mute.dart | 9 +-- .../lib/src/core/models/mute.g.dart | 16 ---- packages/stream_chat/test/fixtures/mute.json | 74 +++++++++++++++++++ .../test/src/core/models/mute_test.dart | 17 +++++ 4 files changed, 95 insertions(+), 21 deletions(-) create mode 100644 packages/stream_chat/test/fixtures/mute.json create mode 100644 packages/stream_chat/test/src/core/models/mute_test.dart diff --git a/packages/stream_chat/lib/src/core/models/mute.dart b/packages/stream_chat/lib/src/core/models/mute.dart index 9166d3e4..4b14ad01 100644 --- a/packages/stream_chat/lib/src/core/models/mute.dart +++ b/packages/stream_chat/lib/src/core/models/mute.dart @@ -1,12 +1,14 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/core/models/channel_model.dart'; -import 'package:stream_chat/src/core/util/serializer.dart'; import 'package:stream_chat/src/core/models/user.dart'; +import 'package:stream_chat/src/core/util/serializer.dart'; part 'mute.g.dart'; /// The class that contains the information about a muted user -@JsonSerializable() +@JsonSerializable( + createToJson: false, +) class Mute { /// Constructor used for json serialization Mute({ @@ -34,7 +36,4 @@ class Mute { /// The date of the last update @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final DateTime updatedAt; - - /// Serialize to json - Map toJson() => _$MuteToJson(this); } diff --git a/packages/stream_chat/lib/src/core/models/mute.g.dart b/packages/stream_chat/lib/src/core/models/mute.g.dart index e77b8707..b847a109 100644 --- a/packages/stream_chat/lib/src/core/models/mute.g.dart +++ b/packages/stream_chat/lib/src/core/models/mute.g.dart @@ -14,19 +14,3 @@ Mute _$MuteFromJson(Map json) { updatedAt: DateTime.parse(json['updated_at'] as String), ); } - -Map _$MuteToJson(Mute instance) { - final val = {}; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('user', readonly(instance.user)); - writeNotNull('channel', readonly(instance.channel)); - writeNotNull('created_at', readonly(instance.createdAt)); - writeNotNull('updated_at', readonly(instance.updatedAt)); - return val; -} diff --git a/packages/stream_chat/test/fixtures/mute.json b/packages/stream_chat/test/fixtures/mute.json new file mode 100644 index 00000000..cc6c3a8c --- /dev/null +++ b/packages/stream_chat/test/fixtures/mute.json @@ -0,0 +1,74 @@ +{ + "user": { + "id": "super-band-9", + "role": "user", + "created_at": "2020-03-03T16:48:28.853674Z", + "updated_at": "2021-05-26T03:22:20.296181Z", + "last_active": "2021-06-16T11:42:29.466165498Z", + "banned": false, + "online": true, + "username": "Rioland", + "image": "https://placehold.jp/150x150.png", + "invisible": false, + "name": "Proud darkness", + "unread_count": 0 + }, + "channel": { + "id": "!members-Qsp7PpigdPkW0rJk0603y5GnTiF1iRfoDc4SAngMMmw", + "type": "messaging", + "cid": "messaging:!members-Qsp7PpigdPkW0rJk0603y5GnTiF1iRfoDc4SAngMMmw", + "last_message_at": "2020-12-02T06:56:18.003432Z", + "created_at": "2020-11-30T10:25:32.494601Z", + "updated_at": "2020-11-30T10:25:32.494601Z", + "created_by": { + "id": "super-band-9", + "role": "user", + "created_at": "2020-03-03T16:48:28.853674Z", + "updated_at": "2021-05-26T03:22:20.296181Z", + "last_active": "2021-06-16T11:42:29.466165498Z", + "banned": false, + "online": true, + "image": "https://placehold.jp/150x150.png", + "invisible": false, + "name": "Proud darkness", + "unread_count": 0, + "username": "Rioland" + }, + "frozen": false, + "disabled": false, + "member_count": 2, + "config": { + "created_at": "2020-04-15T14:57:17.00966Z", + "updated_at": "2021-05-25T14:25:30.405621Z", + "name": "messaging", + "typing_events": true, + "read_events": true, + "connect_events": true, + "search": true, + "reactions": true, + "replies": true, + "mutes": true, + "uploads": true, + "url_enrichment": true, + "custom_events": false, + "push_notifications": true, + "message_retention": "infinite", + "max_message_length": 5000, + "automod": "disabled", + "automod_behavior": "flag", + "blocklist": "profanity_en_2020_v1", + "blocklist_behavior": "block", + "automod_thresholds": {}, + "commands": [ + { + "name": "giphy", + "description": "Post a random gif to the channel", + "args": "[text]", + "set": "fun_set" + } + ] + } + }, + "created_at": "2020-12-04T10:39:06.512021Z", + "updated_at": "2020-12-04T10:39:06.512021Z" +} \ No newline at end of file diff --git a/packages/stream_chat/test/src/core/models/mute_test.dart b/packages/stream_chat/test/src/core/models/mute_test.dart new file mode 100644 index 00000000..f1f7b606 --- /dev/null +++ b/packages/stream_chat/test/src/core/models/mute_test.dart @@ -0,0 +1,17 @@ +import 'package:stream_chat/src/core/models/channel_model.dart'; +import 'package:stream_chat/src/core/models/mute.dart'; +import 'package:stream_chat/src/core/models/user.dart'; +import 'package:test/test.dart'; + +import '../../utils.dart'; + +void main() { + group('src/models/mute', () { + test('should parse json correctly', () { + final mute = Mute.fromJson(jsonFixture('mute.json')); + expect(mute.channel, isA()); + expect(mute.user, isA()); + expect(mute.createdAt, DateTime.parse('2020-12-04T10:39:06.512021Z')); + }); + }); +} From 4e9699dbf49816104f9d4975671f5604cefa60f1 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 16 Jun 2021 16:06:55 +0200 Subject: [PATCH 049/150] add OwnUser test --- .../lib/src/core/models/own_user.dart | 25 ++--- .../lib/src/core/models/own_user.g.dart | 26 ----- .../stream_chat/test/fixtures/own_user.json | 100 ++++++++++++++++++ .../test/src/core/models/own_user_test.dart | 45 ++++++++ 4 files changed, 152 insertions(+), 44 deletions(-) create mode 100644 packages/stream_chat/test/fixtures/own_user.json create mode 100644 packages/stream_chat/test/src/core/models/own_user_test.dart diff --git a/packages/stream_chat/lib/src/core/models/own_user.dart b/packages/stream_chat/lib/src/core/models/own_user.dart index 22705b05..7ceffbfa 100644 --- a/packages/stream_chat/lib/src/core/models/own_user.dart +++ b/packages/stream_chat/lib/src/core/models/own_user.dart @@ -1,14 +1,14 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/core/models/device.dart'; import 'package:stream_chat/src/core/models/mute.dart'; -import 'package:stream_chat/src/core/util/serializer.dart'; import 'package:stream_chat/src/core/models/user.dart'; +import 'package:stream_chat/src/core/util/serializer.dart'; part 'own_user.g.dart'; /// The class that defines the own user model /// This object can be found in [Event] -@JsonSerializable() +@JsonSerializable(createToJson: false) class OwnUser extends User { /// Constructor used for json serialization OwnUser({ @@ -53,28 +53,23 @@ class OwnUser extends User { ); /// List of user devices - @JsonKey( - includeIfNull: false, - toJson: Serializer.readOnly, - defaultValue: []) + @JsonKey(includeIfNull: false, defaultValue: []) final List devices; /// List of users muted by the user - @JsonKey( - includeIfNull: false, toJson: Serializer.readOnly, defaultValue: []) + @JsonKey(includeIfNull: false, defaultValue: []) final List mutes; /// List of users muted by the user - @JsonKey( - includeIfNull: false, toJson: Serializer.readOnly, defaultValue: []) + @JsonKey(includeIfNull: false, defaultValue: []) final List channelMutes; /// Total unread messages by the user - @JsonKey(includeIfNull: false, toJson: Serializer.readOnly, defaultValue: 0) + @JsonKey(includeIfNull: false, defaultValue: 0) final int totalUnreadCount; /// Total unread channels by the user - @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) + @JsonKey(includeIfNull: false) final int? unreadChannels; /// Known top level fields. @@ -87,10 +82,4 @@ class OwnUser extends User { 'channel_mutes', ...User.topLevelFields, ]; - - /// Serialize to json - @override - Map toJson() => Serializer.moveFromExtraDataToRoot( - _$OwnUserToJson(this), - ); } diff --git a/packages/stream_chat/lib/src/core/models/own_user.g.dart b/packages/stream_chat/lib/src/core/models/own_user.g.dart index 760edc01..26e4786e 100644 --- a/packages/stream_chat/lib/src/core/models/own_user.g.dart +++ b/packages/stream_chat/lib/src/core/models/own_user.g.dart @@ -38,29 +38,3 @@ OwnUser _$OwnUserFromJson(Map json) { banned: json['banned'] as bool? ?? false, ); } - -Map _$OwnUserToJson(OwnUser instance) { - final val = { - 'id': instance.id, - }; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('role', readonly(instance.role)); - writeNotNull('created_at', readonly(instance.createdAt)); - writeNotNull('updated_at', readonly(instance.updatedAt)); - writeNotNull('last_active', readonly(instance.lastActive)); - writeNotNull('online', readonly(instance.online)); - writeNotNull('banned', readonly(instance.banned)); - val['extra_data'] = instance.extraData; - writeNotNull('devices', readonly(instance.devices)); - writeNotNull('mutes', readonly(instance.mutes)); - writeNotNull('channel_mutes', readonly(instance.channelMutes)); - writeNotNull('total_unread_count', readonly(instance.totalUnreadCount)); - writeNotNull('unread_channels', readonly(instance.unreadChannels)); - return val; -} diff --git a/packages/stream_chat/test/fixtures/own_user.json b/packages/stream_chat/test/fixtures/own_user.json new file mode 100644 index 00000000..dc6aabd1 --- /dev/null +++ b/packages/stream_chat/test/fixtures/own_user.json @@ -0,0 +1,100 @@ +{ + "id": "super-band-9", + "role": "user", + "created_at": "2020-03-03T16:48:28.853674Z", + "updated_at": "2021-05-26T03:22:20.296181Z", + "last_active": "2021-06-16T11:59:59.003453014Z", + "banned": false, + "online": true, + "devices": [ + { + "push_provider": "firebase", + "id": "cRS8elU4Q-qqdCAvHR2kSa:APA91bFy7MEgPyXbnFWi3uoanr_x8Vbi42JcWOXlg8p3vyIL5FuW4bjpVfamqQjYCgwDGxPA0C4qavOadE-uiKeGQJp6Sp5D2KDW9Od_BlDqzwEPJnVG9gC1zbj7NKCfXRqbOA2Wh2mW", + "created_at": "2020-04-23T14:36:21.838196Z", + "user_id": "super-band-9" + } + ], + "mutes": [], + "channel_mutes": [ + { + "user": { + "id": "super-band-9", + "role": "user", + "created_at": "2020-03-03T16:48:28.853674Z", + "updated_at": "2021-05-26T03:22:20.296181Z", + "last_active": "2021-06-16T11:59:59.003453014Z", + "banned": false, + "online": true, + "image": "https://placehold.jp/150x150.png", + "invisible": false, + "name": "Proud darkness", + "unread_count": 0, + "username": "Rioland" + }, + "channel": { + "id": "!members-Qsp7PpigdPkW0rJk0603y5GnTiF1iRfoDc4SAngMMmw", + "type": "messaging", + "cid": "messaging:!members-Qsp7PpigdPkW0rJk0603y5GnTiF1iRfoDc4SAngMMmw", + "last_message_at": "2020-12-02T06:56:18.003432Z", + "created_at": "2020-11-30T10:25:32.494601Z", + "updated_at": "2020-11-30T10:25:32.494601Z", + "created_by": { + "id": "super-band-9", + "role": "user", + "created_at": "2020-03-03T16:48:28.853674Z", + "updated_at": "2021-05-26T03:22:20.296181Z", + "last_active": "2021-06-16T11:59:59.003453014Z", + "banned": false, + "online": true, + "unread_count": 0, + "username": "Rioland", + "image": "https://placehold.jp/150x150.png", + "invisible": false, + "name": "Proud darkness" + }, + "frozen": false, + "disabled": false, + "member_count": 2, + "config": { + "created_at": "2020-04-15T14:57:17.00966Z", + "updated_at": "2021-05-25T14:25:30.405621Z", + "name": "messaging", + "typing_events": true, + "read_events": true, + "connect_events": true, + "search": true, + "reactions": true, + "replies": true, + "mutes": true, + "uploads": true, + "url_enrichment": true, + "custom_events": false, + "push_notifications": true, + "message_retention": "infinite", + "max_message_length": 5000, + "automod": "disabled", + "automod_behavior": "flag", + "blocklist": "profanity_en_2020_v1", + "blocklist_behavior": "block", + "automod_thresholds": {}, + "commands": [ + { + "name": "giphy", + "description": "Post a random gif to the channel", + "args": "[text]", + "set": "fun_set" + } + ] + } + }, + "created_at": "2020-12-04T10:39:06.512021Z", + "updated_at": "2020-12-04T10:39:06.512021Z" + } + ], + "total_unread_count": 0, + "unread_channels": 0, + "language": "", + "image": "https://placehold.jp/150x150.png", + "name": "Proud darkness", + "username": "Rioland" +} diff --git a/packages/stream_chat/test/src/core/models/own_user_test.dart b/packages/stream_chat/test/src/core/models/own_user_test.dart new file mode 100644 index 00000000..edaf022e --- /dev/null +++ b/packages/stream_chat/test/src/core/models/own_user_test.dart @@ -0,0 +1,45 @@ +import 'package:stream_chat/src/core/models/own_user.dart'; +import 'package:stream_chat/src/core/models/user.dart'; +import 'package:test/test.dart'; + +import '../../utils.dart'; + +void main() { + group('src/models/own_user', () { + test('should parse json correctly', () { + final ownUser = OwnUser.fromJson(jsonFixture('own_user.json')); + expect(ownUser.id, 'super-band-9'); + expect(ownUser.role, 'user'); + + expect(ownUser.createdAt, DateTime.parse('2020-03-03T16:48:28.853674Z')); + expect(ownUser.updatedAt, DateTime.parse('2021-05-26T03:22:20.296181Z')); + expect( + ownUser.lastActive, DateTime.parse('2021-06-16T11:59:59.003453014Z')); + expect(ownUser.banned, false); + expect(ownUser.online, true); + expect(ownUser.devices.length, 1); + expect(ownUser.mutes.length, 0); + expect(ownUser.channelMutes.length, 1); + expect(ownUser.totalUnreadCount, 0); + expect(ownUser.unreadChannels, 0); + expect(ownUser.extraData['image'], 'https://placehold.jp/150x150.png'); + expect(ownUser.extraData['name'], 'Proud darkness'); + expect(ownUser.extraData['username'], 'Rioland'); + }); + + test('should initialize a OwnUser from a User correctly', () { + final user = User.fromJson(jsonFixture('user.json')); + final ownUser = OwnUser.fromUser(user); + + expect(ownUser.id, user.id); + expect(ownUser.id, user.id); + expect(ownUser.role, user.role); + expect(ownUser.createdAt, user.createdAt); + expect(ownUser.updatedAt, user.updatedAt); + expect(ownUser.lastActive, user.lastActive); + expect(ownUser.online, user.online); + expect(ownUser.banned, user.banned); + expect(ownUser.extraData, user.extraData); + }); + }); +} From d4b5154cbad0ce4888fa19617590000d5a6c95f4 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 16 Jun 2021 16:27:21 +0200 Subject: [PATCH 050/150] add reaction test --- .../test/src/core/models/reaction_test.dart | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/packages/stream_chat/test/src/core/models/reaction_test.dart b/packages/stream_chat/test/src/core/models/reaction_test.dart index facb8d07..cfe9f69e 100644 --- a/packages/stream_chat/test/src/core/models/reaction_test.dart +++ b/packages/stream_chat/test/src/core/models/reaction_test.dart @@ -47,5 +47,108 @@ void main() { }, ); }); + + test('copyWith', () { + final reaction = Reaction.fromJson(jsonFixture('reaction.json')); + var newReaction = reaction.copyWith(); + expect(newReaction.messageId, '76cd8c82-b557-4e48-9d12-87995d3a0e04'); + expect( + newReaction.createdAt, DateTime.parse('2020-01-28T22:17:31.108742Z')); + expect(newReaction.type, 'wow'); + expect( + newReaction.user?.toJson(), + User(id: '2de0297c-f3f2-489d-b930-ef77342edccf', extraData: const { + 'image': 'https://randomuser.me/api/portraits/women/45.jpg', + 'name': 'Daisy Morgan', + }).toJson(), + ); + expect(newReaction.score, 1); + expect(newReaction.userId, '2de0297c-f3f2-489d-b930-ef77342edccf'); + expect( + newReaction.extraData, {'updated_at': '2020-01-28T22:17:31.108742Z'}); + + newReaction = reaction.copyWith( + type: 'lol', + createdAt: DateTime.parse('2021-01-28T22:17:31.108742Z'), + extraData: {}, + messageId: 'test', + score: 2, + user: User(id: 'test'), + userId: 'test', + ); + + expect( + newReaction.type, + 'lol', + ); + expect( + newReaction.createdAt, + DateTime.parse('2021-01-28T22:17:31.108742Z'), + ); + expect( + newReaction.extraData, + {}, + ); + expect( + newReaction.messageId, + 'test', + ); + expect( + newReaction.score, + 2, + ); + expect( + newReaction.user, + User(id: 'test'), + ); + expect( + newReaction.userId, + 'test', + ); + }); + + test('merge', () { + final reaction = Reaction.fromJson(jsonFixture('reaction.json')); + final newReaction = reaction.merge( + Reaction( + type: 'lol', + createdAt: DateTime.parse('2021-01-28T22:17:31.108742Z'), + extraData: {}, + messageId: 'test', + score: 2, + user: User(id: 'test'), + userId: 'test', + ), + ); + + expect( + newReaction.type, + 'lol', + ); + expect( + newReaction.createdAt, + DateTime.parse('2021-01-28T22:17:31.108742Z'), + ); + expect( + newReaction.extraData, + {}, + ); + expect( + newReaction.messageId, + 'test', + ); + expect( + newReaction.score, + 2, + ); + expect( + newReaction.user, + User(id: 'test'), + ); + expect( + newReaction.userId, + 'test', + ); + }); }); } From aef63f3722e51752a9182f90a32e32f6906cf238 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 17 Jun 2021 09:40:01 +0200 Subject: [PATCH 051/150] add more read test --- .../test/src/core/models/read_test.dart | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/stream_chat/test/src/core/models/read_test.dart b/packages/stream_chat/test/src/core/models/read_test.dart index eabfc18c..94a28f08 100644 --- a/packages/stream_chat/test/src/core/models/read_test.dart +++ b/packages/stream_chat/test/src/core/models/read_test.dart @@ -26,5 +26,29 @@ void main() { 'unread_messages': 10, }); }); + + test('copyWith', () { + final read = Read.fromJson(jsonFixture('read.json')); + var newRead = read.copyWith(); + expect( + newRead.lastRead, + DateTime.parse('2020-01-28T22:17:30.966485504Z'), + ); + expect(newRead.user.id, 'bbb19d9a-ee50-45bc-84e5-0584e79d0c9e'); + expect(newRead.unreadMessages, 10); + + newRead = read.copyWith( + user: User(id: 'test'), + lastRead: DateTime.parse('2021-01-28T22:17:30.966485504Z'), + unreadMessages: 2, + ); + + expect( + newRead.lastRead, + DateTime.parse('2021-01-28T22:17:30.966485504Z'), + ); + expect(newRead.user.id, 'test'); + expect(newRead.unreadMessages, 2); + }); }); } From 91907639adef5627dc9f4f4fefd7f604978b4f5e Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 17 Jun 2021 10:01:46 +0200 Subject: [PATCH 052/150] add more user and channel config test --- .../lib/src/core/models/channel_config.dart | 5 +---- .../lib/src/core/models/channel_config.g.dart | 19 ---------------- packages/stream_chat/test/fixtures/user.json | 3 ++- .../test/src/core/models/user_test.dart | 22 +++++++++++++++++++ 4 files changed, 25 insertions(+), 24 deletions(-) diff --git a/packages/stream_chat/lib/src/core/models/channel_config.dart b/packages/stream_chat/lib/src/core/models/channel_config.dart index 2d5182b3..b7cafac6 100644 --- a/packages/stream_chat/lib/src/core/models/channel_config.dart +++ b/packages/stream_chat/lib/src/core/models/channel_config.dart @@ -4,7 +4,7 @@ import 'package:stream_chat/src/core/models/command.dart'; part 'channel_config.g.dart'; /// The class that contains the information about the configuration of a channel -@JsonSerializable() +@JsonSerializable(createToJson: false) class ChannelConfig { /// Constructor used for json serialization ChannelConfig({ @@ -87,7 +87,4 @@ class ChannelConfig { /// True if urls appears as attachments @JsonKey(defaultValue: false) final bool urlEnrichment; - - /// Serialize to json - Map toJson() => _$ChannelConfigToJson(this); } diff --git a/packages/stream_chat/lib/src/core/models/channel_config.g.dart b/packages/stream_chat/lib/src/core/models/channel_config.g.dart index 723281c0..b01049f3 100644 --- a/packages/stream_chat/lib/src/core/models/channel_config.g.dart +++ b/packages/stream_chat/lib/src/core/models/channel_config.g.dart @@ -32,22 +32,3 @@ ChannelConfig _$ChannelConfigFromJson(Map json) { urlEnrichment: json['url_enrichment'] as bool? ?? false, ); } - -Map _$ChannelConfigToJson(ChannelConfig instance) => - { - 'automod': instance.automod, - 'commands': instance.commands.map((e) => e.toJson()).toList(), - 'connect_events': instance.connectEvents, - 'created_at': instance.createdAt.toIso8601String(), - 'updated_at': instance.updatedAt.toIso8601String(), - 'max_message_length': instance.maxMessageLength, - 'message_retention': instance.messageRetention, - 'mutes': instance.mutes, - 'reactions': instance.reactions, - 'read_events': instance.readEvents, - 'replies': instance.replies, - 'search': instance.search, - 'typing_events': instance.typingEvents, - 'uploads': instance.uploads, - 'url_enrichment': instance.urlEnrichment, - }; diff --git a/packages/stream_chat/test/fixtures/user.json b/packages/stream_chat/test/fixtures/user.json index 1fad8ea0..49b972a3 100644 --- a/packages/stream_chat/test/fixtures/user.json +++ b/packages/stream_chat/test/fixtures/user.json @@ -1,4 +1,5 @@ { "id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e", - "role": "test-role" + "role": "test-role", + "name": "John" } \ No newline at end of file diff --git a/packages/stream_chat/test/src/core/models/user_test.dart b/packages/stream_chat/test/src/core/models/user_test.dart index 89d22b30..ce488cab 100644 --- a/packages/stream_chat/test/src/core/models/user_test.dart +++ b/packages/stream_chat/test/src/core/models/user_test.dart @@ -8,6 +8,7 @@ void main() { test('should parse json correctly', () { final user = User.fromJson(jsonFixture('user.json')); expect(user.id, 'bbb19d9a-ee50-45bc-84e5-0584e79d0c9e'); + expect(user.name, 'John'); }); test('should serialize to json correctly', () { @@ -20,5 +21,26 @@ void main() { 'id': 'bbb19d9a-ee50-45bc-84e5-0584e79d0c9e', }); }); + + test('copyWith', () { + final user = User.fromJson(jsonFixture('user.json')); + var newUser = user.copyWith(); + + expect(newUser.id, user.id); + expect(newUser.role, user.role); + expect(newUser.name, user.name); + + newUser = user.copyWith( + id: 'test', + role: 'test', + extraData: { + 'name': 'test', + }, + ); + + expect(newUser.id, 'test'); + expect(newUser.role, 'test'); + expect(newUser.name, 'test'); + }); }); } From 907573dab1532cb76bb843c68b0ec8b19e13c62a Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 17 Jun 2021 10:34:53 +0200 Subject: [PATCH 053/150] add event tests --- .../lib/src/core/models/channel_config.dart | 5 +- .../lib/src/core/models/channel_config.g.dart | 19 ++++ .../lib/src/core/models/event.dart | 13 ++- .../lib/src/core/models/event.g.dart | 26 ----- .../test/fixtures/event_channel.json | 99 +++++++++++++++++++ .../test/src/core/models/event_test.dart | 48 +++++++++ 6 files changed, 176 insertions(+), 34 deletions(-) create mode 100644 packages/stream_chat/test/fixtures/event_channel.json diff --git a/packages/stream_chat/lib/src/core/models/channel_config.dart b/packages/stream_chat/lib/src/core/models/channel_config.dart index b7cafac6..2d5182b3 100644 --- a/packages/stream_chat/lib/src/core/models/channel_config.dart +++ b/packages/stream_chat/lib/src/core/models/channel_config.dart @@ -4,7 +4,7 @@ import 'package:stream_chat/src/core/models/command.dart'; part 'channel_config.g.dart'; /// The class that contains the information about the configuration of a channel -@JsonSerializable(createToJson: false) +@JsonSerializable() class ChannelConfig { /// Constructor used for json serialization ChannelConfig({ @@ -87,4 +87,7 @@ class ChannelConfig { /// True if urls appears as attachments @JsonKey(defaultValue: false) final bool urlEnrichment; + + /// Serialize to json + Map toJson() => _$ChannelConfigToJson(this); } diff --git a/packages/stream_chat/lib/src/core/models/channel_config.g.dart b/packages/stream_chat/lib/src/core/models/channel_config.g.dart index b01049f3..723281c0 100644 --- a/packages/stream_chat/lib/src/core/models/channel_config.g.dart +++ b/packages/stream_chat/lib/src/core/models/channel_config.g.dart @@ -32,3 +32,22 @@ ChannelConfig _$ChannelConfigFromJson(Map json) { urlEnrichment: json['url_enrichment'] as bool? ?? false, ); } + +Map _$ChannelConfigToJson(ChannelConfig instance) => + { + 'automod': instance.automod, + 'commands': instance.commands.map((e) => e.toJson()).toList(), + 'connect_events': instance.connectEvents, + 'created_at': instance.createdAt.toIso8601String(), + 'updated_at': instance.updatedAt.toIso8601String(), + 'max_message_length': instance.maxMessageLength, + 'message_retention': instance.messageRetention, + 'mutes': instance.mutes, + 'reactions': instance.reactions, + 'read_events': instance.readEvents, + 'replies': instance.replies, + 'search': instance.search, + 'typing_events': instance.typingEvents, + 'uploads': instance.uploads, + 'url_enrichment': instance.urlEnrichment, + }; diff --git a/packages/stream_chat/lib/src/core/models/event.dart b/packages/stream_chat/lib/src/core/models/event.dart index 33e696c4..21dd069c 100644 --- a/packages/stream_chat/lib/src/core/models/event.dart +++ b/packages/stream_chat/lib/src/core/models/event.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/core/models/channel_model.dart'; import 'package:stream_chat/src/core/models/message.dart'; @@ -160,11 +162,14 @@ class Event { channelType: channelType ?? this.channelType, parentId: parentId ?? this.parentId, extraData: extraData ?? this.extraData, + isLocal: isLocal, ); } /// The channel embedded in the event object -@JsonSerializable() +@JsonSerializable( + createToJson: false, +) class EventChannel extends ChannelModel { /// Constructor used for json serialization EventChannel({ @@ -212,10 +217,4 @@ class EventChannel extends ChannelModel { 'members', ...ChannelModel.topLevelFields, ]; - - /// Serialize to json - @override - Map toJson() => Serializer.moveFromExtraDataToRoot( - _$EventChannelToJson(this), - ); } diff --git a/packages/stream_chat/lib/src/core/models/event.g.dart b/packages/stream_chat/lib/src/core/models/event.g.dart index 90abeb71..5247af17 100644 --- a/packages/stream_chat/lib/src/core/models/event.g.dart +++ b/packages/stream_chat/lib/src/core/models/event.g.dart @@ -89,29 +89,3 @@ EventChannel _$EventChannelFromJson(Map json) { extraData: json['extra_data'] as Map? ?? {}, ); } - -Map _$EventChannelToJson(EventChannel instance) { - final val = { - 'id': instance.id, - 'type': instance.type, - }; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('cid', readonly(instance.cid)); - writeNotNull('config', readonly(instance.config)); - writeNotNull('created_by', readonly(instance.createdBy)); - val['frozen'] = instance.frozen; - writeNotNull('last_message_at', readonly(instance.lastMessageAt)); - writeNotNull('created_at', readonly(instance.createdAt)); - writeNotNull('updated_at', readonly(instance.updatedAt)); - writeNotNull('deleted_at', readonly(instance.deletedAt)); - writeNotNull('member_count', readonly(instance.memberCount)); - val['extra_data'] = instance.extraData; - val['members'] = instance.members?.map((e) => e.toJson()).toList(); - return val; -} diff --git a/packages/stream_chat/test/fixtures/event_channel.json b/packages/stream_chat/test/fixtures/event_channel.json new file mode 100644 index 00000000..6078c545 --- /dev/null +++ b/packages/stream_chat/test/fixtures/event_channel.json @@ -0,0 +1,99 @@ +{ + "id": "!members-v9ktpgmYysZA-MjgC-GMoeEawFHSelkOdTu6JGxFZWU", + "type": "messaging", + "cid": "messaging:!members-v9ktpgmYysZA-MjgC-GMoeEawFHSelkOdTu6JGxFZWU", + "last_message_at": "2020-12-02T04:22:16.755334Z", + "created_at": "2020-10-01T08:49:32.90162Z", + "updated_at": "2021-06-17T08:18:48.188996Z", + "created_by": { + "id": "super-band-9", + "role": "user", + "created_at": "2020-03-03T16:48:28.853674Z", + "updated_at": "2021-05-26T03:22:20.296181Z", + "last_active": "2021-06-17T08:12:05.062115Z", + "banned": false, + "online": true, + "image": "https://placehold.jp/150x150.png", + "invisible": false, + "name": "Proud darkness", + "unread_count": 0, + "username": "Rioland" + }, + "frozen": false, + "disabled": false, + "members": [ + { + "user_id": "super-band-9", + "user": { + "id": "super-band-9", + "role": "user", + "created_at": "2020-03-03T16:48:28.853674Z", + "updated_at": "2021-05-26T03:22:20.296181Z", + "last_active": "2021-06-17T08:12:05.062115Z", + "banned": false, + "online": true, + "image": "https://placehold.jp/150x150.png", + "invisible": false, + "name": "Proud darkness", + "unread_count": 0, + "username": "Rioland" + }, + "role": "owner", + "created_at": "2020-10-01T08:49:32.905052Z", + "updated_at": "2020-10-01T08:49:32.905052Z", + "banned": false, + "shadow_banned": false + }, + { + "user_id": "cc48de8e-b2db-48e7-bba5-c03cefd61430", + "user": { + "id": "cc48de8e-b2db-48e7-bba5-c03cefd61430", + "role": "user", + "created_at": "2020-07-22T14:59:38.026809Z", + "updated_at": "2020-07-22T14:59:38.31338Z", + "banned": false, + "online": false, + "image": "https://images-na.ssl-images-amazon.com/images/M/MV5BMjA3NjYzMzE1MV5BMl5BanBnXkFtZTgwNTA4NDY4OTE@._V1_UX172_CR0,0,172,256_AL_.jpg", + "name": "Ana De Armas" + }, + "role": "member", + "created_at": "2020-10-01T08:49:32.905053Z", + "updated_at": "2020-10-01T08:49:32.905053Z", + "banned": false, + "shadow_banned": false + } + ], + "member_count": 2, + "config": { + "created_at": "2020-04-15T14:57:17.00966Z", + "updated_at": "2021-05-25T14:25:30.405621Z", + "name": "messaging", + "typing_events": true, + "read_events": true, + "connect_events": true, + "search": true, + "reactions": true, + "replies": true, + "mutes": true, + "uploads": true, + "url_enrichment": true, + "custom_events": false, + "push_notifications": true, + "message_retention": "infinite", + "max_message_length": 5000, + "automod": "disabled", + "automod_behavior": "flag", + "blocklist": "profanity_en_2020_v1", + "blocklist_behavior": "block", + "automod_thresholds": {}, + "commands": [ + { + "name": "giphy", + "description": "Post a random gif to the channel", + "args": "[text]", + "set": "fun_set" + } + ] + }, + "name": "test" +} \ No newline at end of file diff --git a/packages/stream_chat/test/src/core/models/event_test.dart b/packages/stream_chat/test/src/core/models/event_test.dart index 7ebd82fa..0cd9a0ad 100644 --- a/packages/stream_chat/test/src/core/models/event_test.dart +++ b/packages/stream_chat/test/src/core/models/event_test.dart @@ -54,5 +54,53 @@ void main() { }, ); }); + + test('copyWith', () { + final event = Event.fromJson(jsonFixture('event.json')); + var newEvent = event.copyWith(); + expect(newEvent.type, 'type'); + expect(newEvent.cid, 'cid'); + expect(newEvent.connectionId, 'connectionId'); + expect(newEvent.createdAt, isA()); + expect(newEvent.me, isA()); + expect(newEvent.user, isA()); + expect(newEvent.isLocal, false); + + newEvent = event.copyWith( + type: 'test', + cid: 'test', + connectionId: 'test', + extraData: {}, + user: User(id: 'test'), + channelId: 'test', + totalUnreadCount: 2, + channelType: 'testtype', + ); + + expect(newEvent.channelType, 'testtype'); + expect(newEvent.totalUnreadCount, 2); + expect(newEvent.type, 'test'); + expect(newEvent.channelId, 'test'); + expect(newEvent.cid, 'test'); + expect(newEvent.connectionId, 'test'); + expect(newEvent.extraData, {}); + expect(newEvent.user!.id, 'test'); + }); + + group('eventChannel', () { + test('should parse json correctly', () { + final eventChannel = + EventChannel.fromJson(jsonFixture('event_channel.json')); + expect(eventChannel.type, 'messaging'); + expect(eventChannel.cid, + 'messaging:!members-v9ktpgmYysZA-MjgC-GMoeEawFHSelkOdTu6JGxFZWU'); + expect(eventChannel.createdBy!.id, 'super-band-9'); + expect(eventChannel.frozen, false); + expect(eventChannel.members!.length, 2); + expect(eventChannel.memberCount, 2); + expect(eventChannel.config, isA()); + expect(eventChannel.name, 'test'); + }); + }); }); } From 0860acb61ef1f0d2f872cc6da283028731472f01 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 17 Jun 2021 14:35:20 +0530 Subject: [PATCH 054/150] fix tests Signed-off-by: Sahil Kumar --- .../test/src/back_button_test.dart | 3 +- .../test/src/channel_header_test.dart | 4 ++ .../test/src/message_action_modal_test.dart | 18 +++------ .../test/src/thread_header_test.dart | 1 + .../test/message_list_core_test.dart | 4 +- .../test/stream_channel_test.dart | 40 ++++++++++++++----- .../test/stream_chat_core_test.dart | 26 ++++++------ 7 files changed, 59 insertions(+), 37 deletions(-) diff --git a/packages/stream_chat_flutter/test/src/back_button_test.dart b/packages/stream_chat_flutter/test/src/back_button_test.dart index 3fae1ace..efb889ac 100644 --- a/packages/stream_chat_flutter/test/src/back_button_test.dart +++ b/packages/stream_chat_flutter/test/src/back_button_test.dart @@ -118,8 +118,9 @@ void main() { final clientState = MockClientState(); when(() => client.state).thenReturn(clientState); + when(() => clientState.totalUnreadCount).thenAnswer((_) => 0); when(() => clientState.totalUnreadCountStream) - .thenAnswer((i) => Stream.value(0)); + .thenAnswer((_) => Stream.value(0)); await tester.pumpWidget( MaterialApp( diff --git a/packages/stream_chat_flutter/test/src/channel_header_test.dart b/packages/stream_chat_flutter/test/src/channel_header_test.dart index 9f6e981d..87f3d028 100644 --- a/packages/stream_chat_flutter/test/src/channel_header_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_header_test.dart @@ -34,6 +34,7 @@ void main() { .thenAnswer((_) => Stream.value(ConnectionStatus.connected)); when(() => channelState.unreadCountStream) .thenAnswer((i) => Stream.value(1)); + when(() => clientState.totalUnreadCount).thenAnswer((i) => 1); when(() => clientState.totalUnreadCountStream) .thenAnswer((i) => Stream.value(1)); when(() => channelState.membersStream).thenAnswer((i) => Stream.value([ @@ -109,6 +110,7 @@ void main() { .thenAnswer((_) => Stream.value(ConnectionStatus.disconnected)); when(() => client.wsConnectionStatus) .thenReturn(ConnectionStatus.disconnected); + when(() => clientState.totalUnreadCount).thenAnswer((i) => 1); when(() => clientState.totalUnreadCountStream) .thenAnswer((i) => Stream.value(1)); @@ -171,6 +173,7 @@ void main() { ]); when(() => client.wsConnectionStatusStream) .thenAnswer((_) => Stream.value(ConnectionStatus.connecting)); + when(() => clientState.totalUnreadCount).thenAnswer((i) => 1); when(() => clientState.totalUnreadCountStream) .thenAnswer((i) => Stream.value(1)); @@ -375,6 +378,7 @@ void main() { ]); when(() => client.wsConnectionStatusStream) .thenAnswer((_) => Stream.value(ConnectionStatus.connecting)); + when(() => clientState.totalUnreadCount).thenAnswer((i) => 1); when(() => clientState.totalUnreadCountStream) .thenAnswer((i) => Stream.value(1)); diff --git a/packages/stream_chat_flutter/test/src/message_action_modal_test.dart b/packages/stream_chat_flutter/test/src/message_action_modal_test.dart index 6118e7d2..9b7b3e52 100644 --- a/packages/stream_chat_flutter/test/src/message_action_modal_test.dart +++ b/packages/stream_chat_flutter/test/src/message_action_modal_test.dart @@ -552,10 +552,8 @@ void main() { when(() => client.state).thenReturn(clientState); when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); - when(() => client.flagMessage(any())).thenThrow(ApiError( - '{}', - 500, - )); + when(() => client.flagMessage(any())) + .thenThrow(StreamChatNetworkError(ChatErrorCode.internalSystemError)); final themeData = ThemeData(); final streamTheme = StreamChatThemeData.fromTheme(themeData); @@ -609,10 +607,8 @@ void main() { when(() => client.state).thenReturn(clientState); when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); - when(() => client.flagMessage(any())).thenThrow(ApiError( - '{"code":4}', - 400, - )); + when(() => client.flagMessage(any())) + .thenThrow(StreamChatNetworkError(ChatErrorCode.inputError)); final themeData = ThemeData(); final streamTheme = StreamChatThemeData.fromTheme(themeData); @@ -719,10 +715,8 @@ void main() { when(() => client.state).thenReturn(clientState); when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); - when(() => channel.deleteMessage(any())).thenThrow(ApiError( - '{}', - 500, - )); + when(() => channel.deleteMessage(any())) + .thenThrow(StreamChatNetworkError(ChatErrorCode.internalSystemError)); final themeData = ThemeData(); final streamTheme = StreamChatThemeData.fromTheme(themeData); diff --git a/packages/stream_chat_flutter/test/src/thread_header_test.dart b/packages/stream_chat_flutter/test/src/thread_header_test.dart index 578b2ad6..2b649be3 100644 --- a/packages/stream_chat_flutter/test/src/thread_header_test.dart +++ b/packages/stream_chat_flutter/test/src/thread_header_test.dart @@ -45,6 +45,7 @@ void main() { ]); when(() => client.wsConnectionStatusStream) .thenAnswer((_) => Stream.value(ConnectionStatus.connecting)); + when(() => clientState.totalUnreadCount).thenAnswer((i) => 1); when(() => clientState.totalUnreadCountStream) .thenAnswer((i) => Stream.value(1)); diff --git a/packages/stream_chat_flutter_core/test/message_list_core_test.dart b/packages/stream_chat_flutter_core/test/message_list_core_test.dart index 4e34f8f2..98e83f9f 100644 --- a/packages/stream_chat_flutter_core/test/message_list_core_test.dart +++ b/packages/stream_chat_flutter_core/test/message_list_core_test.dart @@ -300,7 +300,9 @@ void main() { when(() => mockChannel.state.isUpToDate).thenReturn(false); when(() => mockChannel.initialized).thenAnswer((_) async => true); when(() => mockChannel.query( - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), membersPagination: any(named: 'membersPagination'), messagesPagination: any(named: 'messagesPagination'), preferOffline: any(named: 'preferOffline'), diff --git a/packages/stream_chat_flutter_core/test/stream_channel_test.dart b/packages/stream_chat_flutter_core/test/stream_channel_test.dart index b48c02b1..2b32cf2d 100644 --- a/packages/stream_chat_flutter_core/test/stream_channel_test.dart +++ b/packages/stream_chat_flutter_core/test/stream_channel_test.dart @@ -165,7 +165,9 @@ void main() { when(() => mockChannel.initialized).thenAnswer((_) async => true); final messages = _generateMessages(); when(() => mockChannel.query( - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), messagesPagination: any(named: 'messagesPagination'), membersPagination: any(named: 'membersPagination'), watchersPagination: any(named: 'watchersPagination'), @@ -183,7 +185,9 @@ void main() { verify(() => mockChannel.initialized).called(1); verify(() => mockChannel.query( - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), messagesPagination: any(named: 'messagesPagination'), membersPagination: any(named: 'membersPagination'), watchersPagination: any(named: 'watchersPagination'), @@ -228,7 +232,9 @@ void main() { final messages = _generateMessages(); when(() => mockChannel.query( - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), messagesPagination: beforePagination, membersPagination: any(named: 'membersPagination'), watchersPagination: any(named: 'watchersPagination'), @@ -236,7 +242,9 @@ void main() { )).thenAnswer((_) async => ChannelState(messages: messages)); when(() => mockChannel.query( - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), messagesPagination: afterPagination, membersPagination: any(named: 'membersPagination'), watchersPagination: any(named: 'watchersPagination'), @@ -259,7 +267,9 @@ void main() { await tester.pumpAndSettle(); verify(() => mockChannel.query( - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), messagesPagination: beforePagination, membersPagination: any(named: 'membersPagination'), watchersPagination: any(named: 'watchersPagination'), @@ -267,7 +277,9 @@ void main() { )).called(1); verify(() => mockChannel.query( - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), messagesPagination: afterPagination, membersPagination: any(named: 'membersPagination'), watchersPagination: any(named: 'watchersPagination'), @@ -285,7 +297,9 @@ void main() { ); when(() => mockChannel.query( - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), messagesPagination: updatedBeforePagination, membersPagination: any(named: 'membersPagination'), watchersPagination: any(named: 'watchersPagination'), @@ -293,7 +307,9 @@ void main() { )).thenAnswer((_) async => ChannelState(messages: messages)); when(() => mockChannel.query( - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), messagesPagination: updatedAfterPagination, membersPagination: any(named: 'membersPagination'), watchersPagination: any(named: 'watchersPagination'), @@ -303,7 +319,9 @@ void main() { await tester.pumpAndSettle(); verify(() => mockChannel.query( - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), messagesPagination: updatedBeforePagination, membersPagination: any(named: 'membersPagination'), watchersPagination: any(named: 'watchersPagination'), @@ -311,7 +329,9 @@ void main() { )).called(1); verify(() => mockChannel.query( - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), messagesPagination: updatedAfterPagination, membersPagination: any(named: 'membersPagination'), watchersPagination: any(named: 'watchersPagination'), diff --git a/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart b/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart index 168454c8..f2b1b514 100644 --- a/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart +++ b/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart @@ -227,7 +227,7 @@ void main() { ); testWidgets( - 'didChangeAppLifecycleState should not call client.connect() ' + 'didChangeAppLifecycleState should not call client.openConnection() ' 'if connection is not available in case the ' 'widget lifestyle changes to AppLifecycleState.resume', (tester) async { @@ -238,8 +238,8 @@ void main() { final event = Event(); when(() => mockClient.on()).thenAnswer((_) => Stream.value(event)); - when(() => mockClient.connect()).thenAnswer((_) async => event); - when(() => mockClient.disconnect()).thenAnswer((_) async => null); + when(() => mockClient.openConnection()).thenAnswer((_) async => event); + when(() => mockClient.closeConnection()).thenAnswer((_) async => null); when(() => mockClient.wsConnectionStatus) .thenReturn(ConnectionStatus.disconnected); @@ -267,7 +267,7 @@ void main() { streamChatCoreState .didChangeAppLifecycleState(AppLifecycleState.resumed); - verifyNever(() => mockClient.connect()); + verifyNever(() => mockClient.openConnection()); }); }, ); @@ -327,8 +327,8 @@ void main() { final event = Event(); when(() => mockClient.on()).thenAnswer((_) => Stream.value(event)); - when(() => mockClient.connect()).thenAnswer((_) async => event); - when(() => mockClient.disconnect()).thenAnswer((_) async => null); + when(() => mockClient.openConnection()).thenAnswer((_) async => event); + when(() => mockClient.closeConnection()).thenAnswer((_) async => null); when(() => mockClient.wsConnectionStatus) .thenReturn(ConnectionStatus.disconnected); @@ -348,7 +348,7 @@ void main() { await Future.delayed(const Duration(seconds: 1)); - verify(() => mockClient.connect()).called(1); + verify(() => mockClient.openConnection()).called(1); }); }, ); @@ -376,8 +376,8 @@ void main() { final event = Event(); when(() => mockClient.on()).thenAnswer((_) => Stream.value(event)); - when(() => mockClient.connect()).thenAnswer((_) async => event); - when(mockClient.disconnect).thenAnswer((_) async => null); + when(() => mockClient.openConnection()).thenAnswer((_) async => event); + when(() => mockClient.closeConnection()).thenAnswer((_) async => null); when(() => mockClient.wsConnectionStatus) .thenReturn(ConnectionStatus.connected); @@ -385,7 +385,7 @@ void main() { await Future.delayed(const Duration(seconds: 1)); - verify(() => mockClient.disconnect()).called(1); + verify(() => mockClient.closeConnection()).called(1); }); }, ); @@ -402,8 +402,8 @@ void main() { final event = Event(); when(() => mockClient.on()).thenAnswer((_) => Stream.value(event)); - when(() => mockClient.connect()).thenAnswer((_) async => event); - when(() => mockClient.disconnect()).thenAnswer((_) async => null); + when(() => mockClient.openConnection()).thenAnswer((_) async => event); + when(() => mockClient.closeConnection()).thenAnswer((_) async => null); when(() => mockClient.wsConnectionStatus) .thenReturn(ConnectionStatus.disconnected); @@ -432,7 +432,7 @@ void main() { await Future.delayed(const Duration(seconds: 1)); - verifyNever(() => mockClient.disconnect()); + verifyNever(() => mockClient.closeConnection()); }); }, ); From 875eda8cb5566337bc9bd6fa0e2c88a08171e5eb Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 17 Jun 2021 11:27:31 +0200 Subject: [PATCH 055/150] Update packages/stream_chat/lib/src/core/models/mute.dart Co-authored-by: Sahil Kumar --- packages/stream_chat/lib/src/core/models/mute.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/stream_chat/lib/src/core/models/mute.dart b/packages/stream_chat/lib/src/core/models/mute.dart index 4b14ad01..857b34a1 100644 --- a/packages/stream_chat/lib/src/core/models/mute.dart +++ b/packages/stream_chat/lib/src/core/models/mute.dart @@ -6,9 +6,7 @@ import 'package:stream_chat/src/core/util/serializer.dart'; part 'mute.g.dart'; /// The class that contains the information about a muted user -@JsonSerializable( - createToJson: false, -) +@JsonSerializable(createToJson: false) class Mute { /// Constructor used for json serialization Mute({ From 6a2efb18e44f9447e4781178fa8ada09ca2fe148 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 17 Jun 2021 15:01:45 +0530 Subject: [PATCH 056/150] Update packages/stream_chat/test/src/core/models/reaction_test.dart --- .../test/src/core/models/reaction_test.dart | 30 ++++--------------- 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/packages/stream_chat/test/src/core/models/reaction_test.dart b/packages/stream_chat/test/src/core/models/reaction_test.dart index cfe9f69e..b603477c 100644 --- a/packages/stream_chat/test/src/core/models/reaction_test.dart +++ b/packages/stream_chat/test/src/core/models/reaction_test.dart @@ -77,34 +77,16 @@ void main() { userId: 'test', ); - expect( - newReaction.type, - 'lol', - ); + expect(newReaction.type, 'lol'); expect( newReaction.createdAt, DateTime.parse('2021-01-28T22:17:31.108742Z'), ); - expect( - newReaction.extraData, - {}, - ); - expect( - newReaction.messageId, - 'test', - ); - expect( - newReaction.score, - 2, - ); - expect( - newReaction.user, - User(id: 'test'), - ); - expect( - newReaction.userId, - 'test', - ); + expect(newReaction.extraData, {}); + expect(newReaction.messageId, 'test'); + expect(newReaction.score, 2); + expect(newReaction.user, User(id: 'test')); + expect(newReaction.userId, 'test'); }); test('merge', () { From a98148e0db4a2f603e2bc36818b879d2e1c8efdd Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 17 Jun 2021 15:01:53 +0530 Subject: [PATCH 057/150] Update packages/stream_chat/test/src/core/models/reaction_test.dart --- .../test/src/core/models/reaction_test.dart | 30 ++++--------------- 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/packages/stream_chat/test/src/core/models/reaction_test.dart b/packages/stream_chat/test/src/core/models/reaction_test.dart index b603477c..518a223e 100644 --- a/packages/stream_chat/test/src/core/models/reaction_test.dart +++ b/packages/stream_chat/test/src/core/models/reaction_test.dart @@ -103,34 +103,16 @@ void main() { ), ); - expect( - newReaction.type, - 'lol', - ); + expect(newReaction.type, 'lol'); expect( newReaction.createdAt, DateTime.parse('2021-01-28T22:17:31.108742Z'), ); - expect( - newReaction.extraData, - {}, - ); - expect( - newReaction.messageId, - 'test', - ); - expect( - newReaction.score, - 2, - ); - expect( - newReaction.user, - User(id: 'test'), - ); - expect( - newReaction.userId, - 'test', - ); + expect(newReaction.extraData, {}); + expect(newReaction.messageId, 'test'); + expect(newReaction.score, 2); + expect(newReaction.user, User(id: 'test')); + expect(newReaction.userId, 'test'); }); }); } From b7efe258c983e506b0dea9fbbd1eeee78346b072 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 17 Jun 2021 16:11:05 +0530 Subject: [PATCH 058/150] reset golden files to previous revisions Signed-off-by: Sahil Kumar --- .../src/goldens/deleted_message_custom.png | Bin 2334 -> 2936 bytes .../test/src/goldens/deleted_message_dark.png | Bin 2262 -> 2785 bytes .../src/goldens/deleted_message_light.png | Bin 2161 -> 2750 bytes .../test/src/goldens/reaction_bubble_2.png | Bin 2937 -> 2920 bytes .../test/src/goldens/system_message_dark.png | Bin 1679 -> 1812 bytes .../test/src/goldens/system_message_light.png | Bin 1746 -> 1866 bytes 6 files changed, 0 insertions(+), 0 deletions(-) diff --git a/packages/stream_chat_flutter/test/src/goldens/deleted_message_custom.png b/packages/stream_chat_flutter/test/src/goldens/deleted_message_custom.png index 693db3c78ee724f41d6376bd5c7ba1eb1385750d..c464bc01de48fcef06c538bd284ed284370f55aa 100644 GIT binary patch delta 2416 zcmXArc~n#97R9U9ieQCW;#h`i3J8i+$YTbzfCOYrVi*#rr3#1;5JJEJSEQ)43sVw8 ztP!xHrXhqpjA0(7JSHK6Kx9UykVL`^CSmAD-(B~wyY4!7pMCcEeK+sGr-Q$rgEWpK zoz9~Z9?uRBqK*;unin+p{-V!{HamRo)L6pP0QrsPtS+0ps8_jN$7K29^hRg0_}j*? z<^`v~XH#ZpZ%;kE@Z`hqv0v*8chg{QiWgE8V@!p3cVu7~nKCFR^XrD;+?J`EyLTkO zDTBK{^E|tA$G*>Z9#;F@OiS%YeXV+$nc91r*^VQS@vv_F`3$EE6}V1C`qut@p?JNJ z$0L!g9{h?k#YU60Y;A1=f`bj!U8(CSbpZhZXn)D(!r;oem;FV3B9Wl1OgwEC{W`%; z*M4L9-J)tk{-CkLaCC4Gb?V@W{olm+u6~K#mC^LmK!1NsteRCre0=5@p$guVSMbUYs$q-f%ZG-rIE}w&TMxC}W2)3i-8&lD zd*O7qlXN$oDmz-M5#5dL9lcRI^L6ZolGyR)O*W1|CI^0q_6rOQ+r=_J)lB`Dt2D%x+DeQ)Z^Rse!C1m#6@XQ~Vu17_A!eFrPM0gwy4L{%$85!A1 zEGsKR*xIVfG*3ZS|1I?M)vHYkYh8+7z-1%t;BSGUtiA7)Z0s{j0|XRB!S>gwXv~z z?CAB}S2;m()XsE)seRrvIy(2!tODoh?(QA{MCoh2T@pIZRi}0a7$7w?G{7XfJ3AlT zB9nXj`z0T7;M0#DKSo;|#Y9GOD=RB4936QYhfG^07L4+R5um;H_V&W!;ywJ%; z6Trdlbd5vH-R@`D-bOP6f(Y zm><|^X$$U3LV{mPT`B>WAg3w-GlpF>FVP^J3 zsZ>%ak(nGEJz2j=anbBBIwYiEX(X4;KH2os_jsr_$=`8(KFxgVFc|d6NS>W8C1qm; zi&U|$A`l4bGpY1CQnfXIrh5KP=VR-Fyu3VWI{-mvz9WxLhk1B-oTRn7QXesyf~qPG z(~?lgVqNU$=%8*^D|)-S{_O3=L`Fpw3IrHS!bmn8xbI3eFMjHG>P1tN^OyVfK~@)6 z#(S$)SK~cAJs%B{q5)e7b#&a?nAp6i*%VQH6SJD?bo{HtnxNe~cYvGA<(>rl3|tU* z=Ax35Ykn$u2zrDg`F#GOBB|{fzq89;OU+1I9Z6NHRA8Z<>!u`?yes*6UbU9S4mVO}skH>(4nDDA^h7EOZcADGR7>7I7k2~LOYjY!#NbM8W zNjg`rU31Z;H1Fkq@@raJ_uUrjbA1@NAQt<|ws*n?mikH{Kq{BQboSrFpil*yn<>r( zK1j&(Ex*6$PQ#NC@|?Z+d-e1?V4buxoU$(&K)oUntG3qR^y$;v`R8yrAm`1d?Nc`^ zVD;}4NvB}M8o=BX0sGD=i0ORBH;gpHCdL?Nt zFE4qupE|fUzup*N2_Tv4saN^mD_lnli=Q8W7!bE(u%K6xuyQ>%76s`gFK2<7$38Q4 zL1VEjfdJv*<;8ja+!=*J0q%>QyEC>?B9VkEHc}847Gc1IARhKlZ h$U5GC0pAKv z4-*p;<;L15EYl^oH9Thj_h-*O6$Oz!wmIeJ=S?D&!PzAxK481cqK19J`T+_?jn;C# z93aJH%bvSZsq__yPgd4f#JISyxHvR8jjS_EO6SJb3wkQo=0-_*=8X1M@zNjv`fMya z_baXO-Y$tuhH`iR?b(KGe$O0Kt(a!2-)mG#Sov@y@bi{9mjp*{rv!c=M$UXyS}Fw+ zw~16y^x}m}!;juH8V%ZpCKk(pTeA-8=;#2#1Ox^3EiS%{m6Y=N?-EBqfB`-tl9F&> z^KYqU%k&iCmHbelR6bm_&BpidADXjH%J28B>Lg)I1W`ZykX9YUOW9O}_V)Dr#o@H$ z>UzrC=n;Aq!vh01e(ZTjm;3knE&8fxlAsX8|Mr4_V(WT=sTgb1zI$R z2bGre6_XanE6IHo39#v{l2gnMskE0Potg0t4Q1uzWa2~5=4|; zfu{IF&0O+OYW;R8V@uXJ*TC?P(K%6l{cT@%Hh|jlSi`OwmAtCc?lVJX1j}zBVQ58o ztia9!fzVOqFbz8dSS=ZPw^d6=a+BcP&}Qw4Y)#8G7*?!LBod<>?AvY+s_t`A!RxsL z!qBmN&@CE;qH#i^FCeri%>_VMbLC~qRu1hYp?q_=x_i@d2GFV2M1%TiQ5w=T5d)5n zO;y^~lW{278;_q=R!=?3GD!%aJ Goqqs9Ee-zw delta 1816 zcmV+z2j}?s7M>E2Kz|6xNkl`t918 zW=r}^wyN@nb8~`9;(r3==Ah7?x7$?0|NsZ9Uax+;9%TC zktE#9kTCG-`LqHE}wSQVVIy#DV46z2%Xf)zHJo>=y?ryBdk1ddslM`;p(behc zsZLH#Vmp3pfdoMi*P+p8;+!i4f}VFL0|EL0fdB>qxkZ+jmsPD+yY3eRffg1P)ND3y zU{u#1S1-CXfh;U6XmN4z%56aqXmN4zx7%-vot+)!a=EVi`MxjboPW~k^bL&a`s3$&(ntwA8uq^z3A2i629OU zmkqw}YinyunM|f{U!?2!lS%LU{v#D~qEa_~C)NZ#mJw4s^+;tmQzkj(ESCE~Z9c8mwWipwL zKs?V=GMSWfu4BzzsZ`W#HudJs8_mtlMf*zs1u#85t@-(R<@0&1uC8|7)^4}8w6vsb zHmmvhdCktwYI}P-x}w$Vb(PCyjgOCOZf;H!6BAlrUylN))oSuQPZJXpnxCInE|*iK zQn~W^R;yLj>wk5XN+r$B%|&kx;bU3P@8bQ(@Al2*a+;l;l_bs0%|&rokKJut{iyE) z3D>ykbXw_jI+~EpW>Z^RTPl@Gs@Ln$WxiIc$@l%}UC%kEY&IK(75@3~*M&ksGcz+? zw@K2>%!~?!f+R)nsG(}Ls$#LILZKk%oN~Eb$FajJTz@#3a=F}<|B$oYZfkvgJ=!;X z4C!p0-|hLGU-DdM-%KVGT>-8AyN#<~_5Bj-{EO(Ei(cGHrJ`c7sAMuJ-}j@5Td&vE zYPBwXqE##wC7n(BaH&)-mzB@wH8C;Kb(?fHfy(7_#|H)3Y*x)?Q>|7j8iRBeTMz`% zq?V+mrGKT4F0Zu_2}+}8fx#?`NGMIh(jsn*uk z2z94OG_#ii<+32 zkaJGeYBjpzgu#UG#Nn~RuzlZGKA-QnFZi8yyMNvBI2X@>_53d0e^~pT-+uAeSM6V| zR?TtstG-`ih4&xQf6JQiQZHRzbB~RUX=7s}y3*8YwdgOgn$4y>&+AxXIOmj1CcAD6 zf*?9Ztya_4)>bsGaN=xiY)CqLCth1y>j-;$dplY#`o6D9rE=v**3LOK8ja|fp67L( z1ApuJ{pq;~fXdKr5-Nx0g?!SNk{(o^C_~__JKY#xGtH1`ZGUY|o12>&85z;nuV0nV=ao*UH8eD&4}bbNfQ7cX8Yl}cTD&gpbo-@bj*r%#{s z`SWKv=hSRAqj!4i`E71)>fytO*E$!!e*MyyFJH92zOLubpKEAn$lSl%xciP*n254~?-U?A4->uQa4hK`Po#(8-3fw<>Ltbq&;4r+LKIL;xUhYb%8 z$NnYOv9EDPMn;GPmkbXN$M(t9d9i=Hd+6xsNV~hceB0t3mq;X($z)=`a)f#&kWly^ z=s7XA-_lTcw*IrhN-0000?)dLhd1;NlrLHLns#kaa*+u)$L5{ zT6a79U+sU@+8L+Sb~qt{14Qpb`j}kRVA&_6I@+Z{OI<7-HYApLCwbljp#B zpGyLIPM$p35P+BRc6tH8K}M0$+LK@bB`$-nQ!l(zuWP5?=+w)ji}ZLs>hX5wT;8!j zB+2Xb$m8*JatS!#8ja4LM7hNRlRyC$e}=avkZ?GxP$=}u@nA5ha5(%{e=vFa)G1{$ znYQzKqfvb@JFDK_-Z!wS*I(B`^i~8C3I=uI{CWN5lTYM*$&Vz&Vln;o!UbJ8fBvQ8 zZ-YoUtRsViul32Z>$*xgj-Eb!+IDVlG^)(eqrdpX@^jX85WN*gOEQ^k{W%(qe?Bwj z@p$yd)2Ag##bQxAJ3DW5ZEs4_aU@BrtE+A2I?1F;rIKp3THCqfN>t8h51)wMNc1_qQ)r}g>8i!bbNI2_jbbLZso zc$8aR)!68$d_JF+mX_M?UteEe>)%;(?<_1R6b{Sh^J#s3UDax}?Y7&ks~dHFAfaGT z!9YN{wKc^P3B_Wu*4X=}PpecasZb~=?Ydga=jHeN_2JpGdidy(f397>E=f8wJF9p+ zE=ii4m{2Gf)Mpnjs#dG%$l#zRjvbTN>(#OG@mG%ZMx*k0Jo@(PRps+}`FuY8?bA;c z3r8&@_H9+GRSjh_dhgwL^@kHD z^ws6dDwWIT*xng`C7092#)iVpx13kk)|5yj(vLdvwHgUss4NGmJ-d&|K9UQAP`VKpV!szzH4p6V%i(@(qWI(xQtrjAA;l6HTK za?e02f2B&LqOGkhg+d`U8jY8(>3NYzM2$wHHRkbnluRbI^7wJv{d@lT!=&CH*vH1?p0qbrYqlk(>mxSwIV8)%ZkV28X6kXlZ_1x z4-f0mp+mZS<%$MfS3^TXs#Gc}mCG8+WK^%$e|OLFV`Hxz+q+ssA`vB%NxhTJDjtvP zLG$5st9b^Unw-?_J9m_JUAeBS<%bU?Nu^RrDaX;&jnj%YBQ zRyLcJB>nT+HC3xs6^ca-rqdc48q#n!t6Ht5>vMA|7K`TCcBLXGnbcr9t<2G*+ANjS z+uN&^Tu!U2tJ>Pyl9Nm-nMi0TlWEVO_d(N%eiVQ9j@91^smns9-RtN~QA35Adx0d)>KSuUCGbPutI3 z#jI_&Ti4IkXf))JexLtoLnl4|P2X?tU6|iuED#9j&*#o*C709Vl@+-I14=oLzP)<& znK-1^3uDjTJ58?~H}>y!=ho}>X_t;!+ith6Zu3_BsL4TAsZ{jC+?>LpkftUl<@fvb zkMF;4JqO@m``una-lV1F<<{pV=q_AA00V&l1_A*L1OgZc1TYW?VCd-Ey!CoRQg{BY zGTxjXZ|DA_jgAG<lY3<4pEcg2j7wZA{WE|0l%51RqPmgaR2}S07*qoM6N<$ Ef*kF!`v3p{ delta 1719 zcmV;o21xng71j}uKyCE<|^YbkM_@IGa4*)pNlTiU$lVAZ$lVAZ$lVAZ2F(iAn;loy|SDQv38b~C1 zvfZ8q(rUGI*lH=!qa8h$9}k1j+Y!SJvo$#EPx$@hPICYR01aUAW`YG*yPGjn_lYH!}0YTeTwi(EFV zzkUDxnT+c^x)^_f1VQ`_e|Opt3=eC3d|dTV1UXH&em(6NClac3n+NssFR4S?2Y)X>S zsgw${v+_JoVHj$qT-MgsR@Y^&>#8t2t1r@NNz$wBZIypYC52HGe{C+C)!^Wu_V@R- zy}hj<2=vcifBoR{rBdmmSKfEW?oF zKJ0qzx{a&fTuP_YfA7VHTkXFG`4qp+`)#qPN~NMJSFR`u!}z3{$>%k2>5|^mY8o9G z(UWpnl2m`3o0I2x`t8vpRVo#Yjg86oeO0T~IDq>PAL{YSiY|ZlnX=QV;9HTCFC}^Wv|QB+qpvNoyM$y8qxo*JYA) z|G|HQ_=Ilm?{|!|FgvTKm5SCkHWWsYwyRZz?U-HHjqh;Bac@_v@t96>T~||6Q}MdT z$iD48eXrH|Eb8~vyzH(g+0RjVnV%PE)5DvTnnZ*0hQU3s1tU&XElfgY7g@pD7RYYl%7 z%k?~!+VgYY_jP-5@qf=4A6eFDG<17$QBNxsmCI#?QKZ7mjQqhtNs8ZDJH8ShZ|gYh zAPDq)b2ENzi|4u>uk-FPu=elW=MQV$U!Z6f+v)Of-<2dm)GMS9V z$H(K7YxAAJUKC!hQVsu?`Op6-ANqxt#v}`|o*_|KpnNW;q~qxM^XJ;#+k1a+pBs&af*{cB*I#RFY)oMocH9-L{hRpmONCK%s$(%Q zFraiQrKzbYy?FIX2L}h{`rXF$XY}$RJuyE&-;@7d!K>sH6~X}(z0(@KRn`aoFUKcx4AwT%tU z7mM;dPxX3TOQlj@|M2XugeexzHKtg=IYuCWfj|HQfdB>q0Sp8J7<&41kQSd2f-YJ; z{)mj81(HZ4xP76kUVlVJ&jRV?hoS(d#YG&GApwe$U;#^$U;!x>_%Fneu2z=3CMo~` N002ovPDHLkV1k_0l*0f3 diff --git a/packages/stream_chat_flutter/test/src/goldens/deleted_message_light.png b/packages/stream_chat_flutter/test/src/goldens/deleted_message_light.png index 1f6330eb9bd92be6130bae15872bfe6d51878d88..d5a453ffc0cf157efe7d6a8e84648ab9a4a90530 100644 GIT binary patch delta 2211 zcmV;U2weB^5WW?VKz|BoNklwQ?A6Hw> zC#RnS=Xp4v(?Ua`P$&z)LwSyR0>E)bAb^2D00V&l1_A*L1d;x+k^w<5L*r4su;Vyd zU0pq@mq#6mL?Y_x={b_uKC(b0DG&(gsi&Sg$|>NOb6xjHU!PknlOX{Ye|Ta7$!4?4 zX0wNG&*gH;X0s}n%a3tZkJpteS2RCAUvu2{_O`~x#s#Gc+!3_U>J3BjCTwGK`LqpB6%gf7h97lmbpyt@^ z?QJbAEd1jO%io#Tar8t4e{%n4y3*Jv4og^s~3aPR2NIzZkuep8uwj`;$ zySwIC$8i)42G!KmRCDajn>RHvG4YR2@V_&!&Gc(%S+EO?i*82Lo{C;_-MjkhQfn zee=yX^85X2ZEe-e%#1RbjLw}qr(&_F%a<=plDfLOG(A17Xf&$NKKo3P4&Bz&)THt8 zaXF5ozP>)C(`k*5kL&E&vwG>JmsBp7b@}pTrPFDJ!(nY~e{5)Wc2>>J&3faFH5|IjvLca)rl+S>EEY95I9T)eHa0f2 zySrQSYjNt-DY>qzVzH=jI4qyfS99OB=2egCJ`mS+mCxtZ-QBHJDzz_=@4x?E!C+8N zKmD}g@wmFWf4X${?p=NT_1Ajtx##rOTW?8{#>U2!OeQ5slarHjU00uf{<)f(nsn>d zElo{LsZ=WI#*G_?ZgX8%<#JgceDHy~y1G;>7WKs!Unrl?OOod1=9EsS_3_6atG&Iw zy1jDi@$I+Y>fXJ3x_I%Tyk4(jv6!x2y{c?Bdtj|Fe=#v3zu&L(=g-UM^T}}>jg5_| zySuyQ@l}4>KJ&~o=Gb%R&S_v^K#4>`BO@bfYip~yZ);xls_p}+tUtoxuuhyfq0P-r zNz&}>tk&1pb>YGV{qVyN)%8a-8rAOZt~?%(e*N`VNqe_tEiEmQq~_*kNqgV;7hZTl zCr_T#f55;%&24_aUuVvo(dz1|W@l%W%jG0V?d|Obxq}*VnbYysVLtk?K{w^7O8|yIZkX%sjsP*B`(A z_FK)dKA*383u*1U*1YOf-3Q`0j=Wy4LZOgCf1!}p*4E^@t|lfXbo%sZwY9ZreSKX+ zLqn3Jg@pzAd_Lv#`Rdg@6bdO4iAa)$hll0!`7}2-ryqa(QNR51i!NNaptiO)b6e$# z-`m@(&d$#2$q|V}luRa7EEe_3E3fREx3qWiOifKm+6%DqkfE`$QT_e>@_6#e?KWg#921mwD|JRZ+O=k%bRogFna zG*q{h%VpiZeOu2y`)tkQdvN_>jm_n9a$R@dgAHr^`1rUvuXDA`u zrat=UBY8ZY>Xo_@QEO|f*4EZEH#et_jtO=M|5~H8(dWzu&K5e=w-| z`FVN0UPU4i-Mo48&~269ddp-oT3ub$@4x@9R4P?noL1HllarHr?X}mmw6vt9r6u+E z_e+vaoH!xJadiFqbqx*<%5fasxN$?DeDaC>e*eB>Zf$K^phm1qoYIbyz`DE>GbK-avZ1nzDZ|ir`~`6eYLi>n%i1hS`-e46^q3b zi^bH}*C$EUTZ^8a9)-hUB@ziG5{c@g+uq*Z>gy(xNli~rYkGQGfj~fSf4}|qfjA`T z)TvXtb?cU*(Ws))Xmw3fIT5YnyZ@(Jt+ByiP+qTBKmYu*T3TAv*Vku`uQjiF)leuD zj^wi!j^ikuP9NF*X+EEq&*wXEk1y?AF}+^zzFVS4-PYLHcwnEIOs4u2sIjq8SFT*q zojZ5*>8GFW8(SLx2g!BEI-42W6ODdH5(ESyy6gwnH+%rXA?#h&NE7<+wo zGSkdGleuQ@iLnpjIQ}I7SNhMm2LRk<1OgZc1TYW?U?32{K$E@+>61_a2Rx?6wc(SK z6ZLw%acv$GB$Y~Oetv!|+a6mWQ55O;_*l-laSZ_zAdX}G`t|G23q+EfbDEu<9p?}L z6VH=j0UCez$j6T#)oeCLt{2C#Ha0dChT$!Y>gMCRi|$Pz8yg$i+}yl!SscgO+}yl& z`F(MCc&L0nKXSbw2;`hoCX=~^QQdr8chS8FBnSdYXCcIKd}+Dod5Ys$3kwUkJ+xah zI0m^~?ndk0^jLVFrw<=K+{(D_qI(m_;0?bxZ3usYKzn<8%4V|@`zGDQ@0|2O5Zw4! z{LZ@n>$vWsv3^UO&$k3|@Us@Ln1&ZbDc zUQa74DDv+>J&VUftI4|3 zKLM<)tZ03GU4=qH+uPeCm-Tu*)oL~6ayfskudi!uZB6_8`^g>6_kGptbuBI~s#>k8 zTrO*8XD11u)9J|bJeA93t*@^upUjsmS;JdWXk9BilXS!{`Pu3?dkeK+I-BQ@+Q0Kto|~+j&1RE3ptb&}aZRd;pJJVV z6Ph=222L-uYPGJ}-ilSr;(phYA949BWB-LuQp)p-GclUi?*=%;`6R7hi{iyv5f?#O6 zIBMPVC%3i!sBulIdlAU_D^;u2lIMAH≪w6h+B8hlht+US3X4l8dj^>-B$<`FR+I zs?};k&lp!N%VaXD)oLo0N-CGja?YvQY$kV{K``h4dh&eOK@g}=C=5LpT&LITUHU7` z^T)v2zl+Zw*1G4HU;O#1^_$J6Ij%`H@l&k9^M~}$v}SOtmoCq_7Zw)OZnu*=O{ddI zeu))^p*+tUnqoNTluD&WE{lKTIN3(0)6w4EUNWx1iPLVkC7oS~Tdmem*!%nY$#gLY z0yP?qD?hS!&Z*n&CfoEpZ|E3U`}e!YB93D@=aO+)>yH}OqBo;B zdi3bgzkZD%2-N9x^!oK{J$(33&N+FWr&q6D>G9*olFm-b+1Xh=efob?l9W!T6$F8{ zx3|@9w>332r57(=$T_EUI<2FlBYpbxNuK9vYHCW$%gaiq)8;bgoQ{u=)oQi0ySuBu z{`yM?2L~z?3d&?MnwgnVzu#A@)l$3N)`JHRbaZs2moHx?ul@b|cWrNP%kw<-`+Yrs z{#@yF`qDmUG8ujQ_Dz4EKY!MjFJI)GQy7NHmEPLF-Q8VHPfy?ISp59?Q(wP+)y~e2 zo;`b}nVA`L{ZZrkxBC6Qrl+UJ_BZJd;yC^_jtw6i9O&fa^$8j7Mw z*=$zJ%gYn}%dqsDZ<^x6bmpA2w)%(z(62?fj|HQfdGcFe$3nN_jP)D zI?f^Bj_ddPW8MDP0-2ke8`q{WVa9o`jP1Kvz$D-qlW_q+k6vSP7VLJ0uQDLdT*0K*xQ0nU>j z0X2Ve(>=iDWJ||Z?L4|$flxq>>!>vpNcTvWL{)L6d>)@Fiz_wt>T1m{ot5s922EBh z7E7dih)m|Vjv51N)Y!U4j_ag*oMFM|@o7<2ofcKr$>;HY&)hu< zfn1f9^?gawgn(anZ0V9e98y#HTs4*7tS*mF-;BI2%~M_)laKFHk?W|o?K9Q3 zZB($ZSgU3iX@6xHBq(r1}rWs;0eNE2``D?)ZNN zX`XC&SaRSa-M_m*+sn$-aNA)?Qf>a-Qsjp9Z1eNFDt=i1T->8*K~#&X>U2-_-RT}}K)B3108QVoQ)BBI zNz&u*#I>h*vL3qWY2rACE>EEzy6JyueOFMWhgRoEk{VmrXmfCDnkO3=0!fkMI(lt) zgQVnP)t=(XdUEEYX`X8MdvfNZI*~h8^&gCtB)zy}MMhoiBajsJUB;ad@T;c1UC-4l zOZP~ZMj|1%qeb1{tW$XNTMB>iZ$-ZTP;SRwDUsUI`4clAQB8ZhCItM+%i@1O?u9b} zfuyK0ko-fvV@sD-RM%h7VO40uztp|!(%heH>1waN|dRZplx z(LiP#gZDLSksWK4h{ses z?_TANn=WVj`#Qe%A3E{r3v#}CSDtI8s(9YL$w-=4rgXT~R6bXe4isw;xke`OO1fKjB6d3${p-a*Im`b+(}p?>s2?hi~PbvOqb7qjmDYSBf3_TKN;^ zD8FisV(ohr`)-?Z3P;O3Wr5KxZ0 z-@yNZN47UA5lcU226eY=IQ8?0ifQuJEFPRe2TA-yP&vL)%F4}?Bzb?vOgQzs*rA>3jXBEw z@w2SlJmvUCsW)=G-|c=g@;XgAc2H6|dk`c~%+HMBT(S#BUZQCRBB4Z&`?F8ojutuF z-qVO{Z`6o!Gfpqv<0{LOe<=sNWC$b$V+zJdlG?pvR2~VYyk6g7NT$j0jZ$y4ONrRo zhe5GJJC&VZB=3LJ1yWX)BqLfGJ_HCP1!KL* zK!SxMb&b=J@_Id?{+IRmCtdJdL&f92UXm33cGKCPmrhi{uO^m9x z?emn^>u>KcqH6X<%{7#~({7Vz>_ml|S1Ph=t$LkK70$X-g|qHduhXf>uC)p`uT+k2 zRC2G9g4%z!&$aj3Zb_%V)8US*SSgs}pCxauSMB~I>L@JIk^>*5^uOtO71JdpPuAn1 z4)u1Qkd*uvx?tk3hvnYml&oy|ZoFNQuRqkuK(kH;nt!%Pe$^c1U;i6r<#_tt+>!$y zsiUw+?fxV3=6W;cDMOwINzvPTSL*GSm6{On>w$l!Hr?^tm#>)H+`Cdb#!`PSP(oK95iLOi4Zs*HCv@<01$3 zO!I&9X`X8Md#3q$mBbILp)UF8?SnPTGA0g*KnA3?;ubBcs*@x=`v;GzPwdw6O;4%F z4X1gs;it$AYx$<9bZz&3EnnOtNm^7@moeuaeF$WL?y0_8w^qzkv>>WK|FK6W^Bt}F z@7W?9A^R-H$Q zKrT|>GuEqH-#-<~mCl>%)#9p!T2#fWM{$PD!L8cWxl64-zPvgR%FLS;2dc7oynG&? zs){Q$Wz>|6-edd}fdB@sVgZw}0UeVc0k4xE0U>{`IH8!d=V&6OWpjsk7GT&2#iTcO z_NZZJT$1$0!aUt@b(XnKRxB1vEhQjBof$?bcKY-Adz3^pRZ|v+j zD~vu{+k5+6pK}mg)*8+eha_F!xmSt6esg^S!DXp66wrUSy{XmjEj{P&a}qsz<>O`g z)82T$>Tk_|Kks%31ec}OP(W`UTCF$djM8H}mY?-IUp%7M*FUVkEWAOE>u9!r)_J$X zIS4LGwH3E$OK_{c&{owS-l+eqZ`K#3Gc-H6N7-31HO};C-}soOTwSf&id)XRo&N(J WMtx2FJ+qtu0000#)RxWP1EP?e> z)~H;)Anf4S)%;A*m~y0*d%SMG_p>&TkB6u7gEv;CrrM|`TI6Ubd=duvUWvKh^Q@z$u zb2g09z*x;X;MDZiPI*HoRTes$<_WKiR!^}4f1V})b%m>mu*}xIV0KMAqx@ucC${BX*@ps}n z;<`>x%zji|j{J0wHlh^7y7lbt=XFiILw{Y?r-&<}r4@^{s`CDHk2WBj<{f~#?><#+ z%SK7kjwGqJWutZm>eD>gzz|4^La~ru zYp9Wwd|TC6I8{&1ek{#XjeJkeeoUuwOSJNX5=qjFd)H;u***eE(V5e@6a8MzY-`hV zGuNbhq$?&7?NO|~S-pqF}wP*MDO6qng>)rip%U^0fH(d*Mt#e;_HU z^(X&RSM2K2y2_P8@i+#CjENZP2N@ zm(*SV&y+&xa^!1WvnfvsJM`llp?~iug0* zJ$RxortnuED%`kP?&)_afBt=Pe^uQsSJmyxpL3527Tm8yZ?{g>y`;Xb{y*<|cfuX= zb_Z1&7^_e$lrhI51d@XNC-zH{W;HuiSGHhK+laM)BT343T{C2J4X55iU#R!US1OqI zOXXdElVX8y)V1wZb-w>ch4*h#PC>Eq=ijH?YrG0>c_pQ7)|D;Ll*2Abf9GD)k&Zx8 zaORq-8Eu`aD_c0=^%Jp(qJd^fQf}e6A)ReFB%(bEH*S_|@6I@z<;Oe>;?wf8pi}PB!%QbSm57I=e@?l&L2VsJZAmNs_N{!VphAJP0HOCGKP( zf&3zk54ESfUSF{PpLKYr40^8Nk~@05Bq`R`c)|Bal}=UR&A+)Ij`+z|#X1fsx45F; z?H0sB>K^5kq>Q;wgg{bIQRtH-?JXUx>eg*3ub1U;n#=Rbf2S*F1fJE(8s(O2+9HLU zw<&(?+_6IM!CeY&c}0n6@&G&07gKoOhjNrmlB0NHzuT>D-KNidVO-DKcNkP?^ zDoN6w$q`kzewOlj{p}s{D(0HY(sqzq z=P&k((1Z3>WTMg<;D`_MLc?B{?q9mDO4o-EYVEQB-LLx8>kR4bPgZOBp^wu&+K93If6&Kz`;*n`b%ylZJt0M05#3gPOUB)g zE^!PpXlEYYASECBbN8+;jqC2x=CTE*6nJ~FVI`(J@zM) z4$6OsKrX}1K)qi0@+I}elOLRw2F7Yuvr{wLI+Y)dX!6PaFW|cxG#6c`?om$dEgh{r zR+G;?IL@8x)y3SV7ld&}ecS1N*XI}nSGAf;#34yHoM=+Qf6!c?KyX!R3HtT#J?pjM zy}K{EJSWkoS2nHDubX)Opg-;Y^Ch=KAh;^E1pQinWP{ev8>8RuU3uY z_O>uv2n1q}!-kN7{p)jw+JI3MDXs=)RT?=Q0}&56&w|D3Gz^Xc1uX{}cNqe)>B5De zDd%n({*ERcQM(qrQMziUhse1zj^5X#ZiQimP6RA$gme#88S#%J;c|gnDU4W}8##Pq zmXRp1Vj{LzS;RB)0&m`gOJ2q9kMIK(cO~s@Z6*T5f!zda@v06FX^xJ#K~s$(EMxlLfzrg@t9&3qvsZ7<8FwrhoI(Gq-b) zt#SSWidQ4CxjstNgsH6olPrmNZwUwM9uOGV1h)L_Mw0@3xQO6j0kK(OWjXi4TwIO{ zN68XeM2Fgt&Fp5eGhPf0UHenTNR@ul?6Etv<2sJZbsJ`Lsac=`eE{=PW>F_r3hUtdud%i7e_gNA#1YbPhaPfJUys6wMkp-@QPRU=s~_4V5|AXZW)*Z)h$$EQ5|+_YHCcQ8I{Qjt5Iz6i$%fTFFeu(-Ew@l_0+>TcrL*qDVaSqy^l&=+pryg8(7J>e^g_x}a~ zvzHGHBr};*4n9;C_jVY;CB7c!qAedIJy`6Er41mu_a^pvW~YYQtXK|DG#U&$OJYH$ z`kuVCI%X$JSte_^QC^NfAR0?c*Uy{{ZdA;UA3$w!cBTM6=jLwP=I45G@WwHeOXjt$ z>(;INtRO!-4xYu1e&k09tQxgg9@Ke~kv!${^76#|q7pR;qzRJe48G1deR?7K-3Q%V ze*PL4SJ&jk#BhhL$PulUFO^Egx7VO+@QrmM5nTT4S<=aqbwNr%8?3nCbJdQ#i6u`4Q*dZiKOW<8L1p-b{sa`r{8~nfM(qk^)R0SF$BJ%jDkP-sBE-mU&(? zF+XZxwgZkioQCH)61LASD3iRo=f3iG19dR z8^QmO7ZtJS)1|9-3J{3lTg91dr7&VQRyl)g^{ z0A3ccIr}OW?dVyGJ!V;c`LZtaxPJv@Lnz+;YuntKOw9>v=;8F4=vvUmEv@hMnePYb zD5E-6G@+Lz+?5P^W*WzjwnqUN!YRUdNlF(;m7xe!+hkSn}Py<VJUX<4B-HR z8jh3>1_m}uPZ!6KiaBp@`1{|EVru*Nov%#QeL|S0a{x!sqHCfljuRLC3q0DEYk#$7 zeI4Vb1V{1Ihpa2SLN;*(@#pF!uD;`Xuy|)nd6hZyEW7=(?5Cew{CIfdXL5S_^B@MX zU)O8(87fY2uo#RY>>-fS`1Nb)-pbF<_DyeOZ*UM>aWyN;Z1(rPJT?p$YxN%}zWiF% zwa7zTTl;E+i0iAbRg;#SOj%@ee(Sz{>n!?2T_>K}bfv;3Z2k4%$Z50umY@0@6DYFs z_FJtkrMdIxXIss^H$&SuSkyJ}uig3Lwd>YtoiFxX9y~R5XTdwy=XHt-Ag?DmHd+q0W+}TeohlzyDoHuF1sr>zkX#u`w|~*JWmAdLBQ1 z{MPN;moHtKl(lu%C6lYq&(GhwbLUFG<%{L*>sH);yXyY?*N+}K&GmB!I{eqCr`>UT zt3vM<%$_})OYz0;@9(eHOEXuQ_+HK1E_!_9CY{M&J}+B${q>|JKnbAq z+O@i-vwds-{0QuITWq!LW=>XpeSCU)x>lFd`T6$yw|2|O$-R2>#%K5L-LuZ7Sy@@F zdZycz^ze@7qzpSy0Bf}@SfnA==NU9Db=LXkSF0we?A*9c#dB5m|6gBMAA7vfi9?Eq z?f>fjzXz2kXnRi5Xemfu6SjJ#2G`O|El;3{U5|epGfa&PGk>yGqzJrC;y*$MneRn?RGE zzJ6&z!Gh*yW}rcjb{yg>Qax=1bkx`1f3tF|=Dzy!B?J^oz)z&!}1e)3oFMRfSCot96)&5$d(UsSGv~bPCf)(H2-w(HriJWF-XSc3Q z>gDsiiK#QMUAwkr`}Xj&XRS~8*S~r5CgvV6DRu#a*1a1`*N9;TnXy0>ySNW53ruPC>$PWFBhBU>ZG eC_)Z_WAl3py=T6&7ZU_lDh!^kelF{r5}E+l;D=KH diff --git a/packages/stream_chat_flutter/test/src/goldens/system_message_light.png b/packages/stream_chat_flutter/test/src/goldens/system_message_light.png index 32c2196cf3616acd94887e02b232c13f333e2d99..d82d8d609c2e3f352ef20dc48029787c3ac8cad5 100644 GIT binary patch literal 1866 zcmeHI`%}_c6vr~hn9R~@OEhTKO4-aElX6{=LPOD1M8#G`+)9{)wuea;XR|jx#XuiI^SobqAZlN2l4Q$g7syqt+CTM%jqkq8%|yZ5$fSJ z)U$HwMKfs`hcmm+TicNWHJM))X^;#_33RC*V^JgySgAU z^j=*RBCAV~RqJ%W5XnrY7%#jtJO2dD%K~;)DOhjsAplcrt{s{hR?vhbe{Dhp()%*$ z0XcNfmN2*cSME$YouG`XH7=d;c!(DY{2j~2$LVWUrOWH|LncOD9-l9(ttAZ_6f@gy zK7A~e3iI-|Jd#MNNlq;Jmfma?-*tr}=J&6s)9Ihy_k2G!*bw4MYt<|)$b*YKggrf> zQ~gq(2V|nHt?h@&$)3^Cie0;RZ|3@b`l#F2l5D_peer6wI*LY%1n9TpQXjJRivfti zhSOrPI55Y*V*kv-XFZy)f}5|Bb4-%Fyu3|d_h_dr27__7yY;eoX}k*&@3w8H&DtS( zQXrK|y|l8tWX;g)_42bL8J?b=_c~96qj?FOv2#rWv48{V(Ib&Hu@T_D%Bm{xI)Tw> zbXh#bLM|45*_GS*UIT?CFqvWQcG~{_`&m!RfbETGL=gf1XkNhGiu81|BsM$YMG5wj zX}UPu;pa-Dkxgx82Vt>Tutt7<^6ooBBYXN4H1VQT|4~<0NH6hDi83U|s8lM?- z=H|_}2t3u;rgawj?&Zb#x`KiNO>b}6+qb`B+}$1l(IePr2Xrh7$22{3zP`S`@`M`%LC!%z zLC(B@lgE!AS7VfMq_=01)jAUspmTJ#;X37ZE}mz5*i(Mox>!m#eg1s1PePR^PGoQQ zN)}{fWXKr^1i}u1P=jE|8XHB&q@r_8dx!}v7LZMR{9zzTfSbIRABN^tQ@=KkY#S|n z{W^AR?&8ytvk~ac#FdM;ym7c1p>W6H@=!3m(}t^P=`-Qlnwox+$z%}8Nz~9_;v466 zAGzGMNf$1N6$+YZW;jwv`m5tM@`^_^l^TKO^}cx{0FmN#(U}|<7Z>OJBOie5;F;|K zyKZ7mc(A>_-Oj-wA~CV9HFc?4C=}8LCKz?OofQNELDSPC1X5gHP6Y^%-N&T& z|LEx81y?ao53Ej4>n_FB;fd3FiJRIyR!N*VJUX<4B-HR z8jh3>1_rhoPZ!6KiaBp@Z1j%Kl|T0J^!Lgx^$U+UIn;t@X>zruXl(Jjcrx$k$-v+7 zC7NI23$hm`cxy^_Po5U!^Fm=}(uoT!(UW+Va&D=ZQzUY*d}aGDiS^U<)_?l)`{o|o zIlslvPA^s$K7Rc8awdao_3Nq_9#|<098efVSVLff;jFXE;`UaRUQK==r7*#8kw(z= z+k0Ob7V!S5y~-S;{FCa$pPoBY~nW)D~Ep`ClPw$5r&@TjvlHC^hre6ayfsD{X-`+tky7k=NH zv%YTs`ndJe&-^%Fon|C?v~W$*#uWz>7WgeMei!9YZ(I54$;(jwLfI=>rnP^51a8l} zd+Fc5eSUs^CIA1`u3f+W^^+$qfg&rzR=<3AclWOk51XUbPRqNqW1-vPg;jgk%{I?p zbu}x~Lgv-Exz<&mpY?wG{(Y*~(l7u1R;O-`^!4{w|Gl^7XVCrq_1BLaX(=l!`||U1 z`qCiHx9{F*P4&vGs;YW(Z)=oqubcDBlBnqD=u>H%v&?3TU#t7`W8wAn@!GS`uDY1< z;`R0QCu8pBna_Ov;)MrLp?=(+0IRuStFK;~t{)$wHFe#G{e`t3Sbna&n2}Lf82J6& z-C&8fTQ_e8F28&+!{pMTLrp4~$;r-#9|lY^vA17ev~x}Q`+F;|zkd1faQmz8zi-{R zaUsDVVEgT>i{1ODoPH|8(zxpCtJdWwK7HC$v~$ip`}%dax94YPW_l({th$@0&C#^6 z+C6YdiPhShHzQMxBv~B|%6un@iHnQ2Eo5tMtlAq_RAMt*yGzOQ(!G20Hr-s~wR93N zrnYU{_AkEvTXz3>1$)=PB|d7!iErPWja?fy_55?u;~#fsH!18Wkg>F~nxyiy$kH=N zqV3nWx3`y8_wPQQTCK0EtI5F>swJA6pPyY+RP-iF<~Y!Y7l9V=@$s2R^_EmrXn4+= zIdkWR>lr3NTcbjE-wm^y@BirMWx*5vH*>(%FIh$6WJ@53ldw1@HtPHvI_;^3iFIIEU-PpR>{MV~jtDZf3cJA!e_uuvR zoa^l5T)SpXMrx|-(jZ`f{JXa2f0)e1x~PbVfTckf-@VJ*6s93!Vs5@X{ro)5xqj;M zhyG7j;MlTf&zkM|_g6)&ef8x_NOpGie$(@*)#*l(Krb!5oGB|K^WtDLd)MP1m$okV zpReY>T!8}=RltyYGx_}UP`&A2UtU%RCP(i{Y3b>qI%4(N_v%-7K7Rc8@@lz9yE+zm zw70jSpmWKVgVMe!vTRJs`ur9>HcBCjF|GW{WE9I0IjK) zo;+D{?b{8#i9IRkrlBnrsl>kzs2ss4)v;L{@(8w?N#{;aF7(8A5T-G@y GGywo&aKg+0 From 4182ad35011526870d933da64bfbe729f9e0e940 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 17 Jun 2021 13:04:53 +0200 Subject: [PATCH 059/150] add thread parent indicators --- packages/stream_chat/lib/src/api/channel.dart | 25 ++--- .../lib/src/channel_info.dart | 5 + .../lib/src/channel_preview.dart | 95 ++++++++++--------- .../lib/src/message_input.dart | 5 +- .../lib/src/thread_header.dart | 53 +++++++---- .../lib/src/typing_indicator.dart | 23 ++--- 6 files changed, 118 insertions(+), 88 deletions(-) diff --git a/packages/stream_chat/lib/src/api/channel.dart b/packages/stream_chat/lib/src/api/channel.dart index aeda98e7..286a7e07 100644 --- a/packages/stream_chat/lib/src/api/channel.dart +++ b/packages/stream_chat/lib/src/api/channel.dart @@ -3,7 +3,7 @@ import 'dart:convert'; import 'dart:math'; import 'package:collection/collection.dart' - show IterableExtension, ListEquality; + show IterableExtension, ListEquality, MapEquality; import 'package:dio/dio.dart'; import 'package:logging/logging.dart'; import 'package:rxdart/rxdart.dart'; @@ -1848,17 +1848,17 @@ class ChannelClientState { } /// Channel related typing users last value - List get typingEvents => _typingEventsController.value; + Map get typingEvents => _typingEventsController.value; /// Channel related typing users stream - Stream> get typingEventsStream => - _typingEventsController.stream.distinct(const ListEquality().equals); + Stream> get typingEventsStream => + _typingEventsController.stream; - final BehaviorSubject> _typingEventsController = - BehaviorSubject.seeded([]); + final BehaviorSubject> _typingEventsController = + BehaviorSubject.seeded({}); final Channel _channel; - final Map _typings = {}; + final Map _typings = {}; void _listenTypingEvents() { if (_channelState.channel?.config.typingEvents == false) { @@ -1872,8 +1872,8 @@ class ChannelClientState { if (event.user != null) { final user = event.user!; if (user.id != _channel.client.state.user?.id) { - _typings[user] = DateTime.now(); - _typingEventsController.add(_typings.keys.toList()); + _typings[user] = event; + _typingEventsController.add(_typings); } } }, @@ -1886,7 +1886,7 @@ class ChannelClientState { final user = event.user!; if (user.id != _channel.client.state.user?.id) { _typings.remove(event.user); - _typingEventsController.add(_typings.keys.toList()); + _typingEventsController.add(_typings); } } }, @@ -1964,13 +1964,14 @@ class ChannelClientState { void _clean() { final now = DateTime.now(); - _typings.forEach((user, lastTypingEvent) { - if (now.difference(lastTypingEvent).inSeconds > 7) { + _typings.forEach((user, event) { + if (now.difference(event.createdAt!).inSeconds > 7) { _channel.client.handleEvent( Event( type: EventType.typingStop, user: user, cid: _channel.cid, + parentId: event.parentId, ), ); } diff --git a/packages/stream_chat_flutter/lib/src/channel_info.dart b/packages/stream_chat_flutter/lib/src/channel_info.dart index 32dbeb67..a17260ec 100644 --- a/packages/stream_chat_flutter/lib/src/channel_info.dart +++ b/packages/stream_chat_flutter/lib/src/channel_info.dart @@ -11,6 +11,7 @@ class ChannelInfo extends StatelessWidget { required this.channel, this.textStyle, this.showTypingIndicator = true, + this.parentId, }) : super(key: key); /// The channel about which the info is to be displayed @@ -22,6 +23,9 @@ class ChannelInfo extends StatelessWidget { /// If true the typing indicator will be rendered if a user is typing final bool showTypingIndicator; + /// Id of the parent message in case of a thread + final String? parentId; + @override Widget build(BuildContext context) { final client = StreamChat.of(context).client; @@ -88,6 +92,7 @@ class ChannelInfo extends StatelessWidget { } return TypingIndicator( + parentId: parentId, alignment: Alignment.center, alternativeWidget: alternativeWidget, style: textStyle, diff --git a/packages/stream_chat_flutter/lib/src/channel_preview.dart b/packages/stream_chat_flutter/lib/src/channel_preview.dart index d9170eb8..231b5f94 100644 --- a/packages/stream_chat_flutter/lib/src/channel_preview.dart +++ b/packages/stream_chat_flutter/lib/src/channel_preview.dart @@ -221,59 +221,62 @@ class ChannelPreview extends StatelessWidget { ); } - Widget _buildLastMessage(BuildContext context) => - BetterStreamBuilder?>( - stream: channel.state!.messagesStream, - initialData: channel.state!.messages, - builder: (context, data) { - final lastMessage = - data?.lastWhereOrNull((m) => m.shadowed != true && !m.isDeleted); - if (lastMessage == null) { - return const SizedBox(); - } + Widget _buildLastMessage(BuildContext context) => Align( + alignment: Alignment.centerLeft, + child: BetterStreamBuilder?>( + stream: channel.state!.messagesStream, + initialData: channel.state!.messages, + builder: (context, data) { + final lastMessage = data + ?.lastWhereOrNull((m) => m.shadowed != true && !m.isDeleted); + if (lastMessage == null) { + return const SizedBox(); + } - var text = lastMessage.text; - final parts = [ - ...lastMessage.attachments.map((e) { - if (e.type == 'image') { - return '📷'; - } else if (e.type == 'video') { - return '🎬'; - } else if (e.type == 'giphy') { - return '[GIF]'; - } - return e == lastMessage.attachments.last - ? (e.title ?? 'File') - : '${e.title ?? 'File'} , '; - }), - lastMessage.text ?? '', - ]; + var text = lastMessage.text; + final parts = [ + ...lastMessage.attachments.map((e) { + if (e.type == 'image') { + return '📷'; + } else if (e.type == 'video') { + return '🎬'; + } else if (e.type == 'giphy') { + return '[GIF]'; + } + return e == lastMessage.attachments.last + ? (e.title ?? 'File') + : '${e.title ?? 'File'} , '; + }), + lastMessage.text ?? '', + ]; - text = parts.join(' '); + text = parts.join(' '); - final chatThemeData = StreamChatTheme.of(context); - return Text.rich( - _getDisplayText( - text, - lastMessage.mentionedUsers, - lastMessage.attachments, - chatThemeData.channelPreviewTheme.subtitle?.copyWith( + final chatThemeData = StreamChatTheme.of(context); + return Text.rich( + _getDisplayText( + text, + lastMessage.mentionedUsers, + lastMessage.attachments, + chatThemeData.channelPreviewTheme.subtitle?.copyWith( + color: chatThemeData.channelPreviewTheme.subtitle?.color, + fontStyle: (lastMessage.isSystem || lastMessage.isDeleted) + ? FontStyle.italic + : FontStyle.normal), + chatThemeData.channelPreviewTheme.subtitle?.copyWith( color: chatThemeData.channelPreviewTheme.subtitle?.color, fontStyle: (lastMessage.isSystem || lastMessage.isDeleted) ? FontStyle.italic - : FontStyle.normal), - chatThemeData.channelPreviewTheme.subtitle?.copyWith( - color: chatThemeData.channelPreviewTheme.subtitle?.color, - fontStyle: (lastMessage.isSystem || lastMessage.isDeleted) - ? FontStyle.italic - : FontStyle.normal, - fontWeight: FontWeight.bold, + : FontStyle.normal, + fontWeight: FontWeight.bold, + ), ), - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ); - }, + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.start, + ); + }, + ), ); TextSpan _getDisplayText( diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 7e62f1c3..4f61c9fa 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -700,7 +700,10 @@ class MessageInputState extends State { if (!mounted) { return; } - StreamChannel.of(context).channel.keyStroke().catchError((e) {}); + StreamChannel.of(context) + .channel + .keyStroke(widget.parentMessage?.id) + .catchError((e) {}); setState(() { _messageIsPresent = s.trim().isNotEmpty; diff --git a/packages/stream_chat_flutter/lib/src/thread_header.dart b/packages/stream_chat_flutter/lib/src/thread_header.dart index d16aa530..bfde9a4c 100644 --- a/packages/stream_chat_flutter/lib/src/thread_header.dart +++ b/packages/stream_chat_flutter/lib/src/thread_header.dart @@ -68,6 +68,7 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { this.leading, this.actions, this.onTitleTap, + this.showTypingIndicator = true, }) : preferredSize = const Size.fromHeight(kToolbarHeight), super(key: key); @@ -96,9 +97,32 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { /// AppBar actions final List? actions; + /// If true the typing indicator will be rendered + /// if a user is typing in this thread + final bool showTypingIndicator; + @override Widget build(BuildContext context) { final chatThemeData = StreamChatTheme.of(context); + + final defaultSubtitle = subtitle ?? + Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'with ', + style: chatThemeData.channelTheme.channelHeaderTheme.subtitle, + ), + Flexible( + child: ChannelName( + textStyle: + chatThemeData.channelTheme.channelHeaderTheme.subtitle, + ), + ), + ], + ); + return AppBar( automaticallyImplyLeading: false, textTheme: Theme.of(context).textTheme, @@ -119,6 +143,7 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { onTap: onTitleTap, child: SizedBox( height: preferredSize.height, + width: 250, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -128,24 +153,16 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { style: chatThemeData.channelTheme.channelHeaderTheme.title, ), const SizedBox(height: 2), - subtitle ?? - Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'with ', - style: chatThemeData - .channelTheme.channelHeaderTheme.subtitle, - ), - Flexible( - child: ChannelName( - textStyle: chatThemeData - .channelTheme.channelHeaderTheme.subtitle, - ), - ), - ], - ), + if (showTypingIndicator) + TypingIndicator( + alignment: Alignment.center, + channel: StreamChannel.of(context).channel, + style: chatThemeData.channelTheme.channelHeaderTheme.subtitle, + parentId: parent.id, + alternativeWidget: defaultSubtitle, + ) + else + defaultSubtitle, ], ), ), diff --git a/packages/stream_chat_flutter/lib/src/typing_indicator.dart b/packages/stream_chat_flutter/lib/src/typing_indicator.dart index 100cdb53..4c0ad19f 100644 --- a/packages/stream_chat_flutter/lib/src/typing_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/typing_indicator.dart @@ -12,6 +12,7 @@ class TypingIndicator extends StatelessWidget { this.style, this.alignment = Alignment.centerLeft, this.padding = const EdgeInsets.all(0), + this.parentId, }) : super(key: key); /// Style of the text widget @@ -29,21 +30,21 @@ class TypingIndicator extends StatelessWidget { /// Alignment of the typing indicator final Alignment alignment; + /// Id of the parent message in case of a thread + final String? parentId; + @override Widget build(BuildContext context) { final channelState = channel?.state ?? StreamChannel.of(context).channel.state!; - final altWidget = Align( - key: const Key('alternative'), - alignment: alignment, - child: Container( - child: alternativeWidget ?? const Offstage(), - ), - ); - return BetterStreamBuilder>( - initialData: channelState.typingEvents, - stream: channelState.typingEventsStream, + final altWidget = alternativeWidget ?? const Offstage(); + + return BetterStreamBuilder>( + initialData: channelState.typingEvents.keys, + stream: channelState.typingEventsStream.map((typings) => typings.entries + .where((element) => element.value.parentId == parentId) + .map((e) => e.key)), builder: (context, data) => AnimatedSwitcher( duration: const Duration(milliseconds: 300), child: data.isNotEmpty == true @@ -63,7 +64,7 @@ class TypingIndicator extends StatelessWidget { ), Text( // ignore: lines_longer_than_80_chars - ' ${data[0].name}${data.length == 1 ? '' : ' and ${data.length - 1} more'} ${data.length == 1 ? 'is' : 'are'} typing', + ' ${data.elementAt(0).name}${data.length == 1 ? '' : ' and ${data.length - 1} more'} ${data.length == 1 ? 'is' : 'are'} typing', maxLines: 1, style: style, ), From 5cd9a3be5359b3e65a04c37beac613792ce23d63 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 17 Jun 2021 13:15:01 +0200 Subject: [PATCH 060/150] fix analysis --- packages/stream_chat/lib/src/api/channel.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat/lib/src/api/channel.dart b/packages/stream_chat/lib/src/api/channel.dart index 286a7e07..b2e33084 100644 --- a/packages/stream_chat/lib/src/api/channel.dart +++ b/packages/stream_chat/lib/src/api/channel.dart @@ -3,7 +3,7 @@ import 'dart:convert'; import 'dart:math'; import 'package:collection/collection.dart' - show IterableExtension, ListEquality, MapEquality; + show IterableExtension, ListEquality; import 'package:dio/dio.dart'; import 'package:logging/logging.dart'; import 'package:rxdart/rxdart.dart'; From 73d652772518bb089aa8d421843b8f90cb018e11 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 17 Jun 2021 17:09:50 +0530 Subject: [PATCH 061/150] fix analyzer warnings Signed-off-by: Sahil Kumar --- packages/stream_chat/lib/src/client/client.dart | 2 ++ packages/stream_chat/lib/src/core/models/event.dart | 2 -- packages/stream_chat/test/src/matchers.dart | 1 - packages/stream_chat/test/src/mocks.dart | 3 --- packages/stream_chat_flutter/analysis_options.yaml | 2 +- .../stream_chat_flutter/lib/src/channel_list_view.dart | 4 ++++ .../lib/src/channel_list_core.dart | 10 ++++------ 7 files changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index ecadfe3b..b9e1c5d7 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -1373,6 +1373,7 @@ class ClientState { _userController.add(user); } + /// Update all the [users] with the provided [userList] void updateUsers(List userList) { final newUsers = { ...users, @@ -1382,6 +1383,7 @@ class ClientState { _usersController.add(newUsers); } + /// Update the passed [user] in state void updateUser(User? user) => updateUsers([user]); /// The current user diff --git a/packages/stream_chat/lib/src/core/models/event.dart b/packages/stream_chat/lib/src/core/models/event.dart index 21dd069c..26831555 100644 --- a/packages/stream_chat/lib/src/core/models/event.dart +++ b/packages/stream_chat/lib/src/core/models/event.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/core/models/channel_model.dart'; import 'package:stream_chat/src/core/models/message.dart'; diff --git a/packages/stream_chat/test/src/matchers.dart b/packages/stream_chat/test/src/matchers.dart index c039b416..27cfa43d 100644 --- a/packages/stream_chat/test/src/matchers.dart +++ b/packages/stream_chat/test/src/matchers.dart @@ -1,7 +1,6 @@ import 'package:collection/collection.dart'; import 'package:dio/dio.dart' show MultipartFile; import 'package:stream_chat/src/client/channel.dart'; -import 'package:stream_chat/src/core/models/channel_model.dart'; import 'package:stream_chat/src/core/models/channel_state.dart'; import 'package:stream_chat/src/core/models/event.dart'; import 'package:stream_chat/src/core/models/message.dart'; diff --git a/packages/stream_chat/test/src/mocks.dart b/packages/stream_chat/test/src/mocks.dart index 528c8a91..c4a5d64f 100644 --- a/packages/stream_chat/test/src/mocks.dart +++ b/packages/stream_chat/test/src/mocks.dart @@ -3,7 +3,6 @@ import 'package:logging/logging.dart'; import 'package:mocktail/mocktail.dart'; import 'package:stream_chat/src/client/channel.dart'; import 'package:stream_chat/src/client/client.dart'; -import 'package:stream_chat/src/client/retry_policy.dart'; import 'package:stream_chat/src/core/api/attachment_file_uploader.dart'; import 'package:stream_chat/src/core/api/channel_api.dart'; import 'package:stream_chat/src/core/api/device_api.dart'; @@ -16,12 +15,10 @@ import 'package:stream_chat/src/core/http/connection_id_manager.dart'; import 'package:stream_chat/src/core/http/stream_http_client.dart'; import 'package:stream_chat/src/core/http/token_manager.dart'; import 'package:stream_chat/src/core/models/channel_config.dart'; -import 'package:stream_chat/src/core/models/channel_model.dart'; import 'package:stream_chat/src/db/chat_persistence_client.dart'; import 'package:stream_chat/src/ws/websocket.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; -import 'db/chat_persistence_client_test.dart'; class MockWebSocketChannel extends Mock implements WebSocketChannel {} diff --git a/packages/stream_chat_flutter/analysis_options.yaml b/packages/stream_chat_flutter/analysis_options.yaml index f0a87ea5..ed0ce983 100644 --- a/packages/stream_chat_flutter/analysis_options.yaml +++ b/packages/stream_chat_flutter/analysis_options.yaml @@ -6,7 +6,7 @@ analyzer: - example/** - lib/src/emoji - lib/**/*.freezed.dart - - test/** +# - test/** linter: rules: diff --git a/packages/stream_chat_flutter/lib/src/channel_list_view.dart b/packages/stream_chat_flutter/lib/src/channel_list_view.dart index f6acf35e..d131c33b 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -113,9 +113,13 @@ class ChannelListView extends StatefulWidget { /// If true listen to changes to this Channel in real time. final bool watch; + /// If true you’ll receive user presence updates via the websocket events final bool presence; + /// Number of members to fetch in each channel final int? memberLimit; + + /// Number of messages to fetch in each channel final int? messageLimit; /// Pagination parameters diff --git a/packages/stream_chat_flutter_core/lib/src/channel_list_core.dart b/packages/stream_chat_flutter_core/lib/src/channel_list_core.dart index 684fd630..6dc52647 100644 --- a/packages/stream_chat_flutter_core/lib/src/channel_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/channel_list_core.dart @@ -98,12 +98,6 @@ class ChannelListCore extends StatefulWidget { /// You can also filter other built-in channel fields. final Filter? filter; - // bool state = true, - // bool watch = true, - // bool presence = false, - // int? memberLimit, - // int? messageLimit, - /// The sorting used for the channels matching the filters. /// Sorting is based on field and direction, multiple sorting options can be /// provided. @@ -117,9 +111,13 @@ class ChannelListCore extends StatefulWidget { /// If true listen to changes to this Channel in real time. final bool watch; + /// If true you’ll receive user presence updates via the websocket events final bool presence; + /// Number of members to fetch in each channel final int? memberLimit; + + /// Number of messages to fetch in each channel final int? messageLimit; /// Pagination parameters From a5bc00fe5f61b465a6c59b8f53c3f0b8cd2a4f6e Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 17 Jun 2021 17:18:47 +0530 Subject: [PATCH 062/150] add channel.setExtraData tests Signed-off-by: Sahil Kumar --- .../test/src/api/channel_test.dart | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/packages/stream_chat/test/src/api/channel_test.dart b/packages/stream_chat/test/src/api/channel_test.dart index 22d04a43..6883d3b1 100644 --- a/packages/stream_chat/test/src/api/channel_test.dart +++ b/packages/stream_chat/test/src/api/channel_test.dart @@ -35,6 +35,45 @@ void main() { return logger; } + group('Non-Initialized Channel', () { + late final client = MockStreamChatClient(); + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + late Channel channel; + + setUpAll(() { + // detached loggers + when(() => client.detachedLogger(any())).thenAnswer((invocation) { + final name = invocation.positionalArguments.first; + return _createLogger(name); + }); + + // client logger + when(() => client.logger).thenReturn(_createLogger('mock-client-logger')); + }); + + setUp(() { + channel = Channel(client, channelType, channelId); + }); + + tearDown(() { + channel.dispose(); + }); + + test('should be able to set `extraData`', () { + expect(channel.extraData.isEmpty, isTrue); + + expect( + () => channel.extraData = {'name': 'test-channel-name'}, + returnsNormally, + ); + + expect(channel.extraData.isEmpty, isFalse); + expect(channel.extraData.containsKey('name'), isTrue); + expect(channel.extraData['name'], 'test-channel-name'); + }); + }); + // TODO : test all persistence related logic in this group group('Initialized Channel with Persistence', () { late final client = MockStreamChatClientWithPersistence(); @@ -145,6 +184,14 @@ void main() { channel.dispose(); }); + test('should throw if trying to set `extraData`', () { + try { + channel.extraData = {'name': 'test-channel-name'}; + } catch (e) { + expect(e, isA()); + } + }); + group('`.sendMessage`', () { test('should work fine', () async { final message = Message(id: 'test-message-id'); From dc4b3f11a4f58785b29dfd4152cb8db30e52e75a Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 17 Jun 2021 17:23:53 +0530 Subject: [PATCH 063/150] flutter format . Signed-off-by: Sahil Kumar --- packages/stream_chat/test/src/mocks.dart | 1 - packages/stream_chat_flutter_core/analysis_options.yaml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/stream_chat/test/src/mocks.dart b/packages/stream_chat/test/src/mocks.dart index c4a5d64f..44078519 100644 --- a/packages/stream_chat/test/src/mocks.dart +++ b/packages/stream_chat/test/src/mocks.dart @@ -19,7 +19,6 @@ import 'package:stream_chat/src/db/chat_persistence_client.dart'; import 'package:stream_chat/src/ws/websocket.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; - class MockWebSocketChannel extends Mock implements WebSocketChannel {} class MockWebSocketSink extends Mock implements WebSocketSink {} diff --git a/packages/stream_chat_flutter_core/analysis_options.yaml b/packages/stream_chat_flutter_core/analysis_options.yaml index f0a87ea5..ed0ce983 100644 --- a/packages/stream_chat_flutter_core/analysis_options.yaml +++ b/packages/stream_chat_flutter_core/analysis_options.yaml @@ -6,7 +6,7 @@ analyzer: - example/** - lib/src/emoji - lib/**/*.freezed.dart - - test/** +# - test/** linter: rules: From c37faddd2ee28db16f3195565b955258be226cf7 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 17 Jun 2021 14:03:35 +0200 Subject: [PATCH 064/150] fix tests --- .../test/src/channel_preview_test.dart | 4 ---- .../test/src/full_screen_media_test.dart | 16 ++++++++-------- packages/stream_chat_flutter/test/src/mocks.dart | 4 ++-- .../test/src/typing_indicator_test.dart | 15 ++++++++------- 4 files changed, 18 insertions(+), 21 deletions(-) diff --git a/packages/stream_chat_flutter/test/src/channel_preview_test.dart b/packages/stream_chat_flutter/test/src/channel_preview_test.dart index b913489e..baa82614 100644 --- a/packages/stream_chat_flutter/test/src/channel_preview_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_preview_test.dart @@ -60,10 +60,6 @@ void main() { ) ])); - when(() => channelState.typingEvents).thenReturn([]); - when(() => channelState.typingEventsStream) - .thenAnswer((_) => Stream.value([])); - await tester.pumpWidget(MaterialApp( home: StreamChat( client: client, diff --git a/packages/stream_chat_flutter/test/src/full_screen_media_test.dart b/packages/stream_chat_flutter/test/src/full_screen_media_test.dart index 81c342eb..d418c303 100644 --- a/packages/stream_chat_flutter/test/src/full_screen_media_test.dart +++ b/packages/stream_chat_flutter/test/src/full_screen_media_test.dart @@ -53,15 +53,15 @@ void main() { user: User(id: 'other-user'), ) ])); - - when(() => channelState.typingEvents).thenAnswer((i) => [ - User(id: 'other-user', extraData: {'name': 'demo'}) - ]); + when(() => channelState.typingEvents).thenAnswer((i) => { + User(id: 'other-user', extraData: {'name': 'demo'}): + const Event(type: EventType.typingStart), + }); when(() => channelState.typingEventsStream) - .thenAnswer((i) => Stream.value([ - User(id: 'other-user', extraData: {'name': 'demo'}), - User(id: 'other-user', extraData: {'name': 'demo'}), - ])); + .thenAnswer((i) => Stream.value({ + User(id: 'other-user', extraData: {'name': 'demo'}): + const Event(type: EventType.typingStart), + })); await tester.pumpWidget(MaterialApp( home: StreamChat( diff --git a/packages/stream_chat_flutter/test/src/mocks.dart b/packages/stream_chat_flutter/test/src/mocks.dart index d2b04e11..28e00e4b 100644 --- a/packages/stream_chat_flutter/test/src/mocks.dart +++ b/packages/stream_chat_flutter/test/src/mocks.dart @@ -23,8 +23,8 @@ class MockChannel extends Mock implements Channel { class MockChannelState extends Mock implements ChannelClientState { MockChannelState() { - when(() => typingEvents).thenReturn([]); - when(() => typingEventsStream).thenAnswer((_) => Stream.value([])); + when(() => typingEvents).thenReturn({}); + when(() => typingEventsStream).thenAnswer((_) => Stream.value({})); } } diff --git a/packages/stream_chat_flutter/test/src/typing_indicator_test.dart b/packages/stream_chat_flutter/test/src/typing_indicator_test.dart index cc9af663..75431f44 100644 --- a/packages/stream_chat_flutter/test/src/typing_indicator_test.dart +++ b/packages/stream_chat_flutter/test/src/typing_indicator_test.dart @@ -53,14 +53,15 @@ void main() { ) ])); - when(() => channelState.typingEvents).thenAnswer((i) => [ - User(id: 'other-user', extraData: {'name': 'demo'}) - ]); + when(() => channelState.typingEvents).thenAnswer((i) => { + User(id: 'other-user', extraData: {'name': 'demo'}): + const Event(type: EventType.typingStart), + }); when(() => channelState.typingEventsStream) - .thenAnswer((i) => Stream.value([ - User(id: 'other-user', extraData: {'name': 'demo'}), - User(id: 'other-user', extraData: {'name': 'demo'}), - ])); + .thenAnswer((i) => Stream.value({ + User(id: 'other-user', extraData: {'name': 'demo'}): + const Event(type: EventType.typingStart), + })); await tester.pumpWidget(MaterialApp( home: StreamChat( From 665ac322f30a0f6a4e6473478021f1b7202208d8 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 17 Jun 2021 15:04:03 +0200 Subject: [PATCH 065/150] update changelogs and pubspecs --- packages/stream_chat_flutter/CHANGELOG.md | 6 ++++++ packages/stream_chat_flutter/pubspec.yaml | 4 ++-- packages/stream_chat_flutter_core/CHANGELOG.md | 4 ++++ packages/stream_chat_flutter_core/pubspec.yaml | 2 +- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 170bcf6b..ac8ae565 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,3 +1,9 @@ +## 2.0.0-nullsafety.7 + +- Minor fixes and improvements +- Updated `stream_chat_core` dependency +- Fixed a bug with connectivity implementation + ## 2.0.0-nullsafety.6 - Minor fixes and improvements diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index e165b136..bcebde99 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -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: 2.0.0-nullsafety.6 +version: 2.0.0-nullsafety.7 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -36,7 +36,7 @@ dependencies: scrollable_positioned_list: ^0.2.0-nullsafety.0 share_plus: ^2.0.3 shimmer: ^2.0.0 - stream_chat_flutter_core: ^2.0.0-nullsafety.6 + stream_chat_flutter_core: ^2.0.0-nullsafety.7 substring_highlight: ^1.0.26 synchronized: ^3.0.0 url_launcher: ^6.0.3 diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index 71ff8f51..bd176c12 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.0-nullsafety.7 + +* Fixed a bug with connectivity implementation + ## 2.0.0-nullsafety.6 * Update llc dependency diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml index 40a40c39..14fa0f80 100644 --- a/packages/stream_chat_flutter_core/pubspec.yaml +++ b/packages/stream_chat_flutter_core/pubspec.yaml @@ -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: 2.0.0-nullsafety.6 +version: 2.0.0-nullsafety.7 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues From 678945f7ef1ffd706f11d78bdcf9d97726dc098f Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 17 Jun 2021 21:31:06 +0530 Subject: [PATCH 066/150] refactor stream_flutter_workflow.yml Signed-off-by: Sahil Kumar --- .github/workflows/scripts/install-flutter.sh | 13 -- .github/workflows/scripts/install-tools.sh | 6 - .../{coverage.sh => remove-from_coverage.sh} | 1 + .github/workflows/stream_flutter_workflow.yml | 137 ++++++++++------ ...ysis_options.yaml => analysis_options.yaml | 0 melos.yaml | 96 ++++++----- packages/stream_chat/peanut.yaml | 3 - .../stream_chat_flutter/analysis_options.yaml | 150 ------------------ .../analysis_options.yaml | 150 ------------------ .../analysis_options.yaml | 150 ------------------ 10 files changed, 141 insertions(+), 565 deletions(-) delete mode 100755 .github/workflows/scripts/install-flutter.sh delete mode 100755 .github/workflows/scripts/install-tools.sh rename .github/workflows/scripts/{coverage.sh => remove-from_coverage.sh} (80%) mode change 100755 => 100644 rename packages/stream_chat/analysis_options.yaml => analysis_options.yaml (100%) delete mode 100644 packages/stream_chat/peanut.yaml delete mode 100644 packages/stream_chat_flutter/analysis_options.yaml delete mode 100644 packages/stream_chat_flutter_core/analysis_options.yaml delete mode 100644 packages/stream_chat_persistence/analysis_options.yaml diff --git a/.github/workflows/scripts/install-flutter.sh b/.github/workflows/scripts/install-flutter.sh deleted file mode 100755 index 247d2797..00000000 --- a/.github/workflows/scripts/install-flutter.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash - -BRANCH=$1 - -if [ "$BRANCH" == "dev" ] -then - # TODO Flutter dev branch is currently broken so we're unable to test MacOS. - echo "TODO: Skipping macOS testing due to Flutter dev branch issue. Switching branch to stable." - BRANCH=stable -fi - -git clone https://github.com/flutter/flutter.git --depth 1 -b $BRANCH _flutter -echo "::add-path::$GITHUB_WORKSPACE/_flutter/bin" \ No newline at end of file diff --git a/.github/workflows/scripts/install-tools.sh b/.github/workflows/scripts/install-tools.sh deleted file mode 100755 index 087cfecb..00000000 --- a/.github/workflows/scripts/install-tools.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash - -flutter pub global activate melos -echo "::add-path::$HOME/.pub-cache/bin" -echo "::add-path::$GITHUB_WORKSPACE/_flutter/.pub-cache/bin" -echo "::add-path::$GITHUB_WORKSPACE/_flutter/bin/cache/dart-sdk/bin" \ No newline at end of file diff --git a/.github/workflows/scripts/coverage.sh b/.github/workflows/scripts/remove-from_coverage.sh old mode 100755 new mode 100644 similarity index 80% rename from .github/workflows/scripts/coverage.sh rename to .github/workflows/scripts/remove-from_coverage.sh index edff4ae4..c1190097 --- a/.github/workflows/scripts/coverage.sh +++ b/.github/workflows/scripts/remove-from_coverage.sh @@ -3,4 +3,5 @@ # Fast fail the script on failures. set -e +pub global activate remove_from_coverage pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r '\.g\.dart$' -r '\.freezed\.dart$' \ No newline at end of file diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml index 95f48ee9..a8650e65 100644 --- a/.github/workflows/stream_flutter_workflow.yml +++ b/.github/workflows/stream_flutter_workflow.yml @@ -17,89 +17,124 @@ jobs: timeout-minutes: 15 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - name: "Git Checkout" + uses: actions/checkout@v2 with: fetch-depth: 0 - - name: 'Install Flutter' - run: ./.github/workflows/scripts/install-flutter.sh stable - - name: 'Install Tools' + - name: "Install Flutter" + uses: subosito/flutter-action@v1 + with: + channel: 'stable' + - name: "Install Tools" run: | - ./.github/workflows/scripts/install-tools.sh + flutter pub global activate melos flutter pub global activate tuneup - - name: 'Bootstrap Workspace' - run: melos bootstrap --verbose - - name: 'Dart Analyze' + - name: "Bootstrap Workspace" + run: melos bootstrap + - name: "Dart Analyze" run: | - melos exec -c 3 --ignore="*example*" -- \ - tuneup check - - name: 'Pub Check' + melos run analyze + - name: "Pub Check" if: github.ref == 'refs/heads/master' run: | - melos exec -c 1 --no-private --ignore="*example*" -- \ - pub publish --dry-run + melos run lint:pub + format: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v2 + - name: "Git Checkout" + uses: actions/checkout@v2 with: fetch-depth: 0 - - name: 'Install Flutter' - run: ./.github/workflows/scripts/install-flutter.sh stable - - name: 'Install Tools' + - name: "Install Flutter" + uses: subosito/flutter-action@v1 + with: + channel: 'stable' + - name: "Install Tools" + run: flutter pub global activate melos + - name: "Bootstrap Workspace" + run: melos bootstrap + - name: "Melos Format" + run: melos run format + - name: "Validate Formatting" run: | - ./.github/workflows/scripts/install-tools.sh - - name: 'Bootstrap Workspace' - run: melos bootstrap --verbose - - name: 'Dart' - run: | - melos exec -c 1 -- \ - flutter format . ./.github/workflows/scripts/validate-formatting.sh test: runs-on: macos-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v2 + - name: "Git Checkout" + uses: actions/checkout@v2 with: fetch-depth: 0 - - name: 'Install Flutter' - run: ./.github/workflows/scripts/install-flutter.sh stable - - name: 'Install Tools' - 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 --verbose - - name: 'Dart Test' - run: | - cd packages/stream_chat - flutter pub run test --coverage coverage/ - 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: | - 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/very_good_coverage@v1.1.1 + - name: "Install Flutter" + uses: subosito/flutter-action@v1 + with: + channel: 'stable' + - name: "Install Tools" + run: flutter pub global activate melos + - name: "Bootstrap Workspace" + run: melos bootstrap + - name: "Flutter Test" + run: melos run test:all + - name: "Collect Coverage" + run: melos run coverage:ignore-file + - name: "Upload Coverage" + uses: codecov/codecov-action@v1 + with: + token: ${{secrets.CODECOV_TOKEN}} + files: packages/*/coverage/lcov.info + - name: "Stream Chat Coverage Check" + uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 with: path: packages/stream_chat/coverage/lcov.info min_coverage: 40 + - name: "Stream Chat Persistence Coverage Check" - uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 with: path: packages/stream_chat_persistence/coverage/lcov.info min_coverage: 95 - - uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 + - name: "Stream Chat Flutter Core Coverage Check" + uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 with: path: packages/stream_chat_flutter_core/coverage/lcov.info min_coverage: 90 - - uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 + - name: "Stream Chat Flutter Coverage Check" + uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 with: path: packages/stream_chat_flutter/coverage/lcov.info min_coverage: 35 + +# test: +# runs-on: macos-latest +# timeout-minutes: 15 +# steps: +# - uses: actions/checkout@v2 +# with: +# fetch-depth: 0 +# - name: 'Install Flutter' +# run: ./.github/workflows/scripts/install-flutter.sh stable +# - name: 'Install Tools' +# 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 --verbose +# - name: 'Dart Test' +# run: | +# cd packages/stream_chat +# flutter pub run test --coverage coverage/ +# 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: | +# 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 }} + diff --git a/packages/stream_chat/analysis_options.yaml b/analysis_options.yaml similarity index 100% rename from packages/stream_chat/analysis_options.yaml rename to analysis_options.yaml diff --git a/melos.yaml b/melos.yaml index 3fcad03f..17a896c0 100644 --- a/melos.yaml +++ b/melos.yaml @@ -1,4 +1,4 @@ -name: stream_chat_dart +name: stream_chat_flutter versioning: mode: independent @@ -7,56 +7,68 @@ packages: - packages/** scripts: + lint:all: + run: melos run analyze && melos run format + description: Run all static analysis checks - # - Requires `pub global activate tuneup`. - analyze: > - melos exec -c 1 --fail-fast -- \ - pub global run tuneup check + analyze: + run: | + melos exec -c 4 --ignore="*example*" -- \ + dart analyze --fatal-infos . + description: | + Run `dart analyze` in all packages. + - Note: you can also rely on your IDEs Dart Analysis / Issues window. - format: pub global run flutter_plugin_tools format + format: + run: flutter format --set-exit-if-changed . + description: | + Run `flutter format --set-exit-if-changed .` in all packages. + lint:pub: + run: | + melos exec -c 4 --no-private --ignore="*example*" -- \ + pub publish --dry-run + description: | + Run `pub publish --dry-run` in all packages. + - Note: you can also rely on your IDEs Dart Analysis / Issues window. - build:examples:ios: > - melos exec -c 1 --scope="*example*" --fail-fast -- \ - flutter build ios --no-codesign + generate:all: + run: melos run generate:dart && melos run generate:flutter + description: Build all generated files for Dart & Flutter packages in this project. + generate:dart: + run: melos exec -c 1 --depends-on="build_runner" --no-flutter -- "dart run build_runner build --delete-conflicting-outputs" + description: Build all generated files for Dart packages in this project. - build:examples:android: > - melos exec -c 1 --scope="*example*" --fail-fast -- \ - flutter build apk + generate:flutter: + run: melos exec -c 1 --depends-on="build_runner" --flutter -- "flutter pub run build_runner build --delete-conflicting-outputs" + description: Build all generated files for Flutter packages in this project. - # Build any plugin example apps that have MacOS support. - # - Requires `flutter config --enable-macos-desktop` enabled. - # - Requires `flutter channel master && flutter upgrade`. - build:examples:macos: > - melos exec -c 1 --scope="*example*" --dir-exists=macos --fail-fast -- \ - flutter build macos + test:all: + run: melos run test:dart --no-select && melos run test:flutter --no-select + description: Run all Dart & Flutter tests in this project. + test:dart: + run: melos exec -c 1 --fail-fast -- "flutter test --coverage" + description: Run Dart tests for a specific package in this project. + select-package: + flutter: false + dir-exists: test - test:dart: > - melos exec -c 1 --fail-fast --no-flutter --dir-exists=test --ignore="*example*" --ignore="*web*" -- \ - flutter pub run test + test:flutter: + run: melos exec -c 1 --fail-fast -- "flutter test --coverage" + description: Run Flutter tests for a specific package in this project. + select-package: + flutter: true + dir-exists: test - test:flutter: > - melos exec -c 1 --fail-fast --flutter --dir-exists=test --ignore="*example*" --ignore="*web*" -- \ - flutter test - - test:web: > - melos exec -c 1 --fail-fast --dir-exists=test --scope="*web*" -- \ - flutter test --platform=chrome - - - lint:pub: > - melos exec -c 5 --fail-fast --no-private --ignore="*example*" -- \ - pub publish --dry-run - - - postclean: > - melos exec -- \ - rm -rf ./build ./android/.gradle ./ios/.symlinks ./ios/Pods ./android/.idea ./.idea ./.dart-tool/build - -dev_dependencies: - pedantic: 1.9.2 + coverage:ignore-file: + run: | + melos exec -c 4 --fail-fast -- "\$MELOS_ROOT_PATH/.github/workflows/scripts/remove-from_coverage.sh" + description: Removes all the ignored files from the coverage report. + select-package: + dir-exists: coverage environment: - sdk: ">=2.12.0 <3.0.0" \ No newline at end of file + sdk: '>=2.12.0 <3.0.0' + flutter: '>=1.22.4 <2.0.0' \ No newline at end of file diff --git a/packages/stream_chat/peanut.yaml b/packages/stream_chat/peanut.yaml deleted file mode 100644 index 97d20f52..00000000 --- a/packages/stream_chat/peanut.yaml +++ /dev/null @@ -1,3 +0,0 @@ -# Configuration for https://pub.dev/packages/peanut -directories: - - example/web diff --git a/packages/stream_chat_flutter/analysis_options.yaml b/packages/stream_chat_flutter/analysis_options.yaml deleted file mode 100644 index f0a87ea5..00000000 --- a/packages/stream_chat_flutter/analysis_options.yaml +++ /dev/null @@ -1,150 +0,0 @@ -analyzer: - enable-experiment: - - extension-methods - exclude: - - lib/**/*.g.dart - - example/** - - lib/src/emoji - - lib/**/*.freezed.dart - - test/** - -linter: - rules: - # these rules are documented on and in the same order as - # the Dart Lint rules page to make maintenance easier - # https://github.com/dart-lang/linter/blob/master/example/all.yaml - - 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_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 - - - cast_nullable_to_non_nullable - - unnecessary_null_checks - - tighten_type_of_initializing_formals - - null_check_on_nullable_type_parameter diff --git a/packages/stream_chat_flutter_core/analysis_options.yaml b/packages/stream_chat_flutter_core/analysis_options.yaml deleted file mode 100644 index f0a87ea5..00000000 --- a/packages/stream_chat_flutter_core/analysis_options.yaml +++ /dev/null @@ -1,150 +0,0 @@ -analyzer: - enable-experiment: - - extension-methods - exclude: - - lib/**/*.g.dart - - example/** - - lib/src/emoji - - lib/**/*.freezed.dart - - test/** - -linter: - rules: - # these rules are documented on and in the same order as - # the Dart Lint rules page to make maintenance easier - # https://github.com/dart-lang/linter/blob/master/example/all.yaml - - 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_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 - - - cast_nullable_to_non_nullable - - unnecessary_null_checks - - tighten_type_of_initializing_formals - - null_check_on_nullable_type_parameter diff --git a/packages/stream_chat_persistence/analysis_options.yaml b/packages/stream_chat_persistence/analysis_options.yaml deleted file mode 100644 index f0a87ea5..00000000 --- a/packages/stream_chat_persistence/analysis_options.yaml +++ /dev/null @@ -1,150 +0,0 @@ -analyzer: - enable-experiment: - - extension-methods - exclude: - - lib/**/*.g.dart - - example/** - - lib/src/emoji - - lib/**/*.freezed.dart - - test/** - -linter: - rules: - # these rules are documented on and in the same order as - # the Dart Lint rules page to make maintenance easier - # https://github.com/dart-lang/linter/blob/master/example/all.yaml - - 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_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 - - - cast_nullable_to_non_nullable - - unnecessary_null_checks - - tighten_type_of_initializing_formals - - null_check_on_nullable_type_parameter From ff197e4200b354e63b7470128f9d0c14f0b10cb9 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 17 Jun 2021 21:36:20 +0530 Subject: [PATCH 067/150] remove commented commands Signed-off-by: Sahil Kumar --- .github/workflows/stream_flutter_workflow.yml | 36 +------------------ 1 file changed, 1 insertion(+), 35 deletions(-) diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml index a8650e65..1de175d8 100644 --- a/.github/workflows/stream_flutter_workflow.yml +++ b/.github/workflows/stream_flutter_workflow.yml @@ -9,8 +9,6 @@ on: branches: - master - develop - paths-ignore: - - 'docs/**' jobs: analyze: @@ -105,36 +103,4 @@ jobs: uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 with: path: packages/stream_chat_flutter/coverage/lcov.info - min_coverage: 35 - -# test: -# runs-on: macos-latest -# timeout-minutes: 15 -# steps: -# - uses: actions/checkout@v2 -# with: -# fetch-depth: 0 -# - name: 'Install Flutter' -# run: ./.github/workflows/scripts/install-flutter.sh stable -# - name: 'Install Tools' -# 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 --verbose -# - name: 'Dart Test' -# run: | -# cd packages/stream_chat -# flutter pub run test --coverage coverage/ -# 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: | -# 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 }} - + min_coverage: 35 \ No newline at end of file From 6b302dd494321786337bd77f9afe2da641ff8114 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 17 Jun 2021 18:16:59 +0200 Subject: [PATCH 068/150] Update stream_flutter_workflow.yml --- .github/workflows/stream_flutter_workflow.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml index 1de175d8..daa29977 100644 --- a/.github/workflows/stream_flutter_workflow.yml +++ b/.github/workflows/stream_flutter_workflow.yml @@ -90,7 +90,7 @@ jobs: path: packages/stream_chat/coverage/lcov.info min_coverage: 40 - name: "Stream Chat Persistence Coverage Check" - - uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 + uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 with: path: packages/stream_chat_persistence/coverage/lcov.info min_coverage: 95 @@ -103,4 +103,4 @@ jobs: uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 with: path: packages/stream_chat_flutter/coverage/lcov.info - min_coverage: 35 \ No newline at end of file + min_coverage: 35 From 0b1ef44c0e5f1d5d20a2369c00597c930c90ec51 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 17 Jun 2021 18:35:35 +0200 Subject: [PATCH 069/150] Update melos.yaml --- melos.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/melos.yaml b/melos.yaml index 17a896c0..f50c136a 100644 --- a/melos.yaml +++ b/melos.yaml @@ -66,9 +66,7 @@ scripts: run: | melos exec -c 4 --fail-fast -- "\$MELOS_ROOT_PATH/.github/workflows/scripts/remove-from_coverage.sh" description: Removes all the ignored files from the coverage report. - select-package: - dir-exists: coverage environment: sdk: '>=2.12.0 <3.0.0' - flutter: '>=1.22.4 <2.0.0' \ No newline at end of file + flutter: '>=1.22.4 <2.0.0' From 312364c3c44c82f1d80b4030d03265caf95b4f15 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 17 Jun 2021 18:43:43 +0200 Subject: [PATCH 070/150] fix permission --- .github/workflows/scripts/remove-from_coverage.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 .github/workflows/scripts/remove-from_coverage.sh diff --git a/.github/workflows/scripts/remove-from_coverage.sh b/.github/workflows/scripts/remove-from_coverage.sh old mode 100644 new mode 100755 From 6f1764c85a7c91b3a9ac3ea1bcb41ba934033b12 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 17 Jun 2021 22:24:15 +0530 Subject: [PATCH 071/150] fix stream_flutter_workflow.yml Signed-off-by: Sahil Kumar --- .github/workflows/stream_flutter_workflow.yml | 1 - analysis_options.yaml | 10 +++++----- melos.yaml | 2 +- .../lib/src/message_actions_modal.dart | 1 - 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml index daa29977..6e87981f 100644 --- a/.github/workflows/stream_flutter_workflow.yml +++ b/.github/workflows/stream_flutter_workflow.yml @@ -26,7 +26,6 @@ jobs: - name: "Install Tools" run: | flutter pub global activate melos - flutter pub global activate tuneup - name: "Bootstrap Workspace" run: melos bootstrap - name: "Dart Analyze" diff --git a/analysis_options.yaml b/analysis_options.yaml index ed0ce983..bbf5cf50 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -2,11 +2,11 @@ analyzer: enable-experiment: - extension-methods exclude: - - lib/**/*.g.dart - - example/** - - lib/src/emoji - - lib/**/*.freezed.dart -# - test/** + - packages/*/lib/**/*.g.dart + - packages/*/example/** + - packages/*/lib/src/emoji + - packages/*/lib/**/*.freezed.dart + - packages/*/test/** linter: rules: diff --git a/melos.yaml b/melos.yaml index 17a896c0..d9bc6945 100644 --- a/melos.yaml +++ b/melos.yaml @@ -64,7 +64,7 @@ scripts: coverage:ignore-file: run: | - melos exec -c 4 --fail-fast -- "\$MELOS_ROOT_PATH/.github/workflows/scripts/remove-from_coverage.sh" + melos exec -c 4 --fail-fast -- "\$MELOS_ROOT_PATH/.github/workflows/scripts/remove-from_coverage.sh" --no-select description: Removes all the ignored files from the coverage report. select-package: dir-exists: coverage diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart index 5b4102a1..11818887 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'dart:ui'; import 'package:flutter/foundation.dart'; From 20b41ef3fe716a721e7bce52b28be1717048f6b2 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 17 Jun 2021 22:53:39 +0530 Subject: [PATCH 072/150] change remove-from_coverage.sh name to remove-from-coverage.sh Signed-off-by: Sahil Kumar --- .../{remove-from_coverage.sh => remove-from-coverage.sh} | 0 analysis_options.yaml | 2 -- melos.yaml | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) rename .github/workflows/scripts/{remove-from_coverage.sh => remove-from-coverage.sh} (100%) diff --git a/.github/workflows/scripts/remove-from_coverage.sh b/.github/workflows/scripts/remove-from-coverage.sh similarity index 100% rename from .github/workflows/scripts/remove-from_coverage.sh rename to .github/workflows/scripts/remove-from-coverage.sh diff --git a/analysis_options.yaml b/analysis_options.yaml index bbf5cf50..a5d8631e 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,6 +1,4 @@ analyzer: - enable-experiment: - - extension-methods exclude: - packages/*/lib/**/*.g.dart - packages/*/example/** diff --git a/melos.yaml b/melos.yaml index d9bc6945..0318ec81 100644 --- a/melos.yaml +++ b/melos.yaml @@ -64,7 +64,7 @@ scripts: coverage:ignore-file: run: | - melos exec -c 4 --fail-fast -- "\$MELOS_ROOT_PATH/.github/workflows/scripts/remove-from_coverage.sh" --no-select + melos exec -c 4 --fail-fast -- "\$MELOS_ROOT_PATH/.github/workflows/scripts/remove-from-coverage.sh" --no-select description: Removes all the ignored files from the coverage report. select-package: dir-exists: coverage From de4a6806702e8c42e33abbf23dc32a30568937c5 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 17 Jun 2021 19:36:44 +0200 Subject: [PATCH 073/150] fix permission --- .github/workflows/scripts/remove-from-coverage.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 .github/workflows/scripts/remove-from-coverage.sh diff --git a/.github/workflows/scripts/remove-from-coverage.sh b/.github/workflows/scripts/remove-from-coverage.sh old mode 100644 new mode 100755 From c3ae85a282d66f92e8c39c5831355621cb9f0ca4 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 18 Jun 2021 10:47:11 +0200 Subject: [PATCH 074/150] fix tests --- melos.yaml | 2 ++ .../test/src/mapper/event_mapper_test.dart | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/melos.yaml b/melos.yaml index 75a59320..9a39193b 100644 --- a/melos.yaml +++ b/melos.yaml @@ -66,6 +66,8 @@ scripts: run: | melos exec -c 4 --fail-fast -- "\$MELOS_ROOT_PATH/.github/workflows/scripts/remove-from-coverage.sh" --no-select description: Removes all the ignored files from the coverage report. + select-package: + dir-exists: coverage environment: sdk: '>=2.12.0 <3.0.0' diff --git a/packages/stream_chat_persistence/test/src/mapper/event_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/event_mapper_test.dart index ffa8ee62..3182162b 100644 --- a/packages/stream_chat_persistence/test/src/mapper/event_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/event_mapper_test.dart @@ -1,7 +1,7 @@ -import 'package:test/test.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/mapper/event_mapper.dart'; +import 'package:test/test.dart'; import '../utils/date_matcher.dart'; @@ -22,7 +22,7 @@ void main() { final event = entity.toEvent(); expect(event, isA()); expect(event.type, type); - expect(event.createdAt, isSameDateAs(now)); + expect(event.createdAt.toUtc(), isSameDateAs(now.toUtc())); expect(event.me!.id, ownUser.id); expect(event.totalUnreadCount, entity.totalUnreadCount); expect(event.unreadChannels, entity.unreadChannels); From 3eb336e93dd68b9dee0d5ce224f973613ab77ef4 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 18 Jun 2021 11:04:53 +0200 Subject: [PATCH 075/150] fix analysis --- packages/stream_chat/lib/src/client/channel.dart | 2 +- .../stream_chat_flutter/test/src/full_screen_media_test.dart | 4 ++-- .../stream_chat_flutter/test/src/typing_indicator_test.dart | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 91ea4949..e173c703 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -1888,7 +1888,7 @@ class ChannelClientState { void _clean() { final now = DateTime.now(); _typings.forEach((user, event) { - if (now.difference(event.createdAt!).inSeconds > 7) { + if (now.difference(event.createdAt).inSeconds > 7) { _channel.client.handleEvent( Event( type: EventType.typingStop, diff --git a/packages/stream_chat_flutter/test/src/full_screen_media_test.dart b/packages/stream_chat_flutter/test/src/full_screen_media_test.dart index d418c303..3e6f1743 100644 --- a/packages/stream_chat_flutter/test/src/full_screen_media_test.dart +++ b/packages/stream_chat_flutter/test/src/full_screen_media_test.dart @@ -55,12 +55,12 @@ void main() { ])); when(() => channelState.typingEvents).thenAnswer((i) => { User(id: 'other-user', extraData: {'name': 'demo'}): - const Event(type: EventType.typingStart), + Event(type: EventType.typingStart), }); when(() => channelState.typingEventsStream) .thenAnswer((i) => Stream.value({ User(id: 'other-user', extraData: {'name': 'demo'}): - const Event(type: EventType.typingStart), + Event(type: EventType.typingStart), })); await tester.pumpWidget(MaterialApp( diff --git a/packages/stream_chat_flutter/test/src/typing_indicator_test.dart b/packages/stream_chat_flutter/test/src/typing_indicator_test.dart index 75431f44..67c80b1e 100644 --- a/packages/stream_chat_flutter/test/src/typing_indicator_test.dart +++ b/packages/stream_chat_flutter/test/src/typing_indicator_test.dart @@ -55,12 +55,12 @@ void main() { when(() => channelState.typingEvents).thenAnswer((i) => { User(id: 'other-user', extraData: {'name': 'demo'}): - const Event(type: EventType.typingStart), + Event(type: EventType.typingStart), }); when(() => channelState.typingEventsStream) .thenAnswer((i) => Stream.value({ User(id: 'other-user', extraData: {'name': 'demo'}): - const Event(type: EventType.typingStart), + Event(type: EventType.typingStart), })); await tester.pumpWidget(MaterialApp( From 24931f7f309ebb4d46682f57d51b1bed7fb74d76 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 18 Jun 2021 11:21:53 +0200 Subject: [PATCH 076/150] fix coverage --- .github/workflows/stream_flutter_workflow.yml | 2 +- melos.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml index 6e87981f..90986ed8 100644 --- a/.github/workflows/stream_flutter_workflow.yml +++ b/.github/workflows/stream_flutter_workflow.yml @@ -77,7 +77,7 @@ jobs: - name: "Flutter Test" run: melos run test:all - name: "Collect Coverage" - run: melos run coverage:ignore-file + run: melos run coverage:ignore-file --no-select - name: "Upload Coverage" uses: codecov/codecov-action@v1 with: diff --git a/melos.yaml b/melos.yaml index 9a39193b..11f1d4d8 100644 --- a/melos.yaml +++ b/melos.yaml @@ -64,7 +64,7 @@ scripts: coverage:ignore-file: run: | - melos exec -c 4 --fail-fast -- "\$MELOS_ROOT_PATH/.github/workflows/scripts/remove-from-coverage.sh" --no-select + melos exec -c 4 --fail-fast -- "\$MELOS_ROOT_PATH/.github/workflows/scripts/remove-from-coverage.sh" description: Removes all the ignored files from the coverage report. select-package: dir-exists: coverage From 0fcea3f546caf5ee4e9e10fe1c215b03d5c11642 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 18 Jun 2021 15:27:38 +0530 Subject: [PATCH 077/150] handle offline user connect case Signed-off-by: Sahil Kumar --- packages/stream_chat/lib/src/client/client.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index b9e1c5d7..e861ca19 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -289,6 +289,10 @@ class StreamChatClient { final event = await openConnection(); return event; } catch (e, stk) { + if (e is StreamChatNetworkError && e.isRetriable) { + final event = await _chatPersistenceClient?.getConnectionInfo(); + if (event != null) return event; + } logger.severe('error connecting user : ${ownUser.id}', e, stk); rethrow; } From 4bfda7553d1ea48fa1c3faddc05f7a5c11f62b4c Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 18 Jun 2021 15:33:05 +0530 Subject: [PATCH 078/150] fix stream_flutter_workflow.yml Signed-off-by: Sahil Kumar --- .github/workflows/stream_flutter_workflow.yml | 5 ++--- analysis_options.yaml | 14 ++++++-------- melos.yaml | 6 ++++-- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml index daa29977..1022e0ee 100644 --- a/.github/workflows/stream_flutter_workflow.yml +++ b/.github/workflows/stream_flutter_workflow.yml @@ -26,7 +26,6 @@ jobs: - name: "Install Tools" run: | flutter pub global activate melos - flutter pub global activate tuneup - name: "Bootstrap Workspace" run: melos bootstrap - name: "Dart Analyze" @@ -78,7 +77,7 @@ jobs: - name: "Flutter Test" run: melos run test:all - name: "Collect Coverage" - run: melos run coverage:ignore-file + run: melos run coverage:ignore-file --no-select - name: "Upload Coverage" uses: codecov/codecov-action@v1 with: @@ -103,4 +102,4 @@ jobs: uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 with: path: packages/stream_chat_flutter/coverage/lcov.info - min_coverage: 35 + min_coverage: 35 \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml index f0a87ea5..4b73086c 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,12 +1,10 @@ analyzer: - enable-experiment: - - extension-methods exclude: - - lib/**/*.g.dart - - example/** - - lib/src/emoji - - lib/**/*.freezed.dart - - test/** + - packages/*/lib/**/*.g.dart + - packages/*/example/** + - packages/*/lib/src/emoji + - packages/*/lib/**/*.freezed.dart + - packages/*/test/** linter: rules: @@ -147,4 +145,4 @@ linter: - cast_nullable_to_non_nullable - unnecessary_null_checks - tighten_type_of_initializing_formals - - null_check_on_nullable_type_parameter + - null_check_on_nullable_type_parameter \ No newline at end of file diff --git a/melos.yaml b/melos.yaml index f50c136a..1fb51c38 100644 --- a/melos.yaml +++ b/melos.yaml @@ -64,9 +64,11 @@ scripts: coverage:ignore-file: run: | - melos exec -c 4 --fail-fast -- "\$MELOS_ROOT_PATH/.github/workflows/scripts/remove-from_coverage.sh" + melos exec -c 4 --fail-fast -- "\$MELOS_ROOT_PATH/.github/workflows/scripts/remove-from-coverage.sh" description: Removes all the ignored files from the coverage report. + select-package: + dir-exists: coverage environment: sdk: '>=2.12.0 <3.0.0' - flutter: '>=1.22.4 <2.0.0' + flutter: '>=1.22.4 <2.0.0' \ No newline at end of file From 69b0b86bc151c3497fe09ff359f6686feff72b46 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 18 Jun 2021 15:35:03 +0530 Subject: [PATCH 079/150] change remove-from_coverage.sh to remove-from-coverage.sh Signed-off-by: Sahil Kumar --- .../scripts/{remove-from_coverage.sh => remove-from-coverage.sh} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/scripts/{remove-from_coverage.sh => remove-from-coverage.sh} (100%) mode change 100755 => 100644 diff --git a/.github/workflows/scripts/remove-from_coverage.sh b/.github/workflows/scripts/remove-from-coverage.sh old mode 100755 new mode 100644 similarity index 100% rename from .github/workflows/scripts/remove-from_coverage.sh rename to .github/workflows/scripts/remove-from-coverage.sh From c6a4a99503e21067cb7a7524d7dc6fc0334a0e03 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 17 Jun 2021 19:36:44 +0200 Subject: [PATCH 080/150] fix permission --- .github/workflows/scripts/remove-from-coverage.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 .github/workflows/scripts/remove-from-coverage.sh diff --git a/.github/workflows/scripts/remove-from-coverage.sh b/.github/workflows/scripts/remove-from-coverage.sh old mode 100644 new mode 100755 From 675bffe7b9b99077346add5db677987c80dfe266 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 18 Jun 2021 16:10:34 +0530 Subject: [PATCH 081/150] fix offline user connection, add tests Signed-off-by: Sahil Kumar --- .../stream_chat/lib/src/client/client.dart | 2 +- .../lib/src/core/error/stream_chat_error.dart | 3 + .../stream_chat/test/src/api/client_test.dart | 125 ++++++++++++++++++ 3 files changed, 129 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index e861ca19..3cc3ad0d 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -289,7 +289,7 @@ class StreamChatClient { final event = await openConnection(); return event; } catch (e, stk) { - if (e is StreamChatNetworkError && e.isRetriable) { + if (e is StreamWebSocketError && e.isRetriable) { final event = await _chatPersistenceClient?.getConnectionInfo(); if (event != null) return event; } diff --git a/packages/stream_chat/lib/src/core/error/stream_chat_error.dart b/packages/stream_chat/lib/src/core/error/stream_chat_error.dart index a78e9390..46302b4d 100644 --- a/packages/stream_chat/lib/src/core/error/stream_chat_error.dart +++ b/packages/stream_chat/lib/src/core/error/stream_chat_error.dart @@ -45,6 +45,9 @@ class StreamWebSocketError extends StreamChatError { /// Response body. please refer to [ErrorResponse]. final ErrorResponse? data; + /// + bool get isRetriable => data == null; + @override List get props => [...super.props, code]; diff --git a/packages/stream_chat/test/src/api/client_test.dart b/packages/stream_chat/test/src/api/client_test.dart index b0b30c83..13783865 100644 --- a/packages/stream_chat/test/src/api/client_test.dart +++ b/packages/stream_chat/test/src/api/client_test.dart @@ -330,6 +330,131 @@ void main() { ); }); + group('Fake web-socket connection function with failure and persistence', () { + const apiKey = 'test-api-key'; + late final api = FakeChatApi(); + late final persistence = MockPersistenceClient(); + + late StreamChatClient client; + + setUpAll(() { + // fallback values + registerFallbackValue(FakeUser()); + }); + + setUp(() { + final ws = FakeWebSocketWithConnectionError(); + client = StreamChatClient(apiKey, chatApi: api, ws: ws) + ..chatPersistenceClient = persistence; + }); + + tearDown(() { + client.dispose(); + }); + + test( + '`.connectUser` should connect successfully if persistence contains event', + () async { + final user = User(id: 'test-user-id'); + final token = Token.development(user.id).rawValue; + + final event = Event( + type: EventType.healthCheck, + connectionId: 'test-connection-id', + me: OwnUser.fromUser(user)); + when(persistence.getConnectionInfo).thenAnswer((_) async => event); + + final res = await client.connectUser(user, token); + expect(res, isNotNull); + expect(res.connectionId, 'test-connection-id'); + expect(res.me?.id, user.id); + + verify(persistence.getConnectionInfo).called(1); + verifyNoMoreInteractions(persistence); + }, + ); + + test( + '`.connectUserWithProvider` should connect successfully if persistence contains event', + () async { + final user = User(id: 'test-user-id'); + Future tokenProvider(String userId) async { + expect(userId, user.id); + return Token.development(userId).rawValue; + } + + final event = Event( + type: EventType.healthCheck, + connectionId: 'test-connection-id', + me: OwnUser.fromUser(user)); + when(persistence.getConnectionInfo).thenAnswer((_) async => event); + + final res = await client.connectUserWithProvider(user, tokenProvider); + expect(res, isNotNull); + expect(res.connectionId, 'test-connection-id'); + expect(res.me?.id, user.id); + + verify(persistence.getConnectionInfo).called(1); + verifyNoMoreInteractions(persistence); + }, + ); + + test( + '`.connectGuestUser` should connect successfully if persistence contains event', + () async { + final user = User(id: 'test-user-id'); + final token = Token.development(user.id).rawValue; + + final event = Event( + type: EventType.healthCheck, + connectionId: 'test-connection-id', + me: OwnUser.fromUser(user)); + when(persistence.getConnectionInfo).thenAnswer((_) async => event); + + when(() => api.guest.getGuestUser(any(that: isSameUserAs(user)))) + .thenAnswer( + (_) async => ConnectGuestUserResponse() + ..user = user + ..accessToken = token, + ); + + final res = await client.connectGuestUser(user); + expect(res, isNotNull); + expect(res.connectionId, 'test-connection-id'); + expect(res.me?.id, user.id); + + verify(persistence.getConnectionInfo).called(1); + verifyNoMoreInteractions(persistence); + verify(() => api.guest.getGuestUser(any(that: isSameUserAs(user)))) + .called(1); + verifyNoMoreInteractions(api.guest); + }, + ); + + test( + '`.connectAnonymousUser` should connect successfully if persistence contains event', + () async { + final user = User(id: 'test-user-id'); + + when(persistence.getConnectionInfo).thenAnswer( + (invocation) async => Event( + type: EventType.healthCheck, + connectionId: 'test-connection-id', + me: OwnUser.fromUser(user), + ), + ); + + final res = await client.connectAnonymousUser(); + expect(res, isNotNull); + expect(res.connectionId, 'test-connection-id'); + expect(res.me?.id, user.id); + + verify(persistence.getConnectionInfo).called(1); + verifyNoMoreInteractions(persistence); + }, + ); + }); + group('Client with connected user with persistence', () { const apiKey = 'test-api-key'; late final api = FakeChatApi(); From 940a51691312fb7b5c850ef227a5e03683814590 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 18 Jun 2021 13:04:28 +0200 Subject: [PATCH 082/150] fix action --- .github/workflows/scripts/remove-from-coverage.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scripts/remove-from-coverage.sh b/.github/workflows/scripts/remove-from-coverage.sh index c1190097..9ef430ac 100755 --- a/.github/workflows/scripts/remove-from-coverage.sh +++ b/.github/workflows/scripts/remove-from-coverage.sh @@ -3,5 +3,5 @@ # Fast fail the script on failures. set -e -pub global activate remove_from_coverage +pub global activate remove_from_coverage --overwrite pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r '\.g\.dart$' -r '\.freezed\.dart$' \ No newline at end of file From 6b26ce691564b56ef165e03271a1577d9de28e3c Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 21 Jun 2021 09:54:13 +0200 Subject: [PATCH 083/150] fixes #483: Keyboard covers input text box when editing message --- .../lib/src/message_actions_modal.dart | 83 ++++++++++--------- 1 file changed, 43 insertions(+), 40 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart index f763585b..41f15c81 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -625,48 +625,51 @@ class _MessageActionsModalState extends State { topRight: Radius.circular(16), ), ), - builder: (context) => StreamChannel( - channel: channel, - child: Flex( - direction: Axis.vertical, - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.all(8), - child: StreamSvgIcon.edit( - color: streamChatThemeData.colorTheme.greyGainsboro, + builder: (context) => Padding( + padding: MediaQuery.of(context).viewInsets, + child: StreamChannel( + channel: channel, + child: Flex( + direction: Axis.vertical, + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: StreamSvgIcon.edit( + color: streamChatThemeData.colorTheme.greyGainsboro, + ), ), - ), - const Text( - 'Edit Message', - style: TextStyle(fontWeight: FontWeight.bold), - ), - IconButton( - visualDensity: VisualDensity.compact, - icon: StreamSvgIcon.closeSmall(), - onPressed: Navigator.of(context).pop, - ), - ], + const Text( + 'Edit Message', + style: TextStyle(fontWeight: FontWeight.bold), + ), + IconButton( + visualDensity: VisualDensity.compact, + icon: StreamSvgIcon.closeSmall(), + onPressed: Navigator.of(context).pop, + ), + ], + ), ), - ), - if (widget.editMessageInputBuilder != null) - widget.editMessageInputBuilder!(context, widget.message) - else - MessageInput( - editMessage: widget.message, - preMessageSending: (m) { - FocusScope.of(context).unfocus(); - Navigator.pop(context); - return m; - }, - ), - ], + if (widget.editMessageInputBuilder != null) + widget.editMessageInputBuilder!(context, widget.message) + else + MessageInput( + editMessage: widget.message, + preMessageSending: (m) { + FocusScope.of(context).unfocus(); + Navigator.pop(context); + return m; + }, + ), + ], + ), ), ), ); From 7c16f3bb9b43297852ea4af42700c8fb4580a753 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 21 Jun 2021 09:54:27 +0200 Subject: [PATCH 084/150] fix animation --- .../stream_chat_flutter/lib/src/message_input.dart | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 4f61c9fa..962d237b 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -251,7 +251,6 @@ class MessageInputState extends State { late final FocusNode _focusNode; bool _inputEnabled = true; bool _messageIsPresent = false; - bool _animateContainer = true; bool _commandEnabled = false; OverlayEntry? _commandsOverlay, _mentionsOverlay, _emojiOverlay; late Iterable _emojiNames; @@ -939,10 +938,6 @@ class MessageInputState extends State { } Widget _buildFilePickerSection() { - if (!_openFilePickerSection) { - return const Offstage(); - } - final _attachmentContainsFile = _attachments.values.any((it) => it.type == 'file'); @@ -975,8 +970,7 @@ class MessageInputState extends State { } return AnimatedContainer( - duration: - _animateContainer ? const Duration(milliseconds: 300) : Duration.zero, + duration: const Duration(milliseconds: 300), height: _openFilePickerSection ? _filePickerSize : 0, child: Material( color: _streamChatTheme.colorTheme.whiteSmoke, @@ -1034,7 +1028,6 @@ class MessageInputState extends State { GestureDetector( onVerticalDragUpdate: (update) { setState(() { - _animateContainer = false; _filePickerSize = (_filePickerSize - update.delta.dy).clamp( _kMinMediaPickerSize, MediaQuery.of(context).size.height / 1.7, @@ -1672,7 +1665,6 @@ class MessageInputState extends State { onPressed: () async { if (_openFilePickerSection) { setState(() { - _animateContainer = false; _openFilePickerSection = false; _filePickerSize = _kMinMediaPickerSize; }); @@ -1718,7 +1710,6 @@ class MessageInputState extends State { if (_openFilePickerSection) { setState(() { - _animateContainer = true; _openFilePickerSection = false; _filePickerSize = _kMinMediaPickerSize; }); From 366e77ceb76cfd7dda066874900214969fa6f3f6 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 16 Jun 2021 10:48:19 +0200 Subject: [PATCH 085/150] fix reconnection logic --- .../stream_chat_flutter_core/lib/src/stream_chat_core.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart b/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart index aa23a959..e80472cd 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart @@ -125,7 +125,8 @@ class StreamChatCoreState extends State _isConnectionAvailable = result != ConnectivityResult.none; if (!_isInForeground) return; if (_isConnectionAvailable) { - if (client.wsConnectionStatus == ConnectionStatus.disconnected) { + if (client.wsConnectionStatus == ConnectionStatus.disconnected && + user != null) { client.openConnection(); } } else { From ae3a7e99040090c93063936b3b28d9a0056984b5 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 21 Jun 2021 16:05:33 +0530 Subject: [PATCH 086/150] wrap WebSocketChannelError errors into StreamWebSocketError. Signed-off-by: Sahil Kumar --- .../lib/src/core/error/stream_chat_error.dart | 11 +++++++++++ packages/stream_chat/lib/src/ws/websocket.dart | 14 ++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/packages/stream_chat/lib/src/core/error/stream_chat_error.dart b/packages/stream_chat/lib/src/core/error/stream_chat_error.dart index 46302b4d..f517bea4 100644 --- a/packages/stream_chat/lib/src/core/error/stream_chat_error.dart +++ b/packages/stream_chat/lib/src/core/error/stream_chat_error.dart @@ -1,6 +1,10 @@ +import 'dart:io'; + import 'package:equatable/equatable.dart'; import 'package:stream_chat/src/core/error/chat_error_code.dart'; +import 'package:stream_chat/src/ws/websocket.dart'; import 'package:stream_chat/stream_chat.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; /// class StreamChatError with EquatableMixin implements Exception { @@ -32,6 +36,13 @@ class StreamWebSocketError extends StreamChatError { return StreamWebSocketError(message, data: data); } + /// + factory StreamWebSocketError.fromWebSocketChannelError( + WebSocketChannelException error) { + final message = error.message ?? ''; + return StreamWebSocketError(message); + } + /// int? get code => data?.code; diff --git a/packages/stream_chat/lib/src/ws/websocket.dart b/packages/stream_chat/lib/src/ws/websocket.dart index 4c7c91ed..f9d8a210 100644 --- a/packages/stream_chat/lib/src/ws/websocket.dart +++ b/packages/stream_chat/lib/src/ws/websocket.dart @@ -325,6 +325,20 @@ class WebSocket with TimerHelper { void _onConnectionError(error, [stacktrace]) { _logger?.warning('Error occurred', error, stacktrace); + StreamWebSocketError wsError; + if (error is WebSocketChannelException) { + wsError = StreamWebSocketError.fromWebSocketChannelError(error); + } else { + wsError = StreamWebSocketError(error.toString()); + } + + final completer = connectionCompleter; + // complete with error if not yet completed + if (completer != null && !completer.isCompleted) { + // complete the connection with error + completer.completeError(wsError); + } + // resetting connect, reconnect request flag _resetRequestFlags(); From 7059e2fe21e6385e3272fef50e5e3f24f99f2be4 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 21 Jun 2021 17:09:51 +0530 Subject: [PATCH 087/150] remove unused imports Signed-off-by: Sahil Kumar --- packages/stream_chat/lib/src/core/error/stream_chat_error.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/stream_chat/lib/src/core/error/stream_chat_error.dart b/packages/stream_chat/lib/src/core/error/stream_chat_error.dart index f517bea4..13ccef2a 100644 --- a/packages/stream_chat/lib/src/core/error/stream_chat_error.dart +++ b/packages/stream_chat/lib/src/core/error/stream_chat_error.dart @@ -1,8 +1,5 @@ -import 'dart:io'; - import 'package:equatable/equatable.dart'; import 'package:stream_chat/src/core/error/chat_error_code.dart'; -import 'package:stream_chat/src/ws/websocket.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; From c5fc82415411012aa9757697770d1fdaf3d3c3f6 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 21 Jun 2021 09:54:13 +0200 Subject: [PATCH 088/150] fixes #483: Keyboard covers input text box when editing message --- .../lib/src/message_actions_modal.dart | 83 ++++++++++--------- 1 file changed, 43 insertions(+), 40 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart index 11818887..6f57e7df 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -625,48 +625,51 @@ class _MessageActionsModalState extends State { topRight: Radius.circular(16), ), ), - builder: (context) => StreamChannel( - channel: channel, - child: Flex( - direction: Axis.vertical, - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.all(8), - child: StreamSvgIcon.edit( - color: streamChatThemeData.colorTheme.greyGainsboro, + builder: (context) => Padding( + padding: MediaQuery.of(context).viewInsets, + child: StreamChannel( + channel: channel, + child: Flex( + direction: Axis.vertical, + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: StreamSvgIcon.edit( + color: streamChatThemeData.colorTheme.greyGainsboro, + ), ), - ), - const Text( - 'Edit Message', - style: TextStyle(fontWeight: FontWeight.bold), - ), - IconButton( - visualDensity: VisualDensity.compact, - icon: StreamSvgIcon.closeSmall(), - onPressed: Navigator.of(context).pop, - ), - ], + const Text( + 'Edit Message', + style: TextStyle(fontWeight: FontWeight.bold), + ), + IconButton( + visualDensity: VisualDensity.compact, + icon: StreamSvgIcon.closeSmall(), + onPressed: Navigator.of(context).pop, + ), + ], + ), ), - ), - if (widget.editMessageInputBuilder != null) - widget.editMessageInputBuilder!(context, widget.message) - else - MessageInput( - editMessage: widget.message, - preMessageSending: (m) { - FocusScope.of(context).unfocus(); - Navigator.pop(context); - return m; - }, - ), - ], + if (widget.editMessageInputBuilder != null) + widget.editMessageInputBuilder!(context, widget.message) + else + MessageInput( + editMessage: widget.message, + preMessageSending: (m) { + FocusScope.of(context).unfocus(); + Navigator.pop(context); + return m; + }, + ), + ], + ), ), ), ); From 1ffb878b15aea169f536b264e562093c3ef20106 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 21 Jun 2021 09:54:27 +0200 Subject: [PATCH 089/150] fix animation --- .../stream_chat_flutter/lib/src/message_input.dart | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 4f61c9fa..962d237b 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -251,7 +251,6 @@ class MessageInputState extends State { late final FocusNode _focusNode; bool _inputEnabled = true; bool _messageIsPresent = false; - bool _animateContainer = true; bool _commandEnabled = false; OverlayEntry? _commandsOverlay, _mentionsOverlay, _emojiOverlay; late Iterable _emojiNames; @@ -939,10 +938,6 @@ class MessageInputState extends State { } Widget _buildFilePickerSection() { - if (!_openFilePickerSection) { - return const Offstage(); - } - final _attachmentContainsFile = _attachments.values.any((it) => it.type == 'file'); @@ -975,8 +970,7 @@ class MessageInputState extends State { } return AnimatedContainer( - duration: - _animateContainer ? const Duration(milliseconds: 300) : Duration.zero, + duration: const Duration(milliseconds: 300), height: _openFilePickerSection ? _filePickerSize : 0, child: Material( color: _streamChatTheme.colorTheme.whiteSmoke, @@ -1034,7 +1028,6 @@ class MessageInputState extends State { GestureDetector( onVerticalDragUpdate: (update) { setState(() { - _animateContainer = false; _filePickerSize = (_filePickerSize - update.delta.dy).clamp( _kMinMediaPickerSize, MediaQuery.of(context).size.height / 1.7, @@ -1672,7 +1665,6 @@ class MessageInputState extends State { onPressed: () async { if (_openFilePickerSection) { setState(() { - _animateContainer = false; _openFilePickerSection = false; _filePickerSize = _kMinMediaPickerSize; }); @@ -1718,7 +1710,6 @@ class MessageInputState extends State { if (_openFilePickerSection) { setState(() { - _animateContainer = true; _openFilePickerSection = false; _filePickerSize = _kMinMediaPickerSize; }); From c4ee8d34b06d8bb6ad08c0ad0652879c1ffe2525 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 21 Jun 2021 15:29:45 +0200 Subject: [PATCH 090/150] fix gh action --- .github/workflows/scripts/remove-from-coverage.sh | 3 +-- .github/workflows/stream_flutter_workflow.yml | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/scripts/remove-from-coverage.sh b/.github/workflows/scripts/remove-from-coverage.sh index 9ef430ac..c6850e89 100755 --- a/.github/workflows/scripts/remove-from-coverage.sh +++ b/.github/workflows/scripts/remove-from-coverage.sh @@ -3,5 +3,4 @@ # Fast fail the script on failures. set -e -pub global activate remove_from_coverage --overwrite -pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r '\.g\.dart$' -r '\.freezed\.dart$' \ No newline at end of file +pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r '\.g\.dart$' -r '\.freezed\.dart$' diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml index 1022e0ee..3a6c1650 100644 --- a/.github/workflows/stream_flutter_workflow.yml +++ b/.github/workflows/stream_flutter_workflow.yml @@ -71,7 +71,9 @@ jobs: with: channel: 'stable' - name: "Install Tools" - run: flutter pub global activate melos + run: | + flutter pub global activate melos + pub global activate remove_from_coverage - name: "Bootstrap Workspace" run: melos bootstrap - name: "Flutter Test" From c7d77c8cd93798f20b0600e0c1adfa72c2918f43 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 21 Jun 2021 15:29:45 +0200 Subject: [PATCH 091/150] fix gh action --- .github/workflows/scripts/remove-from-coverage.sh | 3 +-- .github/workflows/stream_flutter_workflow.yml | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/scripts/remove-from-coverage.sh b/.github/workflows/scripts/remove-from-coverage.sh index c1190097..c6850e89 100755 --- a/.github/workflows/scripts/remove-from-coverage.sh +++ b/.github/workflows/scripts/remove-from-coverage.sh @@ -3,5 +3,4 @@ # Fast fail the script on failures. set -e -pub global activate remove_from_coverage -pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r '\.g\.dart$' -r '\.freezed\.dart$' \ No newline at end of file +pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r '\.g\.dart$' -r '\.freezed\.dart$' diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml index 1022e0ee..3a6c1650 100644 --- a/.github/workflows/stream_flutter_workflow.yml +++ b/.github/workflows/stream_flutter_workflow.yml @@ -71,7 +71,9 @@ jobs: with: channel: 'stable' - name: "Install Tools" - run: flutter pub global activate melos + run: | + flutter pub global activate melos + pub global activate remove_from_coverage - name: "Bootstrap Workspace" run: melos bootstrap - name: "Flutter Test" From 00e184f4225f3a09483802cd08944c0700bcbde4 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 22 Jun 2021 12:54:22 +0200 Subject: [PATCH 092/150] show modal using the nearest navigator --- packages/stream_chat_flutter/lib/src/message_widget.dart | 2 ++ packages/stream_chat_flutter/lib/src/utils.dart | 2 ++ packages/stream_chat_flutter/pubspec.yaml | 6 ------ packages/stream_chat_flutter_core/pubspec.yaml | 4 ---- packages/stream_chat_persistence/pubspec.yaml | 4 ---- 5 files changed, 4 insertions(+), 14 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index 7fb092d6..753f201c 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -908,6 +908,7 @@ class _MessageWidgetState extends State final channel = StreamChannel.of(context).channel; showDialog( + useRootNavigator: false, context: context, barrierColor: _streamChatTheme.colorTheme.overlay, builder: (context) => StreamChannel( @@ -959,6 +960,7 @@ class _MessageWidgetState extends State void _showMessageReactionsModalBottomSheet(BuildContext context) { final channel = StreamChannel.of(context).channel; showDialog( + useRootNavigator: false, context: context, barrierColor: _streamChatTheme.colorTheme.overlay, builder: (context) => StreamChannel( diff --git a/packages/stream_chat_flutter/lib/src/utils.dart b/packages/stream_chat_flutter/lib/src/utils.dart index e98112fc..d923b752 100644 --- a/packages/stream_chat_flutter/lib/src/utils.dart +++ b/packages/stream_chat_flutter/lib/src/utils.dart @@ -30,6 +30,7 @@ Future showConfirmationDialog( }) { final chatThemeData = StreamChatTheme.of(context); return showModalBottomSheet( + useRootNavigator: false, backgroundColor: chatThemeData.colorTheme.white, context: context, shape: const RoundedRectangleBorder( @@ -114,6 +115,7 @@ Future showInfoDialog( }) { final chatThemeData = StreamChatTheme.of(context); return showModalBottomSheet( + useRootNavigator: false, backgroundColor: theme?.colorTheme.white ?? chatThemeData.colorTheme.white, context: context, shape: const RoundedRectangleBorder( diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index 4ef84ebc..bcebde99 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -53,12 +53,6 @@ flutter: - animations/ uses-material-design: true -dependency_overrides: - stream_chat: - path: ../stream_chat - stream_chat_flutter_core: - path: ../stream_chat_flutter_core - dev_dependencies: flutter_test: sdk: flutter diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml index ff218936..14fa0f80 100644 --- a/packages/stream_chat_flutter_core/pubspec.yaml +++ b/packages/stream_chat_flutter_core/pubspec.yaml @@ -18,10 +18,6 @@ dependencies: rxdart: ^0.27.0 stream_chat: ^2.0.0-nullsafety.6 -dependency_overrides: - stream_chat: - path: ../stream_chat - dev_dependencies: fake_async: ^1.2.0 flutter_test: diff --git a/packages/stream_chat_persistence/pubspec.yaml b/packages/stream_chat_persistence/pubspec.yaml index ff1a8579..9d141198 100644 --- a/packages/stream_chat_persistence/pubspec.yaml +++ b/packages/stream_chat_persistence/pubspec.yaml @@ -20,10 +20,6 @@ dependencies: sqlite3_flutter_libs: ^0.5.0 stream_chat: ^2.0.0-nullsafety.5 -dependency_overrides: - stream_chat: - path: ../stream_chat - dev_dependencies: build_runner: ^2.0.1 mocktail: ^0.1.1 From c987e8b4887a6c22a5c66695a48d788960083b38 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 22 Jun 2021 14:20:43 +0200 Subject: [PATCH 093/150] add comments --- .../stream_chat/lib/src/client/client.dart | 26 +++++++++---------- .../lib/src/client/retry_policy.dart | 6 +++++ .../lib/src/core/api/channel_api.dart | 4 +-- .../lib/src/core/api/device_api.dart | 4 +-- .../lib/src/core/api/general_api.dart | 4 +-- .../lib/src/core/api/guest_api.dart | 6 ++--- .../lib/src/core/api/message_api.dart | 4 +-- .../lib/src/core/api/moderation_api.dart | 4 +-- .../lib/src/core/api/responses.dart | 10 +++---- .../lib/src/core/api/stream_chat_api.dart | 24 ++++++++--------- .../lib/src/core/api/user_api.dart | 4 +-- .../src/core/http/connection_id_manager.dart | 12 ++++----- .../http/interceptor/auth_interceptor.dart | 10 +++---- .../connection_id_interceptor.dart | 3 +-- .../http/interceptor/logging_interceptor.dart | 21 +++++---------- .../src/core/http/stream_chat_dio_error.dart | 4 +-- .../stream_chat/lib/src/core/http/token.dart | 19 +++++++------- .../lib/src/core/http/token_manager.dart | 15 ++++++----- 18 files changed, 89 insertions(+), 91 deletions(-) diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 3cc3ad0d..b579f030 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -6,33 +6,31 @@ import 'package:dio/dio.dart'; import 'package:logging/logging.dart'; import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/src/client/channel.dart'; -import 'package:stream_chat/src/core/util/utils.dart'; -import 'package:stream_chat/src/core/error/error.dart'; -import 'package:stream_chat/src/location.dart'; -import 'package:stream_chat/src/ws/connection_status.dart'; +import 'package:stream_chat/src/client/retry_policy.dart'; import 'package:stream_chat/src/core/api/attachment_file_uploader.dart'; import 'package:stream_chat/src/core/api/requests.dart'; import 'package:stream_chat/src/core/api/responses.dart'; -import 'package:stream_chat/src/client/retry_policy.dart'; -import 'package:stream_chat/src/ws/websocket.dart'; import 'package:stream_chat/src/core/api/stream_chat_api.dart'; +import 'package:stream_chat/src/core/error/error.dart'; import 'package:stream_chat/src/core/http/connection_id_manager.dart'; import 'package:stream_chat/src/core/http/stream_http_client.dart'; import 'package:stream_chat/src/core/http/token.dart'; import 'package:stream_chat/src/core/http/token_manager.dart'; -import 'package:stream_chat/src/db/chat_persistence_client.dart'; -import 'package:stream_chat/src/event_type.dart'; import 'package:stream_chat/src/core/models/attachment_file.dart'; import 'package:stream_chat/src/core/models/channel_model.dart'; import 'package:stream_chat/src/core/models/channel_state.dart'; import 'package:stream_chat/src/core/models/event.dart'; +import 'package:stream_chat/src/core/models/filter.dart'; +import 'package:stream_chat/src/core/models/member.dart'; import 'package:stream_chat/src/core/models/message.dart'; import 'package:stream_chat/src/core/models/own_user.dart'; import 'package:stream_chat/src/core/models/user.dart'; - -import 'package:stream_chat/src/core/models/filter.dart'; - -import 'package:stream_chat/src/core/models/member.dart'; +import 'package:stream_chat/src/core/util/utils.dart'; +import 'package:stream_chat/src/db/chat_persistence_client.dart'; +import 'package:stream_chat/src/event_type.dart'; +import 'package:stream_chat/src/location.dart'; +import 'package:stream_chat/src/ws/connection_status.dart'; +import 'package:stream_chat/src/ws/websocket.dart'; /// Handler function used for logging records. Function requires a single /// [LogRecord] as the only parameter. @@ -137,10 +135,10 @@ class StreamChatClient { late final RetryPolicy _retryPolicy; - // sync state of the channels present inside state, defaults to false + /// sync state of the channels present inside state, defaults to false bool _synced = false; - // the last dateTime at the which all the channels were synced + /// the last dateTime at the which all the channels were synced DateTime? _lastSyncedAt; /// The retry policy options getter diff --git a/packages/stream_chat/lib/src/client/retry_policy.dart b/packages/stream_chat/lib/src/client/retry_policy.dart index fbebc696..5b7812ab 100644 --- a/packages/stream_chat/lib/src/client/retry_policy.dart +++ b/packages/stream_chat/lib/src/client/retry_policy.dart @@ -2,6 +2,12 @@ import 'package:stream_chat/src/client/client.dart'; import 'package:stream_chat/src/core/error/error.dart'; /// The retry options +/// When sending/updating/deleting a message any temporary error will trigger the retry policy +/// The retry policy exposes 2 methods +/// - shouldRetry: returns a boolean if the request should be retried +/// - retryTimeout: How many milliseconds to wait till the next attempt +/// +/// maxRetryAttempts is a hard limit on maximum retry attempts before giving up class RetryPolicy { /// Instantiate a new RetryPolicy RetryPolicy({ diff --git a/packages/stream_chat/lib/src/core/api/channel_api.dart b/packages/stream_chat/lib/src/core/api/channel_api.dart index 9da05df0..7660c125 100644 --- a/packages/stream_chat/lib/src/core/api/channel_api.dart +++ b/packages/stream_chat/lib/src/core/api/channel_api.dart @@ -9,9 +9,9 @@ import 'package:stream_chat/src/core/models/event.dart'; import 'package:stream_chat/src/core/models/filter.dart'; import 'package:stream_chat/src/core/models/message.dart'; -/// +/// Defines the api dedicated to channel operations class ChannelApi { - /// + /// Initialize a new channel api ChannelApi(this._client); final StreamHttpClient _client; diff --git a/packages/stream_chat/lib/src/core/api/device_api.dart b/packages/stream_chat/lib/src/core/api/device_api.dart index 69f509f8..2d2b9d7b 100644 --- a/packages/stream_chat/lib/src/core/api/device_api.dart +++ b/packages/stream_chat/lib/src/core/api/device_api.dart @@ -19,9 +19,9 @@ extension PushProviderX on PushProvider { }[this]!; } -/// +/// Defines the api dedicated to device operations class DeviceApi { - /// + /// Initialize a new device api DeviceApi(this._client); final StreamHttpClient _client; diff --git a/packages/stream_chat/lib/src/core/api/general_api.dart b/packages/stream_chat/lib/src/core/api/general_api.dart index d26ffc7d..8eeeaa5f 100644 --- a/packages/stream_chat/lib/src/core/api/general_api.dart +++ b/packages/stream_chat/lib/src/core/api/general_api.dart @@ -6,9 +6,9 @@ import 'package:stream_chat/src/core/http/stream_http_client.dart'; import 'package:stream_chat/src/core/models/filter.dart'; import 'package:stream_chat/src/core/models/member.dart'; -/// +/// Defines the api dedicated to general operations class GeneralApi { - /// + /// Initialize a new general api GeneralApi(this._client); final StreamHttpClient _client; diff --git a/packages/stream_chat/lib/src/core/api/guest_api.dart b/packages/stream_chat/lib/src/core/api/guest_api.dart index 438a637f..b6727902 100644 --- a/packages/stream_chat/lib/src/core/api/guest_api.dart +++ b/packages/stream_chat/lib/src/core/api/guest_api.dart @@ -2,14 +2,14 @@ import 'package:stream_chat/src/core/api/responses.dart'; import 'package:stream_chat/src/core/http/stream_http_client.dart'; import 'package:stream_chat/src/core/models/user.dart'; -/// +/// Defines the api dedicated to guest users operations class GuestApi { - /// + /// Initialize a new guest api GuestApi(this._client); final StreamHttpClient _client; - /// + /// Returns the information about guest user Future getGuestUser(User user) async { final response = await _client.post( '/guest', diff --git a/packages/stream_chat/lib/src/core/api/message_api.dart b/packages/stream_chat/lib/src/core/api/message_api.dart index 22f7bfef..f50cb4a1 100644 --- a/packages/stream_chat/lib/src/core/api/message_api.dart +++ b/packages/stream_chat/lib/src/core/api/message_api.dart @@ -3,9 +3,9 @@ import 'package:stream_chat/src/core/api/responses.dart'; import 'package:stream_chat/src/core/http/stream_http_client.dart'; import 'package:stream_chat/src/core/models/message.dart'; -/// +/// Defines the api dedicated to messages operations class MessageApi { - /// + /// Initialize a new message api MessageApi(this._client); final StreamHttpClient _client; diff --git a/packages/stream_chat/lib/src/core/api/moderation_api.dart b/packages/stream_chat/lib/src/core/api/moderation_api.dart index 22c58c35..533fbf63 100644 --- a/packages/stream_chat/lib/src/core/api/moderation_api.dart +++ b/packages/stream_chat/lib/src/core/api/moderation_api.dart @@ -1,9 +1,9 @@ import 'package:stream_chat/src/core/api/responses.dart'; import 'package:stream_chat/src/core/http/stream_http_client.dart'; -/// +/// Defines the api dedicated to moderation operations class ModerationApi { - /// + /// Initialize a new moderation api ModerationApi(this._client); final StreamHttpClient _client; diff --git a/packages/stream_chat/lib/src/core/api/responses.dart b/packages/stream_chat/lib/src/core/api/responses.dart index ccf03822..e06a213a 100644 --- a/packages/stream_chat/lib/src/core/api/responses.dart +++ b/packages/stream_chat/lib/src/core/api/responses.dart @@ -1,5 +1,6 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/client/client.dart'; +import 'package:stream_chat/src/core/error/error.dart'; import 'package:stream_chat/src/core/models/channel_model.dart'; import 'package:stream_chat/src/core/models/channel_state.dart'; import 'package:stream_chat/src/core/models/device.dart'; @@ -9,7 +10,6 @@ import 'package:stream_chat/src/core/models/message.dart'; import 'package:stream_chat/src/core/models/reaction.dart'; import 'package:stream_chat/src/core/models/read.dart'; import 'package:stream_chat/src/core/models/user.dart'; -import 'package:stream_chat/src/core/error/error.dart'; part 'responses.g.dart'; @@ -20,17 +20,17 @@ class _BaseResponse { /// Model response for [StreamChatNetworkError] data @JsonSerializable() class ErrorResponse extends _BaseResponse { - /// + /// The http error code int? code; - /// + /// The message associated to the error code String? message; - /// + /// The backend error code @JsonKey(name: 'StatusCode') int? statusCode; - /// + /// A detailed message about the error String? moreInfo; /// Create a new instance from a json diff --git a/packages/stream_chat/lib/src/core/api/stream_chat_api.dart b/packages/stream_chat/lib/src/core/api/stream_chat_api.dart index 13342160..bcf041c1 100644 --- a/packages/stream_chat/lib/src/core/api/stream_chat_api.dart +++ b/packages/stream_chat/lib/src/core/api/stream_chat_api.dart @@ -1,21 +1,21 @@ import 'package:logging/logging.dart'; +import 'package:stream_chat/src/core/api/attachment_file_uploader.dart'; import 'package:stream_chat/src/core/api/channel_api.dart'; import 'package:stream_chat/src/core/api/device_api.dart'; import 'package:stream_chat/src/core/api/general_api.dart'; import 'package:stream_chat/src/core/api/guest_api.dart'; import 'package:stream_chat/src/core/api/message_api.dart'; import 'package:stream_chat/src/core/api/moderation_api.dart'; +import 'package:stream_chat/src/core/api/user_api.dart'; import 'package:stream_chat/src/core/http/connection_id_manager.dart'; import 'package:stream_chat/src/core/http/stream_http_client.dart'; -import 'package:stream_chat/src/core/api/user_api.dart'; import 'package:stream_chat/src/core/http/token_manager.dart'; -import 'package:stream_chat/src/core/api/attachment_file_uploader.dart'; export 'device_api.dart' show PushProvider; -/// +/// ApiClient that wraps every other specific api class StreamChatApi { - /// + /// Initialize a new stream chat api StreamChatApi( String apiKey, { StreamHttpClient? client, @@ -38,42 +38,42 @@ class StreamChatApi { UserApi? _user; - /// + /// Api dedicated to users operations UserApi get user => _user ??= UserApi(_client); GuestApi? _guest; - /// + /// Api dedicated to guest operations GuestApi get guest => _guest ??= GuestApi(_client); MessageApi? _message; - /// + /// Api dedicated to message operations MessageApi get message => _message ??= MessageApi(_client); ChannelApi? _channel; - /// + /// Api dedicated to channel operations ChannelApi get channel => _channel ??= ChannelApi(_client); DeviceApi? _device; - /// + /// Api dedicated to device operations DeviceApi get device => _device ??= DeviceApi(_client); ModerationApi? _moderation; - /// + /// Api dedicated to moderation operations ModerationApi get moderation => _moderation ??= ModerationApi(_client); GeneralApi? _general; - /// + /// Api dedicated to general operations GeneralApi get general => _general ??= GeneralApi(_client); AttachmentFileUploader? _fileUploader; - /// + /// Class responsible for uploading images and files to a given channel AttachmentFileUploader get fileUploader => _fileUploader ??= StreamAttachmentFileUploader(_client); } diff --git a/packages/stream_chat/lib/src/core/api/user_api.dart b/packages/stream_chat/lib/src/core/api/user_api.dart index 04c72284..61159731 100644 --- a/packages/stream_chat/lib/src/core/api/user_api.dart +++ b/packages/stream_chat/lib/src/core/api/user_api.dart @@ -6,9 +6,9 @@ import 'package:stream_chat/src/core/http/stream_http_client.dart'; import 'package:stream_chat/src/core/models/filter.dart'; import 'package:stream_chat/src/core/models/user.dart'; -/// +/// Defines the api dedicated to users operations class UserApi { - /// + /// Initialize a new user api UserApi(this._client); final StreamHttpClient _client; diff --git a/packages/stream_chat/lib/src/core/http/connection_id_manager.dart b/packages/stream_chat/lib/src/core/http/connection_id_manager.dart index 55fa1896..59dcb1b6 100644 --- a/packages/stream_chat/lib/src/core/http/connection_id_manager.dart +++ b/packages/stream_chat/lib/src/core/http/connection_id_manager.dart @@ -1,26 +1,26 @@ // ignore_for_file: use_setters_to_change_properties -/// +/// Handles the connection id of the websocket connection class ConnectionIdManager { - /// + /// Initialize a new connection id manager ConnectionIdManager({ String? connectionId, }) : _connectionId = connectionId; String? _connectionId; - /// + /// Get the current connection id String? get connectionId => _connectionId; - /// + /// True if there is a connection id bool get hasConnectionId => _connectionId != null; - /// + /// Set the connection id void setConnectionId(String connectionId) { _connectionId = connectionId; } - /// + /// Clear the connection id void reset() { _connectionId = null; } diff --git a/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart b/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart index 3097c2b1..9947eefa 100644 --- a/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart +++ b/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart @@ -1,20 +1,20 @@ import 'package:dio/dio.dart'; import 'package:stream_chat/src/core/api/responses.dart'; +import 'package:stream_chat/src/core/error/error.dart'; import 'package:stream_chat/src/core/http/stream_chat_dio_error.dart'; import 'package:stream_chat/src/core/http/stream_http_client.dart'; import 'package:stream_chat/src/core/http/token.dart'; - import 'package:stream_chat/src/core/http/token_manager.dart'; -import 'package:stream_chat/src/core/error/error.dart'; -/// +/// Authentication interceptor that refreshes the token if +/// an auth error is received class AuthInterceptor extends Interceptor { - /// + /// Initialize a new auth interceptor AuthInterceptor(this._client, this._tokenManager); final StreamHttpClient _client; - /// + /// The token manager used in the client final TokenManager _tokenManager; @override diff --git a/packages/stream_chat/lib/src/core/http/interceptor/connection_id_interceptor.dart b/packages/stream_chat/lib/src/core/http/interceptor/connection_id_interceptor.dart index 78a3147f..59e1c4bc 100644 --- a/packages/stream_chat/lib/src/core/http/interceptor/connection_id_interceptor.dart +++ b/packages/stream_chat/lib/src/core/http/interceptor/connection_id_interceptor.dart @@ -1,8 +1,7 @@ import 'package:dio/dio.dart'; - import 'package:stream_chat/src/core/http/connection_id_manager.dart'; -/// +/// Interceptor that injects the connection id in the request params class ConnectionIdInterceptor extends Interceptor { /// ConnectionIdInterceptor(this.connectionIdManager); diff --git a/packages/stream_chat/lib/src/core/http/interceptor/logging_interceptor.dart b/packages/stream_chat/lib/src/core/http/interceptor/logging_interceptor.dart index 1f5687a8..f78b46ea 100644 --- a/packages/stream_chat/lib/src/core/http/interceptor/logging_interceptor.dart +++ b/packages/stream_chat/lib/src/core/http/interceptor/logging_interceptor.dart @@ -5,26 +5,26 @@ import 'dart:math' as math; import 'package:dio/dio.dart'; -/// +/// Step where we're logging enum InterceptStep { - /// + /// Request request, - /// + /// Response response, - /// + /// Error error, } -/// +/// Function used to print the log typedef LogPrint = void Function(InterceptStep step, Object object); void _defaultLogPrint(InterceptStep step, Object object) => print(object); -/// +/// Interceptor dedicated to logging class LoggingInterceptor extends Interceptor { - /// + /// Initialize a new logging interceptor LoggingInterceptor({ this.request = true, this.requestHeader = false, @@ -310,13 +310,6 @@ class LoggingInterceptor extends Interceptor { }); } -/* bool _canFlattenMap(Map map) => - map.values.where((dynamic val) => val is Map || val is List).isEmpty && - map.toString().length < maxWidth; - - bool _canFlattenList(List list) => - list.length < 10 && list.toString().length < maxWidth;*/ - void _printMapAsTable( void Function(Object) logPrint, Map? map, { diff --git a/packages/stream_chat/lib/src/core/http/stream_chat_dio_error.dart b/packages/stream_chat/lib/src/core/http/stream_chat_dio_error.dart index 987c9d67..a8ee0988 100644 --- a/packages/stream_chat/lib/src/core/http/stream_chat_dio_error.dart +++ b/packages/stream_chat/lib/src/core/http/stream_chat_dio_error.dart @@ -1,9 +1,9 @@ import 'package:dio/dio.dart'; import 'package:stream_chat/src/core/error/error.dart'; -/// +/// Error class specific to StreamChat and Dio class StreamChatDioError extends DioError { - /// + /// Initialize a stream chat dio error StreamChatDioError({ required this.error, required RequestOptions requestOptions, diff --git a/packages/stream_chat/lib/src/core/http/token.dart b/packages/stream_chat/lib/src/core/http/token.dart index 26532e80..7d746286 100644 --- a/packages/stream_chat/lib/src/core/http/token.dart +++ b/packages/stream_chat/lib/src/core/http/token.dart @@ -5,21 +5,22 @@ import 'package:jose/jose.dart'; import 'package:stream_chat/src/core/models/user.dart'; import 'package:stream_chat/src/core/util/utils.dart'; -/// +/// A function which can be used to request a Stream Chat API token from your +/// own backend server typedef GuestTokenProvider = Future Function(User user); -/// +/// Authentication type enum AuthType { - /// + /// JWT token jwt, - /// + /// Anonymous user anonymous, } -/// +/// Extension for returning the AuthType as a string extension AuthTypeX on AuthType { - /// + /// Returns the AuthType as a string String get raw => { AuthType.jwt: 'jwt', AuthType.anonymous: 'anonymous', @@ -71,13 +72,13 @@ class Token extends Equatable { return Token.fromRawValue(rawToken); } - /// + /// Authentication type of this token final AuthType authType; - /// + /// String value of the token final String rawValue; - /// + /// User id associated with this token final String userId; @override diff --git a/packages/stream_chat/lib/src/core/http/token_manager.dart b/packages/stream_chat/lib/src/core/http/token_manager.dart index 88e9746d..e5af0ddc 100644 --- a/packages/stream_chat/lib/src/core/http/token_manager.dart +++ b/packages/stream_chat/lib/src/core/http/token_manager.dart @@ -1,12 +1,13 @@ import 'package:stream_chat/src/core/http/token.dart'; /// A function which can be used to request a Stream Chat API token from your -/// own backend server. Function requires a single [userId]. +/// own backend server. +/// Function requires a single [userId]. typedef TokenProvider = Future Function(String userId); -/// +/// Handles common token operations class TokenManager { - /// + /// Initialize a new token manager TokenManager({ String? userId, Token? token, @@ -25,10 +26,10 @@ class TokenManager { /// User id to which this TokenManager is configured to String? get userId => _userId; - /// + /// True if it's a static token bool get isStatic => _type == 'static'; - /// + /// Set a token or a token provider Future setTokenOrProvider( String userId, { Token? token, @@ -58,7 +59,7 @@ class TokenManager { return loadToken(); } - /// + /// Returns the token refreshing the existing one if [refresh] is true Future loadToken({bool refresh = false}) async { assert( _userId != null && _type != null, @@ -71,7 +72,7 @@ class TokenManager { return _token!; } - /// + /// Resets the token manager void reset() { _userId = null; _token = null; From 4a336815ae14266198ff699314a250db5b4e8689 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 22 Jun 2021 14:47:21 +0200 Subject: [PATCH 094/150] fix showdialog in image gallery --- packages/stream_chat_flutter/lib/src/image_header.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/stream_chat_flutter/lib/src/image_header.dart b/packages/stream_chat_flutter/lib/src/image_header.dart index 434a73c5..3d156b16 100644 --- a/packages/stream_chat_flutter/lib/src/image_header.dart +++ b/packages/stream_chat_flutter/lib/src/image_header.dart @@ -111,6 +111,7 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget { final channel = StreamChannel.of(context).channel; final result = await showDialog( + useRootNavigator: false, context: context, barrierColor: StreamChatTheme.of(context).colorTheme.overlay, builder: (context) => StreamChannel( From bb642842f9bfa039eaea8c7bb39402e31291acdd Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 23 Jun 2021 12:22:17 +0200 Subject: [PATCH 095/150] fix message widget not update when message is updated --- .../lib/src/message_widget.dart | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index 753f201c..a976547f 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -413,39 +413,37 @@ class _MessageWidgetState extends State bool get showTimeStamp => widget.showTimestamp; - late final bool isMessageRead = widget.readList?.isNotEmpty == true; + bool get isMessageRead => widget.readList?.isNotEmpty == true; bool get showInChannel => widget.showInChannelIndicator; bool get hasQuotedMessage => widget.message.quotedMessage != null; - late final bool isSendFailed = - widget.message.status == MessageSendingStatus.failed; + bool get isSendFailed => widget.message.status == MessageSendingStatus.failed; - late final bool isUpdateFailed = + bool get isUpdateFailed => widget.message.status == MessageSendingStatus.failed_update; - late final bool isDeleteFailed = + bool get isDeleteFailed => widget.message.status == MessageSendingStatus.failed_delete; - late final bool isFailedState = - isSendFailed || isUpdateFailed || isDeleteFailed; + bool get isFailedState => isSendFailed || isUpdateFailed || isDeleteFailed; - late final bool isGiphy = + bool get isGiphy => widget.message.attachments.any((element) => element.type == 'giphy') == - true; - - late final bool isOnlyEmoji = widget.message.text?.isOnlyEmoji == true; - - late final bool hasNonUrlAttachments = widget.message.attachments - .where((it) => it.ogScrapeUrl == null) - .isNotEmpty == true; - late final bool hasUrlAttachments = + bool get isOnlyEmoji => widget.message.text?.isOnlyEmoji == true; + + bool get hasNonUrlAttachments => widget.message.attachments + .where((it) => it.ogScrapeUrl == null) + .isNotEmpty; + + bool get hasUrlAttachments => widget.message.attachments.any((it) => it.ogScrapeUrl != null) == true; - late final bool showBottomRow = showThreadReplyIndicator || + bool get showBottomRow => + showThreadReplyIndicator || showUsername || showTimeStamp || showInChannel || @@ -1177,7 +1175,7 @@ class _MessageWidgetState extends State ); } - late final bool isPinned = widget.message.pinned; + bool get isPinned => widget.message.pinned; Color? _getBackgroundColor() { if (hasQuotedMessage) { From 151054b2c662922238fc8258b8196de695c93245 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 23 Jun 2021 12:30:10 +0200 Subject: [PATCH 096/150] null check --- packages/stream_chat_flutter/lib/src/message_input.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 962d237b..39211116 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -798,7 +798,7 @@ class MessageInputState extends State { setState(() { _commandEnabled = true; }); - _commandsOverlay!.remove(); + _commandsOverlay?.remove(); _commandsOverlay = null; } else { _commandsOverlay = _buildCommandsOverlayEntry(); From db801f4cdbc9626a3128c518b0e1e04860d161d3 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 23 Jun 2021 17:18:48 +0530 Subject: [PATCH 097/150] add support for messageListView header/footer Signed-off-by: Sahil Kumar --- .../lib/src/message_list_view.dart | 110 +++++++++++++----- 1 file changed, 80 insertions(+), 30 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index aae9d193..c7966c3d 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -1,3 +1,4 @@ +// ignore_for_file: lines_longer_than_80_chars import 'dart:async'; import 'dart:math'; @@ -147,6 +148,8 @@ class MessageListView extends StatefulWidget { this.messageHighlightColor, this.onShowMessage, this.showConnectionStateTile = false, + this.headerBuilder, + this.footerBuilder, this.loadingBuilder, this.emptyBuilder, this.systemMessageBuilder, @@ -235,6 +238,12 @@ class MessageListView extends StatefulWidget { /// Function called when messages are fetched final Widget Function(BuildContext, List)? messageListBuilder; + /// + final WidgetBuilder? headerBuilder; + + /// + final WidgetBuilder? footerBuilder; + /// Function used to build a loading widget final WidgetBuilder? loadingBuilder; @@ -407,6 +416,12 @@ class _MessageListViewState extends State { break; } + final itemCount = messages.length + // total messages + 2 + // top + bottom loading indicator + 2 + // header + footer + 1 // parent message + ; + return InfoTile( showMessage: widget.showConnectionStateTile && showStatus, tileAnchor: Alignment.topCenter, @@ -448,16 +463,49 @@ class _MessageListViewState extends State { itemScrollController: _scrollController, reverse: true, addAutomaticKeepAlives: false, - itemCount: - messages.length + 2 + (_isThreadConversation ? 1 : 0), + itemCount: itemCount, + + // Item Count -> 8 (1 parent, 2 header+footer, 2 top+bottom, 3 messages) + // eg: |Type| rev(|Index(item)|) rev(|Index(separator)|) |Index(item)| |Index(separator)| + // ParentMessage -> 7 (count-1) + // Separator(ThreadSeparator) -> 6 (count-2) + // Header -> 6 (count-2) + // Separator(Header -> 8??52) -> 5 (count-3) + // TopLoader -> 5 (count-3) + // Separator(2||8) -> 4 (count-4) + // Message -> 4 (count-4) + // Separator(2||8) -> 3 (count-5) + // Message -> 3 (count-5) + // Separator(2||8) -> 2 (count-6) + // Message -> 2 (count-6) + // Separator(2||8) -> 1 (count-7) + // BottomLoader -> 1 (count-7) + // Separator(Footer -> 8??30) -> 0 (count-8) + // Footer -> 0 (count-8) + separatorBuilder: (context, i) { - if (i == messages.length) return const Offstage(); - if (i == 0) return const SizedBox(height: 30); - if (i == messages.length + 1) { + if (i == itemCount - 2) { + if (widget.parentMessage == null) { + return const Offstage(); + } return _buildThreadSeparator(); } + if (i == itemCount - 3) { + if (widget.headerBuilder == null) { + return const SizedBox(height: 52); + } + return const SizedBox(height: 8); + } + if (i == 0) { + if (widget.footerBuilder == null) { + return const SizedBox(height: 30); + } + return const SizedBox(height: 8); + } - final message = messages[i]; + if (i == 1 || i == itemCount - 4) return const Offstage(); + + final message = messages[i - 2]; final nextMessage = messages[i - 1]; if (!Jiffy(message.createdAt.toLocal()).isSame( nextMessage.createdAt.toLocal(), @@ -494,40 +542,51 @@ class _MessageListViewState extends State { return const SizedBox(height: 2); }, itemBuilder: (context, i) { - if (i == messages.length + 2) { - if (widget.parentMessageBuilder != null) { - return widget.parentMessageBuilder!( - context, - widget.parentMessage, - ); - } else { - return buildParentMessage(widget.parentMessage!); - } + if (i == itemCount - 1) { + if (widget.parentMessage == null) return const Offstage(); + return widget.parentMessageBuilder?.call( + context, + widget.parentMessage, + ) ?? + buildParentMessage(widget.parentMessage!); } - if (i == messages.length + 1) { + + if (i == itemCount - 2) { + return widget.headerBuilder?.call(context) ?? + const Offstage(); + } + + if (i == itemCount - 3) { return _buildLoadingIndicator( streamChannel!, QueryDirection.top, ); } - if (i == 0) { + + if (i == 1) { return _buildLoadingIndicator( streamChannel!, QueryDirection.bottom, ); } - final message = messages[i - 1]; + + if (i == 0) { + return widget.footerBuilder?.call(context) ?? + const Offstage(); + } + + final message = messages[i - 2]; Widget messageWidget; - if (i == 1) { + if (i == 2) { messageWidget = _buildBottomMessage( context, message, messages, streamChannel!, ); - } else if (i == messages.length - 1) { + } else if (i == messages.length - 4) { messageWidget = _buildTopMessage( context, message, @@ -578,7 +637,6 @@ class _MessageListViewState extends State { child: Padding( padding: const EdgeInsets.all(8), child: Text( - // ignore: lines_longer_than_80_chars '$replyCount ${replyCount == 1 ? 'Reply' : 'Replies'}', textAlign: TextAlign.center, style: _streamTheme.channelTheme.channelHeaderTheme.subtitle, @@ -1238,15 +1296,7 @@ class _LoadingIndicator extends StatelessWidget { ), ), builder: (context, data) { - if (!data) { - if (!isThreadConversation && direction == QueryDirection.top) { - return const SizedBox( - height: 52, - width: double.infinity, - ); - } - return const Offstage(); - } + if (!data) return const Offstage(); return const Center( child: Padding( padding: EdgeInsets.all(8), From 3b39e84e8fccaa98788b76b4f429a85308939df2 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 23 Jun 2021 17:44:07 +0530 Subject: [PATCH 098/150] fix bottomMessageIndex Signed-off-by: Sahil Kumar --- .../lib/src/message_list_view.dart | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index c7966c3d..b5008054 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -575,24 +575,26 @@ class _MessageListViewState extends State { const Offstage(); } - final message = messages[i - 2]; + final topMessageIndex = itemCount - 4; + const bottomMessageIndex = 2; + final message = messages[i - 2]; Widget messageWidget; - if (i == 2) { - messageWidget = _buildBottomMessage( - context, - message, - messages, - streamChannel!, - ); - } else if (i == messages.length - 4) { + if (i == topMessageIndex) { messageWidget = _buildTopMessage( context, message, messages, streamChannel, ); + } else if (i == bottomMessageIndex) { + messageWidget = _buildBottomMessage( + context, + message, + messages, + streamChannel!, + ); } else { if (widget.messageBuilder != null) { messageWidget = Builder( From 2ad71f16e317280b252d8f4adfe77cff0f527755 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 23 Jun 2021 19:24:37 +0200 Subject: [PATCH 099/150] fix messagelistview not rendering when user is not a member --- packages/stream_chat_flutter/lib/src/message_list_view.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 976aacb0..7b9a672e 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:math'; +import 'package:collection/collection.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -813,7 +814,7 @@ class _MessageListViewState extends State { final currentUser = StreamChat.of(context).user; final members = StreamChannel.of(context).channel.state?.members ?? []; final currentUserMember = - members.firstWhere((e) => e.user!.id == currentUser!.id); + members.firstWhereOrNull((e) => e.user!.id == currentUser!.id); return MessageWidget( showReplyMessage: false, @@ -866,7 +867,8 @@ class _MessageListViewState extends State { textBuilder: widget.textBuilder, usernameBuilder: widget.usernameBuilder, onLinkTap: widget.onLinkTap, - showPinButton: widget.pinPermissions.contains(currentUserMember.role), + showPinButton: currentUserMember != null && + widget.pinPermissions.contains(currentUserMember.role), ); } From 2816124c553e10e5b0e6ada087498ed5a2d357a9 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 24 Jun 2021 18:19:52 +0530 Subject: [PATCH 100/150] fix floatingDate, messageWidgetBuilder index Signed-off-by: Sahil Kumar --- .../lib/src/message_list_view.dart | 90 +++++++++---------- .../lib/src/better_stream_builder.dart | 7 +- 2 files changed, 46 insertions(+), 51 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index b5008054..3194827d 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -396,6 +396,12 @@ class _MessageListViewState extends State { _messageListLength = newMessagesListLength; + final itemCount = messages.length + // total messages + 2 + // top + bottom loading indicator + 2 + // header + footer + 1 // parent message + ; + return Stack( alignment: Alignment.center, children: [ @@ -416,12 +422,6 @@ class _MessageListViewState extends State { break; } - final itemCount = messages.length + // total messages - 2 + // top + bottom loading indicator - 2 + // header + footer - 1 // parent message - ; - return InfoTile( showMessage: widget.showConnectionStateTile && showStatus, tileAnchor: Alignment.topCenter, @@ -470,15 +470,15 @@ class _MessageListViewState extends State { // ParentMessage -> 7 (count-1) // Separator(ThreadSeparator) -> 6 (count-2) // Header -> 6 (count-2) - // Separator(Header -> 8??52) -> 5 (count-3) + // Separator(Header -> 8??T -> 0||52) -> 5 (count-3) // TopLoader -> 5 (count-3) - // Separator(2||8) -> 4 (count-4) + // Separator(0) -> 4 (count-4) // Message -> 4 (count-4) // Separator(2||8) -> 3 (count-5) // Message -> 3 (count-5) // Separator(2||8) -> 2 (count-6) // Message -> 2 (count-6) - // Separator(2||8) -> 1 (count-7) + // Separator(0) -> 1 (count-7) // BottomLoader -> 1 (count-7) // Separator(Footer -> 8??30) -> 0 (count-8) // Footer -> 0 (count-8) @@ -492,6 +492,7 @@ class _MessageListViewState extends State { } if (i == itemCount - 3) { if (widget.headerBuilder == null) { + if (_isThreadConversation) return const Offstage(); return const SizedBox(height: 52); } return const SizedBox(height: 8); @@ -505,8 +506,8 @@ class _MessageListViewState extends State { if (i == 1 || i == itemCount - 4) return const Offstage(); - final message = messages[i - 2]; - final nextMessage = messages[i - 1]; + final message = messages[i - 1]; + final nextMessage = messages[i - 2]; if (!Jiffy(message.createdAt.toLocal()).isSame( nextMessage.createdAt.toLocal(), Units.DAY, @@ -575,8 +576,9 @@ class _MessageListViewState extends State { const Offstage(); } - final topMessageIndex = itemCount - 4; - const bottomMessageIndex = 2; + final topMessageIndex = itemCount - + 4; // 7 -> parent // 6 -> header // 5 -> loader + const bottomMessageIndex = 2; // 1 -> loader // 0 -> footer final message = messages[i - 2]; Widget messageWidget; @@ -587,6 +589,7 @@ class _MessageListViewState extends State { message, messages, streamChannel, + i, ); } else if (i == bottomMessageIndex) { messageWidget = _buildBottomMessage( @@ -594,6 +597,7 @@ class _MessageListViewState extends State { message, messages, streamChannel!, + i, ); } else { if (widget.messageBuilder != null) { @@ -621,7 +625,8 @@ class _MessageListViewState extends State { }, ), if (widget.showScrollToBottom) _buildScrollToBottom(), - if (widget.showFloatingDateDivider) _buildFloatingDateDivider(), + if (widget.showFloatingDateDivider) + _buildFloatingDateDivider(itemCount), ], ); } @@ -647,42 +652,34 @@ class _MessageListViewState extends State { ); } - Positioned _buildFloatingDateDivider() => Positioned( + Positioned _buildFloatingDateDivider(int itemCount) => Positioned( top: 20, child: BetterStreamBuilder>( initialData: _itemPositionListener.itemPositions.value, stream: _itemPositionStream, comparator: (a, b) { - if (a == null) { + if (a == null || b == null) { return false; } - final aTop = _getTopElement(a)?.index; - final bTop = _getTopElement(b)?.index; + final aTop = _getTopElementIndex(a); + final bTop = _getTopElementIndex(b); return aTop == bTop; }, builder: (context, values) { - final items = _itemPositionListener.itemPositions.value; - if (items.isEmpty || messages.isEmpty) { - return const SizedBox(); + if (values.isEmpty || messages.isEmpty) { + return const Offstage(); } - var index = _getTopElement(values)?.index; + final index = _getTopElementIndex(values); - if (index == null || index > messages.length) { - return const SizedBox(); - } - - if (index == messages.length) { - index = max(index - 1, 0); + if (index == null || index <= 2 || index >= itemCount - 3) { + return const Offstage(); } + final message = messages[index - 2]; return widget.dateDividerBuilder != null - ? widget.dateDividerBuilder!( - messages[index].createdAt.toLocal(), - ) - : DateDivider( - dateTime: messages[index].createdAt.toLocal(), - ); + ? widget.dateDividerBuilder!(message.createdAt.toLocal()) + : DateDivider(dateTime: message.createdAt.toLocal()); }, ), ); @@ -691,16 +688,13 @@ class _MessageListViewState extends State { StreamChannelState? channel, QueryDirection direction) => _messageListController.paginateData!(direction: direction); - ItemPosition? _getTopElement(Iterable values) { - final inView = - values.where((ItemPosition position) => position.itemLeadingEdge < 0.9); - - if (inView.isEmpty) { - return null; - } - - return inView.reduce((ItemPosition max, ItemPosition position) => - position.itemLeadingEdge > max.itemLeadingEdge ? position : max); + int? _getTopElementIndex(Iterable values) { + final inView = values.where((position) => position.itemLeadingEdge < 1); + if (inView.isEmpty) return null; + return inView + .reduce((max, position) => + position.itemLeadingEdge > max.itemLeadingEdge ? position : max) + .index; } Widget _buildScrollToBottom() => StreamBuilder>( @@ -796,6 +790,7 @@ class _MessageListViewState extends State { Message message, List messages, StreamChannelState? streamChannel, + int index, ) { Widget messageWidget; if (widget.messageBuilder != null) { @@ -813,7 +808,7 @@ class _MessageListViewState extends State { ), ); } else { - messageWidget = buildMessage(message, messages, messages.length - 1); + messageWidget = buildMessage(message, messages, index); } return messageWidget; } @@ -823,6 +818,7 @@ class _MessageListViewState extends State { Message message, List messages, StreamChannelState streamChannel, + int index, ) { Widget messageWidget; if (widget.messageBuilder != null) { @@ -840,7 +836,7 @@ class _MessageListViewState extends State { ), ); } else { - messageWidget = buildMessage(message, messages, 0); + messageWidget = buildMessage(message, messages, index); } return VisibilityDetector( @@ -952,7 +948,7 @@ class _MessageListViewState extends State { final userId = StreamChat.of(context).user!.id; final isMyMessage = message.user!.id == userId; - final nextMessage = index - 2 >= 0 ? messages[index - 2] : null; + final nextMessage = index - 3 >= 0 ? messages[index - 3] : null; final isNextUserSame = nextMessage != null && message.user!.id == nextMessage.user!.id; diff --git a/packages/stream_chat_flutter_core/lib/src/better_stream_builder.dart b/packages/stream_chat_flutter_core/lib/src/better_stream_builder.dart index ddf342dc..871a8a1d 100644 --- a/packages/stream_chat_flutter_core/lib/src/better_stream_builder.dart +++ b/packages/stream_chat_flutter_core/lib/src/better_stream_builder.dart @@ -25,7 +25,7 @@ class BetterStreamBuilder extends StatefulWidget { final T initialData; /// Comparator used to check if the new data is different than the last one - final bool Function(T?, T)? comparator; + final bool Function(T?, T?)? comparator; /// Builder that builds based on the new snapshot final Widget Function(BuildContext context, T data) builder; @@ -96,9 +96,8 @@ class _BetterStreamBuilderState extends State> { void _onEvent(T event) { _lastError = null; - final isEqual = widget.comparator != null - ? widget.comparator!(_lastEvent, event) - : event == _lastEvent; + final isEqual = + widget.comparator?.call(_lastEvent, event) ?? event == _lastEvent; if (!isEqual) { if (mounted) { setState(() {}); From c169b887af526f4d097e8fc5bf2dc7bc004b6e7d Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 24 Jun 2021 18:24:56 +0530 Subject: [PATCH 101/150] add docs Signed-off-by: Sahil Kumar --- packages/stream_chat_flutter/lib/src/message_list_view.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 3194827d..85c441cd 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -238,10 +238,10 @@ class MessageListView extends StatefulWidget { /// Function called when messages are fetched final Widget Function(BuildContext, List)? messageListBuilder; - /// + /// Function used to build a header widget final WidgetBuilder? headerBuilder; - /// + /// Function used to build a footer widget final WidgetBuilder? footerBuilder; /// Function used to build a loading widget From 5325b7cb5ed55f72f53fcac81cfacbf4d9cafd93 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 24 Jun 2021 18:25:59 +0530 Subject: [PATCH 102/150] fix analyzer warning Signed-off-by: Sahil Kumar --- packages/stream_chat_flutter/lib/src/message_list_view.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 85c441cd..52ea3ab9 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -1,6 +1,5 @@ // ignore_for_file: lines_longer_than_80_chars import 'dart:async'; -import 'dart:math'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; From 87568235d560b5ca2ef127df2a8c522548d98228 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 25 Jun 2021 10:22:49 +0200 Subject: [PATCH 103/150] add copyWith to Messagewidget --- .../lib/src/message_widget.dart | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index a976547f..cc0f0d3c 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -397,6 +397,117 @@ class MessageWidget extends StatefulWidget { /// Customize onTap on attachment final void Function(Message message, Attachment attachment)? onAttachmentTap; + /// Creates a copy of [MessageWidget] with specified attributes overridden. + MessageWidget copyWith({ + Key? key, + void Function(User)? onMentionTap, + void Function(Message)? onThreadTap, + void Function(Message)? onReplyTap, + Widget Function(BuildContext, Message)? editMessageInputBuilder, + Widget Function(BuildContext, Message)? textBuilder, + Widget Function(BuildContext, Message)? usernameBuilder, + void Function(BuildContext, Message)? onMessageActions, + Message? message, + MessageTheme? messageTheme, + bool? reverse, + ShapeBorder? shape, + ShapeBorder? attachmentShape, + BorderSide? borderSide, + BorderSide? attachmentBorderSide, + BorderRadiusGeometry? borderRadiusGeometry, + BorderRadiusGeometry? attachmentBorderRadiusGeometry, + EdgeInsetsGeometry? padding, + EdgeInsets? textPadding, + EdgeInsetsGeometry? attachmentPadding, + DisplayWidget? showUserAvatar, + bool? showSendingIndicator, + bool? showReactions, + bool? allRead, + bool? showThreadReplyIndicator, + bool? showInChannelIndicator, + void Function(User)? onUserAvatarTap, + void Function(String)? onLinkTap, + bool? showReactionPickerIndicator, + List? readList, + ShowMessageCallback? onShowMessage, + ValueChanged? onReturnAction, + bool? showUsername, + bool? showTimestamp, + bool? showReplyMessage, + bool? showThreadReplyMessage, + bool? showEditMessage, + bool? showCopyMessage, + bool? showDeleteMessage, + bool? showResendMessage, + bool? showFlagButton, + bool? showPinButton, + bool? showPinHighlight, + Map? customAttachmentBuilders, + bool? translateUserAvatar, + OnQuotedMessageTap? onQuotedMessageTap, + void Function(Message)? onMessageTap, + List? customActions, + void Function(Message message, Attachment attachment)? onAttachmentTap, + }) => + MessageWidget( + key: key ?? this.key, + onMentionTap: onMentionTap ?? this.onMentionTap, + onThreadTap: onThreadTap ?? this.onThreadTap, + onReplyTap: onReplyTap ?? this.onReplyTap, + editMessageInputBuilder: + editMessageInputBuilder ?? this.editMessageInputBuilder, + textBuilder: textBuilder ?? this.textBuilder, + usernameBuilder: usernameBuilder ?? this.usernameBuilder, + onMessageActions: onMessageActions ?? this.onMessageActions, + message: message ?? this.message, + messageTheme: messageTheme ?? this.messageTheme, + reverse: reverse ?? this.reverse, + shape: shape ?? this.shape, + attachmentShape: attachmentShape ?? this.attachmentShape, + borderSide: borderSide ?? this.borderSide, + attachmentBorderSide: attachmentBorderSide ?? this.attachmentBorderSide, + borderRadiusGeometry: borderRadiusGeometry ?? this.borderRadiusGeometry, + attachmentBorderRadiusGeometry: attachmentBorderRadiusGeometry ?? + this.attachmentBorderRadiusGeometry, + padding: padding ?? this.padding, + textPadding: textPadding ?? this.textPadding, + attachmentPadding: attachmentPadding ?? this.attachmentPadding, + showUserAvatar: showUserAvatar ?? this.showUserAvatar, + showSendingIndicator: showSendingIndicator ?? this.showSendingIndicator, + showReactions: showReactions ?? this.showReactions, + allRead: allRead ?? this.allRead, + showThreadReplyIndicator: + showThreadReplyIndicator ?? this.showThreadReplyIndicator, + showInChannelIndicator: + showInChannelIndicator ?? this.showInChannelIndicator, + onUserAvatarTap: onUserAvatarTap ?? this.onUserAvatarTap, + onLinkTap: onLinkTap ?? this.onLinkTap, + showReactionPickerIndicator: + showReactionPickerIndicator ?? this.showReactionPickerIndicator, + readList: readList ?? this.readList, + onShowMessage: onShowMessage ?? this.onShowMessage, + onReturnAction: onReturnAction ?? this.onReturnAction, + showUsername: showUsername ?? this.showUsername, + showTimestamp: showTimestamp ?? this.showTimestamp, + showReplyMessage: showReplyMessage ?? this.showReplyMessage, + showThreadReplyMessage: + showThreadReplyMessage ?? this.showThreadReplyMessage, + showEditMessage: showEditMessage ?? this.showEditMessage, + showCopyMessage: showCopyMessage ?? this.showCopyMessage, + showDeleteMessage: showDeleteMessage ?? this.showDeleteMessage, + showResendMessage: showResendMessage ?? this.showResendMessage, + showFlagButton: showFlagButton ?? this.showFlagButton, + showPinButton: showPinButton ?? this.showPinButton, + showPinHighlight: showPinHighlight ?? this.showPinHighlight, + customAttachmentBuilders: + customAttachmentBuilders ?? this.attachmentBuilders, + translateUserAvatar: translateUserAvatar ?? this.translateUserAvatar, + onQuotedMessageTap: onQuotedMessageTap ?? this.onQuotedMessageTap, + onMessageTap: onMessageTap ?? this.onMessageTap, + customActions: customActions ?? this.customActions, + onAttachmentTap: onAttachmentTap ?? this.onAttachmentTap, + ); + @override _MessageWidgetState createState() => _MessageWidgetState(); } From dd7fafb3cff20bbbd263bff4a5adeab0adf95005 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 25 Jun 2021 10:39:03 +0200 Subject: [PATCH 104/150] use MessageWidget.copyWith in reactions modal --- .../lib/src/message_reactions_modal.dart | 60 ++----------------- .../lib/src/message_widget.dart | 33 ++++++---- 2 files changed, 27 insertions(+), 66 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart index 842fe79d..0292a05a 100644 --- a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart @@ -1,7 +1,6 @@ import 'dart:ui'; import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/reaction_bubble.dart'; import 'package:stream_chat_flutter/src/reaction_picker.dart'; import 'package:stream_chat_flutter/src/stream_chat.dart'; @@ -15,17 +14,16 @@ class MessageReactionsModal extends StatelessWidget { const MessageReactionsModal({ Key? key, required this.message, + required this.messageWidget, required this.messageTheme, this.showReactions = true, - this.messageShape, - this.attachmentShape, this.reverse = false, - this.showUserAvatar = DisplayWidget.show, this.onUserAvatarTap, - this.attachmentBorderRadiusGeometry, - this.textBuilder, }) : super(key: key); + /// Widget that shows the message + final Widget messageWidget; + /// Message to display reactions of final Message message; @@ -38,24 +36,9 @@ class MessageReactionsModal extends StatelessWidget { /// Flag to show reactions on message final bool showReactions; - /// Enum to change user avatar config - final DisplayWidget showUserAvatar; - - /// [ShapeBorder] to apply to message - final ShapeBorder? messageShape; - - /// [ShapeBorder] to apply to attachment - final ShapeBorder? attachmentShape; - /// Callback when user avatar is tapped final void Function(User)? onUserAvatarTap; - /// [BorderRadius] to apply to attachments - final BorderRadius? attachmentBorderRadiusGeometry; - - /// Customize the MessageWidget textBuilder - final Widget Function(BuildContext context, Message message)? textBuilder; - @override Widget build(BuildContext context) { final size = MediaQuery.of(context).size; @@ -77,8 +60,6 @@ class MessageReactionsModal extends StatelessWidget { final divFactor = message.attachments.isNotEmpty == true ? 1 : (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize)); - final hasFileAttachment = - message.attachments.any((it) => it.type == 'file') == true; final numberOfReactions = StreamChatTheme.of(context).reactionIcons.length; final shiftFactor = @@ -110,38 +91,7 @@ class MessageReactionsModal extends StatelessWidget { ), const SizedBox(height: 8), IgnorePointer( - child: MessageWidget( - key: const Key('MessageWidget'), - reverse: reverse, - message: message.copyWith( - text: message.text!.length > 200 - ? '${message.text!.substring(0, 200)}...' - : message.text, - ), - messageTheme: messageTheme, - showReactions: false, - showUsername: false, - showUserAvatar: showUserAvatar, - showTimestamp: false, - translateUserAvatar: false, - showSendingIndicator: false, - shape: messageShape, - attachmentShape: attachmentShape, - padding: const EdgeInsets.all(0), - attachmentBorderRadiusGeometry: attachmentBorderRadiusGeometry - ?.mirrorBorderIfReversed(reverse: !reverse), - attachmentPadding: EdgeInsets.all( - hasFileAttachment ? 4 : 2, - ), - textPadding: EdgeInsets.symmetric( - vertical: 8, - horizontal: message.text!.isOnlyEmoji ? 0 : 16.0, - ), - showReactionPickerIndicator: showReactions && - (message.status == MessageSendingStatus.sent), - textBuilder: textBuilder, - showPinHighlight: false, - ), + child: messageWidget, ), if (message.latestReactions?.isNotEmpty == true) ...[ const SizedBox(height: 8), diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index cc0f0d3c..aea2f8b9 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -500,7 +500,7 @@ class MessageWidget extends StatefulWidget { showPinButton: showPinButton ?? this.showPinButton, showPinHighlight: showPinHighlight ?? this.showPinHighlight, customAttachmentBuilders: - customAttachmentBuilders ?? this.attachmentBuilders, + customAttachmentBuilders ?? attachmentBuilders, translateUserAvatar: translateUserAvatar ?? this.translateUserAvatar, onQuotedMessageTap: onQuotedMessageTap ?? this.onQuotedMessageTap, onMessageTap: onMessageTap ?? this.onMessageTap, @@ -1075,18 +1075,29 @@ class _MessageWidgetState extends State builder: (context) => StreamChannel( channel: channel, child: MessageReactionsModal( - textBuilder: widget.textBuilder, - attachmentBorderRadiusGeometry: - widget.attachmentBorderRadiusGeometry as BorderRadius?, - showUserAvatar: - widget.message.user!.id == channel.client.state.user!.id - ? DisplayWidget.gone - : DisplayWidget.show, + messageWidget: widget.copyWith( + key: const Key('MessageWidget'), + message: widget.message.copyWith( + text: widget.message.text!.length > 200 + ? '${widget.message.text!.substring(0, 200)}...' + : widget.message.text, + ), + showReactions: false, + showUsername: false, + showTimestamp: false, + translateUserAvatar: false, + showSendingIndicator: false, + padding: const EdgeInsets.all(0), + showReactionPickerIndicator: widget.showReactions && + (widget.message.status == MessageSendingStatus.sent), + showPinHighlight: false, + showUserAvatar: + widget.message.user!.id == channel.client.state.user!.id + ? DisplayWidget.gone + : DisplayWidget.show, + ), onUserAvatarTap: widget.onUserAvatarTap, messageTheme: widget.messageTheme, - messageShape: widget.shape ?? _getDefaultShape(context), - attachmentShape: - widget.attachmentShape ?? _getDefaultAttachmentShape(context), reverse: widget.reverse, message: widget.message, showReactions: widget.showReactions, From 1ceabb90f1f44e87163783984d2aa519d8e18227 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 25 Jun 2021 10:44:28 +0200 Subject: [PATCH 105/150] use MessageWidget.copyWith in actions modal --- .../lib/src/message_actions_modal.dart | 67 ++----------------- .../lib/src/message_widget.dart | 53 ++++++--------- 2 files changed, 26 insertions(+), 94 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart index 6f57e7df..296b1e59 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -15,6 +15,7 @@ class MessageActionsModal extends StatefulWidget { const MessageActionsModal({ Key? key, required this.message, + required this.messageWidget, required this.messageTheme, this.showReactions = true, this.showDeleteMessage = true, @@ -27,18 +28,15 @@ class MessageActionsModal extends StatefulWidget { this.showThreadReplyMessage = true, this.showFlagButton = true, this.showPinButton = true, - this.showPinHighlight = false, - this.showUserAvatar = DisplayWidget.show, this.editMessageInputBuilder, - this.messageShape, - this.attachmentShape, this.reverse = false, this.customActions = const [], - this.attachmentBorderRadiusGeometry, this.onCopyTap, - this.textBuilder, }) : super(key: key); + /// Widget that shows the message + final Widget messageWidget; + /// Builder for edit message final Widget Function(BuildContext, Message)? editMessageInputBuilder; @@ -84,30 +82,12 @@ class MessageActionsModal extends StatefulWidget { /// Flag for showing pin action final bool showPinButton; - /// Display Pin Highlight - final bool showPinHighlight; - /// Flag for reversing message final bool reverse; - /// [ShapeBorder] to apply to the widget - final ShapeBorder? messageShape; - - /// [ShapeBorder] to apply to attachment - final ShapeBorder? attachmentShape; - - /// Enum for displaying user avatar - final DisplayWidget showUserAvatar; - - /// [BorderRadius] for attachment border - final BorderRadius? attachmentBorderRadiusGeometry; - /// List of custom actions final List customActions; - /// Customize the MessageWidget textBuilder - final Widget Function(BuildContext context, Message message)? textBuilder; - @override _MessageActionsModalState createState() => _MessageActionsModalState(); } @@ -142,9 +122,6 @@ class _MessageActionsModalState extends State { ? 1 : (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize)); - final hasFileAttachment = - widget.message.attachments.any((it) => it.type == 'file') == true; - final streamChatThemeData = StreamChatTheme.of(context); final numberOfReactions = streamChatThemeData.reactionIcons.length; @@ -178,41 +155,7 @@ class _MessageActionsModalState extends State { ), const SizedBox(height: 8), IgnorePointer( - child: MessageWidget( - key: const Key('MessageWidget'), - reverse: widget.reverse, - attachmentBorderRadiusGeometry: widget - .attachmentBorderRadiusGeometry - ?.mirrorBorderIfReversed(reverse: !widget.reverse), - message: widget.message.copyWith( - text: widget.message.text!.length > 200 - // ignore: lines_longer_than_80_chars - ? '${widget.message.text!.substring(0, 200)}...' - : widget.message.text, - ), - messageTheme: widget.messageTheme, - showReactions: false, - showUsername: false, - showReplyMessage: false, - showUserAvatar: widget.showUserAvatar, - attachmentPadding: EdgeInsets.all( - hasFileAttachment ? 4 : 2, - ), - showTimestamp: false, - translateUserAvatar: false, - padding: const EdgeInsets.all(0), - textPadding: EdgeInsets.symmetric( - vertical: 8, - horizontal: widget.message.text!.isOnlyEmoji ? 0 : 16.0, - ), - showReactionPickerIndicator: widget.showReactions && - (widget.message.status == MessageSendingStatus.sent), - showSendingIndicator: false, - shape: widget.messageShape, - attachmentShape: widget.attachmentShape, - showPinHighlight: false, - textBuilder: widget.textBuilder, - ), + child: widget.messageWidget, ), const SizedBox(height: 8), Padding( diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index aea2f8b9..49788f80 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -1023,19 +1023,30 @@ class _MessageWidgetState extends State builder: (context) => StreamChannel( channel: channel, child: MessageActionsModal( - textBuilder: widget.textBuilder, + messageWidget: widget.copyWith( + key: const Key('MessageWidget'), + message: widget.message.copyWith( + text: widget.message.text!.length > 200 + ? '${widget.message.text!.substring(0, 200)}...' + : widget.message.text, + ), + showReactions: false, + showUsername: false, + showTimestamp: false, + translateUserAvatar: false, + showSendingIndicator: false, + padding: const EdgeInsets.all(0), + showReactionPickerIndicator: widget.showReactions && + (widget.message.status == MessageSendingStatus.sent), + showPinHighlight: false, + showUserAvatar: + widget.message.user!.id == channel.client.state.user!.id + ? DisplayWidget.gone + : DisplayWidget.show, + ), onCopyTap: (message) => Clipboard.setData(ClipboardData(text: message.text)), - attachmentBorderRadiusGeometry: - widget.attachmentBorderRadiusGeometry as BorderRadius?, - showUserAvatar: - widget.message.user!.id == channel.client.state.user!.id - ? DisplayWidget.gone - : DisplayWidget.show, messageTheme: widget.messageTheme, - messageShape: widget.shape ?? _getDefaultShape(context), - attachmentShape: widget.attachmentShape ?? - _getDefaultAttachmentShape(context), reverse: widget.reverse, showDeleteMessage: widget.showDeleteMessage || isDeleteFailed, message: widget.message, @@ -1106,28 +1117,6 @@ class _MessageWidgetState extends State ); } - ShapeBorder _getDefaultAttachmentShape(BuildContext context) { - final hasFiles = - widget.message.attachments.any((it) => it.type == 'file') == true; - return RoundedRectangleBorder( - side: hasFiles - ? widget.attachmentBorderSide ?? - BorderSide( - color: _streamChatTheme.colorTheme.greyWhisper, - ) - : BorderSide.none, - borderRadius: widget.attachmentBorderRadiusGeometry ?? BorderRadius.zero, - ); - } - - ShapeBorder _getDefaultShape(BuildContext context) => RoundedRectangleBorder( - side: widget.borderSide ?? - BorderSide( - color: _streamChatTheme.colorTheme.greyWhisper, - ), - borderRadius: widget.borderRadiusGeometry ?? BorderRadius.zero, - ); - Widget _parseAttachments() { final attachmentGroups = >{}; From 8b71b32bdbf5ae5e34d8ee1199793596eb518ad9 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 25 Jun 2021 11:23:52 +0200 Subject: [PATCH 106/150] use MessageWidget.copyWith in messagelistview --- .../lib/src/message_list_view.dart | 112 +++++------------- .../lib/src/message_widget.dart | 15 ++- 2 files changed, 42 insertions(+), 85 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 7eeba3e4..525f7313 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -56,32 +56,35 @@ typedef ReplyTapCallback = void Function(Message); class MessageDetails { /// Constructor for creating [MessageDetails] MessageDetails( - BuildContext context, + String currentUserId, this.message, List messages, this.index, + this.defaultMessageWidget, ) { - isMyMessage = message.user?.id == StreamChat.of(context).user?.id; + isMyMessage = message.user?.id == currentUserId; isLastUser = index + 1 < messages.length && message.user?.id == messages[index + 1].user?.id; isNextUser = index - 1 >= 0 && message.user!.id == messages[index - 1].user?.id; } + late final MessageWidget defaultMessageWidget; + /// True if the message belongs to the current user - bool? isMyMessage; + late final bool isMyMessage; /// True if the user message is the same of the previous message - bool? isLastUser; + late final bool isLastUser; /// True if the user message is the same of the next message - bool? isNextUser; + late final bool isNextUser; /// The message - Message message; + late final Message message; /// The index of the message - int index; + late final int index; } /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_listview.png) @@ -576,46 +579,21 @@ class _MessageListViewState extends State { const Offstage(); } - final topMessageIndex = itemCount - - 4; // 7 -> parent // 6 -> header // 5 -> loader const bottomMessageIndex = 2; // 1 -> loader // 0 -> footer final message = messages[i - 2]; Widget messageWidget; - if (i == topMessageIndex) { - messageWidget = _buildTopMessage( - context, - message, - messages, - streamChannel, - i, - ); - } else if (i == bottomMessageIndex) { + if (i == bottomMessageIndex) { messageWidget = _buildBottomMessage( context, message, messages, streamChannel!, - i, + i - 2, ); } else { - if (widget.messageBuilder != null) { - messageWidget = Builder( - key: ValueKey('MESSAGE-${message.id}'), - builder: (context) => widget.messageBuilder!( - context, - MessageDetails( - context, - message, - messages, - i, - ), - messages), - ); - } else { - messageWidget = buildMessage(message, messages, i); - } + messageWidget = buildMessage(message, messages, i - 2); } return messageWidget; }, @@ -785,34 +763,6 @@ class _MessageListViewState extends State { isThreadConversation: _isThreadConversation, ); - Widget _buildTopMessage( - BuildContext context, - Message message, - List messages, - StreamChannelState? streamChannel, - int index, - ) { - Widget messageWidget; - if (widget.messageBuilder != null) { - messageWidget = Builder( - key: const ValueKey('TOP-MESSAGE'), - builder: (_) => widget.messageBuilder!( - context, - MessageDetails( - context, - message, - messages, - messages.length - 1, - ), - messages, - ), - ); - } else { - messageWidget = buildMessage(message, messages, index); - } - return messageWidget; - } - Widget _buildBottomMessage( BuildContext context, Message message, @@ -820,24 +770,7 @@ class _MessageListViewState extends State { StreamChannelState streamChannel, int index, ) { - Widget messageWidget; - if (widget.messageBuilder != null) { - messageWidget = Builder( - key: ValueKey('BOTTOM-MESSAGE-${message.id}'), - builder: (_) => widget.messageBuilder!( - context, - MessageDetails( - context, - message, - messages, - 0, - ), - messages, - ), - ); - } else { - messageWidget = buildMessage(message, messages, index); - } + final messageWidget = buildMessage(message, messages, index); return VisibilityDetector( key: ValueKey('BOTTOM-MESSAGE-${message.id}'), @@ -1015,7 +948,7 @@ class _MessageListViewState extends State { final currentUserMember = members.firstWhere((e) => e.user!.id == currentUser!.id); - Widget child = MessageWidget( + Widget messageWidget = MessageWidget( key: ValueKey('MESSAGE-${message.id}'), message: message, reverse: isMyMessage, @@ -1130,6 +1063,21 @@ class _MessageListViewState extends State { showPinButton: widget.pinPermissions.contains(currentUserMember.role), ); + if (widget.messageBuilder != null) { + messageWidget = widget.messageBuilder!( + context, + MessageDetails( + userId, + message, + messages, + index, + messageWidget as MessageWidget, + ), + messages, + ); + } + + var child = messageWidget; if (!message.isDeleted && !message.isSystem && !message.isEphemeral && diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index 49788f80..de86873d 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -572,7 +572,7 @@ class _MessageWidgetState extends State super.build(context); final avatarWidth = widget.messageTheme.avatarTheme?.constraints.maxWidth ?? 40; - final leftPadding = + final bottomRowPadding = widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 0.5; return Material( @@ -627,7 +627,8 @@ class _MessageWidgetState extends State crossAxisAlignment: CrossAxisAlignment.end, mainAxisSize: MainAxisSize.min, children: [ - if (widget.showUserAvatar == + if (!widget.reverse && + widget.showUserAvatar == DisplayWidget.show && widget.message.user != null) ...[ _buildUserAvatar(), @@ -753,6 +754,13 @@ class _MessageWidgetState extends State ), ), ), + if (widget.reverse && + widget.showUserAvatar == + DisplayWidget.show && + widget.message.user != null) ...[ + _buildUserAvatar(), + const SizedBox(width: 4), + ] ], ), if (showBottomRow) @@ -763,7 +771,8 @@ class _MessageWidgetState extends State if (showBottomRow) Padding( padding: EdgeInsets.only( - left: leftPadding, + left: !widget.reverse ? bottomRowPadding : 0, + right: widget.reverse ? bottomRowPadding : 0, bottom: isPinned && widget.showPinHighlight ? 6.0 : 0.0, ), From e1a9e4d8789527152577adcdf0765d095d397fb2 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 25 Jun 2021 11:30:19 +0200 Subject: [PATCH 107/150] use MessageWidget.copyWith in parentMessageBuilder --- .../lib/src/message_list_view.dart | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 525f7313..f6c6d60d 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -26,9 +26,12 @@ typedef MessageBuilder = Widget Function( ); /// Widget builder for parent message +/// [defaultMessageWidget] is the default [MessageWidget] configuration +/// Use [defaultMessageWidget.copyWith] to easily customize it typedef ParentMessageBuilder = Widget Function( BuildContext, Message?, + MessageWidget defaultMessageWidget, ); /// Widget builder for system message @@ -69,6 +72,8 @@ class MessageDetails { index - 1 >= 0 && message.user!.id == messages[index - 1].user?.id; } + /// Default [MessageWidget] configuration + /// use [defaultMessageWidget.copyWith] to easily customize it late final MessageWidget defaultMessageWidget; /// True if the message belongs to the current user @@ -548,11 +553,7 @@ class _MessageListViewState extends State { itemBuilder: (context, i) { if (i == itemCount - 1) { if (widget.parentMessage == null) return const Offstage(); - return widget.parentMessageBuilder?.call( - context, - widget.parentMessage, - ) ?? - buildParentMessage(widget.parentMessage!); + return buildParentMessage(widget.parentMessage!); } if (i == itemCount - 2) { @@ -804,7 +805,7 @@ class _MessageListViewState extends State { final currentUserMember = members.firstWhereOrNull((e) => e.user!.id == currentUser!.id); - return MessageWidget( + final defaultMessageWidget = MessageWidget( showReplyMessage: false, showResendMessage: false, showThreadReplyMessage: false, @@ -858,6 +859,16 @@ class _MessageListViewState extends State { showPinButton: currentUserMember != null && widget.pinPermissions.contains(currentUserMember.role), ); + + if (widget.parentMessageBuilder != null) { + return widget.parentMessageBuilder!.call( + context, + widget.parentMessage, + defaultMessageWidget, + ); + } + + return defaultMessageWidget; } Widget buildMessage( From f3a5db159423875c5a992e8a44289b2bfa365a28 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 25 Jun 2021 12:02:33 +0200 Subject: [PATCH 108/150] fix tests --- .../test/src/message_action_modal_test.dart | 21 +++++++ .../src/message_reactions_modal_test.dart | 55 ++++--------------- 2 files changed, 31 insertions(+), 45 deletions(-) diff --git a/packages/stream_chat_flutter/test/src/message_action_modal_test.dart b/packages/stream_chat_flutter/test/src/message_action_modal_test.dart index 9b7b3e52..953434de 100644 --- a/packages/stream_chat_flutter/test/src/message_action_modal_test.dart +++ b/packages/stream_chat_flutter/test/src/message_action_modal_test.dart @@ -38,6 +38,10 @@ void main() { id: 'user-id', ), ), + messageWidget: const Text( + 'test', + key: Key('MessageWidget'), + ), messageTheme: streamTheme.ownMessageTheme, ), ), @@ -86,6 +90,10 @@ void main() { ), ), messageTheme: streamTheme.ownMessageTheme, + messageWidget: const Text( + 'test', + key: Key('MessageWidget'), + ), ), ), ), @@ -123,6 +131,7 @@ void main() { client: client, child: SizedBox( child: MessageActionsModal( + messageWidget: const Text('test'), message: Message( text: 'test', user: User( @@ -178,6 +187,7 @@ void main() { client: client, child: SizedBox( child: MessageActionsModal( + messageWidget: const Text('test'), onReplyTap: (m) { tapped = true; }, @@ -223,6 +233,7 @@ void main() { client: client, child: SizedBox( child: MessageActionsModal( + messageWidget: const Text('test'), onThreadReplyTap: (m) { tapped = true; }, @@ -272,6 +283,7 @@ void main() { channel: channel, child: SizedBox( child: MessageActionsModal( + messageWidget: const Text('test'), message: Message( text: 'test', user: User( @@ -320,6 +332,7 @@ void main() { channel: channel, child: SizedBox( child: MessageActionsModal( + messageWidget: const Text('test'), editMessageInputBuilder: (context, m) => const Text('test'), message: Message( text: 'test', @@ -371,6 +384,7 @@ void main() { channel: channel, child: SizedBox( child: MessageActionsModal( + messageWidget: const Text('test'), onCopyTap: (m) => tapped = true, message: Message( text: 'test', @@ -420,6 +434,7 @@ void main() { channel: channel, child: SizedBox( child: MessageActionsModal( + messageWidget: const Text('test'), message: Message( status: MessageSendingStatus.failed, text: 'test', @@ -469,6 +484,7 @@ void main() { channel: channel, child: SizedBox( child: MessageActionsModal( + messageWidget: const Text('test'), message: Message( status: MessageSendingStatus.failed_update, text: 'test', @@ -516,6 +532,7 @@ void main() { channel: channel, child: SizedBox( child: MessageActionsModal( + messageWidget: const Text('test'), message: Message( id: 'testid', text: 'test', @@ -571,6 +588,7 @@ void main() { channel: channel, child: SizedBox( child: MessageActionsModal( + messageWidget: const Text('test'), message: Message( id: 'testid', text: 'test', @@ -626,6 +644,7 @@ void main() { channel: channel, child: SizedBox( child: MessageActionsModal( + messageWidget: const Text('test'), message: Message( id: 'testid', text: 'test', @@ -679,6 +698,7 @@ void main() { channel: channel, child: SizedBox( child: MessageActionsModal( + messageWidget: const Text('test'), message: Message( id: 'testid', text: 'test', @@ -734,6 +754,7 @@ void main() { channel: channel, child: SizedBox( child: MessageActionsModal( + messageWidget: const Text('test'), message: Message( id: 'testid', text: 'test', diff --git a/packages/stream_chat_flutter/test/src/message_reactions_modal_test.dart b/packages/stream_chat_flutter/test/src/message_reactions_modal_test.dart index 6bdb72b0..9452e7e6 100644 --- a/packages/stream_chat_flutter/test/src/message_reactions_modal_test.dart +++ b/packages/stream_chat_flutter/test/src/message_reactions_modal_test.dart @@ -34,6 +34,10 @@ void main() { client: client, streamChatThemeData: streamTheme, child: MessageReactionsModal( + messageWidget: const Text( + 'test', + key: Key('MessageWidget'), + ), message: message, messageTheme: streamTheme.ownMessageTheme, ), @@ -43,28 +47,9 @@ void main() { await tester.pump(const Duration(milliseconds: 1000)); - expect(find.byType(MessageWidget), findsOneWidget); - final messageWidget = - tester.widget(find.byType(MessageWidget)); - expect(messageWidget.message, message); - expect(messageWidget.messageTheme, streamTheme.ownMessageTheme); - expect(messageWidget.attachmentBorderRadiusGeometry, null); - expect(messageWidget.showUserAvatar, DisplayWidget.show); - expect(messageWidget.reverse, false); - expect(messageWidget.attachmentShape, null); - expect(messageWidget.shape, null); - expect(messageWidget.onUserAvatarTap, null); - expect(messageWidget.showReactions, false); - expect(messageWidget.showUsername, false); - expect(messageWidget.showThreadReplyIndicator, false); - expect(messageWidget.showTimestamp, false); - expect(messageWidget.translateUserAvatar, false); - expect(messageWidget.showSendingIndicator, false); - expect(find.byType(ReactionBubble), findsNothing); - //only one avatar (the message one) - expect(find.byType(UserAvatar), findsOneWidget); + expect(find.byType(UserAvatar), findsNothing); }, ); @@ -105,14 +90,14 @@ void main() { client: client, streamChatThemeData: streamTheme, child: MessageReactionsModal( + messageWidget: const Text( + 'test', + key: Key('MessageWidget'), + ), message: message, messageTheme: streamTheme.ownMessageTheme, - showUserAvatar: DisplayWidget.gone, reverse: true, - attachmentBorderRadiusGeometry: BorderRadius.circular(1), - attachmentShape: const RoundedRectangleBorder(), showReactions: false, - messageShape: const RoundedRectangleBorder(), onUserAvatarTap: onUserAvatarTap, ), ), @@ -121,27 +106,7 @@ void main() { await tester.pump(const Duration(milliseconds: 1000)); - expect(find.byType(MessageWidget), findsOneWidget); - final messageWidget = - tester.widget(find.byType(MessageWidget)); - expect(messageWidget.message, message); - expect(messageWidget.messageTheme, streamTheme.ownMessageTheme); - expect(messageWidget.attachmentBorderRadiusGeometry, - BorderRadius.circular(1)); - expect(messageWidget.showUserAvatar, DisplayWidget.gone); - expect(messageWidget.reverse, true); - expect(messageWidget.showReactions, false); - expect(messageWidget.attachmentShape, const RoundedRectangleBorder()); - expect(messageWidget.shape, const RoundedRectangleBorder()); - expect(messageWidget.showReactions, false); - expect(messageWidget.showUsername, false); - expect(messageWidget.showThreadReplyIndicator, false); - expect(messageWidget.showTimestamp, false); - expect(messageWidget.translateUserAvatar, false); - expect(messageWidget.showSendingIndicator, false); - - final userAvatar = tester.widget(find.byType(UserAvatar)); - expect(userAvatar.onTap, onUserAvatarTap); + expect(find.byKey(const Key('MessageWidget')), findsOneWidget); expect(find.byType(ReactionBubble), findsOneWidget); expect(find.byType(UserAvatar), findsOneWidget); From 155706c8fd8b089f9a094ebe7773cdb80c997234 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 25 Jun 2021 14:01:35 +0200 Subject: [PATCH 109/150] remove unnecessary late --- packages/stream_chat_flutter/lib/src/message_list_view.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index f6c6d60d..bdc9d069 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -74,7 +74,7 @@ class MessageDetails { /// Default [MessageWidget] configuration /// use [defaultMessageWidget.copyWith] to easily customize it - late final MessageWidget defaultMessageWidget; + final MessageWidget defaultMessageWidget; /// True if the message belongs to the current user late final bool isMyMessage; @@ -86,10 +86,10 @@ class MessageDetails { late final bool isNextUser; /// The message - late final Message message; + final Message message; /// The index of the message - late final int index; + final int index; } /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_listview.png) From 1c3dbb1595d590bb6519198815cd84f28367f006 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 25 Jun 2021 14:18:56 +0200 Subject: [PATCH 110/150] move messagewidget in the builder --- .../example/lib/tutorial-part-5.dart | 1 + .../stream_chat_flutter/lib/src/message_list_view.dart | 10 ++++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/stream_chat_flutter/example/lib/tutorial-part-5.dart b/packages/stream_chat_flutter/example/lib/tutorial-part-5.dart index 7a75c037..8f1118dc 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial-part-5.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial-part-5.dart @@ -93,6 +93,7 @@ class ChannelPage extends StatelessWidget { BuildContext context, MessageDetails details, List messages, + MessageWidget _, ) { final message = details.message; final isCurrentUser = StreamChat.of(context).user!.id == message.user!.id; diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index bdc9d069..74942259 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -19,10 +19,13 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:visibility_detector/visibility_detector.dart'; /// Widget builder for message +/// [defaultMessageWidget] is the default [MessageWidget] configuration +/// Use [defaultMessageWidget.copyWith] to easily customize it typedef MessageBuilder = Widget Function( BuildContext, MessageDetails, List, + MessageWidget defaultMessageWidget, ); /// Widget builder for parent message @@ -63,7 +66,6 @@ class MessageDetails { this.message, List messages, this.index, - this.defaultMessageWidget, ) { isMyMessage = message.user?.id == currentUserId; isLastUser = index + 1 < messages.length && @@ -72,10 +74,6 @@ class MessageDetails { index - 1 >= 0 && message.user!.id == messages[index - 1].user?.id; } - /// Default [MessageWidget] configuration - /// use [defaultMessageWidget.copyWith] to easily customize it - final MessageWidget defaultMessageWidget; - /// True if the message belongs to the current user late final bool isMyMessage; @@ -1082,9 +1080,9 @@ class _MessageListViewState extends State { message, messages, index, - messageWidget as MessageWidget, ), messages, + messageWidget as MessageWidget, ); } From 56bce0957cc9d9257cf0bb8f182e7c541bbdde78 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Sat, 26 Jun 2021 12:41:52 +0200 Subject: [PATCH 111/150] fix payload and improve tests --- .../lib/src/core/api/general_api.dart | 3 +- .../test/src/core/api/general_api_test.dart | 44 ++++++++++++++----- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/packages/stream_chat/lib/src/core/api/general_api.dart b/packages/stream_chat/lib/src/core/api/general_api.dart index 8eeeaa5f..c6243148 100644 --- a/packages/stream_chat/lib/src/core/api/general_api.dart +++ b/packages/stream_chat/lib/src/core/api/general_api.dart @@ -79,16 +79,17 @@ class GeneralApi { queryParameters: { 'payload': jsonEncode({ 'type': channelType, + 'filter_conditions': filter ?? {}, if (channelId != null) 'id': channelId else if (members != null) 'members': members, if (sort != null) 'sort': sort, - if (filter != null) 'filter': filter, if (pagination != null) ...pagination.toJson(), }), }, ); + return QueryMembersResponse.fromJson(response.data); } } diff --git a/packages/stream_chat/test/src/core/api/general_api_test.dart b/packages/stream_chat/test/src/core/api/general_api_test.dart index 8844262f..bf7a3018 100644 --- a/packages/stream_chat/test/src/core/api/general_api_test.dart +++ b/packages/stream_chat/test/src/core/api/general_api_test.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:dio/dio.dart'; import 'package:mocktail/mocktail.dart'; import 'package:stream_chat/src/core/api/general_api.dart'; @@ -149,11 +151,22 @@ void main() { (index) => Member(userId: 'test-user-id=$index'), ); - when(() => - client.get(path, queryParameters: any(named: 'queryParameters'))) - .thenAnswer((_) async => successResponse(path, data: { - 'members': [...members.map((it) => it.toJson())] - })); + final payload = jsonEncode({ + 'type': channelType, + 'filter_conditions': filter, + 'id': channelId, + 'sort': sort, + ...pagination.toJson(), + }); + + when(() => client.get( + path, + queryParameters: { + 'payload': payload, + }, + )).thenAnswer((_) async => successResponse(path, data: { + 'members': [...members.map((it) => it.toJson())] + })); final res = await generalApi.queryMembers( channelType, @@ -185,11 +198,22 @@ void main() { (index) => Member(userId: 'test-user-id=$index'), ); - when(() => - client.get(path, queryParameters: any(named: 'queryParameters'))) - .thenAnswer((_) async => successResponse(path, data: { - 'members': [...members.map((it) => it.toJson())] - })); + final payload = jsonEncode({ + 'type': channelType, + 'filter_conditions': filter, + 'members': members, + 'sort': sort, + ...pagination.toJson(), + }); + + when(() => client.get( + path, + queryParameters: { + 'payload': payload, + }, + )).thenAnswer((_) async => successResponse(path, data: { + 'members': [...members.map((it) => it.toJson())] + })); final res = await generalApi.queryMembers( channelType, From 9cc6ef124c7f90f5e5c224ffd4f728a99e1d136d Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Sat, 26 Jun 2021 13:25:06 +0200 Subject: [PATCH 112/150] better channel api tests --- .../lib/src/core/api/channel_api.dart | 2 +- .../test/src/core/api/channel_api_test.dart | 201 ++++++++++++++---- .../test/src/core/api/general_api_test.dart | 43 +++- 3 files changed, 192 insertions(+), 54 deletions(-) diff --git a/packages/stream_chat/lib/src/core/api/channel_api.dart b/packages/stream_chat/lib/src/core/api/channel_api.dart index 7660c125..68f6c4f8 100644 --- a/packages/stream_chat/lib/src/core/api/channel_api.dart +++ b/packages/stream_chat/lib/src/core/api/channel_api.dart @@ -76,7 +76,7 @@ class ChannelApi { // pagination ...paginationParams.toJson() - }) + }), }, ); return QueryChannelsResponse.fromJson(response.data); diff --git a/packages/stream_chat/test/src/core/api/channel_api_test.dart b/packages/stream_chat/test/src/core/api/channel_api_test.dart index 2152cee0..ab530ff1 100644 --- a/packages/stream_chat/test/src/core/api/channel_api_test.dart +++ b/packages/stream_chat/test/src/core/api/channel_api_test.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:dio/dio.dart'; import 'package:mocktail/mocktail.dart'; import 'package:stream_chat/src/core/api/channel_api.dart'; @@ -76,11 +78,24 @@ void main() { const path = '$channelPath/query'; final channelState = _generateChannelState(channelId, channelType); - when(() => client.post(path, data: any(named: 'data'))) - .thenAnswer((_) async => successResponse( - path, - data: channelState.toJson(), - )); + + final data = { + 'state': true, + 'watch': false, + 'presence': false, + 'data': channelData, + 'messages': messagePagination, + 'members': membersPagination, + 'watchers': watchersPagination, + }; + + when(() => client.post( + path, + data: data, + )).thenAnswer((_) async => successResponse( + path, + data: channelState.toJson(), + )); final res = await channelApi.queryChannel( channelType, @@ -115,13 +130,34 @@ void main() { const path = '/channels'; final channelState = _generateChannelState(channelId, channelType); - when(() => client.get(path, queryParameters: any(named: 'queryParameters'))) - .thenAnswer((_) async => successResponse( - path, - data: { - 'channels': [channelState.toJson()] - }, - )); + + final payload = jsonEncode({ + // default options + 'state': true, + 'watch': true, + 'presence': false, + + // passed options + 'sort': sort, + 'filter_conditions': filter, + 'member_limit': memberLimit, + 'message_limit': messageLimit, + + // pagination + ...const PaginationParams().toJson() + }); + + when(() => client.get( + path, + queryParameters: { + 'payload': payload, + }, + )).thenAnswer((_) async => successResponse( + path, + data: { + 'channels': [channelState.toJson()] + }, + )); final res = await channelApi.queryChannels( filter: filter, @@ -166,11 +202,18 @@ void main() { extraData: data, ); - when(() => client.post(path, data: any(named: 'data'))) - .thenAnswer((_) async => successResponse(path, data: { - 'channel': channelModel.toJson(), - 'message': message.toJson(), - })); + when(() => client.post( + path, + data: any( + named: 'data', + that: wrapMatcher((Map v) => + containsPair('data', data).matches(v, {}) && + contains('message').matches(v, {})), + ), + )).thenAnswer((_) async => successResponse(path, data: { + 'channel': channelModel.toJson(), + 'message': message.toJson(), + })); final res = await channelApi.updateChannel( channelId, @@ -235,11 +278,16 @@ void main() { final path = _getChannelUrl(channelId, channelType); - when(() => client.post(path, data: any(named: 'data'))) - .thenAnswer((_) async => successResponse(path, data: { - 'channel': channelModel.toJson(), - 'message': message.toJson(), - })); + when(() => client.post( + path, + data: { + 'accept_invite': true, + 'message': message, + }, + )).thenAnswer((_) async => successResponse(path, data: { + 'channel': channelModel.toJson(), + 'message': message.toJson(), + })); final res = await channelApi.acceptChannelInvite( channelId, @@ -264,11 +312,16 @@ void main() { final path = _getChannelUrl(channelId, channelType); - when(() => client.post(path, data: any(named: 'data'))) - .thenAnswer((_) async => successResponse(path, data: { - 'channel': channelModel.toJson(), - 'message': message.toJson(), - })); + when(() => client.post( + path, + data: { + 'reject_invite': true, + 'message': message, + }, + )).thenAnswer((_) async => successResponse(path, data: { + 'channel': channelModel.toJson(), + 'message': message.toJson(), + })); final res = await channelApi.rejectChannelInvite( channelId, @@ -293,11 +346,16 @@ void main() { final path = _getChannelUrl(channelId, channelType); - when(() => client.post(path, data: any(named: 'data'))) - .thenAnswer((_) async => successResponse(path, data: { - 'channel': channelModel.toJson(), - 'message': message.toJson(), - })); + when(() => client.post( + path, + data: { + 'invites': memberIds, + 'message': message, + }, + )).thenAnswer((_) async => successResponse(path, data: { + 'channel': channelModel.toJson(), + 'message': message.toJson(), + })); final res = await channelApi.inviteChannelMembers( channelId, @@ -323,11 +381,16 @@ void main() { final path = _getChannelUrl(channelId, channelType); - when(() => client.post(path, data: any(named: 'data'))) - .thenAnswer((_) async => successResponse(path, data: { - 'channel': channelModel.toJson(), - 'message': message.toJson(), - })); + when(() => client.post( + path, + data: { + 'add_members': memberIds, + 'message': message, + }, + )).thenAnswer((_) async => successResponse(path, data: { + 'channel': channelModel.toJson(), + 'message': message.toJson(), + })); final res = await channelApi.addMembers( channelId, @@ -353,11 +416,16 @@ void main() { final path = _getChannelUrl(channelId, channelType); - when(() => client.post(path, data: any(named: 'data'))) - .thenAnswer((_) async => successResponse(path, data: { - 'channel': channelModel.toJson(), - 'message': message.toJson(), - })); + when(() => client.post( + path, + data: { + 'remove_members': memberIds, + 'message': message, + }, + )).thenAnswer((_) async => successResponse(path, data: { + 'channel': channelModel.toJson(), + 'message': message.toJson(), + })); final res = await channelApi.removeMembers( channelId, @@ -381,7 +449,7 @@ void main() { final path = '${_getChannelUrl(channelId, channelType)}/event'; - when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + when(() => client.post(path, data: {'event': event})).thenAnswer( (_) async => successResponse(path, data: {})); final res = await channelApi.sendEvent(channelId, channelType, event); @@ -432,8 +500,14 @@ void main() { final path = '${_getChannelUrl(channelId, channelType)}/hide'; - when(() => client.post(path, data: any(named: 'data'))).thenAnswer( - (_) async => successResponse(path, data: {})); + when( + () => client.post( + path, + data: { + 'clear_history': false, + }, + ), + ).thenAnswer((_) async => successResponse(path, data: {})); final res = await channelApi.hideChannel(channelId, channelType); @@ -443,6 +517,33 @@ void main() { verifyNoMoreInteractions(client); }); + test('hideChannel with clear_history: true', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + + final path = '${_getChannelUrl(channelId, channelType)}/hide'; + + when( + () => client.post( + path, + data: { + 'clear_history': true, + }, + ), + ).thenAnswer((_) async => successResponse(path, data: {})); + + final res = await channelApi.hideChannel( + channelId, + channelType, + clearHistory: true, + ); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + test('showChannel', () async { const channelId = 'test-channel-id'; const channelType = 'test-channel-type'; @@ -467,8 +568,14 @@ void main() { final path = '${_getChannelUrl(channelId, channelType)}/read'; - when(() => client.post(path, data: any(named: 'data'))).thenAnswer( - (_) async => successResponse(path, data: {})); + when(() => client.post( + path, + data: { + 'message_id': messageId, + }, + )) + .thenAnswer( + (_) async => successResponse(path, data: {})); final res = await channelApi.markRead( channelId, diff --git a/packages/stream_chat/test/src/core/api/general_api_test.dart b/packages/stream_chat/test/src/core/api/general_api_test.dart index bf7a3018..afb3f6aa 100644 --- a/packages/stream_chat/test/src/core/api/general_api_test.dart +++ b/packages/stream_chat/test/src/core/api/general_api_test.dart @@ -35,10 +35,17 @@ void main() { final events = List.generate(3, (index) => Event(type: 'test-event-type-$index')); - when(() => client.post(path, data: any(named: 'data'))) - .thenAnswer((_) async => successResponse(path, data: { - 'events': [...events.map((it) => it.toJson())] - })); + final data = { + 'channel_cids': cids, + 'last_sync_at': lastSyncAt.toUtc().toIso8601String(), + }; + + when(() => client.post( + path, + data: data, + )).thenAnswer((_) async => successResponse(path, data: { + 'events': [...events.map((it) => it.toJson())] + })); final res = await generalApi.sync(cids, lastSyncAt); @@ -87,8 +94,20 @@ void main() { const path = '/search'; + final payload = jsonEncode({ + 'filter_conditions': filter, + 'sort': sort, + 'query': query, + ...pagination.toJson(), + }); + when( - () => client.get(path, queryParameters: any(named: 'queryParameters')), + () => client.get( + path, + queryParameters: { + 'payload': payload, + }, + ), ).thenAnswer((_) async => successResponse(path, data: {'results': []})); final res = await generalApi.searchMessages( @@ -115,8 +134,20 @@ void main() { const path = '/search'; + final payload = jsonEncode({ + 'filter_conditions': filter, + 'sort': sort, + 'message_filter_conditions': messageFilter, + ...pagination.toJson(), + }); + when( - () => client.get(path, queryParameters: any(named: 'queryParameters')), + () => client.get( + path, + queryParameters: { + 'payload': payload, + }, + ), ).thenAnswer((_) async => successResponse(path, data: {'results': []})); final res = await generalApi.searchMessages( From 1a817a3282d635448ac2350853e8eea64eec448b Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Sat, 26 Jun 2021 13:26:25 +0200 Subject: [PATCH 113/150] better device api tests --- .../test/src/core/api/device_api_test.dart | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/stream_chat/test/src/core/api/device_api_test.dart b/packages/stream_chat/test/src/core/api/device_api_test.dart index f12f04f5..7d4f59fd 100644 --- a/packages/stream_chat/test/src/core/api/device_api_test.dart +++ b/packages/stream_chat/test/src/core/api/device_api_test.dart @@ -26,8 +26,15 @@ void main() { const path = '/devices'; - when(() => client.post(path, data: any(named: 'data'))).thenAnswer( - (_) async => successResponse(path, data: {})); + when(() => client.post( + path, + data: { + 'id': deviceId, + 'push_provider': pushProvider.name, + }, + )) + .thenAnswer( + (_) async => successResponse(path, data: {})); final res = await deviceApi.addDevice(deviceId, pushProvider); @@ -69,7 +76,10 @@ void main() { const path = '/devices'; when( - () => client.delete(path, queryParameters: any(named: 'queryParameters')), + () => client.delete( + path, + queryParameters: {'id': deviceId}, + ), ).thenAnswer((_) async => successResponse(path, data: {})); final res = await deviceApi.removeDevice(deviceId); From ff8ff6d7ceefa616c4eae9a2e264c973e306d22c Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Sat, 26 Jun 2021 13:27:09 +0200 Subject: [PATCH 114/150] better guest api tests --- .../test/src/core/api/guest_api_test.dart | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/stream_chat/test/src/core/api/guest_api_test.dart b/packages/stream_chat/test/src/core/api/guest_api_test.dart index 6c6386fa..7db74b4a 100644 --- a/packages/stream_chat/test/src/core/api/guest_api_test.dart +++ b/packages/stream_chat/test/src/core/api/guest_api_test.dart @@ -26,11 +26,13 @@ void main() { const path = '/guest'; - when(() => client.post(path, data: any(named: 'data'))) - .thenAnswer((_) async => successResponse(path, data: { - 'access_token': accessToken, - 'user': user.toJson(), - })); + when(() => client.post( + path, + data: {'user': user}, + )).thenAnswer((_) async => successResponse(path, data: { + 'access_token': accessToken, + 'user': user.toJson(), + })); final res = await guestApi.getGuestUser(user); From 9857db0635bf58b13b5d8cbf77982130bbe91ab2 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Sat, 26 Jun 2021 13:37:40 +0200 Subject: [PATCH 115/150] better message api tests --- .../stream_chat/lib/src/client/channel.dart | 4 +- .../stream_chat/lib/src/client/client.dart | 4 +- .../lib/src/core/api/message_api.dart | 4 +- .../test/src/core/api/message_api_test.dart | 162 ++++++++++++++---- 4 files changed, 139 insertions(+), 35 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index e173c703..eee18356 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -989,11 +989,11 @@ class Channel { /// List the reactions for a message in the channel Future getReactions( String messageId, { - PaginationParams? options, + PaginationParams? pagination, }) => _client.getReactions( messageId, - options: options, + pagination: pagination, ); /// Retrieves a list of messages by ID diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index b579f030..93949df3 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -1149,11 +1149,11 @@ class StreamChatClient { /// Get all the reactions for a [messageId] Future getReactions( String messageId, { - PaginationParams? options, + PaginationParams? pagination, }) => _chatApi.message.getReactions( messageId, - options: options, + pagination: pagination, ); /// Update the given message diff --git a/packages/stream_chat/lib/src/core/api/message_api.dart b/packages/stream_chat/lib/src/core/api/message_api.dart index f50cb4a1..17269820 100644 --- a/packages/stream_chat/lib/src/core/api/message_api.dart +++ b/packages/stream_chat/lib/src/core/api/message_api.dart @@ -143,12 +143,12 @@ class MessageApi { /// Get all the reactions for a [messageId] Future getReactions( String messageId, { - PaginationParams? options, + PaginationParams? pagination, }) async { final response = await _client.get( '/messages/$messageId/reactions', queryParameters: { - if (options != null) ...options.toJson(), + if (pagination != null) ...pagination.toJson(), }, ); return QueryReactionsResponse.fromJson(response.data); diff --git a/packages/stream_chat/test/src/core/api/message_api_test.dart b/packages/stream_chat/test/src/core/api/message_api_test.dart index d7fa93cd..1aacb6d2 100644 --- a/packages/stream_chat/test/src/core/api/message_api_test.dart +++ b/packages/stream_chat/test/src/core/api/message_api_test.dart @@ -27,10 +27,15 @@ void main() { const path = '/channels/$channelType/$channelId/message'; - when(() => client.post(path, data: any(named: 'data'))) - .thenAnswer((_) async => successResponse(path, data: { - 'message': message.toJson(), - })); + when(() => client.post( + path, + data: { + 'message': message, + 'skip_push': false, + }, + )).thenAnswer((_) async => successResponse(path, data: { + 'message': message.toJson(), + })); final res = await messageApi.sendMessage(channelId, channelType, message); @@ -41,6 +46,37 @@ void main() { verifyNoMoreInteractions(client); }); + test('sendMessage with skipPush: true', () async { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + final message = Message(id: 'test-message-id', text: 'test-message-text'); + + const path = '/channels/$channelType/$channelId/message'; + + when(() => client.post( + path, + data: { + 'message': message, + 'skip_push': true, + }, + )).thenAnswer((_) async => successResponse(path, data: { + 'message': message.toJson(), + })); + + final res = await messageApi.sendMessage( + channelId, + channelType, + message, + skipPush: true, + ); + + expect(res, isNotNull); + expect(res.message.id, message.id); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + test('getMessagesById', () async { const channelId = 'test-channel-id'; const channelType = 'test-channel-type'; @@ -53,10 +89,12 @@ void main() { (index) => Message(id: 'test-message-id-$index'), ); - when(() => client.get(path, queryParameters: any(named: 'queryParameters'))) - .thenAnswer((_) async => successResponse(path, data: { - 'messages': [...messages.map((it) => it.toJson())], - })); + when(() => client.get( + path, + queryParameters: {'ids': messageIds.join(',')}, + )).thenAnswer((_) async => successResponse(path, data: { + 'messages': [...messages.map((it) => it.toJson())], + })); final res = await messageApi.getMessagesById( channelId, @@ -97,7 +135,10 @@ void main() { final path = '/messages/${message.id}'; - when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + when(() => client.post( + path, + data: {'message': message}, + )).thenAnswer( (_) async => successResponse(path, data: {'message': message.toJson()}), ); @@ -169,8 +210,17 @@ void main() { const path = '/messages/$messageId/action'; - when(() => client.post(path, data: any(named: 'data'))).thenAnswer( - (_) async => successResponse(path, data: {})); + when(() => client.post( + path, + data: { + 'id': channelId, + 'type': channelType, + 'form_data': formData, + 'message_id': messageId, + }, + )) + .thenAnswer( + (_) async => successResponse(path, data: {})); final res = await messageApi.sendAction( channelId, @@ -195,11 +245,17 @@ void main() { final message = Message(id: messageId); final reaction = Reaction(type: reactionType, messageId: messageId); - when(() => client.post(path, data: any(named: 'data'))) - .thenAnswer((_) async => successResponse(path, data: { - 'message': message.toJson(), - 'reaction': reaction.toJson(), - })); + when(() => client.post( + path, + data: { + 'reaction': Map.from(extraData) + ..addAll({'type': reactionType}), + 'enforce_unique': false, + }, + )).thenAnswer((_) async => successResponse(path, data: { + 'message': message.toJson(), + 'reaction': reaction.toJson(), + })); final res = await messageApi.sendReaction( messageId, @@ -216,6 +272,44 @@ void main() { verifyNoMoreInteractions(client); }); + test('sendReaction with enforceUnique: true', () async { + const messageId = 'test-message-id'; + const reactionType = 'test-reaction-type'; + const extraData = {'test-key': 'test-data'}; + + const path = '/messages/$messageId/reaction'; + + final message = Message(id: messageId); + final reaction = Reaction(type: reactionType, messageId: messageId); + + when(() => client.post( + path, + data: { + 'reaction': Map.from(extraData) + ..addAll({'type': reactionType}), + 'enforce_unique': true, + }, + )).thenAnswer((_) async => successResponse(path, data: { + 'message': message.toJson(), + 'reaction': reaction.toJson(), + })); + + final res = await messageApi.sendReaction( + messageId, + reactionType, + extraData: extraData, + enforceUnique: true, + ); + + expect(res, isNotNull); + expect(res.message.id, messageId); + expect(res.reaction.messageId, messageId); + expect(res.reaction.type, reactionType); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + test('deleteReaction', () async { const messageId = 'test-message-id'; const reactionType = 'test-reaction-type'; @@ -247,12 +341,16 @@ void main() { ), ); - when(() => client.get(path, queryParameters: any(named: 'queryParameters'))) - .thenAnswer((_) async => successResponse(path, data: { - 'reactions': [...reactions.map((it) => it.toJson())] - })); + when(() => client.get( + path, + queryParameters: { + ...const PaginationParams().toJson(), + }, + )).thenAnswer((_) async => successResponse(path, data: { + 'reactions': [...reactions.map((it) => it.toJson())] + })); - final res = await messageApi.getReactions(messageId, options: options); + final res = await messageApi.getReactions(messageId, pagination: options); expect(res, isNotNull); expect(res.reactions.length, reactions.length); @@ -277,10 +375,12 @@ void main() { language: translatedMessageText, }); - when(() => client.post(path, data: any(named: 'data'))) - .thenAnswer((_) async => successResponse(path, data: { - 'message': translatedMessage.toJson(), - })); + when(() => client.post( + path, + data: {'language': language}, + )).thenAnswer((_) async => successResponse(path, data: { + 'message': translatedMessage.toJson(), + })); final res = await messageApi.translateMessage(messageId, language); @@ -306,10 +406,14 @@ void main() { ), ); - when(() => client.get(path, queryParameters: any(named: 'queryParameters'))) - .thenAnswer((_) async => successResponse(path, data: { - 'messages': [...messages.map((it) => it.toJson())] - })); + when(() => client.get( + path, + queryParameters: { + ...options.toJson(), + }, + )).thenAnswer((_) async => successResponse(path, data: { + 'messages': [...messages.map((it) => it.toJson())] + })); final res = await messageApi.getReplies(parentId, options: options); From ddfe015513168270f0a8ccc815645b81a0a818f9 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Sat, 26 Jun 2021 13:41:41 +0200 Subject: [PATCH 116/150] better moderation api tests --- .../lib/src/core/http/stream_http_client.dart | 7 +- .../src/core/api/moderation_api_test.dart | 88 +++++++++++++++---- 2 files changed, 72 insertions(+), 23 deletions(-) diff --git a/packages/stream_chat/lib/src/core/http/stream_http_client.dart b/packages/stream_chat/lib/src/core/http/stream_http_client.dart index 4cf5b0e0..406138c3 100644 --- a/packages/stream_chat/lib/src/core/http/stream_http_client.dart +++ b/packages/stream_chat/lib/src/core/http/stream_http_client.dart @@ -3,18 +3,17 @@ import 'dart:async'; import 'package:dio/dio.dart'; import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; +import 'package:stream_chat/src/core/error/error.dart'; +import 'package:stream_chat/src/core/http/connection_id_manager.dart'; import 'package:stream_chat/src/core/http/interceptor/auth_interceptor.dart'; import 'package:stream_chat/src/core/http/interceptor/connection_id_interceptor.dart'; import 'package:stream_chat/src/core/http/interceptor/logging_interceptor.dart'; import 'package:stream_chat/src/core/http/stream_chat_dio_error.dart'; import 'package:stream_chat/src/core/http/token_manager.dart'; -import 'package:stream_chat/src/core/error/error.dart'; -import 'package:stream_chat/src/location.dart'; import 'package:stream_chat/src/core/platform_detector/platform_detector.dart'; +import 'package:stream_chat/src/location.dart'; import 'package:stream_chat/version.dart'; -import 'package:stream_chat/src/core/http/connection_id_manager.dart'; - part 'stream_http_client_options.dart'; /// This is where we configure the base url, headers, diff --git a/packages/stream_chat/test/src/core/api/moderation_api_test.dart b/packages/stream_chat/test/src/core/api/moderation_api_test.dart index fb2a1f29..69d85fd9 100644 --- a/packages/stream_chat/test/src/core/api/moderation_api_test.dart +++ b/packages/stream_chat/test/src/core/api/moderation_api_test.dart @@ -24,8 +24,12 @@ void main() { const path = '/moderation/mute'; - when(() => client.post(path, data: any(named: 'data'))).thenAnswer( - (_) async => successResponse(path, data: {})); + when( + () => client.post( + path, + data: {'target_id': userId}, + ), + ).thenAnswer((_) async => successResponse(path, data: {})); final res = await moderationApi.muteUser(userId); @@ -40,8 +44,12 @@ void main() { const path = '/moderation/unmute'; - when(() => client.post(path, data: any(named: 'data'))).thenAnswer( - (_) async => successResponse(path, data: {})); + when( + () => client.post( + path, + data: {'target_id': userId}, + ), + ).thenAnswer((_) async => successResponse(path, data: {})); final res = await moderationApi.unmuteUser(userId); @@ -57,8 +65,15 @@ void main() { const path = '/moderation/mute/channel'; - when(() => client.post(path, data: any(named: 'data'))).thenAnswer( - (_) async => successResponse(path, data: {})); + when(() => client.post( + path, + data: { + 'channel_cid': channelCid, + 'expiration': expiration.inMilliseconds, + }, + )) + .thenAnswer( + (_) async => successResponse(path, data: {})); final res = await moderationApi.muteChannel( channelCid, @@ -76,8 +91,12 @@ void main() { const path = '/moderation/unmute/channel'; - when(() => client.post(path, data: any(named: 'data'))).thenAnswer( - (_) async => successResponse(path, data: {})); + when(() => client.post( + path, + data: {'channel_cid': channelCid}, + )) + .thenAnswer( + (_) async => successResponse(path, data: {})); final res = await moderationApi.unmuteChannel(channelCid); @@ -92,8 +111,14 @@ void main() { const path = '/moderation/flag'; - when(() => client.post(path, data: any(named: 'data'))).thenAnswer( - (_) async => successResponse(path, data: {})); + when(() => client.post( + path, + data: { + 'target_message_id': messageId, + }, + )) + .thenAnswer( + (_) async => successResponse(path, data: {})); final res = await moderationApi.flagMessage(messageId); @@ -108,8 +133,14 @@ void main() { const path = '/moderation/unflag'; - when(() => client.post(path, data: any(named: 'data'))).thenAnswer( - (_) async => successResponse(path, data: {})); + when(() => client.post( + path, + data: { + 'target_message_id': messageId, + }, + )) + .thenAnswer( + (_) async => successResponse(path, data: {})); final res = await moderationApi.unflagMessage(messageId); @@ -124,8 +155,11 @@ void main() { const path = '/moderation/flag'; - when(() => client.post(path, data: any(named: 'data'))).thenAnswer( - (_) async => successResponse(path, data: {})); + when(() => client.post(path, data: { + 'target_user_id': userId, + })) + .thenAnswer( + (_) async => successResponse(path, data: {})); final res = await moderationApi.flagUser(userId); @@ -140,8 +174,14 @@ void main() { const path = '/moderation/unflag'; - when(() => client.post(path, data: any(named: 'data'))).thenAnswer( - (_) async => successResponse(path, data: {})); + when(() => client.post( + path, + data: { + 'target_user_id': userId, + }, + )) + .thenAnswer( + (_) async => successResponse(path, data: {})); final res = await moderationApi.unflagUser(userId); @@ -157,8 +197,12 @@ void main() { const path = '/moderation/ban'; - when(() => client.post(path, data: any(named: 'data'))).thenAnswer( - (_) async => successResponse(path, data: {})); + when(() => client.post(path, data: { + 'target_user_id': targetUserId, + ...options, + })) + .thenAnswer( + (_) async => successResponse(path, data: {})); final res = await moderationApi.banUser(targetUserId, options: options); @@ -175,7 +219,13 @@ void main() { const path = '/moderation/ban'; when( - () => client.delete(path, queryParameters: any(named: 'queryParameters')), + () => client.delete( + path, + queryParameters: { + 'target_user_id': targetUserId, + ...options, + }, + ), ).thenAnswer((_) async => successResponse(path, data: {})); final res = await moderationApi.unbanUser(targetUserId, options: options); From c7ea25c67268499b24fa7895f032ab3ff1ece048 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Sat, 26 Jun 2021 13:44:06 +0200 Subject: [PATCH 117/150] better user api tests --- .../test/src/core/api/user_api_test.dart | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/stream_chat/test/src/core/api/user_api_test.dart b/packages/stream_chat/test/src/core/api/user_api_test.dart index ebf0e343..6eecbee1 100644 --- a/packages/stream_chat/test/src/core/api/user_api_test.dart +++ b/packages/stream_chat/test/src/core/api/user_api_test.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:dio/dio.dart'; import 'package:mocktail/mocktail.dart'; import 'package:stream_chat/src/core/api/requests.dart'; @@ -32,10 +34,16 @@ void main() { final users = List.generate(3, (index) => User(id: 'test-user-id-$index')); - when(() => client.get(path, queryParameters: any(named: 'queryParameters'))) - .thenAnswer((_) async => successResponse(path, data: { - 'users': [...users.map((it) => it.toJson())] - })); + when(() => client.get(path, queryParameters: { + 'payload': jsonEncode({ + 'presence': presence, + 'sort': sort, + 'filter_conditions': filter, + ...pagination.toJson(), + }), + })).thenAnswer((_) async => successResponse(path, data: { + 'users': [...users.map((it) => it.toJson())] + })); final res = await userApi.queryUsers( presence: presence, @@ -60,8 +68,10 @@ void main() { final updatedUsers = {for (final user in users) user.id: user}; - when(() => client.post(path, data: any(named: 'data'))).thenAnswer( - (_) async => successResponse(path, data: { + when(() => client.post(path, data: { + 'users': updatedUsers, + })).thenAnswer((_) async => successResponse(path, + data: { 'users': updatedUsers .map((key, value) => MapEntry(key, value.toJson())) })); From d1f12e2f264ab0219b213bdf4e98af33aba1b9ad Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Sat, 26 Jun 2021 14:07:07 +0200 Subject: [PATCH 118/150] add cache in analyze --- .github/workflows/stream_flutter_workflow.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml index 3a6c1650..1ca294df 100644 --- a/.github/workflows/stream_flutter_workflow.yml +++ b/.github/workflows/stream_flutter_workflow.yml @@ -2,6 +2,7 @@ name: stream_flutter_workflow env: ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' + flutter_version: "2.2.2" on: pull_request: @@ -19,7 +20,14 @@ jobs: uses: actions/checkout@v2 with: fetch-depth: 0 + - name: Cache Flutter dependencies + id: flutter-cache + uses: actions/cache@v2 + with: + path: /opt/hostedtoolcache/flutter + key: ${{ env.flutter_version }}-flutter - name: "Install Flutter" + if: steps.flutter-cache.outputs.cache-hit != 'true' uses: subosito/flutter-action@v1 with: channel: 'stable' From e657b131ee27fca771603473f5a8441ff78ab335 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Sat, 26 Jun 2021 14:12:40 +0200 Subject: [PATCH 119/150] use flutter version in flutter action --- .github/workflows/stream_flutter_workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml index 1ca294df..69f6d989 100644 --- a/.github/workflows/stream_flutter_workflow.yml +++ b/.github/workflows/stream_flutter_workflow.yml @@ -30,7 +30,7 @@ jobs: if: steps.flutter-cache.outputs.cache-hit != 'true' uses: subosito/flutter-action@v1 with: - channel: 'stable' + flutter-version: ${{ env.flutter_version }} - name: "Install Tools" run: | flutter pub global activate melos From d4cb197086228c4092882213d5c2adc1b1c55017 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Sat, 26 Jun 2021 14:13:38 +0200 Subject: [PATCH 120/150] do not skip flutter step --- .github/workflows/stream_flutter_workflow.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml index 69f6d989..0285326a 100644 --- a/.github/workflows/stream_flutter_workflow.yml +++ b/.github/workflows/stream_flutter_workflow.yml @@ -21,13 +21,11 @@ jobs: with: fetch-depth: 0 - name: Cache Flutter dependencies - id: flutter-cache uses: actions/cache@v2 with: path: /opt/hostedtoolcache/flutter key: ${{ env.flutter_version }}-flutter - name: "Install Flutter" - if: steps.flutter-cache.outputs.cache-hit != 'true' uses: subosito/flutter-action@v1 with: flutter-version: ${{ env.flutter_version }} From 333f5a00d9cbbe6e9fc4bda3e0f43586b7ce56cb Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Sat, 26 Jun 2021 14:15:42 +0200 Subject: [PATCH 121/150] use cache in all jobs --- .github/workflows/stream_flutter_workflow.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml index 0285326a..d747f463 100644 --- a/.github/workflows/stream_flutter_workflow.yml +++ b/.github/workflows/stream_flutter_workflow.yml @@ -50,10 +50,15 @@ jobs: uses: actions/checkout@v2 with: fetch-depth: 0 + - name: Cache Flutter dependencies + uses: actions/cache@v2 + with: + path: /opt/hostedtoolcache/flutter + key: ${{ env.flutter_version }}-flutter - name: "Install Flutter" uses: subosito/flutter-action@v1 with: - channel: 'stable' + flutter-version: ${{ env.flutter_version }} - name: "Install Tools" run: flutter pub global activate melos - name: "Bootstrap Workspace" @@ -72,10 +77,15 @@ jobs: uses: actions/checkout@v2 with: fetch-depth: 0 + - name: Cache Flutter dependencies + uses: actions/cache@v2 + with: + path: /opt/hostedtoolcache/flutter + key: ${{ env.flutter_version }}-flutter - name: "Install Flutter" uses: subosito/flutter-action@v1 with: - channel: 'stable' + flutter-version: ${{ env.flutter_version }} - name: "Install Tools" run: | flutter pub global activate melos From 78da059d859919ab75e302c6b182c837b9e4ecf5 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Sat, 26 Jun 2021 14:22:20 +0200 Subject: [PATCH 122/150] use macos path for test --- .github/workflows/stream_flutter_workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml index d747f463..6817b269 100644 --- a/.github/workflows/stream_flutter_workflow.yml +++ b/.github/workflows/stream_flutter_workflow.yml @@ -80,7 +80,7 @@ jobs: - name: Cache Flutter dependencies uses: actions/cache@v2 with: - path: /opt/hostedtoolcache/flutter + path: /Users/runner/hostedtoolcache/flutter key: ${{ env.flutter_version }}-flutter - name: "Install Flutter" uses: subosito/flutter-action@v1 From ea8ee7ac6f3c852978846e492bfd2f845f70022e Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 28 Jun 2021 10:57:07 +0200 Subject: [PATCH 123/150] add userAvatarBuilder --- .../lib/src/message_widget.dart | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index de86873d..aed32a03 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -89,6 +89,7 @@ class MessageWidget extends StatefulWidget { this.onLinkTap, this.onMessageActions, this.onShowMessage, + this.userAvatarBuilder, this.editMessageInputBuilder, this.textBuilder, this.onReturnAction, @@ -274,6 +275,8 @@ class MessageWidget extends StatefulWidget { /// Function called on long press final void Function(BuildContext, Message)? onMessageActions; + final Widget Function(BuildContext, User)? userAvatarBuilder; + /// The message final Message message; @@ -448,6 +451,7 @@ class MessageWidget extends StatefulWidget { void Function(Message)? onMessageTap, List? customActions, void Function(Message message, Attachment attachment)? onAttachmentTap, + Widget Function(BuildContext, User)? userAvatarBuilder, }) => MessageWidget( key: key ?? this.key, @@ -506,6 +510,7 @@ class MessageWidget extends StatefulWidget { onMessageTap: onMessageTap ?? this.onMessageTap, customActions: customActions ?? this.customActions, onAttachmentTap: onAttachmentTap ?? this.onAttachmentTap, + userAvatarBuilder: userAvatarBuilder ?? this.userAvatarBuilder, ); @override @@ -1231,13 +1236,14 @@ class _MessageWidgetState extends State 2 : 0, ), - child: UserAvatar( - user: widget.message.user!, - onTap: widget.onUserAvatarTap, - constraints: widget.messageTheme.avatarTheme!.constraints, - borderRadius: widget.messageTheme.avatarTheme!.borderRadius, - showOnlineStatus: false, - ), + child: widget.userAvatarBuilder?.call(context, widget.message.user!) ?? + UserAvatar( + user: widget.message.user!, + onTap: widget.onUserAvatarTap, + constraints: widget.messageTheme.avatarTheme!.constraints, + borderRadius: widget.messageTheme.avatarTheme!.borderRadius, + showOnlineStatus: false, + ), ); Widget _buildTextBubble() { From b8fd33a629f4560edaf5b183fcdfd314fc290aa3 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 28 Jun 2021 11:54:23 +0200 Subject: [PATCH 124/150] fix new lines in list --- packages/stream_chat_flutter/lib/src/message_text.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/message_text.dart b/packages/stream_chat_flutter/lib/src/message_text.dart index e5abede0..6d006085 100644 --- a/packages/stream_chat_flutter/lib/src/message_text.dart +++ b/packages/stream_chat_flutter/lib/src/message_text.dart @@ -29,7 +29,7 @@ class MessageText extends StatelessWidget { @override Widget build(BuildContext context) { - final text = _replaceMentions(message.text ?? '').replaceAll('\n', '\\\n'); + final text = _replaceMentions(message.text ?? '').replaceAll('\n', '\n\n'); final themeData = Theme.of(context); return MarkdownBody( From 0e263ee26def9d75ab245406acf1f7d388887535 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 28 Jun 2021 12:31:43 +0200 Subject: [PATCH 125/150] update melos.yaml --- melos.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/melos.yaml b/melos.yaml index 1fb51c38..6232c3fa 100644 --- a/melos.yaml +++ b/melos.yaml @@ -56,7 +56,7 @@ scripts: dir-exists: test test:flutter: - run: melos exec -c 1 --fail-fast -- "flutter test --coverage" + run: melos exec -c 3 --fail-fast -- "flutter test --coverage" description: Run Flutter tests for a specific package in this project. select-package: flutter: true From 5912edc611a1065fbc79b953eaa77327fb4749f9 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 28 Jun 2021 12:32:58 +0200 Subject: [PATCH 126/150] add doc comment --- packages/stream_chat_flutter/lib/src/message_widget.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index aed32a03..7c95bf0a 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -275,6 +275,7 @@ class MessageWidget extends StatefulWidget { /// Function called on long press final void Function(BuildContext, Message)? onMessageActions; + /// Widget builder for building user avatar final Widget Function(BuildContext, User)? userAvatarBuilder; /// The message From e38381483a840e26d24879e14d90867ab395fc81 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 28 Jun 2021 16:03:33 +0530 Subject: [PATCH 127/150] deprecate baseUrl in StreamChatClient Signed-off-by: Sahil Kumar --- packages/stream_chat/lib/src/client/client.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 93949df3..9276ce22 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -65,7 +65,7 @@ class StreamChatClient { LogHandlerFunction? logHandlerFunction, RetryPolicy? retryPolicy, Location? location, - String? baseURL, + @Deprecated('Use location to change baseUrl instead') String? baseURL, Duration connectTimeout = const Duration(seconds: 6), Duration receiveTimeout = const Duration(seconds: 6), StreamChatApi? chatApi, From 5e7891e523bb77a93a3fd63524617b908962f5d6 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 28 Jun 2021 16:04:25 +0530 Subject: [PATCH 128/150] remove unused `showScrollToBottom` property from `MessageListCore` Signed-off-by: Sahil Kumar --- packages/stream_chat_flutter/lib/src/message_list_view.dart | 1 - .../stream_chat_flutter_core/lib/src/message_list_core.dart | 5 ----- 2 files changed, 6 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 74942259..f4ab2e01 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -368,7 +368,6 @@ class _MessageListViewState extends State { (context, list) => _buildListView(list), messageListController: _messageListController, parentMessage: widget.parentMessage, - showScrollToBottom: widget.showScrollToBottom, errorWidgetBuilder: widget.errorWidgetBuilder ?? (BuildContext context, Object error) => Center( child: Text( diff --git a/packages/stream_chat_flutter_core/lib/src/message_list_core.dart b/packages/stream_chat_flutter_core/lib/src/message_list_core.dart index 2a257fe6..2a259e83 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_list_core.dart @@ -68,7 +68,6 @@ class MessageListCore extends StatefulWidget { required this.emptyBuilder, required this.messageListBuilder, required this.errorWidgetBuilder, - this.showScrollToBottom = true, this.parentMessage, this.messageListController, this.messageFilter, @@ -94,10 +93,6 @@ class MessageListCore extends StatefulWidget { /// 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. - final bool showScrollToBottom; - /// If the current message belongs to a `thread`, this property represents the /// first message or the parent of the conversation. final Message? parentMessage; From 5e706ecba2fc610fc7538b4c9c056e9684b1e743 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 28 Jun 2021 16:05:10 +0530 Subject: [PATCH 129/150] chore: prepare stream_chat for v2.0.0-nullsafety.7 Signed-off-by: Sahil Kumar --- packages/stream_chat/CHANGELOG.md | 52 ++++++++++++++++++++++++++----- packages/stream_chat/pubspec.yaml | 2 +- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index a3bdc6ce..9fbd97bd 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,3 +1,35 @@ +## 2.0.0-nullsafety.7 + +ðŸ›‘ï¸ Breaking Changes from `2.0.0-nullsafety.6` +- `ConnectUserWithProvider` now requires `tokenProvider` as a required param. (Removed from the constructor) +- `client.disconnect()` is now divided into two different functions + - `client.closeConnection()` -> for closing user websocket connection. + - `client.disconnectUser()` -> for disconnecting user and resetting client state. +- `client.devToken()` now returns a `Token` model instead of `String`. +- `ApiError` is removed in favor of `StreamChatError` + - `StreamChatError` -> parent type for all the stream errors. + - `StreamWebSocketError` -> for user websocket related errors. + - `StreamChatNetworkError` -> for network related errors. +- `client.queryChannels()`, `channel.query()` options param is removed in favor of individual params + - `option.state` -> bool state + - `option.watch` -> bool watch + - `option.presence` -> bool presence + +✅ Added +- New `Location` enum is introduced for easily changing the client location/baseUrl. +- New `client.openConnection()` and `client.closeConnection()` is introduced to connect/disconnect user ws connection. + +🔄 Changed +- `baseURL` is now deprecated in favor of using `Location` to change data location. + +🞠Fixed +- Client does not return without internet connection [#369](https://github.com/GetStream/stream-chat-flutter/issues/369) + +## 2.0.0-nullsafety.6 + +- Fix thread reply not working with attachments +- Minor fixes + ## 2.0.0-nullsafety.6 - Fix thread reply not working with attachments @@ -57,7 +89,8 @@ - Save pinned messages in offline storage - Minor fixes - `StreamClient.QueryChannels` now returns a Stream and fetches the channels from storage before calling the api -- Added `StreamClient.QueryChannelsOnline` and `StreamClient.QueryChannelsOffline` to fetch channels only from online or offline +- Added `StreamClient.QueryChannelsOnline` and `StreamClient.QueryChannelsOffline` to fetch channels only from online or + offline ## 1.2.0-beta @@ -68,7 +101,8 @@ ## 1.1.0-beta - Fixed minor bugs -- Add support for custom attachment upload [docs here](https://getstream.io/chat/docs/flutter-dart/file_uploads/?language=dart) +- Add support for custom attachment + upload [docs here](https://getstream.io/chat/docs/flutter-dart/file_uploads/?language=dart) - Add support for asynchronous attachment upload ## 1.0.3-beta @@ -78,7 +112,8 @@ ## 1.0.2-beta -- Deprecated `setUser`, `setGuestUser`, `setUserWithProvider` in favor of `connectUser`, `connectGuestUser`, `connectUserWithProvider` +- Deprecated `setUser`, `setGuestUser`, `setUserWithProvider` in favor of `connectUser`, `connectGuestUser` + , `connectUserWithProvider` - Optimised reaction updates - i.e., Update first call Api later. ## 1.0.1-beta @@ -88,9 +123,11 @@ ## 1.0.0-beta - 🛑 **BREAKING** Renamed `Client` to less generic `StreamChatClient` -- 🛑 **BREAKING** Segregated the persistence layer into separate package [stream_chat_persistence](https://pub.dev/packages/stream_chat_persistence) +- 🛑 **BREAKING** Segregated the persistence layer into separate + package [stream_chat_persistence](https://pub.dev/packages/stream_chat_persistence) - 🛑 **BREAKING** Moved `Client.backgroundKeepAlive` to [core package](https://pub.dev/packages/stream_chat_core) -- 🛑 **BREAKING** Moved `Client.showLocalNotification` to [core package](https://pub.dev/packages/stream_chat_core) and renamed it to `StreamChatCore.onBackgroundEventReceived` +- 🛑 **BREAKING** Moved `Client.showLocalNotification` to [core package](https://pub.dev/packages/stream_chat_core) and + renamed it to `StreamChatCore.onBackgroundEventReceived` - Removed `flutter` dependency. This is now a pure Dart package 🥳 - Minor improvements and bugfixes @@ -145,7 +182,8 @@ ## 0.2.20 -- Return offline data only if the backend is unreachable. This avoids the glitch of the ChannelListView because we cannot sort by custom properties. +- Return offline data only if the backend is unreachable. This avoids the glitch of the ChannelListView because we + cannot sort by custom properties. ## 0.2.19 @@ -159,7 +197,7 @@ ## 0.2.17+1 -- Do not retry messages when server returns error +- Do not retry messages when server returns error ## 0.2.17 diff --git a/packages/stream_chat/pubspec.yaml b/packages/stream_chat/pubspec.yaml index 1b6b91ae..4901adf0 100644 --- a/packages/stream_chat/pubspec.yaml +++ b/packages/stream_chat/pubspec.yaml @@ -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: 2.0.0-nullsafety.6 +version: 2.0.0-nullsafety.7 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues From 05396a5bd2f0e6bfc4f49016daa635110553c044 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 28 Jun 2021 16:15:59 +0530 Subject: [PATCH 130/150] chore: prepare stream_chat_persistence for v2.0.0-nullsafety.7 Signed-off-by: Sahil Kumar --- packages/stream_chat_persistence/CHANGELOG.md | 5 +++++ packages/stream_chat_persistence/pubspec.yaml | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_persistence/CHANGELOG.md b/packages/stream_chat_persistence/CHANGELOG.md index aa7c7513..38742b1e 100644 --- a/packages/stream_chat_persistence/CHANGELOG.md +++ b/packages/stream_chat_persistence/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.0.0-nullsafety.7 + +* Update llc dependency +* Minor fixes and improvements + ## 2.0.0-nullsafety.5 * Update llc dependency diff --git a/packages/stream_chat_persistence/pubspec.yaml b/packages/stream_chat_persistence/pubspec.yaml index 9d141198..e5fb8fe6 100644 --- a/packages/stream_chat_persistence/pubspec.yaml +++ b/packages/stream_chat_persistence/pubspec.yaml @@ -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: 2.0.0-nullsafety.5 +version: 2.0.0-nullsafety.7 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -18,7 +18,7 @@ dependencies: path: ^1.8.0 path_provider: ^2.0.1 sqlite3_flutter_libs: ^0.5.0 - stream_chat: ^2.0.0-nullsafety.5 + stream_chat: ^2.0.0-nullsafety.7 dev_dependencies: build_runner: ^2.0.1 From d5cc22fd10c5f656383fb3d85f2c1a1136084a05 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 28 Jun 2021 16:35:05 +0530 Subject: [PATCH 131/150] chore(stream_chat): add queryUser breaking change in CHANGELOG.md Signed-off-by: Sahil Kumar --- packages/stream_chat/CHANGELOG.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 9fbd97bd..0c67f735 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -11,9 +11,11 @@ - `StreamWebSocketError` -> for user websocket related errors. - `StreamChatNetworkError` -> for network related errors. - `client.queryChannels()`, `channel.query()` options param is removed in favor of individual params - - `option.state` -> bool state - - `option.watch` -> bool watch - - `option.presence` -> bool presence + - `options.state` -> bool state + - `options.watch` -> bool watch + - `options.presence` -> bool presence +- `client.queryUsers()` options param is removed in favor of individual params + - `options.presence` -> bool presence ✅ Added - New `Location` enum is introduced for easily changing the client location/baseUrl. From 80217c9d587be3df95984c442e986ffe0b21affb Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 28 Jun 2021 16:38:23 +0530 Subject: [PATCH 132/150] chore(stream_chat): add queryUser breaking change in CHANGELOG.md Signed-off-by: Sahil Kumar --- packages/stream_chat_flutter_core/CHANGELOG.md | 10 ++++++++++ packages/stream_chat_flutter_core/pubspec.yaml | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index bd176c12..7af9b177 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,3 +1,13 @@ +## 2.0.0-nullsafety.8 + +ðŸ›‘ï¸ Breaking Changes from `2.0.0-nullsafety.7` +- `channelsBloc.queryChannels()`, `ChannelListCore` options param/property is removed in favor of individual params/properties + - `options.state` -> bool state + - `options.watch` -> bool watch + - `options.presence` -> bool presence +- `usersBloc.queryUsers()`, `UserListCore` options param/property is removed in favor of individual params/properties + - `options.presence` -> bool presence + ## 2.0.0-nullsafety.7 * Fixed a bug with connectivity implementation diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml index 14fa0f80..a231caab 100644 --- a/packages/stream_chat_flutter_core/pubspec.yaml +++ b/packages/stream_chat_flutter_core/pubspec.yaml @@ -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: 2.0.0-nullsafety.7 +version: 2.0.0-nullsafety.8 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -16,7 +16,7 @@ dependencies: sdk: flutter meta: ^1.3.0 rxdart: ^0.27.0 - stream_chat: ^2.0.0-nullsafety.6 + stream_chat: ^2.0.0-nullsafety.7 dev_dependencies: fake_async: ^1.2.0 From a3a88aead10d21dc33936a6bce8cb8e7d171f1af Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 28 Jun 2021 13:08:51 +0200 Subject: [PATCH 133/150] fix user list error handling --- .../stream_chat_flutter/lib/src/user_list_view.dart | 12 +++++------- .../lib/src/user_list_core.dart | 5 +++-- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/user_list_view.dart b/packages/stream_chat_flutter/lib/src/user_list_view.dart index aa1886b7..f915fcb0 100644 --- a/packages/stream_chat_flutter/lib/src/user_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/user_list_view.dart @@ -128,7 +128,7 @@ class UserListView extends StatefulWidget { final int crossAxisCount; /// The builder that will be used in case of error - final Widget Function(Error error)? errorBuilder; + final ErrorBuilder? errorBuilder; /// The builder that will be used to build the list final Widget Function(BuildContext context, List users)? @@ -153,8 +153,8 @@ class _UserListViewState extends State @override Widget build(BuildContext context) { final child = UserListCore( - errorBuilder: widget.errorBuilder as Widget Function(Object)? ?? - (err) => _buildError(err as Error), + errorBuilder: widget.errorBuilder ?? + (BuildContext context, Object err) => _buildError(err), emptyBuilder: widget.emptyBuilder ?? (context) => _buildEmpty(), loadingBuilder: widget.loadingBuilder ?? (context) => LayoutBuilder( @@ -194,12 +194,10 @@ class _UserListViewState extends State bool get isListAlreadySorted => widget.sort?.any((e) => e.field == 'name' && e.direction == 1) ?? false; - Widget _buildError(Error error) { - print(error.stackTrace); - + Widget _buildError(Object error) { var message = error.toString(); if (error is DioError) { - final dioError = error as DioError; + final dioError = error; if (dioError.type == DioErrorType.response) { message = dioError.message; } else { diff --git a/packages/stream_chat_flutter_core/lib/src/user_list_core.dart b/packages/stream_chat_flutter_core/lib/src/user_list_core.dart index 6377686b..e7deb356 100644 --- a/packages/stream_chat_flutter_core/lib/src/user_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/user_list_core.dart @@ -4,6 +4,7 @@ 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'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// /// [UserListCore] is a simplified class that allows fetching users while @@ -76,7 +77,7 @@ class UserListCore extends StatefulWidget { final UserListController? userListController; /// The builder that will be used in case of error - final Widget Function(Object error) errorBuilder; + final ErrorBuilder errorBuilder; /// The builder that will be used to build the list final Widget Function(BuildContext context, List users) listBuilder; @@ -170,7 +171,7 @@ class UserListCoreState extends State stream: _buildUserStream(), builder: (context, snapshot) { if (snapshot.hasError) { - return widget.errorBuilder(snapshot.error!); + return widget.errorBuilder(context, snapshot.error!); } if (!snapshot.hasData) { return widget.loadingBuilder(context); From bcd8630235cb2d20338f74c0308f14a55a0a2f8e Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 28 Jun 2021 16:40:43 +0530 Subject: [PATCH 134/150] chore(stream_chat): update version.dart Signed-off-by: Sahil Kumar --- packages/stream_chat/lib/version.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat/lib/version.dart b/packages/stream_chat/lib/version.dart index 3e6a44cd..cb3d587f 100644 --- a/packages/stream_chat/lib/version.dart +++ b/packages/stream_chat/lib/version.dart @@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart'; /// Current package version /// Used in [StreamChatClient] to build the `x-stream-client` header // ignore: constant_identifier_names -const PACKAGE_VERSION = '2.0.0-nullsafety.6'; +const PACKAGE_VERSION = '2.0.0-nullsafety.7'; From 6017a184d68b6b0dcd8ba506f247f9349ce5c430 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 28 Jun 2021 16:50:42 +0530 Subject: [PATCH 135/150] chore(stream_chat_flutter): prepare for v2.0.0-nullsafety.8 Signed-off-by: Sahil Kumar --- packages/stream_chat_flutter/CHANGELOG.md | 52 ++++++++++++------- .../lib/src/user_list_view.dart | 2 +- packages/stream_chat_flutter/pubspec.yaml | 4 +- .../lib/src/user_list_core.dart | 2 +- 4 files changed, 36 insertions(+), 24 deletions(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index ac8ae565..f007a0e6 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,3 +1,13 @@ +## 2.0.0-nullsafety.8 + +ðŸ›‘ï¸ Breaking Changes from `2.0.0-nullsafety.7` +- `ChannelListCore` options property is removed in favor of individual properties + - `options.state` -> bool state + - `options.watch` -> bool watch + - `options.presence` -> bool presence +- `UserListCore` options property is removed in favor of individual properties + - `options.presence` -> bool presence + ## 2.0.0-nullsafety.7 - Minor fixes and improvements @@ -139,7 +149,8 @@ ## 0.2.20+2 -- Added `shouldAddChannel` to ChannelsBloc in order to check if a channel has to be added to the list when a new message arrives +- Added `shouldAddChannel` to ChannelsBloc in order to check if a channel has to be added to the list when a new message + arrives ## 0.2.20+1 @@ -358,27 +369,27 @@ ## 0.2.1-alpha+1 -- Removed the additional `Navigator` in `StreamChat` widget. - It was added to make the app have the `StreamChat` widget as ancestor in every route. - Now the recommended way to add `StreamChat` to your app is using the `builder` property of your `MaterialApp` widget. - Otherwise you can use it in the usual way, but you need to add a `StreamChat` widget to every route of your app. - Read [this issue](https://github.com/GetStream/stream-chat-flutter/issues/47) for more information. +- Removed the additional `Navigator` in `StreamChat` widget. It was added to make the app have the `StreamChat` widget + as ancestor in every route. Now the recommended way to add `StreamChat` to your app is using the `builder` property of + your `MaterialApp` widget. Otherwise you can use it in the usual way, but you need to add a `StreamChat` widget to + every route of your app. Read [this issue](https://github.com/GetStream/stream-chat-flutter/issues/47) for more + information. ```dart @override - Widget build(BuildContext context) { - return MaterialApp( - theme: ThemeData.light(), - darkTheme: ThemeData.dark(), - themeMode: ThemeMode.system, - builder: (context, widget) { - return StreamChat( - child: widget, - client: client, - ); - }, - home: ChannelListPage(), - ); +Widget build(BuildContext context) { + return MaterialApp( + theme: ThemeData.light(), + darkTheme: ThemeData.dark(), + themeMode: ThemeMode.system, + builder: (context, widget) { + return StreamChat( + child: widget, + client: client, + ); + }, + home: ChannelListPage(), + ); ``` - Fix reaction bubble going below previous message on iOS @@ -462,7 +473,8 @@ - Add gesture (vertical drag down) to close the keyboard -- Add keyboard type parameters (set it to TextInputType.text to show the submit button that will even close the keyboard) +- Add keyboard type parameters (set it to TextInputType.text to show the submit button that will even close the + keyboard) The property showVideoFullScreen was added mainly because of this issue brianegan/chewie#261 diff --git a/packages/stream_chat_flutter/lib/src/user_list_view.dart b/packages/stream_chat_flutter/lib/src/user_list_view.dart index aa1886b7..58181774 100644 --- a/packages/stream_chat_flutter/lib/src/user_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/user_list_view.dart @@ -83,7 +83,7 @@ class UserListView extends StatefulWidget { /// Direction can be ascending or descending. final List? sort; - /// + /// If true you’ll receive user presence updates via the websocket events final bool? presence; /// Pagination parameters diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index bcebde99..342914d4 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -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: 2.0.0-nullsafety.7 +version: 2.0.0-nullsafety.8 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -36,7 +36,7 @@ dependencies: scrollable_positioned_list: ^0.2.0-nullsafety.0 share_plus: ^2.0.3 shimmer: ^2.0.0 - stream_chat_flutter_core: ^2.0.0-nullsafety.7 + stream_chat_flutter_core: ^2.0.0-nullsafety.8 substring_highlight: ^1.0.26 synchronized: ^3.0.0 url_launcher: ^6.0.3 diff --git a/packages/stream_chat_flutter_core/lib/src/user_list_core.dart b/packages/stream_chat_flutter_core/lib/src/user_list_core.dart index 6377686b..348e822e 100644 --- a/packages/stream_chat_flutter_core/lib/src/user_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/user_list_core.dart @@ -98,7 +98,7 @@ class UserListCore extends StatefulWidget { /// created_at or member_count. Direction can be ascending or descending. final List? sort; - /// + /// If true you’ll receive user presence updates via the websocket events final bool? presence; /// Pagination parameters From 8052edf09c37445134c090c5e06ed950f346f045 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 28 Jun 2021 15:20:50 +0200 Subject: [PATCH 136/150] fix tests --- .../test/user_list_core_test.dart | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/stream_chat_flutter_core/test/user_list_core_test.dart b/packages/stream_chat_flutter_core/test/user_list_core_test.dart index 6f21123f..95077e5c 100644 --- a/packages/stream_chat_flutter_core/test/user_list_core_test.dart +++ b/packages/stream_chat_flutter_core/test/user_list_core_test.dart @@ -43,7 +43,7 @@ void main() { listBuilder: (_, __) => Offstage(), loadingBuilder: (BuildContext context) => Offstage(), emptyBuilder: (BuildContext context) => Offstage(), - errorBuilder: (Object error) => Offstage(), + errorBuilder: (BuildContext context, Object error) => Offstage(), ); await tester.pumpWidget(userListCore); @@ -62,7 +62,7 @@ void main() { listBuilder: (_, __) => Offstage(), loadingBuilder: (BuildContext context) => Offstage(), emptyBuilder: (BuildContext context) => Offstage(), - errorBuilder: (Object error) => Offstage(), + errorBuilder: (BuildContext context, Object error) => Offstage(), ); final mockClient = MockClient(); @@ -91,7 +91,7 @@ void main() { listBuilder: (_, __) => Offstage(), loadingBuilder: (BuildContext context) => Offstage(), emptyBuilder: (BuildContext context) => Offstage(), - errorBuilder: (Object error) => Offstage(), + errorBuilder: (BuildContext context, Object error) => Offstage(), userListController: controller, ); @@ -125,7 +125,8 @@ void main() { listBuilder: (_, __) => Offstage(), loadingBuilder: (BuildContext context) => Offstage(), emptyBuilder: (BuildContext context) => Offstage(), - errorBuilder: (Object error) => Container(key: errorWidgetKey), + errorBuilder: (BuildContext context, Object error) => + Container(key: errorWidgetKey), ); final mockClient = MockClient(); @@ -170,7 +171,7 @@ void main() { listBuilder: (_, __) => Offstage(), loadingBuilder: (BuildContext context) => Offstage(), emptyBuilder: (BuildContext context) => Container(key: emptyWidgetKey), - errorBuilder: (Object error) => Offstage(), + errorBuilder: (BuildContext context, Object error) => Offstage(), ); final mockClient = MockClient(); @@ -215,7 +216,7 @@ void main() { listBuilder: (_, __) => Container(key: listWidgetKey), loadingBuilder: (BuildContext context) => Offstage(), emptyBuilder: (BuildContext context) => Offstage(), - errorBuilder: (Object error) => Offstage(), + errorBuilder: (BuildContext context, Object error) => Offstage(), ); final mockClient = MockClient(); @@ -273,7 +274,7 @@ void main() { ), loadingBuilder: (BuildContext context) => Offstage(), emptyBuilder: (BuildContext context) => Offstage(), - errorBuilder: (Object error) => Offstage(), + errorBuilder: (BuildContext context, Object error) => Offstage(), groupAlphabetically: true, ); @@ -341,7 +342,7 @@ void main() { ), loadingBuilder: (BuildContext context) => Offstage(), emptyBuilder: (BuildContext context) => Offstage(), - errorBuilder: (Object error) => Offstage(), + errorBuilder: (BuildContext context, Object error) => Offstage(), pagination: pagination, groupAlphabetically: true, ); @@ -446,7 +447,7 @@ void main() { ), loadingBuilder: (BuildContext context) => Offstage(), emptyBuilder: (BuildContext context) => Offstage(), - errorBuilder: (Object error) => Offstage(), + errorBuilder: (BuildContext context, Object error) => Offstage(), pagination: pagination.copyWith(limit: limit), groupAlphabetically: true, ); From 9641926f0700c24122f3518a4a2449e87b258987 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 28 Jun 2021 15:31:36 +0200 Subject: [PATCH 137/150] add golden test --- .../test/src/goldens/message_text.png | Bin 0 -> 10173 bytes .../test/src/message_text_test.dart | 55 ++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 packages/stream_chat_flutter/test/src/goldens/message_text.png diff --git a/packages/stream_chat_flutter/test/src/goldens/message_text.png b/packages/stream_chat_flutter/test/src/goldens/message_text.png new file mode 100644 index 0000000000000000000000000000000000000000..dda158b267997036a5c7573fc56fe52d0813412d GIT binary patch literal 10173 zcmeHNcT|(|n-ckbS^ckk|={bT=|oWt>8zI=JV_kEsUd$@l=N1b<*&?XGS zcnKP3^f8QU4*mSd1OHRg()|Gb;d0YgKZVh2ca6X=>)cKe41a`|&yQDs!?0g4!WlI~ z&&Y{R`-`EZa?Urt4{8q51j$d0dde0b5AI1&h_@VawANH)+)d`U*n8VdxIp$q?gAq* z;lmEcLp3`(@5ROshNy38#sz=m+9RBFtn=}+17{uw$9rO{s?$?#33uW>N849eiSH=O zYc1q@@6Lr{iyAQ%6?P_uz19tnz}@rr?;7>Ouv_*ak01ZiYKGs4VfD`Ic4HX#kBS(^ zQ^456i)&<%`b#?Vzih?kQih#$9e-qTc&#}qsF%GR!r1YT{CQd<CfBt;#WTrpJ_E-!DD9@x~Uaz-K4RWk=Z)cu#Aih|1E;?uV0^(wQJU( z85$ZUdd&{mPIZ+nv$CmK-jhY@gM)+Q-g^Q#LTG3xEU5AQBVkfEWx;lCxWTR|S)1GH zGKM*pD%REe`T5DZ^quh{lO<$jjo55oOyx$43vZd~B7*GQwy;_G>M z6qzxRk(q-vVfM2_pUJ~<0fv^AbacG4%(_@s%?D=1ldfvKM_Gs;8a#0g_7>RNt$ zLPEZEod}`vjo*ebQOi2_oW!q;=SoOP8E0FT4Gaz%P5BiU7B(+=sHxpf zbPF4uotX&<3p*enAb?-!aC7zW2nz}dDrYa-0zNGmu`{g$J165Zzo1~+yFdYZ=su_C@yFFOEDCRkc)5P#A5? z&)9kBVr-a|uZdIXH-Ag&8c91^*@R|jZl3w@;TD4aSW8xDL_|h>{N7HtIy`M)^}N+` zh2)loh6cN@IfLune!zO%iDQD~FewWhA%mFR(voE+OBywF=qfcyN=o7td12d>EF^0e z(%0WF>oI9QI51$-l4&wCJ8N+5S`xG;3%b_6A^t41f5H4@rz8Q|UY|tDw;A=X%{Zs5+tG~DM%1q04vm8#I{DzxiQ(5_C5^w2KK zg2mdPG_|4BYqq(}+XFg5LP=?b<{({OYAJg3=+X4Fv<83^90B3tP;FGke(i9Z=2Shj zcuSveX9>(M^U{SbZ+N_596|-^YQLzgeaj&)Z*TjKLKirMu)x5;>z|&*ah9_=`W3!w z1J3dYuCLWR6<1ZY+O6bn=;Kp*E=VM6VBqS~;^K#@sw#z)+9)MSadE}4nV3A^ zwKV`(MCH*g1q8k2SE&J&E^orR(Y-`OL|SKL;7jRJY?r5Z@7^u0xOk3cZeihc@srrc z%F2XVjpE|s7C|>c@ff{i)STGvyRtAv1Ynp8P+e84SRIsReLN5|{N?2pp`(}2l+7O) z!fv&HtsBa)YR|Jx;1xb`@W26egA^Uc{p|-3kFh4&7mRgzv0FSP zTVkpa#{rNI?fVZ7y%o!iUBc8hV6U+|Z~@mpSok~H+Bnw-iK@~|OG_17rA_lR^z`(2 zcMpXR3=AARbm)8lpD3L~fg*ID>_~%$w)}PxHf+=8%}vlKUX0n_?%&rlH;+t|tgyr| z^FW>rYgq+djof_7vEkuDghptA(CFw%a}I-OWxKY_LU-Q5&o6%Ts2)E*e}1h45Fr^5 zFCXw;QeyQR0^iremv4Lx8ftmEk}ryCef_$GqGH?9 z0${SNZ6gkp0D@ml026KPNBec7GqcN=HI+Ojn-dAahQ`K29o8TL5KXdIDmZ3vT-l^! z-9EFAlFvVn&oV0*<)be8y@wit!xzsyn}0WJ?bx(w6Hw@W0R9Z9L}+1s&@||F0Fru}1*A&H2N$QxS9IVEgyb9}RoILD zHD$FrOe<+Z?a>0KZu;=B%_Cts9e^SOBco8a6#(@$&>IZsYZ)M{&6Xt(9&E}oFVgRS z|EP(xTE`jgdE>_l=f++~oBEZ5gjo9)M7_zzTi5PFh*IJ)_4KAxZBvRa5!fyNTFp*S zKU%b#L!Shh)Qt4>$fKV=eKI_A<}PY4licfR9#dWU{;sWb3v&M>xw(O0-SqhQQ^InN z&HX{DbjvcbxRjI*KzL)Iba|>v|9b)rG@1-$d2aLGg?9p0Htf}7b=y?#B=>CwPWS76~nykc$Z(_=0>m? z8M!M;k)){=CKZZfCXMFh$?)!0G}I24S*qYJDjxkT*KJrAjfa{^k?Zy`01yeoDeT(n z@HOWw(id=2SD+Z}R^}&2I(AU#s2FBvCqWTiyLQd?&7Jk(3U0|y51S?TR|U0l2$t6! z1tMhapYA+tnh2#Yj>ElO>X6En<9T?js6&MkStZ?TI-rE=fu_gs zzh!4ILJ7($q-z-ipoI_;iq476J>`^^Wg`4 zI)HM}b2b)bV2wp`@=T{gbMx~+_$6?MEXU4b+rGE=t3Q5hKy{aI-#YVE8CyNiSf+A7 z>5ZhNL^m>HsC3{BUQreEinR%+e6QK-h=}jqyGNdDPWFApcoEG!`XU%uG4S^N31 zvu3YuG~D9iZqBvVh4Qc}_{N2)?k05&s(4OcLR7Ie;Pn0b_h(qFP!LK;vXVv`&sHqH zlXe_ZwkK8X_Ryd`2n>v>VK4dT_#?!t?0sBO!C7Aez`u_$`ZYJ@^5x5sHSmp5#BKC| zS>k3;mr+Fd+Mp*O1))&knQT`Vzrl9sL1*S)dh-Y`LhgGbe?e<;CLgl95He@s1^TyS!WSXK27!F$N;Jd0S`^k*?}AgwY0BTQdhy8TqjLc+?* z>XG2_bAYx6o}Qjj{TMdxdOs*gRai)f{N@k;XcMB5(FnMs+PYXL|GvJ<`8ExG51e+4 zhMW2NmLoHZN)(OGtzaN=~X+puth?NxEi=WgUgrW!2%22j0E*8wtqij4VsMWzy~tNe7Kl2 z77`rX6bpS*Xt96PB^s5FX-y%7z0Rxz8RRBqJAapQgA&3 zVy_Xfq&b5qy?*`rG@}e-Vx*6;@zZcw`((%jG-&SQU*Z5>LqkFi9z3XltS=G`RJISO z((}N+!4VM=zF;IJ0jjNhmy+1Cs+?R)=^fxta{n7gUx2d(If1;#f96v~@A*ueZSNoV z$fIc~b4$z^1l)V}>>=sIf*J)c9j;0(htwE5IdZ|s$cPxZ+L)vjN~7&1XzJ<` z7bZLNNn^&a*x{E|B25nkj~f~o^eY>Ks;P}s*hgr~vke9Xi)VIY*lGwvXf7w`SSMwn z)AZ%#dx#o9%jk4||T@`);_j z9#$|Gc@of)hgT@Ex!LgD+qX7@)uD=>)3N#n2HB(c51AKUfZLju`xFh;#bkSQQ;5iC zG?!Dn%a(>BfOkL+xkf*!0I+H+aEgHxDBErqWFqSMmaY}EwMzWL!WrP@O~GNcmgOr!R&>YM$vuz z88WsY9&HCcJRu&ZrUa=hBeZ<*=#de4@V$HY&Q(ih%XeSGuuCT3+DmSZ>?N3%k@KNQ zLLjREDUbnLji?+8&Oh3FQsLdZcgSBuJX8;Xg)wuUId#kG)s5${lPoAIEgDE*I4|6R z1NH#r$V!^#+a*;7@YS@pw|kkCf!A6d&B#Xi8t8OmNSdJinis$I|NXe4;|lB;njl>1 zkO(2%PD)HX2&MY<>(_jdx9{E!-r>A|`!-cQNCc^A$g~n5KuM|HxNiM=aqt*_dbz>L zNvVL!xQ>nvGXD1L6)0T;Bv?S7F~I0Th-UkdCT7=B_!0-SVc9s!$Y7v+qY+?K_FDCh zN!Jb7vz~3DqSO;IGIMwCG4~M-3=(E$TxFfQR>QYi8XG5rMg7p!H2a`E8p1Q!(c=E! zmFTvavq;^79IfefHqV&^wFLo{J6*|bXp2V?H|Faq!OC?3A|5ga{2) z;OT&^q1o9^W%GEbTL>DEQ~>@t1qT}z6JwRt?$}*!Q4=QJ`gqT2goL~QB{0@bc>FUK zlGx*;fbcR-UCL{K5u=?Y1rR|Bu}@)`Gq2E1e_se4%T{Mc>~zR8Ei1oras8ks2QHokNz_V_30rqQmll2?wM=``9l{QQ?I;YY9dC%QT9MOF6U zg9Zu&kU)p$Z;EY2fixrb_#AjBl+@AYYOL7$;QB^occ)}#o(G63Kotz!mQh@63c}pz z)Tul1@$ph6PWt*Zw1*Iz*uTETrAS%ML%7FT4C2&t*i#(Ef_4Z#P$Yco7IxUO^dbmH zf0A`lGkE7llj!jKh8v!}n0^l_VW+_!SMn#b{8u9}- zclO8K@JZyQV4-%6i3E_31M92U!wEr#@Kn&AuuTW|?md-VI`cp$ zQhtx9s3_9W0JgOg3Q8NW4vB(ys%B%2Nr5<9)+nQ`n3R#M6P1q8IWNzUHU~8$>Q&tKHB71bxs2jZIA~pz4uIK2>D9&0z`cfM4c7fvC=rl!mc@eD`mMAlMcZ6hy9A z?4yDs2%K{de~#0RI3X@AtqY;eQwVnASj5LERVfD>}IyE4D=3pq0=M( z^1Dqa*8_un`}XbX+S;c$3f%8I(E}!1EKhQ+!hKBX)aq=#4D{|BuX#G=at`ucE$e+XwIAS-y6vNRY*`#zHb zSW3rPvSy=D?EeV4Am?5<_Sb!qT! zK!80otV*KsdWQd0r=!!{kZM0NlxOM;QJ3AG+J?%iGMZ` zxa1%qpYw>Cgm%oqltDpsWpPo)^H^tzhlh%YvgZp3vPmGactiy-Y)oiQuoxl+xGo7M zB-P#B9P(!Z?IsLVG-+E!lv5!_v}(Nq7De07>4ANB`5Nn401lmQZIxMpV<-+{BJ+ZN zc%R`w3R{_?a=d{hP>2jR0;R(sEuC0-V00!rjn(+JY-z4wk5y>W5bz;4SdRY$EtJW*O!ChN24obS}#Rm5(0s$hU``9A#|Za57oOmWYGNp>DgR?SsO(Q&#E zQJ@5b1(-7IL|usx@$BJ3Q3x$0I+~7*v1y?*nwZWtYDtTO+W}$3GUbDs$cD%eG~Zcu z^|2_AE*at?m>M36}G*+5}>j)uwCrH0ZGuelE#^-tz=LF6fhcK z9kggDv1R5!A~jaF&;^somuS=hclq#nqJw{hi<{>|uk%4k$*~7xT#)k;?d!EPHDRpK z<(=&YGWK(GG0C1^KtPM;`z_E)zO|^RXao?F(U72?U5K8Mn$J@POie=+CLf0S2W1zL zaXMOBZuFYEIwBy@4?p}s|MTLm!7fmZFix-mOot)H6L4~%?vRs24+I1NP)URw@9$9` z#5aCCyeLqWE@5AWbAquz^dFU#gA)^!%9tYEstKX{rHkE)L$B9cw zXae1aANlnTE)B&hb4|KY*7HPDX_P+E5}_UJN1M||n^O3ofO>?A-d0t`@bdDOUFv;) z+&VHSNCYq8;o$*s^#Cmj1VI)AzEIi>^0Z_YhdBYJpZ@DIxVlyI)iSA#8#j`uFF$#K zZp?6t3Ro@EgbDlxGxWCr0ae#d`y{bAr7(dnboXIN{?}3a|DsR-Rqg)EdBR^80uKH6 zs&k1y! client.state).thenReturn(clientState); + when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => channel.lastMessageAt).thenReturn(lastMessageAt); + when(() => channel.state).thenReturn(channelState); + when(() => channel.client).thenReturn(client); + when(() => channel.isMuted).thenReturn(false); + when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); + when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ + 'name': 'test', + })); + when(() => channel.extraData).thenReturn({ + 'name': 'test', + }); + + final messageText = '''a message. +with multiple lines +and a list: +- a. okasd +- b lllll + +cool.'''; + + await tester.pumpWidgetBuilder( + materialAppWrapper()(SimpleFrame( + child: StreamChannel( + channel: channel, + child: Scaffold( + body: MessageText( + message: Message( + text: messageText, + ), + messageTheme: streamTheme.otherMessageTheme, + ), + ), + ), + )), + surfaceSize: const Size(500, 500), + ); + await screenMatchesGolden(tester, 'message_text'); + }, + ); } From bde90618947adc1493c468f0b73b7b66cdbde8d8 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 29 Jun 2021 10:10:47 +0200 Subject: [PATCH 138/150] update changelogs --- packages/stream_chat/CHANGELOG.md | 18 ++++++++---------- packages/stream_chat_flutter/CHANGELOG.md | 22 +++++++++++++++++++++- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 0c67f735..d68ef27f 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,6 +1,6 @@ ## 2.0.0-nullsafety.7 -ðŸ›‘ï¸ Breaking Changes from `2.0.0-nullsafety.6` +ðŸ›‘ï¸ Breaking Changes from `2.0.0-nullsafety.6` - `ConnectUserWithProvider` now requires `tokenProvider` as a required param. (Removed from the constructor) - `client.disconnect()` is now divided into two different functions - `client.closeConnection()` -> for closing user websocket connection. @@ -11,21 +11,19 @@ - `StreamWebSocketError` -> for user websocket related errors. - `StreamChatNetworkError` -> for network related errors. - `client.queryChannels()`, `channel.query()` options param is removed in favor of individual params - - `options.state` -> bool state - - `options.watch` -> bool watch - - `options.presence` -> bool presence -- `client.queryUsers()` options param is removed in favor of individual params - - `options.presence` -> bool presence + - `option.state` -> bool state + - `option.watch` -> bool watch + - `option.presence` -> bool presence -✅ Added +✅ Added - New `Location` enum is introduced for easily changing the client location/baseUrl. - New `client.openConnection()` and `client.closeConnection()` is introduced to connect/disconnect user ws connection. -🔄 Changed +🔄 Changed - `baseURL` is now deprecated in favor of using `Location` to change data location. -🞠Fixed -- Client does not return without internet connection [#369](https://github.com/GetStream/stream-chat-flutter/issues/369) +🞠Fixed +- [#369](https://github.com/GetStream/stream-chat-flutter/issues/369): Client does not return without internet connection ## 2.0.0-nullsafety.6 diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index f007a0e6..7955e22a 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,12 +1,32 @@ ## 2.0.0-nullsafety.8 -ðŸ›‘ï¸ Breaking Changes from `2.0.0-nullsafety.7` +ðŸ›‘ï¸ Breaking Changes from `2.0.0-nullsafety.7` - `ChannelListCore` options property is removed in favor of individual properties - `options.state` -> bool state - `options.watch` -> bool watch - `options.presence` -> bool presence - `UserListCore` options property is removed in favor of individual properties - `options.presence` -> bool presence +- `MessageBuilder` and `ParentMessageBuilder` signature is now +```dart +typedef MessageBuilder = Widget Function( + BuildContext, + MessageDetails, + List, + MessageWidget defaultMessageWidget, +); +``` +the last parameter is the default `MessageWidget` +You can call `.copyWith` to customize just a subset of properties + +✅ Added +- TypingIndicator now has a property called `parentId` to show typing indicator specific to threads +- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView header/footer + +🞠Fixed +- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text box when editing message +- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator use case +- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without a reload ## 2.0.0-nullsafety.7 From afb7b1f3ad7221b0c4f0132af03bf8a0f0296364 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 29 Jun 2021 10:15:08 +0200 Subject: [PATCH 139/150] update changelogs --- packages/stream_chat_flutter/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 7955e22a..98530bc2 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -27,6 +27,7 @@ You can call `.copyWith` to customize just a subset of properties - [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text box when editing message - Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator use case - [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without a reload +- `MessageListView` not rendering if the user is not a member of the channel ## 2.0.0-nullsafety.7 From cd4892683b0d0664b04afbe5d19af9f6d590c61b Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 29 Jun 2021 10:27:54 +0200 Subject: [PATCH 140/150] update changelogs --- packages/stream_chat_flutter/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 98530bc2..bb379565 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -22,6 +22,7 @@ You can call `.copyWith` to customize just a subset of properties ✅ Added - TypingIndicator now has a property called `parentId` to show typing indicator specific to threads - [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView header/footer +- `MessageWidget` accepts a `userAvatarBuilder` 🞠Fixed - [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text box when editing message From 29f234f69bdc2ef0890d313e47691b53e0e54844 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 29 Jun 2021 11:09:27 +0200 Subject: [PATCH 141/150] update error builder --- .../lib/src/user_list_view.dart | 63 +++++++------------ 1 file changed, 23 insertions(+), 40 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/user_list_view.dart b/packages/stream_chat_flutter/lib/src/user_list_view.dart index f915fcb0..85666413 100644 --- a/packages/stream_chat_flutter/lib/src/user_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/user_list_view.dart @@ -194,50 +194,33 @@ class _UserListViewState extends State bool get isListAlreadySorted => widget.sort?.any((e) => e.field == 'name' && e.direction == 1) ?? false; - Widget _buildError(Object error) { - var message = error.toString(); - if (error is DioError) { - final dioError = error; - if (dioError.type == DioErrorType.response) { - message = dioError.message; - } else { - message = 'Check your connection and retry'; - } - } - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text.rich( - const TextSpan( - children: [ - WidgetSpan( - child: Padding( - padding: EdgeInsets.only( - right: 2, + Widget _buildError(Object error) => Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text.rich( + const TextSpan( + children: [ + WidgetSpan( + child: Padding( + padding: EdgeInsets.only( + right: 2, + ), + child: Icon(Icons.error_outline), ), - child: Icon(Icons.error_outline), ), - ), - TextSpan(text: 'Error loading channels'), - ], + TextSpan(text: 'Error loading users'), + ], + ), + style: Theme.of(context).textTheme.headline6, ), - style: Theme.of(context).textTheme.headline6, - ), - Padding( - padding: const EdgeInsets.only( - top: 16, + TextButton( + onPressed: () => _userListController.loadData!(), + child: const Text('Retry'), ), - child: Text(message), - ), - TextButton( - onPressed: () => _userListController.loadData!(), - child: const Text('Retry'), - ), - ], - ), - ); - } + ], + ), + ); Widget _buildEmpty() => LayoutBuilder( builder: (context, viewportConstraints) => SingleChildScrollView( From 81b69e95bf1b9bea80470844a02a372994fa5753 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 29 Jun 2021 11:29:42 +0200 Subject: [PATCH 142/150] accept list controllers as params in ui package listviews --- .../lib/src/channel_list_view.dart | 10 +++++++++- .../lib/src/message_list_view.dart | 8 +++++++- .../lib/src/message_search_list_view.dart | 11 +++++++++-- .../stream_chat_flutter/lib/src/user_list_view.dart | 9 ++++++++- 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/channel_list_view.dart b/packages/stream_chat_flutter/lib/src/channel_list_view.dart index d131c33b..ddf3e753 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -89,6 +89,7 @@ class ChannelListView extends StatefulWidget { this.onMoreDetailsPressed, this.onDeletePressed, this.swipeActions, + this.channelListController, }) : super(key: key); /// If true a default swipe to action behaviour will be added to this widget @@ -187,6 +188,12 @@ class ChannelListView extends StatefulWidget { /// List of actions for slidable final List? swipeActions; + /// A [ChannelListController] allows reloading and pagination. + /// Use [ChannelListController.loadData] and + /// [ChannelListController.paginateData] respectively for reloading and + /// pagination. + final ChannelListController? channelListController; + @override _ChannelListViewState createState() => _ChannelListViewState(); } @@ -194,7 +201,8 @@ class ChannelListView extends StatefulWidget { class _ChannelListViewState extends State { final _slideController = SlidableController(); - final _channelListController = ChannelListController(); + late final _channelListController = + widget.channelListController ?? ChannelListController(); @override Widget build(BuildContext context) { diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 74942259..e5063228 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -172,6 +172,7 @@ class MessageListView extends StatefulWidget { this.usernameBuilder, this.showFloatingDateDivider = true, this.threadSeparatorBuilder, + this.messageListController, }) : super(key: key); /// Function used to build a custom message widget @@ -296,6 +297,10 @@ class MessageListView extends StatefulWidget { /// Builder used to build the thread separator in case it's a thread view final WidgetBuilder? threadSeparatorBuilder; + /// A [MessageListController] allows pagination. + /// Use [ChannelListController.paginateData] pagination. + final MessageListController? messageListController; + @override _MessageListViewState createState() => _MessageListViewState(); } @@ -347,7 +352,8 @@ class _MessageListViewState extends State { bool _inBetweenList = false; - final MessageListController _messageListController = MessageListController(); + late final MessageListController _messageListController = + widget.messageListController ?? MessageListController(); @override Widget build(BuildContext context) => MessageListCore( diff --git a/packages/stream_chat_flutter/lib/src/message_search_list_view.dart b/packages/stream_chat_flutter/lib/src/message_search_list_view.dart index 8b8a8dec..c27e2c90 100644 --- a/packages/stream_chat_flutter/lib/src/message_search_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_search_list_view.dart @@ -68,6 +68,7 @@ class MessageSearchListView extends StatefulWidget { this.errorBuilder, this.loadingBuilder, this.childBuilder, + this.messageSearchListController, }) : super(key: key); /// Message String to search on @@ -127,13 +128,19 @@ class MessageSearchListView extends StatefulWidget { /// The builder that will be used in case of loading final WidgetBuilder? loadingBuilder; + /// A [MessageSearchListController] allows reloading and pagination. + /// Use [MessageSearchListController.loadData] and + /// [MessageSearchListController.paginateData] respectively for reloading and + /// pagination. + final MessageSearchListController? messageSearchListController; + @override _MessageSearchListViewState createState() => _MessageSearchListViewState(); } class _MessageSearchListViewState extends State { - final MessageSearchListController _messageSearchListController = - MessageSearchListController(); + late final MessageSearchListController _messageSearchListController = + widget.messageSearchListController ?? MessageSearchListController(); @override Widget build(BuildContext context) => MessageSearchListCore( diff --git a/packages/stream_chat_flutter/lib/src/user_list_view.dart b/packages/stream_chat_flutter/lib/src/user_list_view.dart index 85666413..452cebfd 100644 --- a/packages/stream_chat_flutter/lib/src/user_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/user_list_view.dart @@ -64,6 +64,7 @@ class UserListView extends StatefulWidget { this.emptyBuilder, this.loadingBuilder, this.listBuilder, + this.userListController, }) : assert( crossAxisCount == 1 || groupAlphabetically == false, 'Cannot group alphabetically when crossAxisCount > 1', @@ -140,6 +141,11 @@ class UserListView extends StatefulWidget { /// The builder used when the channel list is empty. final WidgetBuilder? emptyBuilder; + /// A [UserListController] allows reloading and pagination. + /// Use [UserListController.loadData] and [UserListController.paginateData] + /// respectively for reloading and pagination. + final UserListController? userListController; + @override _UserListViewState createState() => _UserListViewState(); } @@ -148,7 +154,8 @@ class _UserListViewState extends State with WidgetsBindingObserver { bool get _isListView => widget.crossAxisCount == 1; - final UserListController _userListController = UserListController(); + late final UserListController _userListController = + widget.userListController ?? UserListController(); @override Widget build(BuildContext context) { From a9c7abb0d697ba52fa13fd3bec8e66ca243b25dd Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 29 Jun 2021 11:43:28 +0200 Subject: [PATCH 143/150] use getters --- packages/stream_chat_flutter/lib/src/channel_list_view.dart | 5 +++-- packages/stream_chat_flutter/lib/src/message_list_view.dart | 5 +++-- .../lib/src/message_search_list_view.dart | 5 +++-- packages/stream_chat_flutter/lib/src/user_list_view.dart | 5 +++-- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/channel_list_view.dart b/packages/stream_chat_flutter/lib/src/channel_list_view.dart index ddf3e753..86712c8a 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -201,8 +201,9 @@ class ChannelListView extends StatefulWidget { class _ChannelListViewState extends State { final _slideController = SlidableController(); - late final _channelListController = - widget.channelListController ?? ChannelListController(); + final _defaultController = ChannelListController(); + ChannelListController get _channelListController => + widget.channelListController ?? _defaultController; @override Widget build(BuildContext context) { diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index e5063228..ad068c4d 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -352,8 +352,9 @@ class _MessageListViewState extends State { bool _inBetweenList = false; - late final MessageListController _messageListController = - widget.messageListController ?? MessageListController(); + final _defaultController = MessageListController(); + MessageListController get _messageListController => + widget.messageListController ?? _defaultController; @override Widget build(BuildContext context) => MessageListCore( diff --git a/packages/stream_chat_flutter/lib/src/message_search_list_view.dart b/packages/stream_chat_flutter/lib/src/message_search_list_view.dart index c27e2c90..50c1d47f 100644 --- a/packages/stream_chat_flutter/lib/src/message_search_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_search_list_view.dart @@ -139,8 +139,9 @@ class MessageSearchListView extends StatefulWidget { } class _MessageSearchListViewState extends State { - late final MessageSearchListController _messageSearchListController = - widget.messageSearchListController ?? MessageSearchListController(); + final _defaultController = MessageSearchListController(); + MessageSearchListController get _messageSearchListController => + widget.messageSearchListController ?? _defaultController; @override Widget build(BuildContext context) => MessageSearchListCore( diff --git a/packages/stream_chat_flutter/lib/src/user_list_view.dart b/packages/stream_chat_flutter/lib/src/user_list_view.dart index 452cebfd..e027989e 100644 --- a/packages/stream_chat_flutter/lib/src/user_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/user_list_view.dart @@ -154,8 +154,9 @@ class _UserListViewState extends State with WidgetsBindingObserver { bool get _isListView => widget.crossAxisCount == 1; - late final UserListController _userListController = - widget.userListController ?? UserListController(); + final _defaultController = UserListController(); + UserListController get _userListController => + widget.userListController ?? _defaultController; @override Widget build(BuildContext context) { From 418c9ae64ec6976ce893378abdad4f58d016ea83 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 29 Jun 2021 11:45:59 +0200 Subject: [PATCH 144/150] use late --- packages/stream_chat_flutter/lib/src/channel_list_view.dart | 2 +- packages/stream_chat_flutter/lib/src/message_list_view.dart | 2 +- .../stream_chat_flutter/lib/src/message_search_list_view.dart | 2 +- packages/stream_chat_flutter/lib/src/user_list_view.dart | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/channel_list_view.dart b/packages/stream_chat_flutter/lib/src/channel_list_view.dart index 86712c8a..b14e0b75 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -201,7 +201,7 @@ class ChannelListView extends StatefulWidget { class _ChannelListViewState extends State { final _slideController = SlidableController(); - final _defaultController = ChannelListController(); + late final _defaultController = ChannelListController(); ChannelListController get _channelListController => widget.channelListController ?? _defaultController; diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index ad068c4d..e46f0d16 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -352,7 +352,7 @@ class _MessageListViewState extends State { bool _inBetweenList = false; - final _defaultController = MessageListController(); + late final _defaultController = MessageListController(); MessageListController get _messageListController => widget.messageListController ?? _defaultController; diff --git a/packages/stream_chat_flutter/lib/src/message_search_list_view.dart b/packages/stream_chat_flutter/lib/src/message_search_list_view.dart index 50c1d47f..1a67702f 100644 --- a/packages/stream_chat_flutter/lib/src/message_search_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_search_list_view.dart @@ -139,7 +139,7 @@ class MessageSearchListView extends StatefulWidget { } class _MessageSearchListViewState extends State { - final _defaultController = MessageSearchListController(); + late final _defaultController = MessageSearchListController(); MessageSearchListController get _messageSearchListController => widget.messageSearchListController ?? _defaultController; diff --git a/packages/stream_chat_flutter/lib/src/user_list_view.dart b/packages/stream_chat_flutter/lib/src/user_list_view.dart index e027989e..07b76f62 100644 --- a/packages/stream_chat_flutter/lib/src/user_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/user_list_view.dart @@ -154,7 +154,7 @@ class _UserListViewState extends State with WidgetsBindingObserver { bool get _isListView => widget.crossAxisCount == 1; - final _defaultController = UserListController(); + late final _defaultController = UserListController(); UserListController get _userListController => widget.userListController ?? _defaultController; From dde047b0e9bfdb1dcb5961faddc7dc1228c9fd9a Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 29 Jun 2021 12:00:40 +0200 Subject: [PATCH 145/150] fix message and user list core controller setup --- .../lib/src/message_search_list_core.dart | 20 +++++++++++++++---- .../lib/src/user_list_core.dart | 19 ++++++++++++++---- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart b/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart index a1eb5d68..636a879f 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart @@ -114,15 +114,22 @@ class MessageSearchListCoreState extends State { if (newMessageSearchBloc != _messageSearchBloc) { _messageSearchBloc = newMessageSearchBloc; loadData(); - if (widget.messageSearchListController != null) { - widget.messageSearchListController!.loadData = loadData; - widget.messageSearchListController!.paginateData = paginateData; - } } super.didChangeDependencies(); } + void _setupController() { + widget.messageSearchListController!.loadData = loadData; + widget.messageSearchListController!.paginateData = paginateData; + } + + @override + void initState() { + super.initState(); + _setupController(); + } + @override Widget build(BuildContext context) => _buildListView(_messageSearchBloc!); @@ -176,6 +183,11 @@ class MessageSearchListCoreState extends State { oldWidget.paginationParams?.toJson().toString()) { loadData(); } + + if (widget.messageSearchListController != + oldWidget.messageSearchListController) { + _setupController(); + } } } diff --git a/packages/stream_chat_flutter_core/lib/src/user_list_core.dart b/packages/stream_chat_flutter_core/lib/src/user_list_core.dart index e7deb356..05a8432d 100644 --- a/packages/stream_chat_flutter_core/lib/src/user_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/user_list_core.dart @@ -128,14 +128,21 @@ class UserListCoreState extends State if (newUsersBloc != _usersBloc) { _usersBloc = newUsersBloc; loadData(); - if (widget.userListController != null) { - widget.userListController!.loadData = loadData; - widget.userListController!.paginateData = paginateData; - } } super.didChangeDependencies(); } + @override + void initState() { + super.initState(); + _setupController(); + } + + void _setupController() { + widget.userListController!.loadData = loadData; + widget.userListController!.paginateData = paginateData; + } + @override Widget build(BuildContext context) => _buildListView(); @@ -212,6 +219,10 @@ class UserListCoreState extends State oldWidget.pagination?.toJson().toString()) { loadData(); } + + if (widget.userListController != oldWidget.userListController) { + _setupController(); + } } } From ba927bf37928b1beb7d96e0e037530aeefb4cb84 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 29 Jun 2021 12:20:37 +0200 Subject: [PATCH 146/150] add null check --- .../lib/src/message_search_list_core.dart | 6 ++++-- .../stream_chat_flutter_core/lib/src/user_list_core.dart | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart b/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart index 636a879f..d354daec 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart @@ -120,8 +120,10 @@ class MessageSearchListCoreState extends State { } void _setupController() { - widget.messageSearchListController!.loadData = loadData; - widget.messageSearchListController!.paginateData = paginateData; + if (widget.messageSearchListController != null) { + widget.messageSearchListController!.loadData = loadData; + widget.messageSearchListController!.paginateData = paginateData; + } } @override diff --git a/packages/stream_chat_flutter_core/lib/src/user_list_core.dart b/packages/stream_chat_flutter_core/lib/src/user_list_core.dart index 05a8432d..db6b3586 100644 --- a/packages/stream_chat_flutter_core/lib/src/user_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/user_list_core.dart @@ -139,8 +139,10 @@ class UserListCoreState extends State } void _setupController() { - widget.userListController!.loadData = loadData; - widget.userListController!.paginateData = paginateData; + if (widget.userListController != null) { + widget.userListController!.loadData = loadData; + widget.userListController!.paginateData = paginateData; + } } @override From be76d25aa308ad89dfe553b96c638cf68232ae9b Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 29 Jun 2021 16:20:29 +0530 Subject: [PATCH 147/150] update changelogs Signed-off-by: Sahil Kumar --- packages/stream_chat/CHANGELOG.md | 35 +++++++++++-------- packages/stream_chat_flutter/CHANGELOG.md | 26 ++++++++------ .../stream_chat_flutter_core/CHANGELOG.md | 14 ++++---- 3 files changed, 45 insertions(+), 30 deletions(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index d68ef27f..b82d3025 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,29 +1,36 @@ ## 2.0.0-nullsafety.7 -ðŸ›‘ï¸ Breaking Changes from `2.0.0-nullsafety.6` +ðŸ›‘ï¸ Breaking Changes from `2.0.0-nullsafety.6` + - `ConnectUserWithProvider` now requires `tokenProvider` as a required param. (Removed from the constructor) -- `client.disconnect()` is now divided into two different functions - - `client.closeConnection()` -> for closing user websocket connection. - - `client.disconnectUser()` -> for disconnecting user and resetting client state. +- `client.disconnect()` is now divided into two different functions + - `client.closeConnection()` -> for closing user websocket connection. + - `client.disconnectUser()` -> for disconnecting user and resetting client state. - `client.devToken()` now returns a `Token` model instead of `String`. - `ApiError` is removed in favor of `StreamChatError` - - `StreamChatError` -> parent type for all the stream errors. - - `StreamWebSocketError` -> for user websocket related errors. - - `StreamChatNetworkError` -> for network related errors. + - `StreamChatError` -> parent type for all the stream errors. + - `StreamWebSocketError` -> for user websocket related errors. + - `StreamChatNetworkError` -> for network related errors. - `client.queryChannels()`, `channel.query()` options param is removed in favor of individual params - - `option.state` -> bool state - - `option.watch` -> bool watch - - `option.presence` -> bool presence + - `option.state` -> bool state + - `option.watch` -> bool watch + - `option.presence` -> bool presence +- `client.queryUsers()` options param is removed in favor of individual params + - `option.presence` -> bool presence + +✅ Added -✅ Added - New `Location` enum is introduced for easily changing the client location/baseUrl. - New `client.openConnection()` and `client.closeConnection()` is introduced to connect/disconnect user ws connection. -🔄 Changed +🔄 Changed + - `baseURL` is now deprecated in favor of using `Location` to change data location. -🞠Fixed -- [#369](https://github.com/GetStream/stream-chat-flutter/issues/369): Client does not return without internet connection +🞠Fixed + +- [#369](https://github.com/GetStream/stream-chat-flutter/issues/369): Client does not return without internet + connection ## 2.0.0-nullsafety.6 diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index bb379565..3407178c 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,31 +1,37 @@ ## 2.0.0-nullsafety.8 -ðŸ›‘ï¸ Breaking Changes from `2.0.0-nullsafety.7` +ðŸ›‘ï¸ Breaking Changes from `2.0.0-nullsafety.7` + - `ChannelListCore` options property is removed in favor of individual properties - `options.state` -> bool state - `options.watch` -> bool watch - `options.presence` -> bool presence -- `UserListCore` options property is removed in favor of individual properties +- `UserListView` options property is removed in favor of individual properties - `options.presence` -> bool presence - `MessageBuilder` and `ParentMessageBuilder` signature is now + ```dart typedef MessageBuilder = Widget Function( - BuildContext, - MessageDetails, - List, - MessageWidget defaultMessageWidget, -); + BuildContext, + MessageDetails, + List, + MessageWidget defaultMessageWidget, + ); ``` + the last parameter is the default `MessageWidget` You can call `.copyWith` to customize just a subset of properties -✅ Added +✅ Added + - TypingIndicator now has a property called `parentId` to show typing indicator specific to threads - [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView header/footer - `MessageWidget` accepts a `userAvatarBuilder` -🞠Fixed -- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text box when editing message +🞠Fixed + +- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text box when editing + message - Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator use case - [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without a reload - `MessageListView` not rendering if the user is not a member of the channel diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index 7af9b177..4453659e 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,13 +1,15 @@ ## 2.0.0-nullsafety.8 ðŸ›‘ï¸ Breaking Changes from `2.0.0-nullsafety.7` -- `channelsBloc.queryChannels()`, `ChannelListCore` options param/property is removed in favor of individual params/properties - - `options.state` -> bool state - - `options.watch` -> bool watch - - `options.presence` -> bool presence + +- `channelsBloc.queryChannels()`, `ChannelListCore` options param/property is removed in favor of individual + params/properties + - `options.state` -> bool state + - `options.watch` -> bool watch + - `options.presence` -> bool presence - `usersBloc.queryUsers()`, `UserListCore` options param/property is removed in favor of individual params/properties - - `options.presence` -> bool presence - + - `options.presence` -> bool presence + ## 2.0.0-nullsafety.7 * Fixed a bug with connectivity implementation From bc0a70ad2530f967b0b10f61d75936cccedd5946 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 29 Jun 2021 16:12:51 +0200 Subject: [PATCH 148/150] rename colors --- .../example/lib/tutorial-part-3.dart | 4 +- .../example/lib/tutorial-part-6.dart | 4 +- .../lib/src/attachment/attachment_title.dart | 2 +- .../attachment_upload_state_builder.dart | 10 +- .../lib/src/attachment/attachment_widget.dart | 8 +- .../lib/src/attachment/file_attachment.dart | 25 +- .../lib/src/attachment/giphy_attachment.dart | 31 +-- .../lib/src/attachment/image_attachment.dart | 4 +- .../lib/src/attachment_actions_modal.dart | 24 +- .../lib/src/back_button.dart | 2 +- .../lib/src/channel_bottom_sheet.dart | 16 +- .../lib/src/channel_image.dart | 12 +- .../lib/src/channel_info.dart | 2 +- .../lib/src/channel_list_header.dart | 6 +- .../lib/src/channel_list_view.dart | 32 +-- .../lib/src/date_divider.dart | 2 +- .../lib/src/deleted_message.dart | 4 +- .../lib/src/group_image.dart | 4 +- .../lib/src/image_footer.dart | 13 +- .../lib/src/image_header.dart | 4 +- .../lib/src/info_tile.dart | 4 +- .../lib/src/media_list_view.dart | 11 +- .../lib/src/mention_tile.dart | 4 +- .../lib/src/message_actions_modal.dart | 18 +- .../lib/src/message_input.dart | 129 +++++----- .../lib/src/message_list_view.dart | 16 +- .../lib/src/message_reactions_modal.dart | 4 +- .../lib/src/message_search_list_view.dart | 6 +- .../lib/src/message_widget.dart | 18 +- .../lib/src/option_list_tile.dart | 4 +- .../lib/src/quoted_message_widget.dart | 4 +- .../lib/src/reaction_bubble.dart | 4 +- .../lib/src/reaction_picker.dart | 2 +- .../lib/src/sending_indicator.dart | 2 +- .../lib/src/stream_chat.dart | 4 +- .../lib/src/stream_chat_theme.dart | 231 +++++++++--------- .../lib/src/swipeable.dart | 5 +- .../lib/src/system_message.dart | 2 +- .../lib/src/upload_progress_indicator.dart | 2 +- .../lib/src/url_attachment.dart | 4 +- .../lib/src/user_avatar.dart | 8 +- .../lib/src/user_item.dart | 6 +- .../lib/src/user_list_view.dart | 8 +- .../stream_chat_flutter/lib/src/utils.dart | 17 +- .../lib/src/video_thumbnail_image.dart | 4 +- .../example/lib/main.dart | 8 +- 46 files changed, 379 insertions(+), 355 deletions(-) diff --git a/packages/stream_chat_flutter/example/lib/tutorial-part-3.dart b/packages/stream_chat_flutter/example/lib/tutorial-part-3.dart index fc55f6d9..6789a69d 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial-part-3.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial-part-3.dart @@ -101,12 +101,12 @@ class ChannelListPage extends StatelessWidget { StreamChatTheme.of(context).channelPreviewTheme.title!.copyWith( color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(opacity), ), ), subtitle: Text(subtitle), - trailing: channel.state!.unreadCount! > 0 + trailing: channel.state!.unreadCount > 0 ? CircleAvatar( radius: 10, child: Text(channel.state!.unreadCount.toString()), diff --git a/packages/stream_chat_flutter/example/lib/tutorial-part-6.dart b/packages/stream_chat_flutter/example/lib/tutorial-part-6.dart index c49ffaf1..e4f5b8a5 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial-part-6.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial-part-6.dart @@ -50,9 +50,9 @@ class MyApp extends StatelessWidget { ), ), otherMessageTheme: MessageTheme( - messageBackgroundColor: colorTheme.black, + messageBackgroundColor: colorTheme.textHighEmphasis, messageText: TextStyle( - color: colorTheme.white, + color: colorTheme.barsBg, ), avatarTheme: AvatarTheme( borderRadius: BorderRadius.circular(8), diff --git a/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart b/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart index ba369e5d..b068a8f9 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart @@ -35,7 +35,7 @@ class AttachmentTitle extends StatelessWidget { attachment.title!, overflow: TextOverflow.ellipsis, style: messageTheme.messageText?.copyWith( - color: StreamChatTheme.of(context).colorTheme.accentBlue, + color: StreamChatTheme.of(context).colorTheme.accentPrimary, fontWeight: FontWeight.bold, ), ), diff --git a/packages/stream_chat_flutter/lib/src/attachment/attachment_upload_state_builder.dart b/packages/stream_chat_flutter/lib/src/attachment/attachment_upload_state_builder.dart index 825def58..b1b29c4c 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/attachment_upload_state_builder.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/attachment_upload_state_builder.dart @@ -129,7 +129,7 @@ class _PreparingState extends StatelessWidget { alignment: Alignment.topRight, child: _IconButton( icon: StreamSvgIcon.close( - color: StreamChatTheme.of(context).colorTheme.white, + color: StreamChatTheme.of(context).colorTheme.barsBg, ), onPressed: () => channel.cancelAttachmentUpload(attachmentId), ), @@ -169,7 +169,7 @@ class _InProgressState extends StatelessWidget { alignment: Alignment.topRight, child: _IconButton( icon: StreamSvgIcon.close( - color: StreamChatTheme.of(context).colorTheme.white, + color: StreamChatTheme.of(context).colorTheme.barsBg, ), onPressed: () => channel.cancelAttachmentUpload(attachmentId), ), @@ -208,7 +208,7 @@ class _FailedState extends StatelessWidget { children: [ _IconButton( icon: StreamSvgIcon.retry( - color: theme.colorTheme.white, + color: theme.colorTheme.barsBg, ), onPressed: () { channel.retryAttachmentUpload(messageId, attachmentId); @@ -228,7 +228,7 @@ class _FailedState extends StatelessWidget { child: Text( 'UPLOAD ERROR', style: theme.textTheme.footnote.copyWith( - color: theme.colorTheme.white, + color: theme.colorTheme.barsBg, ), ), ), @@ -247,7 +247,7 @@ class _SuccessState extends StatelessWidget { backgroundColor: StreamChatTheme.of(context).colorTheme.overlayDark, maxRadius: 12, child: StreamSvgIcon.check( - color: StreamChatTheme.of(context).colorTheme.white, + color: StreamChatTheme.of(context).colorTheme.barsBg, ), ), ); diff --git a/packages/stream_chat_flutter/lib/src/attachment/attachment_widget.dart b/packages/stream_chat_flutter/lib/src/attachment/attachment_widget.dart index ee79d959..e92087b5 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/attachment_widget.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/attachment_widget.dart @@ -75,12 +75,14 @@ class AttachmentError extends StatelessWidget { child: Container( width: size?.width, height: size?.height, - color: - StreamChatTheme.of(context).colorTheme.accentRed.withOpacity(.1), + color: StreamChatTheme.of(context) + .colorTheme + .accentError + .withOpacity(.1), child: Center( child: Icon( Icons.error_outline, - color: StreamChatTheme.of(context).colorTheme.black, + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis, ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart index 7ec5fb04..99c3ec40 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart @@ -54,10 +54,10 @@ class FileAttachment extends AttachmentWidget { width: size?.width ?? 100, height: 56, decoration: BoxDecoration( - color: colorTheme.white, + color: colorTheme.barsBg, borderRadius: BorderRadius.circular(12), border: Border.all( - color: colorTheme.greyWhisper, + color: colorTheme.borders, ), ), child: Row( @@ -141,8 +141,8 @@ class FileAttachment extends AttachmentWidget { final colorTheme = StreamChatTheme.of(context).colorTheme; return Shimmer.fromColors( - baseColor: colorTheme.greyGainsboro, - highlightColor: colorTheme.whiteSmoke, + baseColor: colorTheme.disabled, + highlightColor: colorTheme.inputBg, child: image, ); }, @@ -217,7 +217,7 @@ class FileAttachment extends AttachmentWidget { preparing: () => Padding( padding: const EdgeInsets.all(8), child: _buildButton( - icon: StreamSvgIcon.close(color: theme.colorTheme.white), + icon: StreamSvgIcon.close(color: theme.colorTheme.barsBg), fillColor: theme.colorTheme.overlayDark, onPressed: () => channel.cancelAttachmentUpload(attachmentId), ), @@ -225,7 +225,7 @@ class FileAttachment extends AttachmentWidget { inProgress: (_, __) => Padding( padding: const EdgeInsets.all(8), child: _buildButton( - icon: StreamSvgIcon.close(color: theme.colorTheme.white), + icon: StreamSvgIcon.close(color: theme.colorTheme.barsBg), fillColor: theme.colorTheme.overlayDark, onPressed: () => channel.cancelAttachmentUpload(attachmentId), ), @@ -233,15 +233,15 @@ class FileAttachment extends AttachmentWidget { success: () => Padding( padding: const EdgeInsets.all(8), child: CircleAvatar( - backgroundColor: theme.colorTheme.accentBlue, + backgroundColor: theme.colorTheme.accentPrimary, maxRadius: 12, - child: StreamSvgIcon.check(color: theme.colorTheme.white), + child: StreamSvgIcon.check(color: theme.colorTheme.barsBg), ), ), failed: (_) => Padding( padding: const EdgeInsets.all(8), child: _buildButton( - icon: StreamSvgIcon.retry(color: theme.colorTheme.white), + icon: StreamSvgIcon.retry(color: theme.colorTheme.barsBg), fillColor: theme.colorTheme.overlayDark, onPressed: () => channel.retryAttachmentUpload( message.id, @@ -253,7 +253,8 @@ class FileAttachment extends AttachmentWidget { if (message.status == MessageSendingStatus.sent) { trailingWidget = IconButton( - icon: StreamSvgIcon.cloudDownload(color: theme.colorTheme.black), + icon: StreamSvgIcon.cloudDownload( + color: theme.colorTheme.textHighEmphasis), visualDensity: VisualDensity.compact, splashRadius: 16, onPressed: () { @@ -272,7 +273,7 @@ class FileAttachment extends AttachmentWidget { final theme = StreamChatTheme.of(context); final size = attachment.file?.size ?? attachment.extraData['file_size']; final textStyle = theme.textTheme.footnote.copyWith( - color: theme.colorTheme.grey, + color: theme.colorTheme.textLowEmphasis, ); return attachment.uploadState.when( preparing: () => Text(fileSize(size), style: textStyle), @@ -282,7 +283,7 @@ class FileAttachment extends AttachmentWidget { showBackground: false, padding: EdgeInsets.zero, textStyle: textStyle, - progressIndicatorColor: theme.colorTheme.accentBlue, + progressIndicatorColor: theme.colorTheme.accentPrimary, ), success: () => Text(fileSize(size), style: textStyle), failed: (_) => Text('UPLOAD ERROR', style: textStyle), diff --git a/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart index 1572061e..f14abffc 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart @@ -51,7 +51,7 @@ class GiphyAttachment extends AttachmentWidget { mainAxisSize: MainAxisSize.min, children: [ Card( - color: StreamChatTheme.of(context).colorTheme.white, + color: StreamChatTheme.of(context).colorTheme.barsBg, elevation: 2, clipBehavior: Clip.hardEdge, shape: const RoundedRectangleBorder( @@ -83,7 +83,7 @@ class GiphyAttachment extends AttachmentWidget { style: TextStyle( color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(0.5), ), overflow: TextOverflow.ellipsis, @@ -117,7 +117,7 @@ class GiphyAttachment extends AttachmentWidget { Container( color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(0.2), width: double.infinity, height: 0.5, @@ -141,7 +141,7 @@ class GiphyAttachment extends AttachmentWidget { .copyWith( color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(0.5), ), ), @@ -152,7 +152,7 @@ class GiphyAttachment extends AttachmentWidget { width: 0.5, color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(0.2), height: 50, ), @@ -173,7 +173,7 @@ class GiphyAttachment extends AttachmentWidget { .copyWith( color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(0.5), ), maxLines: 1, @@ -185,7 +185,7 @@ class GiphyAttachment extends AttachmentWidget { width: 0.5, color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(0.2), height: 50, ), @@ -203,7 +203,7 @@ class GiphyAttachment extends AttachmentWidget { style: TextStyle( color: StreamChatTheme.of(context) .colorTheme - .accentBlue, + .accentPrimary, fontWeight: FontWeight.bold, ), ), @@ -226,7 +226,7 @@ class GiphyAttachment extends AttachmentWidget { StreamSvgIcon.eye( color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(0.5), size: 16, ), @@ -241,7 +241,7 @@ class GiphyAttachment extends AttachmentWidget { .copyWith( color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(0.5)), ), ], @@ -306,8 +306,8 @@ class GiphyAttachment extends AttachmentWidget { final colorTheme = StreamChatTheme.of(context).colorTheme; return Shimmer.fromColors( - baseColor: colorTheme.greyGainsboro, - highlightColor: colorTheme.whiteSmoke, + baseColor: colorTheme.disabled, + highlightColor: colorTheme.inputBg, child: image, ); }, @@ -322,7 +322,7 @@ class GiphyAttachment extends AttachmentWidget { child: Material( color: StreamChatTheme.of(context) .colorTheme - .black + .textHighEmphasis .withOpacity(.5), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), @@ -335,13 +335,14 @@ class GiphyAttachment extends AttachmentWidget { child: Row( children: [ StreamSvgIcon.lightning( - color: StreamChatTheme.of(context).colorTheme.white, + color: StreamChatTheme.of(context).colorTheme.barsBg, size: 16, ), Text( 'GIPHY', style: TextStyle( - color: StreamChatTheme.of(context).colorTheme.white, + color: + StreamChatTheme.of(context).colorTheme.barsBg, fontWeight: FontWeight.bold, fontSize: 11, ), diff --git a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart index c677435a..20174567 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart @@ -103,8 +103,8 @@ class ImageAttachment extends AttachmentWidget { ); final colorTheme = StreamChatTheme.of(context).colorTheme; return Shimmer.fromColors( - baseColor: colorTheme.greyGainsboro, - highlightColor: colorTheme.whiteSmoke, + baseColor: colorTheme.disabled, + highlightColor: colorTheme.inputBg, child: image, ); }, diff --git a/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart b/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart index 363fac45..ea189b91 100644 --- a/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart @@ -72,7 +72,7 @@ class AttachmentActionsModal extends StatelessWidget { 'Reply', StreamSvgIcon.iconCurveLineLeftUp( size: 24, - color: theme.colorTheme.grey, + color: theme.colorTheme.textLowEmphasis, ), () { Navigator.pop(context, ReturnActionType.reply); @@ -83,7 +83,7 @@ class AttachmentActionsModal extends StatelessWidget { 'Show in Chat', StreamSvgIcon.eye( size: 24, - color: theme.colorTheme.black, + color: theme.colorTheme.textHighEmphasis, ), onShowMessage, ), @@ -93,7 +93,7 @@ class AttachmentActionsModal extends StatelessWidget { 'Save ${message.attachments[currentIndex].type == 'video' ? 'Video' : 'Image'}', StreamSvgIcon.iconSave( size: 24, - color: theme.colorTheme.grey, + color: theme.colorTheme.textLowEmphasis, ), () { final attachment = message.attachments[currentIndex]; @@ -144,7 +144,7 @@ class AttachmentActionsModal extends StatelessWidget { 'Delete', StreamSvgIcon.delete( size: 24, - color: theme.colorTheme.accentRed, + color: theme.colorTheme.accentError, ), () { final channel = StreamChannel.of(context).channel; @@ -165,7 +165,7 @@ class AttachmentActionsModal extends StatelessWidget { ..maybePop(); } }, - color: theme.colorTheme.accentRed, + color: theme.colorTheme.accentError, ), ] .map((e) => Align( @@ -175,7 +175,7 @@ class AttachmentActionsModal extends StatelessWidget { .insertBetween( Container( height: 1, - color: theme.colorTheme.greyWhisper, + color: theme.colorTheme.borders, ), ), ), @@ -196,7 +196,7 @@ class AttachmentActionsModal extends StatelessWidget { }) => Material( key: key, - color: StreamChatTheme.of(context).colorTheme.white, + color: StreamChatTheme.of(context).colorTheme.barsBg, child: InkWell( onTap: onTap, child: Padding( @@ -241,7 +241,7 @@ class AttachmentActionsModal extends StatelessWidget { width: 182, decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), - color: theme.colorTheme.white, + color: theme.colorTheme.barsBg, ), child: Center( child: progress == null @@ -249,7 +249,7 @@ class AttachmentActionsModal extends StatelessWidget { height: 100, width: 100, child: StreamSvgIcon.error( - color: theme.colorTheme.greyGainsboro, + color: theme.colorTheme.disabled, ), ) : progress.toProgressIndicatorValue == 1.0 @@ -258,7 +258,7 @@ class AttachmentActionsModal extends StatelessWidget { height: 160, width: 160, child: StreamSvgIcon.check( - color: theme.colorTheme.greyGainsboro, + color: theme.colorTheme.disabled, ), ) : SizedBox( @@ -271,14 +271,14 @@ class AttachmentActionsModal extends StatelessWidget { value: progress.toProgressIndicatorValue, strokeWidth: 8, valueColor: AlwaysStoppedAnimation( - theme.colorTheme.accentBlue, + theme.colorTheme.accentPrimary, ), ), Center( child: Text( '${progress.toPercentage}%', style: theme.textTheme.headline.copyWith( - color: theme.colorTheme.grey, + color: theme.colorTheme.textLowEmphasis, ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/back_button.dart b/packages/stream_chat_flutter/lib/src/back_button.dart index e8fe7e31..6bd3e0cc 100644 --- a/packages/stream_chat_flutter/lib/src/back_button.dart +++ b/packages/stream_chat_flutter/lib/src/back_button.dart @@ -44,7 +44,7 @@ class StreamBackButton extends StatelessWidget { padding: const EdgeInsets.all(14), child: StreamSvgIcon.left( size: 24, - color: StreamChatTheme.of(context).colorTheme.black, + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis, ), ), if (showUnreads) diff --git a/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart b/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart index eaabb7e2..368bfbd8 100644 --- a/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart +++ b/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart @@ -32,7 +32,7 @@ class _ChannelBottomSheetState extends State { final isOwner = userAsMember.role == 'owner'; return Material( - color: _streamChatThemeData.colorTheme.white, + color: _streamChatThemeData.colorTheme.barsBg, clipBehavior: Clip.antiAlias, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( @@ -146,7 +146,7 @@ class _ChannelBottomSheetState extends State { leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: StreamSvgIcon.user( - color: _streamChatThemeData.colorTheme.grey, + color: _streamChatThemeData.colorTheme.textLowEmphasis, ), ), title: 'View Info', @@ -157,7 +157,7 @@ class _ChannelBottomSheetState extends State { leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: StreamSvgIcon.userRemove( - color: _streamChatThemeData.colorTheme.grey, + color: _streamChatThemeData.colorTheme.textLowEmphasis, ), ), title: 'Leave Group', @@ -176,11 +176,11 @@ class _ChannelBottomSheetState extends State { leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: StreamSvgIcon.delete( - color: _streamChatThemeData.colorTheme.accentRed, + color: _streamChatThemeData.colorTheme.accentError, ), ), title: 'Delete Conversation', - titleColor: _streamChatThemeData.colorTheme.accentRed, + titleColor: _streamChatThemeData.colorTheme.accentError, onTap: () async { setState(() { _showActions = false; @@ -195,7 +195,7 @@ class _ChannelBottomSheetState extends State { leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: StreamSvgIcon.closeSmall( - color: _streamChatThemeData.colorTheme.grey, + color: _streamChatThemeData.colorTheme.textLowEmphasis, ), ), title: 'Cancel', @@ -224,7 +224,7 @@ class _ChannelBottomSheetState extends State { question: 'Are you sure you want to delete this conversation?', cancelText: 'CANCEL', icon: StreamSvgIcon.delete( - color: _streamChatThemeData.colorTheme.accentRed, + color: _streamChatThemeData.colorTheme.accentError, ), ); final channel = _streamChannelState.channel; @@ -242,7 +242,7 @@ class _ChannelBottomSheetState extends State { question: 'Are you sure you want to leave this conversation?', cancelText: 'CANCEL', icon: StreamSvgIcon.userRemove( - color: _streamChatThemeData.colorTheme.accentRed, + color: _streamChatThemeData.colorTheme.accentError, ), ); if (res == true) { diff --git a/packages/stream_chat_flutter/lib/src/channel_image.dart b/packages/stream_chat_flutter/lib/src/channel_image.dart index d3e028ee..9ac07033 100644 --- a/packages/stream_chat_flutter/lib/src/channel_image.dart +++ b/packages/stream_chat_flutter/lib/src/channel_image.dart @@ -110,8 +110,8 @@ class ChannelImage extends StatelessWidget { .channelPreviewTheme.avatarTheme?.constraints, onTap: onTap != null ? (_) => onTap!() : null, selected: selected, - selectionColor: - selectionColor ?? chatThemeData.colorTheme.accentBlue, + selectionColor: selectionColor ?? + chatThemeData.colorTheme.accentPrimary, selectionThickness: selectionThickness, )); } else { @@ -132,7 +132,7 @@ class ChannelImage extends StatelessWidget { onTap: onTap, selected: selected, selectionColor: - selectionColor ?? chatThemeData.colorTheme.accentBlue, + selectionColor ?? chatThemeData.colorTheme.accentPrimary, selectionThickness: selectionThickness, ); } @@ -144,7 +144,7 @@ class ChannelImage extends StatelessWidget { constraints: constraints ?? chatThemeData.channelPreviewTheme.avatarTheme?.constraints, decoration: BoxDecoration( - color: chatThemeData.colorTheme.accentBlue, + color: chatThemeData.colorTheme.accentPrimary, ), child: Stack( alignment: Alignment.center, @@ -157,7 +157,7 @@ class ChannelImage extends StatelessWidget { child: Text( data.containsKey('name') ? data['name'][0] : '', style: TextStyle( - color: chatThemeData.colorTheme.white, + color: chatThemeData.colorTheme.barsBg, fontWeight: FontWeight.bold, ), ), @@ -189,7 +189,7 @@ class ChannelImage extends StatelessWidget { child: Container( constraints: constraints ?? chatThemeData.ownMessageTheme.avatarTheme?.constraints, - color: selectionColor ?? chatThemeData.colorTheme.accentBlue, + color: selectionColor ?? chatThemeData.colorTheme.accentPrimary, child: Padding( padding: EdgeInsets.all(selectionThickness), child: child, diff --git a/packages/stream_chat_flutter/lib/src/channel_info.dart b/packages/stream_chat_flutter/lib/src/channel_info.dart index 07920b95..3be26ec8 100644 --- a/packages/stream_chat_flutter/lib/src/channel_info.dart +++ b/packages/stream_chat_flutter/lib/src/channel_info.dart @@ -143,7 +143,7 @@ class ChannelInfo extends StatelessWidget { child: Text( 'Try Again', style: textStyle?.copyWith( - color: StreamChatTheme.of(context).colorTheme.accentBlue, + color: StreamChatTheme.of(context).colorTheme.accentPrimary, ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/channel_list_header.dart b/packages/stream_chat_flutter/lib/src/channel_list_header.dart index 5a4275a6..0db31f80 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_header.dart @@ -154,7 +154,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { Color? color; switch (status) { case ConnectionStatus.connected: - color = chatThemeData.colorTheme.accentBlue; + color = chatThemeData.colorTheme.accentPrimary; break; case ConnectionStatus.connecting: color = Colors.grey; @@ -209,7 +209,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { return Text( 'Stream Chat', style: chatThemeData.textTheme.headlineBold.copyWith( - color: chatThemeData.colorTheme.black, + color: chatThemeData.colorTheme.textHighEmphasis, ), ); } @@ -262,7 +262,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { style: chatThemeData.channelListHeaderTheme.title?.copyWith( fontSize: 16, fontWeight: FontWeight.bold, - color: chatThemeData.colorTheme.accentBlue, + color: chatThemeData.colorTheme.accentPrimary, ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/channel_list_view.dart b/packages/stream_chat_flutter/lib/src/channel_list_view.dart index b14e0b75..316d7513 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -290,7 +290,7 @@ class _ChannelListViewState extends State { padding: const EdgeInsets.all(8), child: StreamSvgIcon.message( size: 136, - color: chatThemeData.colorTheme.greyGainsboro, + color: chatThemeData.colorTheme.disabled, ), ), Padding( @@ -309,7 +309,7 @@ class _ChannelListViewState extends State { 'How about sending your first message to a friend?', textAlign: TextAlign.center, style: chatThemeData.textTheme.body.copyWith( - color: chatThemeData.colorTheme.grey, + color: chatThemeData.colorTheme.textLowEmphasis, ), ), ), @@ -327,7 +327,7 @@ class _ChannelListViewState extends State { child: Text( 'Start a chat', style: chatThemeData.textTheme.bodyBold.copyWith( - color: chatThemeData.colorTheme.accentBlue, + color: chatThemeData.colorTheme.accentPrimary, ), ), ), @@ -362,8 +362,8 @@ class _ChannelListViewState extends State { final chatThemeData = StreamChatTheme.of(context); if (widget.crossAxisCount > 1) { return Shimmer.fromColors( - baseColor: chatThemeData.colorTheme.greyGainsboro, - highlightColor: chatThemeData.colorTheme.whiteSmoke, + baseColor: chatThemeData.colorTheme.disabled, + highlightColor: chatThemeData.colorTheme.inputBg, child: Column( children: [ const SizedBox(height: 4), @@ -391,12 +391,12 @@ class _ChannelListViewState extends State { ); } else { return Shimmer.fromColors( - baseColor: chatThemeData.colorTheme.greyGainsboro, - highlightColor: chatThemeData.colorTheme.whiteSmoke, + baseColor: chatThemeData.colorTheme.disabled, + highlightColor: chatThemeData.colorTheme.inputBg, child: ListTile( leading: Container( decoration: BoxDecoration( - color: chatThemeData.colorTheme.white, + color: chatThemeData.colorTheme.barsBg, shape: BoxShape.circle, ), constraints: const BoxConstraints.tightFor( @@ -412,7 +412,7 @@ class _ChannelListViewState extends State { alignment: Alignment.centerLeft, child: Container( decoration: BoxDecoration( - color: chatThemeData.colorTheme.white, + color: chatThemeData.colorTheme.barsBg, borderRadius: BorderRadius.circular(11), ), constraints: const BoxConstraints.tightFor( @@ -429,7 +429,7 @@ class _ChannelListViewState extends State { alignment: Alignment.centerLeft, child: Container( decoration: BoxDecoration( - color: chatThemeData.colorTheme.white, + color: chatThemeData.colorTheme.barsBg, borderRadius: BorderRadius.circular(11), ), constraints: const BoxConstraints.expand( @@ -441,7 +441,7 @@ class _ChannelListViewState extends State { Container( margin: const EdgeInsets.only(left: 16), decoration: BoxDecoration( - color: chatThemeData.colorTheme.white, + color: chatThemeData.colorTheme.barsBg, borderRadius: BorderRadius.circular(11), ), constraints: const BoxConstraints.tightFor( @@ -488,7 +488,7 @@ class _ChannelListViewState extends State { final channelsBloc = ChannelsBloc.of(context); final onTap = _getChannelTap(context); final chatThemeData = StreamChatTheme.of(context); - final backgroundColor = chatThemeData.colorTheme.whiteSmoke; + final backgroundColor = chatThemeData.colorTheme.inputBg; if (i < channels.length) { final channel = channels[i]; @@ -549,7 +549,7 @@ class _ChannelListViewState extends State { IconSlideAction( color: backgroundColor, iconWidget: StreamSvgIcon.delete( - color: chatThemeData.colorTheme.accentRed, + color: chatThemeData.colorTheme.accentError, ), onTap: widget.onDeletePressed != null ? () { @@ -565,7 +565,7 @@ class _ChannelListViewState extends State { 'Are you sure you want to delete this conversation?', cancelText: 'CANCEL', icon: StreamSvgIcon.delete( - color: chatThemeData.colorTheme.accentRed, + color: chatThemeData.colorTheme.accentError, ), ); if (res == true) { @@ -576,7 +576,7 @@ class _ChannelListViewState extends State { ], child: DecoratedBox( decoration: BoxDecoration( - color: chatThemeData.colorTheme.whiteSnow, + color: chatThemeData.colorTheme.appBg, ), child: widget.channelPreviewBuilder?.call(context, channel) ?? ChannelPreview( @@ -664,7 +664,7 @@ class _ChannelListViewState extends State { errorBuilder: (context, err) => Container( color: StreamChatTheme.of(context) .colorTheme - .accentRed + .accentError .withOpacity(.2), child: const Padding( padding: EdgeInsets.symmetric(vertical: 16), diff --git a/packages/stream_chat_flutter/lib/src/date_divider.dart b/packages/stream_chat_flutter/lib/src/date_divider.dart index 185683be..8fff2ade 100644 --- a/packages/stream_chat_flutter/lib/src/date_divider.dart +++ b/packages/stream_chat_flutter/lib/src/date_divider.dart @@ -55,7 +55,7 @@ class DateDivider extends StatelessWidget { child: Text( dayInfo, style: chatThemeData.textTheme.footnote.copyWith( - color: chatThemeData.colorTheme.white, + color: chatThemeData.colorTheme.barsBg, ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/deleted_message.dart b/packages/stream_chat_flutter/lib/src/deleted_message.dart index 9a4e0ae2..011b0c72 100644 --- a/packages/stream_chat_flutter/lib/src/deleted_message.dart +++ b/packages/stream_chat_flutter/lib/src/deleted_message.dart @@ -39,8 +39,8 @@ class DeletedMessage extends StatelessWidget { side: borderSide ?? BorderSide( color: Theme.of(context).brightness == Brightness.dark - ? chatThemeData.colorTheme.white.withAlpha(24) - : chatThemeData.colorTheme.black.withAlpha(24), + ? chatThemeData.colorTheme.barsBg.withAlpha(24) + : chatThemeData.colorTheme.textHighEmphasis.withAlpha(24), ), ), child: Padding( diff --git a/packages/stream_chat_flutter/lib/src/group_image.dart b/packages/stream_chat_flutter/lib/src/group_image.dart index c6778a91..3513bda2 100644 --- a/packages/stream_chat_flutter/lib/src/group_image.dart +++ b/packages/stream_chat_flutter/lib/src/group_image.dart @@ -51,7 +51,7 @@ class GroupImage extends StatelessWidget { constraints: constraints ?? streamChatTheme.ownMessageTheme.avatarTheme?.constraints, decoration: BoxDecoration( - color: streamChatTheme.colorTheme.accentBlue, + color: streamChatTheme.colorTheme.accentPrimary, ), child: Flex( direction: Axis.vertical, @@ -119,7 +119,7 @@ class GroupImage extends StatelessWidget { BorderRadius.zero) + BorderRadius.circular(selectionThickness), child: Container( - color: selectionColor ?? streamChatTheme.colorTheme.accentBlue, + color: selectionColor ?? streamChatTheme.colorTheme.accentPrimary, height: 64, width: 64, child: Padding( diff --git a/packages/stream_chat_flutter/lib/src/image_footer.dart b/packages/stream_chat_flutter/lib/src/image_footer.dart index 0a6571cc..4b32afd8 100644 --- a/packages/stream_chat_flutter/lib/src/image_footer.dart +++ b/packages/stream_chat_flutter/lib/src/image_footer.dart @@ -88,7 +88,7 @@ class _ImageFooterState extends State { context: context, removeTop: true, child: BottomAppBar( - color: chatThemeData.colorTheme.white, + color: chatThemeData.colorTheme.barsBg, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -100,7 +100,7 @@ class _ImageFooterState extends State { IconButton( icon: StreamSvgIcon.iconShare( size: 24, - color: chatThemeData.colorTheme.black, + color: chatThemeData.colorTheme.textHighEmphasis, ), onPressed: () async { final attachment = @@ -144,7 +144,7 @@ class _ImageFooterState extends State { ), IconButton( icon: StreamSvgIcon.iconGrid( - color: chatThemeData.colorTheme.black, + color: chatThemeData.colorTheme.textHighEmphasis, ), onPressed: () => _showPhotosModal(context), ), @@ -160,7 +160,7 @@ class _ImageFooterState extends State { showModalBottomSheet( context: context, barrierColor: chatThemeData.colorTheme.overlay, - backgroundColor: chatThemeData.colorTheme.white, + backgroundColor: chatThemeData.colorTheme.barsBg, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( @@ -199,7 +199,7 @@ class _ImageFooterState extends State { alignment: Alignment.centerRight, child: IconButton( icon: StreamSvgIcon.close( - color: chatThemeData.colorTheme.black, + color: chatThemeData.colorTheme.textHighEmphasis, ), onPressed: () => Navigator.maybePop(context), ), @@ -262,7 +262,8 @@ class _ImageFooterState extends State { boxShadow: [ BoxShadow( blurRadius: 8, - color: chatThemeData.colorTheme.black + color: chatThemeData + .colorTheme.textHighEmphasis .withOpacity(0.3), ), ], diff --git a/packages/stream_chat_flutter/lib/src/image_header.dart b/packages/stream_chat_flutter/lib/src/image_header.dart index 3d156b16..92f4fa10 100644 --- a/packages/stream_chat_flutter/lib/src/image_header.dart +++ b/packages/stream_chat_flutter/lib/src/image_header.dart @@ -59,7 +59,7 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget { leading: showBackButton ? IconButton( icon: StreamSvgIcon.close( - color: chatThemeData.colorTheme.black, + color: chatThemeData.colorTheme.textHighEmphasis, size: 24, ), onPressed: onBackPressed, @@ -70,7 +70,7 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget { if (message.type != 'ephemeral') IconButton( icon: StreamSvgIcon.iconMenuPoint( - color: chatThemeData.colorTheme.black, + color: chatThemeData.colorTheme.textHighEmphasis, ), onPressed: () { _showMessageActionModalBottomSheet(context); diff --git a/packages/stream_chat_flutter/lib/src/info_tile.dart b/packages/stream_chat_flutter/lib/src/info_tile.dart index 1e935942..965d4fed 100644 --- a/packages/stream_chat_flutter/lib/src/info_tile.dart +++ b/packages/stream_chat_flutter/lib/src/info_tile.dart @@ -46,8 +46,8 @@ class InfoTile extends StatelessWidget { childAnchor: childAnchor ?? Alignment.bottomCenter, portal: Container( height: 25, - color: - backgroundColor ?? chatThemeData.colorTheme.grey.withOpacity(0.9), + color: backgroundColor ?? + chatThemeData.colorTheme.textLowEmphasis.withOpacity(0.9), child: Center( child: Text( message, diff --git a/packages/stream_chat_flutter/lib/src/media_list_view.dart b/packages/stream_chat_flutter/lib/src/media_list_view.dart index fe0070f9..77359f8b 100644 --- a/packages/stream_chat_flutter/lib/src/media_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/media_list_view.dart @@ -90,8 +90,8 @@ class _MediaListViewState extends State { ? 1.0 : 0.0, child: Container( - color: - chatThemeData.colorTheme.black.withOpacity(0.5), + color: chatThemeData.colorTheme.textHighEmphasis + .withOpacity(0.5), alignment: Alignment.topRight, padding: const EdgeInsets.only( top: 8, @@ -99,10 +99,11 @@ class _MediaListViewState extends State { ), child: CircleAvatar( radius: 12, - backgroundColor: chatThemeData.colorTheme.white, + backgroundColor: chatThemeData.colorTheme.barsBg, child: StreamSvgIcon.check( size: 24, - color: chatThemeData.colorTheme.black, + color: + chatThemeData.colorTheme.textHighEmphasis, ), ), ), @@ -124,7 +125,7 @@ class _MediaListViewState extends State { child: Text( media.videoDuration.format(), style: TextStyle( - color: chatThemeData.colorTheme.white, + color: chatThemeData.colorTheme.barsBg, ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/mention_tile.dart b/packages/stream_chat_flutter/lib/src/mention_tile.dart index 99b4b962..d9469215 100644 --- a/packages/stream_chat_flutter/lib/src/mention_tile.dart +++ b/packages/stream_chat_flutter/lib/src/mention_tile.dart @@ -76,7 +76,7 @@ class MentionTile extends StatelessWidget { maxLines: 1, overflow: TextOverflow.ellipsis, style: chatThemeData.textTheme.footnoteBold.copyWith( - color: chatThemeData.colorTheme.grey, + color: chatThemeData.colorTheme.textLowEmphasis, ), ), ], @@ -90,7 +90,7 @@ class MentionTile extends StatelessWidget { left: 8, ), child: StreamSvgIcon.mentions( - color: chatThemeData.colorTheme.accentBlue, + color: chatThemeData.colorTheme.accentPrimary, ), ), ], diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart index 296b1e59..3e653187 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -165,7 +165,7 @@ class _MessageActionsModalState extends State { child: SizedBox( width: mediaQueryData.size.width * 0.75, child: Material( - color: streamChatThemeData.colorTheme.whiteSnow, + color: streamChatThemeData.colorTheme.appBg, clipBehavior: Clip.hardEdge, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), @@ -197,7 +197,7 @@ class _MessageActionsModalState extends State { ].insertBetween( Container( height: 1, - color: streamChatThemeData.colorTheme.greyWhisper, + color: streamChatThemeData.colorTheme.borders, ), ), ), @@ -270,7 +270,7 @@ class _MessageActionsModalState extends State { context, title: 'Flag Message', icon: StreamSvgIcon.flag( - color: streamChatThemeData.colorTheme.accentRed, + color: streamChatThemeData.colorTheme.accentError, size: 24, ), question: @@ -287,7 +287,7 @@ class _MessageActionsModalState extends State { await showInfoDialog( context, icon: StreamSvgIcon.flag( - color: theme.colorTheme.accentRed, + color: theme.colorTheme.accentError, size: 24, ), details: 'The message has been reported to a moderator.', @@ -300,7 +300,7 @@ class _MessageActionsModalState extends State { await showInfoDialog( context, icon: StreamSvgIcon.flag( - color: theme.colorTheme.accentRed, + color: theme.colorTheme.accentError, size: 24, ), details: 'The message has been reported to a moderator.', @@ -337,7 +337,7 @@ class _MessageActionsModalState extends State { context, title: 'Delete message', icon: StreamSvgIcon.flag( - color: StreamChatTheme.of(context).colorTheme.accentRed, + color: StreamChatTheme.of(context).colorTheme.accentError, size: 24, ), question: 'Are you sure you want to permanently delete this\nmessage?', @@ -363,7 +363,7 @@ class _MessageActionsModalState extends State { showInfoDialog( context, icon: StreamSvgIcon.error( - color: StreamChatTheme.of(context).colorTheme.accentRed, + color: StreamChatTheme.of(context).colorTheme.accentError, size: 24, ), details: 'The operation couldn\'t be completed.', @@ -540,7 +540,7 @@ class _MessageActionsModalState extends State { child: Row( children: [ StreamSvgIcon.circleUp( - color: streamChatThemeData.colorTheme.accentBlue, + color: streamChatThemeData.colorTheme.accentPrimary, ), const SizedBox(width: 16), Text( @@ -585,7 +585,7 @@ class _MessageActionsModalState extends State { Padding( padding: const EdgeInsets.all(8), child: StreamSvgIcon.edit( - color: streamChatThemeData.colorTheme.greyGainsboro, + color: streamChatThemeData.colorTheme.disabled, ), ), const Text( diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 39211116..a0ff93cf 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -334,7 +334,7 @@ class MessageInputState extends State { Padding( padding: const EdgeInsets.all(8), child: StreamSvgIcon.reply( - color: _streamChatTheme.colorTheme.greyGainsboro, + color: _streamChatTheme.colorTheme.disabled, ), ), const Text( @@ -401,7 +401,8 @@ class MessageInputState extends State { border: _sendAsDm ? null : Border.all( - color: _streamChatTheme.colorTheme.black.withOpacity(.5), + color: _streamChatTheme.colorTheme.textHighEmphasis + .withOpacity(.5), width: 2, ), borderRadius: BorderRadius.circular(3), @@ -410,8 +411,8 @@ class MessageInputState extends State { child: Material( borderRadius: BorderRadius.circular(3), color: _sendAsDm - ? _streamChatTheme.colorTheme.accentBlue - : _streamChatTheme.colorTheme.white, + ? _streamChatTheme.colorTheme.accentPrimary + : _streamChatTheme.colorTheme.barsBg, child: InkWell( onTap: () { setState(() { @@ -426,7 +427,7 @@ class MessageInputState extends State { : CrossFadeState.showSecond, firstChild: StreamSvgIcon.check( size: 16, - color: _streamChatTheme.colorTheme.white, + color: _streamChatTheme.colorTheme.barsBg, ), secondChild: const SizedBox( height: 16, @@ -442,7 +443,8 @@ class MessageInputState extends State { child: Text( 'Also send as direct message', style: _streamChatTheme.textTheme.footnote.copyWith( - color: _streamChatTheme.colorTheme.black.withOpacity(0.5), + color: _streamChatTheme.colorTheme.textHighEmphasis + .withOpacity(0.5), ), ), ), @@ -582,7 +584,7 @@ class MessageInputState extends State { isDense: true, hintText: _getHint(), hintStyle: _streamChatTheme.messageInputTheme.inputTextStyle!.copyWith( - color: _streamChatTheme.colorTheme.grey, + color: _streamChatTheme.colorTheme.textLowEmphasis, ), border: const OutlineInputBorder( borderSide: BorderSide( @@ -620,7 +622,7 @@ class MessageInputState extends State { constraints: BoxConstraints.tight(const Size(64, 24)), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), - color: _streamChatTheme.colorTheme.accentBlue, + color: _streamChatTheme.colorTheme.accentPrimary, ), alignment: Alignment.center, child: Row( @@ -834,12 +836,12 @@ class MessageInputState extends State { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), - color: _streamChatTheme.colorTheme.white, + color: _streamChatTheme.colorTheme.barsBg, clipBehavior: Clip.hardEdge, child: Container( constraints: BoxConstraints.loose(const Size.fromHeight(400)), decoration: BoxDecoration( - color: _streamChatTheme.colorTheme.white, + color: _streamChatTheme.colorTheme.barsBg, borderRadius: BorderRadius.circular(8)), child: ListView( padding: const EdgeInsets.all(0), @@ -855,14 +857,14 @@ class MessageInputState extends State { horizontal: 8, ), child: StreamSvgIcon.lightning( - color: _streamChatTheme.colorTheme.accentBlue, + color: _streamChatTheme.colorTheme.accentPrimary, ), ), Text( 'Instant Commands', style: TextStyle( - color: - _streamChatTheme.colorTheme.black.withOpacity(.5), + color: _streamChatTheme.colorTheme.textHighEmphasis + .withOpacity(.5), ), ) ], @@ -902,7 +904,7 @@ class MessageInputState extends State { color: _streamChatTheme // ignore: lines_longer_than_80_chars .colorTheme - .grey, + .textLowEmphasis, ), ), ], @@ -946,24 +948,29 @@ class MessageInputState extends State { switch (index) { case 0: return _attachments.isEmpty - ? streamChatThemeData.colorTheme.accentBlue + ? streamChatThemeData.colorTheme.accentPrimary : (!_attachmentContainsFile - ? streamChatThemeData.colorTheme.accentBlue - : streamChatThemeData.colorTheme.black.withOpacity(0.2)); + ? streamChatThemeData.colorTheme.accentPrimary + : streamChatThemeData.colorTheme.textHighEmphasis + .withOpacity(0.2)); case 1: return _attachmentContainsFile - ? streamChatThemeData.colorTheme.accentBlue + ? streamChatThemeData.colorTheme.accentPrimary : (_attachments.isEmpty - ? streamChatThemeData.colorTheme.black.withOpacity(0.5) - : streamChatThemeData.colorTheme.black.withOpacity(0.2)); + ? streamChatThemeData.colorTheme.textHighEmphasis + .withOpacity(0.5) + : streamChatThemeData.colorTheme.textHighEmphasis + .withOpacity(0.2)); case 2: return _attachmentContainsFile && _attachments.isNotEmpty - ? streamChatThemeData.colorTheme.black.withOpacity(0.2) - : streamChatThemeData.colorTheme.black.withOpacity(0.5); + ? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2) + : streamChatThemeData.colorTheme.textHighEmphasis + .withOpacity(0.5); case 3: return _attachmentContainsFile && _attachments.isNotEmpty - ? streamChatThemeData.colorTheme.black.withOpacity(0.2) - : streamChatThemeData.colorTheme.black.withOpacity(0.5); + ? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2) + : streamChatThemeData.colorTheme.textHighEmphasis + .withOpacity(0.5); default: return Colors.black; } @@ -973,7 +980,7 @@ class MessageInputState extends State { duration: const Duration(milliseconds: 300), height: _openFilePickerSection ? _filePickerSize : 0, child: Material( - color: _streamChatTheme.colorTheme.whiteSmoke, + color: _streamChatTheme.colorTheme.inputBg, child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -1036,7 +1043,7 @@ class MessageInputState extends State { }, child: DecoratedBox( decoration: BoxDecoration( - color: _streamChatTheme.colorTheme.white, + color: _streamChatTheme.colorTheme.barsBg, borderRadius: const BorderRadius.only( topLeft: Radius.circular(16), topRight: Radius.circular(16), @@ -1052,7 +1059,7 @@ class MessageInputState extends State { height: 4, child: DecoratedBox( decoration: BoxDecoration( - color: _streamChatTheme.colorTheme.whiteSmoke, + color: _streamChatTheme.colorTheme.inputBg, borderRadius: BorderRadius.circular(4), ), ), @@ -1066,7 +1073,7 @@ class MessageInputState extends State { Expanded( child: DecoratedBox( decoration: BoxDecoration( - color: _streamChatTheme.colorTheme.white, + color: _streamChatTheme.colorTheme.barsBg, borderRadius: BorderRadius.circular(8), ), child: _PickerWidget( @@ -1153,7 +1160,7 @@ class MessageInputState extends State { ); case 'ban': return CircleAvatar( - backgroundColor: _streamChatTheme.colorTheme.accentBlue, + backgroundColor: _streamChatTheme.colorTheme.accentPrimary, radius: 12, child: StreamSvgIcon.iconUserDelete( size: 16, @@ -1162,7 +1169,7 @@ class MessageInputState extends State { ); case 'flag': return CircleAvatar( - backgroundColor: _streamChatTheme.colorTheme.accentBlue, + backgroundColor: _streamChatTheme.colorTheme.accentPrimary, radius: 12, child: StreamSvgIcon.flag( size: 14, @@ -1171,7 +1178,7 @@ class MessageInputState extends State { ); case 'imgur': return CircleAvatar( - backgroundColor: _streamChatTheme.colorTheme.accentBlue, + backgroundColor: _streamChatTheme.colorTheme.accentPrimary, radius: 12, child: ClipOval( child: StreamSvgIcon.imgur( @@ -1181,7 +1188,7 @@ class MessageInputState extends State { ); case 'mute': return CircleAvatar( - backgroundColor: _streamChatTheme.colorTheme.accentBlue, + backgroundColor: _streamChatTheme.colorTheme.accentPrimary, radius: 12, child: StreamSvgIcon.mute( size: 16, @@ -1190,7 +1197,7 @@ class MessageInputState extends State { ); case 'unban': return CircleAvatar( - backgroundColor: _streamChatTheme.colorTheme.accentBlue, + backgroundColor: _streamChatTheme.colorTheme.accentPrimary, radius: 12, child: StreamSvgIcon.userAdd( size: 16, @@ -1199,7 +1206,7 @@ class MessageInputState extends State { ); case 'unmute': return CircleAvatar( - backgroundColor: _streamChatTheme.colorTheme.accentBlue, + backgroundColor: _streamChatTheme.colorTheme.accentPrimary, radius: 12, child: StreamSvgIcon.volumeUp( size: 16, @@ -1208,7 +1215,7 @@ class MessageInputState extends State { ); default: return CircleAvatar( - backgroundColor: _streamChatTheme.colorTheme.accentBlue, + backgroundColor: _streamChatTheme.colorTheme.accentPrimary, radius: 12, child: StreamSvgIcon.lightning( size: 16, @@ -1248,7 +1255,7 @@ class MessageInputState extends State { final child = Card( margin: const EdgeInsets.all(8), elevation: 2, - color: _streamChatTheme.colorTheme.white, + color: _streamChatTheme.colorTheme.barsBg, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), @@ -1256,7 +1263,7 @@ class MessageInputState extends State { child: Container( constraints: BoxConstraints.loose(const Size.fromHeight(240)), decoration: BoxDecoration( - color: _streamChatTheme.colorTheme.white, + color: _streamChatTheme.colorTheme.barsBg, ), child: FutureBuilder>( future: queryMembers ?? Future.value(members), @@ -1272,7 +1279,7 @@ class MessageInputState extends State { .where((it) => it.user != null) .map( (m) => Material( - color: _streamChatTheme.colorTheme.white, + color: _streamChatTheme.colorTheme.barsBg, child: InkWell( onTap: () { if (m.user != null) { @@ -1359,7 +1366,7 @@ class MessageInputState extends State { child: Card( margin: const EdgeInsets.all(8), elevation: 2, - color: _streamChatTheme.colorTheme.white, + color: _streamChatTheme.colorTheme.barsBg, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), @@ -1374,7 +1381,7 @@ class MessageInputState extends State { offset: Offset(0, -4), ), ], - color: _streamChatTheme.colorTheme.white, + color: _streamChatTheme.colorTheme.barsBg, ), child: ListView.builder( padding: const EdgeInsets.all(0), @@ -1390,15 +1397,16 @@ class MessageInputState extends State { padding: const EdgeInsets.symmetric(horizontal: 8), child: StreamSvgIcon.smile( - color: - _streamChatTheme.colorTheme.accentBlue, + color: _streamChatTheme + .colorTheme.accentPrimary, ), ), Flexible( child: Text( 'Emoji matching "$query"', style: TextStyle( - color: _streamChatTheme.colorTheme.black + color: _streamChatTheme + .colorTheme.textHighEmphasis .withOpacity(.5), ), ), @@ -1570,11 +1578,12 @@ class MessageInputState extends State { onPressed: () { setState(() => _attachments.remove(attachment.id)); }, - fillColor: _streamChatTheme.colorTheme.black.withOpacity(.5), + fillColor: + _streamChatTheme.colorTheme.textHighEmphasis.withOpacity(.5), child: Center( child: StreamSvgIcon.close( size: 24, - color: _streamChatTheme.colorTheme.white, + color: _streamChatTheme.colorTheme.barsBg, ), ), ), @@ -1609,8 +1618,8 @@ class MessageInputState extends State { errorWidget: (_, obj, trace) => getFileTypeImage(attachment.extraData['other'] as String?), placeholder: (context, _) => Shimmer.fromColors( - baseColor: _streamChatTheme.colorTheme.greyGainsboro, - highlightColor: _streamChatTheme.colorTheme.whiteSmoke, + baseColor: _streamChatTheme.colorTheme.disabled, + highlightColor: _streamChatTheme.colorTheme.inputBg, child: Image.asset( 'images/placeholder.png', fit: BoxFit.cover, @@ -1651,7 +1660,7 @@ class MessageInputState extends State { return IconButton( icon: StreamSvgIcon.lightning( color: s.isNotEmpty - ? _streamChatTheme.colorTheme.greyGainsboro + ? _streamChatTheme.colorTheme.disabled : (_commandsOverlay != null ? _streamChatTheme.messageInputTheme.actionButtonColor : _streamChatTheme.messageInputTheme.actionButtonIdleColor), @@ -2058,7 +2067,7 @@ class MessageInputState extends State { void _showErrorAlert(String description) { showModalBottomSheet( - backgroundColor: _streamChatTheme.colorTheme.white, + backgroundColor: _streamChatTheme.colorTheme.barsBg, context: context, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( @@ -2072,7 +2081,7 @@ class MessageInputState extends State { height: 26, ), StreamSvgIcon.error( - color: _streamChatTheme.colorTheme.accentRed, + color: _streamChatTheme.colorTheme.accentError, size: 24, ), const SizedBox( @@ -2096,7 +2105,8 @@ class MessageInputState extends State { height: 36, ), Container( - color: _streamChatTheme.colorTheme.black.withOpacity(.08), + color: + _streamChatTheme.colorTheme.textHighEmphasis.withOpacity(.08), height: 1, ), Row( @@ -2108,8 +2118,8 @@ class MessageInputState extends State { }, child: Text( 'OK', - style: _streamChatTheme.textTheme.bodyBold - .copyWith(color: _streamChatTheme.colorTheme.accentBlue), + style: _streamChatTheme.textTheme.bodyBold.copyWith( + color: _streamChatTheme.colorTheme.accentPrimary), ), ), ], @@ -2250,12 +2260,12 @@ class __PickerWidgetState extends State<_PickerWidget> { }, child: Container( constraints: const BoxConstraints.expand(), - color: widget.streamChatTheme.colorTheme.whiteSmoke, + color: widget.streamChatTheme.colorTheme.inputBg, alignment: Alignment.center, child: Text( 'Add more files', style: TextStyle( - color: widget.streamChatTheme.colorTheme.accentBlue, + color: widget.streamChatTheme.colorTheme.accentPrimary, fontWeight: FontWeight.bold, ), ), @@ -2273,7 +2283,7 @@ class __PickerWidgetState extends State<_PickerWidget> { PhotoManager.openSetting(); }, child: Container( - color: widget.streamChatTheme.colorTheme.whiteSmoke, + color: widget.streamChatTheme.colorTheme.inputBg, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, @@ -2282,13 +2292,14 @@ class __PickerWidgetState extends State<_PickerWidget> { 'svgs/icon_picture_empty_state.svg', package: 'stream_chat_flutter', height: 140, - color: widget.streamChatTheme.colorTheme.greyGainsboro, + color: widget.streamChatTheme.colorTheme.disabled, ), Text( // ignore: lines_longer_than_80_chars 'Please enable access to your photos \nand videos so you can share them with friends.', style: widget.streamChatTheme.textTheme.body.copyWith( - color: widget.streamChatTheme.colorTheme.grey), + color: + widget.streamChatTheme.colorTheme.textLowEmphasis), textAlign: TextAlign.center, ), const SizedBox(height: 6), @@ -2296,7 +2307,7 @@ class __PickerWidgetState extends State<_PickerWidget> { child: Text( 'Allow access to your gallery', style: widget.streamChatTheme.textTheme.bodyBold.copyWith( - color: widget.streamChatTheme.colorTheme.accentBlue, + color: widget.streamChatTheme.colorTheme.accentPrimary, ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index e6bf7a17..6e61612b 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -368,7 +368,8 @@ class _MessageListViewState extends State { child: Text( 'No chats here yet...', style: _streamTheme.textTheme.footnote.copyWith( - color: _streamTheme.colorTheme.black.withOpacity(.5)), + color: _streamTheme.colorTheme.textHighEmphasis + .withOpacity(.5)), ), ), messageListBuilder: widget.messageListBuilder ?? @@ -380,7 +381,8 @@ class _MessageListViewState extends State { child: Text( 'Something went wrong', style: _streamTheme.textTheme.footnote.copyWith( - color: _streamTheme.colorTheme.black.withOpacity(.5)), + color: _streamTheme.colorTheme.textHighEmphasis + .withOpacity(.5)), ), ), ); @@ -710,7 +712,7 @@ class _MessageListViewState extends State { clipBehavior: Clip.none, children: [ FloatingActionButton( - backgroundColor: _streamTheme.colorTheme.white, + backgroundColor: _streamTheme.colorTheme.barsBg, onPressed: () { if (unreadCount > 0) { streamChannel!.channel.markRead(); @@ -729,7 +731,7 @@ class _MessageListViewState extends State { } }, child: StreamSvgIcon.down( - color: _streamTheme.colorTheme.black, + color: _streamTheme.colorTheme.textHighEmphasis, ), ), if (showUnreadCount) @@ -1106,7 +1108,7 @@ class _MessageListViewState extends State { widget.onMessageSwiped?.call(message); }, backgroundIcon: StreamSvgIcon.reply( - color: _streamTheme.colorTheme.accentBlue, + color: _streamTheme.colorTheme.accentPrimary, ), child: child, ), @@ -1122,7 +1124,7 @@ class _MessageListViewState extends State { child = TweenAnimationBuilder( tween: ColorTween( begin: highlightColor, - end: colorTheme.white.withOpacity(0), + end: colorTheme.barsBg.withOpacity(0), ), duration: const Duration(seconds: 3), onEnd: () => initialMessageHighlightComplete = true, @@ -1252,7 +1254,7 @@ class _LoadingIndicator extends StatelessWidget { stream: stream, initialData: false, errorBuilder: (context, error) => Container( - color: streamTheme.colorTheme.accentRed.withOpacity(.2), + color: streamTheme.colorTheme.accentError.withOpacity(.2), child: const Center( child: Text('Error loading messages'), ), diff --git a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart index 0292a05a..3d9a907e 100644 --- a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart @@ -142,7 +142,7 @@ class MessageReactionsModal extends StatelessWidget { Widget _buildReactionCard(BuildContext context, User? user) { final chatThemeData = StreamChatTheme.of(context); return Card( - color: chatThemeData.colorTheme.white, + color: chatThemeData.colorTheme.barsBg, clipBehavior: Clip.hardEdge, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), @@ -224,7 +224,7 @@ class MessageReactionsModal extends StatelessWidget { messageTheme.reactionsBorderColor ?? Colors.transparent, backgroundColor: messageTheme.reactionsBackgroundColor ?? Colors.transparent, - maskColor: chatThemeData.colorTheme.white, + maskColor: chatThemeData.colorTheme.barsBg, tailCirclesSpacing: 1, highlightOwnReactions: false, ), diff --git a/packages/stream_chat_flutter/lib/src/message_search_list_view.dart b/packages/stream_chat_flutter/lib/src/message_search_list_view.dart index 1a67702f..f364d0f2 100644 --- a/packages/stream_chat_flutter/lib/src/message_search_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_search_list_view.dart @@ -199,7 +199,7 @@ class _MessageSearchListViewState extends State { Widget _separatorBuilder(BuildContext context, int index) => Container( height: 1, - color: StreamChatTheme.of(context).colorTheme.greyWhisper, + color: StreamChatTheme.of(context).colorTheme.borders, ); Widget _listItemBuilder( @@ -224,7 +224,7 @@ class _MessageSearchListViewState extends State { return Container( color: StreamChatTheme.of(context) .colorTheme - .accentRed + .accentError .withOpacity(.2), child: const Padding( padding: EdgeInsets.symmetric(vertical: 16), @@ -294,7 +294,7 @@ class _MessageSearchListViewState extends State { child: Text( '${items.length} results', style: TextStyle( - color: chatThemeData.colorTheme.grey, + color: chatThemeData.colorTheme.textLowEmphasis, ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index 7c95bf0a..c9dfd9d4 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -222,7 +222,7 @@ class MessageWidget extends StatefulWidget { final border = RoundedRectangleBorder( side: attachmentBorderSide ?? BorderSide( - color: StreamChatTheme.of(context).colorTheme.greyWhisper, + color: StreamChatTheme.of(context).colorTheme.borders, ), borderRadius: attachmentBorderRadiusGeometry ?? BorderRadius.zero, ); @@ -749,7 +749,7 @@ class _MessageWidgetState extends State child: CustomPaint( painter: ReactionBubblePainter( _streamChatTheme - .colorTheme.white, + .colorTheme.barsBg, Colors.transparent, Colors.transparent, tailCirclesSpace: 1, @@ -835,14 +835,14 @@ class _MessageWidgetState extends State mainAxisSize: MainAxisSize.min, children: [ StreamSvgIcon.eye( - color: chatThemeData.colorTheme.grey, + color: chatThemeData.colorTheme.textLowEmphasis, size: 16, ), const SizedBox(width: 8), Text( 'Only visible to you', style: chatThemeData.textTheme.footnote - .copyWith(color: chatThemeData.colorTheme.grey), + .copyWith(color: chatThemeData.colorTheme.textLowEmphasis), ), ], ); @@ -1218,7 +1218,7 @@ class _MessageWidgetState extends State Text( widget.readList!.length.toString(), style: style.copyWith( - color: _streamChatTheme.colorTheme.accentBlue, + color: _streamChatTheme.colorTheme.accentPrimary, ), ), const SizedBox(width: 2), @@ -1292,7 +1292,7 @@ class _MessageWidgetState extends State Text( 'Pinned by ${pinnedByMe ? 'You' : pinnedBy.name}', style: TextStyle( - color: _streamChatTheme.colorTheme.grey, + color: _streamChatTheme.colorTheme.textLowEmphasis, fontSize: 13, fontWeight: FontWeight.w400, ), @@ -1310,7 +1310,7 @@ class _MessageWidgetState extends State } if (hasUrlAttachments) { - return _streamChatTheme.colorTheme.blueAlice; + return _streamChatTheme.colorTheme.linkBg; } if (isOnlyEmoji) { @@ -1366,7 +1366,7 @@ class _ThreadParticipants extends StatelessWidget { child: Container( decoration: BoxDecoration( shape: BoxShape.circle, - color: _streamChatTheme.colorTheme.white, + color: _streamChatTheme.colorTheme.barsBg, ), padding: const EdgeInsets.all(1), child: UserAvatar( @@ -1395,7 +1395,7 @@ class _ThreadReplyPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final paint = Paint() - ..color = color ?? StreamChatTheme.of(context!).colorTheme.greyGainsboro + ..color = color ?? StreamChatTheme.of(context!).colorTheme.disabled ..style = PaintingStyle.stroke ..strokeWidth = 1 ..strokeCap = StrokeCap.round; diff --git a/packages/stream_chat_flutter/lib/src/option_list_tile.dart b/packages/stream_chat_flutter/lib/src/option_list_tile.dart index 10ae1b5f..16bf9e97 100644 --- a/packages/stream_chat_flutter/lib/src/option_list_tile.dart +++ b/packages/stream_chat_flutter/lib/src/option_list_tile.dart @@ -46,11 +46,11 @@ class OptionListTile extends StatelessWidget { return Column( children: [ Container( - color: separatorColor ?? chatThemeData.colorTheme.greyGainsboro, + color: separatorColor ?? chatThemeData.colorTheme.disabled, height: 1, ), Material( - color: tileColor ?? chatThemeData.colorTheme.white, + color: tileColor ?? chatThemeData.colorTheme.barsBg, child: SizedBox( height: 63, child: InkWell( diff --git a/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart b/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart index 43620640..22ca4d4c 100644 --- a/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart @@ -154,7 +154,7 @@ class QuotedMessageWidget extends StatelessWidget { color: _getBackgroundColor(context), border: showBorder ? Border.all( - color: StreamChatTheme.of(context).colorTheme.greyGainsboro, + color: StreamChatTheme.of(context).colorTheme.disabled, ) : null, borderRadius: BorderRadius.only( @@ -280,7 +280,7 @@ class QuotedMessageWidget extends StatelessWidget { Color? _getBackgroundColor(BuildContext context) { if (_containsScrapeUrl) { - return StreamChatTheme.of(context).colorTheme.blueAlice; + return StreamChatTheme.of(context).colorTheme.linkBg; } return messageTheme.messageBackgroundColor; } diff --git a/packages/stream_chat_flutter/lib/src/reaction_bubble.dart b/packages/stream_chat_flutter/lib/src/reaction_bubble.dart index a3b2052f..9e279344 100644 --- a/packages/stream_chat_flutter/lib/src/reaction_bubble.dart +++ b/packages/stream_chat_flutter/lib/src/reaction_bubble.dart @@ -141,8 +141,8 @@ class ReactionBubble extends StatelessWidget { Icons.help_outline_rounded, size: 16, color: (!highlightOwnReactions || reaction.user?.id == userId) - ? chatThemeData.colorTheme.accentBlue - : chatThemeData.colorTheme.black.withOpacity(.5), + ? chatThemeData.colorTheme.accentPrimary + : chatThemeData.colorTheme.textHighEmphasis.withOpacity(.5), ), ); } diff --git a/packages/stream_chat_flutter/lib/src/reaction_picker.dart b/packages/stream_chat_flutter/lib/src/reaction_picker.dart index 5101cbb7..3e1e5e50 100644 --- a/packages/stream_chat_flutter/lib/src/reaction_picker.dart +++ b/packages/stream_chat_flutter/lib/src/reaction_picker.dart @@ -49,7 +49,7 @@ class _ReactionPickerState extends State final child = Material( borderRadius: BorderRadius.circular(24), - color: chatThemeData.colorTheme.white, + color: chatThemeData.colorTheme.barsBg, clipBehavior: Clip.hardEdge, child: Padding( padding: const EdgeInsets.symmetric( diff --git a/packages/stream_chat_flutter/lib/src/sending_indicator.dart b/packages/stream_chat_flutter/lib/src/sending_indicator.dart index 88560432..7f44e5b8 100644 --- a/packages/stream_chat_flutter/lib/src/sending_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/sending_indicator.dart @@ -25,7 +25,7 @@ class SendingIndicator extends StatelessWidget { if (isMessageRead) { return StreamSvgIcon.checkAll( size: size, - color: StreamChatTheme.of(context).colorTheme.accentBlue, + color: StreamChatTheme.of(context).colorTheme.accentPrimary, ); } if (message.status == MessageSendingStatus.sent) { diff --git a/packages/stream_chat_flutter/lib/src/stream_chat.dart b/packages/stream_chat_flutter/lib/src/stream_chat.dart index 64acae98..c050d8cb 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat.dart @@ -101,8 +101,8 @@ class StreamChatState extends State { return Theme( data: materialTheme.copyWith( primaryIconTheme: streamTheme.primaryIconTheme, - accentColor: streamTheme.colorTheme.accentBlue, - scaffoldBackgroundColor: streamTheme.colorTheme.white, + accentColor: streamTheme.colorTheme.accentPrimary, + scaffoldBackgroundColor: streamTheme.colorTheme.barsBg, ), child: StreamChatCore( client: client, diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart index 91ed2537..72b04a80 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart @@ -114,7 +114,7 @@ class StreamChatThemeData { final defaultTheme = StreamChatThemeData(brightness: theme.brightness); final customizedTheme = StreamChatThemeData.fromColorAndTextTheme( defaultTheme.colorTheme.copyWith( - accentBlue: theme.accentColor, + accentPrimary: theme.accentColor, ), defaultTheme.textTheme, ); @@ -216,8 +216,9 @@ class StreamChatThemeData { ColorTheme colorTheme, TextTheme textTheme, ) { - final accentColor = colorTheme.accentBlue; - final iconTheme = IconThemeData(color: colorTheme.black.withOpacity(.5)); + final accentColor = colorTheme.accentPrimary; + final iconTheme = + IconThemeData(color: colorTheme.textHighEmphasis.withOpacity(.5)); return StreamChatThemeData.raw( textTheme: textTheme, colorTheme: colorTheme, @@ -231,7 +232,7 @@ class StreamChatThemeData { ), ), channelPreviewTheme: ChannelPreviewTheme( - unreadCounterColor: colorTheme.accentRed, + unreadCounterColor: colorTheme.accentError, avatarTheme: AvatarTheme( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( @@ -244,7 +245,7 @@ class StreamChatThemeData { color: const Color(0xff7A7A7A), ), lastMessageAt: textTheme.footnote.copyWith( - color: colorTheme.black.withOpacity(.5), + color: colorTheme.textHighEmphasis.withOpacity(.5), ), indicatorIconSize: 16, ), @@ -256,7 +257,7 @@ class StreamChatThemeData { width: 40, ), ), - color: colorTheme.white, + color: colorTheme.barsBg, title: textTheme.headlineBold, ), channelTheme: ChannelTheme( @@ -268,7 +269,7 @@ class StreamChatThemeData { width: 40, ), ), - color: colorTheme.white, + color: colorTheme.barsBg, title: textTheme.headlineBold, subtitle: textTheme.footnote.copyWith( color: const Color(0xff7A7A7A), @@ -276,15 +277,17 @@ class StreamChatThemeData { ), ), ownMessageTheme: MessageTheme( - messageAuthor: textTheme.footnote.copyWith(color: colorTheme.grey), + messageAuthor: + textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis), messageText: textTheme.body, - createdAt: textTheme.footnote.copyWith(color: colorTheme.grey), + createdAt: + textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis), replies: textTheme.footnoteBold.copyWith(color: accentColor), - messageBackgroundColor: colorTheme.greyGainsboro, - reactionsBackgroundColor: colorTheme.white, - reactionsBorderColor: colorTheme.greyWhisper, - reactionsMaskColor: colorTheme.whiteSnow, - messageBorderColor: colorTheme.greyGainsboro, + messageBackgroundColor: colorTheme.disabled, + reactionsBackgroundColor: colorTheme.barsBg, + reactionsBorderColor: colorTheme.borders, + reactionsMaskColor: colorTheme.appBg, + messageBorderColor: colorTheme.disabled, avatarTheme: AvatarTheme( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( @@ -297,18 +300,20 @@ class StreamChatThemeData { ), ), otherMessageTheme: MessageTheme( - reactionsBackgroundColor: colorTheme.greyGainsboro, - reactionsBorderColor: colorTheme.white, - reactionsMaskColor: colorTheme.whiteSnow, + reactionsBackgroundColor: colorTheme.disabled, + reactionsBorderColor: colorTheme.barsBg, + reactionsMaskColor: colorTheme.appBg, messageText: textTheme.body, - createdAt: textTheme.footnote.copyWith(color: colorTheme.grey), - messageAuthor: textTheme.footnote.copyWith(color: colorTheme.grey), + createdAt: + textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis), + messageAuthor: + textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis), replies: textTheme.footnoteBold.copyWith(color: accentColor), messageLinks: TextStyle( color: accentColor, ), - messageBackgroundColor: colorTheme.white, - messageBorderColor: colorTheme.greyWhisper, + messageBackgroundColor: colorTheme.barsBg, + messageBorderColor: colorTheme.borders, avatarTheme: AvatarTheme( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( @@ -320,23 +325,23 @@ class StreamChatThemeData { messageInputTheme: MessageInputTheme( borderRadius: BorderRadius.circular(20), sendAnimationDuration: const Duration(milliseconds: 300), - actionButtonColor: colorTheme.accentBlue, - actionButtonIdleColor: colorTheme.grey, - expandButtonColor: colorTheme.accentBlue, - sendButtonColor: colorTheme.accentBlue, - sendButtonIdleColor: colorTheme.greyGainsboro, - inputBackground: colorTheme.white, + actionButtonColor: colorTheme.accentPrimary, + actionButtonIdleColor: colorTheme.textLowEmphasis, + expandButtonColor: colorTheme.accentPrimary, + sendButtonColor: colorTheme.accentPrimary, + sendButtonIdleColor: colorTheme.disabled, + inputBackground: colorTheme.barsBg, inputTextStyle: textTheme.body, idleBorderGradient: LinearGradient( colors: [ - colorTheme.greyGainsboro, - colorTheme.greyGainsboro, + colorTheme.disabled, + colorTheme.disabled, ], ), activeBorderGradient: LinearGradient( colors: [ - colorTheme.greyGainsboro, - colorTheme.greyGainsboro, + colorTheme.disabled, + colorTheme.disabled, ], ), ), @@ -347,7 +352,7 @@ class StreamChatThemeData { final theme = StreamChatTheme.of(context); return StreamSvgIcon.loveReaction( color: highlighted - ? theme.colorTheme.accentBlue + ? theme.colorTheme.accentPrimary : theme.primaryIconTheme.color!.withOpacity(.5), size: size, ); @@ -359,7 +364,7 @@ class StreamChatThemeData { final theme = StreamChatTheme.of(context); return StreamSvgIcon.thumbsUpReaction( color: highlighted - ? theme.colorTheme.accentBlue + ? theme.colorTheme.accentPrimary : theme.primaryIconTheme.color!.withOpacity(.5), size: size, ); @@ -371,7 +376,7 @@ class StreamChatThemeData { final theme = StreamChatTheme.of(context); return StreamSvgIcon.thumbsDownReaction( color: highlighted - ? theme.colorTheme.accentBlue + ? theme.colorTheme.accentPrimary : theme.primaryIconTheme.color!.withOpacity(.5), size: size, ); @@ -383,7 +388,7 @@ class StreamChatThemeData { final theme = StreamChatTheme.of(context); return StreamSvgIcon.lolReaction( color: highlighted - ? theme.colorTheme.accentBlue + ? theme.colorTheme.accentPrimary : theme.primaryIconTheme.color!.withOpacity(.5), size: size, ); @@ -395,7 +400,7 @@ class StreamChatThemeData { final theme = StreamChatTheme.of(context); return StreamSvgIcon.wutReaction( color: highlighted - ? theme.colorTheme.accentBlue + ? theme.colorTheme.accentPrimary : theme.primaryIconTheme.color!.withOpacity(.5), size: size, ); @@ -572,17 +577,17 @@ class TextTheme { class ColorTheme { /// Initialise with light theme ColorTheme.light({ - this.black = const Color(0xff000000), - this.grey = const Color(0xff7a7a7a), - this.greyGainsboro = const Color(0xffdbdbdb), - this.greyWhisper = const Color(0xffecebeb), - this.whiteSmoke = const Color(0xfff2f2f2), - this.whiteSnow = const Color(0xfffcfcfc), - this.white = const Color(0xffffffff), - this.blueAlice = const Color(0xffe9f2ff), - this.accentBlue = const Color(0xff005FFF), - this.accentRed = const Color(0xffFF3842), - this.accentGreen = const Color(0xff20E070), + this.textHighEmphasis = const Color(0xff000000), + this.textLowEmphasis = const Color(0xff7a7a7a), + this.disabled = const Color(0xffdbdbdb), + this.borders = const Color(0xffecebeb), + this.inputBg = const Color(0xfff2f2f2), + this.appBg = const Color(0xfffcfcfc), + this.barsBg = const Color(0xffffffff), + this.linkBg = const Color(0xffe9f2ff), + this.accentPrimary = const Color(0xff005FFF), + this.accentError = const Color(0xffFF3842), + this.accentInfo = const Color(0xff20E070), this.highlight = const Color(0xfffbf4dd), this.overlay = const Color.fromRGBO(0, 0, 0, 0.2), this.overlayDark = const Color.fromRGBO(0, 0, 0, 0.6), @@ -604,17 +609,17 @@ class ColorTheme { /// Initialise with dark theme ColorTheme.dark({ - this.black = const Color(0xffffffff), - this.grey = const Color(0xff7a7a7a), - this.greyGainsboro = const Color(0xff2d2f2f), - this.greyWhisper = const Color(0xff1c1e22), - this.whiteSmoke = const Color(0xff13151b), - this.whiteSnow = const Color(0xff070A0D), - this.white = const Color(0xff101418), - this.blueAlice = const Color(0xff00193D), - this.accentBlue = const Color(0xff005FFF), - this.accentRed = const Color(0xffFF3742), - this.accentGreen = const Color(0xff20E070), + this.textHighEmphasis = const Color(0xffffffff), + this.textLowEmphasis = const Color(0xff7a7a7a), + this.disabled = const Color(0xff2d2f2f), + this.borders = const Color(0xff1c1e22), + this.inputBg = const Color(0xff13151b), + this.appBg = const Color(0xff070A0D), + this.barsBg = const Color(0xff101418), + this.linkBg = const Color(0xff00193D), + this.accentPrimary = const Color(0xff005FFF), + this.accentError = const Color(0xffFF3742), + this.accentInfo = const Color(0xff20E070), this.borderTop = const Effect( sigmaX: 0, sigmaY: -1, @@ -657,37 +662,37 @@ class ColorTheme { }) : brightness = Brightness.dark; /// - final Color black; + final Color textHighEmphasis; /// - final Color grey; + final Color textLowEmphasis; /// - final Color greyGainsboro; + final Color disabled; /// - final Color greyWhisper; + final Color borders; /// - final Color whiteSmoke; + final Color inputBg; /// - final Color whiteSnow; + final Color appBg; /// - final Color white; + final Color barsBg; /// - final Color blueAlice; + final Color linkBg; /// - final Color accentBlue; + final Color accentPrimary; /// - final Color accentRed; + final Color accentError; /// - final Color accentGreen; + final Color accentInfo; /// final Effect borderTop; @@ -719,17 +724,17 @@ class ColorTheme { /// Copy with theme ColorTheme copyWith({ Brightness brightness = Brightness.light, - Color? black, - Color? grey, - Color? greyGainsboro, - Color? greyWhisper, - Color? whiteSmoke, - Color? whiteSnow, - Color? white, - Color? blueAlice, - Color? accentBlue, - Color? accentRed, - Color? accentGreen, + Color? textHighEmphasis, + Color? textLowEmphasis, + Color? disabled, + Color? borders, + Color? inputBg, + Color? appBg, + Color? barsBg, + Color? linkBg, + Color? accentPrimary, + Color? accentError, + Color? accentInfo, Effect? borderTop, Effect? borderBottom, Effect? shadowIconButton, @@ -741,17 +746,17 @@ class ColorTheme { }) => brightness == Brightness.light ? ColorTheme.light( - black: black ?? this.black, - grey: grey ?? this.grey, - greyGainsboro: greyGainsboro ?? this.greyGainsboro, - greyWhisper: greyWhisper ?? this.greyWhisper, - whiteSmoke: whiteSmoke ?? this.whiteSmoke, - whiteSnow: whiteSnow ?? this.whiteSnow, - white: white ?? this.white, - blueAlice: blueAlice ?? this.blueAlice, - accentBlue: accentBlue ?? this.accentBlue, - accentRed: accentRed ?? this.accentRed, - accentGreen: accentGreen ?? this.accentGreen, + textHighEmphasis: textHighEmphasis ?? this.textHighEmphasis, + textLowEmphasis: textLowEmphasis ?? this.textLowEmphasis, + disabled: disabled ?? this.disabled, + borders: borders ?? this.borders, + inputBg: inputBg ?? this.inputBg, + appBg: appBg ?? this.appBg, + barsBg: barsBg ?? this.barsBg, + linkBg: linkBg ?? this.linkBg, + accentPrimary: accentPrimary ?? this.accentPrimary, + accentError: accentError ?? this.accentError, + accentInfo: accentInfo ?? this.accentInfo, borderTop: borderTop ?? this.borderTop, borderBottom: borderBottom ?? this.borderBottom, shadowIconButton: shadowIconButton ?? this.shadowIconButton, @@ -762,17 +767,17 @@ class ColorTheme { bgGradient: bgGradient ?? this.bgGradient, ) : ColorTheme.dark( - black: black ?? this.black, - grey: grey ?? this.grey, - greyGainsboro: greyGainsboro ?? this.greyGainsboro, - greyWhisper: greyWhisper ?? this.greyWhisper, - whiteSmoke: whiteSmoke ?? this.whiteSmoke, - whiteSnow: whiteSnow ?? this.whiteSnow, - white: white ?? this.white, - blueAlice: blueAlice ?? this.blueAlice, - accentBlue: accentBlue ?? this.accentBlue, - accentRed: accentRed ?? this.accentRed, - accentGreen: accentGreen ?? this.accentGreen, + textHighEmphasis: textHighEmphasis ?? this.textHighEmphasis, + textLowEmphasis: textLowEmphasis ?? this.textLowEmphasis, + disabled: disabled ?? this.disabled, + borders: borders ?? this.borders, + inputBg: inputBg ?? this.inputBg, + appBg: appBg ?? this.appBg, + barsBg: barsBg ?? this.barsBg, + linkBg: linkBg ?? this.linkBg, + accentPrimary: accentPrimary ?? this.accentPrimary, + accentError: accentError ?? this.accentError, + accentInfo: accentInfo ?? this.accentInfo, borderTop: borderTop ?? this.borderTop, borderBottom: borderBottom ?? this.borderBottom, shadowIconButton: shadowIconButton ?? this.shadowIconButton, @@ -787,17 +792,17 @@ class ColorTheme { ColorTheme merge(ColorTheme? other) { if (other == null) return this; return copyWith( - black: other.black, - grey: other.grey, - greyGainsboro: other.greyGainsboro, - greyWhisper: other.greyWhisper, - whiteSmoke: other.whiteSmoke, - whiteSnow: other.whiteSnow, - white: other.white, - blueAlice: other.blueAlice, - accentBlue: other.accentBlue, - accentRed: other.accentRed, - accentGreen: other.accentGreen, + textHighEmphasis: other.textHighEmphasis, + textLowEmphasis: other.textLowEmphasis, + disabled: other.disabled, + borders: other.borders, + inputBg: other.inputBg, + appBg: other.appBg, + barsBg: other.barsBg, + linkBg: other.linkBg, + accentPrimary: other.accentPrimary, + accentError: other.accentError, + accentInfo: other.accentInfo, highlight: other.highlight, overlay: other.overlay, overlayDark: other.overlayDark, diff --git a/packages/stream_chat_flutter/lib/src/swipeable.dart b/packages/stream_chat_flutter/lib/src/swipeable.dart index 5895e8b5..041111e4 100644 --- a/packages/stream_chat_flutter/lib/src/swipeable.dart +++ b/packages/stream_chat_flutter/lib/src/swipeable.dart @@ -151,9 +151,8 @@ class _SwipeableState extends State with TickerProviderStateMixin { decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( - color: StreamChatTheme.of(context) - .colorTheme - .greyGainsboro, + color: + StreamChatTheme.of(context).colorTheme.disabled, ), ), child: widget.backgroundIcon, diff --git a/packages/stream_chat_flutter/lib/src/system_message.dart b/packages/stream_chat_flutter/lib/src/system_message.dart index b847eb3c..11c80300 100644 --- a/packages/stream_chat_flutter/lib/src/system_message.dart +++ b/packages/stream_chat_flutter/lib/src/system_message.dart @@ -32,7 +32,7 @@ class SystemMessage extends StatelessWidget { textAlign: TextAlign.center, softWrap: true, style: theme.textTheme.captionBold.copyWith( - color: theme.colorTheme.grey, + color: theme.colorTheme.textLowEmphasis, ), ), ); diff --git a/packages/stream_chat_flutter/lib/src/upload_progress_indicator.dart b/packages/stream_chat_flutter/lib/src/upload_progress_indicator.dart index 9905141a..b3fe510d 100644 --- a/packages/stream_chat_flutter/lib/src/upload_progress_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/upload_progress_indicator.dart @@ -59,7 +59,7 @@ class UploadProgressIndicator extends StatelessWidget { '${_percentage.toInt()}%', style: textStyle ?? theme.textTheme.footnote.copyWith( - color: theme.colorTheme.white, + color: theme.colorTheme.barsBg, ), ), ], diff --git a/packages/stream_chat_flutter/lib/src/url_attachment.dart b/packages/stream_chat_flutter/lib/src/url_attachment.dart index 4944c73c..5d5ca489 100644 --- a/packages/stream_chat_flutter/lib/src/url_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/url_attachment.dart @@ -60,7 +60,7 @@ class UrlAttachment extends StatelessWidget { borderRadius: const BorderRadius.only( topRight: Radius.circular(16), ), - color: chatThemeData.colorTheme.blueAlice, + color: chatThemeData.colorTheme.linkBg, ), child: Padding( padding: const EdgeInsets.only( @@ -71,7 +71,7 @@ class UrlAttachment extends StatelessWidget { child: Text( hostDisplayName, style: chatThemeData.textTheme.bodyBold.copyWith( - color: chatThemeData.colorTheme.accentBlue, + color: chatThemeData.colorTheme.accentPrimary, ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/user_avatar.dart b/packages/stream_chat_flutter/lib/src/user_avatar.dart index ecf1783f..31e07f0a 100644 --- a/packages/stream_chat_flutter/lib/src/user_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/user_avatar.dart @@ -70,7 +70,7 @@ class UserAvatar extends StatelessWidget { constraints: constraints ?? streamChatTheme.ownMessageTheme.avatarTheme?.constraints, decoration: BoxDecoration( - color: streamChatTheme.colorTheme.accentBlue, + color: streamChatTheme.colorTheme.accentPrimary, ), child: hasImage ? CachedNetworkImage( @@ -95,7 +95,7 @@ class UserAvatar extends StatelessWidget { child: Container( constraints: constraints ?? streamChatTheme.ownMessageTheme.avatarTheme?.constraints, - color: selectionColor ?? streamChatTheme.colorTheme.accentBlue, + color: selectionColor ?? streamChatTheme.colorTheme.accentPrimary, child: Padding( padding: EdgeInsets.all(selectionThickness), child: avatar, @@ -115,7 +115,7 @@ class UserAvatar extends StatelessWidget { alignment: onlineIndicatorAlignment, child: Material( type: MaterialType.circle, - color: streamChatTheme.colorTheme.white, + color: streamChatTheme.colorTheme.barsBg, child: Container( margin: const EdgeInsets.all(2), constraints: onlineIndicatorConstraints ?? @@ -125,7 +125,7 @@ class UserAvatar extends StatelessWidget { ), child: Material( shape: const CircleBorder(), - color: streamChatTheme.colorTheme.accentGreen, + color: streamChatTheme.colorTheme.accentInfo, ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/user_item.dart b/packages/stream_chat_flutter/lib/src/user_item.dart index e9d9f7c0..a13b544e 100644 --- a/packages/stream_chat_flutter/lib/src/user_item.dart +++ b/packages/stream_chat_flutter/lib/src/user_item.dart @@ -75,7 +75,7 @@ class UserItem extends StatelessWidget { ), trailing: selected ? StreamSvgIcon.checkSend( - color: chatThemeData.colorTheme.accentBlue, + color: chatThemeData.colorTheme.accentPrimary, ) : null, title: Text( @@ -92,8 +92,8 @@ class UserItem extends StatelessWidget { user.online == true ? 'Online' : 'Last online ${Jiffy(user.lastActive).fromNow()}', - style: chatTheme.textTheme.footnote - .copyWith(color: chatTheme.colorTheme.black.withOpacity(.5)), + style: chatTheme.textTheme.footnote.copyWith( + color: chatTheme.colorTheme.textHighEmphasis.withOpacity(.5)), ); } } diff --git a/packages/stream_chat_flutter/lib/src/user_list_view.dart b/packages/stream_chat_flutter/lib/src/user_list_view.dart index f64f3c76..923e0334 100644 --- a/packages/stream_chat_flutter/lib/src/user_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/user_list_view.dart @@ -285,7 +285,7 @@ class _UserListViewState extends State final chatThemeData = StreamChatTheme.of(context); return Container( key: ValueKey('HEADER-$header'), - color: chatThemeData.colorTheme.black.withOpacity(0.05), + color: chatThemeData.colorTheme.textHighEmphasis.withOpacity(0.05), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), child: Text( @@ -293,7 +293,7 @@ class _UserListViewState extends State style: TextStyle( fontWeight: FontWeight.bold, fontSize: 14.5, - color: chatThemeData.colorTheme.grey, + color: chatThemeData.colorTheme.textLowEmphasis, ), ), ), @@ -385,7 +385,7 @@ class _UserListViewState extends State return Container( color: StreamChatTheme.of(context) .colorTheme - .accentRed + .accentError .withOpacity(.2), child: const Padding( padding: EdgeInsets.symmetric(vertical: 16), @@ -408,6 +408,6 @@ class _UserListViewState extends State Widget _separatorBuilder(context, i) => Container( height: 1, - color: StreamChatTheme.of(context).colorTheme.greyWhisper, + color: StreamChatTheme.of(context).colorTheme.borders, ); } diff --git a/packages/stream_chat_flutter/lib/src/utils.dart b/packages/stream_chat_flutter/lib/src/utils.dart index d923b752..aee46800 100644 --- a/packages/stream_chat_flutter/lib/src/utils.dart +++ b/packages/stream_chat_flutter/lib/src/utils.dart @@ -31,7 +31,7 @@ Future showConfirmationDialog( final chatThemeData = StreamChatTheme.of(context); return showModalBottomSheet( useRootNavigator: false, - backgroundColor: chatThemeData.colorTheme.white, + backgroundColor: chatThemeData.colorTheme.barsBg, context: context, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( @@ -75,7 +75,7 @@ Future showConfirmationDialog( child: Text( cancelText, style: chatThemeData.textTheme.bodyBold.copyWith( - color: chatThemeData.colorTheme.black + color: chatThemeData.colorTheme.textHighEmphasis .withOpacity(0.5)), ), ), @@ -91,7 +91,7 @@ Future showConfirmationDialog( child: Text( okText, style: chatThemeData.textTheme.bodyBold.copyWith( - color: chatThemeData.colorTheme.accentRed), + color: chatThemeData.colorTheme.accentError), ), ), ), @@ -116,7 +116,8 @@ Future showInfoDialog( final chatThemeData = StreamChatTheme.of(context); return showModalBottomSheet( useRootNavigator: false, - backgroundColor: theme?.colorTheme.white ?? chatThemeData.colorTheme.white, + backgroundColor: + theme?.colorTheme.barsBg ?? chatThemeData.colorTheme.barsBg, context: context, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( @@ -147,8 +148,8 @@ Future showInfoDialog( height: 36, ), Container( - color: theme?.colorTheme.black.withOpacity(.08) ?? - chatThemeData.colorTheme.black.withOpacity(.08), + color: theme?.colorTheme.textHighEmphasis.withOpacity(.08) ?? + chatThemeData.colorTheme.textHighEmphasis.withOpacity(.08), height: 1, ), Center( @@ -159,8 +160,8 @@ Future showInfoDialog( child: Text( okText, style: TextStyle( - color: theme?.colorTheme.black.withOpacity(0.5) ?? - chatThemeData.colorTheme.accentBlue, + color: theme?.colorTheme.textHighEmphasis.withOpacity(0.5) ?? + chatThemeData.colorTheme.accentPrimary, fontWeight: FontWeight.w400, ), ), diff --git a/packages/stream_chat_flutter/lib/src/video_thumbnail_image.dart b/packages/stream_chat_flutter/lib/src/video_thumbnail_image.dart index 23cc0e35..2286bc3d 100644 --- a/packages/stream_chat_flutter/lib/src/video_thumbnail_image.dart +++ b/packages/stream_chat_flutter/lib/src/video_thumbnail_image.dart @@ -94,8 +94,8 @@ class _VideoThumbnailImageState extends State { constraints: const BoxConstraints.expand(), child: widget.placeholderBuilder?.call(context) ?? Shimmer.fromColors( - baseColor: _streamChatTheme.colorTheme.greyGainsboro, - highlightColor: _streamChatTheme.colorTheme.whiteSmoke, + baseColor: _streamChatTheme.colorTheme.disabled, + highlightColor: _streamChatTheme.colorTheme.inputBg, child: Image.asset( 'images/placeholder.png', fit: BoxFit.cover, diff --git a/packages/stream_chat_flutter_core/example/lib/main.dart b/packages/stream_chat_flutter_core/example/lib/main.dart index e7ef400a..e12d2177 100644 --- a/packages/stream_chat_flutter_core/example/lib/main.dart +++ b/packages/stream_chat_flutter_core/example/lib/main.dart @@ -203,9 +203,9 @@ class _MessageScreenState extends State { final channel = StreamChannel.of(context).channel; return Scaffold( appBar: AppBar( - title: StreamBuilder>( - initialData: channel.state?.typingEvents, - stream: channel.state?.typingEventsStream, + title: StreamBuilder>( + initialData: channel.state?.typingEvents.keys, + stream: channel.state?.typingEventsStream.map((it) => it.keys), builder: (context, snapshot) { if (snapshot.hasData && snapshot.data!.isNotEmpty) { return Text('${snapshot.data!.first.name} is typing...'); @@ -344,7 +344,7 @@ extension on Channel { String? get name { final _channelName = extraData['name']; if (_channelName != null) { - return _channelName; + return _channelName as String; } else { return cid; } From d043d1ca0f73513c25840ad22e682a69cdde853e Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 2 Jul 2021 15:33:03 +0200 Subject: [PATCH 149/150] export stream_chat_api --- packages/stream_chat/lib/stream_chat.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/stream_chat/lib/stream_chat.dart b/packages/stream_chat/lib/stream_chat.dart index 3c2e3d21..bb2a01de 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -13,6 +13,7 @@ export './src/core/api/attachment_file_uploader.dart' export './src/core/api/requests.dart'; export './src/core/api/requests.dart'; export './src/core/api/responses.dart'; +export './src/core/api/stream_chat_api.dart'; export './src/core/error/error.dart'; export './src/core/models/action.dart'; export './src/core/models/attachment.dart'; From b64631efee8ac438215b7e7a8b1e854cb469f09f Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 2 Jul 2021 15:55:18 +0200 Subject: [PATCH 150/150] Update packages/stream_chat/lib/stream_chat.dart Co-authored-by: Sahil Kumar --- packages/stream_chat/lib/stream_chat.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat/lib/stream_chat.dart b/packages/stream_chat/lib/stream_chat.dart index bb2a01de..571d78f5 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -13,7 +13,7 @@ export './src/core/api/attachment_file_uploader.dart' export './src/core/api/requests.dart'; export './src/core/api/requests.dart'; export './src/core/api/responses.dart'; -export './src/core/api/stream_chat_api.dart'; +export './src/core/api/stream_chat_api.dart' show PushProvider; export './src/core/error/error.dart'; export './src/core/models/action.dart'; export './src/core/models/attachment.dart';