From 6b29630a644db6e7cecec771850901dc63320b62 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 2 Jun 2023 12:24:04 +0530 Subject: [PATCH 01/11] feat(llc, persistence): add support for open and close persistence via client. Signed-off-by: xsahil03x --- .../stream_chat/lib/src/client/channel.dart | 17 ++- .../stream_chat/lib/src/client/client.dart | 117 ++++++++++++------ .../lib/src/db/chat_persistence_client.dart | 5 + .../src/db/chat_persistence_client_test.dart | 3 + packages/stream_chat/test/src/mocks.dart | 11 +- .../src/stream_chat_persistence_client.dart | 3 + 6 files changed, 103 insertions(+), 53 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 3b249fca..54feb2ab 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -1379,13 +1379,14 @@ class Channel { this.state?.updateChannelState(updatedState); return updatedState; } catch (e) { - if (!_client.persistenceEnabled) { - rethrow; + if (_client.persistenceEnabled) { + return _client.chatPersistenceClient!.getChannelStateByCid( + cid!, + messagePagination: messagesPagination, + ); } - return _client.chatPersistenceClient!.getChannelStateByCid( - cid!, - messagePagination: messagesPagination, - ); + + rethrow; } } @@ -1841,9 +1842,7 @@ class ChannelClientState { /// [isUpToDate] flag count as a stream. Stream get isUpToDateStream => _isUpToDateController.stream; - - final BehaviorSubject _isUpToDateController = - BehaviorSubject.seeded(true); + final _isUpToDateController = BehaviorSubject.seeded(true); /// The retry queue associated to this channel. late final RetryQueue _retryQueue; diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 7627cf1d..54fefd57 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -125,10 +125,6 @@ class StreamChatClient { final _tokenManager = TokenManager(); final _connectionIdManager = ConnectionIdManager(); - set chatPersistenceClient(ChatPersistenceClient? value) { - _originalChatPersistenceClient = value; - } - /// Default user agent for all requests static String defaultUserAgent = 'stream-chat-dart-client-${CurrentPlatform.name}'; @@ -139,15 +135,15 @@ class StreamChatClient { /// The current package version static const packageVersion = PACKAGE_VERSION; - ChatPersistenceClient? _originalChatPersistenceClient; - /// Chat persistence client - ChatPersistenceClient? get chatPersistenceClient => _chatPersistenceClient; + ChatPersistenceClient? chatPersistenceClient; - ChatPersistenceClient? _chatPersistenceClient; - - /// Whether the chat persistence is available or not - bool get persistenceEnabled => _chatPersistenceClient != null; + /// Returns `True` if the [chatPersistenceClient] is available and connected. + /// Otherwise, returns `False`. + bool get persistenceEnabled { + final client = chatPersistenceClient; + return client != null && client.isConnected; + } late final RetryPolicy _retryPolicy; @@ -324,20 +320,27 @@ class StreamChatClient { final ownUser = OwnUser.fromUser(user); state.currentUser = ownUser; - if (!connectWebSocket) return ownUser; - try { - if (_originalChatPersistenceClient != null) { - _chatPersistenceClient = _originalChatPersistenceClient; - await _chatPersistenceClient!.connect(ownUser.id); + // Connect to persistence client if its set. + if (chatPersistenceClient != null) { + await openPersistenceConnection(ownUser); } - final connectedUser = await openConnection( - includeUserDetailsInConnectCall: true, - ); - return state.currentUser = connectedUser; + + // Connect to websocket if [connectWebSocket] is true. + // + // This is useful when you want to connect to websocket + // at a later stage or use the client in connection-less mode. + if (connectWebSocket) { + final connectedUser = await openConnection( + includeUserDetailsInConnectCall: true, + ); + state.currentUser = connectedUser; + } + + return state.currentUser!; } catch (e, stk) { if (e is StreamWebSocketError && e.isRetriable) { - final event = await _chatPersistenceClient?.getConnectionInfo(); + final event = await chatPersistenceClient?.getConnectionInfo(); if (event != null) return ownUser.merge(event.me); } logger.severe('error connecting user : ${ownUser.id}', e, stk); @@ -345,6 +348,40 @@ class StreamChatClient { } } + /// Connects the [chatPersistenceClient] to the given [user]. + Future openPersistenceConnection(User user) async { + final client = chatPersistenceClient; + if (client == null) { + throw const StreamChatError('Chat persistence client is not set'); + } + + if (client.isConnected) { + // If the persistence client is already connected to the userId, + // we don't need to connect again. + if (client.userId == user.id) return; + + throw const StreamChatError(''' + Chat persistence client is already connected to a different user, + please close the connection before connecting a new one.'''); + } + + // Connect the persistence client to the userId. + return client.connect(user.id); + } + + /// Disconnects the [chatPersistenceClient] from the current user. + Future closePersistenceConnection({bool flush = false}) async { + final client = chatPersistenceClient; + // If the persistence client is never connected, we don't need to close it. + if (client == null || !client.isConnected) { + logger.info('Chat persistence client is not connected'); + return; + } + + // Disconnect the persistence client. + return client.disconnect(flush: flush); + } + /// Creates a new WebSocket connection with the current user. /// If [includeUserDetailsInConnectCall] is true it will include the current /// user details in the connect call. @@ -422,7 +459,7 @@ class StreamChatClient { final connectionId = event.connectionId; if (connectionId != null) { _connectionIdManager.setConnectionId(connectionId); - _chatPersistenceClient?.updateConnectionInfo(event); + chatPersistenceClient?.updateConnectionInfo(event); } } @@ -460,9 +497,9 @@ class StreamChatClient { // channels are empty, assuming it's a fresh start // and making sure `lastSyncAt` is initialized if (persistenceEnabled) { - final lastSyncAt = await _chatPersistenceClient?.getLastSyncAt(); + final lastSyncAt = await chatPersistenceClient?.getLastSyncAt(); if (lastSyncAt == null) { - await _chatPersistenceClient?.updateLastSyncAt(DateTime.now()); + await chatPersistenceClient?.updateLastSyncAt(DateTime.now()); } } } @@ -493,13 +530,12 @@ class StreamChatClient { /// Will automatically fetch [cids] and [lastSyncedAt] if [persistenceEnabled] Future sync({List? cids, DateTime? lastSyncAt}) { return synchronized(() async { - final channels = cids ?? await _chatPersistenceClient?.getChannelCids(); + final channels = cids ?? await chatPersistenceClient?.getChannelCids(); if (channels == null || channels.isEmpty) { return; } - final syncAt = - lastSyncAt ?? await _chatPersistenceClient?.getLastSyncAt(); + final syncAt = lastSyncAt ?? await chatPersistenceClient?.getLastSyncAt(); if (syncAt == null) { return; } @@ -520,7 +556,7 @@ class StreamChatClient { final now = DateTime.now(); _lastSyncedAt = now; - _chatPersistenceClient?.updateLastSyncAt(now); + chatPersistenceClient?.updateLastSyncAt(now); } catch (e, stk) { logger.severe('Error during sync', e, stk); } @@ -679,7 +715,7 @@ class StreamChatClient { final updateData = _mapChannelStateToChannel(channels); - await _chatPersistenceClient?.updateChannelQueries( + await chatPersistenceClient?.updateChannelQueries( filter, channels.map((c) => c.channel!.cid).toList(), clearQueryCache: paginationParams.offset == 0, @@ -698,7 +734,7 @@ class StreamChatClient { List>? channelStateSort, PaginationParams paginationParams = const PaginationParams(), }) async { - final offlineChannels = (await _chatPersistenceClient?.getChannelStates( + final offlineChannels = (await chatPersistenceClient?.getChannelStates( filter: filter, // ignore: deprecated_member_use_from_same_package sort: sort, @@ -1362,7 +1398,7 @@ class StreamChatClient { final response = await _chatApi.message.deleteMessage(messageId, hard: hard); if (hard == true) { - await _chatPersistenceClient?.deleteMessageById(messageId); + await chatPersistenceClient?.deleteMessageById(messageId); } return response; } @@ -1468,34 +1504,33 @@ class StreamChatClient { Future disconnectUser({bool flushChatPersistence = false}) async { logger.info('Disconnecting user : ${state.currentUser?.id}'); - // resetting state + // resetting state. state.dispose(); state = ClientState(this); _lastSyncedAt = null; - // resetting credentials + // resetting credentials. _tokenManager.reset(); _connectionIdManager.reset(); - // disconnecting persistence client - await _chatPersistenceClient?.disconnect(flush: flushChatPersistence); - _chatPersistenceClient = null; + // closing persistence connection. + await closePersistenceConnection(flush: flushChatPersistence); // closing web-socket connection - closeConnection(); + return closeConnection(); } /// Call this function to dispose the client Future dispose() async { logger.info('Disposing new StreamChatClient'); - // disposing state + // disposing state. state.dispose(); - // disconnecting persistence client - await _chatPersistenceClient?.disconnect(); + // closing persistence connection. + await closePersistenceConnection(); - // closing web-socket connection + // closing web-socket connection. closeConnection(); await _eventController.close(); diff --git a/packages/stream_chat/lib/src/db/chat_persistence_client.dart b/packages/stream_chat/lib/src/db/chat_persistence_client.dart index 4710190e..bf0b1a76 100644 --- a/packages/stream_chat/lib/src/db/chat_persistence_client.dart +++ b/packages/stream_chat/lib/src/db/chat_persistence_client.dart @@ -17,6 +17,11 @@ abstract class ChatPersistenceClient { /// Whether the connection is established. bool get isConnected; + /// The current user id to which the client is connected. + /// + /// Returns `null` if the client is not connected. + String? get userId; + /// Creates a new connection to the client Future connect(String userId); diff --git a/packages/stream_chat/test/src/db/chat_persistence_client_test.dart b/packages/stream_chat/test/src/db/chat_persistence_client_test.dart index 62a7cf25..a0a12935 100644 --- a/packages/stream_chat/test/src/db/chat_persistence_client_test.dart +++ b/packages/stream_chat/test/src/db/chat_persistence_client_test.dart @@ -15,6 +15,9 @@ class TestPersistenceClient extends ChatPersistenceClient { @override bool get isConnected => throw UnimplementedError(); + @override + String? get userId => throw UnimplementedError(); + @override Future connect(String userId) => throw UnimplementedError(); diff --git a/packages/stream_chat/test/src/mocks.dart b/packages/stream_chat/test/src/mocks.dart index 44078519..86a77a66 100644 --- a/packages/stream_chat/test/src/mocks.dart +++ b/packages/stream_chat/test/src/mocks.dart @@ -64,11 +64,16 @@ class MockAttachmentFileUploader extends Mock implements AttachmentFileUploader {} class MockPersistenceClient extends Mock implements ChatPersistenceClient { - @override - Future connect(String userId) => Future.value(); + bool _isConnected = false; @override - Future disconnect({bool flush = false}) => Future.value(); + bool get isConnected => _isConnected; + + @override + Future connect(String userId) async => _isConnected = true; + + @override + Future disconnect({bool flush = false}) async => _isConnected = false; } class MockStreamChatClient extends Mock implements StreamChatClient { diff --git a/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart b/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart index 374bfa59..e8737baa 100644 --- a/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart +++ b/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart @@ -82,6 +82,9 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { @override bool get isConnected => db != null; + @override + String? get userId => db?.userId; + @override Future connect( String userId, { From 4ad042341be57a22d7922eec3c77a16ec45ce136 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 2 Jun 2023 12:26:52 +0530 Subject: [PATCH 02/11] chore: update CHANGELOG.md Signed-off-by: xsahil03x --- packages/stream_chat/CHANGELOG.md | 3 +++ packages/stream_chat_persistence/CHANGELOG.md | 2 ++ 2 files changed, 5 insertions(+) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 1989aea0..5406b1fa 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -8,6 +8,9 @@ ✅ Added - Added support for `ChatPersistenceClient.isConnected` for checking if the client is connected to the database. +- Added support for `ChatPersistenceClient.userId` for getting the current connected user id. +- Added two new methods `ChatPersistenceClient.disconnect` and `ChatPersistenceClient.connect` for disconnecting and + connecting to the database. ## 6.1.0 diff --git a/packages/stream_chat_persistence/CHANGELOG.md b/packages/stream_chat_persistence/CHANGELOG.md index 1295ba14..c2b608a3 100644 --- a/packages/stream_chat_persistence/CHANGELOG.md +++ b/packages/stream_chat_persistence/CHANGELOG.md @@ -3,6 +3,8 @@ - Added support for `StreamChatPersistenceClient.isConnected` for checking if the client is connected to the database. - [[#1422]](https://github.com/GetStream/stream-chat-flutter/issues/1422) Removed default values from `UserEntity` `createdAt` and `updatedAt` fields. +- Added support for `StreamChatPersistenceClient.openPersistenceConnection` + and `StreamChatPersistenceClient.closePersistenceConnection` for opening and closing the database connection. ## 6.1.0 From 02c5d039d702eb7c0d75ed666787c4a9141e1a1f Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 2 Jun 2023 12:55:39 +0530 Subject: [PATCH 03/11] test: update test. Signed-off-by: xsahil03x --- .../test/stream_chat_persistence_client_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart b/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart index 6791e07c..0f3223a6 100644 --- a/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart +++ b/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart @@ -20,7 +20,7 @@ void main() { await client.connect(userId, databaseProvider: testDatabaseProvider); expect(client.isConnected, true); expect(client.db, isA()); - expect(client.db!.userId, userId); + expect(client.userId, userId); addTearDown(() async { await client.disconnect(); @@ -33,7 +33,7 @@ void main() { await client.connect(userId, databaseProvider: testDatabaseProvider); expect(client.isConnected, true); expect(client.db, isA()); - expect(client.db!.userId, userId); + expect(client.userId, userId); expect( () => client.connect(userId, databaseProvider: testDatabaseProvider), throwsException, From 8978cb0035696b25583d6b27e195ea27caf06a35 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 5 Jun 2023 16:55:41 +0530 Subject: [PATCH 04/11] fix(llc): Remove cached channel once left. Signed-off-by: xsahil03x --- .../stream_chat/lib/src/client/client.dart | 22 ++++++++++++++++++- packages/stream_chat/lib/src/event_type.dart | 4 ++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 7627cf1d..d44750ed 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -1541,6 +1541,8 @@ class ClientState { currentUser = currentUser?.copyWith(totalUnreadCount: count); })); + _listenChannelLeft(); + _listenChannelDeleted(); _listenChannelHidden(); @@ -1601,12 +1603,30 @@ class ClientState { ); } + void _listenChannelLeft() { + _eventsSubscription?.add( + _client + .on( + EventType.memberRemoved, + EventType.notificationRemovedFromChannel, + ) + .listen((event) async { + final isCurrentUser = event.user!.id == currentUser!.id; + if (isCurrentUser) { + final eventChannel = event.channel!; + await _client.chatPersistenceClient + ?.deleteChannels([eventChannel.cid]); + channels.remove(eventChannel.cid)?.dispose(); + } + }), + ); + } + void _listenChannelDeleted() { _eventsSubscription?.add( _client .on( EventType.channelDeleted, - EventType.notificationRemovedFromChannel, EventType.notificationChannelDeleted, ) .listen((Event event) async { diff --git a/packages/stream_chat/lib/src/event_type.dart b/packages/stream_chat/lib/src/event_type.dart index f8fb7ed7..5bf6e0fd 100644 --- a/packages/stream_chat/lib/src/event_type.dart +++ b/packages/stream_chat/lib/src/event_type.dart @@ -63,7 +63,7 @@ class EventType { static const String notificationAddedToChannel = 'notification.added_to_channel'; - /// Event sent when the user is removed to a channel + /// Event sent when the user is removed from a channel static const String notificationRemovedFromChannel = 'notification.removed_from_channel'; @@ -76,7 +76,7 @@ class EventType { /// Event sent when a member is added to a channel static const String memberAdded = 'member.added'; - /// Event sent when a member is removed to a channel + /// Event sent when a member is removed from a channel static const String memberRemoved = 'member.removed'; /// Event sent when a member is updated in a channel From b6f4c194e5209420b954add295ff38b20f9b1d60 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 5 Jun 2023 16:57:15 +0530 Subject: [PATCH 05/11] chore: update CHANGELOG.md Signed-off-by: xsahil03x --- packages/stream_chat/CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index b001d99e..158a8722 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,3 +1,10 @@ +## Upcoming + +🐞 Fixed + +- [[#1585]](https://github.com/GetStream/stream-chat-flutter/issues/1585) Fixed channels left not being removed from + the persistent storage. + ## 6.2.0 🐞 Fixed From 7ef9dea72b43e3e8c39c463947279d0f1026638e Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 6 Jun 2023 15:17:06 +0530 Subject: [PATCH 06/11] chore(llc, core, ui): bump dio to ^5.2.0. Signed-off-by: xsahil03x --- .../lib/src/core/error/stream_chat_error.dart | 15 ++++++++---- .../http/interceptor/auth_interceptor.dart | 16 ++++++------- .../http/interceptor/logging_interceptor.dart | 22 ++++++++--------- .../src/core/http/stream_chat_dio_error.dart | 2 +- .../lib/src/core/http/stream_http_client.dart | 24 +++++++++---------- packages/stream_chat/lib/stream_chat.dart | 18 ++++++++------ packages/stream_chat/pubspec.yaml | 2 +- .../core/error/stream_chat_error_test.dart | 6 ++--- .../interceptor/auth_interceptor_test.dart | 16 ++++++------- .../core/http/stream_chat_dio_error_test.dart | 2 +- .../core/http/stream_http_client_test.dart | 18 +++++++------- packages/stream_chat_flutter/pubspec.yaml | 2 +- .../lib/src/stream_channel.dart | 15 ++++++------ .../test/stream_channel_test.dart | 4 ++-- 14 files changed, 85 insertions(+), 77 deletions(-) diff --git a/packages/stream_chat/lib/src/core/error/stream_chat_error.dart b/packages/stream_chat/lib/src/core/error/stream_chat_error.dart index 1df28558..333211f2 100644 --- a/packages/stream_chat/lib/src/core/error/stream_chat_error.dart +++ b/packages/stream_chat/lib/src/core/error/stream_chat_error.dart @@ -89,8 +89,13 @@ class StreamChatNetworkError extends StreamChatError { }) : super(message); /// - factory StreamChatNetworkError.fromDioError(DioError error) { - final response = error.response; + @Deprecated('Use `StreamChatNetworkError.fromDioException` instead') + factory StreamChatNetworkError.fromDioError(DioException error) = + StreamChatNetworkError.fromDioException; + + /// + factory StreamChatNetworkError.fromDioException(DioException exception) { + final response = exception.response; ErrorResponse? errorResponse; final data = response?.data; if (data != null) { @@ -100,12 +105,12 @@ class StreamChatNetworkError extends StreamChatError { code: errorResponse?.code ?? -1, message: errorResponse?.message ?? response?.statusMessage ?? - error.message ?? + exception.message ?? '', statusCode: errorResponse?.statusCode ?? response?.statusCode, data: errorResponse, - isRequestCancelledError: error.type == DioErrorType.cancel, - )..stackTrace = error.stackTrace; + isRequestCancelledError: exception.type == DioExceptionType.cancel, + )..stackTrace = exception.stackTrace; } /// Error code diff --git a/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart b/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart index 759e8e79..7bd9a957 100644 --- a/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart +++ b/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart @@ -46,26 +46,26 @@ class AuthInterceptor extends QueuedInterceptor { @override void onError( - DioError err, + DioException exception, ErrorInterceptorHandler handler, ) async { - final data = err.response?.data; + final data = exception.response?.data; if (data == null || data is! Map) { - return handler.next(err); + return handler.next(exception); } final error = ErrorResponse.fromJson(data); if (error.code == ChatErrorCode.tokenExpired.code) { - if (_tokenManager.isStatic) return handler.next(err); + if (_tokenManager.isStatic) return handler.next(exception); await _tokenManager.loadToken(refresh: true); try { - final options = err.requestOptions; + final options = exception.requestOptions; final response = await _client.fetch(options); return handler.resolve(response); - } on DioError catch (error) { - return handler.next(error); + } on DioException catch (exception) { + return handler.next(exception); } } - return handler.next(err); + return handler.next(exception); } } diff --git a/packages/stream_chat/lib/src/core/http/interceptor/logging_interceptor.dart b/packages/stream_chat/lib/src/core/http/interceptor/logging_interceptor.dart index 72fd70d0..6d6f74c3 100644 --- a/packages/stream_chat/lib/src/core/http/interceptor/logging_interceptor.dart +++ b/packages/stream_chat/lib/src/core/http/interceptor/logging_interceptor.dart @@ -119,32 +119,32 @@ class LoggingInterceptor extends Interceptor { } @override - void onError(DioError err, ErrorInterceptorHandler handler) { + void onError(DioException exception, ErrorInterceptorHandler handler) { if (error) { - if (err.type == DioErrorType.badResponse) { - final uri = err.response?.requestOptions.uri; + if (exception.type == DioExceptionType.badResponse) { + final uri = exception.response?.requestOptions.uri; _printBoxed( _logPrintError, header: - 'DioError ║ Status: ${err.response?.statusCode} ${err.response?.statusMessage}', + 'DioException ║ Status: ${exception.response?.statusCode} ${exception.response?.statusMessage}', text: uri.toString(), ); - if (err.response != null && err.response?.data != null) { - _logPrintError('╔ ${err.type.toString()}'); - _printResponse(_logPrintError, err.response!); + if (exception.response != null && exception.response?.data != null) { + _logPrintError('╔ ${exception.type.toString()}'); + _printResponse(_logPrintError, exception.response!); } _printLine(_logPrintError, '╚'); _logPrintError(''); } else { _printBoxed( _logPrintError, - header: 'DioError ║ ${err.type}', - text: err.message, + header: 'DioException ║ ${exception.type}', + text: exception.message, ); - _printRequestHeader(_logPrintError, err.requestOptions); + _printRequestHeader(_logPrintError, exception.requestOptions); } } - super.onError(err, handler); + super.onError(exception, handler); } @override diff --git a/packages/stream_chat/lib/src/core/http/stream_chat_dio_error.dart b/packages/stream_chat/lib/src/core/http/stream_chat_dio_error.dart index b494671c..5b6f91a0 100644 --- a/packages/stream_chat/lib/src/core/http/stream_chat_dio_error.dart +++ b/packages/stream_chat/lib/src/core/http/stream_chat_dio_error.dart @@ -2,7 +2,7 @@ import 'package:dio/dio.dart'; import 'package:stream_chat/src/core/error/error.dart'; /// Error class specific to StreamChat and Dio -class StreamChatDioError extends DioError { +class StreamChatDioError extends DioException { /// Initialize a stream chat dio error StreamChatDioError({ required this.error, diff --git a/packages/stream_chat/lib/src/core/http/stream_http_client.dart b/packages/stream_chat/lib/src/core/http/stream_http_client.dart index 99e72339..0d21f973 100644 --- a/packages/stream_chat/lib/src/core/http/stream_http_client.dart +++ b/packages/stream_chat/lib/src/core/http/stream_http_client.dart @@ -92,16 +92,16 @@ class StreamHttpClient { /// calling [close] will throw an exception. void close({bool force = false}) => httpClient.close(force: force); - StreamChatNetworkError _parseError(DioError err) { + StreamChatNetworkError _parseError(DioException exception) { StreamChatNetworkError error; // locally thrown dio error - if (err is StreamChatDioError) { - error = err.error; + if (exception is StreamChatDioError) { + error = exception.error; } else { // real network request dio error - error = StreamChatNetworkError.fromDioError(err); + error = StreamChatNetworkError.fromDioException(exception); } - return error..stackTrace = err.stackTrace; + return error..stackTrace = exception.stackTrace; } /// Handy method to make http GET request with error parsing. @@ -121,7 +121,7 @@ class StreamHttpClient { cancelToken: cancelToken, ); return response; - } on DioError catch (error) { + } on DioException catch (error) { throw _parseError(error); } } @@ -147,7 +147,7 @@ class StreamHttpClient { cancelToken: cancelToken, ); return response; - } on DioError catch (error) { + } on DioException catch (error) { throw _parseError(error); } } @@ -167,7 +167,7 @@ class StreamHttpClient { cancelToken: cancelToken, ); return response; - } on DioError catch (error) { + } on DioException catch (error) { throw _parseError(error); } } @@ -193,7 +193,7 @@ class StreamHttpClient { cancelToken: cancelToken, ); return response; - } on DioError catch (error) { + } on DioException catch (error) { throw _parseError(error); } } @@ -219,7 +219,7 @@ class StreamHttpClient { cancelToken: cancelToken, ); return response; - } on DioError catch (error) { + } on DioException catch (error) { throw _parseError(error); } } @@ -268,7 +268,7 @@ class StreamHttpClient { cancelToken: cancelToken, ); return response; - } on DioError catch (error) { + } on DioException catch (error) { throw _parseError(error); } } @@ -281,7 +281,7 @@ class StreamHttpClient { try { final response = await httpClient.fetch(requestOptions); return response; - } on DioError catch (error) { + } on DioException catch (error) { throw _parseError(error); } } diff --git a/packages/stream_chat/lib/stream_chat.dart b/packages/stream_chat/lib/stream_chat.dart index 1e0df28c..2dbf1afd 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -1,12 +1,17 @@ library stream_chat; export 'package:async/async.dart'; -export 'package:dio/src/cancel_token.dart'; -export 'package:dio/src/dio_error.dart'; -export 'package:dio/src/dio_mixin.dart' show Interceptor, InterceptorsWrapper; -export 'package:dio/src/multipart_file.dart'; -export 'package:dio/src/options.dart'; -export 'package:dio/src/options.dart' show ProgressCallback; +export 'package:dio/dio.dart' + show + DioException, + DioExceptionType, + RequestOptions, + CancelToken, + Interceptor, + InterceptorsWrapper, + MultipartFile, + Options, + ProgressCallback; export 'package:logging/logging.dart' show Logger, Level, LogRecord; export 'package:rate_limiter/rate_limiter.dart'; export 'package:uuid/uuid.dart'; @@ -17,7 +22,6 @@ export 'src/client/key_stroke_handler.dart'; export 'src/core/api/attachment_file_uploader.dart'; export 'src/core/api/requests.dart'; export 'src/core/api/responses.dart'; -export 'src/core/api/stream_chat_api.dart' show PushProvider; export 'src/core/api/stream_chat_api.dart'; export 'src/core/error/error.dart'; export 'src/core/http/interceptor/logging_interceptor.dart'; diff --git a/packages/stream_chat/pubspec.yaml b/packages/stream_chat/pubspec.yaml index 62bf7b52..cbd95b88 100644 --- a/packages/stream_chat/pubspec.yaml +++ b/packages/stream_chat/pubspec.yaml @@ -11,7 +11,7 @@ environment: dependencies: async: ^2.10.0 collection: ^1.17.0 - dio: ^5.1.1 + dio: ^5.2.0 equatable: ^2.0.5 freezed_annotation: ^2.2.0 http_parser: ^4.0.2 diff --git a/packages/stream_chat/test/src/core/error/stream_chat_error_test.dart b/packages/stream_chat/test/src/core/error/stream_chat_error_test.dart index a32e0d71..8d4e5465 100644 --- a/packages/stream_chat/test/src/core/error/stream_chat_error_test.dart +++ b/packages/stream_chat/test/src/core/error/stream_chat_error_test.dart @@ -60,7 +60,7 @@ void main() { expect(error.message, message); }); - test('.fromDioError', () { + test('.fromDioException', () { const code = 333; const statusCode = 666; const message = 'test-error-message'; @@ -69,7 +69,7 @@ void main() { ..code = code ..statusCode = statusCode ..message = message; - final dioError = DioError( + final dioError = DioException( requestOptions: options, response: Response( requestOptions: options, @@ -77,7 +77,7 @@ void main() { data: data.toJson(), ), ); - final error = StreamChatNetworkError.fromDioError(dioError); + final error = StreamChatNetworkError.fromDioException(dioError); expect(error, isNotNull); expect(error.code, code); expect(error.message, message); diff --git a/packages/stream_chat/test/src/core/http/interceptor/auth_interceptor_test.dart b/packages/stream_chat/test/src/core/http/interceptor/auth_interceptor_test.dart index 7bd81ff0..e5d06600 100644 --- a/packages/stream_chat/test/src/core/http/interceptor/auth_interceptor_test.dart +++ b/packages/stream_chat/test/src/core/http/interceptor/auth_interceptor_test.dart @@ -88,7 +88,7 @@ void main() { requestOptions: options, data: errorResponse.toJson(), ); - final err = DioError(requestOptions: options, response: response); + final err = DioException(requestOptions: options, response: response); final handler = ErrorInterceptorHandler(); when(() => tokenManager.isStatic).thenReturn(false); @@ -135,7 +135,7 @@ void main() { requestOptions: options, data: errorResponse.toJson(), ); - final err = DioError(requestOptions: options, response: response); + final err = DioException(requestOptions: options, response: response); final handler = ErrorInterceptorHandler(); when(() => tokenManager.isStatic).thenReturn(false); @@ -153,7 +153,7 @@ void main() { } catch (e) { // need to cast it as the type is private in dio final error = (e as dynamic).data; - expect(error, isA()); + expect(error, isA()); } verify(() => tokenManager.isStatic).called(1); @@ -179,7 +179,7 @@ void main() { requestOptions: options, data: errorResponse.toJson(), ); - final err = DioError(requestOptions: options, response: response); + final err = DioException(requestOptions: options, response: response); final handler = ErrorInterceptorHandler(); when(() => tokenManager.isStatic).thenReturn(true); @@ -191,8 +191,8 @@ void main() { } catch (e) { // need to cast it as the type is private in dio final error = (e as dynamic).data; - expect(error, isA()); - final response = StreamChatNetworkError.fromDioError(error); + expect(error, isA()); + final response = StreamChatNetworkError.fromDioException(error); expect(response.errorCode, code); } @@ -207,7 +207,7 @@ void main() { const path = 'test-request-path'; final options = RequestOptions(path: path); final response = Response(requestOptions: options); - final err = DioError(requestOptions: options, response: response); + final err = DioException(requestOptions: options, response: response); final handler = ErrorInterceptorHandler(); authInterceptor.onError(err, handler); @@ -217,7 +217,7 @@ void main() { } catch (e) { // need to cast it as the type is private in dio final error = (e as dynamic).data; - expect(error, isA()); + expect(error, isA()); } }, ); diff --git a/packages/stream_chat/test/src/core/http/stream_chat_dio_error_test.dart b/packages/stream_chat/test/src/core/http/stream_chat_dio_error_test.dart index f13cf50d..e29148bf 100644 --- a/packages/stream_chat/test/src/core/http/stream_chat_dio_error_test.dart +++ b/packages/stream_chat/test/src/core/http/stream_chat_dio_error_test.dart @@ -12,7 +12,7 @@ void main() { requestOptions: options, ); - expect(dioError, isA()); + expect(dioError, isA()); expect(dioError, isNotNull); expect(dioError.error, error); expect(dioError.requestOptions, options); diff --git a/packages/stream_chat/test/src/core/http/stream_http_client_test.dart b/packages/stream_chat/test/src/core/http/stream_http_client_test.dart index 51533b11..f63e08f1 100644 --- a/packages/stream_chat/test/src/core/http/stream_http_client_test.dart +++ b/packages/stream_chat/test/src/core/http/stream_http_client_test.dart @@ -21,7 +21,7 @@ void main() { statusCode: 200, ); - DioError throwableError( + DioException throwableError( String path, { StreamChatNetworkError? error, bool streamChatDioError = false, @@ -32,11 +32,11 @@ void main() { ..code = error?.code ..statusCode = error?.statusCode ..message = error?.message; - DioError? dioError; + DioException? dioError; if (streamChatDioError) { dioError = StreamChatDioError(error: error!, requestOptions: options); } else { - dioError = DioError( + dioError = DioException( error: error, requestOptions: options, response: Response( @@ -210,7 +210,7 @@ void main() { await client.get(path); } catch (e) { expect(e, isA()); - expect(e, StreamChatNetworkError.fromDioError(error)); + expect(e, StreamChatNetworkError.fromDioException(error)); } verify(() => dio.get( @@ -263,7 +263,7 @@ void main() { await client.post(path); } catch (e) { expect(e, isA()); - expect(e, StreamChatNetworkError.fromDioError(error)); + expect(e, StreamChatNetworkError.fromDioException(error)); } verify(() => dio.post( @@ -317,7 +317,7 @@ void main() { await client.delete(path); } catch (e) { expect(e, isA()); - expect(e, StreamChatNetworkError.fromDioError(error)); + expect(e, StreamChatNetworkError.fromDioException(error)); } verify(() => dio.delete( @@ -371,7 +371,7 @@ void main() { await client.patch(path); } catch (e) { expect(e, isA()); - expect(e, StreamChatNetworkError.fromDioError(error)); + expect(e, StreamChatNetworkError.fromDioException(error)); } verify(() => dio.patch( @@ -425,7 +425,7 @@ void main() { await client.put(path); } catch (e) { expect(e, isA()); - expect(e, StreamChatNetworkError.fromDioError(error)); + expect(e, StreamChatNetworkError.fromDioException(error)); } verify(() => dio.put( @@ -486,7 +486,7 @@ void main() { await client.postFile(path, file); } catch (e) { expect(e, isA()); - expect(e, StreamChatNetworkError.fromDioError(error)); + expect(e, StreamChatNetworkError.fromDioException(error)); } verify(() => dio.post( diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index c360f1ec..03ee2b22 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -17,7 +17,7 @@ dependencies: dart_vlc: ^0.4.0 desktop_drop: ^0.4.0 diacritic: ^0.1.3 - dio: ^5.1.1 + dio: ^5.2.0 ezanimation: ^0.6.0 file_picker: ^5.2.4 file_selector: ^0.9.0 diff --git a/packages/stream_chat_flutter_core/lib/src/stream_channel.dart b/packages/stream_chat_flutter_core/lib/src/stream_channel.dart index 9cfc3464..5bb8dee7 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_channel.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel.dart @@ -430,16 +430,15 @@ class StreamChannelState extends State { ], builder: (context, snapshot) { if (snapshot.hasError) { - var message = snapshot.error.toString(); - if (snapshot.error is DioError) { - final dioError = snapshot.error as DioError?; - if (dioError?.type == DioErrorType.badResponse) { - message = dioError!.message ?? 'Bad response'; - } else { - message = 'Check your connection and retry'; + final error = snapshot.error; + if (error is DioException) { + if (error.type == DioExceptionType.badResponse) { + return Center(child: Text(error.message ?? 'Bad response')); } + return const Center(child: Text('Check your connection and retry')); } - return Center(child: Text(message)); + + return Center(child: Text(error.toString())); } final dataLoaded = snapshot.data?.every((it) => it) == true; diff --git a/packages/stream_chat_flutter_core/test/stream_channel_test.dart b/packages/stream_chat_flutter_core/test/stream_channel_test.dart index f4acf3c9..5ba31042 100644 --- a/packages/stream_chat_flutter_core/test/stream_channel_test.dart +++ b/packages/stream_chat_flutter_core/test/stream_channel_test.dart @@ -91,8 +91,8 @@ void main() { ); const errorMessage = 'Error! Error! Error!'; - final error = DioError( - type: DioErrorType.badResponse, + final error = DioException( + type: DioExceptionType.badResponse, message: errorMessage, requestOptions: RequestOptions(), ); From 4ba0e9fc0ef6aeb59b17157c9c0c90177b5ce457 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 6 Jun 2023 15:19:20 +0530 Subject: [PATCH 07/11] chore: update CHANGELOG.md Signed-off-by: xsahil03x --- packages/stream_chat/CHANGELOG.md | 6 ++++++ packages/stream_chat_flutter/CHANGELOG.md | 6 ++++++ packages/stream_chat_flutter_core/CHANGELOG.md | 4 ++++ 3 files changed, 16 insertions(+) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index b001d99e..9173d27a 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,3 +1,9 @@ +## Upcoming + +🔄 Changed + +- Updated `dio` dependency to `^5.2.0`. + ## 6.2.0 🐞 Fixed diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 0ea80c8f..2e1b875f 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,3 +1,9 @@ +## Upcoming + +🔄 Changed + +- Updated `dio` dependency to `^5.2.0`. + ## 6.2.0 🐞 Fixed diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index c7869125..0c178fd4 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,3 +1,7 @@ +## Upcoming + +- Updated `stream_chat` dependency to [`Upcoming`](https://pub.dev/packages/stream_chat/changelog). + ## 6.2.0 - Fixed `StreamMessageInputController.textPatternStyle` not matching case-insensitive patterns. From 5322ad717068d6bd29408d3a8fb4774dba9a1ad3 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 6 Jun 2023 16:07:31 +0530 Subject: [PATCH 08/11] test: add tests. Signed-off-by: xsahil03x --- .../test/src/client/client_test.dart | 118 ++++++++++++++++++ packages/stream_chat/test/src/mocks.dart | 14 ++- 2 files changed, 130 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat/test/src/client/client_test.dart b/packages/stream_chat/test/src/client/client_test.dart index 7a605557..b1f724af 100644 --- a/packages/stream_chat/test/src/client/client_test.dart +++ b/packages/stream_chat/test/src/client/client_test.dart @@ -2544,4 +2544,122 @@ void main() { }, ); }); + + group('PersistenceConnectionTests', () { + const apiKey = 'test-api-key'; + late final api = FakeChatApi(); + late final ws = FakeWebSocket(); + + final user = User(id: 'test-user-id'); + final token = Token.development(user.id).rawValue; + + late StreamChatClient client; + + setUp(() async { + client = StreamChatClient(apiKey, chatApi: api, ws: ws); + expect(client.persistenceEnabled, isFalse); + }); + + tearDown(() { + client.chatPersistenceClient = null; + expect(client.persistenceEnabled, isFalse); + client.dispose(); + }); + + test('openPersistenceConnection connects the client to the user', () async { + client.chatPersistenceClient = MockPersistenceClient(); + await client.openPersistenceConnection(user); + expect(client.persistenceEnabled, isTrue); + }); + + test( + '''multiple call to openPersistenceConnection does not throws an error if already connected to the same user''', + () async { + client.chatPersistenceClient = MockPersistenceClient(); + await client.openPersistenceConnection(user); + expect(client.persistenceEnabled, isTrue); + + expect(() => client.openPersistenceConnection(user), returnsNormally); + expect(() => client.openPersistenceConnection(user), returnsNormally); + expect(() => client.openPersistenceConnection(user), returnsNormally); + }, + ); + + test( + '''openPersistenceConnection throws an error if client is already connected to a different user''', + () async { + client.chatPersistenceClient = MockPersistenceClient(); + await client.openPersistenceConnection(user); + expect(client.persistenceEnabled, isTrue); + + expect( + () => client.openPersistenceConnection(user.copyWith(id: 'new-id')), + throwsA(const TypeMatcher()), + ); + }, + ); + + test( + '''openPersistenceConnection throws an error if chatPersistenceClient is not set''', + () async { + expect( + () => client.openPersistenceConnection(user), + throwsA(const TypeMatcher()), + ); + }, + ); + + test('closePersistenceConnection disconnects the client', () async { + client.chatPersistenceClient = MockPersistenceClient(); + await client.openPersistenceConnection(user); + expect(client.persistenceEnabled, isTrue); + + await client.closePersistenceConnection(); + expect(client.persistenceEnabled, isFalse); + }); + + test( + '''closePersistenceConnection does nothing if chatPersistenceClient is not connected''', + () async { + client.chatPersistenceClient = MockPersistenceClient(); + expect(client.persistenceEnabled, isFalse); + + expect(() => client.closePersistenceConnection(), returnsNormally); + }, + ); + + test( + '''closePersistenceConnection does nothing if chatPersistenceClient is not set''', + () async { + expect(client.persistenceEnabled, isFalse); + expect(() => client.closePersistenceConnection(), returnsNormally); + }, + ); + + test( + '''connectUser should re-use the persistence connection if already connected''', + () async { + client.chatPersistenceClient = MockPersistenceClient(); + await client.openPersistenceConnection(user); + expect(client.persistenceEnabled, isTrue); + + await client.connectUser(user, token, connectWebSocket: false); + expect(client.persistenceEnabled, isTrue); + }, + ); + + test( + '''connectUser should throw if the persistence connection if already connected to a different user''', + () async { + client.chatPersistenceClient = MockPersistenceClient(); + await client.openPersistenceConnection(user.copyWith(id: 'new-id')); + expect(client.persistenceEnabled, isTrue); + + expect( + () => client.connectUser(user, token, connectWebSocket: false), + throwsA(const TypeMatcher()), + ); + }, + ); + }); } diff --git a/packages/stream_chat/test/src/mocks.dart b/packages/stream_chat/test/src/mocks.dart index 86a77a66..f412d15a 100644 --- a/packages/stream_chat/test/src/mocks.dart +++ b/packages/stream_chat/test/src/mocks.dart @@ -64,16 +64,26 @@ class MockAttachmentFileUploader extends Mock implements AttachmentFileUploader {} class MockPersistenceClient extends Mock implements ChatPersistenceClient { + String? _userId; bool _isConnected = false; @override bool get isConnected => _isConnected; @override - Future connect(String userId) async => _isConnected = true; + String? get userId => _userId; @override - Future disconnect({bool flush = false}) async => _isConnected = false; + Future connect(String userId) async { + _userId = userId; + _isConnected = true; + } + + @override + Future disconnect({bool flush = false}) async { + _userId = null; + _isConnected = false; + } } class MockStreamChatClient extends Mock implements StreamChatClient { From 0e67cdacf5282d1bf8b2510ee4fd0489636fc3ec Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 6 Jun 2023 16:15:54 +0530 Subject: [PATCH 09/11] test: update test for async methods. Signed-off-by: xsahil03x --- .../test/src/client/client_test.dart | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/packages/stream_chat/test/src/client/client_test.dart b/packages/stream_chat/test/src/client/client_test.dart index b1f724af..9ff0c69f 100644 --- a/packages/stream_chat/test/src/client/client_test.dart +++ b/packages/stream_chat/test/src/client/client_test.dart @@ -2579,9 +2579,9 @@ void main() { await client.openPersistenceConnection(user); expect(client.persistenceEnabled, isTrue); - expect(() => client.openPersistenceConnection(user), returnsNormally); - expect(() => client.openPersistenceConnection(user), returnsNormally); - expect(() => client.openPersistenceConnection(user), returnsNormally); + await expectLater(client.openPersistenceConnection(user), completes); + await expectLater(client.openPersistenceConnection(user), completes); + await expectLater(client.openPersistenceConnection(user), completes); }, ); @@ -2592,8 +2592,8 @@ void main() { await client.openPersistenceConnection(user); expect(client.persistenceEnabled, isTrue); - expect( - () => client.openPersistenceConnection(user.copyWith(id: 'new-id')), + await expectLater( + client.openPersistenceConnection(user.copyWith(id: 'new-id')), throwsA(const TypeMatcher()), ); }, @@ -2602,8 +2602,8 @@ void main() { test( '''openPersistenceConnection throws an error if chatPersistenceClient is not set''', () async { - expect( - () => client.openPersistenceConnection(user), + await expectLater( + client.openPersistenceConnection(user), throwsA(const TypeMatcher()), ); }, @@ -2619,32 +2619,34 @@ void main() { }); test( - '''closePersistenceConnection does nothing if chatPersistenceClient is not connected''', + '''closePersistenceConnection compeletes normally if chatPersistenceClient is not connected''', () async { client.chatPersistenceClient = MockPersistenceClient(); - expect(client.persistenceEnabled, isFalse); + expect(client.chatPersistenceClient!.isConnected, isFalse); - expect(() => client.closePersistenceConnection(), returnsNormally); + await expectLater(client.closePersistenceConnection(), completes); }, ); test( - '''closePersistenceConnection does nothing if chatPersistenceClient is not set''', + '''closePersistenceConnection completes normally if chatPersistenceClient is not set''', () async { expect(client.persistenceEnabled, isFalse); - expect(() => client.closePersistenceConnection(), returnsNormally); + await expectLater(client.closePersistenceConnection(), completes); }, ); test( - '''connectUser should re-use the persistence connection if already connected''', + '''connectUser completes normally if the persistence connection is already connected to the same user''', () async { client.chatPersistenceClient = MockPersistenceClient(); await client.openPersistenceConnection(user); expect(client.persistenceEnabled, isTrue); - await client.connectUser(user, token, connectWebSocket: false); - expect(client.persistenceEnabled, isTrue); + await expectLater( + client.connectUser(user, token, connectWebSocket: false), + completes, + ); }, ); @@ -2655,8 +2657,8 @@ void main() { await client.openPersistenceConnection(user.copyWith(id: 'new-id')); expect(client.persistenceEnabled, isTrue); - expect( - () => client.connectUser(user, token, connectWebSocket: false), + await expectLater( + client.connectUser(user, token, connectWebSocket: false), throwsA(const TypeMatcher()), ); }, From b70c1b97a8eb57ef272bb03a6d369026654ca304 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 7 Jun 2023 15:01:46 +0530 Subject: [PATCH 10/11] fix(ui): fix attachment download on web. Signed-off-by: xsahil03x --- .../lib/src/attachment/handler/common.dart | 70 ++++++++++++------- .../stream_attachment_handler_html.dart | 12 +++- .../handler/stream_attachment_handler_io.dart | 67 +++++++----------- 3 files changed, 80 insertions(+), 69 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/attachment/handler/common.dart b/packages/stream_chat_flutter/lib/src/attachment/handler/common.dart index fb4f192a..96d5e1ed 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/handler/common.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/handler/common.dart @@ -4,9 +4,41 @@ import 'package:dio/dio.dart'; import 'package:file_selector/file_selector.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -/// Downloads the [attachment] to the device and returns -/// the path to the file. -Future downloadWebOrDesktopAttachment( +/// Represents the url and bytes of an attachment. +class AttachmentData { + /// Creates a new [AttachmentData] instance. + const AttachmentData({ + required this.bytes, + required this.downloadUrl, + required this.fileName, + this.mimeType, + }); + + /// The data downloaded from the [downloadUrl]. + final Uint8List bytes; + + /// The url of the attachment that was used to download the [bytes]. + final String downloadUrl; + + /// The name of the file to use when saving the [bytes]. + final String fileName; + + /// The mime type of the attachment. + final String? mimeType; + + /// Creates an [XFile] from the [AttachmentData]. + XFile toXFile({String? path}) { + return XFile.fromData( + bytes, + mimeType: mimeType, + name: fileName, + path: path, + ); + } +} + +/// Downloads the [attachment] and returns the [AttachmentData]. +Future downloadAttachmentData( Attachment attachment, { ProgressCallback? onReceiveProgress, Map? queryParameters, @@ -34,13 +66,14 @@ Future downloadWebOrDesktopAttachment( fileName = attachment.title; } - assert( - downloadUrl != null, - 'Attachment must have an assetUrl or imageUrl or thumbUrl', - ); + if (downloadUrl == null) { + throw ArgumentError( + 'Attachment must have an assetUrl or imageUrl or thumbUrl', + ); + } final response = await Dio().get>( - downloadUrl!, + downloadUrl, onReceiveProgress: onReceiveProgress, queryParameters: queryParameters, cancelToken: cancelToken, @@ -49,23 +82,12 @@ Future downloadWebOrDesktopAttachment( Options(responseType: ResponseType.bytes), ); - // Open the native file browser so the user can select the download path. - final path = await getSavePath(suggestedName: fileName); + final bytes = Uint8List.fromList(response.data!); - if (path == null) { - // Operation was canceled by the user. - return null; - } - - // Create an XFile for proper file saving - final file = XFile.fromData( - Uint8List.fromList(response.data!), + return AttachmentData( + bytes: bytes, + downloadUrl: downloadUrl, + fileName: fileName!, mimeType: attachment.mimeType, - name: fileName, - path: path, ); - - // Save the file to the user's selected path. - await file.saveTo(path); - return path; } diff --git a/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_html.dart b/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_html.dart index 2d3a6fba..b3d4b121 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_html.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_html.dart @@ -50,13 +50,21 @@ class StreamAttachmentHandler extends StreamAttachmentHandlerBase { Map? queryParameters, CancelToken? cancelToken, Options? options, - }) { - return downloadWebOrDesktopAttachment( + }) async { + final data = await downloadAttachmentData( attachment, onReceiveProgress: onReceiveProgress, queryParameters: queryParameters, cancelToken: cancelToken, options: options, ); + + // Create an XFile for proper file saving. + final file = data.toXFile(); + + // Save the file. We are not using the path parameter because it is not + // supported on web. + await file.saveTo(''); + return null; } } diff --git a/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_io.dart b/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_io.dart index 268dbf15..0b04ee61 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_io.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_io.dart @@ -1,8 +1,7 @@ import 'dart:io'; -import 'dart:typed_data'; -import 'package:dio/dio.dart'; import 'package:file_picker/file_picker.dart'; +import 'package:file_selector/file_selector.dart'; import 'package:image_picker/image_picker.dart'; import 'package:path_provider/path_provider.dart'; import 'package:stream_chat_flutter/src/attachment/handler/common.dart'; @@ -21,14 +20,29 @@ class StreamAttachmentHandlerDesktop extends StreamAttachmentHandler { Map? queryParameters, CancelToken? cancelToken, Options? options, - }) { - return downloadWebOrDesktopAttachment( + }) async { + final data = await downloadAttachmentData( attachment, onReceiveProgress: onReceiveProgress, queryParameters: queryParameters, cancelToken: cancelToken, options: options, ); + + // Open the native file browser so the user can select the download path. + final path = await getSavePath(suggestedName: data.fileName); + + if (path == null) { + // Operation was canceled by the user. + return null; + } + + // Create an XFile for proper file saving. + final file = data.toXFile(path: path); + + // Save the file to the user's selected path. + await file.saveTo(path); + return path; } } @@ -160,53 +174,20 @@ class StreamAttachmentHandler extends StreamAttachmentHandlerBase { CancelToken? cancelToken, Options? options, }) async { - final type = attachment.type; - - String? downloadUrl; - String? fileName; - /* ---IMAGES/GIFS--- */ - if (type == 'image') { - downloadUrl = attachment.imageUrl ?? attachment.assetUrl; - fileName = attachment.title; - fileName ??= 'attachment.${attachment.mimeType ?? 'png'}'; - } - /* ---GIPHY's--- */ - else if (type == 'giphy') { - downloadUrl = attachment.thumbUrl; - fileName = '${attachment.title}.gif'; - } - /* ---FILES AND VIDEOS--- */ - else if (type == 'file' || type == 'video') { - downloadUrl = attachment.assetUrl; - fileName = attachment.title; - } - - assert( - downloadUrl != null, - 'Attachment must have an assetUrl or imageUrl or thumbUrl', - ); - - final response = await Dio().get>( - downloadUrl!, + final data = await downloadAttachmentData( + attachment, onReceiveProgress: onReceiveProgress, queryParameters: queryParameters, cancelToken: cancelToken, - // set responseType to `bytes` - options: options?.copyWith(responseType: ResponseType.bytes) ?? - Options(responseType: ResponseType.bytes), + options: options, ); final appDir = await getTemporaryDirectory(); - final ext = Uri.parse(downloadUrl).pathSegments.last; + final ext = Uri.parse(data.downloadUrl).pathSegments.last; final path = '${appDir.path}/${attachment.id}.$ext'; - // Create an XFile for proper file saving - final file = XFile.fromData( - Uint8List.fromList(response.data!), - mimeType: attachment.mimeType, - name: fileName, - path: path, - ); + // Create an XFile for proper file saving. + final file = data.toXFile(path: path); // Save the file to the user's selected path. await file.saveTo(path); From fa2263e93f7a9eb15148076fba9476994b6f6d28 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 7 Jun 2023 15:03:06 +0530 Subject: [PATCH 11/11] chore: update CHANGELOG.md Signed-off-by: xsahil03x --- packages/stream_chat_flutter/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 2e1b875f..981c2986 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,5 +1,9 @@ ## Upcoming +🐞 Fixed + +- [[#1592]](https://github.com/GetStream/stream-chat-flutter/issues/1592) Fixed broken attachment download on web. + 🔄 Changed - Updated `dio` dependency to `^5.2.0`.