main
hrxiang 3 years ago
parent 57340e52a7
commit 71897bcb4e
  1. 10
      CHANGELOG.md
  2. 2
      android/build.gradle
  3. 8
      android/src/main/java/io/openim/flutter_openim_sdk/manager/ConversationManager.java
  4. 8
      android/src/main/java/io/openim/flutter_openim_sdk/manager/IMManager.java
  5. 31
      android/src/main/java/io/openim/flutter_openim_sdk/manager/MessageManager.java
  6. 9
      example/ios/Runner.xcodeproj/project.pbxproj
  7. 5
      ios/Classes/Module/ConversationManager.swift
  8. 33
      ios/Classes/Module/IMManager.swift
  9. 24
      ios/Classes/Module/MessageManager.swift
  10. 8
      ios/Framework/OpenIMCore.framework/Headers/Open_im_sdk.objc.h
  11. BIN
      ios/Framework/OpenIMCore.framework/OpenIMCore
  12. 1
      lib/flutter_openim_sdk.dart
  13. 1
      lib/src/enum/conversation_type.dart
  14. 2
      lib/src/enum/message_type.dart
  15. 13
      lib/src/manager/im_conversation_manager.dart
  16. 7
      lib/src/manager/im_manager.dart
  17. 53
      lib/src/manager/im_message_manager.dart
  18. 16
      lib/src/models/message.dart
  19. 81
      lib/src/models/notification_info.dart
  20. 2
      pubspec.yaml

@ -1,3 +1,13 @@
## 2.0.0+5
1.New oa notification </br>
2.New method deleteConversationMsgFromLocalAndSvr </br>
2.New method deleteMessageFromLocalAndSvr </br>
3.New method deleteAllMsgFromLocal </br>
3.New method deleteAllMsgFromLocalAndSvr </br>
3.New method markMessageAsReadByConID </br>
3.New method wakeUp </br>
## 2.0.0+4
1.New dismiss group </br>

@ -41,5 +41,5 @@ android {
}
}
dependencies {
implementation 'io.openim:client-sdk:2.0.22@aar'
implementation 'io.openim:client-sdk:2.0.27@aar'
}

@ -127,4 +127,12 @@ public class ConversationManager extends BaseManager {
value(methodCall, "isPrivate")
);
}
public void deleteConversationMsgFromLocalAndSvr(MethodCall methodCall, MethodChannel.Result result) {
Open_im_sdk.deleteConversationMsgFromLocalAndSvr(
new OnBaseListener(result),
value(methodCall, "operationID"),
value(methodCall, "conversationID")
);
}
}

@ -36,4 +36,12 @@ public class IMManager extends BaseManager {
public void getLoginStatus(MethodCall methodCall, MethodChannel.Result result) {
CommonUtil.runMainThreadReturn(result, Open_im_sdk.getLoginStatus());
}
public void wakeUp(MethodCall methodCall, MethodChannel.Result result) {
Open_im_sdk.wakeUp(
new OnBaseListener(result),
value(methodCall, "operationID")
);
}
}

@ -331,4 +331,35 @@ public class MessageManager extends BaseManager {
jsonValue(methodCall, "filter")
);
}
public void deleteMessageFromLocalAndSvr(MethodCall methodCall, MethodChannel.Result result) {
Open_im_sdk.deleteMessageFromLocalAndSvr(
new OnBaseListener(result),
value(methodCall, "operationID"),
jsonValue(methodCall)
);
}
public void deleteAllMsgFromLocal(MethodCall methodCall, MethodChannel.Result result) {
Open_im_sdk.deleteAllMsgFromLocal(
new OnBaseListener(result),
value(methodCall, "operationID")
);
}
public void deleteAllMsgFromLocalAndSvr(MethodCall methodCall, MethodChannel.Result result) {
Open_im_sdk.deleteAllMsgFromLocalAndSvr(
new OnBaseListener(result),
value(methodCall, "operationID")
);
}
public void markMessageAsReadByConID(MethodCall methodCall, MethodChannel.Result result) {
Open_im_sdk.markMessageAsReadByConID(
new OnBaseListener(result),
value(methodCall, "operationID"),
value(methodCall, "conversationID"),
jsonValue(methodCall, "messageIDList")
);
}
}

