From bdc19a9d5975b60a039147f6c61053396f290ed8 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 21 May 2021 10:41:50 +0200 Subject: [PATCH 01/19] make timeoutOrExpirationDate optional --- packages/stream_chat/lib/src/client.dart | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/stream_chat/lib/src/client.dart b/packages/stream_chat/lib/src/client.dart index 05f2559b..6e7caa9e 100644 --- a/packages/stream_chat/lib/src/client.dart +++ b/packages/stream_chat/lib/src/client.dart @@ -1361,12 +1361,13 @@ class StreamChatClient { /// [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? timeoutOrExpirationDate, + ]) { assert(() { if (timeoutOrExpirationDate is! DateTime && - timeoutOrExpirationDate is! num) { + timeoutOrExpirationDate is! num && + timeoutOrExpirationDate != null) { throw ArgumentError('Invalid timeout or Expiration date'); } return true; @@ -1383,13 +1384,19 @@ class StreamChatClient { .toUtc(); } return updateMessage( - message.copyWith(pinned: true, pinExpires: pinExpires), + message.copyWith( + pinned: true, + pinExpires: pinExpires, + ), ); } /// Unpins provided message - Future unpinMessage(Message message) => - updateMessage(message.copyWith(pinned: false)); + Future unpinMessage(Message message) => updateMessage( + message.copyWith( + pinned: false, + ), + ); } /// The class that handles the state of the channel listening to the events From 32cb1e77001d038911f538a4181562d08e44c0da Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 21 May 2021 10:45:59 +0200 Subject: [PATCH 02/19] replicate in channel --- packages/stream_chat/lib/src/api/channel.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat/lib/src/api/channel.dart b/packages/stream_chat/lib/src/api/channel.dart index 69d967fe..384ca105 100644 --- a/packages/stream_chat/lib/src/api/channel.dart +++ b/packages/stream_chat/lib/src/api/channel.dart @@ -507,9 +507,9 @@ class Channel { /// Pins provided message Future pinMessage( - Message message, + Message message, [ Object? timeoutOrExpirationDate, - ) { + ]) { assert(() { if (timeoutOrExpirationDate is! DateTime && timeoutOrExpirationDate != null && From a543cf325a11fcfd8315ecbeaa211fecc37e1bef Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 21 May 2021 10:50:35 +0200 Subject: [PATCH 03/19] fix assert error message --- 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 384ca105..657126c1 100644 --- a/packages/stream_chat/lib/src/api/channel.dart +++ b/packages/stream_chat/lib/src/api/channel.dart @@ -517,7 +517,7 @@ class Channel { throw ArgumentError('Invalid timeout or Expiration date'); } return true; - }(), 'Check for invalid token or expiration date'); + }(), 'Check whether time out is valid'); DateTime? pinExpires; if (timeoutOrExpirationDate is DateTime) { From 5b39893f768970f68ad80c47e64e254851d99982 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 21 May 2021 11:36:08 +0200 Subject: [PATCH 04/19] add test --- .../stream_chat/test/src/client_test.dart | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/stream_chat/test/src/client_test.dart b/packages/stream_chat/test/src/client_test.dart index b2a5bbac..53792764 100644 --- a/packages/stream_chat/test/src/client_test.dart +++ b/packages/stream_chat/test/src/client_test.dart @@ -1112,6 +1112,28 @@ void main() { 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'); From d764de9f689d59c6c71320e4f7d02e57b4bfcd1f Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 21 May 2021 11:37:03 +0200 Subject: [PATCH 05/19] add test --- .../test/src/api/channel_test.dart | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/packages/stream_chat/test/src/api/channel_test.dart b/packages/stream_chat/test/src/api/channel_test.dart index c3409b7e..f92b05bb 100644 --- a/packages/stream_chat/test/src/api/channel_test.dart +++ b/packages/stream_chat/test/src/api/channel_test.dart @@ -449,6 +449,51 @@ void main() { .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(); From 69fdeb813d983bbb83c09ec7f8ae4dc93bf3cd1a Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 21 May 2021 15:08:58 +0200 Subject: [PATCH 06/19] fix channel header text theme --- .../lib/src/channel_header.dart | 1 + .../lib/src/channel_name.dart | 7 ++-- .../lib/src/stream_chat_theme.dart | 34 ++++++++++--------- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/channel_header.dart b/packages/stream_chat_flutter/lib/src/channel_header.dart index d06c128c..7773b36f 100644 --- a/packages/stream_chat_flutter/lib/src/channel_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_header.dart @@ -137,6 +137,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { showMessage: showConnectionStateTile ? showStatus : false, message: statusString, child: AppBar( + textTheme: Theme.of(context).textTheme, brightness: Theme.of(context).brightness, elevation: 1, leading: leadingWidget, diff --git a/packages/stream_chat_flutter/lib/src/channel_name.dart b/packages/stream_chat_flutter/lib/src/channel_name.dart index 2cd804c9..5db3341a 100644 --- a/packages/stream_chat_flutter/lib/src/channel_name.dart +++ b/packages/stream_chat_flutter/lib/src/channel_name.dart @@ -29,8 +29,11 @@ class ChannelName extends StatelessWidget { return StreamBuilder>( stream: channel.extraDataStream, initialData: channel.extraData, - builder: (context, snapshot) => - _buildName(snapshot.data!, channel.state?.members, client), + builder: (context, snapshot) => _buildName( + snapshot.data!, + channel.state?.members, + 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 97363df7..25c2ca32 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart @@ -230,22 +230,23 @@ class StreamChatThemeData { ), ), channelPreviewTheme: ChannelPreviewTheme( - unreadCounterColor: colorTheme.accentRed, - avatarTheme: AvatarTheme( - borderRadius: BorderRadius.circular(20), - constraints: const BoxConstraints.tightFor( - height: 40, - width: 40, - ), + unreadCounterColor: colorTheme.accentRed, + avatarTheme: AvatarTheme( + borderRadius: BorderRadius.circular(20), + constraints: const BoxConstraints.tightFor( + height: 40, + width: 40, ), - title: textTheme.bodyBold, - subtitle: textTheme.footnote.copyWith( - color: const Color(0xff7A7A7A), - ), - lastMessageAt: textTheme.footnote.copyWith( - color: colorTheme.black.withOpacity(.5), - ), - indicatorIconSize: 16), + ), + title: textTheme.bodyBold, + subtitle: textTheme.footnote.copyWith( + color: const Color(0xff7A7A7A), + ), + lastMessageAt: textTheme.footnote.copyWith( + color: colorTheme.black.withOpacity(.5), + ), + indicatorIconSize: 16, + ), channelListHeaderTheme: ChannelListHeaderTheme( avatarTheme: AvatarTheme( borderRadius: BorderRadius.circular(20), @@ -1186,7 +1187,8 @@ class MessageInputTheme { actionButtonIdleColor: other.actionButtonIdleColor, sendButtonColor: other.sendButtonColor, sendButtonIdleColor: other.sendButtonIdleColor, - inputTextStyle: other.inputTextStyle, + inputTextStyle: + inputTextStyle?.merge(other.inputTextStyle) ?? other.inputTextStyle, inputDecoration: inputDecoration?.merge(other.inputDecoration) ?? other.inputDecoration, activeBorderGradient: other.activeBorderGradient, From 05402ee3384044cfe5ca8afff64a798dc2be8b83 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 21 May 2021 15:10:07 +0200 Subject: [PATCH 07/19] apply texttheme to rest of headers --- packages/stream_chat_flutter/lib/src/channel_list_header.dart | 1 + packages/stream_chat_flutter/lib/src/image_header.dart | 1 + packages/stream_chat_flutter/lib/src/thread_header.dart | 1 + 3 files changed, 3 insertions(+) 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..4a8f78c3 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_header.dart @@ -120,6 +120,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { showMessage: showConnectionStateTile ? showStatus : false, message: statusString, child: AppBar( + textTheme: Theme.of(context).textTheme, brightness: Theme.of(context).brightness, elevation: 1, backgroundColor: chatThemeData.channelListHeaderTheme.color, diff --git a/packages/stream_chat_flutter/lib/src/image_header.dart b/packages/stream_chat_flutter/lib/src/image_header.dart index 095d7534..434a73c5 100644 --- a/packages/stream_chat_flutter/lib/src/image_header.dart +++ b/packages/stream_chat_flutter/lib/src/image_header.dart @@ -53,6 +53,7 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget { Widget build(BuildContext context) { final chatThemeData = StreamChatTheme.of(context); return AppBar( + textTheme: Theme.of(context).textTheme, brightness: Theme.of(context).brightness, elevation: 1, leading: showBackButton diff --git a/packages/stream_chat_flutter/lib/src/thread_header.dart b/packages/stream_chat_flutter/lib/src/thread_header.dart index 347fc59d..d16aa530 100644 --- a/packages/stream_chat_flutter/lib/src/thread_header.dart +++ b/packages/stream_chat_flutter/lib/src/thread_header.dart @@ -101,6 +101,7 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { final chatThemeData = StreamChatTheme.of(context); return AppBar( automaticallyImplyLeading: false, + textTheme: Theme.of(context).textTheme, brightness: Theme.of(context).brightness, elevation: 1, leading: leading ?? From c151a86fc746fa6282f56e3a869058f05d803e83 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 21 May 2021 15:23:20 +0200 Subject: [PATCH 08/19] fix typo --- 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 657126c1..2b6bc0a9 100644 --- a/packages/stream_chat/lib/src/api/channel.dart +++ b/packages/stream_chat/lib/src/api/channel.dart @@ -517,7 +517,7 @@ class Channel { throw ArgumentError('Invalid timeout or Expiration date'); } return true; - }(), 'Check whether time out is valid'); + }(), 'Check whether timeout is valid'); DateTime? pinExpires; if (timeoutOrExpirationDate is DateTime) { From d07b3776ecc5037904c4238d2faf8f317c6c76db Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 27 May 2021 09:49:11 +0200 Subject: [PATCH 09/19] fix use of reduce --- .../lib/src/message_list_view.dart | 19 +++++++++++++------ 1 file changed, 13 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 e07d9358..db14924b 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -570,9 +570,9 @@ class _MessageListViewState extends State { 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(); } @@ -598,10 +598,17 @@ class _MessageListViewState extends State { StreamChannelState? channel, QueryDirection direction) => _messageListController.paginateData!(direction: direction); - ItemPosition _getTopElement(Iterable values) => values - .where((ItemPosition position) => position.itemLeadingEdge < 0.9) - .reduce((ItemPosition max, ItemPosition position) => - position.itemLeadingEdge > max.itemLeadingEdge ? position : max); + 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); + } Widget _buildScrollToBottom() => StreamBuilder>( stream: Rx.combineLatest2( From 4f5958a067e33129eddc46ae1caea635b6bdbb0a Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 27 May 2021 11:04:30 +0200 Subject: [PATCH 10/19] implement connectivity plus --- .../stream_chat/lib/src/api/websocket.dart | 2 +- .../stream_chat_flutter/example/lib/main.dart | 1 - .../lib/src/stream_chat_core.dart | 89 ++++++--- .../stream_chat_flutter_core/pubspec.yaml | 1 + .../test/stream_chat_core_test.dart | 169 ++++++++++++++++++ 5 files changed, 236 insertions(+), 26 deletions(-) diff --git a/packages/stream_chat/lib/src/api/websocket.dart b/packages/stream_chat/lib/src/api/websocket.dart index b5bbbb69..657aa125 100644 --- a/packages/stream_chat/lib/src/api/websocket.dart +++ b/packages/stream_chat/lib/src/api/websocket.dart @@ -100,7 +100,7 @@ class WebSocket { /// connection unhealthy final int reconnectionMonitorTimeout; - final _connectionStatusController = + final BehaviorSubject _connectionStatusController = BehaviorSubject.seeded(ConnectionStatus.disconnected); set _connectionStatus(ConnectionStatus status) => diff --git a/packages/stream_chat_flutter/example/lib/main.dart b/packages/stream_chat_flutter/example/lib/main.dart index 1f3b51d2..d3522886 100644 --- a/packages/stream_chat_flutter/example/lib/main.dart +++ b/packages/stream_chat_flutter/example/lib/main.dart @@ -4,7 +4,6 @@ import 'package:stream_chat_persistence/stream_chat_persistence.dart'; final chatPersistentClient = StreamChatPersistenceClient( logLevel: Level.INFO, - connectionMode: ConnectionMode.background, ); void main() async { 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..e65eb4e2 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 @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat/stream_chat.dart'; @@ -43,6 +44,7 @@ class StreamChatCore extends StatefulWidget { required this.child, this.onBackgroundEventReceived, this.backgroundKeepAlive = const Duration(minutes: 1), + this.connectivityStream, }) : super(key: key); /// Instance of Stream Chat Client containing information about the current @@ -61,6 +63,11 @@ class StreamChatCore extends StatefulWidget { /// upon the [Event.type] final EventHandler? onBackgroundEventReceived; + /// Stream of connectivity result + /// Visible for testing + @visibleForTesting + final Stream? connectivityStream; + @override StreamChatCoreState createState() => StreamChatCoreState(); @@ -96,50 +103,84 @@ class StreamChatCoreState extends State /// The current user as a stream Stream get userStream => client.state.userStream; + late final StreamSubscription _connectivitySubscription; + + var _isInForeground = true; + var _isConnectionAvailable = true; + @override void initState() { super.initState(); WidgetsBinding.instance?.addObserver(this); + _connectivitySubscription = + (widget.connectivityStream ?? Connectivity().onConnectivityChanged) + .listen((ConnectivityResult result) async { + _isConnectionAvailable = result != ConnectivityResult.none; + if (!_isInForeground) { + return; + } + if (_isConnectionAvailable) { + if (client.wsConnectionStatus == ConnectionStatus.disconnected) { + await client.connect(); + } + } else { + if (client.wsConnectionStatus == ConnectionStatus.connected) { + await client.disconnect(); + } + } + }); } StreamSubscription? _eventSubscription; @override void didChangeAppLifecycleState(AppLifecycleState state) { + _isInForeground = state == AppLifecycleState.resumed; if (user != null) { - if (state == AppLifecycleState.paused) { - if (widget.onBackgroundEventReceived == null) { - client.disconnect(); - return; - } - _eventSubscription = client.on().listen( - widget.onBackgroundEventReceived, - ); - - void onTimerComplete() { - _eventSubscription?.cancel(); - client.disconnect(); - } - - _disconnectTimer = Timer(widget.backgroundKeepAlive, onTimerComplete); - } else if (state == AppLifecycleState.resumed) { - if (_disconnectTimer?.isActive == true) { - _eventSubscription?.cancel(); - _disconnectTimer?.cancel(); - } else { - if (client.wsConnectionStatus == ConnectionStatus.disconnected) { - client.connect(); - } - } + if (!_isInForeground) { + _onBackground(); + } else { + _onForeground(); } } } + 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 void dispose() { WidgetsBinding.instance?.removeObserver(this); _eventSubscription?.cancel(); _disconnectTimer?.cancel(); + _connectivitySubscription.cancel(); super.dispose(); } } diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml index e8d1014d..7bae31f9 100644 --- a/packages/stream_chat_flutter_core/pubspec.yaml +++ b/packages/stream_chat_flutter_core/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: meta: ^1.3.0 rxdart: ^0.27.0 stream_chat: ^2.0.0-nullsafety.2 + connectivity_plus: ^1.0.1 dev_dependencies: fake_async: ^1.2.0 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..936e03f0 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 @@ -1,8 +1,10 @@ import 'dart:async'; +import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:rxdart/rxdart.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'mocks.dart'; @@ -100,6 +102,7 @@ void main() { child: Offstage(key: childKey), onBackgroundEventReceived: mockOnBackgroundEventReceived, backgroundKeepAlive: backgroundKeepAlive, + connectivityStream: Stream.value(ConnectivityResult.mobile), ); await tester.pumpWidget(streamChatCore); @@ -191,6 +194,7 @@ void main() { key: streamChatCoreKey, client: mockClient, child: Offstage(key: childKey), + connectivityStream: Stream.value(ConnectivityResult.mobile), ); 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( + find.byKey(streamChatCoreKey), + ); + + streamChatCoreState + .didChangeAppLifecycleState(AppLifecycleState.paused); + + await Future.delayed(const Duration(seconds: 1)); + + streamChatCoreState + .didChangeAppLifecycleState(AppLifecycleState.resumed); + + verifyNever(() => mockClient.connect()); + }); + }, + ); + testWidgets( 'streamChatCoreState.userStream should emit all the user events ' '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( + 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()); + }); + }, + ); } From 65bf45306a15e8f641664cc7b81ce437b9edd512 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 27 May 2021 16:27:23 +0200 Subject: [PATCH 11/19] fix lint and tests --- packages/stream_chat_flutter/lib/src/stream_chat.dart | 7 +++++++ .../stream_chat_flutter/test/src/deleted_message_test.dart | 3 +++ .../stream_chat_flutter/test/src/reaction_bubble_test.dart | 5 +++++ .../stream_chat_flutter/test/src/system_message_test.dart | 2 ++ .../lib/stream_chat_flutter_core.dart | 1 + packages/stream_chat_flutter_core/pubspec.yaml | 4 ++-- 6 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/stream_chat.dart b/packages/stream_chat_flutter/lib/src/stream_chat.dart index 424d4bcf..64acae98 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat.dart @@ -39,6 +39,7 @@ class StreamChat extends StatefulWidget { this.streamChatThemeData, this.onBackgroundEventReceived, this.backgroundKeepAlive = const Duration(minutes: 1), + this.connectivityStream, }) : super(key: key); /// Client to do chat ops with @@ -59,6 +60,11 @@ class StreamChat extends StatefulWidget { /// upon the [Event.type] final EventHandler? onBackgroundEventReceived; + /// Stream of connectivity result + /// Visible for testing + @visibleForTesting + final Stream? connectivityStream; + @override StreamChatState createState() => StreamChatState(); @@ -102,6 +108,7 @@ class StreamChatState extends State { client: client, onBackgroundEventReceived: widget.onBackgroundEventReceived, backgroundKeepAlive: widget.backgroundKeepAlive, + connectivityStream: widget.connectivityStream, child: widget.child ?? const Offstage(), ), ); diff --git a/packages/stream_chat_flutter/test/src/deleted_message_test.dart b/packages/stream_chat_flutter/test/src/deleted_message_test.dart index bcf9cf62..206d92e3 100644 --- a/packages/stream_chat_flutter/test/src/deleted_message_test.dart +++ b/packages/stream_chat_flutter/test/src/deleted_message_test.dart @@ -79,6 +79,7 @@ void main() { ), ), ), + connectivityStream: Stream.value(ConnectivityResult.mobile), ), ), surfaceSize: const Size.square(200), @@ -131,6 +132,7 @@ void main() { ), ), ), + connectivityStream: Stream.value(ConnectivityResult.mobile), ), ), surfaceSize: const Size.square(200), @@ -187,6 +189,7 @@ void main() { ), ), ), + connectivityStream: Stream.value(ConnectivityResult.mobile), ), ), surfaceSize: const Size.square(200), diff --git a/packages/stream_chat_flutter/test/src/reaction_bubble_test.dart b/packages/stream_chat_flutter/test/src/reaction_bubble_test.dart index 770a040b..c5183f35 100644 --- a/packages/stream_chat_flutter/test/src/reaction_bubble_test.dart +++ b/packages/stream_chat_flutter/test/src/reaction_bubble_test.dart @@ -60,6 +60,7 @@ void main() { maskColor: theme.ownMessageTheme.reactionsMaskColor!, ), ), + connectivityStream: Stream.value(ConnectivityResult.mobile), ), surfaceSize: const Size(100, 100), ); @@ -96,6 +97,7 @@ void main() { maskColor: theme.ownMessageTheme.reactionsMaskColor!, ), ), + connectivityStream: Stream.value(ConnectivityResult.mobile), ), surfaceSize: const Size(100, 100), ); @@ -118,6 +120,7 @@ void main() { StreamChat( client: client, streamChatThemeData: StreamChatThemeData.fromTheme(themeData), + connectivityStream: Stream.value(ConnectivityResult.mobile), child: Container( color: Colors.black, child: ReactionBubble( @@ -162,6 +165,7 @@ void main() { StreamChat( client: client, streamChatThemeData: StreamChatThemeData.fromTheme(themeData), + connectivityStream: Stream.value(ConnectivityResult.mobile), child: Container( color: Colors.black, child: ReactionBubble( @@ -204,6 +208,7 @@ void main() { await tester.pumpWidgetBuilder( StreamChat( client: client, + connectivityStream: Stream.value(ConnectivityResult.mobile), streamChatThemeData: StreamChatThemeData.fromTheme(themeData), child: SizedBox( child: ReactionBubble( diff --git a/packages/stream_chat_flutter/test/src/system_message_test.dart b/packages/stream_chat_flutter/test/src/system_message_test.dart index 8fa3ad44..6bb1da43 100644 --- a/packages/stream_chat_flutter/test/src/system_message_test.dart +++ b/packages/stream_chat_flutter/test/src/system_message_test.dart @@ -100,6 +100,7 @@ void main() { ), ), ), + connectivityStream: Stream.value(ConnectivityResult.mobile), ), ), surfaceSize: const Size.square(200), @@ -151,6 +152,7 @@ void main() { ), ), ), + connectivityStream: Stream.value(ConnectivityResult.mobile), ), ), surfaceSize: const Size.square(200), 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..ef947a0f 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 @@ -1,5 +1,6 @@ library stream_chat_flutter_core; +export 'package:connectivity_plus/connectivity_plus.dart'; export 'package:stream_chat/stream_chat.dart'; export 'src/channel_list_core.dart' hide ChannelListCoreState; diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml index 7bae31f9..4292772b 100644 --- a/packages/stream_chat_flutter_core/pubspec.yaml +++ b/packages/stream_chat_flutter_core/pubspec.yaml @@ -11,16 +11,16 @@ environment: dependencies: collection: ^1.15.0 + connectivity_plus: ^1.0.1 flutter: sdk: flutter meta: ^1.3.0 rxdart: ^0.27.0 stream_chat: ^2.0.0-nullsafety.2 - connectivity_plus: ^1.0.1 dev_dependencies: fake_async: ^1.2.0 flutter_test: sdk: flutter - mocktail: ^0.1.1 + mocktail: ^0.1.3 From 644c8b67d8e122d5f49f7370bbe1b991553e67f8 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 28 May 2021 12:02:49 +0530 Subject: [PATCH 12/19] re-subscribe `connectivityStream` if `widget.connectivityStream` changes Signed-off-by: Sahil Kumar --- .../lib/src/stream_chat_core.dart | 70 ++++++++++++------- 1 file changed, 46 insertions(+), 24 deletions(-) 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 e65eb4e2..d3ed4a0b 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 @@ -103,7 +103,7 @@ class StreamChatCoreState extends State /// The current user as a stream Stream get userStream => client.state.userStream; - late final StreamSubscription _connectivitySubscription; + StreamSubscription? _connectivitySubscription; var _isInForeground = true; var _isConnectionAvailable = true; @@ -112,23 +112,46 @@ class StreamChatCoreState extends State void initState() { super.initState(); WidgetsBinding.instance?.addObserver(this); - _connectivitySubscription = - (widget.connectivityStream ?? Connectivity().onConnectivityChanged) - .listen((ConnectivityResult result) async { - _isConnectionAvailable = result != ConnectivityResult.none; - if (!_isInForeground) { - return; - } - if (_isConnectionAvailable) { - if (client.wsConnectionStatus == ConnectionStatus.disconnected) { - await client.connect(); + _subscribeToConnectivityChange(widget.connectivityStream); + } + + void _subscribeToConnectivityChange([ + Stream? 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(); + } } - } else { - if (client.wsConnectionStatus == ConnectionStatus.connected) { - await 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; @@ -137,10 +160,10 @@ class StreamChatCoreState extends State void didChangeAppLifecycleState(AppLifecycleState state) { _isInForeground = state == AppLifecycleState.resumed; if (user != null) { - if (!_isInForeground) { - _onBackground(); - } else { + if (_isInForeground) { _onForeground(); + } else { + _onBackground(); } } } @@ -162,9 +185,8 @@ class StreamChatCoreState extends State } return; } - _eventSubscription = client.on().listen( - widget.onBackgroundEventReceived, - ); + + _eventSubscription = client.on().listen(widget.onBackgroundEventReceived); void onTimerComplete() { _eventSubscription?.cancel(); @@ -178,9 +200,9 @@ class StreamChatCoreState extends State @override void dispose() { WidgetsBinding.instance?.removeObserver(this); + _unsubscribeFromConnectivityChange(); _eventSubscription?.cancel(); _disconnectTimer?.cancel(); - _connectivitySubscription.cancel(); super.dispose(); } } From 765ca6034396cdf8927602e1540c4a38a27953aa Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Mon, 31 May 2021 11:16:40 +0530 Subject: [PATCH 13/19] feat: Added callbacks for default slidable options --- .../lib/src/channel_list_view.dart | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 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..1d0a4966 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -78,6 +78,8 @@ class ChannelListView extends StatefulWidget { this.emptyBuilder, this.loadingBuilder, this.listBuilder, + this.onMoreDetailsPressed, + this.onDeletePressed, }) : super(key: key); /// If true a default swipe to action behaviour will be added to this widget @@ -158,6 +160,12 @@ class ChannelListView extends StatefulWidget { /// The builder used when the channel list is empty. final WidgetBuilder? emptyBuilder; + /// Callback used when the more details slidable option is pressed + final VoidCallback? onMoreDetailsPressed; + + /// Callback used when the delete slidable option is pressed + final VoidCallback? onDeletePressed; + @override _ChannelListViewState createState() => _ChannelListViewState(); } @@ -469,7 +477,7 @@ class _ChannelListViewState extends State { IconSlideAction( color: backgroundColor, icon: Icons.more_horiz, - onTap: () { + onTap: widget.onMoreDetailsPressed ?? () { showModalBottomSheet( clipBehavior: Clip.hardEdge, shape: const RoundedRectangleBorder( @@ -502,7 +510,7 @@ class _ChannelListViewState extends State { iconWidget: StreamSvgIcon.delete( color: chatThemeData.colorTheme.accentRed, ), - onTap: () async { + onTap: widget.onDeletePressed ?? () async { final res = await showConfirmationDialog( context, title: 'Delete Conversation', From ceaf30e5454a8e12337e3eeca09471037a34bbb8 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Mon, 31 May 2021 11:30:51 +0530 Subject: [PATCH 14/19] feat: Added list of actions to channel_list_view.dart --- .../lib/src/channel_list_view.dart | 109 ++++++++++-------- 1 file changed, 58 insertions(+), 51 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 1d0a4966..8d7c833a 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -80,6 +80,7 @@ class ChannelListView extends StatefulWidget { this.listBuilder, this.onMoreDetailsPressed, this.onDeletePressed, + this.swipeActions, }) : super(key: key); /// If true a default swipe to action behaviour will be added to this widget @@ -166,6 +167,9 @@ class ChannelListView extends StatefulWidget { /// Callback used when the delete slidable option is pressed final VoidCallback? onDeletePressed; + /// Actions shown when list tile is swiped + final List? swipeActions; + @override _ChannelListViewState createState() => _ChannelListViewState(); } @@ -473,61 +477,64 @@ class _ChannelListViewState extends State { enabled: widget.swipeToAction, actionPane: const SlidableBehindActionPane(), actionExtentRatio: 0.12, - secondaryActions: [ - IconSlideAction( - color: backgroundColor, - icon: Icons.more_horiz, - onTap: widget.onMoreDetailsPressed ?? () { - 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); + secondaryActions: widget.swipeActions ?? + [ + IconSlideAction( + color: backgroundColor, + icon: Icons.more_horiz, + onTap: widget.onMoreDetailsPressed ?? + () { + 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, - iconWidget: StreamSvgIcon.delete( - color: chatThemeData.colorTheme.accentRed, ), - onTap: widget.onDeletePressed ?? () async { - final res = await showConfirmationDialog( - context, - title: 'Delete Conversation', - okText: 'DELETE', - question: - 'Are you sure you want to delete this conversation?', - cancelText: 'CANCEL', - icon: StreamSvgIcon.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, ), - ); - if (res == true) { - await channel.delete(); - } - }, - ), - ], + onTap: widget.onDeletePressed ?? + () async { + final res = await showConfirmationDialog( + context, + title: 'Delete Conversation', + okText: 'DELETE', + question: + '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( color: chatThemeData.colorTheme.whiteSnow, child: widget.channelPreviewBuilder?.call(context, channel) ?? From ba0b525255a3b5341311a8a58da344768a3628c9 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Mon, 31 May 2021 11:37:48 +0530 Subject: [PATCH 15/19] removed swipe actions list --- .../lib/src/channel_list_view.dart | 117 +++++++++--------- 1 file changed, 56 insertions(+), 61 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 8d7c833a..46e2ebf9 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -80,7 +80,6 @@ class ChannelListView extends StatefulWidget { this.listBuilder, this.onMoreDetailsPressed, this.onDeletePressed, - this.swipeActions, }) : super(key: key); /// If true a default swipe to action behaviour will be added to this widget @@ -167,9 +166,6 @@ class ChannelListView extends StatefulWidget { /// Callback used when the delete slidable option is pressed final VoidCallback? onDeletePressed; - /// Actions shown when list tile is swiped - final List? swipeActions; - @override _ChannelListViewState createState() => _ChannelListViewState(); } @@ -477,64 +473,63 @@ class _ChannelListViewState extends State { enabled: widget.swipeToAction, actionPane: const SlidableBehindActionPane(), actionExtentRatio: 0.12, - secondaryActions: widget.swipeActions ?? - [ - IconSlideAction( - color: backgroundColor, - icon: Icons.more_horiz, - onTap: widget.onMoreDetailsPressed ?? - () { - 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); - }, - ), - ), - ); - }, + secondaryActions: [ + IconSlideAction( + color: backgroundColor, + icon: Icons.more_horiz, + onTap: widget.onMoreDetailsPressed ?? + () { + 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, + iconWidget: StreamSvgIcon.delete( + color: chatThemeData.colorTheme.accentRed, ), - 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 ?? - () async { - final res = await showConfirmationDialog( - context, - title: 'Delete Conversation', - okText: 'DELETE', - question: - 'Are you sure you want to delete this conversation?', - cancelText: 'CANCEL', - icon: StreamSvgIcon.delete( - color: chatThemeData.colorTheme.accentRed, - ), - ); - if (res == true) { - await channel.delete(); - } - }, - ), - ], + onTap: widget.onDeletePressed ?? + () async { + final res = await showConfirmationDialog( + context, + title: 'Delete Conversation', + okText: 'DELETE', + question: + '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( color: chatThemeData.colorTheme.whiteSnow, child: widget.channelPreviewBuilder?.call(context, channel) ?? From f75e50d651a6573e1105cd442c8e4118af99a96b Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Mon, 31 May 2021 14:21:17 +0530 Subject: [PATCH 16/19] added swipe actions list --- .../lib/src/channel_list_view.dart | 135 ++++++++++-------- 1 file changed, 79 insertions(+), 56 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 46e2ebf9..1478c745 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -80,6 +80,7 @@ class ChannelListView extends StatefulWidget { this.listBuilder, this.onMoreDetailsPressed, this.onDeletePressed, + this.swipeActions, }) : super(key: key); /// If true a default swipe to action behaviour will be added to this widget @@ -166,6 +167,9 @@ class ChannelListView extends StatefulWidget { /// Callback used when the delete slidable option is pressed final VoidCallback? onDeletePressed; + /// List of actions for slidable + final List? swipeActions; + @override _ChannelListViewState createState() => _ChannelListViewState(); } @@ -473,63 +477,70 @@ class _ChannelListViewState extends State { enabled: widget.swipeToAction, actionPane: const SlidableBehindActionPane(), actionExtentRatio: 0.12, - secondaryActions: [ - IconSlideAction( - color: backgroundColor, - icon: Icons.more_horiz, - onTap: widget.onMoreDetailsPressed ?? - () { - 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, - iconWidget: StreamSvgIcon.delete( - color: chatThemeData.colorTheme.accentRed, + secondaryActions: widget.swipeActions + ?.map((e) => IconSlideAction( + color: e.color, + iconWidget: e.iconWidget, + onTap: e.onTap, + )) + .toList() ?? + [ + IconSlideAction( + color: backgroundColor, + icon: Icons.more_horiz, + onTap: widget.onMoreDetailsPressed ?? + () { + 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); + }, + ), + ), + ); + }, ), - onTap: widget.onDeletePressed ?? - () async { - final res = await showConfirmationDialog( - context, - title: 'Delete Conversation', - okText: 'DELETE', - question: - '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 ?? + () async { + final res = await showConfirmationDialog( + context, + title: 'Delete Conversation', + okText: 'DELETE', + question: + '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( color: chatThemeData.colorTheme.whiteSnow, child: widget.channelPreviewBuilder?.call(context, channel) ?? @@ -650,3 +661,15 @@ class _ChannelListViewState extends State { ); } } + +class SwipeAction { + Color? color; + Widget iconWidget; + VoidCallback? onTap; + + SwipeAction({ + this.color, + required this.iconWidget, + this.onTap, + }); +} From 56e7d45cb8e4f590316fb8f23f0274cca92913cc Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Mon, 31 May 2021 14:28:49 +0530 Subject: [PATCH 17/19] analysis fixes --- .../lib/src/channel_list_view.dart | 15 +++++++++++---- 1 file changed, 11 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 1478c745..049a51af 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -662,14 +662,21 @@ class _ChannelListViewState extends State { } } +/// Class for slidable action class SwipeAction { - Color? color; - Widget iconWidget; - VoidCallback? onTap; - + /// 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 + VoidCallback? onTap; } From 5ed6932ef20df419a3593322ca55e928eb288ecd Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Mon, 31 May 2021 14:29:11 +0530 Subject: [PATCH 18/19] analysis fixes --- packages/stream_chat_flutter/lib/src/channel_list_view.dart | 1 + 1 file changed, 1 insertion(+) 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 049a51af..8a5bc78d 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -529,6 +529,7 @@ class _ChannelListViewState extends State { 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( From 23d992740f23e54f7144c9fe8d4a5e1c9b2a5581 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 1 Jun 2021 17:46:12 +0530 Subject: [PATCH 19/19] Changed callback type --- .../lib/src/channel_list_view.dart | 91 +++++++++++-------- 1 file changed, 51 insertions(+), 40 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 8a5bc78d..911e0a41 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -12,6 +12,9 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Callback called when tapping on a channel 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] typedef ChannelPreviewBuilder = Widget Function(BuildContext, Channel); @@ -162,10 +165,10 @@ class ChannelListView extends StatefulWidget { final WidgetBuilder? emptyBuilder; /// Callback used when the more details slidable option is pressed - final VoidCallback? onMoreDetailsPressed; + final ChannelInfoCallback? onMoreDetailsPressed; /// Callback used when the delete slidable option is pressed - final VoidCallback? onDeletePressed; + final ChannelInfoCallback? onDeletePressed; /// List of actions for slidable final List? swipeActions; @@ -481,34 +484,39 @@ class _ChannelListViewState extends State { ?.map((e) => IconSlideAction( color: e.color, iconWidget: e.iconWidget, - onTap: e.onTap, + onTap: () { + e.onTap?.call(channel); + }, )) .toList() ?? [ IconSlideAction( color: backgroundColor, icon: Icons.more_horiz, - onTap: widget.onMoreDetailsPressed ?? - () { - showModalBottomSheet( - clipBehavior: Clip.hardEdge, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(32), - topRight: Radius.circular(32), + 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); - }, + context: context, + builder: (context) => StreamChannel( + channel: channel, + child: ChannelBottomSheet( + onViewInfoTap: () { + widget.onViewInfoTap?.call(channel); + }, + ), ), - ), - ); - }, + ); + }, ), if ([ 'admin', @@ -522,24 +530,27 @@ class _ChannelListViewState extends State { iconWidget: StreamSvgIcon.delete( color: chatThemeData.colorTheme.accentRed, ), - onTap: widget.onDeletePressed ?? - () 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(); + 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( @@ -679,5 +690,5 @@ class SwipeAction { Widget iconWidget; /// Callback when icon is tapped - VoidCallback? onTap; + ChannelInfoCallback? onTap; }