diff --git a/CHANGELOG.md b/CHANGELOG.md index 002a8e11..7eb97132 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [3.0.13] - May 17, 2021 +* Fixed file type mis mapping bug +* Added `cancelUploadingFileMessage` in `BaseChannel` +* Added `joinedAt` in `GroupChannel` +* Improved stability + ## [3.0.12] - Apr 25, 2021 * Fixed to apply option to `SendbirdSdk` properly * Fixed `sendFileMessage` progress inconsistency diff --git a/lib/core/channel/base/base_channel_messages.dart b/lib/core/channel/base/base_channel_messages.dart index 7077ac11..fa268cc3 100644 --- a/lib/core/channel/base/base_channel_messages.dart +++ b/lib/core/channel/base/base_channel_messages.dart @@ -150,78 +150,104 @@ extension Messages on BaseChannel { pending.sender = Sender.fromUser(_sdk.state.currentUser, this); final queue = _sdk.getMsgQueue(channelUrl); - queue.enqueue(AsyncSimpleTask(() async { - if (params.uploadFile.hasBinary) { - upload = await _sdk.api - .uploadFile( - channelUrl: channelUrl, - requestId: pending.requestId, - params: params, - progress: progress) - .timeout( - Duration(seconds: _sdk.options.fileTransferTimeout), - onTimeout: () { - logger.e('upload timeout'); - if (onCompleted != null) { - onCompleted( - pending..sendingStatus = MessageSendingStatus.failed, - SBError( - message: 'upload timeout', - code: ErrorCode.fileUploadTimeout, - ), - ); + final task = AsyncSimpleTask( + () async { + if (params.uploadFile.hasBinary) { + try { + upload = await _sdk.api + .uploadFile( + channelUrl: channelUrl, + requestId: pending.requestId, + params: params, + progress: progress) + .timeout( + Duration(seconds: _sdk.options.fileTransferTimeout), + onTimeout: () { + logger.e('upload timeout'); + if (onCompleted != null) { + onCompleted( + pending..sendingStatus = MessageSendingStatus.failed, + SBError( + message: 'upload timeout', + code: ErrorCode.fileUploadTimeout, + ), + ); + } + return; + }, + ); + if (upload == null) { + throw SBError(code: ErrorCode.fileUploadTimeout); } - return; - }, + fileSize = upload.fileSize; + url = upload.url; + } catch (e) { + rethrow; + } + } + + if (fileSize != null) params.uploadFile.fileSize = fileSize; + if (url != null) params.uploadFile.url = url; + + final cmd = Command.buildFileMessage( + channelUrl: channelUrl, + params: params, + requestId: pending.requestId, + requireAuth: upload?.requireAuth, + thumbnails: upload?.thumbnails, ); - if (upload == null) throw SBError(code: ErrorCode.fileUploadTimeout); - fileSize = upload.fileSize; - url = upload.url; - } - - if (fileSize != null) params.uploadFile.fileSize = fileSize; - if (url != null) params.uploadFile.url = url; - - final cmd = Command.buildFileMessage( - channelUrl: channelUrl, - params: params, - requestId: pending.requestId, - requireAuth: upload?.requireAuth, - thumbnails: upload?.thumbnails, - ); - - final msgFromPayload = BaseMessage.msgFromJson( - cmd.payload, - channelType: channelType, - ); + final msgFromPayload = BaseMessage.msgFromJson( + cmd.payload, + channelType: channelType, + ); - if (!_sdk.state.hasActiveUser) { - final error = ConnectionRequiredError(); - msgFromPayload - ..errorCode = error.code - ..sendingStatus = MessageSendingStatus.failed; - if (onCompleted != null) onCompleted(msgFromPayload, error); - return msgFromPayload; - } - - _sdk.cmdManager.sendCommand(cmd).then((cmdResult) { - final msg = BaseMessage.msgFromJson(cmdResult.payload); - if (onCompleted != null) onCompleted(msg, null); - }).catchError((e) { - // pending.errorCode = e?.code ?? ErrorCode.unknownError; - pending - ..errorCode = e.code - ..sendingStatus = MessageSendingStatus.failed; - if (onCompleted != null) onCompleted(pending, e); - }); - })); + if (!_sdk.state.hasActiveUser) { + final error = ConnectionRequiredError(); + msgFromPayload + ..errorCode = error.code + ..sendingStatus = MessageSendingStatus.failed; + if (onCompleted != null) onCompleted(msgFromPayload, error); + return msgFromPayload; + } + + _sdk.cmdManager.sendCommand(cmd).then((cmdResult) { + final msg = BaseMessage.msgFromJson(cmdResult.payload); + if (onCompleted != null) onCompleted(msg, null); + }).catchError((e) { + // pending.errorCode = e?.code ?? ErrorCode.unknownError; + pending + ..errorCode = e.code + ..sendingStatus = MessageSendingStatus.failed; + if (onCompleted != null) onCompleted(pending, e); + }); + }, + onCancel: () { + if (onCompleted != null) onCompleted(pending, OperationCancelError()); + }, + ); + queue.enqueue(task); + _sdk.setUploadTask(pending.requestId, task); _sdk.setMsgQueue(channelUrl, queue); return pending; } + bool cancelUploadingFileMessage(String requestId) { + if (requestId == null || requestId == '') { + throw InvalidParameterError(); + } + final task = _sdk.getUploadTask(requestId); + if (task == null) { + throw NotFoundError(); + } + + final queue = _sdk.getMsgQueue(channelUrl); + _sdk.api.cancelUploadingFile(requestId); + return queue.cancel(task.hashCode); + } + /// Resends failed [FileMessage] on this channel with [message]. /// /// It returns [FileMessage] with [MessageSendingStatus.pending] and diff --git a/lib/core/channel/group/group_channel.dart b/lib/core/channel/group/group_channel.dart index c50a67dd..a2c42aa9 100644 --- a/lib/core/channel/group/group_channel.dart +++ b/lib/core/channel/group/group_channel.dart @@ -122,10 +122,15 @@ class GroupChannel extends BaseChannel { /// User who invited Member inviter; - /// The time stamp when current user got a invitation + /// Timestamp when current user got a invitation /// from other user in the channel + @JsonKey(defaultValue: 0) int invitedAt; + /// Timestamp when current user joined on this channel + @JsonKey(name: 'joined_ts', defaultValue: 0) + int joinedAt; + /// True if this channel is hidden bool isHidden; @@ -166,6 +171,7 @@ class GroupChannel extends BaseChannel { this.members, this.memberCount, this.joinedMemberCount, + this.joinedAt, this.myPushTriggerOption, this.myMemberState, this.myRole, diff --git a/lib/core/channel/group/group_channel_operations.dart b/lib/core/channel/group/group_channel_operations.dart index 823959da..8011d3d5 100644 --- a/lib/core/channel/group/group_channel_operations.dart +++ b/lib/core/channel/group/group_channel_operations.dart @@ -61,6 +61,8 @@ extension GroupChannelOperations on GroupChannel { /// [ChannelEventHandler.onUserLeaved] will be invoked. Future leave() async { await _sdk.api.leaveGroupChannel(channelUrl: channelUrl); + invitedAt = 0; + joinedAt = 0; } /// Resets (clear) any previous messages on this channel. diff --git a/lib/core/message/base_message.dart b/lib/core/message/base_message.dart index 871ee724..640cc4ba 100644 --- a/lib/core/message/base_message.dart +++ b/lib/core/message/base_message.dart @@ -337,9 +337,12 @@ class BaseMessage { reactions, ); - static T msgFromJson(Map json, - {ChannelType channelType}) { - final cmd = json['type'] as String; + static T msgFromJson( + Map json, { + ChannelType channelType, + String type, + }) { + final cmd = type ?? json['type'] as String; T msg; //basemessage backward compatibility - if (json['custom'] != null) json['data'] = json['custom']; diff --git a/lib/core/models/error.dart b/lib/core/models/error.dart index ce5d75c5..35da8ec6 100644 --- a/lib/core/models/error.dart +++ b/lib/core/models/error.dart @@ -114,3 +114,7 @@ class InvalidAccessTokenError extends SBError { } class UnknownError extends SBError {} + +class NotFoundError extends SBError {} + +class OperationCancelError extends SBError {} diff --git a/lib/managers/command_manager.dart b/lib/managers/command_manager.dart index c816a237..175a49dd 100644 --- a/lib/managers/command_manager.dart +++ b/lib/managers/command_manager.dart @@ -59,8 +59,6 @@ class CommandManager with SdkAccessor { Future sendCommand(Command cmd) async { if (appState.currentUser == null) { //NOTE: some test cases execute async socket data - //even after test case was finished - // print('[E] ${this.hashCode}'); logger.e('sendCommand: connection is requred'); throw ConnectionRequiredError(); } @@ -68,10 +66,6 @@ class CommandManager with SdkAccessor { logger.e('sendCommand: command parameter is null'); throw InvalidParameterError(); } - // if (!webSocket.isConnected()) { - // logger.e('sendCommand: Websocket connection is closed'); - // throw WebSocketConnectionClosedError(); - // } try { await ConnectionManager.readyToExecuteWSRequest(); @@ -668,6 +662,7 @@ class CommandManager with SdkAccessor { if (member.isCurrentUser) { channel.myMemberState = MemberState.none; channel.invitedAt = 0; + channel.joinedAt = 0; channel.clearUnreadCount(); if (!channel.isPublic) { channel.removeFromCache(); diff --git a/lib/sdk/internal/sendbird_sdk_internal.dart b/lib/sdk/internal/sendbird_sdk_internal.dart index 66c70ac2..e372445e 100644 --- a/lib/sdk/internal/sendbird_sdk_internal.dart +++ b/lib/sdk/internal/sendbird_sdk_internal.dart @@ -24,7 +24,7 @@ import 'package:sendbird_sdk/utils/async/async_queue.dart'; import 'package:sendbird_sdk/utils/logger.dart'; import 'package:sendbird_sdk/utils/parsers.dart'; -const sdk_version = '3.0.12'; +const sdk_version = '3.0.13'; const platform = 'flutter'; /// Internal implementation for main class. Do not directly access this class. @@ -42,12 +42,14 @@ class SendbirdSdkInternal with WidgetsBindingObserver { Completer _loginCompleter; Options _options; + AsyncQueue _commandQueue = AsyncQueue(); Map _messageQueues = {}; + Map _uploads = {}; Timer _reconnectTimer; ConnectivityResult _connectionResult; StreamSubscription _connectionSub; - AsyncQueue _commandQueue = AsyncQueue(); + Map _extensions = {}; List _extraDatas = [ constants.sbExtraDataPremiumFeatureList, @@ -90,6 +92,9 @@ class SendbirdSdkInternal with WidgetsBindingObserver { _messageQueues[channelUrl] ?? AsyncQueue(); void setMsgQueue(String channelUrl, AsyncQueue queue) => _messageQueues[channelUrl] = queue; + AsyncSimpleTask getUploadTask(String requestId) => _uploads[requestId]; + void setUploadTask(String requestId, AsyncSimpleTask task) => + _uploads[requestId] = task; // socket callbacks @@ -123,22 +128,19 @@ class SendbirdSdkInternal with WidgetsBindingObserver { //NOTE: compute does not gaurantee the order of commands final op = AsyncTask(func: parseCommand, arg: stringCommand); final cmd = await _commandQueue.enqueue(op); - // cmdManager.processCommand(cmd); - runZoned(() async { + + runZonedGuarded(() async { try { _cmdManager.processCommand(cmd); } catch (e) { rethrow; } - }, onError: (e, s) { - //handle error how to toss this..? - //get waiting func and error? + }, (e, trace) { if (_loginCompleter != null) { _loginCompleter?.completeError(e); } else { logger.e('fatal error thrown ${e.toString()}'); } - // throw e; }); } @@ -270,6 +272,13 @@ class SendbirdSdkInternal with WidgetsBindingObserver { _cmdManager = CommandManager(); _streamManager.reset(); + _commandQueue.cleanUp(); + _messageQueues.forEach((key, q) => q.cleanUp()); + _messageQueues = {}; + _uploads.forEach((key, value) => _api.cancelUploadingFile(key)); + _uploads = {}; + _loginCompleter = null; + _api = ApiClient(); _api.initialize(appId: _state.appId); @@ -280,10 +289,6 @@ class SendbirdSdkInternal with WidgetsBindingObserver { WidgetsBinding.instance?.removeObserver(this); _connectionSub?.cancel(); - _commandQueue.cleanUp(); - _messageQueues = {}; - _loginCompleter = null; - ConnectionManager.flushCompleters(error: ConnectionClosedError()); } diff --git a/lib/services/network/api_client.dart b/lib/services/network/api_client.dart index d8b41a22..33e51019 100644 --- a/lib/services/network/api_client.dart +++ b/lib/services/network/api_client.dart @@ -721,19 +721,23 @@ class ApiClient { body['thumbnail${index + 1}'] = '${value.width.round()},${value.height.round()}'); - final res = await client.multipartRequest( - method: Method.post, - url: url, - body: body, - progress: progress, - ); - return UploadResponse.fromJson(res); + try { + final res = await client.multipartRequest( + method: Method.post, + url: url, + body: body, + progress: progress, + ); + return UploadResponse.fromJson(res); + } catch (_) { + rethrow; + } } // https://github.com/dart-lang/http/issues/424 - // Future cancelUploading { - - // } + bool cancelUploadingFile(String requestId) { + return client.cancelUploadRequest(requestId); + } Future translateUserMessage({ @required ChannelType channelType, diff --git a/lib/services/network/http_client.dart b/lib/services/network/http_client.dart index ff11cb12..7982b37b 100644 --- a/lib/services/network/http_client.dart +++ b/lib/services/network/http_client.dart @@ -33,6 +33,8 @@ class HttpClient { StreamController errorController = StreamController.broadcast(sync: true); + Map uploadRequests = {}; + HttpClient({ this.baseUrl, this.port, @@ -46,6 +48,7 @@ class HttpClient { sessionKey = null; token = null; headers = {}; + uploadRequests = {}; errorController?.close(); } @@ -130,6 +133,7 @@ class HttpClient { path: url, queryParameters: _convertQueryParams(queryParams), ); + final request = http.Request('PUT', uri); request.body = jsonEncode(body); request.headers.addAll(commonHeaders()); @@ -212,11 +216,26 @@ class HttpClient { request.headers.addAll(commonHeaders()); if (headers != null && headers.isNotEmpty) request.headers.addAll(headers); + String reqId = body['request_id']; + uploadRequests[reqId] = request; + final res = await request.send(); final result = await http.Response.fromStream(res); + + uploadRequests.remove(reqId); return _response(result); } + bool cancelUploadRequest(String requestId) { + final req = uploadRequests[requestId]; + if (req != null) { + req.cancel(); + uploadRequests.remove(requestId); + return true; + } + return false; + } + Future _response(http.Response response) async { dynamic res; @@ -265,7 +284,6 @@ class HttpClient { throw UnauthorizeError(message: res['message'], code: res['code']); case 500: default: - logger.e('internal server error ${res['message']}'); throw InternalServerError( message: 'internal server error :${response.statusCode}'); } @@ -301,6 +319,8 @@ class MultipartRequest extends http.MultipartRequest { final void Function(int bytes, int totalBytes) onProgress; + void cancel() => client.close(); + @override Future send() async { try { @@ -349,11 +369,3 @@ class MultipartRequest extends http.MultipartRequest { return http.ByteStream(stream); } } - -class CloseableMultipartRequest extends http.MultipartRequest { - var client = http.Client(); - - CloseableMultipartRequest(String method, Uri uri) : super(method, uri); - - void close() => client.close(); -} diff --git a/lib/utils/async/async_operation.dart b/lib/utils/async/async_operation.dart index bc72d060..f2ae33ed 100644 --- a/lib/utils/async/async_operation.dart +++ b/lib/utils/async/async_operation.dart @@ -1,22 +1,30 @@ -abstract class Operation {} +abstract class Operation { + Function() onCancel; +} class AsyncSimpleTask implements Operation { + @override + Function() onCancel; Future Function() func; - AsyncSimpleTask(this.func); + AsyncSimpleTask(this.func, {this.onCancel}); } class AsyncTask implements Operation { + @override + Function() onCancel; Future Function(T) func; T arg; - AsyncTask({this.func, this.arg}); + AsyncTask({this.func, this.arg, this.onCancel}); } class AsyncTask2 implements Operation { + @override + Function() onCancel; Future Function(T, D) func; T arg; D arg2; - AsyncTask2(this.func, this.arg, this.arg2); + AsyncTask2(this.func, this.arg, this.arg2, {this.onCancel}); } diff --git a/lib/utils/async/async_queue.dart b/lib/utils/async/async_queue.dart index bfe2cf3b..eb487fcc 100644 --- a/lib/utils/async/async_queue.dart +++ b/lib/utils/async/async_queue.dart @@ -8,6 +8,7 @@ class AsyncQueue { Queue _queue = Queue(); Map _completers = {}; + Operation _currentOp; Future enqueue(Operation operation) { _queue.add(operation); @@ -22,6 +23,18 @@ class AsyncQueue { return completer.future; } + bool cancel(int hashCode) { + final completer = _completers.remove(hashCode); + if (completer != null && !completer.isCompleted) { + if (_currentOp?.onCancel != null) { + _currentOp.onCancel(); + } + completer.complete(); + return true; + } + return false; + } + Future _execute() async { while (true) { if (_queue.isEmpty) { @@ -29,18 +42,27 @@ class AsyncQueue { return; } - var first = _queue.removeFirst(); - if (first is AsyncTask) { - final res = await first.func(first.arg); - _completers.remove(first.hashCode)?.complete(res); - } else if (first is AsyncSimpleTask) { - await first.func(); - _completers.remove(first.hashCode)?.complete(); + var task = _queue.removeFirst(); + _currentOp = task; + try { + if (task is AsyncTask) { + final res = await task.func(task.arg); + _completers.remove(task.hashCode)?.complete(res); + } else if (task is AsyncSimpleTask) { + await task.func(); + _completers.remove(task.hashCode)?.complete(); + } + } catch (e) { + // do nothing } } } void cleanUp() { + _queue.forEach((q) { + cancel(q.hashCode); + }); _queue.removeWhere((element) => true); + _currentOp = null; } } diff --git a/lib/utils/isolate/isolate_bridge_master.dart b/lib/utils/isolate/isolate_bridge_master.dart index b95aa615..c0f8f322 100644 --- a/lib/utils/isolate/isolate_bridge_master.dart +++ b/lib/utils/isolate/isolate_bridge_master.dart @@ -149,9 +149,7 @@ class IsolateMaster { throw UnimplementedError(); } - void handleError(String action) async { - //print('Override Handleerror:' + action); - } + void handleError(String action) async {} void didInitialize() async {} diff --git a/lib/utils/isolate/isolate_bridge_slave.dart b/lib/utils/isolate/isolate_bridge_slave.dart index bb190d98..1d634c89 100644 --- a/lib/utils/isolate/isolate_bridge_slave.dart +++ b/lib/utils/isolate/isolate_bridge_slave.dart @@ -47,11 +47,9 @@ class IsolateSlave { } void _processMessage(dynamic action) { - //print('exec receive message ' + action.toString()); try { handleMessage(action); } catch (e) { - //print('Exec Paction Exception:' + e.toString()); setError('Exception Thrown:' + e.toString() + '-' + diff --git a/lib/utils/parsers.dart b/lib/utils/parsers.dart index 00f8b7ee..9473979e 100644 --- a/lib/utils/parsers.dart +++ b/lib/utils/parsers.dart @@ -23,6 +23,5 @@ Future parseMessage(Command data) async { BaseMessage parseMessageFromCommand(Command command) { final payload = command.payload; - payload['type'] = command.cmd; - return BaseMessage.msgFromJson(payload); + return BaseMessage.msgFromJson(payload, type: command.cmd); } diff --git a/pubspec.lock b/pubspec.lock index 7b487487..991bc50a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -35,14 +35,14 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.5.0-nullsafety.1" + version: "2.5.0" boolean_selector: dependency: transitive description: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.1" + version: "2.1.0" build: dependency: transitive description: @@ -105,14 +105,14 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.3" + version: "1.1.0" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-nullsafety.1" + version: "1.2.0" checked_yaml: dependency: transitive description: @@ -133,7 +133,7 @@ packages: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.1" + version: "1.1.0" code_builder: dependency: transitive description: @@ -147,7 +147,7 @@ packages: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0-nullsafety.3" + version: "1.15.0" connectivity: dependency: "direct main" description: @@ -231,7 +231,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-nullsafety.1" + version: "1.2.0" ffi: dependency: transitive description: @@ -323,7 +323,7 @@ packages: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.3-nullsafety.2" + version: "0.6.3" json_annotation: dependency: "direct main" description: @@ -358,14 +358,14 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10-nullsafety.1" + version: "0.12.10" meta: dependency: "direct main" description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0-nullsafety.3" + version: "1.3.0" mime: dependency: "direct main" description: @@ -407,7 +407,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0-nullsafety.1" + version: "1.8.0" path_provider_linux: dependency: transitive description: @@ -435,7 +435,7 @@ packages: name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "1.10.0-nullsafety.2" + version: "1.11.0" platform: dependency: transitive description: @@ -463,7 +463,7 @@ packages: name: pool url: "https://pub.dartlang.org" source: hosted - version: "1.5.0-nullsafety.2" + version: "1.5.0" process: dependency: transitive description: @@ -580,35 +580,35 @@ packages: name: source_map_stack_trace url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.3" + version: "2.1.0" source_maps: dependency: transitive description: name: source_maps url: "https://pub.dartlang.org" source: hosted - version: "0.10.10-nullsafety.2" + version: "0.10.10" source_span: dependency: transitive description: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.0-nullsafety.2" + version: "1.8.0" stack_trace: dependency: "direct dev" description: name: stack_trace url: "https://pub.dartlang.org" source: hosted - version: "1.10.0-nullsafety.1" + version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.1" + version: "2.1.0" stream_transform: dependency: transitive description: @@ -622,35 +622,35 @@ packages: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.1" + version: "1.1.0" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-nullsafety.1" + version: "1.2.0" test: dependency: "direct dev" description: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.16.0-nullsafety.5" + version: "1.16.5" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.19-nullsafety.2" + version: "0.2.19" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.3.12-nullsafety.5" + version: "0.3.15" timing: dependency: transitive description: @@ -664,7 +664,7 @@ packages: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.3.0-nullsafety.3" + version: "1.3.0" uuid: dependency: "direct main" description: @@ -678,7 +678,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.3" + version: "2.1.0" vm_service: dependency: transitive description: @@ -729,5 +729,5 @@ packages: source: hosted version: "2.2.1" sdks: - dart: ">=2.10.0 <2.11.0" - flutter: ">=1.17.0 <2.0.0" + dart: ">=2.12.0-0.0 <3.0.0" + flutter: ">=1.17.0" diff --git a/pubspec.yaml b/pubspec.yaml index 29caf4fd..36cd57ec 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: sendbird_sdk description: The Flutter SDK for Sendbird Chat brings modern messenger chat features to your iOS and Android deployments.. -version: 3.0.12 +version: 3.0.13 homepage: https://www.sendbird.com repository: https://www.github.com/sendbird/sendbird-sdk-flutter documentation: https://sendbird.com/docs/chat/v3/flutter/getting-started/about-chat-sdk