@ -302,6 +302,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ARCHS = arm64;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
@ -352,7 +353,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ARCHS = "$(ARCHS_STANDARD)";
ARCHS = arm64;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
@ -375,6 +376,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ARCHS = arm64;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
@ -430,6 +432,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ARCHS = arm64;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
@ -482,7 +485,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ARCHS = "$(ARCHS_STANDARD)";
ARCHS = arm64;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
@ -506,7 +509,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ARCHS = "$(ARCHS_STANDARD)";
ARCHS = arm64;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";

@ -20,6 +20,7 @@ public class ConversationManager: BaseServiceManager {
self["setConversationRecvMessageOpt"] = setConversationRecvMessageOpt
self["getConversationRecvMessageOpt"] = getConversationRecvMessageOpt
self["setOneConversationPrivateChat"] = setOneConversationPrivateChat
self["deleteConversationMsgFromLocalAndSvr"] = deleteConversationMsgFromLocalAndSvr
}
func setConversationListener(methodCall: FlutterMethodCall, result: @escaping FlutterResult){
@ -83,6 +84,10 @@ public class ConversationManager: BaseServiceManager {
func setOneConversationPrivateChat(methodCall: FlutterMethodCall, result: @escaping FlutterResult){
Open_im_sdkSetOneConversationPrivateChat(BaseCallback(result: result), methodCall[string: "operationID"], methodCall[string: "conversationID"],methodCall[bool: "isPrivate"])
}
func deleteConversationMsgFromLocalAndSvr(methodCall: FlutterMethodCall, result: @escaping FlutterResult){
Open_im_sdkDeleteConversationMsgFromLocalAndSvr(BaseCallback(result: result), methodCall[string: "operationID"], methodCall[string: "conversationID"])
}
}

@ -9,12 +9,7 @@ public class IMMananger: BaseServiceManager {
self["login"] = login
self["logout"] = logout
self["getLoginStatus"] = getLoginStatus
// self["getLoginUid"] = getLoginUid
// self["getUsersInfo"] = getUsersInfo
// self["setSelfInfo"] = setSelfInfo
// self["forceSyncLoginUerInfo"] = forceSyncLoginUerInfo
// self["forceReConn"] = forceReConn
// self["setSdkLog"] = setSdkLog
self["wakeUp"] = wakeUp
}
func initSDK(methodCall: FlutterMethodCall, result: @escaping FlutterResult){
@ -34,30 +29,10 @@ public class IMMananger: BaseServiceManager {
callBack(result, Open_im_sdkGetLoginStatus())
}
// func getLoginUid(methodCall: FlutterMethodCall, result: @escaping FlutterResult) {
// callBack(result, Open_im_sdkGetLoginUser())
// }
// func getUsersInfo(methodCall: FlutterMethodCall, result: @escaping FlutterResult) {
// Open_im_sdkGetUsersInfo(methodCall[jsonString: "uidList"], BaseCallback(result: result))
// }
// func setSelfInfo(methodCall: FlutterMethodCall, result: @escaping FlutterResult) {
// Open_im_sdkSetSelfInfo(methodCall.toJsonString(), BaseCallback(result: result))
// }
func wakeUp(methodCall: FlutterMethodCall, result: @escaping FlutterResult) {
Open_im_sdkWakeUp(BaseCallback(result: result), methodCall[string: "operationID"])
}
// func forceSyncLoginUerInfo(methodCall: FlutterMethodCall, result: @escaping FlutterResult) {
// Open_im_sdkForceSyncLoginUerInfo()
// callBack(result)
// }
// func setSdkLog(methodCall: FlutterMethodCall, result: @escaping FlutterResult) {
// Open_im_sdkSetSdkLog(methodCall[int32: "sdkLog"])
// }
// func forceReConn(methodCall: FlutterMethodCall, result: @escaping FlutterResult) {
// Open_im_sdkForceReConn()
// callBack(result)
// }
}
public class ConnListener: NSObject, Open_im_sdk_callbackOnConnListenerProtocol {

@ -12,7 +12,6 @@ public class MessageManager: BaseServiceManager {
self["getHistoryMessageList"] = getHistoryMessageList
self["revokeMessage"] = revokeMessage
self["deleteMessageFromLocalStorage"] = deleteMessageFromLocalStorage
self["deleteMessages"] = deleteMessages
self["insertSingleMessageToLocalStorage"] = insertSingleMessageToLocalStorage
self["insertGroupMessageToLocalStorage"] = insertGroupMessageToLocalStorage
self["markC2CMessageAsRead"] = markC2CMessageAsRead
@ -38,6 +37,10 @@ public class MessageManager: BaseServiceManager {
self["clearC2CHistoryMessage"] = clearC2CHistoryMessage
self["clearGroupHistoryMessage"] = clearGroupHistoryMessage
self["searchLocalMessages"] = searchLocalMessages
self["deleteMessageFromLocalAndSvr"] = deleteMessageFromLocalAndSvr
self["deleteAllMsgFromLocal"] = deleteAllMsgFromLocal
self["deleteAllMsgFromLocalAndSvr"] = deleteAllMsgFromLocalAndSvr
self["markMessageAsReadByConID"] = markMessageAsReadByConID
}
func setAdvancedMsgListener(methodCall: FlutterMethodCall, result: @escaping FlutterResult){
@ -65,9 +68,6 @@ public class MessageManager: BaseServiceManager {
Open_im_sdkDeleteMessageFromLocalStorage(BaseCallback(result: result), methodCall[string: "operationID"], methodCall.toJsonString())
}
// deprecated
func deleteMessages(methodCall: FlutterMethodCall, result: FlutterResult){
}
func insertSingleMessageToLocalStorage(methodCall: FlutterMethodCall, result: @escaping FlutterResult){
Open_im_sdkInsertSingleMessageToLocalStorage(BaseCallback(result: result), methodCall[string: "operationID"], methodCall[jsonString: "message"],
@ -182,6 +182,22 @@ public class MessageManager: BaseServiceManager {
Open_im_sdkSearchLocalMessages(BaseCallback(result: result), methodCall[string: "operationID"], methodCall[jsonString: "filter"])
}
func deleteMessageFromLocalAndSvr(methodCall: FlutterMethodCall, result: @escaping FlutterResult) {
Open_im_sdkDeleteMessageFromLocalAndSvr(BaseCallback(result: result), methodCall[string: "operationID"], methodCall.toJsonString())
}
func deleteAllMsgFromLocal(methodCall: FlutterMethodCall, result: @escaping FlutterResult) {
Open_im_sdkDeleteAllMsgFromLocal(BaseCallback(result: result), methodCall[string: "operationID"])
}
func deleteAllMsgFromLocalAndSvr(methodCall: FlutterMethodCall, result: @escaping FlutterResult) {
Open_im_sdkDeleteAllMsgFromLocalAndSvr(BaseCallback(result: result), methodCall[string: "operationID"])
}
func markMessageAsReadByConID(methodCall: FlutterMethodCall, result: @escaping FlutterResult) {
Open_im_sdkMarkMessageAsReadByConID(BaseCallback(result: result), methodCall[string: "operationID"], methodCall[string: "conversationID"], methodCall[jsonString: "messageIDList"])
}
public class SendMsgProgressListener: NSObject, Open_im_sdk_callbackSendMsgCallBackProtocol {
private let channel: FlutterMethodChannel
private let result: FlutterResult

@ -86,6 +86,10 @@ FOUNDATION_EXPORT NSString* _Nonnull Open_im_sdkCreateVideoMessageByURL(NSString
FOUNDATION_EXPORT NSString* _Nonnull Open_im_sdkCreateVideoMessageFromFullPath(NSString* _Nullable operationID, NSString* _Nullable videoFullPath, NSString* _Nullable videoType, int64_t duration, NSString* _Nullable snapshotFullPath);
FOUNDATION_EXPORT void Open_im_sdkDeleteAllMsgFromLocal(id<Open_im_sdk_callbackBase> _Nullable callback, NSString* _Nullable operationID);
FOUNDATION_EXPORT void Open_im_sdkDeleteAllMsgFromLocalAndSvr(id<Open_im_sdk_callbackBase> _Nullable callback, NSString* _Nullable operationID);
FOUNDATION_EXPORT void Open_im_sdkDeleteConversation(id<Open_im_sdk_callbackBase> _Nullable callback, NSString* _Nullable operationID, NSString* _Nullable conversationID);
FOUNDATION_EXPORT void Open_im_sdkDeleteConversationMsgFromLocalAndSvr(id<Open_im_sdk_callbackBase> _Nullable callback, NSString* _Nullable operationID, NSString* _Nullable conversationID);
@ -178,6 +182,8 @@ FOUNDATION_EXPORT void Open_im_sdkMarkGroupMessageAsRead(id<Open_im_sdk_callback
*/
FOUNDATION_EXPORT void Open_im_sdkMarkGroupMessageHasRead(id<Open_im_sdk_callbackBase> _Nullable callback, NSString* _Nullable operationID, NSString* _Nullable groupID);
FOUNDATION_EXPORT void Open_im_sdkMarkMessageAsReadByConID(id<Open_im_sdk_callbackBase> _Nullable callback, NSString* _Nullable operationID, NSString* _Nullable conversationID, NSString* _Nullable msgIDList);
FOUNDATION_EXPORT void Open_im_sdkPinConversation(id<Open_im_sdk_callbackBase> _Nullable callback, NSString* _Nullable operationID, NSString* _Nullable conversationID, BOOL isPinned);
FOUNDATION_EXPORT void Open_im_sdkQuitGroup(id<Open_im_sdk_callbackBase> _Nullable callback, NSString* _Nullable operationID, NSString* _Nullable groupID);
@ -217,6 +223,8 @@ FOUNDATION_EXPORT void Open_im_sdkSetGroupInfo(id<Open_im_sdk_callbackBase> _Nul
*/
FOUNDATION_EXPORT void Open_im_sdkSetGroupListener(id<Open_im_sdk_callbackOnGroupListener> _Nullable callback);
FOUNDATION_EXPORT void Open_im_sdkSetHeartbeatInterval(long heartbeatInterval);
FOUNDATION_EXPORT void Open_im_sdkSetOneConversationPrivateChat(id<Open_im_sdk_callbackBase> _Nullable callback, NSString* _Nullable operationID, NSString* _Nullable conversationID, BOOL isPrivate);
FOUNDATION_EXPORT void Open_im_sdkSetOneConversationRecvMessageOpt(id<Open_im_sdk_callbackBase> _Nullable callback, NSString* _Nullable operationID, NSString* _Nullable conversationID, long opt);

@ -25,6 +25,7 @@ export 'src/manager/im_user_manager.dart';
export 'src/models/conversation_info.dart';
export 'src/models/group_info.dart';
export 'src/models/message.dart';
export 'src/models/notification_info.dart';
export 'src/models/search_info.dart';
export 'src/models/signaling_info.dart';
export 'src/models/user_info.dart';

@ -1,4 +1,5 @@
class ConversationType {
static const single = 1;
static const group = 2;
static const notification = 4;
}

@ -36,6 +36,8 @@ class MessageType {
static const userInfoUpdatedNotification = 1303;
static const userNotificationEnd = 1399;
static const oaNotification = 1400;
static const groupNotificationBegin = 1500;
static const groupCreatedNotification = 1501;
static const groupInfoSetNotification = 1502;

@ -219,6 +219,19 @@ class ConversationManager {
"operationID": Utils.checkOperationID(operationID),
}));
/// Delete conversation from local and service
///
Future<dynamic> deleteConversationMsgFromLocalAndSvr({
required String conversationID,
String? operationID,
}) =>
_channel.invokeMethod(
'deleteConversationMsgFromLocalAndSvr',
_buildParam({
"conversationID": conversationID,
"operationID": Utils.checkOperationID(operationID),
}));
/// Custom sort for conversation list
///
List<ConversationInfo> simpleSort(List<ConversationInfo> list) => list

@ -355,6 +355,13 @@ class IMManager {
///
Future<UserInfo> getLoginUserInfo() async => uInfo;
/// wakeup
Future wakeUp({String? operationID}) => _channel.invokeMethod(
'wakeUp',
_buildParam({
'operationID': Utils.checkOperationID(operationID),
}));
static Map _buildParam(Map param) {
param["ManagerName"] = "imManager";
return param;

@ -56,10 +56,12 @@ class MessageManager {
/// Find all history message
///
/// [userID]id
/// [conversationID] id
/// [groupID]id
Future<List<Message>> getHistoryMessageList({
String? userID,
String? groupID,
String? conversationID,
Message? startMsg,
int? count,
String? operationID,
@ -70,6 +72,7 @@ class MessageManager {
_buildParam({
'userID': userID ?? '',
'groupID': groupID ?? '',
'conversationID': conversationID ?? '',
'startClientMsgID': startMsg?.clientMsgID ?? '',
'count': count ?? 10,
'operationID': Utils.checkOperationID(operationID),
@ -566,6 +569,56 @@ class MessageManager {
.then((value) =>
Utils.toObj(value, (map) => SearchResult.fromJson(map)));
/// Delete message from local and service
///
Future<dynamic> deleteMessageFromLocalAndSvr({
required Message message,
String? operationID,
}) =>
_channel.invokeMethod(
'deleteMessageFromLocalAndSvr',
_buildParam(message.toJson()
..addAll({
"operationID": Utils.checkOperationID(operationID),
})));
/// Delete all message from local
///
Future<dynamic> deleteAllMsgFromLocal({
String? operationID,
}) =>
_channel.invokeMethod(
'deleteAllMsgFromLocal',
_buildParam({
"operationID": Utils.checkOperationID(operationID),
}));
/// Delete all message from service
///
Future<dynamic> deleteAllMsgFromLocalAndSvr({
String? operationID,
}) =>
_channel.invokeMethod(
'deleteAllMsgFromLocalAndSvr',
_buildParam({
"operationID": Utils.checkOperationID(operationID),
}));
/// Mark conversation message as read
///
Future markMessageAsReadByConID({
required String conversationID,
required List<String> messageIDList,
String? operationID,
}) =>
_channel.invokeMethod(
'markMessageAsReadByConID',
_buildParam({
"messageIDList": messageIDList,
"conversationID": conversationID,
"operationID": Utils.checkOperationID(operationID),
}));
static Map _buildParam(Map param) {
param["ManagerName"] = "messageManager";
return param;

@ -19,6 +19,7 @@ class Message {
String? content;
int? seq;
bool? isRead;
int? hasReadTime;
/// [MessageStatus]
int? status;
@ -56,6 +57,7 @@ class Message {
this.content,
this.seq,
this.isRead,
this.hasReadTime,
this.status,
this.offlinePush,
this.attachedInfo,
@ -132,6 +134,7 @@ class Message {
attachedInfoElem = json['attachedInfoElem'] != null
? AttachedInfoElem.fromJson(json['attachedInfoElem'])
: null;
hasReadTime = json['hasReadTime'] ?? attachedInfoElem?.hasReadTime;
}
Map<String, dynamic> toJson() {
@ -151,6 +154,7 @@ class Message {
data['content'] = this.content;
data['seq'] = this.seq;
data['isRead'] = this.isRead;
data['hasReadTime'] = this.hasReadTime;
data['status'] = this.status;
data['offlinePush'] = this.offlinePush?.toJson();
data['attachedInfo'] = this.attachedInfo;
@ -198,6 +202,7 @@ class Message {
content = message.content;
seq = message.seq;
isRead = message.isRead;
hasReadTime = message.hasReadTime;
status = message.status;
offlinePush = message.offlinePush;
attachedInfo = message.attachedInfo;
@ -581,21 +586,30 @@ class FaceElem {
class AttachedInfoElem {
GroupHasReadInfo? groupHasReadInfo;
///
bool? isPrivateChat;
int? hasReadTime;
AttachedInfoElem({this.groupHasReadInfo, this.isPrivateChat});
AttachedInfoElem({
this.groupHasReadInfo,
this.isPrivateChat,
this.hasReadTime,
});
AttachedInfoElem.fromJson(Map<String, dynamic> json) {
groupHasReadInfo = json['groupHasReadInfo'] == null
? null
: GroupHasReadInfo.fromJson(json['groupHasReadInfo']);
isPrivateChat = json['isPrivateChat'];
hasReadTime = json['hasReadTime'];
}
Map<String, dynamic> toJson() {
final data = Map<String, dynamic>();
data['groupHasReadInfo'] = this.groupHasReadInfo?.toJson();
data['isPrivateChat'] = this.isPrivateChat;
data['hasReadTime'] = this.hasReadTime;
return data;
}
}

@ -0,0 +1,81 @@
import '../../flutter_openim_sdk.dart';
/// OA notification
class OANotification {
String? notificationName;
String? notificationFaceURL;
int? notificationType;
String? text;
String? externalUrl;
/// Notification Mix Type
/// 0: Plain text notification
/// 1: Text+picture notification
/// 2: Text+video notification
/// 3: Text+file notification
/// 0 1+ 2+ 3+
int? mixType;
PictureElem? pictureElem;
SoundElem? soundElem;
VideoElem? videoElem;
FileElem? fileElem;
String? ex;
OANotification(
{this.notificationName,
this.notificationFaceURL,
this.notificationType,
this.text,
this.externalUrl,
this.mixType,
this.pictureElem,
this.soundElem,
this.videoElem,
this.fileElem,
this.ex});
OANotification.fromJson(Map<String, dynamic> json) {
notificationName = json['notificationName'];
notificationFaceURL = json['notificationFaceURL'];
notificationType = json['notificationType'];
text = json['text'];
externalUrl = json['externalUrl'];
mixType = json['mixType'];
pictureElem = json['pictureElem'] != null
? PictureElem.fromJson(json['pictureElem'])
: null;
soundElem = json['soundElem'] != null
? SoundElem.fromJson(json['soundElem'])
: null;
videoElem = json['videoElem'] != null
? VideoElem.fromJson(json['videoElem'])
: null;
fileElem =
json['fileElem'] != null ? FileElem.fromJson(json['fileElem']) : null;
ex = json['ex'];
}
Map<String, dynamic> toJson() {
final data = Map<String, dynamic>();
data['notificationName'] = this.notificationName;
data['notificationFaceURL'] = this.notificationFaceURL;
data['notificationType'] = this.notificationType;
data['text'] = this.text;
data['externalUrl'] = this.externalUrl;
data['mixType'] = this.mixType;
if (this.pictureElem != null) {
data['pictureElem'] = this.pictureElem!.toJson();
}
if (this.soundElem != null) {
data['soundElem'] = this.soundElem!.toJson();
}
if (this.videoElem != null) {
data['videoElem'] = this.videoElem!.toJson();
}
if (this.fileElem != null) {
data['fileElem'] = this.fileElem!.toJson();
}
data['ex'] = this.ex;
return data;
}
}

@ -1,6 +1,6 @@
name: flutter_openim_sdk
description: An instant messaging plug-in that supports Android and IOS. And the server is also all open source.
version: 2.0.0+4
version: 2.0.0+5
homepage: https://www.rentsoft.cn
repository: https://github.com/OpenIMSDK/Open-IM-SDK-Flutter

Loading…
Cancel
Save