diff --git a/packages/stream_chat/lib/src/api/channel.dart b/packages/stream_chat/lib/src/api/channel.dart index cb3cfe37..abdae6ce 100644 --- a/packages/stream_chat/lib/src/api/channel.dart +++ b/packages/stream_chat/lib/src/api/channel.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:convert'; import 'dart:math'; import 'package:collection/collection.dart' @@ -10,9 +9,9 @@ import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/src/api/retry_queue.dart'; import 'package:stream_chat/src/event_type.dart'; import 'package:stream_chat/src/extensions/rate_limit.dart'; -import 'package:stream_chat/src/models/attachment_file.dart'; -import 'package:stream_chat/src/models/channel_state.dart'; -import 'package:stream_chat/src/models/user.dart'; +import 'package:stream_chat/src/core/models/attachment_file.dart'; +import 'package:stream_chat/src/core/models/channel_state.dart'; +import 'package:stream_chat/src/core/models/user.dart'; import 'package:stream_chat/stream_chat.dart'; /// This a the class that manages a specific channel. @@ -214,8 +213,6 @@ class Channel { StreamChatClient get client => _client; final StreamChatClient _client; - String get _channelURL => '/channels/$type/$id'; - final Completer _initializedCompleter = Completer(); /// True if this is initialized @@ -492,7 +489,7 @@ class Channel { state?.addMessage(message); - final response = await _client.deleteMessage(message); + final response = await _client.deleteMessage(message.id); state?.addMessage(message.copyWith(status: MessageSendingStatus.sent)); @@ -507,9 +504,9 @@ class Channel { /// Pins provided message Future pinMessage( - Message message, - Object? timeoutOrExpirationDate, - ) { + Message message, { + Object? /*num|DateTime*/ timeoutOrExpirationDate, + }) { assert(() { if (timeoutOrExpirationDate is! DateTime && timeoutOrExpirationDate != null && @@ -517,7 +514,7 @@ class Channel { throw ArgumentError('Invalid timeout or Expiration date'); } return true; - }(), 'Check for invalid token or expiration date'); + }(), 'Check for invalid timeout or expiration date'); DateTime? pinExpires; if (timeoutOrExpirationDate is DateTime) { @@ -619,10 +616,7 @@ class Channel { /// Send an event on this channel Future sendEvent(Event event) { _checkInitialized(); - return _client.post( - '$_channelURL/event', - data: {'event': event.toJson()}, - ).then((res) => _client.decode(res.data, EmptyResponse.fromJson)!); + return _client.sendEvent(id!, type, event); } /// Send a reaction to this channel @@ -674,21 +668,13 @@ class Channel { state?.addMessage(newMessage); - final data = Map.from(extraData) - ..addAll({ - 'type': type, - }); - try { - final res = await _client.post( - '/messages/$messageId/reaction', - data: { - 'reaction': data, - 'enforce_unique': enforceUnique, - }, + final reactionResp = await _client.sendReaction( + messageId, + type, + extraData: extraData, + enforceUnique: enforceUnique, ); - final reactionResp = - _client.decode(res.data, SendReactionResponse.fromJson); return reactionResp; } catch (_) { // Reset the message if the update fails @@ -731,9 +717,11 @@ class Channel { state?.addMessage(newMessage); try { - final res = await client - .delete('/messages/${message.id}/reaction/${reaction.type}'); - return _client.decode(res.data, EmptyResponse.fromJson); + final deleteResponse = await _client.deleteReaction( + message.id, + reaction.type, + ); + return deleteResponse; } catch (_) { // Reset the message if the update fails state?.addMessage(message); @@ -746,45 +734,45 @@ class Channel { Map channelData, [ Message? updateMessage, ]) async { - final response = await _client.post(_channelURL, data: { - if (updateMessage != null) - 'message': updateMessage.copyWith(updatedAt: DateTime.now()).toJson(), - 'data': channelData, - }); - return _client.decode(response.data, UpdateChannelResponse.fromJson); + _checkInitialized(); + return _client.updateChannel( + id!, + type, + channelData, + message: updateMessage, + ); } /// Edit the channel custom data Future updatePartial( - Map channelData) async { - final response = await _client.patch(_channelURL, data: channelData); - return _client.decode(response.data, PartialUpdateChannelResponse.fromJson); + Map channelData, + ) async { + _checkInitialized(); + return _client.updateChannelPartial(id!, type, channelData); } /// Delete this channel. Messages are permanently removed. Future delete() async { - final response = await _client.delete(_channelURL); - return _client.decode(response.data, EmptyResponse.fromJson); + _checkInitialized(); + return _client.deleteChannel(id!, type); } /// Removes all messages from the channel Future truncate() async { - final response = await _client.post('$_channelURL/truncate'); - return _client.decode(response.data, EmptyResponse.fromJson); + _checkInitialized(); + return _client.truncateChannel(id!, type); } /// Accept invitation to the channel Future acceptInvite([Message? message]) async { - final res = await _client.post(_channelURL, - data: {'accept_invite': true, 'message': message?.toJson()}); - return _client.decode(res.data, AcceptInviteResponse.fromJson); + _checkInitialized(); + return _client.acceptChannelInvite(id!, type, message: message); } /// Reject invitation to the channel Future rejectInvite([Message? message]) async { - final res = await _client.post(_channelURL, - data: {'reject_invite': true, 'message': message?.toJson()}); - return _client.decode(res.data, RejectInviteResponse.fromJson); + _checkInitialized(); + return _client.rejectChannelInvite(id!, type, message: message); } /// Add members to the channel @@ -792,11 +780,8 @@ class Channel { List memberIds, [ Message? message, ]) async { - final res = await _client.post(_channelURL, data: { - 'add_members': memberIds, - 'message': message?.toJson(), - }); - return _client.decode(res.data, AddMembersResponse.fromJson); + _checkInitialized(); + return _client.addChannelMembers(id!, type, memberIds, message: message); } /// Invite members to the channel @@ -804,11 +789,8 @@ class Channel { List memberIds, [ Message? message, ]) async { - final res = await _client.post(_channelURL, data: { - 'invites': memberIds, - 'message': message?.toJson(), - }); - return _client.decode(res.data, InviteMembersResponse.fromJson); + _checkInitialized(); + return _client.inviteChannelMembers(id!, type, memberIds, message: message); } /// Remove members from the channel @@ -816,11 +798,8 @@ class Channel { List memberIds, [ Message? message, ]) async { - final res = await _client.post(_channelURL, data: { - 'remove_members': memberIds, - 'message': message?.toJson(), - }); - return _client.decode(res.data, RemoveMembersResponse.fromJson); + _checkInitialized(); + return _client.removeChannelMembers(id!, type, memberIds, message: message); } /// Send action for a specific message of this channel @@ -829,16 +808,8 @@ class Channel { Map formData, ) async { _checkInitialized(); - final messageId = message.id; - final response = await _client.post('/messages/$messageId/action', data: { - 'id': id, - 'type': type, - 'form_data': formData, - 'message_id': messageId, - }); - - final res = _client.decode(response.data, SendActionResponse.fromJson); + final res = await _client.sendAction(id!, type, messageId, formData); if (res.message != null) { state!.addMessage(res.message!); @@ -867,21 +838,20 @@ class Channel { state!.threads[oldMessage.parentId!]!..remove(oldMessage)); } } - await _client.chatPersistenceClient?.deleteMessageById(messageId); } - return res; } - /// Mark all channel messages as read - Future markRead() async { + /// Mark all messages as read + /// Optionally provide a [messageId] if you want to mark a + /// particular message as read + Future markRead({String? messageId}) async { _checkInitialized(); client.state.totalUnreadCount = max( 0, (client.state.totalUnreadCount ?? 0) - (state!.unreadCount ?? 0)); state!._unreadCountController.add(0); - final response = await _client.post('$_channelURL/read', data: {}); - return _client.decode(response.data, EmptyResponse.fromJson); + return _client.markChannelRead(id!, type, messageId: messageId); } /// Loads the initial channel state and watches for changes @@ -924,11 +894,8 @@ class Channel { /// Stop watching the channel Future stopWatching() async { - final response = await _client.post( - '$_channelURL/stop-watching', - data: {}, - ); - return _client.decode(response.data, EmptyResponse.fromJson); + _checkInitialized(); + return _client.stopChannelWatching(id!, type); } /// List the message replies for a parent message @@ -949,50 +916,29 @@ class Channel { return QueryRepliesResponse()..messages = cachedReplies; } } - - final response = await _client.get('/messages/$parentId/replies', - queryParameters: options.toJson()); - - final repliesResponse = _client.decode( - response.data, - QueryRepliesResponse.fromJson, - ); - + final repliesResponse = await _client.getReplies(parentId, options); state?.updateThreadInfo(parentId, repliesResponse.messages); - return repliesResponse; } /// List the reactions for a message in the channel Future getReactions( - String messageID, + String messageId, PaginationParams options, - ) async { - final response = await _client.get( - '/messages/$messageID/reactions', - queryParameters: options.toJson(), - ); - return _client.decode( - response.data, QueryReactionsResponse.fromJson); - } + ) => + _client.getReactions( + messageId, + options, + ); /// Retrieves a list of messages by ID Future getMessagesById( - List messageIDs) async { - final response = await _client.get( - '$_channelURL/messages', - queryParameters: {'ids': messageIDs.join(',')}, - ); - - final res = _client.decode( - response.data, - GetMessagesByIdResponse.fromJson, - ); - + List messageIDs, + ) async { + _checkInitialized(); + final res = await _client.getMessagesById(id!, type, messageIDs); final messages = res.messages; - state?.updateChannelState(ChannelState(messages: messages)); - return res; } @@ -1000,18 +946,11 @@ class Channel { Future translateMessage( String messageId, String language, - ) async { - final response = await _client.post( - '/messages/$messageId/translate', - data: { - 'language': language, - }, - ); - return _client.decode( - response.data, - TranslateMessageResponse.fromJson, - ); - } + ) => + _client.translateMessage( + messageId, + language, + ); /// Creates a new channel Future create() async => query(options: { @@ -1030,36 +969,10 @@ class Channel { PaginationParams? watchersPagination, bool preferOffline = false, }) async { - var path = '/channels/$type'; - if (id != null) path = '$path/$id'; - path = '$path/query'; - - final payload = Map.from({ - 'state': true, - }) - ..addAll(options); - - if (_extraData.isNotEmpty) { - payload['data'] = _extraData; - } - - if (messagesPagination != null) { - payload['messages'] = messagesPagination.toJson(); - } - if (membersPagination != null) { - payload['members'] = membersPagination.toJson(); - } - if (watchersPagination != null) { - payload['watchers'] = watchersPagination.toJson(); - } - if (preferOffline && cid != null) { - final updatedState = - (await _client.chatPersistenceClient?.getChannelStateByCid( - cid!, - messagePagination: messagesPagination, - ))!; - if (updatedState.messages.isNotEmpty) { + final updatedState = await _client.chatPersistenceClient + ?.getChannelStateByCid(cid!, messagePagination: messagesPagination); + if (updatedState != null && updatedState.messages.isNotEmpty) { if (state == null) { _initState(updatedState); } else { @@ -1070,8 +983,14 @@ class Channel { } try { - final response = await _client.post(path, data: payload); - final updatedState = _client.decode(response.data, ChannelState.fromJson); + final updatedState = await _client.queryChannel( + type, + channelId: id, + channelData: _extraData, + messagesPagination: messagesPagination, + membersPagination: membersPagination, + watchersPagination: watchersPagination, + ); if (_id == null) { _id = updatedState.channel!.id; @@ -1096,45 +1015,26 @@ class Channel { Filter? filter, List? sort, PaginationParams? pagination, - }) async { - final payload = { - 'sort': sort, - 'filter_conditions': filter ?? {}, - 'type': type, - }; - - if (pagination != null) { - payload.addAll(pagination.toJson()); - } - - if (id != null) { - payload['id'] = id; - } else if (state?.members.isNotEmpty == true) { - payload['members'] = state!.members; - } - - final rawRes = await _client.get('/members', queryParameters: { - 'payload': jsonEncode(payload), - }); - final response = _client.decode(rawRes.data, QueryMembersResponse.fromJson); - return response; - } + }) => + _client.queryMembers( + type, + channelId: id, + filter: filter, + members: state?.members, + sort: sort, + pagination: pagination, + ); /// Mutes the channel - Future mute({Duration? expiration}) async { - final response = await _client.post('/moderation/mute/channel', data: { - 'channel_cid': cid, - if (expiration != null) 'expiration': expiration.inMilliseconds, - }); - return _client.decode(response.data, EmptyResponse.fromJson); + Future mute({Duration? expiration}) { + _checkInitialized(); + return _client.muteChannel(cid!, expiration: expiration); } /// Unmutes the channel - Future unmute() async { - final response = await _client.post('/moderation/unmute/channel', data: { - 'channel_cid': cid, - }); - return _client.decode(response.data, EmptyResponse.fromJson); + Future unmute() { + _checkInitialized(); + return _client.unmuteChannel(cid!); } /// Bans a user from the channel @@ -1188,9 +1088,11 @@ class Channel { /// will be removed for the user Future hide({bool clearHistory = false}) async { _checkInitialized(); - final response = await _client - .post('$_channelURL/hide', data: {'clear_history': clearHistory}); - + final response = await _client.hideChannel( + id!, + type, + clearHistory: clearHistory, + ); if (clearHistory == true) { state!.truncate(); final cid = _cid; @@ -1198,15 +1100,13 @@ class Channel { await _client.chatPersistenceClient?.deleteMessageByCid(cid); } } - - return _client.decode(response.data, EmptyResponse.fromJson); + return response; } /// Removes the hidden status for the channel Future show() async { _checkInitialized(); - final response = await _client.post('$_channelURL/show'); - return _client.decode(response.data, EmptyResponse.fromJson); + return _client.showChannel(id!, type); } /// Stream of [Event] coming from websocket connection specific for the @@ -1588,7 +1488,7 @@ class ChannelClientState { } readList.add(Read( user: event.user!, - lastRead: event.createdAt!, + lastRead: event.createdAt, unreadMessages: event.totalUnreadCount ?? 0, )); _channelState = _channelState.copyWith(read: readList); @@ -1793,7 +1693,7 @@ class ChannelClientState { BehaviorSubject.seeded({}); set _threads(Map> v) { - _channel._client.chatPersistenceClient?.updateMessages( + _channel.client.chatPersistenceClient?.updateMessages( _channel.cid!, v.values.expand((v) => v).toList(), ); diff --git a/packages/stream_chat/lib/src/api/retry_queue.dart b/packages/stream_chat/lib/src/api/retry_queue.dart index c13db9c6..e6d883a9 100644 --- a/packages/stream_chat/lib/src/api/retry_queue.dart +++ b/packages/stream_chat/lib/src/api/retry_queue.dart @@ -6,7 +6,7 @@ import 'package:stream_chat/src/api/channel.dart'; import 'package:stream_chat/src/api/retry_policy.dart'; import 'package:stream_chat/src/event_type.dart'; import 'package:stream_chat/src/exceptions.dart'; -import 'package:stream_chat/src/models/message.dart'; +import 'package:stream_chat/src/core/models/message.dart'; import 'package:stream_chat/stream_chat.dart'; /// The retry queue associated to a channel diff --git a/packages/stream_chat/lib/src/api/web_socket_channel_html.dart b/packages/stream_chat/lib/src/api/web_socket_channel_html.dart deleted file mode 100644 index ab821ca9..00000000 --- a/packages/stream_chat/lib/src/api/web_socket_channel_html.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:web_socket_channel/html.dart'; -import 'package:web_socket_channel/web_socket_channel.dart'; - -/// Html version of websocket implementation -/// Used in Flutter web version -WebSocketChannel connectWebSocket(String url, {Iterable? protocols}) => - HtmlWebSocketChannel.connect(url, protocols: protocols); diff --git a/packages/stream_chat/lib/src/api/web_socket_channel_io.dart b/packages/stream_chat/lib/src/api/web_socket_channel_io.dart deleted file mode 100644 index 8402ca0c..00000000 --- a/packages/stream_chat/lib/src/api/web_socket_channel_io.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:web_socket_channel/io.dart'; -import 'package:web_socket_channel/web_socket_channel.dart'; - -/// IO version of websocket implementation -/// Used in Flutter mobile version -WebSocketChannel connectWebSocket(String url, {Iterable? protocols}) => - IOWebSocketChannel.connect(url, protocols: protocols); diff --git a/packages/stream_chat/lib/src/api/web_socket_channel_stub.dart b/packages/stream_chat/lib/src/api/web_socket_channel_stub.dart deleted file mode 100644 index e2efaee6..00000000 --- a/packages/stream_chat/lib/src/api/web_socket_channel_stub.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:web_socket_channel/web_socket_channel.dart'; - -/// Stub version of websocket implementation -/// Used just for conditional library import -WebSocketChannel connectWebSocket(String url, - {Iterable? protocols, - Map? headers, - Duration? pingInterval}) => - throw UnimplementedError(); diff --git a/packages/stream_chat/lib/src/api/websocket.dart b/packages/stream_chat/lib/src/api/websocket.dart deleted file mode 100644 index b5bbbb69..00000000 --- a/packages/stream_chat/lib/src/api/websocket.dart +++ /dev/null @@ -1,321 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:math'; - -import 'package:logging/logging.dart'; -import 'package:meta/meta.dart'; -import 'package:rxdart/rxdart.dart'; -import 'package:stream_chat/src/api/connection_status.dart'; -import 'package:stream_chat/src/models/event.dart'; -import 'package:stream_chat/src/models/user.dart'; -import 'package:web_socket_channel/web_socket_channel.dart'; - -/// Typedef which exposes an [Event] as the only parameter. -typedef EventHandler = void Function(Event); - -/// Typedef used for connecting to a websocket. Method returns a -/// [WebSocketChannel] and accepts a connection [url] and an optional -/// [Iterable] of `protocols`. -typedef ConnectWebSocket = WebSocketChannel Function(String? url, - {Iterable? protocols}); - -// TODO: parse error even -// TODO: if parsing an error into an event fails we should not hide the -// TODO: original error -/// A WebSocket connection that reconnects upon failure. -class WebSocket { - /// Creates a new websocket - /// To connect the WS call [connect] - WebSocket({ - required this.baseUrl, - required this.user, - required this.handler, - this.connectParams = const {}, - this.connectPayload = const {}, - this.logger, - this.connectFunc, - this.reconnectionMonitorInterval = 1, - this.healthCheckInterval = 20, - this.reconnectionMonitorTimeout = 40, - }) { - final qs = Map.from(connectParams); - - final data = Map.from(connectPayload); - - data['user_details'] = user.toJson(); - qs['json'] = json.encode(data); - - if (baseUrl.startsWith('https')) { - _path = baseUrl.replaceFirst('https://', ''); - _path = Uri.https(_path, 'connect', qs) - .toString() - .replaceFirst('https', 'wss'); - } else if (baseUrl.startsWith('http')) { - _path = baseUrl.replaceFirst('http://', ''); - _path = - Uri.http(_path, 'connect', qs).toString().replaceFirst('http', 'ws'); - } else { - _path = Uri.https(baseUrl, 'connect', qs) - .toString() - .replaceFirst('https', 'wss'); - } - } - - /// WS base url - final String baseUrl; - - /// User performing the WS connection - final User user; - - /// Querystring connection parameters - final Map connectParams; - - /// WS connection payload - final Map connectPayload; - - /// Functions that will be called every time a new event is received from the - /// connection - final EventHandler handler; - - /// A WS specific logger instance - final Logger? logger; - - /// Connection function - /// Used only for testing purpose - @visibleForTesting - final ConnectWebSocket? connectFunc; - - /// Interval of the reconnection monitor timer - /// This checks that it received a new event in the last - /// [reconnectionMonitorTimeout] seconds, otherwise it considers the - /// connection unhealthy and reconnects the WS - final int reconnectionMonitorInterval; - - /// Interval of the health event sending timer - /// This sends a health event every [healthCheckInterval] seconds in order to - /// make the server aware that the client is still listening - final int healthCheckInterval; - - /// The timeout that uses the reconnection monitor timer to consider the - /// connection unhealthy - final int reconnectionMonitorTimeout; - - final _connectionStatusController = - BehaviorSubject.seeded(ConnectionStatus.disconnected); - - set _connectionStatus(ConnectionStatus status) => - _connectionStatusController.add(status); - - /// The current connection status value - ConnectionStatus? get connectionStatus => _connectionStatusController.value; - - /// This notifies of connection status changes - Stream get connectionStatusStream => - _connectionStatusController.stream; - - late String _path; - int _retryAttempt = 1; - late WebSocketChannel _channel; - Timer? _healthCheck, _reconnectionMonitor; - DateTime? _lastEventAt; - bool _manuallyDisconnected = false; - bool _connecting = false; - bool _reconnecting = false; - - Event _decodeEvent(String source) => Event.fromJson(json.decode(source)); - - Completer _connectionCompleter = Completer(); - - /// Connect the WS using the parameters passed in the constructor - Future connect() async { - _manuallyDisconnected = false; - - if (_connecting) { - logger?.severe('already connecting'); - return null; - } - - _connecting = true; - _connectionStatus = ConnectionStatus.connecting; - - logger?.info('connecting to $_path'); - - _channel = - connectFunc?.call(_path) ?? WebSocketChannel.connect(Uri.parse(_path)); - _channel.stream.listen( - (data) async { - final jsonData = json.decode(data); - if (jsonData['error'] != null) { - return _onConnectionError(jsonData['error']); - } - _onData(data); - }, - onError: (error, stacktrace) { - _onConnectionError(error, stacktrace); - }, - onDone: _onDone, - ); - return _connectionCompleter.future; - } - - void _onDone() { - _connecting = false; - if (_manuallyDisconnected) { - return; - } - - logger?.info('connection closed | closeCode: ${_channel.closeCode} | ' - 'closedReason: ${_channel.closeReason}'); - - if (!_reconnecting) { - _reconnect(); - } - } - - void _onData(data) { - if (_manuallyDisconnected) { - return; - } - - final event = _decodeEvent(data); - logger?.info('received new event: $data'); - - if (_lastEventAt == null) { - logger?.info('connection estabilished'); - _connecting = false; - _reconnecting = false; - _lastEventAt = DateTime.now(); - - _connectionStatus = ConnectionStatus.connected; - _retryAttempt = 1; - - if (!_connectionCompleter.isCompleted) { - _connectionCompleter.complete(event); - } - - _startReconnectionMonitor(); - _startHealthCheck(); - } - - handler(event); - _lastEventAt = DateTime.now(); - } - - Future _onConnectionError(error, [stacktrace]) async { - logger?..severe('error connecting')..severe(error); - if (stacktrace != null) { - logger?.severe(stacktrace); - } - _connecting = false; - - if (!_reconnecting) { - _connectionStatus = ConnectionStatus.disconnected; - } - - if (!_connectionCompleter.isCompleted) { - _cancelTimers(); - _connectionCompleter.completeError(error, stacktrace); - } else if (!_reconnecting) { - return _reconnect(); - } - } - - void _reconnectionTimer(_) { - final now = DateTime.now(); - if (_lastEventAt != null && - now.difference(_lastEventAt!).inSeconds > reconnectionMonitorTimeout) { - _channel.sink.close(); - } - } - - void _startReconnectionMonitor() { - _reconnectionMonitor = Timer.periodic( - Duration(seconds: reconnectionMonitorInterval), - _reconnectionTimer, - ); - - _reconnectionTimer(_reconnectionMonitor); - } - - void _reconnectTimer() async { - if (!_reconnecting) { - return; - } - if (_connecting) { - logger?.info('already connecting'); - return; - } - - logger?.info('reconnecting..'); - - _cancelTimers(); - - try { - await connect(); - } catch (e) { - logger?.log(Level.SEVERE, e.toString()); - } - await Future.delayed( - Duration(seconds: min(_retryAttempt * 5, 25)), - () { - _reconnectTimer(); - _retryAttempt++; - }, - ); - } - - Future _reconnect() async { - logger?.info('reconnect'); - if (!_reconnecting) { - _reconnecting = true; - _connectionStatus = ConnectionStatus.connecting; - } - - _reconnectTimer(); - } - - void _cancelTimers() { - _lastEventAt = null; - if (_healthCheck != null) { - _healthCheck!.cancel(); - } - if (_reconnectionMonitor != null) { - _reconnectionMonitor!.cancel(); - } - } - - void _healthCheckTimer(_) { - logger?.info('sending health.check'); - _channel.sink.add("{'type': 'health.check'}"); - } - - void _startHealthCheck() { - logger?.info('start health check monitor'); - - _healthCheck = Timer.periodic( - Duration(seconds: healthCheckInterval), - _healthCheckTimer, - ); - - _healthCheckTimer(_healthCheck); - } - - /// Disconnects the WS and releases eventual resources - Future disconnect() async { - _connecting = false; - if (!_connectionCompleter.isCompleted) { - _connectionCompleter.complete(); - } - if (_manuallyDisconnected) { - return; - } - logger?.info('disconnecting'); - _connectionCompleter = Completer(); - _cancelTimers(); - _reconnecting = false; - _manuallyDisconnected = true; - _connectionStatus = ConnectionStatus.disconnected; - await _connectionStatusController.close(); - await _channel.sink.close(); - } -} diff --git a/packages/stream_chat/lib/src/client.dart b/packages/stream_chat/lib/src/client.dart index 3cabe274..936ccfc0 100644 --- a/packages/stream_chat/lib/src/client.dart +++ b/packages/stream_chat/lib/src/client.dart @@ -5,61 +5,46 @@ import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:logging/logging.dart'; -import 'package:meta/meta.dart'; import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/src/api/channel.dart'; -import 'package:stream_chat/src/api/connection_status.dart'; -import 'package:stream_chat/src/api/requests.dart'; -import 'package:stream_chat/src/api/responses.dart'; +import 'package:stream_chat/src/ws/connection_status.dart'; +import 'package:stream_chat/src/core/api/attachment_file_uploader.dart'; +import 'package:stream_chat/src/core/api/requests.dart'; +import 'package:stream_chat/src/core/api/responses.dart'; import 'package:stream_chat/src/api/retry_policy.dart'; -import 'package:stream_chat/src/api/websocket.dart'; -import 'package:stream_chat/src/attachment_file_uploader.dart'; +import 'package:stream_chat/src/ws/websocket.dart'; +import 'package:stream_chat/src/core/api/stream_chat_api.dart'; +import 'package:stream_chat/src/core/http/connection_id_manager.dart'; +import 'package:stream_chat/src/core/http/stream_http_client.dart'; +import 'package:stream_chat/src/core/http/token.dart'; +import 'package:stream_chat/src/core/http/token_manager.dart'; import 'package:stream_chat/src/db/chat_persistence_client.dart'; import 'package:stream_chat/src/event_type.dart'; -import 'package:stream_chat/src/exceptions.dart'; -import 'package:stream_chat/src/extensions/map_extension.dart'; -import 'package:stream_chat/src/models/attachment_file.dart'; -import 'package:stream_chat/src/models/channel_model.dart'; -import 'package:stream_chat/src/models/channel_state.dart'; -import 'package:stream_chat/src/models/event.dart'; -import 'package:stream_chat/src/models/filter.dart'; -import 'package:stream_chat/src/models/message.dart'; -import 'package:stream_chat/src/models/own_user.dart'; -import 'package:stream_chat/src/models/user.dart'; -import 'package:stream_chat/src/platform_detector/platform_detector.dart'; -import 'package:stream_chat/version.dart'; -import 'package:uuid/uuid.dart'; +import 'package:stream_chat/src/core/models/attachment_file.dart'; +import 'package:stream_chat/src/core/models/channel_model.dart'; +import 'package:stream_chat/src/core/models/channel_state.dart'; +import 'package:stream_chat/src/core/models/event.dart'; +import 'package:stream_chat/src/core/models/message.dart'; +import 'package:stream_chat/src/core/models/own_user.dart'; +import 'package:stream_chat/src/core/models/user.dart'; + +import 'package:stream_chat/src/core/models/filter.dart'; + +import 'package:stream_chat/src/core/models/member.dart'; /// Handler function used for logging records. Function requires a single /// [LogRecord] as the only parameter. typedef LogHandlerFunction = void Function(LogRecord record); -/// Used for decoding [Map] data to a generic type `T`. -typedef DecoderFunction = T Function(Map); - /// A function which can be used to request a Stream Chat API token from your /// own backend server. Function requires a single [userId]. -typedef TokenProvider = Future Function(String? userId); +typedef TokenProvider = Future Function(String userId); -/// Provider used to send push notifications. -enum PushProvider { - /// Send notifications using Google's Firebase Cloud Messaging - firebase, - - /// Send notifications using Apple's Push Notification service - apn -} - -extension on PushProvider { - /// Returns the string notion for [PushProvider]. - String get name { - if (this == PushProvider.apn) { - return 'apn'; - } else { - return 'firebase'; - } - } -} +final _levelEmojiMapper = { + Level.INFO: 'â„šī¸', + Level.WARNING: 'âš ī¸', + Level.SEVERE: '🚨', +}; /// The official Dart client for Stream Chat, /// a service for building chat applications. @@ -79,37 +64,64 @@ class StreamChatClient { /// You should only create the client once and re-use it across your /// application. StreamChatClient( - this.apiKey, { - this.tokenProvider, - this.baseURL = _defaultBaseURL, - this.logLevel = Level.WARNING, + String apiKey, { + this.logLevel = Level.ALL, LogHandlerFunction? logHandlerFunction, + RetryPolicy? retryPolicy, + Location? location, + String? baseURL, Duration connectTimeout = const Duration(seconds: 6), Duration receiveTimeout = const Duration(seconds: 6), - Dio? httpClient, - RetryPolicy? retryPolicy, - this.attachmentFileUploader, + StreamChatApi? chatApi, + AttachmentFileUploader? attachmentFileUploader, }) { - _retryPolicy = retryPolicy ?? - RetryPolicy( - retryTimeout: - (StreamChatClient client, int attempt, ApiError? error) => - Duration(seconds: 1 * attempt), - shouldRetry: - (StreamChatClient client, int attempt, ApiError? error) => - attempt < 5, + _setupLogger(logHandlerFunction); + + final options = StreamHttpClientOptions( + baseUrl: baseURL, + location: location, + connectTimeout: connectTimeout, + receiveTimeout: receiveTimeout, + ); + + _chatApi = chatApi ?? + StreamChatApi( + apiKey, + options: options, + tokenManager: _tokenManager, + connectionIdManager: _connectionIdManager, + attachmentFileUploader: attachmentFileUploader, + logger: _detachedLogger('đŸ•¸ī¸'), ); - attachmentFileUploader ??= StreamAttachmentFileUploader(this); + _ws = WebSocket( + apiKey: apiKey, + baseUrl: options.baseUrl, + tokenManager: _tokenManager, + handler: handleEvent, + logger: _detachedLogger('🔌'), + ); + + _retryPolicy = retryPolicy ?? + RetryPolicy( + retryTimeout: (_, int attempt, __) => Duration(seconds: 1 * attempt), + shouldRetry: (_, int attempt, __) => attempt < 5, + ); state = ClientState(this); - _setupLogger(logHandlerFunction); - _setupDio(httpClient, receiveTimeout, connectTimeout); - logger.info('instantiating new client'); } + late final StreamChatApi _chatApi; + late final WebSocket _ws; + + /// This client state + late ClientState state; + + final _tokenManager = TokenManager(); + final _connectionIdManager = ConnectionIdManager(); + set chatPersistenceClient(ChatPersistenceClient? value) { _originalChatPersistenceClient = value; } @@ -121,22 +133,20 @@ class StreamChatClient { ChatPersistenceClient? _chatPersistenceClient; - /// Attachment uploader - AttachmentFileUploader? attachmentFileUploader; - /// Whether the chat persistence is available or not bool get persistenceEnabled => _chatPersistenceClient != null; RetryPolicy? _retryPolicy; + // sync state of the channels present inside state, defaults to false bool _synced = false; + // the last dateTime at the which all the channels were synced + DateTime? _lastSyncedAt; + /// The retry policy options getter RetryPolicy? get retryPolicy => _retryPolicy; - /// This client state - late ClientState state; - /// By default the Chat client will write all messages with level Warn or /// Error to stdout. /// @@ -173,38 +183,13 @@ class StreamChatClient { ///``` late LogHandlerFunction logHandlerFunction; - /// Your project Stream Chat api key. - /// Find your API keys here https://getstream.io/dashboard/ - String apiKey; - - /// Your project Stream Chat base url. - final String baseURL; - - /// A function in which you send a request to your own backend to get a Stream - /// Chat API token. - /// - /// The token will be the return value of the function. - /// It's used by the client to refresh the token once expired or to connect - /// the user without a predefined token using [connectUserWithProvider]. - final TokenProvider? tokenProvider; - - /// [Dio] httpClient - /// It's be chosen because it's easy to use and supports interesting features - /// out of the box (Interceptors, Global configuration, FormData, - /// File downloading etc.) - @visibleForTesting - Dio httpClient = Dio(); - - static const _defaultBaseURL = 'chat-us-east-1.stream-io-api.com'; - static const _tokenExpiredErrorCode = 40; StreamSubscription? _connectionStatusSubscription; - Future Function(ConnectionStatus)? _connectionStatusHandler; - final BehaviorSubject _controller = BehaviorSubject(); + final _eventController = BehaviorSubject(); /// Stream of [Event] coming from websocket connection /// Listen to this or use the [on] method to filter specific event types - Stream get stream => _controller.stream; + Stream get eventStream => _eventController.stream; final _wsConnectionStatusController = BehaviorSubject.seeded(ConnectionStatus.disconnected); @@ -219,153 +204,17 @@ class StreamChatClient { /// This notifies the connection status of the websocket connection. /// Listen to this to get notified when the websocket tries to reconnect. Stream get wsConnectionStatusStream => - _wsConnectionStatusController.stream; + _wsConnectionStatusController.stream.distinct(); - /// The current user token - String? token; - - /// The id of the current websocket connection - String? get connectionId => _connectionId; - - bool _anonymous = false; - String? _connectionId; - late WebSocket _ws; - - bool get _hasConnectionId => _connectionId != null; - - void _setupDio( - Dio? httpClient, - Duration receiveTimeout, - Duration connectTimeout, - ) { - logger.info('http client setup'); - - this.httpClient = httpClient ?? Dio(); - - String url; - if (!baseURL.startsWith('https') && !baseURL.startsWith('http')) { - url = Uri.https(baseURL, '').toString(); - } else { - url = baseURL; - } - - this.httpClient.options.baseUrl = url; - this.httpClient.options.receiveTimeout = receiveTimeout.inMilliseconds; - this.httpClient.options.connectTimeout = connectTimeout.inMilliseconds; - this.httpClient.interceptors.add( - InterceptorsWrapper( - onRequest: (options, handler) async { - options.queryParameters.addAll(_commonQueryParams); - options.headers.addAll(_httpHeaders); - - if (_connectionId != null && - (options.data is Map || options.data == null)) { - options.data = { - 'connection_id': _connectionId, - ...options.data ?? {}, - }; - } - - var stringData = options.data.toString(); - - if (options.data is FormData) { - final multiPart = (options.data as FormData).files[0].value; - stringData = '${multiPart.filename} - ${multiPart.contentType}'; - } - - logger.info(''' - - method: ${options.method} - url: ${options.uri} - headers: ${options.headers} - data: $stringData - - '''); - handler.next(options); - }, - onError: _tokenExpiredInterceptor, - ), + LogHandlerFunction get _defaultLogHandler => (LogRecord record) { + print( + '${record.time} ' + '${_levelEmojiMapper[record.level] ?? record.level.name} ' + '${record.loggerName} ${record.message} ', ); - } - - Future _tokenExpiredInterceptor( - DioError err, - ErrorInterceptorHandler handler, - ) async { - final apiError = ApiError( - err.response?.data, - err.response?.statusCode, - ); - - if (apiError.code == _tokenExpiredErrorCode) { - logger.info('token expired'); - - if (tokenProvider != null) { - httpClient.lock(); - final userId = state.user!.id; - - await _disconnect(); - - final newToken = await tokenProvider!(userId); - await Future.delayed(const Duration(seconds: 4)); - token = newToken; - - httpClient.unlock(); - - await connectUser(User(id: userId), newToken); - - try { - handler.resolve( - await httpClient.request( - err.requestOptions.path, - cancelToken: err.requestOptions.cancelToken, - data: err.requestOptions.data, - onReceiveProgress: err.requestOptions.onReceiveProgress, - onSendProgress: err.requestOptions.onSendProgress, - queryParameters: err.requestOptions.queryParameters, - options: Options( - method: err.requestOptions.method, - sendTimeout: err.requestOptions.sendTimeout, - receiveTimeout: err.requestOptions.receiveTimeout, - extra: err.requestOptions.extra, - headers: err.requestOptions.headers, - responseType: err.requestOptions.responseType, - contentType: err.requestOptions.contentType, - validateStatus: err.requestOptions.validateStatus, - receiveDataWhenStatusError: - err.requestOptions.receiveDataWhenStatusError, - followRedirects: err.requestOptions.followRedirects, - maxRedirects: err.requestOptions.maxRedirects, - requestEncoder: err.requestOptions.requestEncoder, - responseDecoder: err.requestOptions.responseDecoder, - listFormat: err.requestOptions.listFormat, - ), - ), - ); - } on DioError { - handler.reject(err); - } - } - } - } - - LogHandlerFunction _getDefaultLogHandler() { - final levelEmojiMapper = { - Level.INFO.name: 'â„šī¸', - Level.WARNING.name: 'âš ī¸', - Level.SEVERE.name: '🚨', - }; - return (LogRecord record) { - print( - '(${record.time}) ' - '${levelEmojiMapper[record.level.name] ?? record.level.name} ' - '${record.loggerName} ${record.message}', - ); - if (record.stackTrace != null) { - print(record.stackTrace); - } - }; - } + if (record.error != null) print(record.error); + if (record.stackTrace != null) print(record.stackTrace); + }; Logger _detachedLogger( String name, @@ -376,265 +225,272 @@ class StreamChatClient { void _setupLogger(LogHandlerFunction? logHandlerFunction) { logger.level = logLevel; - - this.logHandlerFunction = logHandlerFunction ?? _getDefaultLogHandler(); - + this.logHandlerFunction = logHandlerFunction ?? _defaultLogHandler; logger.onRecord.listen(this.logHandlerFunction); - logger.info('logger setup'); } - /// Call this function to dispose the client - void dispose() async { - await _chatPersistenceClient?.disconnect(); - await _disconnect(); - httpClient.close(); - await _controller.close(); - state.dispose(); - await _wsConnectionStatusController.close(); - } - - Map get _httpHeaders => { - 'Authorization': token, - 'stream-auth-type': _authType, - 'X-Stream-Client': _userAgent, - 'Content-Encoding': 'gzip', - }; - + /// Set the current user, this triggers a connection to the API. /// Set the current user, this triggers a connection to the API. /// It returns a [Future] that resolves when the connection is setup. @Deprecated('Use `connectUser` instead. Will be removed in Future releases') - Future setUser(User user, String token) => connectUser(user, token); + Future setUser(User user, String token) => connectUser(user, token); /// Connects the current user, this triggers a connection to the API. /// It returns a [Future] that resolves when the connection is setup. - Future connectUser(User? user, String? token) async { - if (_connectCompleter != null && !_connectCompleter!.isCompleted) { - logger.warning('Already connecting'); - throw Exception('Already connecting'); - } - - _connectCompleter = Completer(); - - logger.info('connect user'); - - if (user == null) { - final e = Error(); - _connectCompleter! - .completeError(e, StackTrace.fromString('No user provided.')); - throw e; - } - - state.user = OwnUser.fromJson(user.toJson()); - this.token = token; - _anonymous = false; - - return connect().then((event) { - _connectCompleter!.complete(event); - return event; - }).catchError((e, s) { - _connectCompleter!.completeError(e, s); - throw e; - }); - } + Future connectUser(User user, String token) => + _connectUser(user, token: Token.fromRawValue(token)); /// Set the current user using the [tokenProvider] to fetch the token. /// It returns a [Future] that resolves when the connection is setup. @Deprecated( 'Use `connectUserWithProvider` instead. Will be removed in Future releases', ) - Future setUserWithProvider(User user) => - connectUserWithProvider(user); + Future setUserWithProvider(User user, TokenProvider tokenProvider) => + connectUserWithProvider(user, tokenProvider); /// Connects the current user using the [tokenProvider] to fetch the token. /// It returns a [Future] that resolves when the connection is setup. - Future connectUserWithProvider(User user) async { - if (tokenProvider == null) { - throw Exception(''' - TokenProvider must be provided in the constructor in order to use `connectUserWithProvider` method. - Use `connectUser` providing a token. - '''); - } - final token = await tokenProvider!(user.id); - return connectUser(user, token); + Future connectUserWithProvider( + User user, TokenProvider tokenProvider) => + _connectUser(user, provider: tokenProvider); + + /// Set the current user with an anonymous id, this triggers a connection to + /// the API. It returns a [Future] that resolves when the connection is setup. + @Deprecated( + 'Use `connectAnonymousUser` instead. Will be removed in Future releases', + ) + Future setAnonymousUser() => connectAnonymousUser(); + + /// Connects the current user with an anonymous id, this triggers a connection + /// to the API. It returns a [Future] that resolves when the connection is + /// setup. + Future connectAnonymousUser() async { + final token = Token.anonymous(); + final user = OwnUser(id: token.userId); + return _connectUser(user, token: token); } - /// Stream of [Event] coming from websocket connection + /// Set the current user as guest, this triggers a connection to the API. + /// It returns a [Future] that resolves when the connection is setup. + @Deprecated( + 'Use `connectGuestUser` instead. Will be removed in Future releases', + ) + Future setGuestUser(User user) => connectGuestUser(user); + + /// Connects the current user as guest, this triggers a connection to the API. + /// It returns a [Future] that resolves when the connection is setup. + Future connectGuestUser(User user) async { + final userId = user.id; + final anonymousToken = Token.anonymous(userId: userId); + + // setting anonymous token so that getGuestUser works + _tokenManager.setTokenOrProvider(userId, token: anonymousToken); + + final guestUser = await _chatApi.guest.getGuestUser(user); + + // resetting tokenManager after successful request + _tokenManager.reset(); + + final guestUserToken = Token.fromRawValue(guestUser.accessToken); + return _connectUser(guestUser.user, token: guestUserToken); + } + + Future _connectUser( + User user, { + Token? token, + TokenProvider? provider, + }) async { + if (_ws.connectionCompleter?.isCompleted == false) { + throw Exception( + 'User already getting connected, try calling `disconnectUser` ' + 'before trying to connect again', + ); + } + + logger.info('connecting user : ${user.id}'); + + await _tokenManager.setTokenOrProvider( + user.id, + token: token, + provider: provider, + ); + + final ownUser = OwnUser.fromUser(user); + state.user = ownUser; + + try { + if (_originalChatPersistenceClient != null) { + _chatPersistenceClient = _originalChatPersistenceClient; + await _chatPersistenceClient!.connect(ownUser.id); + } + final event = await openConnection(); + return event; + } catch (e, stk) { + logger.severe('error connecting user : ${ownUser.id}', e, stk); + rethrow; + } + } + + /// Creates a new WebSocket connection with the current user. + Future openConnection() async { + assert( + state.user != null, + 'User is not set on client, ' + 'use `connectUser` or `connectAnonymousUser` instead', + ); + + final user = state.user!; + + logger.info('Opening web-socket connection for ${user.id}'); + + if (wsConnectionStatus == ConnectionStatus.connecting) { + throw Exception('Connection already in progress for ${user.id}'); + } + + if (wsConnectionStatus == ConnectionStatus.connected) { + throw Exception('Connection already connected for ${user.id}'); + } + + _wsConnectionStatus = ConnectionStatus.connecting; + + _connectionStatusSubscription = _ws.connectionStatusStream.listen( + _connectionStatusHandler, + ); + + try { + return await _ws.connect(user); + } catch (e, stk) { + logger.severe('error connecting ws', e, stk); + rethrow; + } + } + + /// Disconnects the [_ws] connection, + /// without removing the user set on client. + /// + /// This will not trigger default auto-retry mechanism for reconnection. + /// You need to call [openConnection] to reconnect to [_ws]. + void closeConnection() { + _wsConnectionStatus = ConnectionStatus.disconnected; + + _connectionStatusSubscription?.cancel(); + _connectionStatusSubscription = null; + + _ws.disconnect(); + } + + void _handleHealthCheckEvent(Event event) { + final user = event.me; + if (user != null) state.user = user; + + final connectionId = event.connectionId; + if (connectionId != null) { + _connectionIdManager.setConnectionId(connectionId); + _chatPersistenceClient?.updateConnectionInfo(event); + } + } + + /// Method called to add a new event to the [_eventController]. + void handleEvent(Event event) { + if (event.type == EventType.healthCheck) { + return _handleHealthCheckEvent(event); + } + if (!event.isLocal && _synced) { + _lastSyncedAt = event.createdAt; + _chatPersistenceClient?.updateLastSyncAt(event.createdAt); + } + state.updateUser(event.user); + return _eventController.add(event); + } + + void _connectionStatusHandler(ConnectionStatus status) async { + final currentState = _wsConnectionStatus = status; + + handleEvent(Event( + type: EventType.connectionChanged, + online: status == ConnectionStatus.connected, + )); + + if (currentState == ConnectionStatus.connected) { + // connection recovered + final cids = state.channels.keys.toList(growable: false); + if (cids.isNotEmpty) { + await queryChannelsOnline( + filter: Filter.in_('cid', cids), + paginationParams: const PaginationParams(limit: 30), + ).then((_) => sync(cids: cids, lastSyncAt: _lastSyncedAt)); + } + handleEvent(Event( + type: EventType.connectionRecovered, + online: true, + )); + } else { + _synced = false; + } + } + + /// Stream of [Event] coming from [_ws] connection /// Pass an eventType as parameter in order to filter just a type of event Stream on([ String? eventType, String? eventType2, String? eventType3, String? eventType4, - ]) => - stream.where((event) => - eventType == null || - (event.type != null && - (event.type == eventType || - event.type == eventType2 || - event.type == eventType3 || - event.type == eventType4))); - - /// Method called to add a new event to the [_controller]. - void handleEvent(Event event) async { - logger.info('handle new event: ${event.toJson()}'); - if (event.connectionId != null) { - _connectionId = event.connectionId; - } - - if (!event.isLocal) { - final createdAt = event.createdAt; - if (_synced && createdAt != null) { - await _chatPersistenceClient?.updateConnectionInfo(event); - await _chatPersistenceClient?.updateLastSyncAt(createdAt); - } - } - - if (event.user != null) { - state._updateUser(event.user); - } - - if (event.me != null) { - state.user = event.me; - } - _controller.add(event); - } - - Completer? _connectCompleter; - - /// Connect the client websocket - Future connect() async { - logger.info('connecting'); - if (wsConnectionStatus == ConnectionStatus.connecting) { - logger.warning('Already connecting'); - throw Exception('Already connecting'); - } - - if (wsConnectionStatus == ConnectionStatus.connected) { - logger.warning('Already connected'); - throw Exception('Already connected'); - } - - _wsConnectionStatus = ConnectionStatus.connecting; - - if (_originalChatPersistenceClient != null) { - _chatPersistenceClient = _originalChatPersistenceClient; - await _chatPersistenceClient!.connect(state.user!.id); - } - - _ws = WebSocket( - baseUrl: baseURL, - user: state.user!, - connectParams: { - 'api_key': apiKey, - 'authorization': token!, - 'stream-auth-type': _authType, - 'X-Stream-Client': _userAgent, - }, - connectPayload: { - 'user_id': state.user!.id, - 'server_determines_connection_id': true, - }, - handler: handleEvent, - logger: _detachedLogger('🔌'), - ); - - _connectionStatusHandler = (ConnectionStatus status) async { - _wsConnectionStatus = status; - handleEvent( - Event( - type: EventType.connectionChanged, - online: status == ConnectionStatus.connected, - ), - ); - - if (status == ConnectionStatus.connected) { - handleEvent(const Event( - type: EventType.connectionRecovered, - online: true, - )); - if (state.channels.isNotEmpty == true) { - // ignore: unawaited_futures - queryChannelsOnline( - filter: Filter.in_('cid', state.channels.keys.toList()), - ).then( - (_) async { - await resync(); - }, - ); - } else { - _synced = false; - } - } - }; - - _connectionStatusSubscription = - _ws.connectionStatusStream.listen(_connectionStatusHandler); - - var event = await _chatPersistenceClient?.getConnectionInfo(); - - await _ws.connect().then((e) async { - if (e != null) { - _chatPersistenceClient?.updateConnectionInfo(e); - event = e; - } - resync(); - }).catchError((err, stacktrace) { - logger.severe('error connecting ws', err, stacktrace); - if (err is Map) { - // ignore: only_throw_errors - throw err; - } - }); - - return event; + ]) { + if (eventType == null) return eventStream; + return eventStream.where((event) => + event.type == eventType || + event.type == eventType2 || + event.type == eventType3 || + event.type == eventType4); } /// Get the events missed while offline to sync the offline storage - Future resync([List? cids]) async { - final lastSyncAt = await _chatPersistenceClient?.getLastSyncAt(); + /// Will automatically fetch [cids] and [lastSyncedAt] if [persistenceEnabled] + Future sync({List? cids, DateTime? lastSyncAt}) async { + cids ??= await _chatPersistenceClient?.getChannelCids(); + if (cids == null || cids.isEmpty) { + _synced = true; + return; + } + lastSyncAt ??= await _chatPersistenceClient?.getLastSyncAt(); if (lastSyncAt == null) { _synced = true; return; } - cids ??= await _chatPersistenceClient?.getChannelCids(); - - if (cids?.isEmpty == true) { - return; - } - try { - final rawRes = await post('/sync', data: { - 'channel_cids': cids, - 'last_sync_at': lastSyncAt.toUtc().toIso8601String(), - }); - logger.fine('rawRes: $rawRes'); + final res = await _chatApi.general.sync(cids, lastSyncAt); + final events = res.events + ..sort((a, b) => a.createdAt.compareTo(b.createdAt)); - final res = decode( - rawRes.data, - SyncResponse.fromJson, - ); + for (final event in events) { + logger.fine('event.type: ${event.type}'); + final messageText = event.message?.text; + if (messageText != null) { + logger.fine('event.message.text: $messageText'); + } + handleEvent(event); + } - res.events.sort((a, b) => a.createdAt!.compareTo(b.createdAt!)); - - res.events.forEach((element) { - logger - ..fine('element.type: ${element.type}') - ..fine('element.message.text: ${element.message?.text}'); - }); - - res.events.forEach(handleEvent); - - await _chatPersistenceClient?.updateLastSyncAt(DateTime.now()); _synced = true; - } catch (error) { - logger.severe('Error during resync $error'); + final now = DateTime.now(); + _lastSyncedAt = now; + _chatPersistenceClient?.updateLastSyncAt(now); + } catch (e, stk) { + _synced = false; + logger.severe('Error during sync', e, stk); } } - String? _asMap(sort) => sort?.map((s) => s.toJson().toString())?.join(''); + String _generateHash(List objects) { + final payload = json.encode(objects); + final payloadBytes = utf8.encode(payload); + final payloadB64 = base64.encode(payloadBytes); + return payloadB64; + } final _queryChannelsStreams = >>{}; @@ -642,15 +498,29 @@ class StreamChatClient { Stream> queryChannels({ Filter? filter, List>? sort, - Map? options, - PaginationParams paginationParams = const PaginationParams(), + bool state = true, + bool watch = true, + bool presence = false, + int? memberLimit, int? messageLimit, + PaginationParams paginationParams = const PaginationParams(), bool waitForConnect = true, }) async* { - final hash = base64.encode(utf8.encode( - '$filter${_asMap(sort)}$options${paginationParams.toJson()}' - '$messageLimit', - )); + if (!_connectionIdManager.hasConnectionId) { + // ignore: parameter_assignments + watch = false; + } + + final hash = _generateHash([ + filter, + sort, + state, + watch, + presence, + memberLimit, + messageLimit, + paginationParams, + ]); if (_queryChannelsStreams.containsKey(hash)) { yield await _queryChannelsStreams[hash]!; @@ -666,9 +536,12 @@ class StreamChatClient { final newQueryChannelsFuture = queryChannelsOnline( filter: filter, sort: sort, - options: options, - paginationParams: paginationParams, + state: state, + watch: watch, + presence: presence, + memberLimit: memberLimit, messageLimit: messageLimit, + paginationParams: paginationParams, waitForConnect: waitForConnect, ).whenComplete(() { _queryChannelsStreams.remove(hash); @@ -687,67 +560,52 @@ class StreamChatClient { Future> queryChannelsOnline({ Filter? filter, List>? sort, - Map? options, + bool state = true, + bool watch = true, + bool presence = false, + int? memberLimit, int? messageLimit, - PaginationParams paginationParams = const PaginationParams(), bool waitForConnect = true, + PaginationParams paginationParams = const PaginationParams(), }) async { if (waitForConnect) { - if (_connectCompleter != null && !_connectCompleter!.isCompleted) { + if (_ws.connectionCompleter?.isCompleted == false) { logger.info('awaiting connection completer'); - await _connectCompleter!.future; + await _ws.connectionCompleter?.future; } if (wsConnectionStatus != ConnectionStatus.connected) { throw Exception( - 'You cannot use queryChannels without an active connection.' - ' Please call `connectUser` to connect the client.', + 'You cannot use queryChannels without an active connection. ' + 'Please call `connectUser` to connect the client.', ); } } + if (!_connectionIdManager.hasConnectionId) { + // ignore: parameter_assignments + watch = false; + } + logger.info('Query channel start'); - final defaultOptions = { - 'state': true, - 'watch': true, - 'presence': false, - }; - - final payload = { - 'filter_conditions': filter, - 'sort': sort, - }; - - if (messageLimit != null) { - payload['message_limit'] = messageLimit; - } - - payload.addAll(defaultOptions); - - if (options != null) { - payload.addAll(options); - } - - payload.addAll(paginationParams.toJson()); - - final response = await get( - '/channels', - queryParameters: { - 'payload': jsonEncode(payload), - }, - ); - - final res = decode( - response.data, - QueryChannelsResponse.fromJson, + print('Query Channel Started : ${DateTime.now()}'); + final res = await _chatApi.channel.queryChannels( + filter: filter, + sort: sort, + state: state, + watch: watch, + presence: presence, + memberLimit: memberLimit, + messageLimit: messageLimit, + paginationParams: paginationParams, ); + print('Query Channel Completed : ${DateTime.now()}'); if (res.channels.isEmpty && paginationParams.offset == 0) { - logger.warning( - ''' + logger.warning(''' We could not find any channel for this query. - Please make sure to take a look at the Flutter tutorial: https://getstream.io/chat/flutter/tutorial - If your application already has users and channels, you might need to adjust your query channel as explained in the docs https://getstream.io/chat/docs/query_channels/?language=dart''', - ); + Please make sure to take a look at the Flutter tutorial: https://getstream.io/chat/flutter/tutorial + If your application already has users and channels, you might need to adjust your query channel as explained in the docs https://getstream.io/chat/docs/query_channels/?language=dart + '''); return []; } @@ -758,7 +616,7 @@ class StreamChatClient { .map((it) => it.user) .toList(growable: false); - state._updateUsers(users); + this.state.updateUsers(users); logger.info('Got ${res.channels.length} channels from api'); @@ -770,7 +628,7 @@ class StreamChatClient { clearQueryCache: paginationParams.offset == 0, ); - state.channels = updateData.key; + this.state.channels = updateData.key; return updateData.value; } @@ -812,253 +670,20 @@ class StreamChatClient { return MapEntry(channels, newChannels); } - Object _parseError(DioError error) { - if (error.type == DioErrorType.response) { - final apiError = - ApiError(error.response?.data, error.response?.statusCode); - logger.severe('apiError: ${apiError.toString()}'); - return apiError; - } - - return error; - } - - /// Handy method to make http GET request with error parsing. - Future> get( - String path, { - Map? queryParameters, - }) async { - try { - final response = await httpClient.get( - path, - queryParameters: queryParameters, - ); - return response; - } on DioError catch (error) { - // ignore: only_throw_errors - throw _parseError(error); - } - } - - /// Handy method to make http POST request with error parsing. - Future> post( - String path, { - dynamic data, - ProgressCallback? onSendProgress, - CancelToken? cancelToken, - }) async { - try { - final response = await httpClient.post( - path, - data: data, - onSendProgress: onSendProgress, - cancelToken: cancelToken, - ); - return response; - } on DioError catch (error) { - // ignore: only_throw_errors - throw _parseError(error); - } - } - - /// Handy method to make http DELETE request with error parsing. - Future> delete( - String path, { - Map? queryParameters, - CancelToken? cancelToken, - }) async { - try { - final response = await httpClient.delete( - path, - queryParameters: queryParameters, - cancelToken: cancelToken, - ); - return response; - } on DioError catch (error) { - // ignore: only_throw_errors - throw _parseError(error); - } - } - - /// Handy method to make http PATCH request with error parsing. - Future> patch( - String path, { - Map? queryParameters, - dynamic data, - }) async { - try { - final response = await httpClient.patch( - path, - queryParameters: queryParameters, - data: data, - ); - return response; - } on DioError catch (error) { - // ignore: only_throw_errors - throw _parseError(error); - } - } - - /// Handy method to make http PUT request with error parsing. - Future> put( - String path, { - Map? queryParameters, - dynamic data, - }) async { - try { - final response = await httpClient.put( - path, - queryParameters: queryParameters, - data: data, - ); - return response; - } on DioError catch (error) { - // ignore: only_throw_errors - throw _parseError(error); - } - } - - /// Used to log errors and stacktrace in case of bad json deserialization - T decode(String? j, DecoderFunction decoderFunction) { - try { - final data = j ?? '{}'; - return decoderFunction(json.decode(data)); - } catch (error, stacktrace) { - logger.severe('Error decoding response', error, stacktrace); - rethrow; - } - } - - String get _authType => _anonymous ? 'anonymous' : 'jwt'; - - String get _userAgent => 'stream-chat-dart-client-${CurrentPlatform.name}-' - '${PACKAGE_VERSION.split('+')[0]}'; - - Map get _commonQueryParams => { - 'user_id': state.user?.id, - 'api_key': apiKey, - 'connection_id': _connectionId, - }; - - /// Set the current user with an anonymous id, this triggers a connection to - /// the API. It returns a [Future] that resolves when the connection is setup. - @Deprecated( - 'Use `connectAnonymousUser` instead. Will be removed in Future releases') - Future setAnonymousUser() => connectAnonymousUser(); - - /// Connects the current user with an anonymous id, this triggers a connection - /// to the API. It returns a [Future] that resolves when the connection is - /// setup. - Future connectAnonymousUser() async { - if (_connectCompleter != null && !_connectCompleter!.isCompleted) { - logger.warning('Already connecting'); - throw Exception('Already connecting'); - } - - _connectCompleter = Completer(); - - _anonymous = true; - const uuid = Uuid(); - state.user = OwnUser(id: uuid.v4()); - - return connect().then((event) { - _connectCompleter!.complete(event); - return event; - }).catchError((e, s) { - _connectCompleter!.completeError(e, s); - throw e; - }); - } - - /// Set the current user as guest, this triggers a connection to the API. - /// It returns a [Future] that resolves when the connection is setup. - @Deprecated( - 'Use `connectGuestUser` instead. Will be removed in Future releases') - Future setGuestUser(User user) => connectGuestUser(user); - - /// Connects the current user as guest, this triggers a connection to the API. - /// It returns a [Future] that resolves when the connection is setup. - Future connectGuestUser(User user) async { - _anonymous = true; - final response = await post('/guest', data: {'user': user.toJson()}) - .then((res) => decode( - res.data, ConnectGuestUserResponse.fromJson)) - .whenComplete(() => _anonymous = false); - - return connectUser( - response.user, - response.accessToken, - ); - } - - /// Closes the websocket connection and resets the client - /// If [flushChatPersistence] is true the client deletes all offline - /// user's data. If [clearUser] is true the client unsets the current user - Future disconnect({ - bool flushChatPersistence = false, - bool clearUser = false, - }) async { - logger.info('Disconnecting flushOfflineStorage: $flushChatPersistence; ' - 'clearUser: $clearUser'); - - await _chatPersistenceClient?.disconnect(flush: flushChatPersistence); - _chatPersistenceClient = null; - - _connectCompleter = null; - - if (clearUser == true) { - state.dispose(); - state = ClientState(this); - } - - await _disconnect(); - } - - Future _disconnect() async { - logger.info('Client disconnecting'); - - await _ws.disconnect(); - await _connectionStatusSubscription?.cancel(); - } - /// Requests users with a given query. Future queryUsers({ + bool? presence, Filter? filter, List? sort, - Map? options, PaginationParams? pagination, }) async { - final defaultOptions = { - 'presence': _hasConnectionId, - }; - - final payload = { - 'filter_conditions': filter, - 'sort': sort, - }..addAll(defaultOptions); - - if (pagination != null) { - payload.addAll(pagination.toJson()); - } - - if (options != null) { - payload.addAll(options); - } - - final rawRes = await get( - '/users', - queryParameters: { - 'payload': jsonEncode(payload), - }, + final response = await _chatApi.user.queryUsers( + presence: presence ?? _connectionIdManager.hasConnectionId, + filter: filter, + sort: sort, + pagination: pagination, ); - - final response = decode( - rawRes.data, - QueryUsersResponse.fromJson, - ); - - state._updateUsers(response.users); - + state.updateUsers(response.users); return response; } @@ -1069,34 +694,14 @@ class StreamChatClient { List? sort, PaginationParams? paginationParams, Filter? messageFilters, - }) async { - assert(() { - if (query == null && messageFilters == null) { - throw ArgumentError('Provide at least `query` or `messageFilters`'); - } - if (query != null && messageFilters != null) { - throw ArgumentError( - "Can't provide both `query` and `messageFilters` at the same time", - ); - } - return true; - }(), 'Check incoming params.'); - - final payload = { - 'filter_conditions': filter, - 'message_filter_conditions': messageFilters, - 'query': query, - 'sort': sort, - if (paginationParams != null) ...paginationParams.toJson(), - }.nullProtected; - - final response = await get('/search', queryParameters: { - 'payload': json.encode(payload), - }); - - return decode( - response.data, SearchMessagesResponse.fromJson); - } + }) => + _chatApi.general.searchMessages( + filter, + query: query, + sort: sort, + pagination: paginationParams, + messageFilters: messageFilters, + ); /// Send a [file] to the [channelId] of type [channelType] Future sendFile( @@ -1106,7 +711,7 @@ class StreamChatClient { ProgressCallback? onSendProgress, CancelToken? cancelToken, }) => - attachmentFileUploader!.sendFile( + _chatApi.fileUploader.sendFile( file, channelId, channelType, @@ -1122,7 +727,7 @@ class StreamChatClient { ProgressCallback? onSendProgress, CancelToken? cancelToken, }) => - attachmentFileUploader!.sendImage( + _chatApi.fileUploader.sendImage( image, channelId, channelType, @@ -1137,7 +742,7 @@ class StreamChatClient { String channelType, { CancelToken? cancelToken, }) => - attachmentFileUploader!.deleteFile( + _chatApi.fileUploader.deleteFile( url, channelId, channelType, @@ -1151,44 +756,52 @@ class StreamChatClient { String channelType, { CancelToken? cancelToken, }) => - attachmentFileUploader!.deleteImage( + _chatApi.fileUploader.deleteImage( url, channelId, channelType, cancelToken: cancelToken, ); + /// Replaces the [channelId] of type [ChannelType] data with [data] + Future updateChannel( + String channelId, + String channelType, + Map data, { + Message? message, + }) => + _chatApi.channel.updateChannel( + channelId, + channelType, + data, + message: message, + ); + + /// Updates the [channelId] of type [ChannelType] data with [data] + Future updateChannelPartial( + String channelId, + String channelType, + Map data, + ) => + _chatApi.channel.updateChannelPartial( + channelId, + channelType, + data, + ); + /// Add a device for Push Notifications. - Future addDevice(String id, PushProvider pushProvider) async { - final response = await post('/devices', data: { - 'id': id, - 'push_provider': pushProvider.name, - }); - return decode(response.data, EmptyResponse.fromJson); - } + Future addDevice(String id, PushProvider pushProvider) => + _chatApi.device.addDevice(id, pushProvider); /// Gets a list of user devices. - Future getDevices() async { - final response = await get('/devices'); - return decode( - response.data, ListDevicesResponse.fromJson); - } + Future getDevices() => _chatApi.device.getDevices(); /// Remove a user's device. - Future removeDevice(String id) async { - final response = await delete('/devices', queryParameters: { - 'id': id, - }); - return decode(response.data, EmptyResponse.fromJson); - } + Future removeDevice(String id) => + _chatApi.device.removeDevice(id); /// Get a development token - String devToken(String userId) { - final payload = json.encode({'user_id': userId}); - final payloadBytes = utf8.encode(payload); - final payloadB64 = base64.encode(payloadBytes); - return 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.$payloadB64.devtoken'; - } + String devToken(String userId) => Token.development(userId).rawValue; /// Returns a channel client with the given type, id and custom data. Channel channel( @@ -1202,58 +815,268 @@ class StreamChatClient { return Channel(this, type, id, extraData: extraData); } + /// Creates a new channel + Future createChannel( + String channelType, { + String? channelId, + Map? channelData, + }) => + queryChannel( + channelType, + channelId: channelId, + state: false, + channelData: channelData, + ); + + /// watches the provided channel + Future watchChannel( + String channelType, { + String? channelId, + Map? channelData, + }) => + queryChannel( + channelType, + channelId: channelId, + watch: true, + channelData: channelData, + ); + + /// Query the API, get messages, members or other channel fields + Future queryChannel( + String channelType, { + bool state = true, + bool watch = false, + bool presence = false, + String? channelId, + Map? channelData, + PaginationParams? messagesPagination, + PaginationParams? membersPagination, + PaginationParams? watchersPagination, + }) => + _chatApi.channel.queryChannel( + channelType, + channelId: channelId, + channelData: channelData, + state: state, + watch: watch, + presence: presence, + messagesPagination: messagesPagination, + membersPagination: membersPagination, + watchersPagination: watchersPagination, + ); + + /// Query channel members + Future queryMembers( + String channelType, { + Filter? filter, + String? channelId, + List? members, + List? sort, + PaginationParams? pagination, + }) => + _chatApi.general.queryMembers( + channelType, + channelId: channelId, + filter: filter, + members: members, + sort: sort, + pagination: pagination, + ); + + /// Hides the channel from [queryChannels] for the user + /// until a message is added If [clearHistory] is set to true - all messages + /// will be removed for the user + Future hideChannel( + String channelId, + String channelType, { + bool clearHistory = false, + }) => + _chatApi.channel.hideChannel( + channelId, + channelType, + clearHistory: clearHistory, + ); + + /// Removes the hidden status for the channel + Future showChannel( + String channelId, + String channelType, + ) => + _chatApi.channel.showChannel( + channelId, + channelType, + ); + + /// Delete this channel. Messages are permanently removed. + Future deleteChannel( + String channelId, + String channelType, + ) => + _chatApi.channel.deleteChannel( + channelId, + channelType, + ); + + /// Removes all messages from the channel + Future truncateChannel( + String channelId, + String channelType, + ) => + _chatApi.channel.truncateChannel( + channelId, + channelType, + ); + + /// Mutes the channel + Future muteChannel( + String channelCid, { + Duration? expiration, + }) => + _chatApi.moderation.muteChannel( + channelCid, + expiration: expiration, + ); + + /// Unmutes the channel + Future unmuteChannel(String channelCid) => + _chatApi.moderation.unmuteChannel(channelCid); + + /// Accept invitation to the channel + Future acceptChannelInvite( + String channelId, + String channelType, { + Message? message, + }) => + _chatApi.channel.acceptChannelInvite( + channelId, + channelType, + message: message, + ); + + /// Reject invitation to the channel + Future rejectChannelInvite( + String channelId, + String channelType, { + Message? message, + }) => + _chatApi.channel.rejectChannelInvite( + channelId, + channelType, + message: message, + ); + + /// Add members to the channel + Future addChannelMembers( + String channelId, + String channelType, + List memberIds, { + Message? message, + }) => + _chatApi.channel.addMembers( + channelId, + channelType, + memberIds, + message: message, + ); + + /// Remove members from the channel + Future removeChannelMembers( + String channelId, + String channelType, + List memberIds, { + Message? message, + }) => + _chatApi.channel.removeMembers( + channelId, + channelType, + memberIds, + message: message, + ); + + /// Invite members to the channel + Future inviteChannelMembers( + String channelId, + String channelType, + List memberIds, { + Message? message, + }) => + _chatApi.channel.inviteChannelMembers( + channelId, + channelType, + memberIds, + message: message, + ); + + /// Stop watching the channel + Future stopChannelWatching( + String channelId, + String channelType, + ) => + _chatApi.channel.stopWatching( + channelId, + channelType, + ); + + /// Send action for a specific message of this channel + Future sendAction( + String channelId, + String channelType, + String messageId, + Map formData, + ) => + _chatApi.message.sendAction( + channelId, + channelType, + messageId, + formData, + ); + + /// Mark [channelId] of type [channelType] all messages as read + /// Optionally provide a [messageId] if you want to mark a + /// particular message as read + Future markChannelRead( + String channelId, + String channelType, { + String? messageId, + }) => + _chatApi.channel.markRead( + channelId, + channelType, + messageId: messageId, + ); + /// Update or Create the given user object. - Future updateUser(User user) async => - updateUsers([user]); + Future updateUser(User user) => updateUsers([user]); /// Batch update a list of users - Future updateUsers(List users) async { - final response = await post('/users', data: { - 'users': users.asMap().map((_, u) => MapEntry(u.id, u.toJson())), - }); - return decode( - response.data, - UpdateUsersResponse.fromJson, - ); - } + Future updateUsers(List users) => + _chatApi.user.updateUsers(users); /// Bans a user from all channels Future banUser( - String targetUserID, [ + String targetUserId, [ Map options = const {}, - ]) async { - final data = Map.from(options) - ..addAll({ - 'target_user_id': targetUserID, - }); - final response = await post( - '/moderation/ban', - data: data, - ); - return decode(response.data, EmptyResponse.fromJson); - } + ]) => + _chatApi.moderation.banUser( + targetUserId, + options: options, + ); /// Remove global ban for a user Future unbanUser( - String targetUserID, [ + String targetUserId, [ Map options = const {}, - ]) async { - final data = Map.from(options) - ..addAll({ - 'target_user_id': targetUserID, - }); - final response = await delete( - '/moderation/ban', - queryParameters: data, - ); - return decode(response.data, EmptyResponse.fromJson); - } + ]) => + _chatApi.moderation.unbanUser( + targetUserId, + options: options, + ); /// Shadow bans a user Future shadowBan( String targetID, [ Map options = const {}, - ]) async => + ]) => banUser(targetID, { 'shadow': true, ...options, @@ -1263,124 +1086,166 @@ class StreamChatClient { Future removeShadowBan( String targetID, [ Map options = const {}, - ]) async => + ]) => unbanUser(targetID, { 'shadow': true, ...options, }); /// Mutes a user - Future muteUser(String targetID) async { - final response = await post('/moderation/mute', data: { - 'target_id': targetID, - }); - return decode(response.data, EmptyResponse.fromJson); - } + Future muteUser(String userId) => + _chatApi.moderation.muteUser(userId); /// Unmutes a user - Future unmuteUser(String targetID) async { - final response = await post('/moderation/unmute', data: { - 'target_id': targetID, - }); - return decode(response.data, EmptyResponse.fromJson); - } + Future unmuteUser(String userId) => + _chatApi.moderation.unmuteUser(userId); /// Flag a message - Future flagMessage(String messageID) async { - final response = await post('/moderation/flag', data: { - 'target_message_id': messageID, - }); - return decode(response.data, EmptyResponse.fromJson); - } + Future flagMessage(String messageId) => + _chatApi.moderation.flagMessage(messageId); /// Unflag a message - Future unflagMessage(String messageId) async { - final response = await post('/moderation/unflag', data: { - 'target_message_id': messageId, - }); - return decode(response.data, EmptyResponse.fromJson); - } + Future unflagMessage(String messageId) => + _chatApi.moderation.unflagMessage(messageId); /// Flag a user - Future flagUser(String userId) async { - final response = await post('/moderation/flag', data: { - 'target_user_id': userId, - }); - return decode(response.data, EmptyResponse.fromJson); - } + Future flagUser(String userId) => + _chatApi.moderation.flagUser(userId); /// Unflag a message - Future unflagUser(String userId) async { - final response = await post('/moderation/unflag', data: { - 'target_user_id': userId, - }); - return decode(response.data, EmptyResponse.fromJson); - } + Future unflagUser(String userId) => + _chatApi.moderation.unflagUser(userId); /// Mark all channels for this user as read - Future markAllRead() async { - final response = await post('/channels/read'); - return decode(response.data, EmptyResponse.fromJson); - } + Future markAllRead() => _chatApi.channel.markAllRead(); + + /// Send an event to a particular channel + Future sendEvent( + String channelId, + String channelType, + Event event, + ) => + _chatApi.channel.sendEvent( + channelId, + channelType, + event, + ); + + /// Send a [reactionType] for this [messageId] + /// Set [enforceUnique] to true to remove the existing user reaction + Future sendReaction( + String messageId, + String reactionType, { + Map extraData = const {}, + bool enforceUnique = false, + }) => + _chatApi.message.sendReaction( + messageId, + reactionType, + extraData: extraData, + enforceUnique: enforceUnique, + ); + + /// Delete a [reactionType] from this [messageId] + Future deleteReaction( + String messageId, + String reactionType, + ) => + _chatApi.message.deleteReaction( + messageId, + reactionType, + ); /// Sends the message to the given channel Future sendMessage( Message message, String channelId, String channelType, - ) async { - final response = await post( - '/channels/$channelType/$channelId/message', - data: {'message': message.toJson()}, - ); - return decode(response.data, SendMessageResponse.fromJson); - } + ) => + _chatApi.message.sendMessage( + channelId, + channelType, + message, + ); + + /// Lists all the message replies for the [parentId] + Future getReplies( + String parentId, + PaginationParams options, + ) => + _chatApi.message.getReplies( + parentId, + options, + ); + + /// Get all the reactions for a [messageId] + Future getReactions( + String messageId, + PaginationParams options, + ) => + _chatApi.message.getReactions( + messageId, + options, + ); /// Update the given message - Future updateMessage(Message message) async { - final response = await post( - '/messages/${message.id}', - data: {'message': message.toJson()}, - ); - return decode(response.data, UpdateMessageResponse.fromJson); - } + Future updateMessage(Message message) => + _chatApi.message.updateMessage(message); /// Deletes the given message - Future deleteMessage(Message message) async { - final response = await delete('/messages/${message.id}'); - return decode(response.data, EmptyResponse.fromJson); - } + Future deleteMessage(String messageId) => + _chatApi.message.deleteMessage(messageId); - /// Get a message by id - Future getMessage(String messageId) async { - final response = await get('/messages/$messageId'); - return decode(response.data, GetMessageResponse.fromJson); - } + /// Get a message by [messageId] + Future getMessage(String messageId) => + _chatApi.message.getMessage(messageId); + + /// Retrieves a list of messages by [messageIDs] + /// from the given [channelId] of type [channelType] + Future getMessagesById( + String channelId, + String channelType, + List messageIDs, + ) => + _chatApi.message.getMessagesById( + channelId, + channelType, + messageIDs, + ); + + /// Translates the [messageId] in provided [language] + Future translateMessage( + String messageId, + String language, + ) => + _chatApi.message.translateMessage( + messageId, + language, + ); /// Pins provided message /// [timeoutOrExpirationDate] can either be a [DateTime] or a value in seconds /// to be added to [DateTime.now] Future pinMessage( - Message message, - Object timeoutOrExpirationDate, - ) { + Message message, { + Object? /*num|DateTime*/ timeoutOrExpirationDate, + }) { assert(() { if (timeoutOrExpirationDate is! DateTime && + timeoutOrExpirationDate != null && timeoutOrExpirationDate is! num) { throw ArgumentError('Invalid timeout or Expiration date'); } return true; - }(), 'Check whether time out is valid'); + }(), 'Check for invalid timeout or expiration date'); DateTime? pinExpires; if (timeoutOrExpirationDate is DateTime) { - pinExpires = timeoutOrExpirationDate.toUtc(); + pinExpires = timeoutOrExpirationDate; } else if (timeoutOrExpirationDate is num) { - pinExpires = DateTime.now() - .add( - Duration(seconds: timeoutOrExpirationDate.toInt()), - ) - .toUtc(); + pinExpires = DateTime.now().add( + Duration(seconds: timeoutOrExpirationDate.toInt()), + ); } return updateMessage( message.copyWith(pinned: true, pinExpires: pinExpires), @@ -1390,6 +1255,39 @@ class StreamChatClient { /// Unpins provided message Future unpinMessage(Message message) => updateMessage(message.copyWith(pinned: false)); + + /// Closes the [_ws] connection and resets the [state] + /// If [flushChatPersistence] is true the client deletes all offline + /// user's data. + Future disconnectUser({bool flushChatPersistence = false}) async { + // resetting state + state.dispose(); + state = ClientState(this); + + // resetting credentials + _tokenManager.reset(); + _connectionIdManager.reset(); + + // disconnecting persistence client + await _chatPersistenceClient?.disconnect(flush: flushChatPersistence); + _chatPersistenceClient = null; + + // closing web-socket connection + closeConnection(); + } + + /// Call this function to dispose the client + Future dispose() async { + state.dispose(); + await _eventController.close(); + await _wsConnectionStatusController.close(); + + // disconnecting persistence client + await _chatPersistenceClient?.disconnect(); + + // closing web-socket connection + closeConnection(); + } } /// The class that handles the state of the channel listening to the events @@ -1453,7 +1351,7 @@ class ClientState { if (event.user!.id == user!.id) { user = OwnUser.fromJson(event.user!.toJson()); } - _updateUser(event.user); + updateUser(event.user); })); } @@ -1478,15 +1376,16 @@ class ClientState { _userController.add(user); } - void _updateUsers(List userList) { + void updateUsers(List userList) { final newUsers = { ...users, - for (var user in userList) user!.id: user, + for (var user in userList) + if (user != null) user.id: user, }; _usersController.add(newUsers); } - void _updateUser(User? user) => _updateUsers([user]); + void updateUser(User? user) => updateUsers([user]); /// The current user OwnUser? get user => _userController.value; @@ -1495,10 +1394,10 @@ class ClientState { Stream get userStream => _userController.stream; /// The current user - Map get users => _usersController.value!; + Map get users => _usersController.value!; /// The current user as a stream - Stream> get usersStream => _usersController.stream; + Stream> get usersStream => _usersController.stream; /// The current unread channels count int? get unreadChannels => _unreadChannelsController.value; @@ -1513,23 +1412,21 @@ class ClientState { Stream get totalUnreadCountStream => _totalUnreadCountController.stream; /// The current list of channels in memory as a stream - Stream?> get channelsStream => - _channelsController.stream; + Stream> get channelsStream => _channelsController.stream; /// The current list of channels in memory Map get channels => _channelsController.value!; - set channels(Map v) { - _channelsController.add(v); + set channels(Map channelMap) { + final newChannels = {...channels, ...channelMap}; + _channelsController.add(newChannels); } - final BehaviorSubject> _channelsController = - BehaviorSubject.seeded({}); - final BehaviorSubject _userController = BehaviorSubject(); - final BehaviorSubject> _usersController = - BehaviorSubject.seeded({}); - final BehaviorSubject _unreadChannelsController = BehaviorSubject(); - final BehaviorSubject _totalUnreadCountController = BehaviorSubject(); + final _channelsController = BehaviorSubject>.seeded({}); + final _userController = BehaviorSubject(); + final _usersController = BehaviorSubject>.seeded({}); + final _unreadChannelsController = BehaviorSubject(); + final _totalUnreadCountController = BehaviorSubject(); /// Call this method to dispose this object void dispose() { diff --git a/packages/stream_chat/lib/src/attachment_file_uploader.dart b/packages/stream_chat/lib/src/core/api/attachment_file_uploader.dart similarity index 90% rename from packages/stream_chat/lib/src/attachment_file_uploader.dart rename to packages/stream_chat/lib/src/core/api/attachment_file_uploader.dart index d00dd4e7..272188af 100644 --- a/packages/stream_chat/lib/src/attachment_file_uploader.dart +++ b/packages/stream_chat/lib/src/core/api/attachment_file_uploader.dart @@ -1,8 +1,8 @@ import 'package:dio/dio.dart'; -import 'package:stream_chat/src/api/responses.dart'; -import 'package:stream_chat/src/client.dart'; +import 'package:stream_chat/src/core/api/responses.dart'; +import 'package:stream_chat/src/core/http/stream_http_client.dart'; import 'package:stream_chat/src/extensions/string_extension.dart'; -import 'package:stream_chat/src/models/attachment_file.dart'; +import 'package:stream_chat/src/core/models/attachment_file.dart'; /// Class responsible for uploading images and files to a given channel abstract class AttachmentFileUploader { @@ -60,7 +60,7 @@ class StreamAttachmentFileUploader implements AttachmentFileUploader { /// Creates a new [StreamAttachmentFileUploader] instance. const StreamAttachmentFileUploader(this._client); - final StreamChatClient _client; + final StreamHttpClient _client; @override Future sendImage( @@ -96,7 +96,7 @@ class StreamAttachmentFileUploader implements AttachmentFileUploader { onSendProgress: onSendProgress, cancelToken: cancelToken, ); - return _client.decode(response.data, SendImageResponse.fromJson); + return SendImageResponse.fromJson(response.data); } @override @@ -133,7 +133,7 @@ class StreamAttachmentFileUploader implements AttachmentFileUploader { onSendProgress: onSendProgress, cancelToken: cancelToken, ); - return _client.decode(response.data, SendFileResponse.fromJson); + return SendFileResponse.fromJson(response.data); } @override @@ -148,7 +148,7 @@ class StreamAttachmentFileUploader implements AttachmentFileUploader { queryParameters: {'url': url}, cancelToken: cancelToken, ); - return _client.decode(response.data, EmptyResponse.fromJson); + return EmptyResponse.fromJson(response.data); } @override @@ -163,6 +163,6 @@ class StreamAttachmentFileUploader implements AttachmentFileUploader { queryParameters: {'url': url}, cancelToken: cancelToken, ); - return _client.decode(response.data, EmptyResponse.fromJson); + return EmptyResponse.fromJson(response.data); } } diff --git a/packages/stream_chat/lib/src/core/api/channel_api.dart b/packages/stream_chat/lib/src/core/api/channel_api.dart new file mode 100644 index 00000000..7407bcb5 --- /dev/null +++ b/packages/stream_chat/lib/src/core/api/channel_api.dart @@ -0,0 +1,293 @@ +import 'dart:convert'; + +import 'package:stream_chat/src/core/api/requests.dart'; +import 'package:stream_chat/src/core/api/responses.dart'; +import 'package:stream_chat/src/core/http/stream_http_client.dart'; +import 'package:stream_chat/src/core/models/channel_model.dart'; +import 'package:stream_chat/src/core/models/channel_state.dart'; +import 'package:stream_chat/src/core/models/event.dart'; +import 'package:stream_chat/src/core/models/filter.dart'; +import 'package:stream_chat/src/core/models/message.dart'; + +/// +class ChannelApi { + /// + ChannelApi(this._client); + + final StreamHttpClient _client; + + String _getChannelUrl(String channelId, String channelType) => + '/channels/$channelType/$channelId'; + + /// Query the API, get messages, members or other channel fields + Future queryChannel( + String channelType, { + bool state = true, + bool watch = false, + bool presence = false, + String? channelId, + Map? channelData, + PaginationParams? messagesPagination, + PaginationParams? membersPagination, + PaginationParams? watchersPagination, + }) async { + var channelPath = '/channels/$channelType'; + if (channelId != null) channelPath = '$channelPath/$channelId'; + final response = await _client.post( + '$channelPath/query', + data: { + 'state': state, + 'watch': watch, + 'presence': presence, + if (channelData != null) 'data': channelData, + if (messagesPagination != null) 'messages': messagesPagination, + if (membersPagination != null) 'members': membersPagination, + if (watchersPagination != null) 'watchers': watchersPagination, + }, + ); + return ChannelState.fromJson(response.data); + } + + /// Requests channels with a given query from the API. + Future queryChannels({ + Filter? filter, + List>? sort, + int? memberLimit, + int? messageLimit, + bool state = true, + bool watch = true, + bool presence = false, + PaginationParams paginationParams = const PaginationParams(), + }) async { + print('Query Channel Started 2 : ${DateTime.now()}'); + final response = await _client.get( + '/channels', + queryParameters: { + 'payload': jsonEncode({ + // default options + 'state': state, + 'watch': watch, + 'presence': presence, + + // passed options + if (sort != null) 'sort': sort, + if (filter != null) 'filter_conditions': filter, + if (memberLimit != null) 'member_limit': memberLimit, + if (messageLimit != null) 'message_limit': messageLimit, + + // pagination + ...paginationParams.toJson() + }) + }, + ); + print('Query Channel Completed 2 : ${DateTime.now()}'); + return QueryChannelsResponse.fromJson(response.data); + } + + /// Mark all channels for this user as read + Future markAllRead() async { + final response = await _client.post('channels/read'); + return EmptyResponse.fromJson(response.data); + } + + /// Replaces the [channelId] of type [ChannelType] data with [data] + Future updateChannel( + String channelId, + String channelType, + Map data, { + Message? message, + }) async { + final response = await _client.post( + _getChannelUrl(channelId, channelType), + data: { + 'data': data, + if (message != null) + 'message': message.copyWith(updatedAt: DateTime.now()), + }, + ); + return UpdateChannelResponse.fromJson(response.data); + } + + /// Updates the [channelId] of type [ChannelType] data with [data] + Future updateChannelPartial( + String channelId, + String channelType, + Map data, + ) async { + final response = await _client.patch( + _getChannelUrl(channelId, channelType), + data: data, + ); + return PartialUpdateChannelResponse.fromJson(response.data); + } + + /// Accept invitation to the channel + Future acceptChannelInvite( + String channelId, + String channelType, { + Message? message, + }) async { + final response = await _client.post( + _getChannelUrl(channelId, channelType), + data: { + 'accept_invite': true, + 'message': message, + }, + ); + return AcceptInviteResponse.fromJson(response.data); + } + + /// Reject invitation to the channel + Future rejectChannelInvite( + String channelId, + String channelType, { + Message? message, + }) async { + final response = await _client.post( + _getChannelUrl(channelId, channelType), + data: { + 'reject_invite': true, + 'message': message, + }, + ); + return RejectInviteResponse.fromJson(response.data); + } + + /// Invite members to the channel + Future inviteChannelMembers( + String channelId, + String channelType, + List memberIds, { + Message? message, + }) async { + final response = await _client.post( + _getChannelUrl(channelId, channelType), + data: { + 'invites': memberIds, + 'message': message, + }, + ); + return InviteMembersResponse.fromJson(response.data); + } + + /// Add members to the channel + Future addMembers( + String channelId, + String channelType, + List memberIds, { + Message? message, + }) async { + final response = await _client.post( + _getChannelUrl(channelId, channelType), + data: { + 'add_members': memberIds, + 'message': message, + }, + ); + return AddMembersResponse.fromJson(response.data); + } + + /// Remove members from the channel + Future removeMembers( + String channelId, + String channelType, + List memberIds, { + Message? message, + }) async { + final response = await _client.post( + _getChannelUrl(channelId, channelType), + data: { + 'remove_members': memberIds, + 'message': message, + }, + ); + return RemoveMembersResponse.fromJson(response.data); + } + + /// Send an event on this channel + Future sendEvent( + String channelId, + String channelType, + Event event, + ) async { + final response = await _client.post( + '${_getChannelUrl(channelId, channelType)}/event', + data: {'event': event}, + ); + return EmptyResponse.fromJson(response.data); + } + + /// Delete this channel. Messages are permanently removed. + Future deleteChannel( + String channelId, + String channelType, + ) async { + final response = await _client.delete( + _getChannelUrl(channelId, channelType), + ); + return EmptyResponse.fromJson(response.data); + } + + /// Removes all messages from the channel + Future truncateChannel( + String channelId, + String channelType, + ) async { + final response = await _client.post( + '${_getChannelUrl(channelId, channelType)}/truncate', + ); + return EmptyResponse.fromJson(response.data); + } + + /// Hides the channel from [StreamChatClient.queryChannels] for the user + /// until a message is added If [clearHistory] is set to true - all messages + /// will be removed for the user + Future hideChannel( + String channelId, + String channelType, { + bool clearHistory = false, + }) async { + final response = await _client.post( + '${_getChannelUrl(channelId, channelType)}/hide', + data: {'clear_history': clearHistory}, + ); + return EmptyResponse.fromJson(response.data); + } + + /// Removes the hidden status for the channel + Future showChannel( + String channelId, + String channelType, + ) async { + final response = await _client.post( + '${_getChannelUrl(channelId, channelType)}/show', + ); + return EmptyResponse.fromJson(response.data); + } + + /// Mark [channelId] of type [channelType] all messages as read + /// Optionally provide a [messageId] if you want to mark a + /// particular message as read + Future markRead( + String channelId, + String channelType, { + String? messageId, + }) async { + final response = await _client.post( + '${_getChannelUrl(channelId, channelType)}/read', + data: {if (messageId != null) 'message_id': messageId}, + ); + return EmptyResponse.fromJson(response.data); + } + + /// Stop watching the channel + Future stopWatching( + String channelId, + String channelType, + ) async { + final response = await _client.post( + '${_getChannelUrl(channelId, channelType)}/stop-watching', + ); + return EmptyResponse.fromJson(response.data); + } +} diff --git a/packages/stream_chat/lib/src/core/api/device_api.dart b/packages/stream_chat/lib/src/core/api/device_api.dart new file mode 100644 index 00000000..2d5e0fea --- /dev/null +++ b/packages/stream_chat/lib/src/core/api/device_api.dart @@ -0,0 +1,59 @@ +import 'package:stream_chat/src/core/api/responses.dart'; +import 'package:stream_chat/src/core/http/stream_http_client.dart'; + +/// Provider used to send push notifications. +enum PushProvider { + /// Send notifications using Google's Firebase Cloud Messaging + firebase, + + /// Send notifications using Apple's Push Notification service + apn +} + +extension on PushProvider { + /// Returns the string notion for [PushProvider]. + String get name => { + PushProvider.apn: 'apn', + PushProvider.firebase: 'firebase', + }[this]!; +} + +/// +class DeviceApi { + /// + DeviceApi(this._client); + + final StreamHttpClient _client; + + /// Add a device for Push Notifications. + Future addDevice( + String deviceId, + PushProvider pushProvider, + ) async { + final response = await _client.post( + '/devices', + data: { + 'id': deviceId, + 'push_provider': pushProvider.name, + }, + ); + return EmptyResponse.fromJson(response.data); + } + + /// Gets a list of user devices. + Future getDevices() async { + final response = await _client.get('/devices'); + return ListDevicesResponse.fromJson(response.data); + } + + /// Remove a user's device. + Future removeDevice( + String deviceId, + ) async { + final response = await _client.delete( + '/devices', + queryParameters: {'id': deviceId}, + ); + return EmptyResponse.fromJson(response.data); + } +} diff --git a/packages/stream_chat/lib/src/core/api/general_api.dart b/packages/stream_chat/lib/src/core/api/general_api.dart new file mode 100644 index 00000000..d26ffc7d --- /dev/null +++ b/packages/stream_chat/lib/src/core/api/general_api.dart @@ -0,0 +1,94 @@ +import 'dart:convert'; + +import 'package:stream_chat/src/core/api/requests.dart'; +import 'package:stream_chat/src/core/api/responses.dart'; +import 'package:stream_chat/src/core/http/stream_http_client.dart'; +import 'package:stream_chat/src/core/models/filter.dart'; +import 'package:stream_chat/src/core/models/member.dart'; + +/// +class GeneralApi { + /// + GeneralApi(this._client); + + final StreamHttpClient _client; + + /// Get all the missed events + Future sync( + List cids, + DateTime lastSyncAt, + ) async { + final response = await _client.post( + '/sync', + data: { + 'channel_cids': cids, + 'last_sync_at': lastSyncAt.toUtc().toIso8601String(), + }, + ); + return SyncResponse.fromJson(response.data); + } + + /// A message search. + Future searchMessages( + Filter filter, { + String? query, + List? sort, + PaginationParams? pagination, + Filter? messageFilters, + }) async { + assert(() { + if (query == null && messageFilters == null) { + throw ArgumentError('Provide at least `query` or `messageFilters`'); + } + if (query != null && messageFilters != null) { + throw ArgumentError( + "Can't provide both `query` and `messageFilters` at the same time", + ); + } + return true; + }(), 'Check incoming params.'); + + final response = await _client.get( + '/search', + queryParameters: { + 'payload': jsonEncode({ + 'filter_conditions': filter, + if (sort != null) 'sort': sort, + if (query != null) 'query': query, + if (messageFilters != null) + 'message_filter_conditions': messageFilters, + if (pagination != null) ...pagination.toJson(), + }), + }, + ); + + return SearchMessagesResponse.fromJson(response.data); + } + + /// Query channel members + Future queryMembers( + String channelType, { + Filter? filter, + String? channelId, + List? members, + List? sort, + PaginationParams? pagination, + }) async { + final response = await _client.get( + '/members', + queryParameters: { + 'payload': jsonEncode({ + 'type': channelType, + if (channelId != null) + 'id': channelId + else if (members != null) + 'members': members, + if (sort != null) 'sort': sort, + if (filter != null) 'filter': filter, + if (pagination != null) ...pagination.toJson(), + }), + }, + ); + return QueryMembersResponse.fromJson(response.data); + } +} diff --git a/packages/stream_chat/lib/src/core/api/guest_api.dart b/packages/stream_chat/lib/src/core/api/guest_api.dart new file mode 100644 index 00000000..438a637f --- /dev/null +++ b/packages/stream_chat/lib/src/core/api/guest_api.dart @@ -0,0 +1,20 @@ +import 'package:stream_chat/src/core/api/responses.dart'; +import 'package:stream_chat/src/core/http/stream_http_client.dart'; +import 'package:stream_chat/src/core/models/user.dart'; + +/// +class GuestApi { + /// + GuestApi(this._client); + + final StreamHttpClient _client; + + /// + Future getGuestUser(User user) async { + final response = await _client.post( + '/guest', + data: {'user': user}, + ); + return ConnectGuestUserResponse.fromJson(response.data); + } +} diff --git a/packages/stream_chat/lib/src/core/api/message_api.dart b/packages/stream_chat/lib/src/core/api/message_api.dart new file mode 100644 index 00000000..eccd2f44 --- /dev/null +++ b/packages/stream_chat/lib/src/core/api/message_api.dart @@ -0,0 +1,160 @@ +import 'package:stream_chat/src/core/api/requests.dart'; +import 'package:stream_chat/src/core/api/responses.dart'; +import 'package:stream_chat/src/core/http/stream_http_client.dart'; +import 'package:stream_chat/src/core/models/message.dart'; + +/// +class MessageApi { + /// + MessageApi(this._client); + + final StreamHttpClient _client; + + /// Sends the [message] to the given [channelId] of given [channelType] + Future sendMessage( + String channelId, + String channelType, + Message message, + ) async { + final response = await _client.post( + '/channels/$channelType/$channelId/message', + data: {'message': message}, + ); + return SendMessageResponse.fromJson(response.data); + } + + /// Retrieves a list of messages by [messageIDs] + /// from the given [channelId] of type [channelType] + Future getMessagesById( + String channelId, + String channelType, + List messageIDs, + ) async { + final response = await _client.get( + '/channels/$channelType/$channelId/messages', + queryParameters: {'ids': messageIDs.join(',')}, + ); + return GetMessagesByIdResponse.fromJson(response.data); + } + + /// Get a message by [messageId] + Future getMessage(String messageId) async { + final response = await _client.get( + '/messages/$messageId', + ); + return GetMessageResponse.fromJson(response.data); + } + + /// Updates the given [message] + Future updateMessage( + Message message, + ) async { + final response = await _client.post( + '/messages/${message.id}', + data: {'message': message}, + ); + return UpdateMessageResponse.fromJson(response.data); + } + + /// Deletes the given [messageId] + Future deleteMessage( + String messageId, + ) async { + final response = await _client.delete( + '/messages/$messageId', + ); + return EmptyResponse.fromJson(response.data); + } + + /// Send action for a specific [messageId] + /// of the given [channelId] of given [channelType] + Future sendAction( + String channelId, + String channelType, + String messageId, + Map formData, + ) async { + final response = await _client.post( + '/messages/$messageId/action', + data: { + 'id': channelId, + 'type': channelType, + 'form_data': formData, + 'message_id': messageId, + }, + ); + return SendActionResponse.fromJson(response.data); + } + + /// Send a [reactionType] for this [messageId] + /// Set [enforceUnique] to true to remove the existing user reaction + Future sendReaction( + String messageId, + String reactionType, { + Map extraData = const {}, + bool enforceUnique = false, + }) async { + final reaction = Map.from(extraData) + ..addAll({'type': reactionType}); + + final response = await _client.post( + '/messages/$messageId/reaction', + data: { + 'reaction': reaction, + 'enforce_unique': enforceUnique, + }, + ); + return SendReactionResponse.fromJson(response.data); + } + + /// Delete a [reactionType] from this [messageId] + Future deleteReaction( + String messageId, + String reactionType, + ) async { + final response = await _client.delete( + '/messages/$messageId/reaction/$reactionType', + ); + return EmptyResponse.fromJson(response.data); + } + + /// Get all the reactions for a [messageId] + Future getReactions( + String messageId, + PaginationParams options, + ) async { + final response = await _client.get( + '/messages/$messageId/reactions', + queryParameters: { + ...options.toJson(), + }, + ); + return QueryReactionsResponse.fromJson(response.data); + } + + /// Translates the [messageId] in provided [language] + Future translateMessage( + String messageId, + String language, + ) async { + final response = await _client.post( + '/messages/$messageId/translate', + data: {'language': language}, + ); + return TranslateMessageResponse.fromJson(response.data); + } + + /// Lists all the message replies for the [parentId] + Future getReplies( + String parentId, + PaginationParams options, + ) async { + final response = await _client.get( + '/messages/$parentId/replies', + queryParameters: { + ...options.toJson(), + }, + ); + return QueryRepliesResponse.fromJson(response.data); + } +} diff --git a/packages/stream_chat/lib/src/core/api/moderation_api.dart b/packages/stream_chat/lib/src/core/api/moderation_api.dart new file mode 100644 index 00000000..22c58c35 --- /dev/null +++ b/packages/stream_chat/lib/src/core/api/moderation_api.dart @@ -0,0 +1,128 @@ +import 'package:stream_chat/src/core/api/responses.dart'; +import 'package:stream_chat/src/core/http/stream_http_client.dart'; + +/// +class ModerationApi { + /// + ModerationApi(this._client); + + final StreamHttpClient _client; + + /// Mutes a user + Future muteUser(String userId) async { + final response = await _client.post( + '/moderation/mute', + data: {'target_id': userId}, + ); + return EmptyResponse.fromJson(response.data); + } + + /// Unmutes a user + Future unmuteUser(String userId) async { + final response = await _client.post( + '/moderation/unmute', + data: {'target_id': userId}, + ); + return EmptyResponse.fromJson(response.data); + } + + /// Mutes the channel + Future muteChannel( + String channelCid, { + Duration? expiration, + }) async { + final response = await _client.post( + '/moderation/mute/channel', + data: { + 'channel_cid': channelCid, + if (expiration != null) 'expiration': expiration.inMilliseconds, + }, + ); + return EmptyResponse.fromJson(response.data); + } + + /// Unmutes the channel + Future unmuteChannel( + String channelCid, + ) async { + final response = await _client.post( + '/moderation/unmute/channel', + data: {'channel_cid': channelCid}, + ); + return EmptyResponse.fromJson(response.data); + } + + /// Flag a message + Future flagMessage( + String messageId, + ) async { + final response = await _client.post( + '/moderation/flag', + data: {'target_message_id': messageId}, + ); + return EmptyResponse.fromJson(response.data); + } + + /// Unflag a message + Future unflagMessage( + String messageId, + ) async { + final response = await _client.post( + '/moderation/unflag', + data: {'target_message_id': messageId}, + ); + return EmptyResponse.fromJson(response.data); + } + + /// Flag a user + Future flagUser( + String userId, + ) async { + final response = await _client.post( + '/moderation/flag', + data: {'target_user_id': userId}, + ); + return EmptyResponse.fromJson(response.data); + } + + /// Unflag a user + Future unflagUser( + String userId, + ) async { + final response = await _client.post( + '/moderation/unflag', + data: {'target_user_id': userId}, + ); + return EmptyResponse.fromJson(response.data); + } + + /// Bans a user from all channels + Future banUser( + String targetUserId, { + Map? options, + }) async { + final response = await _client.post( + '/moderation/ban', + data: { + 'target_user_id': targetUserId, + if (options != null) ...options, + }, + ); + return EmptyResponse.fromJson(response.data); + } + + /// Remove global ban for a user + Future unbanUser( + String targetUserId, { + Map? options, + }) async { + final response = await _client.delete( + '/moderation/ban', + queryParameters: { + 'target_user_id': targetUserId, + if (options != null) ...options, + }, + ); + return EmptyResponse.fromJson(response.data); + } +} diff --git a/packages/stream_chat/lib/src/api/requests.dart b/packages/stream_chat/lib/src/core/api/requests.dart similarity index 77% rename from packages/stream_chat/lib/src/api/requests.dart rename to packages/stream_chat/lib/src/core/api/requests.dart index e78a80ec..29112d72 100644 --- a/packages/stream_chat/lib/src/api/requests.dart +++ b/packages/stream_chat/lib/src/core/api/requests.dart @@ -1,9 +1,10 @@ +import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; part 'requests.g.dart'; /// Sorting options -@JsonSerializable(createFactory: false) +@JsonSerializable(includeIfNull: false) class SortOption { /// Creates a new SortOption instance /// @@ -18,6 +19,10 @@ class SortOption { this.comparator, }); + /// Create a new instance from a json + factory SortOption.fromJson(Map json) => + _$SortOptionFromJson(json); + /// Ascending order // ignore: constant_identifier_names static const ASC = 1; @@ -41,8 +46,8 @@ class SortOption { } /// Pagination options. -@JsonSerializable(createFactory: false, includeIfNull: false) -class PaginationParams { +@JsonSerializable(includeIfNull: false) +class PaginationParams extends Equatable { /// Creates a new PaginationParams instance /// /// For example: @@ -62,6 +67,10 @@ class PaginationParams { this.lessThanOrEqual, }); + /// Create a new instance from a json + factory PaginationParams.fromJson(Map json) => + _$PaginationParamsFromJson(json); + /// The amount of items requested from the APIs. final int limit; @@ -106,24 +115,12 @@ class PaginationParams { ); @override - @JsonKey(ignore: true) - int get hashCode => - runtimeType.hashCode ^ - limit.hashCode ^ - offset.hashCode ^ - greaterThan.hashCode ^ - greaterThanOrEqual.hashCode ^ - lessThan.hashCode ^ - lessThanOrEqual.hashCode; - - @override - bool operator ==(covariant PaginationParams other) => - identical(this, other) || - runtimeType == other.runtimeType && - limit == other.limit && - offset == other.offset && - greaterThan == other.greaterThan && - greaterThanOrEqual == other.greaterThanOrEqual && - lessThan == other.lessThan && - lessThanOrEqual == other.lessThanOrEqual; + List get props => [ + limit, + offset, + greaterThan, + greaterThanOrEqual, + lessThan, + lessThanOrEqual, + ]; } diff --git a/packages/stream_chat/lib/src/api/requests.g.dart b/packages/stream_chat/lib/src/core/api/requests.g.dart similarity index 63% rename from packages/stream_chat/lib/src/api/requests.g.dart rename to packages/stream_chat/lib/src/core/api/requests.g.dart index 1ec7b5fc..6cd935b8 100644 --- a/packages/stream_chat/lib/src/api/requests.g.dart +++ b/packages/stream_chat/lib/src/core/api/requests.g.dart @@ -6,12 +6,30 @@ part of 'requests.dart'; // JsonSerializableGenerator // ************************************************************************** +SortOption _$SortOptionFromJson(Map json) { + return SortOption( + json['field'] as String, + direction: json['direction'] as int, + ); +} + Map _$SortOptionToJson(SortOption instance) => { 'field': instance.field, 'direction': instance.direction, }; +PaginationParams _$PaginationParamsFromJson(Map json) { + return PaginationParams( + limit: json['limit'] as int, + offset: json['offset'] as int, + greaterThan: json['id_gt'] as String?, + greaterThanOrEqual: json['id_gte'] as String?, + lessThan: json['id_lt'] as String?, + lessThanOrEqual: json['id_lte'] as String?, + ); +} + Map _$PaginationParamsToJson(PaginationParams instance) { final val = { 'limit': instance.limit, diff --git a/packages/stream_chat/lib/src/api/responses.dart b/packages/stream_chat/lib/src/core/api/responses.dart similarity index 90% rename from packages/stream_chat/lib/src/api/responses.dart rename to packages/stream_chat/lib/src/core/api/responses.dart index 92493e8a..c639a960 100644 --- a/packages/stream_chat/lib/src/api/responses.dart +++ b/packages/stream_chat/lib/src/core/api/responses.dart @@ -1,14 +1,15 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/client.dart'; -import 'package:stream_chat/src/models/channel_model.dart'; -import 'package:stream_chat/src/models/channel_state.dart'; -import 'package:stream_chat/src/models/device.dart'; -import 'package:stream_chat/src/models/event.dart'; -import 'package:stream_chat/src/models/member.dart'; -import 'package:stream_chat/src/models/message.dart'; -import 'package:stream_chat/src/models/reaction.dart'; -import 'package:stream_chat/src/models/read.dart'; -import 'package:stream_chat/src/models/user.dart'; +import 'package:stream_chat/src/core/models/channel_model.dart'; +import 'package:stream_chat/src/core/models/channel_state.dart'; +import 'package:stream_chat/src/core/models/device.dart'; +import 'package:stream_chat/src/core/models/event.dart'; +import 'package:stream_chat/src/core/models/member.dart'; +import 'package:stream_chat/src/core/models/message.dart'; +import 'package:stream_chat/src/core/models/reaction.dart'; +import 'package:stream_chat/src/core/models/read.dart'; +import 'package:stream_chat/src/core/models/user.dart'; +import 'package:stream_chat/src/errors/stream_chat_error.dart'; part 'responses.g.dart'; @@ -16,7 +17,34 @@ class _BaseResponse { String? duration; } -/// Model response for [StreamChatClient.resync] api call +/// Model response for [StreamChatNetworkError] data +@JsonSerializable(createToJson: false) +class ErrorResponse extends _BaseResponse { + /// + int? code; + + /// + String? message; + + /// + @JsonKey(name: 'StatusCode') + int? statusCode; + + /// + String? moreInfo; + + /// Create a new instance from a json + static ErrorResponse fromJson(Map json) => + _$ErrorResponseFromJson(json); + + @override + String toString() => 'ErrorResponse(code: $code, ' + 'message: $message, ' + 'statusCode: $statusCode, ' + 'moreInfo: $moreInfo)'; +} + +/// Model response for [StreamChatClient.sync] api call @JsonSerializable(createToJson: false) class SyncResponse extends _BaseResponse { /// The list of events diff --git a/packages/stream_chat/lib/src/api/responses.g.dart b/packages/stream_chat/lib/src/core/api/responses.g.dart similarity index 97% rename from packages/stream_chat/lib/src/api/responses.g.dart rename to packages/stream_chat/lib/src/core/api/responses.g.dart index 2358593d..c1c1f302 100644 --- a/packages/stream_chat/lib/src/api/responses.g.dart +++ b/packages/stream_chat/lib/src/core/api/responses.g.dart @@ -6,6 +6,15 @@ part of 'responses.dart'; // JsonSerializableGenerator // ************************************************************************** +ErrorResponse _$ErrorResponseFromJson(Map json) { + return ErrorResponse() + ..duration = json['duration'] as String? + ..code = json['code'] as int? + ..message = json['message'] as String? + ..statusCode = json['StatusCode'] as int? + ..moreInfo = json['more_info'] as String?; +} + SyncResponse _$SyncResponseFromJson(Map json) { return SyncResponse() ..duration = json['duration'] as String? diff --git a/packages/stream_chat/lib/src/core/api/stream_chat_api.dart b/packages/stream_chat/lib/src/core/api/stream_chat_api.dart new file mode 100644 index 00000000..13342160 --- /dev/null +++ b/packages/stream_chat/lib/src/core/api/stream_chat_api.dart @@ -0,0 +1,79 @@ +import 'package:logging/logging.dart'; +import 'package:stream_chat/src/core/api/channel_api.dart'; +import 'package:stream_chat/src/core/api/device_api.dart'; +import 'package:stream_chat/src/core/api/general_api.dart'; +import 'package:stream_chat/src/core/api/guest_api.dart'; +import 'package:stream_chat/src/core/api/message_api.dart'; +import 'package:stream_chat/src/core/api/moderation_api.dart'; +import 'package:stream_chat/src/core/http/connection_id_manager.dart'; +import 'package:stream_chat/src/core/http/stream_http_client.dart'; +import 'package:stream_chat/src/core/api/user_api.dart'; +import 'package:stream_chat/src/core/http/token_manager.dart'; +import 'package:stream_chat/src/core/api/attachment_file_uploader.dart'; + +export 'device_api.dart' show PushProvider; + +/// +class StreamChatApi { + /// + StreamChatApi( + String apiKey, { + StreamHttpClient? client, + StreamHttpClientOptions? options, + TokenManager? tokenManager, + ConnectionIdManager? connectionIdManager, + AttachmentFileUploader? attachmentFileUploader, + Logger? logger, + }) : _fileUploader = attachmentFileUploader, + _client = client ?? + StreamHttpClient( + apiKey, + options: options, + tokenManager: tokenManager, + connectionIdManager: connectionIdManager, + logger: logger, + ); + + final StreamHttpClient _client; + + UserApi? _user; + + /// + UserApi get user => _user ??= UserApi(_client); + + GuestApi? _guest; + + /// + GuestApi get guest => _guest ??= GuestApi(_client); + + MessageApi? _message; + + /// + MessageApi get message => _message ??= MessageApi(_client); + + ChannelApi? _channel; + + /// + ChannelApi get channel => _channel ??= ChannelApi(_client); + + DeviceApi? _device; + + /// + DeviceApi get device => _device ??= DeviceApi(_client); + + ModerationApi? _moderation; + + /// + ModerationApi get moderation => _moderation ??= ModerationApi(_client); + + GeneralApi? _general; + + /// + GeneralApi get general => _general ??= GeneralApi(_client); + + AttachmentFileUploader? _fileUploader; + + /// + AttachmentFileUploader get fileUploader => + _fileUploader ??= StreamAttachmentFileUploader(_client); +} diff --git a/packages/stream_chat/lib/src/core/api/user_api.dart b/packages/stream_chat/lib/src/core/api/user_api.dart new file mode 100644 index 00000000..04c72284 --- /dev/null +++ b/packages/stream_chat/lib/src/core/api/user_api.dart @@ -0,0 +1,49 @@ +import 'dart:convert'; + +import 'package:stream_chat/src/core/api/requests.dart'; +import 'package:stream_chat/src/core/api/responses.dart'; +import 'package:stream_chat/src/core/http/stream_http_client.dart'; +import 'package:stream_chat/src/core/models/filter.dart'; +import 'package:stream_chat/src/core/models/user.dart'; + +/// +class UserApi { + /// + UserApi(this._client); + + final StreamHttpClient _client; + + /// Requests users with a given query. + Future queryUsers({ + bool presence = false, + Filter? filter, + List? sort, + PaginationParams? pagination, + }) async { + final response = await _client.get( + '/users', + queryParameters: { + 'payload': jsonEncode({ + 'presence': presence, + if (sort != null) 'sort': sort, + if (filter != null) 'filter_conditions': filter, + if (pagination != null) ...pagination.toJson(), + }), + }, + ); + return QueryUsersResponse.fromJson(response.data); + } + + /// Batch update a list of users + Future updateUsers( + List users, + ) async { + final response = await _client.post( + '/users', + data: { + 'users': {for (final user in users) user.id: user}, + }, + ); + return UpdateUsersResponse.fromJson(response.data); + } +} diff --git a/packages/stream_chat/lib/src/core/http/connection_id_manager.dart b/packages/stream_chat/lib/src/core/http/connection_id_manager.dart new file mode 100644 index 00000000..55fa1896 --- /dev/null +++ b/packages/stream_chat/lib/src/core/http/connection_id_manager.dart @@ -0,0 +1,27 @@ +// ignore_for_file: use_setters_to_change_properties + +/// +class ConnectionIdManager { + /// + ConnectionIdManager({ + String? connectionId, + }) : _connectionId = connectionId; + + String? _connectionId; + + /// + String? get connectionId => _connectionId; + + /// + bool get hasConnectionId => _connectionId != null; + + /// + void setConnectionId(String connectionId) { + _connectionId = connectionId; + } + + /// + void reset() { + _connectionId = null; + } +} diff --git a/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart b/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart new file mode 100644 index 00000000..19d0293b --- /dev/null +++ b/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart @@ -0,0 +1,91 @@ +import 'package:dio/dio.dart'; +import 'package:stream_chat/src/core/api/responses.dart'; +import 'package:stream_chat/src/core/http/stream_chat_dio_error.dart'; +import 'package:stream_chat/src/core/http/token.dart'; + +import 'package:stream_chat/src/core/http/token_manager.dart'; +import 'package:stream_chat/src/errors/chat_error_code.dart'; +import 'package:stream_chat/src/errors/stream_chat_error.dart'; + +/// +class AuthInterceptor extends Interceptor { + /// + AuthInterceptor(this._httpClient, this._tokenManager); + + final Dio _httpClient; + + /// + final TokenManager _tokenManager; + + @override + Future onRequest( + RequestOptions options, + RequestInterceptorHandler handler, + ) async { + late Token token; + try { + token = await _tokenManager.loadToken(); + } catch (_) { + final error = StreamChatError(ChatErrorCode.undefinedToken); + final dioError = StreamChatDioError( + error: error, + requestOptions: options, + ); + return handler.reject(dioError); + } + final params = {'user_id': token.userId}; + final headers = { + 'Authorization': token.rawValue, + 'stream-auth-type': token.authType.raw, + }; + options..queryParameters.addAll(params)..headers.addAll(headers); + return handler.next(options); + } + + @override + void onError( + DioError err, + ErrorInterceptorHandler handler, + ) async { + ErrorResponse? error; + final data = err.response?.data; + if (data != null) error = ErrorResponse.fromJson(data); + if (error?.code == ChatErrorCode.tokenExpired.code) { + if (_tokenManager.isStatic) return handler.next(err); + _httpClient.lock(); + await _tokenManager.loadToken(refresh: true); + _httpClient.unlock(); + try { + final options = err.requestOptions; + final response = await _httpClient.request( + options.path, + cancelToken: options.cancelToken, + data: options.data, + onReceiveProgress: options.onReceiveProgress, + onSendProgress: options.onSendProgress, + queryParameters: options.queryParameters, + options: Options( + method: options.method, + sendTimeout: options.sendTimeout, + receiveTimeout: options.receiveTimeout, + extra: options.extra, + headers: options.headers, + responseType: options.responseType, + contentType: options.contentType, + validateStatus: options.validateStatus, + receiveDataWhenStatusError: options.receiveDataWhenStatusError, + followRedirects: options.followRedirects, + maxRedirects: options.maxRedirects, + requestEncoder: options.requestEncoder, + responseDecoder: options.responseDecoder, + listFormat: options.listFormat, + ), + ); + return handler.resolve(response); + } on DioError catch (error) { + return handler.reject(error); + } + } + return handler.next(err); + } +} diff --git a/packages/stream_chat/lib/src/core/http/interceptor/connection_id_interceptor.dart b/packages/stream_chat/lib/src/core/http/interceptor/connection_id_interceptor.dart new file mode 100644 index 00000000..78a3147f --- /dev/null +++ b/packages/stream_chat/lib/src/core/http/interceptor/connection_id_interceptor.dart @@ -0,0 +1,25 @@ +import 'package:dio/dio.dart'; + +import 'package:stream_chat/src/core/http/connection_id_manager.dart'; + +/// +class ConnectionIdInterceptor extends Interceptor { + /// + ConnectionIdInterceptor(this.connectionIdManager); + + /// + final ConnectionIdManager connectionIdManager; + + @override + void onRequest( + RequestOptions options, + RequestInterceptorHandler handler, + ) async { + if (connectionIdManager.hasConnectionId) { + options.queryParameters.addAll({ + 'connection_id': connectionIdManager.connectionId, + }); + } + handler.next(options); + } +} diff --git a/packages/stream_chat/lib/src/core/http/interceptor/logging_interceptor.dart b/packages/stream_chat/lib/src/core/http/interceptor/logging_interceptor.dart new file mode 100644 index 00000000..a9075e34 --- /dev/null +++ b/packages/stream_chat/lib/src/core/http/interceptor/logging_interceptor.dart @@ -0,0 +1,338 @@ +// ignore_for_file: lines_longer_than_80_chars + +import 'dart:math' as math; + +import 'package:dio/dio.dart'; + +/// +enum InterceptStep { + /// + request, + + /// + response, + + /// + error, +} + +/// +typedef LogPrint = void Function(InterceptStep step, Object object); + +void _defaultLogPrint(InterceptStep step, Object object) => print(object); + +/// +class LoggingInterceptor extends Interceptor { + /// + LoggingInterceptor({ + this.request = true, + this.requestHeader = false, + this.requestBody = true, + this.responseHeader = false, + this.responseBody = true, + this.error = true, + this.maxWidth = 120, + this.compact = true, + this.logPrint = _defaultLogPrint, + }); + + /// Print request [Options] + final bool request; + + /// Print request header [Options.headers] + final bool requestHeader; + + /// Print request data [Options.data] + final bool requestBody; + + /// Print [Response.data] + final bool responseBody; + + /// Print [Response.headers] + final bool responseHeader; + + /// Print error message + final bool error; + + /// InitialTab count to logPrint json response + static const int initialTab = 1; + + /// 1 tab length + static const String tabStep = ' '; + + /// Print compact json response + final bool compact; + + /// Width size per logPrint + final int maxWidth; + + /// Log printer; defaults logPrint log to console. + /// In flutter, you'd better use debugPrint. + /// you can also write log in a file. + void Function(InterceptStep step, Object object) logPrint; + + @override + void onRequest(RequestOptions options, RequestInterceptorHandler handler) { + if (request) { + _printRequestHeader(_logPrintRequest, options); + } + if (requestHeader) { + _printMapAsTable( + _logPrintRequest, + options.queryParameters, + header: 'Query Parameters', + ); + final requestHeaders = {...options.headers}; + requestHeaders['contentType'] = options.contentType?.toString(); + requestHeaders['responseType'] = options.responseType.toString(); + requestHeaders['followRedirects'] = options.followRedirects; + requestHeaders['connectTimeout'] = options.connectTimeout; + requestHeaders['receiveTimeout'] = options.receiveTimeout; + _printMapAsTable(_logPrintRequest, requestHeaders, header: 'Headers'); + _printMapAsTable(_logPrintRequest, options.extra, header: 'Extras'); + } + if (requestBody && options.method != 'GET') { + final dynamic data = options.data; + if (data != null) { + if (data is Map) { + _printMapAsTable( + _logPrintRequest, + options.data as Map?, + header: 'Body', + ); + } else if (data is FormData) { + final formDataMap = {} + ..addEntries(data.fields) + ..addEntries(data.files); + _printMapAsTable(_logPrintRequest, formDataMap, + header: 'Form data | ${data.boundary}'); + } else { + _printBlock(_logPrintRequest, data.toString()); + } + } + } + super.onRequest(options, handler); + } + + @override + void onError(DioError err, ErrorInterceptorHandler handler) { + if (error) { + if (err.type == DioErrorType.response) { + final uri = err.response?.requestOptions.uri; + _printBoxed( + _logPrintError, + header: + 'DioError ║ Status: ${err.response?.statusCode} ${err.response?.statusMessage}', + text: uri.toString(), + ); + if (err.response != null && err.response?.data != null) { + _logPrintError('╔ ${err.type.toString()}'); + _printResponse(_logPrintError, err.response!); + } + _printLine(_logPrintError, '╚'); + _logPrintError(''); + } else { + _printBoxed( + _logPrintError, + header: 'DioError ║ ${err.type}', + text: err.message, + ); + _printRequestHeader(_logPrintError, err.requestOptions); + } + } + super.onError(err, handler); + } + + @override + void onResponse(Response response, ResponseInterceptorHandler handler) { + _printResponseHeader(_logPrintResponse, response); + if (responseHeader) { + final responseHeaders = {}; + response.headers + .forEach((k, list) => responseHeaders[k] = list.toString()); + _printMapAsTable(_logPrintResponse, responseHeaders, header: 'Headers'); + } + + if (responseBody) { + _logPrintResponse('╔ Body'); + _logPrintResponse('║'); + _printResponse(_logPrintResponse, response); + _logPrintResponse('║'); + _printLine(_logPrintResponse, '╚'); + } + super.onResponse(response, handler); + } + + void _printBoxed( + void Function(Object) logPrint, { + String? header, + String? text, + }) { + logPrint(''); + logPrint('â•”â•Ŗ $header'); + logPrint('║ $text'); + _printLine(logPrint, '╚'); + } + + void _printResponse(void Function(Object) logPrint, Response response) { + if (response.data != null) { + if (response.data is Map) { + _printPrettyMap(logPrint, response.data as Map); + } else if (response.data is List) { + logPrint('║${_indent()}['); + _printList(logPrint, response.data as List); + logPrint('║${_indent()}['); + } else { + _printBlock(logPrint, response.data.toString()); + } + } + } + + void _printResponseHeader(void Function(Object) logPrint, Response response) { + final uri = response.requestOptions.uri; + final method = response.requestOptions.method; + _printBoxed( + logPrint, + header: + 'Response ║ $method ║ Status: ${response.statusCode} ${response.statusMessage}', + text: uri.toString(), + ); + } + + void _printRequestHeader( + void Function(Object) logPrint, RequestOptions options) { + final uri = options.uri; + final method = options.method; + _printBoxed(logPrint, header: 'Request ║ $method ', text: uri.toString()); + } + + void _printLine(void Function(Object) logPrint, + [String pre = '', String suf = '╝']) => + logPrint('$pre${'═' * maxWidth}$suf'); + + void _printKV(void Function(Object) logPrint, String? key, Object? v) { + final pre = '╟ $key: '; + final msg = v.toString(); + + if (pre.length + msg.length > maxWidth) { + logPrint(pre); + _printBlock(logPrint, msg); + } else { + logPrint('$pre$msg'); + } + } + + void _printBlock(void Function(Object) logPrint, String msg) { + final lines = (msg.length / maxWidth).ceil(); + for (var i = 0; i < lines; ++i) { + logPrint((i >= 0 ? '║ ' : '') + + msg.substring(i * maxWidth, + math.min(i * maxWidth + maxWidth, msg.length))); + } + } + + String _indent([int tabCount = initialTab]) => tabStep * tabCount; + + void _printPrettyMap( + void Function(Object) logPrint, + Map data, { + int tabs = initialTab, + bool isListItem = false, + bool isLast = false, + }) { + var _tabs = tabs; + final isRoot = _tabs == initialTab; + final initialIndent = _indent(_tabs); + _tabs++; + + if (isRoot || isListItem) logPrint('║$initialIndent{'); + + data.keys.toList().asMap().forEach((index, dynamic key) { + final isLast = index == data.length - 1; + dynamic value = data[key]; + if (value is String) { + value = '"${value.toString().replaceAll(RegExp(r'(\r|\n)+'), " ")}"'; + } + if (value is Map) { + if (compact) { + logPrint('║${_indent(_tabs)} $key: $value${!isLast ? ',' : ''}'); + } else { + logPrint('║${_indent(_tabs)} $key: {'); + _printPrettyMap(logPrint, value, tabs: _tabs); + } + } else if (value is List) { + if (compact) { + logPrint('║${_indent(_tabs)} $key: ${value.toString()}'); + } else { + logPrint('║${_indent(_tabs)} $key: ['); + _printList(logPrint, value, tabs: _tabs); + logPrint('║${_indent(_tabs)} ]${isLast ? '' : ','}'); + } + } else { + final msg = value.toString().replaceAll('\n', ''); + final indent = _indent(_tabs); + final linWidth = maxWidth - indent.length; + if (msg.length + indent.length > linWidth) { + final lines = (msg.length / linWidth).ceil(); + for (var i = 0; i < lines; ++i) { + logPrint('║${_indent(_tabs)} ${msg.substring( + i * linWidth, + math.min(i * linWidth + linWidth, msg.length), + )}'); + } + } else { + logPrint('║${_indent(_tabs)} $key: $msg${!isLast ? ',' : ''}'); + } + } + }); + + logPrint('║$initialIndent}${isListItem && !isLast ? ',' : ''}'); + } + + void _printList( + void Function(Object) logPrint, + List list, { + int tabs = initialTab, + }) { + list.asMap().forEach((i, dynamic e) { + final isLast = i == list.length - 1; + if (e is Map) { + if (compact) { + logPrint('║${_indent(tabs)} $e${!isLast ? ',' : ''}'); + } else { + _printPrettyMap(logPrint, e, + tabs: tabs + 1, isListItem: true, isLast: isLast); + } + } else { + logPrint('║${_indent(tabs + 2)} $e${isLast ? '' : ','}'); + } + }); + } + + bool _canFlattenMap(Map map) => + map.values.where((dynamic val) => val is Map || val is List).isEmpty && + map.toString().length < maxWidth; + + bool _canFlattenList(List list) => + list.length < 10 && list.toString().length < maxWidth; + + void _printMapAsTable( + void Function(Object) logPrint, + Map? map, { + String? header, + }) { + if (map == null || map.isEmpty) return; + logPrint('╔ $header '); + map.forEach((dynamic key, dynamic value) => + _printKV(logPrint, key.toString(), value)); + _printLine(logPrint, '╚'); + } + + void _logPrintRequest(Object object) => + logPrint(InterceptStep.request, object); + + void _logPrintResponse(Object object) => + logPrint(InterceptStep.response, object); + + void _logPrintError(Object object) => logPrint(InterceptStep.error, object); +} diff --git a/packages/stream_chat/lib/src/core/http/stream_chat_dio_error.dart b/packages/stream_chat/lib/src/core/http/stream_chat_dio_error.dart new file mode 100644 index 00000000..dd2c2535 --- /dev/null +++ b/packages/stream_chat/lib/src/core/http/stream_chat_dio_error.dart @@ -0,0 +1,21 @@ +import 'package:dio/dio.dart'; +import 'package:stream_chat/src/errors/stream_chat_error.dart'; + +/// +class StreamChatDioError extends DioError { + /// + StreamChatDioError({ + required this.error, + required RequestOptions requestOptions, + Response? response, + DioErrorType type = DioErrorType.other, + }) : super( + error: error, + requestOptions: requestOptions, + response: response, + type: type, + ); + + @override + final StreamChatError error; +} diff --git a/packages/stream_chat/lib/src/core/http/stream_http_client.dart b/packages/stream_chat/lib/src/core/http/stream_http_client.dart new file mode 100644 index 00000000..ae0a5f72 --- /dev/null +++ b/packages/stream_chat/lib/src/core/http/stream_http_client.dart @@ -0,0 +1,241 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:dio/dio.dart'; +import 'package:logging/logging.dart'; +import 'package:meta/meta.dart'; +import 'package:stream_chat/src/core/http/interceptor/auth_interceptor.dart'; +import 'package:stream_chat/src/core/http/interceptor/connection_id_interceptor.dart'; +import 'package:stream_chat/src/core/http/interceptor/logging_interceptor.dart'; +import 'package:stream_chat/src/core/http/stream_chat_dio_error.dart'; +import 'package:stream_chat/src/core/http/token_manager.dart'; +import 'package:stream_chat/src/errors/stream_chat_error.dart'; +import 'package:stream_chat/src/platform_detector/platform_detector.dart'; +import 'package:stream_chat/version.dart'; + +import 'package:stream_chat/src/core/http/connection_id_manager.dart'; + +part 'stream_http_client_options.dart'; + +/// This is where we configure the base url, headers, +/// query parameters and convenient methods for http verbs with error parsing. +class StreamHttpClient { + /// [StreamHttpClient] constructor + StreamHttpClient( + this.apiKey, { + Dio? dio, + StreamHttpClientOptions? options, + TokenManager? tokenManager, + ConnectionIdManager? connectionIdManager, + Logger? logger, + }) : _options = options ?? const StreamHttpClientOptions(), + httpClient = dio ?? Dio() { + httpClient + ..options.baseUrl = _options.baseUrl + ..options.receiveTimeout = _options.receiveTimeout.inMilliseconds + ..options.connectTimeout = _options.connectTimeout.inMilliseconds + ..options.queryParameters = {'api_key': apiKey} + ..options.headers = { + 'Content-Type': 'application/json', + 'X-Stream-Client': _options.userAgent, + 'Content-Encoding': 'application/gzip', + } + ..interceptors.addAll([ + if (tokenManager != null) AuthInterceptor(httpClient, tokenManager), + if (connectionIdManager != null) + ConnectionIdInterceptor(connectionIdManager), + if (logger != null && logger.level != Level.OFF) + LoggingInterceptor( + requestHeader: true, + logPrint: (step, message) { + switch (step) { + case InterceptStep.request: + return logger.info(message); + case InterceptStep.response: + return logger.info(message); + case InterceptStep.error: + return logger.severe(message); + } + }, + ), + ]); + } + + /// Your project Stream Chat api key. + /// Find your API keys here https://getstream.io/dashboard/ + final String apiKey; + + /// Your project Stream Chat ClientOptions + final StreamHttpClientOptions _options; + + /// [Dio] httpClient + /// It's been chosen because it's easy to use + /// and supports interesting features out of the box + /// (Interceptors, Global configuration, FormData, File downloading etc.) + @visibleForTesting + final Dio httpClient; + + /// Shuts down the [httpClient]. + /// + /// If [force] is `false` (the default) the [httpClient] will be kept alive + /// until all active connections are done. If [force] is `true` any active + /// connections will be closed to immediately release all resources. These + /// closed connections will receive an error event to indicate that the client + /// was shut down. In both cases trying to establish a new connection after + /// calling [close] will throw an exception. + void close({bool force = false}) => httpClient.close(force: force); + + StreamChatNetworkError _parseError(DioError err) { + // locally thrown dio error + if (err is StreamChatDioError) { + final code = err.error.code; + final message = err.error.message; + return StreamChatNetworkError.raw(code: code, message: message); + } + // real network request dio error + return StreamChatNetworkError.fromDioError(err); + } + + /// Handy method to make http GET request with error parsing. + Future> get( + String path, { + Map? queryParameters, + Map? headers, + ProgressCallback? onReceiveProgress, + CancelToken? cancelToken, + }) async { + try { + final response = await httpClient.get( + path, + queryParameters: queryParameters, + options: Options(headers: headers), + onReceiveProgress: onReceiveProgress, + cancelToken: cancelToken, + ); + return response; + } on DioError catch (error) { + throw _parseError(error); + } + } + + /// Handy method to make http POST request with error parsing. + Future> post( + String path, { + Object? data, + Map? queryParameters, + Map? headers, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + CancelToken? cancelToken, + }) async { + try { + final response = await httpClient.post( + path, + queryParameters: queryParameters, + data: data, + options: Options(headers: headers), + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + cancelToken: cancelToken, + ); + return response; + } on DioError catch (error) { + throw _parseError(error); + } + } + + /// Handy method to make http DELETE request with error parsing. + Future> delete( + String path, { + Map? queryParameters, + Map? headers, + CancelToken? cancelToken, + }) async { + try { + final response = await httpClient.delete( + path, + queryParameters: queryParameters, + options: Options(headers: headers), + cancelToken: cancelToken, + ); + return response; + } on DioError catch (error) { + throw _parseError(error); + } + } + + /// Handy method to make http PATCH request with error parsing. + Future> patch( + String path, { + Object? data, + Map? queryParameters, + Map? headers, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + CancelToken? cancelToken, + }) async { + try { + final response = await httpClient.patch( + path, + queryParameters: queryParameters, + data: data, + options: Options(headers: headers), + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + cancelToken: cancelToken, + ); + return response; + } on DioError catch (error) { + throw _parseError(error); + } + } + + /// Handy method to make http PUT request with error parsing. + Future> put( + String path, { + Object? data, + Map? queryParameters, + Map? headers, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + CancelToken? cancelToken, + }) async { + try { + final response = await httpClient.put( + path, + queryParameters: queryParameters, + data: data, + options: Options(headers: headers), + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + cancelToken: cancelToken, + ); + return response; + } on DioError catch (error) { + throw _parseError(error); + } + } + + /// Handy method to post files with error parsing. + Future> postFile( + String path, + MultipartFile file, { + Map? queryParameters, + Map? headers, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + CancelToken? cancelToken, + }) async { + final formData = FormData.fromMap({'file': file}); + final response = await post( + path, + data: formData, + queryParameters: queryParameters, + headers: headers, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + cancelToken: cancelToken, + ); + return response; + } +} diff --git a/packages/stream_chat/lib/src/core/http/stream_http_client_options.dart b/packages/stream_chat/lib/src/core/http/stream_http_client_options.dart new file mode 100644 index 00000000..0f06e4d0 --- /dev/null +++ b/packages/stream_chat/lib/src/core/http/stream_http_client_options.dart @@ -0,0 +1,69 @@ +part of 'stream_http_client.dart'; + +/// +enum Location { + /// + usEast, + + /// + euWest, + + /// + mumbai, + + /// + sydney, + + /// + singapore, +} + +/// +extension LocationX on Location { + /// + String get name => { + Location.usEast: 'us-east', + Location.euWest: 'dublin', + Location.mumbai: 'mumbai', + Location.sydney: 'sydney', + Location.singapore: 'singapore', + }[this]!; +} + +const _defaultBaseURL = 'https://chat-us-east-1.stream-io-api.com'; + +/// Client options to modify [StreamHttpClient] +class StreamHttpClientOptions { + /// Instantiates a new [StreamHttpClientOptions] + const StreamHttpClientOptions({ + String? baseUrl, + this.location, + this.connectTimeout = const Duration(seconds: 6), + this.receiveTimeout = const Duration(seconds: 6), + }) : _baseUrl = baseUrl ?? _defaultBaseURL; + + final String _baseUrl; + + /// base url to use with client. + String get baseUrl { + if (location == null) return _baseUrl; + const serviceName = 'chat'; + final locationName = location!.name; + const baseDomainName = 'stream-io-api.com'; + return 'https://$serviceName-proxy-$locationName.$baseDomainName'; + } + + /// data center to use with client + final Location? location; + + /// connect timeout, default to 6s + final Duration connectTimeout; + + /// received timeout, default to 6s + final Duration receiveTimeout; + + /// Get the current user agent + String get userAgent => 'stream-chat-dart-client-' + '${CurrentPlatform.name}-' + '${PACKAGE_VERSION.split('+')[0]}'; +} diff --git a/packages/stream_chat/lib/src/core/http/token.dart b/packages/stream_chat/lib/src/core/http/token.dart new file mode 100644 index 00000000..a914521c --- /dev/null +++ b/packages/stream_chat/lib/src/core/http/token.dart @@ -0,0 +1,85 @@ +import 'dart:convert'; + +import 'package:equatable/equatable.dart'; +import 'package:jose/jose.dart'; +import 'package:stream_chat/src/core/models/user.dart'; +import 'package:stream_chat/src/core/utils.dart'; + +/// +typedef GuestTokenProvider = Future Function(User user); + +/// +enum AuthType { + /// + jwt, + + /// + anonymous, +} + +/// +extension AuthTypeX on AuthType { + /// + String get raw => { + AuthType.jwt: 'jwt', + AuthType.anonymous: 'anonymous', + }[this]!; +} + +/// Token designed to store the JWT and the user it is related to. +class Token extends Equatable { + const Token._({ + required this.rawValue, + required this.userId, + required this.authType, + }); + + /// The token that can be used when user is unknown. + /// Is used by `anonymous` token provider. + factory Token.anonymous({String? userId}) => Token._( + rawValue: '', + userId: userId ?? randomId(), + authType: AuthType.anonymous, + ); + + /// Creates a [Token] instance from the provided [rawValue] if it's valid. + factory Token.fromRawValue(String rawValue) { + final jwtBody = JsonWebToken.unverified(rawValue); + final userId = jwtBody.claims.getTyped('user_id'); + assert( + userId != null, + 'Invalid `token`, It should contain `user_id`', + ); + return Token._(rawValue: rawValue, userId: userId!, authType: AuthType.jwt); + } + + /// The token which can be used during the development. + /// Is used by `development(userId:)` token provider. + factory Token.development(String userId) { + const devSignature = 'devtoken'; + const header = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'; + final payload = json.encode({'user_id': userId}); + final payloadBytes = utf8.encode(payload); + final payloadB64 = base64.encode(payloadBytes); + final jwt = '$header.$payloadB64.$devSignature'; + return Token._(rawValue: jwt, userId: userId, authType: AuthType.jwt); + } + + /// The token which designed to be used for guest users. + static Future guest(User user, GuestTokenProvider provider) async { + final rawToken = await provider(user); + return Token.fromRawValue(rawToken); + } + + /// + final AuthType authType; + + /// + final String rawValue; + + /// + final String userId; + + @override + List get props => [authType, rawValue, userId]; +} diff --git a/packages/stream_chat/lib/src/core/http/token_manager.dart b/packages/stream_chat/lib/src/core/http/token_manager.dart new file mode 100644 index 00000000..88e9746d --- /dev/null +++ b/packages/stream_chat/lib/src/core/http/token_manager.dart @@ -0,0 +1,80 @@ +import 'package:stream_chat/src/core/http/token.dart'; + +/// A function which can be used to request a Stream Chat API token from your +/// own backend server. Function requires a single [userId]. +typedef TokenProvider = Future Function(String userId); + +/// +class TokenManager { + /// + TokenManager({ + String? userId, + Token? token, + TokenProvider? tokenProvider, + }) : _userId = userId, + _token = token, + _provider = tokenProvider; + + String? _type; + Token? _token; + + TokenProvider? _provider; + + String? _userId; + + /// User id to which this TokenManager is configured to + String? get userId => _userId; + + /// + bool get isStatic => _type == 'static'; + + /// + Future setTokenOrProvider( + String userId, { + Token? token, + TokenProvider? provider, + }) async { + assert(() { + if (token == null && provider == null) { + throw AssertionError('Provide at-least token or provider'); + } + if (token != null && provider != null) { + throw AssertionError("Can't set both token and provider"); + } + return true; + }(), ''); + + _userId = userId; + + if (token != null) { + _type = 'static'; + _token = token; + } + if (provider != null) { + _type = 'provider'; + _provider = provider; + } + + return loadToken(); + } + + /// + Future loadToken({bool refresh = false}) async { + assert( + _userId != null && _type != null, + 'Please call `setTokenOrProvider` before calling `loadToken`', + ); + if (refresh || _token == null) { + final rawValue = await _provider!(_userId!); + _token = Token.fromRawValue(rawValue); + } + return _token!; + } + + /// + void reset() { + _userId = null; + _token = null; + _provider = null; + } +} diff --git a/packages/stream_chat/lib/src/models/action.dart b/packages/stream_chat/lib/src/core/models/action.dart similarity index 100% rename from packages/stream_chat/lib/src/models/action.dart rename to packages/stream_chat/lib/src/core/models/action.dart diff --git a/packages/stream_chat/lib/src/models/action.g.dart b/packages/stream_chat/lib/src/core/models/action.g.dart similarity index 100% rename from packages/stream_chat/lib/src/models/action.g.dart rename to packages/stream_chat/lib/src/core/models/action.g.dart diff --git a/packages/stream_chat/lib/src/models/attachment.dart b/packages/stream_chat/lib/src/core/models/attachment.dart similarity index 97% rename from packages/stream_chat/lib/src/models/attachment.dart rename to packages/stream_chat/lib/src/core/models/attachment.dart index b68a1bae..1df785a8 100644 --- a/packages/stream_chat/lib/src/models/attachment.dart +++ b/packages/stream_chat/lib/src/core/models/attachment.dart @@ -2,9 +2,9 @@ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; -import 'package:stream_chat/src/models/action.dart'; -import 'package:stream_chat/src/models/attachment_file.dart'; -import 'package:stream_chat/src/models/serialization.dart'; +import 'package:stream_chat/src/core/models/action.dart'; +import 'package:stream_chat/src/core/models/attachment_file.dart'; +import 'package:stream_chat/src/core/models/serialization.dart'; import 'package:uuid/uuid.dart'; part 'attachment.g.dart'; diff --git a/packages/stream_chat/lib/src/models/attachment.g.dart b/packages/stream_chat/lib/src/core/models/attachment.g.dart similarity index 100% rename from packages/stream_chat/lib/src/models/attachment.g.dart rename to packages/stream_chat/lib/src/core/models/attachment.g.dart diff --git a/packages/stream_chat/lib/src/models/attachment_file.dart b/packages/stream_chat/lib/src/core/models/attachment_file.dart similarity index 100% rename from packages/stream_chat/lib/src/models/attachment_file.dart rename to packages/stream_chat/lib/src/core/models/attachment_file.dart diff --git a/packages/stream_chat/lib/src/models/attachment_file.freezed.dart b/packages/stream_chat/lib/src/core/models/attachment_file.freezed.dart similarity index 100% rename from packages/stream_chat/lib/src/models/attachment_file.freezed.dart rename to packages/stream_chat/lib/src/core/models/attachment_file.freezed.dart diff --git a/packages/stream_chat/lib/src/models/attachment_file.g.dart b/packages/stream_chat/lib/src/core/models/attachment_file.g.dart similarity index 100% rename from packages/stream_chat/lib/src/models/attachment_file.g.dart rename to packages/stream_chat/lib/src/core/models/attachment_file.g.dart diff --git a/packages/stream_chat/lib/src/models/channel_config.dart b/packages/stream_chat/lib/src/core/models/channel_config.dart similarity index 97% rename from packages/stream_chat/lib/src/models/channel_config.dart rename to packages/stream_chat/lib/src/core/models/channel_config.dart index 9ba36e4f..2d5182b3 100644 --- a/packages/stream_chat/lib/src/models/channel_config.dart +++ b/packages/stream_chat/lib/src/core/models/channel_config.dart @@ -1,5 +1,5 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:stream_chat/src/models/command.dart'; +import 'package:stream_chat/src/core/models/command.dart'; part 'channel_config.g.dart'; diff --git a/packages/stream_chat/lib/src/models/channel_config.g.dart b/packages/stream_chat/lib/src/core/models/channel_config.g.dart similarity index 100% rename from packages/stream_chat/lib/src/models/channel_config.g.dart rename to packages/stream_chat/lib/src/core/models/channel_config.g.dart diff --git a/packages/stream_chat/lib/src/models/channel_model.dart b/packages/stream_chat/lib/src/core/models/channel_model.dart similarity index 96% rename from packages/stream_chat/lib/src/models/channel_model.dart rename to packages/stream_chat/lib/src/core/models/channel_model.dart index 41230bf7..892713e7 100644 --- a/packages/stream_chat/lib/src/models/channel_model.dart +++ b/packages/stream_chat/lib/src/core/models/channel_model.dart @@ -1,7 +1,7 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:stream_chat/src/models/channel_config.dart'; -import 'package:stream_chat/src/models/serialization.dart'; -import 'package:stream_chat/src/models/user.dart'; +import 'package:stream_chat/src/core/models/channel_config.dart'; +import 'package:stream_chat/src/core/models/serialization.dart'; +import 'package:stream_chat/src/core/models/user.dart'; part 'channel_model.g.dart'; diff --git a/packages/stream_chat/lib/src/models/channel_model.g.dart b/packages/stream_chat/lib/src/core/models/channel_model.g.dart similarity index 100% rename from packages/stream_chat/lib/src/models/channel_model.g.dart rename to packages/stream_chat/lib/src/core/models/channel_model.g.dart diff --git a/packages/stream_chat/lib/src/models/channel_state.dart b/packages/stream_chat/lib/src/core/models/channel_state.dart similarity index 87% rename from packages/stream_chat/lib/src/models/channel_state.dart rename to packages/stream_chat/lib/src/core/models/channel_state.dart index 9d18f434..998efa98 100644 --- a/packages/stream_chat/lib/src/models/channel_state.dart +++ b/packages/stream_chat/lib/src/core/models/channel_state.dart @@ -1,9 +1,9 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:stream_chat/src/models/channel_model.dart'; -import 'package:stream_chat/src/models/member.dart'; -import 'package:stream_chat/src/models/message.dart'; -import 'package:stream_chat/src/models/read.dart'; -import 'package:stream_chat/src/models/user.dart'; +import 'package:stream_chat/src/core/models/channel_model.dart'; +import 'package:stream_chat/src/core/models/member.dart'; +import 'package:stream_chat/src/core/models/message.dart'; +import 'package:stream_chat/src/core/models/read.dart'; +import 'package:stream_chat/src/core/models/user.dart'; part 'channel_state.g.dart'; diff --git a/packages/stream_chat/lib/src/models/channel_state.g.dart b/packages/stream_chat/lib/src/core/models/channel_state.g.dart similarity index 100% rename from packages/stream_chat/lib/src/models/channel_state.g.dart rename to packages/stream_chat/lib/src/core/models/channel_state.g.dart diff --git a/packages/stream_chat/lib/src/models/command.dart b/packages/stream_chat/lib/src/core/models/command.dart similarity index 100% rename from packages/stream_chat/lib/src/models/command.dart rename to packages/stream_chat/lib/src/core/models/command.dart diff --git a/packages/stream_chat/lib/src/models/command.g.dart b/packages/stream_chat/lib/src/core/models/command.g.dart similarity index 100% rename from packages/stream_chat/lib/src/models/command.g.dart rename to packages/stream_chat/lib/src/core/models/command.g.dart diff --git a/packages/stream_chat/lib/src/models/device.dart b/packages/stream_chat/lib/src/core/models/device.dart similarity index 100% rename from packages/stream_chat/lib/src/models/device.dart rename to packages/stream_chat/lib/src/core/models/device.dart diff --git a/packages/stream_chat/lib/src/models/device.g.dart b/packages/stream_chat/lib/src/core/models/device.g.dart similarity index 100% rename from packages/stream_chat/lib/src/models/device.g.dart rename to packages/stream_chat/lib/src/core/models/device.g.dart diff --git a/packages/stream_chat/lib/src/models/event.dart b/packages/stream_chat/lib/src/core/models/event.dart similarity index 93% rename from packages/stream_chat/lib/src/models/event.dart rename to packages/stream_chat/lib/src/core/models/event.dart index b4ee9c1d..d0d17aa0 100644 --- a/packages/stream_chat/lib/src/models/event.dart +++ b/packages/stream_chat/lib/src/core/models/event.dart @@ -1,7 +1,7 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:stream_chat/src/models/channel_model.dart'; -import 'package:stream_chat/src/models/message.dart'; -import 'package:stream_chat/src/models/serialization.dart'; +import 'package:stream_chat/src/core/models/channel_model.dart'; +import 'package:stream_chat/src/core/models/message.dart'; +import 'package:stream_chat/src/core/models/serialization.dart'; import 'package:stream_chat/stream_chat.dart'; part 'event.g.dart'; @@ -10,11 +10,11 @@ part 'event.g.dart'; @JsonSerializable() class Event { /// Constructor used for json serialization - const Event({ - this.type, + Event({ + this.type = 'local.event', this.cid, this.connectionId, - this.createdAt, + DateTime? createdAt, this.me, this.user, this.message, @@ -29,7 +29,7 @@ class Event { this.parentId, this.extraData = const {}, this.isLocal = true, - }); + }) : createdAt = createdAt ?? DateTime.now(); /// Create a new instance from a json factory Event.fromJson(Map json) => @@ -40,7 +40,7 @@ class Event { /// The type of the event /// [EventType] contains some predefined constant types - final String? type; + final String type; /// The channel cid to which the event belongs final String? cid; @@ -55,7 +55,7 @@ class Event { final String? connectionId; /// The date of creation of the event - final DateTime? createdAt; + final DateTime createdAt; /// User object of the health check user final OwnUser? me; diff --git a/packages/stream_chat/lib/src/models/event.g.dart b/packages/stream_chat/lib/src/core/models/event.g.dart similarity index 97% rename from packages/stream_chat/lib/src/models/event.g.dart rename to packages/stream_chat/lib/src/core/models/event.g.dart index aba142f8..90abeb71 100644 --- a/packages/stream_chat/lib/src/models/event.g.dart +++ b/packages/stream_chat/lib/src/core/models/event.g.dart @@ -8,7 +8,7 @@ part of 'event.dart'; Event _$EventFromJson(Map json) { return Event( - type: json['type'] as String?, + type: json['type'] as String, cid: json['cid'] as String?, connectionId: json['connection_id'] as String?, createdAt: json['created_at'] == null @@ -49,7 +49,7 @@ Map _$EventToJson(Event instance) => { 'channel_id': instance.channelId, 'channel_type': instance.channelType, 'connection_id': instance.connectionId, - 'created_at': instance.createdAt?.toIso8601String(), + 'created_at': instance.createdAt.toIso8601String(), 'me': instance.me?.toJson(), 'user': instance.user?.toJson(), 'message': instance.message?.toJson(), diff --git a/packages/stream_chat/lib/src/models/filter.dart b/packages/stream_chat/lib/src/core/models/filter.dart similarity index 100% rename from packages/stream_chat/lib/src/models/filter.dart rename to packages/stream_chat/lib/src/core/models/filter.dart diff --git a/packages/stream_chat/lib/src/models/member.dart b/packages/stream_chat/lib/src/core/models/member.dart similarity index 98% rename from packages/stream_chat/lib/src/models/member.dart rename to packages/stream_chat/lib/src/core/models/member.dart index 49df170c..87c82473 100644 --- a/packages/stream_chat/lib/src/models/member.dart +++ b/packages/stream_chat/lib/src/core/models/member.dart @@ -1,5 +1,5 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:stream_chat/src/models/user.dart'; +import 'package:stream_chat/src/core/models/user.dart'; part 'member.g.dart'; diff --git a/packages/stream_chat/lib/src/models/member.g.dart b/packages/stream_chat/lib/src/core/models/member.g.dart similarity index 100% rename from packages/stream_chat/lib/src/models/member.g.dart rename to packages/stream_chat/lib/src/core/models/member.g.dart diff --git a/packages/stream_chat/lib/src/models/message.dart b/packages/stream_chat/lib/src/core/models/message.dart similarity index 98% rename from packages/stream_chat/lib/src/models/message.dart rename to packages/stream_chat/lib/src/core/models/message.dart index f928ce3a..f3bf100f 100644 --- a/packages/stream_chat/lib/src/models/message.dart +++ b/packages/stream_chat/lib/src/core/models/message.dart @@ -1,9 +1,9 @@ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; -import 'package:stream_chat/src/models/attachment.dart'; -import 'package:stream_chat/src/models/reaction.dart'; -import 'package:stream_chat/src/models/serialization.dart'; -import 'package:stream_chat/src/models/user.dart'; +import 'package:stream_chat/src/core/models/attachment.dart'; +import 'package:stream_chat/src/core/models/reaction.dart'; +import 'package:stream_chat/src/core/models/serialization.dart'; +import 'package:stream_chat/src/core/models/user.dart'; import 'package:uuid/uuid.dart'; part 'message.g.dart'; diff --git a/packages/stream_chat/lib/src/models/message.g.dart b/packages/stream_chat/lib/src/core/models/message.g.dart similarity index 100% rename from packages/stream_chat/lib/src/models/message.g.dart rename to packages/stream_chat/lib/src/core/models/message.g.dart diff --git a/packages/stream_chat/lib/src/models/mute.dart b/packages/stream_chat/lib/src/core/models/mute.dart similarity index 85% rename from packages/stream_chat/lib/src/models/mute.dart rename to packages/stream_chat/lib/src/core/models/mute.dart index 3ba26230..8c3bef89 100644 --- a/packages/stream_chat/lib/src/models/mute.dart +++ b/packages/stream_chat/lib/src/core/models/mute.dart @@ -1,7 +1,7 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:stream_chat/src/models/channel_model.dart'; -import 'package:stream_chat/src/models/serialization.dart'; -import 'package:stream_chat/src/models/user.dart'; +import 'package:stream_chat/src/core/models/channel_model.dart'; +import 'package:stream_chat/src/core/models/serialization.dart'; +import 'package:stream_chat/src/core/models/user.dart'; part 'mute.g.dart'; diff --git a/packages/stream_chat/lib/src/models/mute.g.dart b/packages/stream_chat/lib/src/core/models/mute.g.dart similarity index 100% rename from packages/stream_chat/lib/src/models/mute.g.dart rename to packages/stream_chat/lib/src/core/models/mute.g.dart diff --git a/packages/stream_chat/lib/src/models/own_user.dart b/packages/stream_chat/lib/src/core/models/own_user.dart similarity index 79% rename from packages/stream_chat/lib/src/models/own_user.dart rename to packages/stream_chat/lib/src/core/models/own_user.dart index a93af19a..6f975a94 100644 --- a/packages/stream_chat/lib/src/models/own_user.dart +++ b/packages/stream_chat/lib/src/core/models/own_user.dart @@ -1,8 +1,8 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:stream_chat/src/models/device.dart'; -import 'package:stream_chat/src/models/mute.dart'; -import 'package:stream_chat/src/models/serialization.dart'; -import 'package:stream_chat/src/models/user.dart'; +import 'package:stream_chat/src/core/models/device.dart'; +import 'package:stream_chat/src/core/models/mute.dart'; +import 'package:stream_chat/src/core/models/serialization.dart'; +import 'package:stream_chat/src/core/models/user.dart'; part 'own_user.g.dart'; @@ -40,6 +40,18 @@ class OwnUser extends User { factory OwnUser.fromJson(Map json) => _$OwnUserFromJson( Serialization.moveToExtraDataFromRoot(json, topLevelFields)); + /// Create a new instance from [User] object + factory OwnUser.fromUser(User user) => OwnUser( + id: user.id, + role: user.role, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + lastActive: user.lastActive, + online: user.online, + banned: user.banned, + extraData: user.extraData, + ); + /// List of user devices @JsonKey( includeIfNull: false, diff --git a/packages/stream_chat/lib/src/models/own_user.g.dart b/packages/stream_chat/lib/src/core/models/own_user.g.dart similarity index 100% rename from packages/stream_chat/lib/src/models/own_user.g.dart rename to packages/stream_chat/lib/src/core/models/own_user.g.dart diff --git a/packages/stream_chat/lib/src/models/reaction.dart b/packages/stream_chat/lib/src/core/models/reaction.dart similarity index 95% rename from packages/stream_chat/lib/src/models/reaction.dart rename to packages/stream_chat/lib/src/core/models/reaction.dart index 85697204..ace4a15e 100644 --- a/packages/stream_chat/lib/src/models/reaction.dart +++ b/packages/stream_chat/lib/src/core/models/reaction.dart @@ -1,6 +1,6 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:stream_chat/src/models/serialization.dart'; -import 'package:stream_chat/src/models/user.dart'; +import 'package:stream_chat/src/core/models/serialization.dart'; +import 'package:stream_chat/src/core/models/user.dart'; part 'reaction.g.dart'; diff --git a/packages/stream_chat/lib/src/models/reaction.g.dart b/packages/stream_chat/lib/src/core/models/reaction.g.dart similarity index 100% rename from packages/stream_chat/lib/src/models/reaction.g.dart rename to packages/stream_chat/lib/src/core/models/reaction.g.dart diff --git a/packages/stream_chat/lib/src/models/read.dart b/packages/stream_chat/lib/src/core/models/read.dart similarity index 94% rename from packages/stream_chat/lib/src/models/read.dart rename to packages/stream_chat/lib/src/core/models/read.dart index cbd47dd1..55912762 100644 --- a/packages/stream_chat/lib/src/models/read.dart +++ b/packages/stream_chat/lib/src/core/models/read.dart @@ -1,5 +1,5 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:stream_chat/src/models/user.dart'; +import 'package:stream_chat/src/core/models/user.dart'; part 'read.g.dart'; diff --git a/packages/stream_chat/lib/src/models/read.g.dart b/packages/stream_chat/lib/src/core/models/read.g.dart similarity index 100% rename from packages/stream_chat/lib/src/models/read.g.dart rename to packages/stream_chat/lib/src/core/models/read.g.dart diff --git a/packages/stream_chat/lib/src/models/serialization.dart b/packages/stream_chat/lib/src/core/models/serialization.dart similarity index 96% rename from packages/stream_chat/lib/src/models/serialization.dart rename to packages/stream_chat/lib/src/core/models/serialization.dart index d584912e..692ad01a 100644 --- a/packages/stream_chat/lib/src/models/serialization.dart +++ b/packages/stream_chat/lib/src/core/models/serialization.dart @@ -1,4 +1,4 @@ -import 'package:stream_chat/src/models/user.dart'; +import 'package:stream_chat/src/core/models/user.dart'; /// Used to avoid to serialize properties to json // ignore: prefer_void_to_null diff --git a/packages/stream_chat/lib/src/models/user.dart b/packages/stream_chat/lib/src/core/models/user.dart similarity index 98% rename from packages/stream_chat/lib/src/models/user.dart rename to packages/stream_chat/lib/src/core/models/user.dart index 9335f8c8..ae1dd742 100644 --- a/packages/stream_chat/lib/src/models/user.dart +++ b/packages/stream_chat/lib/src/core/models/user.dart @@ -1,5 +1,5 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:stream_chat/src/models/serialization.dart'; +import 'package:stream_chat/src/core/models/serialization.dart'; part 'user.g.dart'; diff --git a/packages/stream_chat/lib/src/models/user.g.dart b/packages/stream_chat/lib/src/core/models/user.g.dart similarity index 100% rename from packages/stream_chat/lib/src/models/user.g.dart rename to packages/stream_chat/lib/src/core/models/user.g.dart diff --git a/packages/stream_chat/lib/src/core/utils.dart b/packages/stream_chat/lib/src/core/utils.dart new file mode 100644 index 00000000..9366e34e --- /dev/null +++ b/packages/stream_chat/lib/src/core/utils.dart @@ -0,0 +1,16 @@ +import 'dart:math' as math; + +// This alphabet uses `A-Za-z0-9_-` symbols. The genetic algorithm helped +// optimize the gzip compression for this alphabet. +const _alphabet = + 'ModuleSymbhasOwnPr0123456789ABCDEFGHNRVfgctiUvzKqYTJkLxpZXIjQW'; + +/// Generates a random String id +/// Adopted from: https://github.com/ai/nanoid/blob/main/non-secure/index.js +String randomId({int size = 21}) { + var id = ''; + for (var i = 0; i < size; i++) { + id += _alphabet[(math.Random().nextInt(32) * 64) | 0]; + } + return id; +} diff --git a/packages/stream_chat/lib/src/db/chat_persistence_client.dart b/packages/stream_chat/lib/src/db/chat_persistence_client.dart index 8f60482f..d7e0f765 100644 --- a/packages/stream_chat/lib/src/db/chat_persistence_client.dart +++ b/packages/stream_chat/lib/src/db/chat_persistence_client.dart @@ -1,13 +1,13 @@ -import 'package:stream_chat/src/api/requests.dart'; -import 'package:stream_chat/src/models/channel_model.dart'; -import 'package:stream_chat/src/models/channel_state.dart'; -import 'package:stream_chat/src/models/event.dart'; -import 'package:stream_chat/src/models/filter.dart'; -import 'package:stream_chat/src/models/member.dart'; -import 'package:stream_chat/src/models/message.dart'; -import 'package:stream_chat/src/models/reaction.dart'; -import 'package:stream_chat/src/models/read.dart'; -import 'package:stream_chat/src/models/user.dart'; +import 'package:stream_chat/src/core/api/requests.dart'; +import 'package:stream_chat/src/core/models/channel_model.dart'; +import 'package:stream_chat/src/core/models/channel_state.dart'; +import 'package:stream_chat/src/core/models/event.dart'; +import 'package:stream_chat/src/core/models/filter.dart'; +import 'package:stream_chat/src/core/models/member.dart'; +import 'package:stream_chat/src/core/models/message.dart'; +import 'package:stream_chat/src/core/models/reaction.dart'; +import 'package:stream_chat/src/core/models/read.dart'; +import 'package:stream_chat/src/core/models/user.dart'; import 'package:stream_chat/src/extensions/iterable_extension.dart'; /// A simple client used for persisting chat data locally. diff --git a/packages/stream_chat/lib/src/errors/chat_error_code.dart b/packages/stream_chat/lib/src/errors/chat_error_code.dart new file mode 100644 index 00000000..3dbb600c --- /dev/null +++ b/packages/stream_chat/lib/src/errors/chat_error_code.dart @@ -0,0 +1,91 @@ +/// +enum ChatErrorCode { + // client error codes + networkFailed, + parserError, + socketClosed, + socketFailure, + cantParseConnectionEvent, + cantParseEvent, + invalidToken, + undefinedToken, + unableToParseSocketEvent, + noErrorBody, + + // server error codes + authenticationError, + tokenExpired, + tokenNotValid, + tokenDateIncorrect, + tokenSignatureIncorrect, + apiKeyNotFound, +} + +/// +extension ChatErrorCodeX on ChatErrorCode { + /// + String get message => { + // client error message + ChatErrorCode.networkFailed: 'Response is failed. See cause', + ChatErrorCode.parserError: 'Unable to parse error', + ChatErrorCode.socketClosed: 'Server closed connection', + ChatErrorCode.socketFailure: 'See stack trace in logs. ' + 'Intercept error in error handler of setUser', + ChatErrorCode.cantParseConnectionEvent: + 'Unable to parse connection event', + ChatErrorCode.cantParseEvent: 'Unable to parse event', + ChatErrorCode.invalidToken: 'Invalid token', + ChatErrorCode.undefinedToken: + 'No defined token. Check if client.setUser was called and finished', + ChatErrorCode.unableToParseSocketEvent: + 'Socket event payload either invalid or null', + ChatErrorCode.noErrorBody: 'No error body. See http status code', + + // server error message + ChatErrorCode.authenticationError: + 'Unauthenticated, problem with authentication', + ChatErrorCode.tokenExpired: 'Token expired, new one must be requested.', + ChatErrorCode.tokenNotValid: 'Unauthenticated, token not valid yet', + ChatErrorCode.tokenDateIncorrect: + 'Unauthenticated, token date incorrect', + ChatErrorCode.tokenSignatureIncorrect: + 'Unauthenticated, token signature invalid', + ChatErrorCode.apiKeyNotFound: + "Api key is not found, verify it if it's correct or was created.", + }[this]!; + + /// + int get code => { + // client error codes + ChatErrorCode.networkFailed: 1000, + ChatErrorCode.parserError: 1001, + ChatErrorCode.socketClosed: 1002, + ChatErrorCode.socketFailure: 1003, + ChatErrorCode.cantParseConnectionEvent: 1004, + ChatErrorCode.cantParseEvent: 1005, + ChatErrorCode.invalidToken: 1006, + ChatErrorCode.undefinedToken: 1007, + ChatErrorCode.unableToParseSocketEvent: 1008, + ChatErrorCode.noErrorBody: 1009, + + // server error codes + ChatErrorCode.authenticationError: 5, + ChatErrorCode.tokenExpired: 40, + ChatErrorCode.tokenNotValid: 41, + ChatErrorCode.tokenDateIncorrect: 42, + ChatErrorCode.tokenSignatureIncorrect: 43, + ChatErrorCode.apiKeyNotFound: 2, + }[this]!; + + /// + Set get authenticationErrors => { + ChatErrorCode.authenticationError, + ChatErrorCode.tokenExpired, + ChatErrorCode.tokenNotValid, + ChatErrorCode.tokenDateIncorrect, + ChatErrorCode.tokenSignatureIncorrect, + }.map((it) => it.code).toSet(); + + /// + bool isAuthenticationError(int code) => authenticationErrors.contains(code); +} diff --git a/packages/stream_chat/lib/src/errors/stream_chat_error.dart b/packages/stream_chat/lib/src/errors/stream_chat_error.dart new file mode 100644 index 00000000..280ab6c7 --- /dev/null +++ b/packages/stream_chat/lib/src/errors/stream_chat_error.dart @@ -0,0 +1,77 @@ +import 'package:equatable/equatable.dart'; +import 'package:stream_chat/src/errors/chat_error_code.dart'; +import 'package:stream_chat/stream_chat.dart'; + +/// +class StreamChatError extends Equatable implements Exception { + /// + StreamChatError(ChatErrorCode errorCode) + : code = errorCode.code, + message = errorCode.message; + + /// + const StreamChatError.raw(this.code, this.message); + + /// Error code + final int code; + + /// Error message + final String message; + + @override + List get props => [code, message]; + + @override + String toString() => 'StreamChatError(code: $code, message: $message)'; +} + +/// +class StreamChatNetworkError extends StreamChatError { + /// + StreamChatNetworkError({ + required ChatErrorCode errorCode, + int? statusCode, + this.data, + }) : statusCode = statusCode ?? data?.statusCode, + super(errorCode); + + /// + const StreamChatNetworkError.raw({ + required int code, + required String message, + this.statusCode, + this.data, + }) : super.raw(code, message); + + /// + factory StreamChatNetworkError.fromDioError(DioError error) { + final response = error.response; + ErrorResponse? errorResponse; + final data = response?.data; + if (data != null) { + errorResponse = ErrorResponse.fromJson(data); + } + return StreamChatNetworkError.raw( + code: errorResponse?.code ?? -1, + message: errorResponse?.message ?? response?.statusMessage ?? '', + statusCode: errorResponse?.statusCode ?? response?.statusCode, + data: errorResponse, + ); + } + + /// HTTP status code + final int? statusCode; + + /// Response body. please refer to [ErrorResponse]. + final ErrorResponse? data; + + @override + List get props => [...super.props, statusCode]; + + @override + String toString() => 'StreamChatNetworkError(' + 'code: $code, ' + 'message: $message, ' + 'statusCode: $statusCode, ' + 'data: $data)'; +} diff --git a/packages/stream_chat/lib/src/event_type.dart b/packages/stream_chat/lib/src/event_type.dart index fd93af2a..87529399 100644 --- a/packages/stream_chat/lib/src/event_type.dart +++ b/packages/stream_chat/lib/src/event_type.dart @@ -3,6 +3,9 @@ class EventType { /// Indicates any type of events static const String any = '*'; + /// + static const String healthCheck = 'health.check'; + /// Event sent when a user starts typing a message static const String typingStart = 'typing.start'; diff --git a/packages/stream_chat/lib/src/api/connection_status.dart b/packages/stream_chat/lib/src/ws/connection_status.dart similarity index 100% rename from packages/stream_chat/lib/src/api/connection_status.dart rename to packages/stream_chat/lib/src/ws/connection_status.dart diff --git a/packages/stream_chat/lib/src/ws/socket_error.dart b/packages/stream_chat/lib/src/ws/socket_error.dart new file mode 100644 index 00000000..00b052b9 --- /dev/null +++ b/packages/stream_chat/lib/src/ws/socket_error.dart @@ -0,0 +1,25 @@ +/// +class SocketError { + /// + const SocketError({ + this.code = -1, + this.statusCode = -1, + this.message = '', + }); + + /// + factory SocketError.fromJson(Map json) => SocketError( + code: json['code'], + statusCode: json['StatusCode'], + message: json['message'], + ); + + /// + final int code; + + /// + final int statusCode; + + /// + final String message; +} diff --git a/packages/stream_chat/lib/src/ws/timer_helper.dart b/packages/stream_chat/lib/src/ws/timer_helper.dart new file mode 100644 index 00000000..0974ee80 --- /dev/null +++ b/packages/stream_chat/lib/src/ws/timer_helper.dart @@ -0,0 +1,51 @@ +import 'dart:async'; +import 'package:uuid/uuid.dart'; + +/// +class TimerHelper { + final _uuid = const Uuid(); + late final _timers = {}; + + /// + String setTimer( + Duration duration, + void Function() callback, { + bool immediate = false, + }) { + final id = _uuid.v1(); + final timer = Timer(duration, callback); + if (immediate) callback(); + _timers[id] = timer; + return id; + } + + /// + String setPeriodicTimer( + Duration duration, + void Function(Timer) callback, { + bool immediate = false, + }) { + final id = _uuid.v1(); + final timer = Timer.periodic(duration, callback); + if (immediate) callback.call(timer); + _timers[id] = timer; + return id; + } + + /// + void cancelTimer(String id) { + final timer = _timers.remove(id); + return timer?.cancel(); + } + + /// + void cancelAllTimers() { + for (final t in _timers.values) { + t.cancel(); + } + _timers.clear(); + } + + /// + bool get hasTimers => _timers.isNotEmpty; +} diff --git a/packages/stream_chat/lib/src/ws/websocket.dart b/packages/stream_chat/lib/src/ws/websocket.dart new file mode 100644 index 00000000..0e19f9a6 --- /dev/null +++ b/packages/stream_chat/lib/src/ws/websocket.dart @@ -0,0 +1,395 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:math' as math; + +import 'package:logging/logging.dart'; +import 'package:meta/meta.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:stream_chat/src/ws/connection_status.dart'; +import 'package:stream_chat/src/core/http/token.dart'; +import 'package:stream_chat/src/core/http/token_manager.dart'; +import 'package:stream_chat/src/core/models/event.dart'; +import 'package:stream_chat/src/core/models/user.dart'; +import 'package:stream_chat/src/event_type.dart'; +import 'package:stream_chat/src/ws/socket_error.dart'; +import 'package:stream_chat/src/ws/timer_helper.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; +import 'package:web_socket_channel/status.dart' as status; + +/// Typedef which exposes an [Event] as the only parameter. +typedef EventHandler = void Function(Event); + +/// Typedef used for connecting to a websocket. Method returns a +/// [WebSocketChannel] and accepts a connection [url] and an optional +/// [Iterable] of `protocols`. +typedef WebSocketChannelProvider = WebSocketChannel Function( + Uri uri, { + Iterable? protocols, +}); + +const _tokenExpiredErrorCode = 40; + +/// A WebSocket connection that reconnects upon failure. +class WebSocket with TimerHelper { + /// Creates a new websocket + /// To connect the WS call [connect] + WebSocket({ + required this.apiKey, + required this.baseUrl, + required this.tokenManager, + this.handler, + Logger? logger, + this.webSocketChannelProvider, + this.reconnectionMonitorInterval = 10, + this.healthCheckInterval = 20, + this.reconnectionMonitorTimeout = 40, + }) : _logger = logger; + + /// + final String apiKey; + + /// WS base url + final String baseUrl; + + /// + final TokenManager tokenManager; + + /// Functions that will be called every time a new event is received from the + /// connection + final EventHandler? handler; + + final Logger? _logger; + + /// Connection function + /// Used only for testing purpose + @visibleForTesting + final WebSocketChannelProvider? webSocketChannelProvider; + + /// Interval of the reconnection monitor timer + /// This checks that it received a new event in the last + /// [reconnectionMonitorTimeout] seconds, otherwise it considers the + /// connection unhealthy and reconnects the WS + final int reconnectionMonitorInterval; + + /// Interval of the health event sending timer + /// This sends a health event every [healthCheckInterval] seconds in order to + /// make the server aware that the client is still listening + final int healthCheckInterval; + + /// The timeout that uses the reconnection monitor timer to consider the + /// connection unhealthy + final int reconnectionMonitorTimeout; + + User? _user; + String? _connectionId; + DateTime? _lastEventAt; + WebSocketChannel? _webSocketChannel; + StreamSubscription? _webSocketChannelSubscription; + + /// + Completer? connectionCompleter; + + /// + String? get connectionId => _connectionId; + + final _connectionStatusController = + BehaviorSubject.seeded(ConnectionStatus.disconnected); + + set _connectionStatus(ConnectionStatus status) => + _connectionStatusController.add(status); + + /// The current connection status value + ConnectionStatus get connectionStatus => _connectionStatusController.value!; + + /// This notifies of connection status changes + Stream get connectionStatusStream => + _connectionStatusController.stream.distinct(); + + void _initWebSocketChannel(Uri uri) { + _logger?.info('Initiating connection with $baseUrl'); + if (_webSocketChannel != null) { + _closeWebSocketChannel(); + } + _webSocketChannel = + webSocketChannelProvider?.call(uri) ?? WebSocketChannel.connect(uri); + _subscribeToWebSocketChannel(); + } + + void _closeWebSocketChannel() { + _logger?.info('Closing connection with $baseUrl'); + if (_webSocketChannel != null) { + _unsubscribeFromWebSocketChannel(); + _webSocketChannel?.sink.close(status.goingAway); + _webSocketChannel = null; + } + } + + void _subscribeToWebSocketChannel() { + _logger?.info('Started listening to $baseUrl'); + if (_webSocketChannelSubscription != null) { + _unsubscribeFromWebSocketChannel(); + } + _webSocketChannelSubscription = _webSocketChannel?.stream.listen( + _onDataReceived, + onError: _onConnectionError, + onDone: _onConnectionClosed, + ); + } + + void _unsubscribeFromWebSocketChannel() { + _logger?.info('Stopped listening to $baseUrl'); + if (_webSocketChannelSubscription != null) { + _webSocketChannelSubscription?.cancel(); + _webSocketChannelSubscription = null; + } + } + + Future _buildUri({bool refreshToken = false}) async { + final user = _user!; + final token = await tokenManager.loadToken(refresh: refreshToken); + final params = { + 'user_id': user.id, + 'user_details': user, + 'user_token': token.rawValue, + 'server_determines_connection_id': true, + }; + final qs = { + 'json': jsonEncode(params), + 'api_key': apiKey, + 'authorization': token.rawValue, + 'stream-auth-type': token.authType.raw, + }; + final scheme = baseUrl.startsWith('https') ? 'wss' : 'ws'; + final host = baseUrl.replaceAll(RegExp(r'(^\w+:|^)\/\/'), ''); + return Uri( + scheme: scheme, + host: host, + pathSegments: ['connect'], + queryParameters: qs, + ); + } + + bool _connectRequestInProgress = false; + + /// Connect the WS using the parameters passed in the constructor + Future connect(User user) async { + if (_connectRequestInProgress) { + throw Exception(''' + You've called connect twice, + can only attempt 1 connection at the time, + '''); + } + _connectRequestInProgress = true; + _manuallyClosed = false; + + _user = user; + _connectionStatus = ConnectionStatus.connecting; + connectionCompleter = Completer(); + + final uri = await _buildUri(); + _initWebSocketChannel(uri); + + return connectionCompleter!.future; + } + + int _reconnectAttempt = 0; + bool _reconnectRequestInProgress = false; + + Future _reconnect({bool refreshToken = false}) async { + _logger?.info('Retrying connection : $_reconnectAttempt'); + if (_reconnectRequestInProgress) return; + _reconnectRequestInProgress = true; + + _stopMonitoringEvents(); + // Closing any previously opened web-socket + _closeWebSocketChannel(); + + _reconnectAttempt += 1; + _connectionStatus = ConnectionStatus.connecting; + + final delay = _getReconnectInterval(_reconnectAttempt); + setTimer( + Duration(milliseconds: delay), + () async { + final uri = await _buildUri(refreshToken: refreshToken); + _initWebSocketChannel(uri); + }, + ); + } + + // returns the reconnect interval based on `reconnectAttempt` in milliseconds + int _getReconnectInterval(int reconnectAttempt) { + // try to reconnect in 0.25-25 seconds + // (random to spread out the load from failures) + final max = math.min(500 + reconnectAttempt * 2000, 25000); + final min = math.min( + math.max(250, (reconnectAttempt - 1) * 2000), + 25000, + ); + return (math.Random().nextDouble() * (max - min) + min).floor(); + } + + void _startMonitoringEvents() { + _logger?.info('Starting monitoring events'); + // cancel all previous timers + cancelAllTimers(); + + _startHealthCheck(); + _startReconnectionMonitor(); + } + + void _stopMonitoringEvents() { + _logger?.info('Stopped monitoring events'); + // reset lastEvent + _lastEventAt = null; + + cancelAllTimers(); + } + + void _handleConnectedEvent(Event event) { + // updating connectionId and status + _connectionId = event.connectionId; + _connectionStatus = ConnectionStatus.connected; + + _logger?.info('Connection successful: $_connectionId'); + + // notify user that connection is completed + final completer = connectionCompleter; + if (completer != null && !completer.isCompleted) { + completer.complete(event); + } + + // start monitoring health-check events + _startMonitoringEvents(); + } + + void _handleHealthCheckEvent(Event event) { + _logger?.info('HealthCheck received : ${event.connectionId}'); + + _connectionId = event.connectionId; + _connectionStatus = ConnectionStatus.connected; + } + + void _onDataReceived(dynamic data) { + final jsonData = json.decode(data); + final error = jsonData['error']; + if (error != null) return _onConnectionError(error); + + // resetting connect, reconnect request flag + _resetRequestFlags(resetAttempts: true); + + Event? event; + try { + event = Event.fromJson(jsonData); + } catch (_) {} + + if (event == null) return; + + _lastEventAt = DateTime.now(); + _logger?.info('Event received: ${event.type}'); + + if (event.type == EventType.healthCheck) { + if (event.me != null) { + _handleConnectedEvent(event); + } else { + _handleHealthCheckEvent(event); + } + } + + return handler?.call(event); + } + + void _onConnectionError(error, [stacktrace]) { + _logger?.severe('Error occurred', error, stacktrace); + + // resetting connect, reconnect request flag + _resetRequestFlags(); + + var refreshToken = false; + try { + final socketError = SocketError.fromJson(json.decode(error)); + refreshToken = socketError.code == _tokenExpiredErrorCode; + } catch (_) {} + + // refresh token in case it is expired + _reconnect(refreshToken: refreshToken); + } + + bool _manuallyClosed = false; + + void _onConnectionClosed() { + _logger?.info('Connection closed : $connectionId'); + + // resetting connect, reconnect request flag + _resetRequestFlags(); + + // resetting connection + _connectionId = null; + + // check if we manually closed the connection + if (_manuallyClosed) return; + _reconnect(); + } + + bool get _needsToReconnect { + final lastEventAt = _lastEventAt; + // means not yet connected or disconnected + if (lastEventAt == null) return false; + + // means we missed a health check + final now = DateTime.now(); + return now.difference(lastEventAt).inSeconds > reconnectionMonitorTimeout; + } + + void _resetRequestFlags({bool resetAttempts = false}) { + _connectRequestInProgress = false; + _reconnectRequestInProgress = false; + if (resetAttempts) _reconnectAttempt = 0; + } + + void _startReconnectionMonitor() { + _logger?.info('Starting reconnection monitor'); + setPeriodicTimer( + Duration(seconds: reconnectionMonitorInterval), + (_) { + final needsToReconnect = _needsToReconnect; + _logger?.info('Needs to reconnect : $needsToReconnect'); + if (needsToReconnect) _reconnect(); + }, + immediate: true, + ); + } + + void _startHealthCheck() { + _logger?.info('Starting health check monitor'); + setPeriodicTimer( + Duration(seconds: healthCheckInterval), + (_) { + _logger?.info('Sending Event: ${EventType.healthCheck}'); + final event = Event( + type: EventType.healthCheck, + connectionId: connectionId, + ); + _webSocketChannel?.sink.add(jsonEncode(event)); + }, + immediate: true, + ); + } + + /// Disconnects the WS and releases eventual resources + void disconnect() { + if (connectionStatus == ConnectionStatus.disconnected) return; + _connectionStatus = ConnectionStatus.disconnected; + + _logger?.info('Disconnecting $connectionId'); + + // resetting user + _user = null; + connectionCompleter = null; + + _stopMonitoringEvents(); + + _manuallyClosed = true; + _closeWebSocketChannel(); + } +} diff --git a/packages/stream_chat/lib/stream_chat.dart b/packages/stream_chat/lib/stream_chat.dart index 2ac56b10..de49dcdd 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -8,31 +8,32 @@ export 'package:dio/src/options.dart' show ProgressCallback; export 'package:logging/logging.dart' show Logger, Level; export './src/api/channel.dart'; -export './src/api/connection_status.dart'; -export './src/api/requests.dart'; -export './src/api/requests.dart'; -export './src/api/responses.dart'; -export './src/attachment_file_uploader.dart' show AttachmentFileUploader; export './src/client.dart'; +export './src/core/api/attachment_file_uploader.dart' + show AttachmentFileUploader; +export './src/core/models/action.dart'; +export './src/core/models/attachment.dart'; +export './src/core/models/attachment_file.dart'; +export './src/core/models/channel_config.dart'; +export './src/core/models/channel_model.dart'; +export './src/core/models/channel_state.dart'; +export './src/core/models/command.dart'; +export './src/core/models/device.dart'; +export './src/core/models/event.dart'; +export './src/core/models/filter.dart' show Filter; +export './src/core/models/member.dart'; +export './src/core/models/message.dart'; +export './src/core/models/mute.dart'; +export './src/core/models/own_user.dart'; +export './src/core/models/reaction.dart'; +export './src/core/models/read.dart'; +export './src/core/models/user.dart'; export './src/db/chat_persistence_client.dart'; export './src/event_type.dart'; export './src/exceptions.dart'; export './src/extensions/rate_limit.dart'; export './src/extensions/string_extension.dart'; -export './src/models/action.dart'; -export './src/models/attachment.dart'; -export './src/models/attachment_file.dart'; -export './src/models/channel_config.dart'; -export './src/models/channel_model.dart'; -export './src/models/channel_state.dart'; -export './src/models/command.dart'; -export './src/models/device.dart'; -export './src/models/event.dart'; -export './src/models/filter.dart' show Filter; -export './src/models/member.dart'; -export './src/models/message.dart'; -export './src/models/mute.dart'; -export './src/models/own_user.dart'; -export './src/models/reaction.dart'; -export './src/models/read.dart'; -export './src/models/user.dart'; +export 'src/core/api/requests.dart'; +export 'src/core/api/requests.dart'; +export 'src/core/api/responses.dart'; +export 'src/ws/connection_status.dart'; diff --git a/packages/stream_chat/pubspec.yaml b/packages/stream_chat/pubspec.yaml index 6b8ee0aa..4fd82f3f 100644 --- a/packages/stream_chat/pubspec.yaml +++ b/packages/stream_chat/pubspec.yaml @@ -15,6 +15,7 @@ dependencies: equatable: ^2.0.0 freezed_annotation: ^0.14.0 http_parser: ^4.0.0 + jose: ^0.3.2 json_annotation: ^4.0.1 logging: ^1.0.1 meta: ^1.3.0 diff --git a/packages/stream_chat/test/src/api/channel_test.dart b/packages/stream_chat/test/src/api/channel_test.dart index c3409b7e..901eb4af 100644 --- a/packages/stream_chat/test/src/api/channel_test.dart +++ b/packages/stream_chat/test/src/api/channel_test.dart @@ -3,13 +3,13 @@ import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:dio/native_imp.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat/src/api/requests.dart'; +import 'package:stream_chat/src/core/api/requests.dart'; import 'package:stream_chat/src/client.dart'; import 'package:stream_chat/src/event_type.dart'; -import 'package:stream_chat/src/models/event.dart'; -import 'package:stream_chat/src/models/message.dart'; -import 'package:stream_chat/src/models/own_user.dart'; -import 'package:stream_chat/src/models/reaction.dart'; +import 'package:stream_chat/src/core/models/event.dart'; +import 'package:stream_chat/src/core/models/message.dart'; +import 'package:stream_chat/src/core/models/own_user.dart'; +import 'package:stream_chat/src/core/models/reaction.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:test/test.dart'; diff --git a/packages/stream_chat/test/src/api/responses_test.dart b/packages/stream_chat/test/src/api/responses_test.dart index f91248af..77e7abc3 100644 --- a/packages/stream_chat/test/src/api/responses_test.dart +++ b/packages/stream_chat/test/src/api/responses_test.dart @@ -1,12 +1,12 @@ import 'dart:convert'; import 'package:test/test.dart'; -import 'package:stream_chat/src/api/responses.dart'; -import 'package:stream_chat/src/models/device.dart'; -import 'package:stream_chat/src/models/member.dart'; -import 'package:stream_chat/src/models/message.dart'; -import 'package:stream_chat/src/models/reaction.dart'; -import 'package:stream_chat/src/models/read.dart'; +import 'package:stream_chat/src/core/api/responses.dart'; +import 'package:stream_chat/src/core/models/device.dart'; +import 'package:stream_chat/src/core/models/member.dart'; +import 'package:stream_chat/src/core/models/message.dart'; +import 'package:stream_chat/src/core/models/reaction.dart'; +import 'package:stream_chat/src/core/models/read.dart'; import 'package:stream_chat/stream_chat.dart'; void main() { diff --git a/packages/stream_chat/test/src/api/web_socket_stub_test.dart b/packages/stream_chat/test/src/api/web_socket_stub_test.dart index 7f5bf4b1..77a67421 100644 --- a/packages/stream_chat/test/src/api/web_socket_stub_test.dart +++ b/packages/stream_chat/test/src/api/web_socket_stub_test.dart @@ -4,7 +4,7 @@ import 'package:stream_chat/src/api/web_socket_channel_stub.dart'; void main() { test('src/api/web_socket_stub_test', () { expect( - () => connectWebSocket('fakeurl'), + () => openConnection('fakeurl'), throwsA(isA()), ); }); diff --git a/packages/stream_chat/test/src/api/websocket_test.dart b/packages/stream_chat/test/src/api/websocket_test.dart index b9c897a6..11270a9e 100644 --- a/packages/stream_chat/test/src/api/websocket_test.dart +++ b/packages/stream_chat/test/src/api/websocket_test.dart @@ -2,10 +2,10 @@ import 'dart:async'; import 'package:logging/logging.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat/src/api/connection_status.dart'; -import 'package:stream_chat/src/api/websocket.dart'; -import 'package:stream_chat/src/models/event.dart'; -import 'package:stream_chat/src/models/user.dart'; +import 'package:stream_chat/src/ws/connection_status.dart'; +import 'package:stream_chat/src/ws/websocket.dart'; +import 'package:stream_chat/src/core/models/event.dart'; +import 'package:stream_chat/src/core/models/user.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:test/test.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; @@ -39,7 +39,7 @@ void main() { final ws = WebSocket( baseUrl: 'baseurl', user: User(id: 'testid'), - logger: Logger('ws'), + _logger: Logger('ws'), connectParams: {'test': 'true'}, connectPayload: {'payload': 'test'}, handler: print, @@ -77,7 +77,7 @@ void main() { final ws = WebSocket( baseUrl: 'baseurl', user: User(id: 'testid'), - logger: Logger('ws'), + _logger: Logger('ws'), connectParams: {'test': 'true'}, connectPayload: {'payload': 'test'}, handler: handleFunc, @@ -113,7 +113,7 @@ void main() { final ws = WebSocket( baseUrl: 'baseurl', user: User(id: 'testid'), - logger: Logger('ws'), + _logger: Logger('ws'), connectParams: {'test': 'true'}, connectPayload: {'payload': 'test'}, handler: handleFunc, @@ -153,7 +153,7 @@ void main() { final ws = WebSocket( baseUrl: 'baseurl', user: User(id: 'testid'), - logger: Logger('ws'), + _logger: Logger('ws'), connectParams: {'test': 'true'}, connectPayload: {'payload': 'test'}, handler: handleFunc, @@ -189,7 +189,7 @@ void main() { final ws = WebSocket( baseUrl: 'baseurl', user: User(id: 'testid'), - logger: Logger('ws'), + _logger: Logger('ws'), connectParams: {'test': 'true'}, connectPayload: {'payload': 'test'}, handler: handleFunc, @@ -235,7 +235,7 @@ void main() { final ws = WebSocket( baseUrl: 'baseurl', user: User(id: 'testid'), - logger: Logger('ws'), + _logger: Logger('ws'), connectParams: {'test': 'true'}, connectPayload: {'payload': 'test'}, handler: handleFunc, @@ -280,7 +280,7 @@ void main() { final ws = WebSocket( baseUrl: 'baseurl', user: User(id: 'testid'), - logger: Logger('ws'), + _logger: Logger('ws'), connectParams: {'test': 'true'}, connectPayload: {'payload': 'test'}, handler: handleFunc, @@ -318,7 +318,7 @@ void main() { final ws = WebSocket( baseUrl: 'baseurl', user: User(id: 'testid'), - logger: Logger('ws'), + _logger: Logger('ws'), connectParams: {'test': 'true'}, connectPayload: {'payload': 'test'}, handler: print, diff --git a/packages/stream_chat/test/src/client_test.dart b/packages/stream_chat/test/src/client_test.dart index b2a5bbac..751d7a50 100644 --- a/packages/stream_chat/test/src/client_test.dart +++ b/packages/stream_chat/test/src/client_test.dart @@ -6,13 +6,13 @@ import 'package:dio/dio.dart'; import 'package:dio/native_imp.dart'; import 'package:logging/logging.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat/src/api/requests.dart'; +import 'package:stream_chat/src/core/api/requests.dart'; import 'package:stream_chat/src/client.dart'; import 'package:stream_chat/src/exceptions.dart'; -import 'package:stream_chat/src/models/channel_model.dart'; -import 'package:stream_chat/src/models/filter.dart'; -import 'package:stream_chat/src/models/message.dart'; -import 'package:stream_chat/src/models/user.dart'; +import 'package:stream_chat/src/core/models/channel_model.dart'; +import 'package:stream_chat/src/core/models/filter.dart'; +import 'package:stream_chat/src/core/models/message.dart'; +import 'package:stream_chat/src/core/models/user.dart'; import 'package:test/test.dart'; class MockDio extends Mock implements DioForNative {} @@ -55,8 +55,8 @@ void main() { expect(client.baseURL, 'chat-us-east-1.stream-io-api.com'); expect(client.apiKey, 'api-key'); expect(client.logLevel, Level.WARNING); - expect(client.httpClient.options.connectTimeout, 6000); - expect(client.httpClient.options.receiveTimeout, 6000); + expect(client.httpClient._options.connectTimeout, 6000); + expect(client.httpClient._options.receiveTimeout, 6000); }); test('should create the object correctly', overridePrint(() { @@ -76,8 +76,8 @@ void main() { expect(client.baseURL, 'test.com'); expect(client.apiKey, 'api-key'); expect(Logger.root.level, Level.INFO); - expect(client.httpClient.options.connectTimeout, 10000); - expect(client.httpClient.options.receiveTimeout, 12000); + expect(client.httpClient._options.connectTimeout, 10000); + expect(client.httpClient._options.receiveTimeout, 12000); client.logger.warning('test'); client.logger.config('test config'); @@ -170,7 +170,7 @@ void main() { await client.queryChannelsOnline( filter: queryFilter, sort: sortOptions, - options: options, + _options: options, paginationParams: paginationParams, waitForConnect: false, ); @@ -419,7 +419,7 @@ void main() { await client.queryUsers( filter: queryFilter, sort: sortOptions, - options: options, + _options: options, ); verify(() => diff --git a/packages/stream_chat/test/src/models/filter_test.dart b/packages/stream_chat/test/src/models/filter_test.dart index ac026c6c..663ab702 100644 --- a/packages/stream_chat/test/src/models/filter_test.dart +++ b/packages/stream_chat/test/src/models/filter_test.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'package:test/test.dart'; -import 'package:stream_chat/src/models/filter.dart'; +import 'package:stream_chat/src/core/models/filter.dart'; void main() { group('operators', () { diff --git a/packages/stream_chat/test/version_test.dart b/packages/stream_chat/test/version_test.dart index 34ecc750..8e7112a4 100644 --- a/packages/stream_chat/test/version_test.dart +++ b/packages/stream_chat/test/version_test.dart @@ -1,5 +1,7 @@ +import 'dart:async'; import 'dart:io'; +import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat/version.dart'; import 'package:test/test.dart'; @@ -12,13 +14,38 @@ void prepareTest() { void main() { prepareTest(); - test('stream chat version matches pubspec', () { - final pubspecPath = '${Directory.current.path}/pubspec.yaml'; - final pubspec = File(pubspecPath).readAsStringSync(); - // ignore: unnecessary_string_escapes - final regex = RegExp('version:\s*(.*)'); - final match = regex.firstMatch(pubspec); - expect(match, isNotNull); - expect(PACKAGE_VERSION, match?.group(1)?.trim()); + test('stream chat version matches pubspec', () async { + /// Create a new instance of [StreamChatClient] + /// by passing the apikey obtained from your project dashboard. + final client = StreamChatClient('b67pax5b2wdq', logLevel: Level.INFO); + + /// Set the current user. In a production scenario, this should be done using + /// a backend to generate a user token using our server SDK. + /// Please see the following for more information: + /// https://getstream.io/chat/docs/ios_user_setup_and_tokens/ + await client.connectUser( + User( + id: 'cool-shadow-7', + extraData: { + 'image': + 'https://getstream.io/random_png/?id=cool-shadow-7&name=Cool+shadow', + }, + ), + '''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiY29vbC1zaGFkb3ctNyJ9.gkOlCRb1qgy4joHPaxFwPOdXcGvSPvp6QY0S4mpRkVo''', + ); + + try { + await client.banUser('asdasdas'); + } catch (e) { + print(e); + } + + // final pubspecPath = '${Directory.current.path}/pubspec.yaml'; + // final pubspec = File(pubspecPath).readAsStringSync(); + // // ignore: unnecessary_string_escapes + // final regex = RegExp('version:\s*(.*)'); + // final match = regex.firstMatch(pubspec); + // expect(match, isNotNull); + // expect(PACKAGE_VERSION, match?.group(1)?.trim()); }); } diff --git a/packages/stream_chat_flutter/example/pubspec.yaml b/packages/stream_chat_flutter/example/pubspec.yaml index 3feec144..408bf9e1 100644 --- a/packages/stream_chat_flutter/example/pubspec.yaml +++ b/packages/stream_chat_flutter/example/pubspec.yaml @@ -23,6 +23,10 @@ environment: dependencies: flutter: sdk: flutter +# stream_chat: +# path: ../../stream_chat +# stream_chat_flutter_core: +# path: ../../stream_chat_flutter_core stream_chat_flutter: path: ../ stream_chat_persistence: diff --git a/packages/stream_chat_flutter/lib/src/channel_info.dart b/packages/stream_chat_flutter/lib/src/channel_info.dart index f59ac2e0..05622812 100644 --- a/packages/stream_chat_flutter/lib/src/channel_info.dart +++ b/packages/stream_chat_flutter/lib/src/channel_info.dart @@ -132,10 +132,9 @@ class ChannelInfo extends StatelessWidget { vertical: VisualDensity.minimumDensity, ), ), - onPressed: () async { - await client.disconnect(); - await client.connect(); - }, + onPressed: () => client + ..closeConnection() + ..openConnection(), child: Text( 'Try Again', style: textStyle?.copyWith( diff --git a/packages/stream_chat_flutter/lib/src/channel_list_header.dart b/packages/stream_chat_flutter/lib/src/channel_list_header.dart index 325736f1..1c7befcf 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_header.dart @@ -253,10 +253,9 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { ), ), TextButton( - onPressed: () async { - await client.disconnect(); - await client.connect(); - }, + onPressed: () => client + ..closeConnection() + ..openConnection(), child: Text( 'Try Again', style: chatThemeData.channelListHeaderTheme.title?.copyWith( diff --git a/packages/stream_chat_flutter/lib/src/channel_list_view.dart b/packages/stream_chat_flutter/lib/src/channel_list_view.dart index 766fe4ba..0fa7bca4 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -58,9 +58,15 @@ class ChannelListView extends StatefulWidget { const ChannelListView({ Key? key, this.filter, - this.options, this.sort, - this.pagination, + this.state = true, + this.watch = true, + this.presence = false, + this.memberLimit, + this.messageLimit, + this.pagination = const PaginationParams( + limit: 25, + ), this.onChannelTap, this.onChannelLongPress, this.channelWidget, @@ -88,12 +94,6 @@ class ChannelListView extends StatefulWidget { /// You can also filter other built-in channel fields. final Filter? filter; - /// Query channels options. - /// - /// state: if true returns the Channel state - /// watch: if true listen to changes to this Channel in real time. - final Map? options; - /// The sorting used for the channels matching the filters. /// Sorting is based on field and direction, multiple sorting options /// can be provided. @@ -102,11 +102,22 @@ class ChannelListView extends StatefulWidget { /// Direction can be ascending or descending. final List>? sort; + /// If true returns the Channel state + final bool state; + + /// If true listen to changes to this Channel in real time. + final bool watch; + + final bool presence; + + final int? memberLimit; + final int? messageLimit; + /// Pagination parameters /// limit: the number of channels to return (max is 30) /// offset: the offset (max is 1000) /// message_limit: how many messages should be included to each channel - final PaginationParams? pagination; + final PaginationParams pagination; /// Function called when tapping on a channel /// By default it calls [Navigator.push] building a [MaterialPageRoute] @@ -170,13 +181,14 @@ class _ChannelListViewState extends State { @override Widget build(BuildContext context) { Widget child = ChannelListCore( - pagination: widget.pagination ?? - const PaginationParams( - limit: 25, - ), - options: widget.options, - sort: widget.sort, filter: widget.filter, + sort: widget.sort, + state: widget.state, + watch: widget.watch, + presence: widget.presence, + memberLimit: widget.memberLimit, + messageLimit: widget.messageLimit, + pagination: widget.pagination, channelListController: _channelListController, listBuilder: widget.listBuilder ?? _buildListView, emptyBuilder: widget.emptyBuilder ?? _buildEmptyWidget, diff --git a/packages/stream_chat_flutter/lib/src/user_list_view.dart b/packages/stream_chat_flutter/lib/src/user_list_view.dart index 3b1efd04..aa1886b7 100644 --- a/packages/stream_chat_flutter/lib/src/user_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/user_list_view.dart @@ -47,8 +47,8 @@ class UserListView extends StatefulWidget { const UserListView({ Key? key, this.filter, - this.options, this.sort, + this.presence, this.pagination, this.onUserTap, this.onUserLongPress, @@ -75,12 +75,6 @@ class UserListView extends StatefulWidget { /// You can also filter other built-in channel fields. final Filter? filter; - /// Query channels options. - /// - /// state: if true returns the Channel state - /// watch: if true listen to changes to this Channel in real time. - final Map? options; - /// The sorting used for the channels matching the filters. /// Sorting is based on field and direction, multiple sorting options can /// be provided. @@ -89,6 +83,9 @@ class UserListView extends StatefulWidget { /// Direction can be ascending or descending. final List? sort; + /// + final bool? presence; + /// Pagination parameters /// limit: the number of users to return (max is 30) /// offset: the offset (max is 1000) @@ -177,9 +174,9 @@ class _UserListViewState extends State listBuilder: widget.listBuilder ?? (context, list) => _buildListView(list), pagination: widget.pagination, - options: widget.options, sort: widget.sort, filter: widget.filter, + presence: widget.presence, groupAlphabetically: widget.groupAlphabetically, userListController: _userListController, ); diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index 1b2dbe2f..9d3e7bc2 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -53,6 +53,12 @@ flutter: - animations/ uses-material-design: true +dependency_overrides: + stream_chat: + path: ../stream_chat + stream_chat_flutter_core: + path: ../stream_chat_flutter_core + dev_dependencies: flutter_test: sdk: flutter diff --git a/packages/stream_chat_flutter_core/lib/src/channel_list_core.dart b/packages/stream_chat_flutter_core/lib/src/channel_list_core.dart index a3be273d..684fd630 100644 --- a/packages/stream_chat_flutter_core/lib/src/channel_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/channel_list_core.dart @@ -63,7 +63,11 @@ class ChannelListCore extends StatefulWidget { required this.loadingBuilder, required this.listBuilder, this.filter, - this.options, + this.state = true, + this.watch = true, + this.presence = false, + this.memberLimit, + this.messageLimit, this.sort, this.pagination = const PaginationParams( limit: 25, @@ -94,11 +98,11 @@ class ChannelListCore extends StatefulWidget { /// You can also filter other built-in channel fields. final Filter? filter; - /// Query channels options. - /// - /// state: if true returns the Channel state - /// watch: if true listen to changes to this Channel in real time. - final Map? options; + // bool state = true, + // bool watch = true, + // bool presence = false, + // int? memberLimit, + // int? messageLimit, /// The sorting used for the channels matching the filters. /// Sorting is based on field and direction, multiple sorting options can be @@ -107,6 +111,17 @@ class ChannelListCore extends StatefulWidget { /// _at or member_count. Direction can be ascending or descending. final List>? sort; + /// If true returns the Channel state + final bool state; + + /// If true listen to changes to this Channel in real time. + final bool watch; + + final bool presence; + + final int? memberLimit; + final int? messageLimit; + /// Pagination parameters /// limit: the number of channels to return (max is 30) /// offset: the offset (max is 1000) @@ -149,18 +164,26 @@ class ChannelListCoreState extends State { Future loadData() => _channelsBloc.queryChannels( filter: widget.filter, sortOptions: widget.sort, + state: widget.state, + watch: widget.watch, + presence: widget.presence, + memberLimit: widget.memberLimit, + messageLimit: widget.messageLimit, paginationParams: widget.pagination, - options: widget.options, ); /// Fetches more channels with updated pagination and updates the widget Future paginateData() => _channelsBloc.queryChannels( filter: widget.filter, sortOptions: widget.sort, + state: widget.state, + watch: widget.watch, + presence: widget.presence, + memberLimit: widget.memberLimit, + messageLimit: widget.messageLimit, paginationParams: widget.pagination.copyWith( offset: _channelsBloc.channels?.length ?? 0, ), - options: widget.options, ); StreamSubscription? _subscription; @@ -200,7 +223,11 @@ class ChannelListCoreState extends State { if (widget.filter?.toString() != oldWidget.filter?.toString() || jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) || - widget.options?.toString() != oldWidget.options?.toString() || + widget.state != oldWidget.state || + widget.watch != oldWidget.watch || + widget.presence != oldWidget.presence || + widget.messageLimit != oldWidget.messageLimit || + widget.memberLimit != oldWidget.memberLimit || widget.pagination.toJson().toString() != oldWidget.pagination.toJson().toString()) { loadData(); diff --git a/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart b/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart index d46ee4bd..5e2475b4 100644 --- a/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart +++ b/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart @@ -95,8 +95,13 @@ class ChannelsBlocState extends State Future queryChannels({ Filter? filter, List>? sortOptions, + bool state = true, + bool watch = true, + bool presence = false, + int? memberLimit, + int? messageLimit, + bool waitForConnect = true, PaginationParams paginationParams = const PaginationParams(limit: 30), - Map? options, }) async { final client = _streamChatCoreState!.client; @@ -117,7 +122,12 @@ class ChannelsBlocState extends State await for (final channels in client.queryChannels( filter: filter, sort: sortOptions, - options: options, + state: state, + watch: watch, + presence: presence, + memberLimit: memberLimit, + messageLimit: messageLimit, + waitForConnect: waitForConnect, paginationParams: paginationParams, )) { newChannels = channels; diff --git a/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart b/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart index 89d4853a..0bf78b2e 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart @@ -109,7 +109,7 @@ class StreamChatCoreState extends State if (user != null) { if (state == AppLifecycleState.paused) { if (widget.onBackgroundEventReceived == null) { - client.disconnect(); + client.closeConnection(); return; } _eventSubscription = client.on().listen( @@ -118,7 +118,7 @@ class StreamChatCoreState extends State void onTimerComplete() { _eventSubscription?.cancel(); - client.disconnect(); + client.closeConnection(); } _disconnectTimer = Timer(widget.backgroundKeepAlive, onTimerComplete); @@ -128,7 +128,7 @@ class StreamChatCoreState extends State _disconnectTimer?.cancel(); } else { if (client.wsConnectionStatus == ConnectionStatus.disconnected) { - client.connect(); + client.openConnection(); } } } diff --git a/packages/stream_chat_flutter_core/lib/src/user_list_core.dart b/packages/stream_chat_flutter_core/lib/src/user_list_core.dart index d34c563c..6377686b 100644 --- a/packages/stream_chat_flutter_core/lib/src/user_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/user_list_core.dart @@ -63,8 +63,8 @@ class UserListCore extends StatefulWidget { required this.listBuilder, Key? key, this.filter, - this.options, this.sort, + this.presence, this.pagination, this.groupAlphabetically = false, this.userListController, @@ -92,18 +92,15 @@ class UserListCore extends StatefulWidget { /// You can also filter other built-in channel fields. final Filter? filter; - /// Query channels options. - /// - /// state: if true returns the Channel state - /// watch: if true listen to changes to this Channel in real time. - final Map? options; - /// The sorting used for the channels matching the filters. /// Sorting is based on field and direction, multiple sorting options can be /// provided. You can sort based on last_updated, last_message_at, updated_at, /// created_at or member_count. Direction can be ascending or descending. final List? sort; + /// + final bool? presence; + /// Pagination parameters /// limit: the number of users to return (max is 30) /// offset: the offset (max is 1000) @@ -186,22 +183,22 @@ class UserListCoreState extends State }, ); - // ignore: public_member_api_docs + /// Fetches initial users and updates the widget Future loadData() => _usersBloc!.queryUsers( filter: widget.filter, sort: widget.sort, + presence: widget.presence, pagination: widget.pagination, - options: widget.options, ); - // ignore: public_member_api_docs + /// Fetches more users with updated pagination and updates the widget Future paginateData() => _usersBloc!.queryUsers( filter: widget.filter, sort: widget.sort, + presence: widget.presence, pagination: widget.pagination!.copyWith( offset: _usersBloc!.users?.length ?? 0, ), - options: widget.options, ); @override @@ -209,7 +206,7 @@ class UserListCoreState extends State super.didUpdateWidget(oldWidget); if (widget.filter?.toString() != oldWidget.filter?.toString() || jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) || - widget.options?.toString() != oldWidget.options?.toString() || + widget.presence != oldWidget.presence || widget.pagination?.toJson().toString() != oldWidget.pagination?.toJson().toString()) { loadData(); diff --git a/packages/stream_chat_flutter_core/lib/src/users_bloc.dart b/packages/stream_chat_flutter_core/lib/src/users_bloc.dart index 57edfecb..6a02904d 100644 --- a/packages/stream_chat_flutter_core/lib/src/users_bloc.dart +++ b/packages/stream_chat_flutter_core/lib/src/users_bloc.dart @@ -63,7 +63,7 @@ class UsersBlocState extends State Future queryUsers({ Filter? filter, List? sort, - Map? options, + bool? presence, PaginationParams? pagination, }) async { final client = _streamChatCore.client; @@ -82,7 +82,7 @@ class UsersBlocState extends State final usersResponse = await client.queryUsers( filter: filter, sort: sort, - options: options, + presence: presence, pagination: pagination, ); diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml index c84242b8..f26bb2cf 100644 --- a/packages/stream_chat_flutter_core/pubspec.yaml +++ b/packages/stream_chat_flutter_core/pubspec.yaml @@ -17,6 +17,10 @@ dependencies: rxdart: ^0.26.0 stream_chat: ^2.0.0-nullsafety.1 +dependency_overrides: + stream_chat: + path: ../stream_chat + dev_dependencies: fake_async: ^1.2.0 flutter_test: diff --git a/packages/stream_chat_flutter_core/test/channel_list_core_test.dart b/packages/stream_chat_flutter_core/test/channel_list_core_test.dart index 14cbc232..a69f81c9 100644 --- a/packages/stream_chat_flutter_core/test/channel_list_core_test.dart +++ b/packages/stream_chat_flutter_core/test/channel_list_core_test.dart @@ -142,7 +142,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: pagination, )).thenThrow(error); @@ -162,7 +166,11 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: pagination, )).called(1); }, @@ -191,7 +199,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: pagination, )).thenAnswer((_) => Stream.value(channels)); @@ -211,7 +223,11 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: pagination, )).called(1); }, @@ -240,7 +256,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: pagination, )).thenAnswer((_) => Stream.value(channels)); @@ -260,7 +280,11 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: pagination, )).called(1); }, @@ -297,7 +321,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: pagination, )).thenAnswer((_) => Stream.value(channels)); @@ -321,7 +349,11 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: pagination, )).called(1); @@ -335,7 +367,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: updatedPagination, )).thenAnswer((_) => Stream.value(paginatedChannels)); @@ -355,7 +391,11 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: updatedPagination, )).called(1); }, @@ -396,7 +436,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: pagination, )).thenAnswer((_) => Stream.value(channels)); @@ -424,7 +468,11 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: pagination, )).called(1); @@ -436,7 +484,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: updatedPagination, )).thenAnswer((_) => Stream.value(updatedChannels)); @@ -451,7 +503,11 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: updatedPagination, )).called(1); }, diff --git a/packages/stream_chat_flutter_core/test/channels_bloc_test.dart b/packages/stream_chat_flutter_core/test/channels_bloc_test.dart index ff373878..3c0286a0 100644 --- a/packages/stream_chat_flutter_core/test/channels_bloc_test.dart +++ b/packages/stream_chat_flutter_core/test/channels_bloc_test.dart @@ -114,7 +114,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), )).thenAnswer( (_) => Stream.fromIterable([offlineChannels, onlineChannels]), @@ -133,7 +137,11 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), )).called(1); }, @@ -176,7 +184,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), )).thenThrow(error); @@ -190,7 +202,11 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), )).called(1); }, @@ -228,7 +244,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), )).thenAnswer((_) => Stream.value(channels)); @@ -245,7 +265,11 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), )).called(1); @@ -257,7 +281,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: paginationParams, )).thenAnswer( (_) => Stream.value(newChannels), @@ -279,7 +307,11 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: paginationParams, )).called(1); }, @@ -320,7 +352,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: paginationParams, )).thenAnswer((_) => Stream.value(channels)); @@ -336,7 +372,11 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: paginationParams, )).called(1); @@ -345,7 +385,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: paginationParams, )).thenThrow(error); @@ -359,7 +403,11 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: paginationParams, )).called(1); }, @@ -404,7 +452,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), )).thenAnswer( (_) => Stream.value(channels), @@ -415,7 +467,11 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), )).called(1); @@ -476,7 +532,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), )).thenAnswer( (_) => Stream.value(channels), @@ -487,11 +547,16 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), )).called(1); final channelDeletedOrNotificationRemovedEvent = Event( + type: EventType.channelDeleted, channel: EventChannel( cid: channels.first.cid!, updatedAt: DateTime.now(), @@ -557,7 +622,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), )).thenAnswer( (_) => Stream.value(channels), @@ -568,7 +637,11 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), )).called(1); @@ -648,7 +721,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), )).thenAnswer( (_) => Stream.value(channels), @@ -659,7 +736,11 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), )).called(1); @@ -735,7 +816,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), )).thenAnswer( (_) => Stream.value(channels), @@ -746,7 +831,11 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), )).called(1); @@ -813,7 +902,11 @@ void main() { when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), )).thenAnswer( (_) => Stream.value(channels), @@ -824,7 +917,11 @@ void main() { verify(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), )).called(1); diff --git a/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart b/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart index bc5324b8..94b38238 100644 --- a/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart +++ b/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart @@ -51,7 +51,7 @@ void main() { ); testWidgets( - 'didChangeAppLifecycleState should call client.disconnect() and return ' + 'didChangeAppLifecycleState should call client.closeConnection and return ' 'if onBackgroundEventReceived is null and the widget lifestyle changes to ' 'AppLifecycleState.paused', (tester) async { @@ -69,7 +69,7 @@ void main() { expect(find.byKey(streamChatCoreKey), findsOneWidget); expect(find.byKey(childKey), findsOneWidget); - when(() => mockClient.disconnect()).thenAnswer((_) async { + when(() => mockClient.closeConnection()).thenAnswer((_) async { return; }); @@ -79,7 +79,7 @@ void main() { streamChatCoreState.didChangeAppLifecycleState(AppLifecycleState.paused); - verify(() => mockClient.disconnect()).called(1); + verify(() => mockClient.closeConnection()).called(1); }, ); @@ -107,9 +107,9 @@ void main() { expect(find.byKey(streamChatCoreKey), findsOneWidget); expect(find.byKey(childKey), findsOneWidget); - final event = Event(); + final event = Event(type: EventType.any); when(() => mockClient.on()).thenAnswer((_) => Stream.value(event)); - when(() => mockClient.disconnect()).thenAnswer((_) async { + when(() => mockClient.closeConnection()).thenAnswer((_) async { return; }); @@ -126,7 +126,7 @@ void main() { await Future.delayed(backgroundKeepAlive); - verify(() => mockClient.disconnect()).called(1); + verify(() => mockClient.closeConnection()).called(1); verifyNever(() => mockOnBackgroundEventReceived.call(event)); }); }, @@ -156,7 +156,7 @@ void main() { expect(find.byKey(streamChatCoreKey), findsOneWidget); expect(find.byKey(childKey), findsOneWidget); - final event = Event(); + final event = Event(type: EventType.any); when(() => mockClient.on()).thenAnswer((_) => Stream.value(event)); final streamChatCoreState = tester.state( @@ -198,10 +198,10 @@ void main() { expect(find.byKey(streamChatCoreKey), findsOneWidget); expect(find.byKey(childKey), findsOneWidget); - final event = Event(); + final event = Event(type: EventType.any); when(() => mockClient.on()).thenAnswer((_) => Stream.value(event)); - when(() => mockClient.connect()).thenAnswer((_) async => event); - when(mockClient.disconnect).thenAnswer((_) async => null); + when(() => mockClient.openConnection()).thenAnswer((_) async => event); + when(() => mockClient.closeConnection()).thenAnswer((_) async => null); when(() => mockClient.wsConnectionStatus) .thenReturn(ConnectionStatus.disconnected); @@ -217,7 +217,7 @@ void main() { streamChatCoreState .didChangeAppLifecycleState(AppLifecycleState.resumed); - verify(() => mockClient.connect()).called(1); + verify(() => mockClient.openConnection()).called(1); }); }, ); diff --git a/packages/stream_chat_flutter_core/test/user_list_core_test.dart b/packages/stream_chat_flutter_core/test/user_list_core_test.dart index 820e479f..6f21123f 100644 --- a/packages/stream_chat_flutter_core/test/user_list_core_test.dart +++ b/packages/stream_chat_flutter_core/test/user_list_core_test.dart @@ -134,7 +134,7 @@ void main() { when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).thenThrow(error); @@ -154,7 +154,7 @@ void main() { verify(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).called(1); }, @@ -179,7 +179,7 @@ void main() { when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).thenAnswer((_) async => QueryUsersResponse()..users = users); @@ -199,7 +199,7 @@ void main() { verify(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).called(1); }, @@ -224,7 +224,7 @@ void main() { when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).thenAnswer((_) async => QueryUsersResponse()..users = users); @@ -244,7 +244,7 @@ void main() { verify(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).called(1); }, @@ -283,7 +283,7 @@ void main() { when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).thenAnswer((_) async => QueryUsersResponse()..users = users); @@ -310,7 +310,7 @@ void main() { verify(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).called(1); }, @@ -352,7 +352,7 @@ void main() { when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).thenAnswer((_) async => QueryUsersResponse()..users = users); @@ -379,7 +379,7 @@ void main() { verify(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).called(1); @@ -393,7 +393,7 @@ void main() { when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: updatedPagination, )) .thenAnswer( @@ -411,7 +411,7 @@ void main() { verify(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: updatedPagination, )).called(1); }, @@ -457,7 +457,7 @@ void main() { when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).thenAnswer((_) async => QueryUsersResponse()..users = users); @@ -488,7 +488,7 @@ void main() { verify(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).called(1); @@ -500,7 +500,7 @@ void main() { when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: updatedPagination, )) .thenAnswer((_) async => QueryUsersResponse()..users = updatedUsers); @@ -515,7 +515,7 @@ void main() { verify(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: updatedPagination, )).called(1); }, diff --git a/packages/stream_chat_flutter_core/test/users_bloc_test.dart b/packages/stream_chat_flutter_core/test/users_bloc_test.dart index 2722d08a..b659ca34 100644 --- a/packages/stream_chat_flutter_core/test/users_bloc_test.dart +++ b/packages/stream_chat_flutter_core/test/users_bloc_test.dart @@ -72,7 +72,7 @@ void main() { when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).thenAnswer((_) async => QueryUsersResponse()..users = users); @@ -86,7 +86,7 @@ void main() { verify(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).called(1); }, @@ -121,7 +121,7 @@ void main() { when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).thenThrow(error); @@ -135,7 +135,7 @@ void main() { verify(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).called(1); }, @@ -171,7 +171,7 @@ void main() { when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).thenAnswer((_) async => QueryUsersResponse()..users = users); @@ -185,7 +185,7 @@ void main() { verify(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).called(1); @@ -196,7 +196,7 @@ void main() { when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: pagination, )) .thenAnswer( @@ -218,7 +218,7 @@ void main() { verify(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: pagination, )).called(1); }, @@ -254,7 +254,7 @@ void main() { when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).thenAnswer((_) async => QueryUsersResponse()..users = users); @@ -268,7 +268,7 @@ void main() { verify(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: any(named: 'pagination'), )).called(1); @@ -280,7 +280,7 @@ void main() { when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: pagination, )).thenThrow(error); @@ -294,7 +294,7 @@ void main() { verify(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), - options: any(named: 'options'), + presence: any(named: 'presence'), pagination: pagination, )).called(1); }, diff --git a/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.dart b/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.dart index 4a169cd2..0cef23b0 100644 --- a/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.dart @@ -29,8 +29,9 @@ class ConnectionEventDao extends DatabaseAccessor return into(connectionEvents).insert( ConnectionEventEntity( id: 1, + type: event.type, lastSyncAt: connectionInfo?.lastSyncAt, - lastEventAt: event.createdAt ?? connectionInfo?.lastEventAt, + lastEventAt: event.createdAt, totalUnreadCount: event.totalUnreadCount ?? connectionInfo?.totalUnreadCount, ownUser: event.me?.toJson() ?? connectionInfo?.ownUser, diff --git a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.g.dart b/packages/stream_chat_persistence/lib/src/db/moor_chat_database.g.dart index b8d404b2..09156642 100644 --- a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.g.dart +++ b/packages/stream_chat_persistence/lib/src/db/moor_chat_database.g.dart @@ -60,31 +60,30 @@ class ChannelEntity extends DataClass implements Insertable { Map data, GeneratedDatabase db, {String? prefix}) { final effectivePrefix = prefix ?? ''; - final stringType = db.typeSystem.forDartType(); - final boolType = db.typeSystem.forDartType(); - final dateTimeType = db.typeSystem.forDartType(); - final intType = db.typeSystem.forDartType(); return ChannelEntity( - id: stringType.mapFromDatabaseResponse(data['${effectivePrefix}id'])!, - type: stringType.mapFromDatabaseResponse(data['${effectivePrefix}type'])!, - cid: stringType.mapFromDatabaseResponse(data['${effectivePrefix}cid'])!, - config: $ChannelsTable.$converter0.mapToDart(stringType + id: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}id'])!, + type: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}type'])!, + cid: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}cid'])!, + config: $ChannelsTable.$converter0.mapToDart(const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}config']))!, - frozen: - boolType.mapFromDatabaseResponse(data['${effectivePrefix}frozen'])!, - lastMessageAt: dateTimeType + frozen: const BoolType() + .mapFromDatabaseResponse(data['${effectivePrefix}frozen'])!, + lastMessageAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}last_message_at']), - createdAt: dateTimeType + createdAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}created_at'])!, - updatedAt: dateTimeType + updatedAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}updated_at'])!, - deletedAt: dateTimeType + deletedAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}deleted_at']), - memberCount: intType + memberCount: const IntType() .mapFromDatabaseResponse(data['${effectivePrefix}member_count'])!, - createdById: stringType + createdById: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}created_by_id']), - extraData: $ChannelsTable.$converter1.mapToDart(stringType + extraData: $ChannelsTable.$converter1.mapToDart(const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}extra_data'])), ); } @@ -226,7 +225,7 @@ class ChannelEntity extends DataClass implements Insertable { $mrjc(createdById.hashCode, extraData.hashCode)))))))))))); @override - bool operator ==(dynamic other) => + bool operator ==(Object other) => identical(this, other) || (other is ChannelEntity && other.id == this.id && @@ -742,56 +741,54 @@ class MessageEntity extends DataClass implements Insertable { Map data, GeneratedDatabase db, {String? prefix}) { final effectivePrefix = prefix ?? ''; - final stringType = db.typeSystem.forDartType(); - final intType = db.typeSystem.forDartType(); - final boolType = db.typeSystem.forDartType(); - final dateTimeType = db.typeSystem.forDartType(); return MessageEntity( - id: stringType.mapFromDatabaseResponse(data['${effectivePrefix}id'])!, - messageText: stringType + id: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}id'])!, + messageText: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}message_text']), - attachments: $MessagesTable.$converter0.mapToDart(stringType + attachments: $MessagesTable.$converter0.mapToDart(const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}attachments']))!, - status: $MessagesTable.$converter1.mapToDart( - intType.mapFromDatabaseResponse(data['${effectivePrefix}status']))!, - type: stringType.mapFromDatabaseResponse(data['${effectivePrefix}type'])!, - mentionedUsers: $MessagesTable.$converter2.mapToDart(stringType + status: $MessagesTable.$converter1.mapToDart(const IntType() + .mapFromDatabaseResponse(data['${effectivePrefix}status']))!, + type: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}type'])!, + mentionedUsers: $MessagesTable.$converter2.mapToDart(const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}mentioned_users']))!, - reactionCounts: $MessagesTable.$converter3.mapToDart(stringType + reactionCounts: $MessagesTable.$converter3.mapToDart(const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}reaction_counts'])), - reactionScores: $MessagesTable.$converter4.mapToDart(stringType + reactionScores: $MessagesTable.$converter4.mapToDart(const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}reaction_scores'])), - parentId: stringType + parentId: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}parent_id']), - quotedMessageId: stringType + quotedMessageId: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}quoted_message_id']), - replyCount: intType + replyCount: const IntType() .mapFromDatabaseResponse(data['${effectivePrefix}reply_count']), - showInChannel: boolType + showInChannel: const BoolType() .mapFromDatabaseResponse(data['${effectivePrefix}show_in_channel']), - shadowed: - boolType.mapFromDatabaseResponse(data['${effectivePrefix}shadowed'])!, - command: - stringType.mapFromDatabaseResponse(data['${effectivePrefix}command']), - createdAt: dateTimeType + shadowed: const BoolType() + .mapFromDatabaseResponse(data['${effectivePrefix}shadowed'])!, + command: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}command']), + createdAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}created_at'])!, - updatedAt: dateTimeType + updatedAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}updated_at'])!, - deletedAt: dateTimeType + deletedAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}deleted_at']), - userId: - stringType.mapFromDatabaseResponse(data['${effectivePrefix}user_id']), - pinned: - boolType.mapFromDatabaseResponse(data['${effectivePrefix}pinned'])!, - pinnedAt: dateTimeType + userId: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}user_id']), + pinned: const BoolType() + .mapFromDatabaseResponse(data['${effectivePrefix}pinned'])!, + pinnedAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}pinned_at']), - pinExpires: dateTimeType + pinExpires: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}pin_expires']), - pinnedByUserId: stringType + pinnedByUserId: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}pinned_by_user_id']), - channelCid: stringType + channelCid: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}channel_cid']), - extraData: $MessagesTable.$converter5.mapToDart(stringType + extraData: $MessagesTable.$converter5.mapToDart(const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}extra_data'])), ); } @@ -1066,7 +1063,7 @@ class MessageEntity extends DataClass implements Insertable { pinned.hashCode, $mrjc(pinnedAt.hashCode, $mrjc(pinExpires.hashCode, $mrjc(pinnedByUserId.hashCode, $mrjc(channelCid.hashCode, extraData.hashCode)))))))))))))))))))))))); @override - bool operator ==(dynamic other) => + bool operator ==(Object other) => identical(this, other) || (other is MessageEntity && other.id == this.id && @@ -1923,56 +1920,57 @@ class PinnedMessageEntity extends DataClass Map data, GeneratedDatabase db, {String? prefix}) { final effectivePrefix = prefix ?? ''; - final stringType = db.typeSystem.forDartType(); - final intType = db.typeSystem.forDartType(); - final boolType = db.typeSystem.forDartType(); - final dateTimeType = db.typeSystem.forDartType(); return PinnedMessageEntity( - id: stringType.mapFromDatabaseResponse(data['${effectivePrefix}id'])!, - messageText: stringType + id: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}id'])!, + messageText: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}message_text']), - attachments: $PinnedMessagesTable.$converter0.mapToDart(stringType + attachments: $PinnedMessagesTable.$converter0.mapToDart(const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}attachments']))!, - status: $PinnedMessagesTable.$converter1.mapToDart( - intType.mapFromDatabaseResponse(data['${effectivePrefix}status']))!, - type: stringType.mapFromDatabaseResponse(data['${effectivePrefix}type'])!, - mentionedUsers: $PinnedMessagesTable.$converter2.mapToDart(stringType - .mapFromDatabaseResponse(data['${effectivePrefix}mentioned_users']))!, - reactionCounts: $PinnedMessagesTable.$converter3.mapToDart(stringType - .mapFromDatabaseResponse(data['${effectivePrefix}reaction_counts'])), - reactionScores: $PinnedMessagesTable.$converter4.mapToDart(stringType - .mapFromDatabaseResponse(data['${effectivePrefix}reaction_scores'])), - parentId: stringType + status: $PinnedMessagesTable.$converter1.mapToDart(const IntType() + .mapFromDatabaseResponse(data['${effectivePrefix}status']))!, + type: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}type'])!, + mentionedUsers: $PinnedMessagesTable.$converter2.mapToDart( + const StringType().mapFromDatabaseResponse( + data['${effectivePrefix}mentioned_users']))!, + reactionCounts: $PinnedMessagesTable.$converter3.mapToDart( + const StringType().mapFromDatabaseResponse( + data['${effectivePrefix}reaction_counts'])), + reactionScores: $PinnedMessagesTable.$converter4.mapToDart( + const StringType().mapFromDatabaseResponse( + data['${effectivePrefix}reaction_scores'])), + parentId: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}parent_id']), - quotedMessageId: stringType + quotedMessageId: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}quoted_message_id']), - replyCount: intType + replyCount: const IntType() .mapFromDatabaseResponse(data['${effectivePrefix}reply_count']), - showInChannel: boolType + showInChannel: const BoolType() .mapFromDatabaseResponse(data['${effectivePrefix}show_in_channel']), - shadowed: - boolType.mapFromDatabaseResponse(data['${effectivePrefix}shadowed'])!, - command: - stringType.mapFromDatabaseResponse(data['${effectivePrefix}command']), - createdAt: dateTimeType + shadowed: const BoolType() + .mapFromDatabaseResponse(data['${effectivePrefix}shadowed'])!, + command: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}command']), + createdAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}created_at'])!, - updatedAt: dateTimeType + updatedAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}updated_at'])!, - deletedAt: dateTimeType + deletedAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}deleted_at']), - userId: - stringType.mapFromDatabaseResponse(data['${effectivePrefix}user_id']), - pinned: - boolType.mapFromDatabaseResponse(data['${effectivePrefix}pinned'])!, - pinnedAt: dateTimeType + userId: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}user_id']), + pinned: const BoolType() + .mapFromDatabaseResponse(data['${effectivePrefix}pinned'])!, + pinnedAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}pinned_at']), - pinExpires: dateTimeType + pinExpires: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}pin_expires']), - pinnedByUserId: stringType + pinnedByUserId: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}pinned_by_user_id']), - channelCid: stringType + channelCid: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}channel_cid']), - extraData: $PinnedMessagesTable.$converter5.mapToDart(stringType + extraData: $PinnedMessagesTable.$converter5.mapToDart(const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}extra_data'])), ); } @@ -2247,7 +2245,7 @@ class PinnedMessageEntity extends DataClass pinned.hashCode, $mrjc(pinnedAt.hashCode, $mrjc(pinExpires.hashCode, $mrjc(pinnedByUserId.hashCode, $mrjc(channelCid.hashCode, extraData.hashCode)))))))))))))))))))))))); @override - bool operator ==(dynamic other) => + bool operator ==(Object other) => identical(this, other) || (other is PinnedMessageEntity && other.id == this.id && @@ -3031,19 +3029,18 @@ class ReactionEntity extends DataClass implements Insertable { Map data, GeneratedDatabase db, {String? prefix}) { final effectivePrefix = prefix ?? ''; - final stringType = db.typeSystem.forDartType(); - final dateTimeType = db.typeSystem.forDartType(); - final intType = db.typeSystem.forDartType(); return ReactionEntity( - userId: stringType + userId: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}user_id'])!, - messageId: stringType + messageId: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}message_id'])!, - type: stringType.mapFromDatabaseResponse(data['${effectivePrefix}type'])!, - createdAt: dateTimeType + type: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}type'])!, + createdAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}created_at'])!, - score: intType.mapFromDatabaseResponse(data['${effectivePrefix}score'])!, - extraData: $ReactionsTable.$converter0.mapToDart(stringType + score: const IntType() + .mapFromDatabaseResponse(data['${effectivePrefix}score'])!, + extraData: $ReactionsTable.$converter0.mapToDart(const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}extra_data'])), ); } @@ -3125,7 +3122,7 @@ class ReactionEntity extends DataClass implements Insertable { $mrjc(createdAt.hashCode, $mrjc(score.hashCode, extraData.hashCode)))))); @override - bool operator ==(dynamic other) => + bool operator ==(Object other) => identical(this, other) || (other is ReactionEntity && other.userId == this.userId && @@ -3395,23 +3392,22 @@ class UserEntity extends DataClass implements Insertable { factory UserEntity.fromData(Map data, GeneratedDatabase db, {String? prefix}) { final effectivePrefix = prefix ?? ''; - final stringType = db.typeSystem.forDartType(); - final dateTimeType = db.typeSystem.forDartType(); - final boolType = db.typeSystem.forDartType(); return UserEntity( - id: stringType.mapFromDatabaseResponse(data['${effectivePrefix}id'])!, - role: stringType.mapFromDatabaseResponse(data['${effectivePrefix}role']), - createdAt: dateTimeType + id: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}id'])!, + role: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}role']), + createdAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}created_at'])!, - updatedAt: dateTimeType + updatedAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}updated_at'])!, - lastActive: dateTimeType + lastActive: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}last_active']), - online: - boolType.mapFromDatabaseResponse(data['${effectivePrefix}online'])!, - banned: - boolType.mapFromDatabaseResponse(data['${effectivePrefix}banned'])!, - extraData: $UsersTable.$converter0.mapToDart(stringType + online: const BoolType() + .mapFromDatabaseResponse(data['${effectivePrefix}online'])!, + banned: const BoolType() + .mapFromDatabaseResponse(data['${effectivePrefix}banned'])!, + extraData: $UsersTable.$converter0.mapToDart(const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}extra_data']))!, ); } @@ -3513,7 +3509,7 @@ class UserEntity extends DataClass implements Insertable { $mrjc(online.hashCode, $mrjc(banned.hashCode, extraData.hashCode)))))))); @override - bool operator ==(dynamic other) => + bool operator ==(Object other) => identical(this, other) || (other is UserEntity && other.id == this.id && @@ -3841,30 +3837,28 @@ class MemberEntity extends DataClass implements Insertable { factory MemberEntity.fromData(Map data, GeneratedDatabase db, {String? prefix}) { final effectivePrefix = prefix ?? ''; - final stringType = db.typeSystem.forDartType(); - final dateTimeType = db.typeSystem.forDartType(); - final boolType = db.typeSystem.forDartType(); return MemberEntity( - userId: stringType + userId: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}user_id'])!, - channelCid: stringType + channelCid: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}channel_cid'])!, - role: stringType.mapFromDatabaseResponse(data['${effectivePrefix}role']), - inviteAcceptedAt: dateTimeType.mapFromDatabaseResponse( + role: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}role']), + inviteAcceptedAt: const DateTimeType().mapFromDatabaseResponse( data['${effectivePrefix}invite_accepted_at']), - inviteRejectedAt: dateTimeType.mapFromDatabaseResponse( + inviteRejectedAt: const DateTimeType().mapFromDatabaseResponse( data['${effectivePrefix}invite_rejected_at']), - invited: - boolType.mapFromDatabaseResponse(data['${effectivePrefix}invited'])!, - banned: - boolType.mapFromDatabaseResponse(data['${effectivePrefix}banned'])!, - shadowBanned: boolType + invited: const BoolType() + .mapFromDatabaseResponse(data['${effectivePrefix}invited'])!, + banned: const BoolType() + .mapFromDatabaseResponse(data['${effectivePrefix}banned'])!, + shadowBanned: const BoolType() .mapFromDatabaseResponse(data['${effectivePrefix}shadow_banned'])!, - isModerator: boolType + isModerator: const BoolType() .mapFromDatabaseResponse(data['${effectivePrefix}is_moderator'])!, - createdAt: dateTimeType + createdAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}created_at'])!, - updatedAt: dateTimeType + updatedAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}updated_at'])!, ); } @@ -3997,7 +3991,7 @@ class MemberEntity extends DataClass implements Insertable { $mrjc(createdAt.hashCode, updatedAt.hashCode))))))))))); @override - bool operator ==(dynamic other) => + bool operator ==(Object other) => identical(this, other) || (other is MemberEntity && other.userId == this.userId && @@ -4396,17 +4390,14 @@ class ReadEntity extends DataClass implements Insertable { factory ReadEntity.fromData(Map data, GeneratedDatabase db, {String? prefix}) { final effectivePrefix = prefix ?? ''; - final dateTimeType = db.typeSystem.forDartType(); - final stringType = db.typeSystem.forDartType(); - final intType = db.typeSystem.forDartType(); return ReadEntity( - lastRead: dateTimeType + lastRead: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}last_read'])!, - userId: stringType + userId: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}user_id'])!, - channelCid: stringType + channelCid: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}channel_cid'])!, - unreadMessages: intType + unreadMessages: const IntType() .mapFromDatabaseResponse(data['${effectivePrefix}unread_messages'])!, ); } @@ -4469,7 +4460,7 @@ class ReadEntity extends DataClass implements Insertable { $mrjc(userId.hashCode, $mrjc(channelCid.hashCode, unreadMessages.hashCode)))); @override - bool operator ==(dynamic other) => + bool operator ==(Object other) => identical(this, other) || (other is ReadEntity && other.lastRead == this.lastRead && @@ -4666,11 +4657,10 @@ class ChannelQueryEntity extends DataClass Map data, GeneratedDatabase db, {String? prefix}) { final effectivePrefix = prefix ?? ''; - final stringType = db.typeSystem.forDartType(); return ChannelQueryEntity( - queryHash: stringType + queryHash: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}query_hash'])!, - channelCid: stringType + channelCid: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}channel_cid'])!, ); } @@ -4716,7 +4706,7 @@ class ChannelQueryEntity extends DataClass @override int get hashCode => $mrjf($mrjc(queryHash.hashCode, channelCid.hashCode)); @override - bool operator ==(dynamic other) => + bool operator ==(Object other) => identical(this, other) || (other is ChannelQueryEntity && other.queryHash == this.queryHash && @@ -4851,6 +4841,9 @@ class ConnectionEventEntity extends DataClass /// event id final int id; + /// event type + final String type; + /// User object of the current user final Map? ownUser; @@ -4867,6 +4860,7 @@ class ConnectionEventEntity extends DataClass final DateTime? lastSyncAt; ConnectionEventEntity( {required this.id, + required this.type, this.ownUser, this.totalUnreadCount, this.unreadChannels, @@ -4876,20 +4870,20 @@ class ConnectionEventEntity extends DataClass Map data, GeneratedDatabase db, {String? prefix}) { final effectivePrefix = prefix ?? ''; - final intType = db.typeSystem.forDartType(); - final stringType = db.typeSystem.forDartType(); - final dateTimeType = db.typeSystem.forDartType(); return ConnectionEventEntity( - id: intType.mapFromDatabaseResponse(data['${effectivePrefix}id'])!, - ownUser: $ConnectionEventsTable.$converter0.mapToDart(stringType + id: const IntType() + .mapFromDatabaseResponse(data['${effectivePrefix}id'])!, + type: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}type'])!, + ownUser: $ConnectionEventsTable.$converter0.mapToDart(const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}own_user'])), - totalUnreadCount: intType.mapFromDatabaseResponse( + totalUnreadCount: const IntType().mapFromDatabaseResponse( data['${effectivePrefix}total_unread_count']), - unreadChannels: intType + unreadChannels: const IntType() .mapFromDatabaseResponse(data['${effectivePrefix}unread_channels']), - lastEventAt: dateTimeType + lastEventAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}last_event_at']), - lastSyncAt: dateTimeType + lastSyncAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}last_sync_at']), ); } @@ -4897,6 +4891,7 @@ class ConnectionEventEntity extends DataClass Map toColumns(bool nullToAbsent) { final map = {}; map['id'] = Variable(id); + map['type'] = Variable(type); if (!nullToAbsent || ownUser != null) { final converter = $ConnectionEventsTable.$converter0; map['own_user'] = Variable(converter.mapToSql(ownUser)); @@ -4921,6 +4916,7 @@ class ConnectionEventEntity extends DataClass serializer ??= moorRuntimeOptions.defaultSerializer; return ConnectionEventEntity( id: serializer.fromJson(json['id']), + type: serializer.fromJson(json['type']), ownUser: serializer.fromJson?>(json['ownUser']), totalUnreadCount: serializer.fromJson(json['totalUnreadCount']), unreadChannels: serializer.fromJson(json['unreadChannels']), @@ -4933,6 +4929,7 @@ class ConnectionEventEntity extends DataClass serializer ??= moorRuntimeOptions.defaultSerializer; return { 'id': serializer.toJson(id), + 'type': serializer.toJson(type), 'ownUser': serializer.toJson?>(ownUser), 'totalUnreadCount': serializer.toJson(totalUnreadCount), 'unreadChannels': serializer.toJson(unreadChannels), @@ -4943,6 +4940,7 @@ class ConnectionEventEntity extends DataClass ConnectionEventEntity copyWith( {int? id, + String? type, Value?> ownUser = const Value.absent(), Value totalUnreadCount = const Value.absent(), Value unreadChannels = const Value.absent(), @@ -4950,6 +4948,7 @@ class ConnectionEventEntity extends DataClass Value lastSyncAt = const Value.absent()}) => ConnectionEventEntity( id: id ?? this.id, + type: type ?? this.type, ownUser: ownUser.present ? ownUser.value : this.ownUser, totalUnreadCount: totalUnreadCount.present ? totalUnreadCount.value @@ -4963,6 +4962,7 @@ class ConnectionEventEntity extends DataClass String toString() { return (StringBuffer('ConnectionEventEntity(') ..write('id: $id, ') + ..write('type: $type, ') ..write('ownUser: $ownUser, ') ..write('totalUnreadCount: $totalUnreadCount, ') ..write('unreadChannels: $unreadChannels, ') @@ -4976,16 +4976,19 @@ class ConnectionEventEntity extends DataClass int get hashCode => $mrjf($mrjc( id.hashCode, $mrjc( - ownUser.hashCode, + type.hashCode, $mrjc( - totalUnreadCount.hashCode, - $mrjc(unreadChannels.hashCode, - $mrjc(lastEventAt.hashCode, lastSyncAt.hashCode)))))); + ownUser.hashCode, + $mrjc( + totalUnreadCount.hashCode, + $mrjc(unreadChannels.hashCode, + $mrjc(lastEventAt.hashCode, lastSyncAt.hashCode))))))); @override - bool operator ==(dynamic other) => + bool operator ==(Object other) => identical(this, other) || (other is ConnectionEventEntity && other.id == this.id && + other.type == this.type && other.ownUser == this.ownUser && other.totalUnreadCount == this.totalUnreadCount && other.unreadChannels == this.unreadChannels && @@ -4995,6 +4998,7 @@ class ConnectionEventEntity extends DataClass class ConnectionEventsCompanion extends UpdateCompanion { final Value id; + final Value type; final Value?> ownUser; final Value totalUnreadCount; final Value unreadChannels; @@ -5002,6 +5006,7 @@ class ConnectionEventsCompanion extends UpdateCompanion { final Value lastSyncAt; const ConnectionEventsCompanion({ this.id = const Value.absent(), + this.type = const Value.absent(), this.ownUser = const Value.absent(), this.totalUnreadCount = const Value.absent(), this.unreadChannels = const Value.absent(), @@ -5010,14 +5015,16 @@ class ConnectionEventsCompanion extends UpdateCompanion { }); ConnectionEventsCompanion.insert({ this.id = const Value.absent(), + required String type, this.ownUser = const Value.absent(), this.totalUnreadCount = const Value.absent(), this.unreadChannels = const Value.absent(), this.lastEventAt = const Value.absent(), this.lastSyncAt = const Value.absent(), - }); + }) : type = Value(type); static Insertable custom({ Expression? id, + Expression? type, Expression?>? ownUser, Expression? totalUnreadCount, Expression? unreadChannels, @@ -5026,6 +5033,7 @@ class ConnectionEventsCompanion extends UpdateCompanion { }) { return RawValuesInsertable({ if (id != null) 'id': id, + if (type != null) 'type': type, if (ownUser != null) 'own_user': ownUser, if (totalUnreadCount != null) 'total_unread_count': totalUnreadCount, if (unreadChannels != null) 'unread_channels': unreadChannels, @@ -5036,6 +5044,7 @@ class ConnectionEventsCompanion extends UpdateCompanion { ConnectionEventsCompanion copyWith( {Value? id, + Value? type, Value?>? ownUser, Value? totalUnreadCount, Value? unreadChannels, @@ -5043,6 +5052,7 @@ class ConnectionEventsCompanion extends UpdateCompanion { Value? lastSyncAt}) { return ConnectionEventsCompanion( id: id ?? this.id, + type: type ?? this.type, ownUser: ownUser ?? this.ownUser, totalUnreadCount: totalUnreadCount ?? this.totalUnreadCount, unreadChannels: unreadChannels ?? this.unreadChannels, @@ -5057,6 +5067,9 @@ class ConnectionEventsCompanion extends UpdateCompanion { if (id.present) { map['id'] = Variable(id.value); } + if (type.present) { + map['type'] = Variable(type.value); + } if (ownUser.present) { final converter = $ConnectionEventsTable.$converter0; map['own_user'] = Variable(converter.mapToSql(ownUser.value)); @@ -5080,6 +5093,7 @@ class ConnectionEventsCompanion extends UpdateCompanion { String toString() { return (StringBuffer('ConnectionEventsCompanion(') ..write('id: $id, ') + ..write('type: $type, ') ..write('ownUser: $ownUser, ') ..write('totalUnreadCount: $totalUnreadCount, ') ..write('unreadChannels: $unreadChannels, ') @@ -5106,6 +5120,17 @@ class $ConnectionEventsTable extends ConnectionEvents ); } + final VerificationMeta _typeMeta = const VerificationMeta('type'); + @override + late final GeneratedTextColumn type = _constructType(); + GeneratedTextColumn _constructType() { + return GeneratedTextColumn( + 'type', + $tableName, + false, + ); + } + final VerificationMeta _ownUserMeta = const VerificationMeta('ownUser'); @override late final GeneratedTextColumn ownUser = _constructOwnUser(); @@ -5165,8 +5190,15 @@ class $ConnectionEventsTable extends ConnectionEvents } @override - List get $columns => - [id, ownUser, totalUnreadCount, unreadChannels, lastEventAt, lastSyncAt]; + List get $columns => [ + id, + type, + ownUser, + totalUnreadCount, + unreadChannels, + lastEventAt, + lastSyncAt + ]; @override $ConnectionEventsTable get asDslTable => this; @override @@ -5182,6 +5214,12 @@ class $ConnectionEventsTable extends ConnectionEvents if (data.containsKey('id')) { context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); } + if (data.containsKey('type')) { + context.handle( + _typeMeta, type.isAcceptableOrUnknown(data['type']!, _typeMeta)); + } else if (isInserting) { + context.missing(_typeMeta); + } context.handle(_ownUserMeta, const VerificationResult.success()); if (data.containsKey('total_unread_count')) { context.handle( diff --git a/packages/stream_chat_persistence/lib/src/entity/connection_events.dart b/packages/stream_chat_persistence/lib/src/entity/connection_events.dart index cf0819ae..9e91a9cb 100644 --- a/packages/stream_chat_persistence/lib/src/entity/connection_events.dart +++ b/packages/stream_chat_persistence/lib/src/entity/connection_events.dart @@ -8,6 +8,9 @@ class ConnectionEvents extends Table { /// event id IntColumn get id => integer()(); + /// event type + TextColumn get type => text()(); + /// User object of the current user TextColumn get ownUser => text().nullable().map(MapConverter())(); diff --git a/packages/stream_chat_persistence/lib/src/mapper/event_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/event_mapper.dart index c5cc4198..f0350797 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/event_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/event_mapper.dart @@ -5,6 +5,8 @@ import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; extension ConnectionEventX on ConnectionEventEntity { /// Maps a [ConnectionEventEntity] into [Event] Event toEvent() => Event( + type: type, + createdAt: lastEventAt, me: ownUser != null ? OwnUser.fromJson(ownUser!) : null, totalUnreadCount: totalUnreadCount, unreadChannels: unreadChannels, diff --git a/packages/stream_chat_persistence/pubspec.yaml b/packages/stream_chat_persistence/pubspec.yaml index 5d1927c3..a512c69a 100644 --- a/packages/stream_chat_persistence/pubspec.yaml +++ b/packages/stream_chat_persistence/pubspec.yaml @@ -20,6 +20,10 @@ dependencies: sqlite3_flutter_libs: ^0.4.1 stream_chat: ^2.0.0-nullsafety.0 +dependency_overrides: + stream_chat: + path: ../stream_chat + dev_dependencies: build_runner: ^2.0.1 mocktail: ^0.1.1 diff --git a/packages/stream_chat_persistence/test/src/mapper/event_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/event_mapper_test.dart index cb977aa5..ffa8ee62 100644 --- a/packages/stream_chat_persistence/test/src/mapper/event_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/event_mapper_test.dart @@ -3,19 +3,26 @@ import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; import 'package:stream_chat_persistence/src/mapper/event_mapper.dart'; +import '../utils/date_matcher.dart'; + void main() { test('toEvent should map entity into Event', () { + const type = 'dummy.type'; + final now = DateTime.now(); final ownUser = OwnUser(id: 'testUserId'); final entity = ConnectionEventEntity( id: 3, + type: type, ownUser: ownUser.toJson(), totalUnreadCount: 33, unreadChannels: 33, - lastSyncAt: DateTime.now(), - lastEventAt: DateTime.now(), + lastSyncAt: now, + lastEventAt: now, ); final event = entity.toEvent(); expect(event, isA()); + expect(event.type, type); + expect(event.createdAt, isSameDateAs(now)); expect(event.me!.id, ownUser.id); expect(event.totalUnreadCount, entity.totalUnreadCount); expect(event.unreadChannels, entity.unreadChannels); diff --git a/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart b/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart index 8a275201..e771954b 100644 --- a/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart +++ b/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart @@ -79,7 +79,7 @@ void main() { }); test('getConnectionInfo', () async { - const event = Event(type: 'testEvent'); + final event = Event(); when(() => mockDatabase.connectionEventDao.connectionEvent) .thenAnswer((_) async => event); @@ -100,7 +100,7 @@ void main() { }); test('updateConnectionInfo', () async { - const event = Event(type: 'testEvent'); + final event = Event(); when(() => mockDatabase.connectionEventDao.updateConnectionEvent(event)) .thenAnswer((_) async => 1);