Merge branch 'develop' into perf
This commit is contained in:
@@ -510,9 +510,9 @@ class Channel {
|
|||||||
|
|
||||||
/// Pins provided message
|
/// Pins provided message
|
||||||
Future<UpdateMessageResponse> pinMessage(
|
Future<UpdateMessageResponse> pinMessage(
|
||||||
Message message,
|
Message message, [
|
||||||
Object? timeoutOrExpirationDate,
|
Object? timeoutOrExpirationDate,
|
||||||
) {
|
]) {
|
||||||
assert(() {
|
assert(() {
|
||||||
if (timeoutOrExpirationDate is! DateTime &&
|
if (timeoutOrExpirationDate is! DateTime &&
|
||||||
timeoutOrExpirationDate != null &&
|
timeoutOrExpirationDate != null &&
|
||||||
@@ -520,7 +520,7 @@ class Channel {
|
|||||||
throw ArgumentError('Invalid timeout or Expiration date');
|
throw ArgumentError('Invalid timeout or Expiration date');
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}(), 'Check for invalid token or expiration date');
|
}(), 'Check whether timeout is valid');
|
||||||
|
|
||||||
DateTime? pinExpires;
|
DateTime? pinExpires;
|
||||||
if (timeoutOrExpirationDate is DateTime) {
|
if (timeoutOrExpirationDate is DateTime) {
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ class WebSocket {
|
|||||||
/// connection unhealthy
|
/// connection unhealthy
|
||||||
final int reconnectionMonitorTimeout;
|
final int reconnectionMonitorTimeout;
|
||||||
|
|
||||||
final _connectionStatusController =
|
final BehaviorSubject<ConnectionStatus> _connectionStatusController =
|
||||||
BehaviorSubject.seeded(ConnectionStatus.disconnected);
|
BehaviorSubject.seeded(ConnectionStatus.disconnected);
|
||||||
|
|
||||||
set _connectionStatus(ConnectionStatus status) =>
|
set _connectionStatus(ConnectionStatus status) =>
|
||||||
|
|||||||
@@ -1361,12 +1361,13 @@ class StreamChatClient {
|
|||||||
/// [timeoutOrExpirationDate] can either be a [DateTime] or a value in seconds
|
/// [timeoutOrExpirationDate] can either be a [DateTime] or a value in seconds
|
||||||
/// to be added to [DateTime.now]
|
/// to be added to [DateTime.now]
|
||||||
Future<UpdateMessageResponse> pinMessage(
|
Future<UpdateMessageResponse> pinMessage(
|
||||||
Message message,
|
Message message, [
|
||||||
Object timeoutOrExpirationDate,
|
Object? timeoutOrExpirationDate,
|
||||||
) {
|
]) {
|
||||||
assert(() {
|
assert(() {
|
||||||
if (timeoutOrExpirationDate is! DateTime &&
|
if (timeoutOrExpirationDate is! DateTime &&
|
||||||
timeoutOrExpirationDate is! num) {
|
timeoutOrExpirationDate is! num &&
|
||||||
|
timeoutOrExpirationDate != null) {
|
||||||
throw ArgumentError('Invalid timeout or Expiration date');
|
throw ArgumentError('Invalid timeout or Expiration date');
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -1383,13 +1384,19 @@ class StreamChatClient {
|
|||||||
.toUtc();
|
.toUtc();
|
||||||
}
|
}
|
||||||
return updateMessage(
|
return updateMessage(
|
||||||
message.copyWith(pinned: true, pinExpires: pinExpires),
|
message.copyWith(
|
||||||
|
pinned: true,
|
||||||
|
pinExpires: pinExpires,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Unpins provided message
|
/// Unpins provided message
|
||||||
Future<UpdateMessageResponse> unpinMessage(Message message) =>
|
Future<UpdateMessageResponse> unpinMessage(Message message) => updateMessage(
|
||||||
updateMessage(message.copyWith(pinned: false));
|
message.copyWith(
|
||||||
|
pinned: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The class that handles the state of the channel listening to the events
|
/// The class that handles the state of the channel listening to the events
|
||||||
|
|||||||
@@ -449,6 +449,51 @@ void main() {
|
|||||||
.called(1);
|
.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<String>(
|
||||||
|
any(),
|
||||||
|
data: any(named: 'data'),
|
||||||
|
)).thenAnswer((_) async => Response(
|
||||||
|
data: jsonEncode(ChannelState()),
|
||||||
|
statusCode: 200,
|
||||||
|
requestOptions: FakeRequestOptions(),
|
||||||
|
));
|
||||||
|
|
||||||
|
await channelClient.watch();
|
||||||
|
|
||||||
|
when(
|
||||||
|
() => mockDio.post<String>(
|
||||||
|
'/messages/${message.id}',
|
||||||
|
data: anything,
|
||||||
|
),
|
||||||
|
).thenAnswer(
|
||||||
|
(_) async => Response(
|
||||||
|
data: jsonEncode({'message': message}),
|
||||||
|
statusCode: 200,
|
||||||
|
requestOptions: FakeRequestOptions(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await channelClient.pinMessage(message);
|
||||||
|
|
||||||
|
verify(() =>
|
||||||
|
mockDio.post<String>('/messages/${message.id}', data: anything))
|
||||||
|
.called(1);
|
||||||
|
});
|
||||||
|
|
||||||
test('should be unpinned successfully', () async {
|
test('should be unpinned successfully', () async {
|
||||||
final mockDio = MockDio();
|
final mockDio = MockDio();
|
||||||
|
|
||||||
|
|||||||
@@ -1112,6 +1112,28 @@ void main() {
|
|||||||
data: {'message': anything})).called(1);
|
data: {'message': anything})).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should complete successfully with a null value', () async {
|
||||||
|
final message = Message(text: 'Hello');
|
||||||
|
|
||||||
|
when(
|
||||||
|
() => mockDio.post<String>(
|
||||||
|
'/messages/${message.id}',
|
||||||
|
data: anything,
|
||||||
|
),
|
||||||
|
).thenAnswer(
|
||||||
|
(_) async => Response(
|
||||||
|
data: jsonEncode({'message': message}),
|
||||||
|
statusCode: 200,
|
||||||
|
requestOptions: FakeRequestOptions(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await client.pinMessage(message);
|
||||||
|
|
||||||
|
verify(() => mockDio.post<String>('/messages/${message.id}',
|
||||||
|
data: {'message': anything})).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
test('should unpin message successfully', () async {
|
test('should unpin message successfully', () async {
|
||||||
final message = Message(text: 'Hello');
|
final message = Message(text: 'Hello');
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import 'package:stream_chat_persistence/stream_chat_persistence.dart';
|
|||||||
|
|
||||||
final chatPersistentClient = StreamChatPersistenceClient(
|
final chatPersistentClient = StreamChatPersistenceClient(
|
||||||
logLevel: Level.INFO,
|
logLevel: Level.INFO,
|
||||||
connectionMode: ConnectionMode.background,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
showMessage: showConnectionStateTile ? showStatus : false,
|
showMessage: showConnectionStateTile ? showStatus : false,
|
||||||
message: statusString,
|
message: statusString,
|
||||||
child: AppBar(
|
child: AppBar(
|
||||||
|
textTheme: Theme.of(context).textTheme,
|
||||||
brightness: Theme.of(context).brightness,
|
brightness: Theme.of(context).brightness,
|
||||||
elevation: 1,
|
elevation: 1,
|
||||||
leading: leadingWidget,
|
leading: leadingWidget,
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
showMessage: showConnectionStateTile ? showStatus : false,
|
showMessage: showConnectionStateTile ? showStatus : false,
|
||||||
message: statusString,
|
message: statusString,
|
||||||
child: AppBar(
|
child: AppBar(
|
||||||
|
textTheme: Theme.of(context).textTheme,
|
||||||
brightness: Theme.of(context).brightness,
|
brightness: Theme.of(context).brightness,
|
||||||
elevation: 1,
|
elevation: 1,
|
||||||
backgroundColor: chatThemeData.channelListHeaderTheme.color,
|
backgroundColor: chatThemeData.channelListHeaderTheme.color,
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
|||||||
/// Callback called when tapping on a channel
|
/// Callback called when tapping on a channel
|
||||||
typedef ChannelTapCallback = void Function(Channel, Widget?);
|
typedef ChannelTapCallback = void Function(Channel, Widget?);
|
||||||
|
|
||||||
|
/// Callback called when tapping on a channel
|
||||||
|
typedef ChannelInfoCallback = void Function(Channel);
|
||||||
|
|
||||||
/// Builder used to create a custom [ChannelPreview] from a [Channel]
|
/// Builder used to create a custom [ChannelPreview] from a [Channel]
|
||||||
typedef ChannelPreviewBuilder = Widget Function(BuildContext, Channel);
|
typedef ChannelPreviewBuilder = Widget Function(BuildContext, Channel);
|
||||||
|
|
||||||
@@ -78,6 +81,9 @@ class ChannelListView extends StatefulWidget {
|
|||||||
this.emptyBuilder,
|
this.emptyBuilder,
|
||||||
this.loadingBuilder,
|
this.loadingBuilder,
|
||||||
this.listBuilder,
|
this.listBuilder,
|
||||||
|
this.onMoreDetailsPressed,
|
||||||
|
this.onDeletePressed,
|
||||||
|
this.swipeActions,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
/// If true a default swipe to action behaviour will be added to this widget
|
/// If true a default swipe to action behaviour will be added to this widget
|
||||||
@@ -158,6 +164,15 @@ class ChannelListView extends StatefulWidget {
|
|||||||
/// The builder used when the channel list is empty.
|
/// The builder used when the channel list is empty.
|
||||||
final WidgetBuilder? emptyBuilder;
|
final WidgetBuilder? emptyBuilder;
|
||||||
|
|
||||||
|
/// Callback used when the more details slidable option is pressed
|
||||||
|
final ChannelInfoCallback? onMoreDetailsPressed;
|
||||||
|
|
||||||
|
/// Callback used when the delete slidable option is pressed
|
||||||
|
final ChannelInfoCallback? onDeletePressed;
|
||||||
|
|
||||||
|
/// List of actions for slidable
|
||||||
|
final List<SwipeAction>? swipeActions;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_ChannelListViewState createState() => _ChannelListViewState();
|
_ChannelListViewState createState() => _ChannelListViewState();
|
||||||
}
|
}
|
||||||
@@ -466,61 +481,79 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
enabled: widget.swipeToAction,
|
enabled: widget.swipeToAction,
|
||||||
actionPane: const SlidableBehindActionPane(),
|
actionPane: const SlidableBehindActionPane(),
|
||||||
actionExtentRatio: 0.12,
|
actionExtentRatio: 0.12,
|
||||||
secondaryActions: <Widget>[
|
secondaryActions: widget.swipeActions
|
||||||
IconSlideAction(
|
?.map((e) => IconSlideAction(
|
||||||
color: backgroundColor,
|
color: e.color,
|
||||||
icon: Icons.more_horiz,
|
iconWidget: e.iconWidget,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
showModalBottomSheet(
|
e.onTap?.call(channel);
|
||||||
clipBehavior: Clip.hardEdge,
|
},
|
||||||
shape: const RoundedRectangleBorder(
|
))
|
||||||
borderRadius: BorderRadius.only(
|
.toList() ??
|
||||||
topLeft: Radius.circular(32),
|
<Widget>[
|
||||||
topRight: Radius.circular(32),
|
IconSlideAction(
|
||||||
),
|
color: backgroundColor,
|
||||||
),
|
icon: Icons.more_horiz,
|
||||||
context: context,
|
onTap: widget.onMoreDetailsPressed != null
|
||||||
builder: (context) => StreamChannel(
|
? () {
|
||||||
channel: channel,
|
widget.onMoreDetailsPressed!(channel);
|
||||||
child: ChannelBottomSheet(
|
}
|
||||||
onViewInfoTap: () {
|
: () {
|
||||||
widget.onViewInfoTap?.call(channel);
|
showModalBottomSheet(
|
||||||
},
|
clipBehavior: Clip.hardEdge,
|
||||||
),
|
shape: const RoundedRectangleBorder(
|
||||||
),
|
borderRadius: BorderRadius.only(
|
||||||
);
|
topLeft: Radius.circular(32),
|
||||||
},
|
topRight: Radius.circular(32),
|
||||||
),
|
),
|
||||||
if ([
|
),
|
||||||
'admin',
|
context: context,
|
||||||
'owner',
|
builder: (context) => StreamChannel(
|
||||||
].contains(channel.state!.members
|
channel: channel,
|
||||||
.firstWhereOrNull(
|
child: ChannelBottomSheet(
|
||||||
(m) => m.userId == channel.client.state.user?.id)
|
onViewInfoTap: () {
|
||||||
?.role))
|
widget.onViewInfoTap?.call(channel);
|
||||||
IconSlideAction(
|
},
|
||||||
color: backgroundColor,
|
),
|
||||||
iconWidget: StreamSvgIcon.delete(
|
),
|
||||||
color: chatThemeData.colorTheme.accentRed,
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
onTap: () async {
|
if ([
|
||||||
final res = await showConfirmationDialog(
|
'admin',
|
||||||
context,
|
'owner',
|
||||||
title: 'Delete Conversation',
|
].contains(channel.state!.members
|
||||||
okText: 'DELETE',
|
.firstWhereOrNull(
|
||||||
question:
|
(m) => m.userId == channel.client.state.user?.id)
|
||||||
'Are you sure you want to delete this conversation?',
|
?.role))
|
||||||
cancelText: 'CANCEL',
|
IconSlideAction(
|
||||||
icon: StreamSvgIcon.delete(
|
color: backgroundColor,
|
||||||
|
iconWidget: StreamSvgIcon.delete(
|
||||||
color: chatThemeData.colorTheme.accentRed,
|
color: chatThemeData.colorTheme.accentRed,
|
||||||
),
|
),
|
||||||
);
|
onTap: widget.onDeletePressed != null
|
||||||
if (res == true) {
|
? () {
|
||||||
await channel.delete();
|
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: Container(
|
||||||
color: chatThemeData.colorTheme.whiteSnow,
|
color: chatThemeData.colorTheme.whiteSnow,
|
||||||
child: widget.channelPreviewBuilder?.call(context, channel) ??
|
child: widget.channelPreviewBuilder?.call(context, channel) ??
|
||||||
@@ -641,3 +674,22 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Class for slidable action
|
||||||
|
class SwipeAction {
|
||||||
|
/// Constructor for creating [SwipeAction]
|
||||||
|
SwipeAction({
|
||||||
|
this.color,
|
||||||
|
required this.iconWidget,
|
||||||
|
this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Background color of action
|
||||||
|
Color? color;
|
||||||
|
|
||||||
|
/// Widget to display as icon
|
||||||
|
Widget iconWidget;
|
||||||
|
|
||||||
|
/// Callback when icon is tapped
|
||||||
|
ChannelInfoCallback? onTap;
|
||||||
|
}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final chatThemeData = StreamChatTheme.of(context);
|
final chatThemeData = StreamChatTheme.of(context);
|
||||||
return AppBar(
|
return AppBar(
|
||||||
|
textTheme: Theme.of(context).textTheme,
|
||||||
brightness: Theme.of(context).brightness,
|
brightness: Theme.of(context).brightness,
|
||||||
elevation: 1,
|
elevation: 1,
|
||||||
leading: showBackButton
|
leading: showBackButton
|
||||||
|
|||||||
@@ -571,9 +571,9 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
return const SizedBox();
|
return const SizedBox();
|
||||||
}
|
}
|
||||||
|
|
||||||
var index = _getTopElement(values).index;
|
var index = _getTopElement(values)?.index;
|
||||||
|
|
||||||
if (index > messages.length) {
|
if (index == null || index > messages.length) {
|
||||||
return const SizedBox();
|
return const SizedBox();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -599,10 +599,17 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
StreamChannelState? channel, QueryDirection direction) =>
|
StreamChannelState? channel, QueryDirection direction) =>
|
||||||
_messageListController.paginateData!(direction: direction);
|
_messageListController.paginateData!(direction: direction);
|
||||||
|
|
||||||
ItemPosition _getTopElement(Iterable<ItemPosition> values) => values
|
ItemPosition? _getTopElement(Iterable<ItemPosition> values) {
|
||||||
.where((ItemPosition position) => position.itemLeadingEdge < 0.9)
|
final inView =
|
||||||
.reduce((ItemPosition max, ItemPosition position) =>
|
values.where((ItemPosition position) => position.itemLeadingEdge < 0.9);
|
||||||
position.itemLeadingEdge > max.itemLeadingEdge ? position : max);
|
|
||||||
|
if (inView.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return inView.reduce((ItemPosition max, ItemPosition position) =>
|
||||||
|
position.itemLeadingEdge > max.itemLeadingEdge ? position : max);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildScrollToBottom() => StreamBuilder<Tuple2<bool, int>>(
|
Widget _buildScrollToBottom() => StreamBuilder<Tuple2<bool, int>>(
|
||||||
stream: Rx.combineLatest2(
|
stream: Rx.combineLatest2(
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class StreamChat extends StatefulWidget {
|
|||||||
this.streamChatThemeData,
|
this.streamChatThemeData,
|
||||||
this.onBackgroundEventReceived,
|
this.onBackgroundEventReceived,
|
||||||
this.backgroundKeepAlive = const Duration(minutes: 1),
|
this.backgroundKeepAlive = const Duration(minutes: 1),
|
||||||
|
this.connectivityStream,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
/// Client to do chat ops with
|
/// Client to do chat ops with
|
||||||
@@ -59,6 +60,11 @@ class StreamChat extends StatefulWidget {
|
|||||||
/// upon the [Event.type]
|
/// upon the [Event.type]
|
||||||
final EventHandler? onBackgroundEventReceived;
|
final EventHandler? onBackgroundEventReceived;
|
||||||
|
|
||||||
|
/// Stream of connectivity result
|
||||||
|
/// Visible for testing
|
||||||
|
@visibleForTesting
|
||||||
|
final Stream<ConnectivityResult>? connectivityStream;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
StreamChatState createState() => StreamChatState();
|
StreamChatState createState() => StreamChatState();
|
||||||
|
|
||||||
@@ -102,6 +108,7 @@ class StreamChatState extends State<StreamChat> {
|
|||||||
client: client,
|
client: client,
|
||||||
onBackgroundEventReceived: widget.onBackgroundEventReceived,
|
onBackgroundEventReceived: widget.onBackgroundEventReceived,
|
||||||
backgroundKeepAlive: widget.backgroundKeepAlive,
|
backgroundKeepAlive: widget.backgroundKeepAlive,
|
||||||
|
connectivityStream: widget.connectivityStream,
|
||||||
child: widget.child ?? const Offstage(),
|
child: widget.child ?? const Offstage(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -230,22 +230,23 @@ class StreamChatThemeData {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
channelPreviewTheme: ChannelPreviewTheme(
|
channelPreviewTheme: ChannelPreviewTheme(
|
||||||
unreadCounterColor: colorTheme.accentRed,
|
unreadCounterColor: colorTheme.accentRed,
|
||||||
avatarTheme: AvatarTheme(
|
avatarTheme: AvatarTheme(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
constraints: const BoxConstraints.tightFor(
|
constraints: const BoxConstraints.tightFor(
|
||||||
height: 40,
|
height: 40,
|
||||||
width: 40,
|
width: 40,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
title: textTheme.bodyBold,
|
),
|
||||||
subtitle: textTheme.footnote.copyWith(
|
title: textTheme.bodyBold,
|
||||||
color: const Color(0xff7A7A7A),
|
subtitle: textTheme.footnote.copyWith(
|
||||||
),
|
color: const Color(0xff7A7A7A),
|
||||||
lastMessageAt: textTheme.footnote.copyWith(
|
),
|
||||||
color: colorTheme.black.withOpacity(.5),
|
lastMessageAt: textTheme.footnote.copyWith(
|
||||||
),
|
color: colorTheme.black.withOpacity(.5),
|
||||||
indicatorIconSize: 16),
|
),
|
||||||
|
indicatorIconSize: 16,
|
||||||
|
),
|
||||||
channelListHeaderTheme: ChannelListHeaderTheme(
|
channelListHeaderTheme: ChannelListHeaderTheme(
|
||||||
avatarTheme: AvatarTheme(
|
avatarTheme: AvatarTheme(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
@@ -1186,7 +1187,8 @@ class MessageInputTheme {
|
|||||||
actionButtonIdleColor: other.actionButtonIdleColor,
|
actionButtonIdleColor: other.actionButtonIdleColor,
|
||||||
sendButtonColor: other.sendButtonColor,
|
sendButtonColor: other.sendButtonColor,
|
||||||
sendButtonIdleColor: other.sendButtonIdleColor,
|
sendButtonIdleColor: other.sendButtonIdleColor,
|
||||||
inputTextStyle: other.inputTextStyle,
|
inputTextStyle:
|
||||||
|
inputTextStyle?.merge(other.inputTextStyle) ?? other.inputTextStyle,
|
||||||
inputDecoration: inputDecoration?.merge(other.inputDecoration) ??
|
inputDecoration: inputDecoration?.merge(other.inputDecoration) ??
|
||||||
other.inputDecoration,
|
other.inputDecoration,
|
||||||
activeBorderGradient: other.activeBorderGradient,
|
activeBorderGradient: other.activeBorderGradient,
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
final chatThemeData = StreamChatTheme.of(context);
|
final chatThemeData = StreamChatTheme.of(context);
|
||||||
return AppBar(
|
return AppBar(
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
|
textTheme: Theme.of(context).textTheme,
|
||||||
brightness: Theme.of(context).brightness,
|
brightness: Theme.of(context).brightness,
|
||||||
elevation: 1,
|
elevation: 1,
|
||||||
leading: leading ??
|
leading: leading ??
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
connectivityStream: Stream.value(ConnectivityResult.mobile),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
surfaceSize: const Size.square(200),
|
surfaceSize: const Size.square(200),
|
||||||
@@ -131,6 +132,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
connectivityStream: Stream.value(ConnectivityResult.mobile),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
surfaceSize: const Size.square(200),
|
surfaceSize: const Size.square(200),
|
||||||
@@ -187,6 +189,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
connectivityStream: Stream.value(ConnectivityResult.mobile),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
surfaceSize: const Size.square(200),
|
surfaceSize: const Size.square(200),
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ void main() {
|
|||||||
maskColor: theme.ownMessageTheme.reactionsMaskColor!,
|
maskColor: theme.ownMessageTheme.reactionsMaskColor!,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
connectivityStream: Stream.value(ConnectivityResult.mobile),
|
||||||
),
|
),
|
||||||
surfaceSize: const Size(100, 100),
|
surfaceSize: const Size(100, 100),
|
||||||
);
|
);
|
||||||
@@ -96,6 +97,7 @@ void main() {
|
|||||||
maskColor: theme.ownMessageTheme.reactionsMaskColor!,
|
maskColor: theme.ownMessageTheme.reactionsMaskColor!,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
connectivityStream: Stream.value(ConnectivityResult.mobile),
|
||||||
),
|
),
|
||||||
surfaceSize: const Size(100, 100),
|
surfaceSize: const Size(100, 100),
|
||||||
);
|
);
|
||||||
@@ -118,6 +120,7 @@ void main() {
|
|||||||
StreamChat(
|
StreamChat(
|
||||||
client: client,
|
client: client,
|
||||||
streamChatThemeData: StreamChatThemeData.fromTheme(themeData),
|
streamChatThemeData: StreamChatThemeData.fromTheme(themeData),
|
||||||
|
connectivityStream: Stream.value(ConnectivityResult.mobile),
|
||||||
child: Container(
|
child: Container(
|
||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
child: ReactionBubble(
|
child: ReactionBubble(
|
||||||
@@ -162,6 +165,7 @@ void main() {
|
|||||||
StreamChat(
|
StreamChat(
|
||||||
client: client,
|
client: client,
|
||||||
streamChatThemeData: StreamChatThemeData.fromTheme(themeData),
|
streamChatThemeData: StreamChatThemeData.fromTheme(themeData),
|
||||||
|
connectivityStream: Stream.value(ConnectivityResult.mobile),
|
||||||
child: Container(
|
child: Container(
|
||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
child: ReactionBubble(
|
child: ReactionBubble(
|
||||||
@@ -204,6 +208,7 @@ void main() {
|
|||||||
await tester.pumpWidgetBuilder(
|
await tester.pumpWidgetBuilder(
|
||||||
StreamChat(
|
StreamChat(
|
||||||
client: client,
|
client: client,
|
||||||
|
connectivityStream: Stream.value(ConnectivityResult.mobile),
|
||||||
streamChatThemeData: StreamChatThemeData.fromTheme(themeData),
|
streamChatThemeData: StreamChatThemeData.fromTheme(themeData),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
child: ReactionBubble(
|
child: ReactionBubble(
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
connectivityStream: Stream.value(ConnectivityResult.mobile),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
surfaceSize: const Size.square(200),
|
surfaceSize: const Size.square(200),
|
||||||
@@ -151,6 +152,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
connectivityStream: Stream.value(ConnectivityResult.mobile),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
surfaceSize: const Size.square(200),
|
surfaceSize: const Size.square(200),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat/stream_chat.dart';
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
@@ -43,6 +44,7 @@ class StreamChatCore extends StatefulWidget {
|
|||||||
required this.child,
|
required this.child,
|
||||||
this.onBackgroundEventReceived,
|
this.onBackgroundEventReceived,
|
||||||
this.backgroundKeepAlive = const Duration(minutes: 1),
|
this.backgroundKeepAlive = const Duration(minutes: 1),
|
||||||
|
this.connectivityStream,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
/// Instance of Stream Chat Client containing information about the current
|
/// Instance of Stream Chat Client containing information about the current
|
||||||
@@ -61,6 +63,11 @@ class StreamChatCore extends StatefulWidget {
|
|||||||
/// upon the [Event.type]
|
/// upon the [Event.type]
|
||||||
final EventHandler? onBackgroundEventReceived;
|
final EventHandler? onBackgroundEventReceived;
|
||||||
|
|
||||||
|
/// Stream of connectivity result
|
||||||
|
/// Visible for testing
|
||||||
|
@visibleForTesting
|
||||||
|
final Stream<ConnectivityResult>? connectivityStream;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
StreamChatCoreState createState() => StreamChatCoreState();
|
StreamChatCoreState createState() => StreamChatCoreState();
|
||||||
|
|
||||||
@@ -96,48 +103,104 @@ class StreamChatCoreState extends State<StreamChatCore>
|
|||||||
/// The current user as a stream
|
/// The current user as a stream
|
||||||
Stream<User?> get userStream => client.state.userStream;
|
Stream<User?> get userStream => client.state.userStream;
|
||||||
|
|
||||||
|
StreamSubscription<ConnectivityResult>? _connectivitySubscription;
|
||||||
|
|
||||||
|
var _isInForeground = true;
|
||||||
|
var _isConnectionAvailable = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance?.addObserver(this);
|
WidgetsBinding.instance?.addObserver(this);
|
||||||
|
_subscribeToConnectivityChange(widget.connectivityStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _subscribeToConnectivityChange([
|
||||||
|
Stream<ConnectivityResult>? connectivityStream,
|
||||||
|
]) {
|
||||||
|
if (_connectivitySubscription == null) {
|
||||||
|
connectivityStream ??= Connectivity().onConnectivityChanged;
|
||||||
|
_connectivitySubscription =
|
||||||
|
connectivityStream.distinct().listen((result) {
|
||||||
|
_isConnectionAvailable = result != ConnectivityResult.none;
|
||||||
|
if (!_isInForeground) return;
|
||||||
|
if (_isConnectionAvailable) {
|
||||||
|
if (client.wsConnectionStatus == ConnectionStatus.disconnected) {
|
||||||
|
client.connect();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (client.wsConnectionStatus == ConnectionStatus.connected) {
|
||||||
|
client.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _unsubscribeFromConnectivityChange() {
|
||||||
|
if (_connectivitySubscription != null) {
|
||||||
|
_connectivitySubscription?.cancel();
|
||||||
|
_connectivitySubscription = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(StreamChatCore oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
final connectivityStream = widget.connectivityStream;
|
||||||
|
if (connectivityStream != oldWidget.connectivityStream) {
|
||||||
|
_unsubscribeFromConnectivityChange();
|
||||||
|
_subscribeToConnectivityChange(connectivityStream);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StreamSubscription? _eventSubscription;
|
StreamSubscription? _eventSubscription;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
_isInForeground = state == AppLifecycleState.resumed;
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
if (state == AppLifecycleState.paused) {
|
if (_isInForeground) {
|
||||||
if (widget.onBackgroundEventReceived == null) {
|
_onForeground();
|
||||||
client.disconnect();
|
} else {
|
||||||
return;
|
_onBackground();
|
||||||
}
|
|
||||||
_eventSubscription = client.on().listen(
|
|
||||||
widget.onBackgroundEventReceived,
|
|
||||||
);
|
|
||||||
|
|
||||||
void onTimerComplete() {
|
|
||||||
_eventSubscription?.cancel();
|
|
||||||
client.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
_disconnectTimer = Timer(widget.backgroundKeepAlive, onTimerComplete);
|
|
||||||
} else if (state == AppLifecycleState.resumed) {
|
|
||||||
if (_disconnectTimer?.isActive == true) {
|
|
||||||
_eventSubscription?.cancel();
|
|
||||||
_disconnectTimer?.cancel();
|
|
||||||
} else {
|
|
||||||
if (client.wsConnectionStatus == ConnectionStatus.disconnected) {
|
|
||||||
client.connect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onForeground() {
|
||||||
|
if (_disconnectTimer?.isActive == true) {
|
||||||
|
_eventSubscription?.cancel();
|
||||||
|
_disconnectTimer?.cancel();
|
||||||
|
} else if (client.wsConnectionStatus == ConnectionStatus.disconnected &&
|
||||||
|
_isConnectionAvailable) {
|
||||||
|
client.connect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onBackground() {
|
||||||
|
if (widget.onBackgroundEventReceived == null) {
|
||||||
|
if (client.wsConnectionStatus != ConnectionStatus.disconnected) {
|
||||||
|
client.disconnect();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_eventSubscription = client.on().listen(widget.onBackgroundEventReceived);
|
||||||
|
|
||||||
|
void onTimerComplete() {
|
||||||
|
_eventSubscription?.cancel();
|
||||||
|
client.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
_disconnectTimer = Timer(widget.backgroundKeepAlive, onTimerComplete);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
WidgetsBinding.instance?.removeObserver(this);
|
WidgetsBinding.instance?.removeObserver(this);
|
||||||
|
_unsubscribeFromConnectivityChange();
|
||||||
_eventSubscription?.cancel();
|
_eventSubscription?.cancel();
|
||||||
_disconnectTimer?.cancel();
|
_disconnectTimer?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
library stream_chat_flutter_core;
|
library stream_chat_flutter_core;
|
||||||
|
|
||||||
|
export 'package:connectivity_plus/connectivity_plus.dart';
|
||||||
export 'package:stream_chat/stream_chat.dart';
|
export 'package:stream_chat/stream_chat.dart';
|
||||||
|
|
||||||
export 'src/better_stream_builder.dart';
|
export 'src/better_stream_builder.dart';
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ environment:
|
|||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
collection: ^1.15.0
|
collection: ^1.15.0
|
||||||
|
connectivity_plus: ^1.0.1
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
meta: ^1.3.0
|
meta: ^1.3.0
|
||||||
@@ -21,5 +22,5 @@ dev_dependencies:
|
|||||||
fake_async: ^1.2.0
|
fake_async: ^1.2.0
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
mocktail: ^0.1.1
|
mocktail: ^0.1.3
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
import 'package:rxdart/rxdart.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
import 'mocks.dart';
|
import 'mocks.dart';
|
||||||
@@ -100,6 +102,7 @@ void main() {
|
|||||||
child: Offstage(key: childKey),
|
child: Offstage(key: childKey),
|
||||||
onBackgroundEventReceived: mockOnBackgroundEventReceived,
|
onBackgroundEventReceived: mockOnBackgroundEventReceived,
|
||||||
backgroundKeepAlive: backgroundKeepAlive,
|
backgroundKeepAlive: backgroundKeepAlive,
|
||||||
|
connectivityStream: Stream.value(ConnectivityResult.mobile),
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.pumpWidget(streamChatCore);
|
await tester.pumpWidget(streamChatCore);
|
||||||
@@ -191,6 +194,7 @@ void main() {
|
|||||||
key: streamChatCoreKey,
|
key: streamChatCoreKey,
|
||||||
client: mockClient,
|
client: mockClient,
|
||||||
child: Offstage(key: childKey),
|
child: Offstage(key: childKey),
|
||||||
|
connectivityStream: Stream.value(ConnectivityResult.mobile),
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.pumpWidget(streamChatCore);
|
await tester.pumpWidget(streamChatCore);
|
||||||
@@ -222,6 +226,51 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'didChangeAppLifecycleState should not call client.connect() '
|
||||||
|
'if connection is not available in case the '
|
||||||
|
'widget lifestyle changes to AppLifecycleState.resume',
|
||||||
|
(tester) async {
|
||||||
|
await tester.runAsync(() async {
|
||||||
|
final mockClient = MockClient();
|
||||||
|
const streamChatCoreKey = Key('streamChatCore');
|
||||||
|
const childKey = Key('child');
|
||||||
|
final streamChatCore = StreamChatCore(
|
||||||
|
key: streamChatCoreKey,
|
||||||
|
client: mockClient,
|
||||||
|
child: Offstage(key: childKey),
|
||||||
|
connectivityStream: Stream.value(ConnectivityResult.none),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(streamChatCore);
|
||||||
|
|
||||||
|
expect(find.byKey(streamChatCoreKey), findsOneWidget);
|
||||||
|
expect(find.byKey(childKey), findsOneWidget);
|
||||||
|
|
||||||
|
final event = Event();
|
||||||
|
when(() => mockClient.on()).thenAnswer((_) => Stream.value(event));
|
||||||
|
when(() => mockClient.connect()).thenAnswer((_) async => event);
|
||||||
|
when(mockClient.disconnect).thenAnswer((_) async => null);
|
||||||
|
when(() => mockClient.wsConnectionStatus)
|
||||||
|
.thenReturn(ConnectionStatus.disconnected);
|
||||||
|
|
||||||
|
final streamChatCoreState = tester.state<StreamChatCoreState>(
|
||||||
|
find.byKey(streamChatCoreKey),
|
||||||
|
);
|
||||||
|
|
||||||
|
streamChatCoreState
|
||||||
|
.didChangeAppLifecycleState(AppLifecycleState.paused);
|
||||||
|
|
||||||
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
|
||||||
|
streamChatCoreState
|
||||||
|
.didChangeAppLifecycleState(AppLifecycleState.resumed);
|
||||||
|
|
||||||
|
verifyNever(() => mockClient.connect());
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'streamChatCoreState.userStream should emit all the user events '
|
'streamChatCoreState.userStream should emit all the user events '
|
||||||
'provided by client',
|
'provided by client',
|
||||||
@@ -264,4 +313,124 @@ void main() {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'should call connect if in foreground and connection is back',
|
||||||
|
(tester) async {
|
||||||
|
await tester.runAsync(() async {
|
||||||
|
final mockClient = MockClient();
|
||||||
|
const streamChatCoreKey = Key('streamChatCore');
|
||||||
|
const childKey = Key('child');
|
||||||
|
final _connectivityController =
|
||||||
|
BehaviorSubject.seeded(ConnectivityResult.none);
|
||||||
|
final streamChatCore = StreamChatCore(
|
||||||
|
key: streamChatCoreKey,
|
||||||
|
client: mockClient,
|
||||||
|
child: Offstage(key: childKey),
|
||||||
|
connectivityStream: _connectivityController.stream,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(streamChatCore);
|
||||||
|
|
||||||
|
expect(find.byKey(streamChatCoreKey), findsOneWidget);
|
||||||
|
expect(find.byKey(childKey), findsOneWidget);
|
||||||
|
|
||||||
|
final event = Event();
|
||||||
|
when(() => mockClient.on()).thenAnswer((_) => Stream.value(event));
|
||||||
|
when(() => mockClient.connect()).thenAnswer((_) async => event);
|
||||||
|
when(mockClient.disconnect).thenAnswer((_) async => null);
|
||||||
|
when(() => mockClient.wsConnectionStatus)
|
||||||
|
.thenReturn(ConnectionStatus.disconnected);
|
||||||
|
|
||||||
|
_connectivityController.add(ConnectivityResult.mobile);
|
||||||
|
|
||||||
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
|
||||||
|
verify(() => mockClient.connect()).called(1);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'should call disconnect if in foreground and connection goes away',
|
||||||
|
(tester) async {
|
||||||
|
await tester.runAsync(() async {
|
||||||
|
final mockClient = MockClient();
|
||||||
|
const streamChatCoreKey = Key('streamChatCore');
|
||||||
|
const childKey = Key('child');
|
||||||
|
final _connectivityController =
|
||||||
|
BehaviorSubject.seeded(ConnectivityResult.mobile);
|
||||||
|
final streamChatCore = StreamChatCore(
|
||||||
|
key: streamChatCoreKey,
|
||||||
|
client: mockClient,
|
||||||
|
child: Offstage(key: childKey),
|
||||||
|
connectivityStream: _connectivityController.stream,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(streamChatCore);
|
||||||
|
|
||||||
|
expect(find.byKey(streamChatCoreKey), findsOneWidget);
|
||||||
|
expect(find.byKey(childKey), findsOneWidget);
|
||||||
|
|
||||||
|
final event = Event();
|
||||||
|
when(() => mockClient.on()).thenAnswer((_) => Stream.value(event));
|
||||||
|
when(() => mockClient.connect()).thenAnswer((_) async => event);
|
||||||
|
when(mockClient.disconnect).thenAnswer((_) async => null);
|
||||||
|
when(() => mockClient.wsConnectionStatus)
|
||||||
|
.thenReturn(ConnectionStatus.connected);
|
||||||
|
|
||||||
|
_connectivityController.add(ConnectivityResult.none);
|
||||||
|
|
||||||
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
|
||||||
|
verify(() => mockClient.disconnect()).called(1);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'should ignore connectivity in background',
|
||||||
|
(tester) async {
|
||||||
|
await tester.runAsync(() async {
|
||||||
|
final mockClient = MockClient();
|
||||||
|
const streamChatCoreKey = Key('streamChatCore');
|
||||||
|
const childKey = Key('child');
|
||||||
|
final _connectivityController =
|
||||||
|
BehaviorSubject.seeded(ConnectivityResult.none);
|
||||||
|
final streamChatCore = StreamChatCore(
|
||||||
|
key: streamChatCoreKey,
|
||||||
|
client: mockClient,
|
||||||
|
child: Offstage(key: childKey),
|
||||||
|
connectivityStream: _connectivityController.stream,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(streamChatCore);
|
||||||
|
|
||||||
|
expect(find.byKey(streamChatCoreKey), findsOneWidget);
|
||||||
|
expect(find.byKey(childKey), findsOneWidget);
|
||||||
|
|
||||||
|
final event = Event();
|
||||||
|
when(() => mockClient.on()).thenAnswer((_) => Stream.value(event));
|
||||||
|
when(() => mockClient.connect()).thenAnswer((_) async => event);
|
||||||
|
when(mockClient.disconnect).thenAnswer((_) async => null);
|
||||||
|
when(() => mockClient.wsConnectionStatus)
|
||||||
|
.thenReturn(ConnectionStatus.disconnected);
|
||||||
|
|
||||||
|
final streamChatCoreState = tester.state<StreamChatCoreState>(
|
||||||
|
find.byKey(streamChatCoreKey),
|
||||||
|
);
|
||||||
|
|
||||||
|
streamChatCoreState
|
||||||
|
.didChangeAppLifecycleState(AppLifecycleState.paused);
|
||||||
|
|
||||||
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
|
||||||
|
_connectivityController.add(ConnectivityResult.mobile);
|
||||||
|
|
||||||
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
|
||||||
|
verifyNever(() => mockClient.disconnect());
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user